TibixDev/winboat/main 297k tokens More Tools
```
├── .github/
   ├── FUNDING.yml (200 tokens)
   ├── ISSUE_TEMPLATE/
      ├── 1-bug-report.yml (500 tokens)
      ├── 2-feature-request.yml (300 tokens)
      ├── config.yml
   ├── renovate.json5 (100 tokens)
   ├── workflows/
      ├── release.yml (1800 tokens)
├── .gitignore
├── .npmrc
├── .prettierignore (omitted)
├── .prettierrc
├── CODE_OF_CONDUCT.md (100 tokens)
├── CONTRIBUTING.md (900 tokens)
├── LICENSE (omitted)
├── README.md (1900 tokens)
├── SECURITY.md (200 tokens)
├── build-guest-server.sh (300 tokens)
├── bun.lock (omitted)
├── data/
   ├── usb.ids
├── electron-builder.json (300 tokens)
├── gh_assets/
   ├── features/
      ├── feat_apps.png
      ├── feat_dash.png
      ├── feat_fs.png
      ├── feat_install.png
      ├── feat_misc.png
      ├── feat_native.png
├── guest_server/
   ├── RDPApps.reg (200 tokens)
   ├── argon2.go (400 tokens)
   ├── go.mod (100 tokens)
   ├── go.sum (500 tokens)
   ├── install.bat (200 tokens)
   ├── main.go (2k tokens)
   ├── nssm.exe
   ├── nssm.sha1.txt
   ├── scripts/
      ├── apps.ps1 (5.4k tokens)
      ├── get-icon.ps1 (1200 tokens)
      ├── time-sync.bat
      ├── update.ps1 (1500 tokens)
   ├── securekey.go (900 tokens)
   ├── util.go (100 tokens)
├── icons/
   ├── winboat_logo.svg (1100 tokens)
├── package.json (400 tokens)
├── patches/
   ├── usb@2.17.0.patch (100 tokens)
├── postcss.config.ts
├── scripts/
   ├── build.ts (200 tokens)
   ├── dev-server.ts (700 tokens)
   ├── private/
      ├── tsc.ts (100 tokens)
├── src/
   ├── main/
      ├── main.ts (900 tokens)
      ├── static/
         ├── .gitkeep
      ├── tsconfig.json (100 tokens)
   ├── renderer/
      ├── App.vue (2.8k tokens)
      ├── assets/
         ├── LICENSE.txt (200 tokens)
         ├── podman.svg (1600 tokens)
         ├── vue.svg (100 tokens)
      ├── components/
         ├── ConfigCard.vue (1000 tokens)
         ├── WBContextMenu.vue (900 tokens)
         ├── WBMenuItem.vue (400 tokens)
      ├── data/
         ├── appicons.ts (8.5k tokens)
         ├── docker.ts (300 tokens)
         ├── internalapps.ts
         ├── podman.ts (300 tokens)
      ├── directives/
         ├── autoscroll.ts (400 tokens)
      ├── index.css (200 tokens)
      ├── index.html (100 tokens)
      ├── lib/
         ├── config.ts (1400 tokens)
         ├── constants.ts (400 tokens)
         ├── containers/
            ├── common.ts (400 tokens)
            ├── container.ts (300 tokens)
            ├── docker.ts (1600 tokens)
            ├── podman.ts (1500 tokens)
         ├── exec-helper.ts (200 tokens)
         ├── install.ts (2.6k tokens)
         ├── migrate.ts (600 tokens)
         ├── qmp.ts (1600 tokens)
         ├── specs.ts (600 tokens)
         ├── usbmanager.ts (4.2k tokens)
         ├── winboat.ts (6.2k tokens)
      ├── main.ts (200 tokens)
      ├── public/
         ├── img/
            ├── pfp.svg (100 tokens)
            ├── wallpaper/
               ├── win10.webp
               ├── win11.webp
            ├── winboat_logo.png
            ├── winboat_logo.svg (1100 tokens)
         ├── xel/
            ├── CHANGELOG.md (3.7k tokens)
            ├── LICENSE.md (3.2k tokens)
            ├── README.md (200 tokens)
            ├── icons/
               ├── fluent-outlined.svg (11.6k tokens)
               ├── fluent.svg (9.3k tokens)
               ├── material-outlined.svg (5.6k tokens)
               ├── material.svg (4.7k tokens)
            ├── package.json (100 tokens)
            ├── themes/
               ├── adwaita-dark.css (7.5k tokens)
               ├── adwaita.css (7.5k tokens)
               ├── base.css (5.1k tokens)
               ├── cupertino-dark.css (7.7k tokens)
               ├── cupertino.css (7.8k tokens)
               ├── fluent-dark.css (9.8k tokens)
               ├── fluent.css (9.7k tokens)
               ├── material-dark.css (9.4k tokens)
               ├── material.css (9.2k tokens)
            ├── xel.js (90.8k tokens)
      ├── router.ts (200 tokens)
      ├── tsconfig.json (100 tokens)
      ├── utils/
         ├── bus.ts (100 tokens)
         ├── capitalize.ts
         ├── debounce.ts (100 tokens)
         ├── getFreeRDP.ts (300 tokens)
         ├── icons.ts (100 tokens)
         ├── interval.ts
         ├── log.ts (200 tokens)
         ├── openLink.ts (100 tokens)
         ├── port.ts (2.3k tokens)
      ├── views/
         ├── About.vue (200 tokens)
         ├── Apps.vue (4.4k tokens)
         ├── Blank.vue
         ├── Config.vue (7.6k tokens)
         ├── Home.vue (2.3k tokens)
         ├── Migration.vue (500 tokens)
         ├── SetupUI.vue (12.1k tokens)
      ├── vite-env.d.ts (omitted)
      ├── window.d.ts (omitted)
   ├── types.ts (600 tokens)
├── tailwind.config.ts (100 tokens)
├── vite.config.ts (200 tokens)
```


## /.github/FUNDING.yml

```yml path="/.github/FUNDING.yml" 
# These are supported funding model platforms

github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: winboat
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: # Replace with a single Buy Me a Coffee username
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']

```

## /.github/ISSUE_TEMPLATE/1-bug-report.yml

```yml path="/.github/ISSUE_TEMPLATE/1-bug-report.yml" 
name: 🚨 Bug Report
description: Report an issue that you've encountered
title: "[Bug] "
labels:
    - bug
    - needs-triage
body:
    - type: markdown
      attributes:
          value: |
              Thanks for taking your time reporting an issue.
              Please make sure to look through the [issue tracker](https://github.com/TibixDev/winboat/issues) before submitting an issue as to avoid duplicates.
    - type: input
      attributes:
          label: What version of WinBoat are you running?
      validations:
          required: true
    - type: textarea
      attributes:
          label: Your Environment
          placeholder: |
              Linux Distro used: 
              Desktop Environment used:
              FreeRDP type and version used: (e.g. flatpak 3.17.1)
              Anything else you may find relevant...
      validations:
          required: true
    - type: textarea
      attributes:
          label: Steps to Reproduce / Context
          description: |
              Please provide clear steps to reproduce this issue. 
              If the issue is not completely reproducible, please provide some context about your case.
      validations:
          required: true
    - type: textarea
      attributes:
          label: Logs
          description: |
              Please drag 'n drop your `.log` files from `~/.winboat`, then format them accordingly. 
              WARNING: winboat.log contains your VM's password in plaintext, so if it's sensitive make sure to remove it before uploading."
          placeholder: |
              - [winboat.log](https://github.com/user-attachments/files/.../winboat.log)
              - [install.log](https://github.com/user-attachments/files/.../install.log)
              - [qmp.log](https://github.com/user-attachments/files/.../qmp.log)
              ...
      validations:
          required: true
    - type: textarea
      attributes:
          label: Expected Behavior
          description: Describe what should happen in this case.
      validations:
          required: true
    - type: textarea
      attributes:
          label: Current Behavior
          description: Tell us what happens instead of the Expected Behavior
      validations:
          required: true
    - type: textarea
      attributes:
          label: Possible Solution
          description: Optionally, suggest a fix/reason for the issue.
    - type: checkboxes
      attributes:
          label: Quality Notice
          options:
              - label: I have checked the issue tracker and verified that this bug is a unique case.
                required: true

```

## /.github/ISSUE_TEMPLATE/2-feature-request.yml

```yml path="/.github/ISSUE_TEMPLATE/2-feature-request.yml" 
name: 🧩 Feature Request
description: Suggest a new feature, enhancement, or just share your idea
title: "[Feature] "
labels:
    - enhancement
body:
    - type: markdown
      attributes:
          value: |
              Thank you for taking your time submitting a Feature Request
              Please make sure to look through the [issue tracker](https://github.com/TibixDev/winboat/issues) before submitting a Feature Request as to avoid duplicates.
    - type: textarea
      attributes:
          label: Feature Description
          description: Please take your time in outlining your Feature Request, as it helps others understand it more clearly.
      validations:
          required: true
    - type: textarea
      attributes:
          label: Use-Case
          description: Why do you think this Feature would be useful?
      validations:
          required: true
    - type: textarea
      attributes:
          label: Proposed Implementation
          description: Optionally, you could provide us with a proposal as to how you'd implement this feature.
    - type: checkboxes
      attributes:
          label: Notice
          options:
              - label: I have checked the issue tracker and verified that this Feature Request has not been proposed before.
                required: true
              - label: I would like to help out implementing this feature

```

## /.github/ISSUE_TEMPLATE/config.yml

```yml path="/.github/ISSUE_TEMPLATE/config.yml" 
blank_issues_enabled: false
contact_links:
    - name: Ask for Help
      about: If you have any questions regarding WinBoat, join our Discord
      url: https://discord.gg/MEwmpWm4tN

```

## /.github/renovate.json5

```json5 path="/.github/renovate.json5" 
{
    $schema: "https://docs.renovatebot.com/renovate-schema.json",
    extends: ["config:recommended", ":disableRateLimiting"],
    includePaths: ["**/src/renderer/lib/**"],
    regexManagers: [
        {
            fileMatch: ["src/renderer/lib/install\\.ts{{contextString}}quot;],
            matchStrings: ['"image":\\s*"(?<depName>ghcr\\.io/dockur/windows):(?<currentValue>[^"]+)"'],
            datasourceTemplate: "docker",
            depTypeTemplate: "container",
        },
    ],
}

```

## /.github/workflows/release.yml

```yml path="/.github/workflows/release.yml" 
name: Build WinBoat
permissions:
    contents: write
on:
    push:
        branches:
            - main
        tags:
            - "v*"
    workflow_dispatch:
        inputs:
            build_type:
                description: "Build type"
                required: true
                default: "all"
                type: choice
                options:
                    - all
                    - linux-only
                    - guest-server-only
jobs:
    build:
        runs-on: ubuntu-22.04
        if: >-
            (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/')) ||
            (github.event_name == 'workflow_dispatch' && github.event.inputs.build_type != 'guest-server-only')
        steps:
            - name: Checkout code
              uses: actions/checkout@v6
            - name: Update USB IDs database
              run: |
                  echo "Removing old usb.ids file..."
                  rm -f data/usb.ids
                  echo "Downloading latest usb.ids from linux-usb.org..."
                  curl -o data/usb.ids http://www.linux-usb.org/usb.ids
                  echo "USB IDs database updated successfully"
            - name: Show build type
              run: |
                  if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
                    echo "Manual workflow triggered with build type: ${{ github.event.inputs.build_type }}"
                  else
                    echo "Automatic workflow triggered by ${{ github.event_name }}"
                  fi
            - name: Install system dependencies
              run: |
                  sudo apt-get update
                  sudo apt-get install -y libudev-dev libusb-1.0-0-dev
            - name: Set up Bun
              uses: oven-sh/setup-bun@v2
            - name: Set up Go
              uses: actions/setup-go@v6
              with:
                  go-version: "stable"
                  cache-dependency-path: "guest_server/go.sum"
            - name: Install dependencies
              run: bun ci
            - name: Build guest server and app
              run: bun run build:linux-gs
              env:
                  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                  ELECTRON_BUILDER_COMPRESSION_LEVEL: 5
            - name: Derive artifact naming
              id: meta
              shell: bash
              run: |
                  bun -e "const p=require('./package.json'); console.log('NAME='+p.name)" >> "$GITHUB_OUTPUT"
                  bun -e "const p=require('./package.json'); console.log('VERSION='+p.version)" >> "$GITHUB_OUTPUT"
                  ARCH_FROM_FILE=$(ls dist/*.{AppImage,deb,rpm,tar.bz2} 2>/dev/null | head -n1 | xargs -I{} basename {} | sed -E 's/.*-([A-Za-z0-9_]+)\.[^.]+$/\1/')
                  if [ -n "$ARCH_FROM_FILE" ]; then
                    echo "ARCH=$ARCH_FROM_FILE" >> "$GITHUB_OUTPUT"
                  else
                    BUN_ARCH=$(bun -p "process.arch")
                    case "$BUN_ARCH" in
                      x64)   echo "ARCH=x86_64" >> "$GITHUB_OUTPUT" ;;
                      arm64) echo "ARCH=arm64"  >> "$GITHUB_OUTPUT" ;;
                      arm)   echo "ARCH=armv7l" >> "$GITHUB_OUTPUT" ;;
                      *)     echo "ARCH=$BUN_ARCH" >> "$GITHUB_OUTPUT" ;;
                    esac
                  fi
            - name: Upload AppImage
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}.appimage
                  path: dist/*.AppImage
                  if-no-files-found: ignore
            - name: Upload DEB
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}.deb
                  path: dist/*.deb
                  if-no-files-found: ignore
            - name: Upload RPM
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}.rpm
                  path: dist/*.rpm
                  if-no-files-found: ignore
            - name: Upload TAR.BZ2
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}.tar.bz2
                  path: dist/*.tar.bz2
                  if-no-files-found: ignore
            - name: Upload unpacked directory
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}-unpacked.zip
                  path: dist/linux-unpacked/**
                  if-no-files-found: ignore
            - name: Upload guest server zip
              uses: actions/upload-artifact@v7
              with:
                  name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}-guest_server.zip
                  path: dist/linux-unpacked/resources/guest_server/winboat_guest_server.zip
                  if-no-files-found: ignore
            # - name: Upload metadata
            #   uses: actions/upload-artifact@v7
            #   with:
            #     name: ${{ steps.meta.outputs.NAME }}-${{ steps.meta.outputs.VERSION }}-${{ steps.meta.outputs.ARCH }}-metadata
            #     path: |
            #       dist/latest-linux.yml
            #       dist/linux-unpacked/resources/guest_server/winboat_guest_server.zip
            #     if-no-files-found: ignore
    guest-server-only:
        runs-on: ubuntu-22.04
        if: github.event_name == 'workflow_dispatch' && github.event.inputs.build_type == 'guest-server-only'
        steps:
            - name: Checkout code
              uses: actions/checkout@v6
            - name: Show build type
              run: |
                  echo "Manual workflow triggered with build type: guest-server-only"
            - name: Set up Go
              uses: actions/setup-go@v6
              with:
                  go-version: "stable"
                  cache-dependency-path: "guest_server/go.sum"
            - name: Build guest server only
              run: bash build-guest-server.sh
            - name: Upload guest server artifacts
              uses: actions/upload-artifact@v7
              with:
                  name: guest-server-artifacts
                  path: guest_server/
    release:
        runs-on: ubuntu-22.04
        if: >-
            startsWith(github.ref, 'refs/tags/') ||
            (github.event_name == 'workflow_dispatch' && github.event.inputs.build_type == 'all')
        steps:
            - name: Checkout code
              uses: actions/checkout@v6
            - name: Update USB IDs database
              run: |
                  echo "Removing old usb.ids file..."
                  rm -f data/usb.ids
                  echo "Downloading latest usb.ids from linux-usb.org..."
                  curl -o data/usb.ids http://www.linux-usb.org/usb.ids
                  echo "USB IDs database updated successfully"
            - name: Show build type
              run: |
                  if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
                    echo "Manual workflow triggered with build type: ${{ github.event.inputs.build_type }}"
                  else
                    echo "Release workflow triggered by tag: ${{ github.ref_name }}"
                  fi
            - name: Install system dependencies
              run: |
                  sudo apt-get update
                  sudo apt-get install -y libudev-dev libusb-1.0-0-dev
            - name: Set up Bun
              uses: oven-sh/setup-bun@v2
            - name: Set up Go
              uses: actions/setup-go@v6
              with:
                  go-version: "stable"
                  cache-dependency-path: "guest_server/go.sum"
            - name: Install dependencies
              run: bun ci
            - name: Build guest server and app
              run: bun run build:linux-gs
              env:
                  GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
                  ELECTRON_BUILDER_COMPRESSION_LEVEL: 5
            - name: Zip unpacked variant
              run: cd dist && zip -r winboat-linux-unpacked.zip linux-unpacked/
            - name: Create GitHub Release
              uses: softprops/action-gh-release@v3
              with:
                  files: |
                      dist/*.AppImage
                      dist/*.deb
                      dist/*.rpm
                      dist/*.tar.bz2
                      dist/winboat-linux-unpacked.zip
                      dist/latest-linux.yml
                      dist/linux-unpacked/resources/guest_server/winboat_guest_server.zip
                  draft: false
                  prerelease: false
                  generate_release_notes: true
              env:
                  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

```

## /.gitignore

```gitignore path="/.gitignore" 
node_modules
dist
build

.vscode
.idea

result

package-lock.json
# src/renderer/public/*

# Custom
# *.exe
temp
guest_server/winboat_guest_server.exe
guest_server/winboat_guest_server.zip
```

## /.npmrc

```npmrc path="/.npmrc" 
engine-strict=true
```

## /.prettierrc

```prettierrc path="/.prettierrc" 
{
    "tabWidth": 4,
    "printWidth": 120,
    "arrowParens": "avoid"
}

```

## /CODE_OF_CONDUCT.md

The WinBoat code of conduct is derived from [Ladybird](https://github.com/LadybirdBrowser/ladybird/blob/master/CODE_OF_CONDUCT.md).

- Participants will be tolerant of opposing views.
- Participants must ensure that their language and actions are free of personal attacks and disparaging personal remarks.
- When interpreting the words and actions of others, participants should always assume good intentions.
- Behavior that can be reasonably considered harassment will not be tolerated.


## /CONTRIBUTING.md

# WinBoat Contribution Guidelines

This document outlines the general contribution guidelines that apply to WinBoat.

> [!TIP]
> If you're new to open source, the [Open Source Etiquette](https://developer.mozilla.org/en-US/docs/MDN/Community/Open_source_etiquette) is an excellent read that you should check out before reading the rest of this document.

> [!IMPORTANT]
> Basic familiarity with Git and GitHub is required: If you are also new to these tools, visit [GitHub for complete beginners](https://github.com/git-guides#learning-git-basics) for a comprehensive introduction to them.

## General

1. **Avoid vibe-coding large features** - While probably most of us use AI while coding in our day to day lives, you should try to use it in a clever and targetted manner. Avoid submitting large vibe-coded features, because in 99% of the cases they will not behave as expected. These kinds of PR-s drain our energy and time that could be focused on something more useful, because what took a few seconds for the AI assistant to pump out will take us hours to test, discuss, and ultimately scrap because it doesn't take into account half the things a real programmer probably would. If you made heavy use of AI in your pull request, **indicate it**.

2. **Do not break existing code** - WinBoat aims to provide a seamless user experience, so please avoid the breakage of any existing features to the best of your abilities. Ensure that folks potentially upgrading to a new version that includes your changes won't have any problems, and address any migrations if they're required.

3. **Do rigorous testing** - WinBoat is used by tens of thousands of folks every day on a wide range of Linux distros, make sure that the PR you submit doesn't break compatibility on any of the mainstream distros.

4. **If you're unsure, ask** - If you want to start working on something large that you then intend for us to merge into WinBoat, it may be worth asking us first in an issue to understand if the feature is compatible with the philosophy of WinBoat and whether we want to include it. The same idea applies if you're unsure how to implement a feature in a way that's compatible with how WinBoat operates.

5. **Avoid introducing new programming languages** - WinBoat uses TypeScript & Vue for the Electron app, and Go & PowerShell for the Guest Server. Use these for getting things done.

6. **Avoid introducing new foreign dependencies** - Chances are there's an NPM / Go package for what you're trying to achieve or at least something similar. If there isn't, and it's a must to include it, discuss with us first.

## Branch Management

Before contributing, you should always create a personal fork of WinBoat, where you can create a dedicated branch for the changes you're aiming to make. Once you're finished with your changes, you can go ahead and create a PR aiming to merge your changes to WinBoat's main branch. If you're still in the process of development, but you'd still like to open a PR for visibility or discussion, make sure that you open a draft PR instead.

**Branch naming convention:** Use descriptive names like `feat/your-feature-name` or `fix/issue-description`

## Pull Requests & Commits

Your PR title should include what it's trying to do, e.g. `feat: Add VNC port into Configuration`, while the description should outline it in more detail, potentially explaining how the feature works, details on the implementation, and/or why the feature is useful.

Make sure you reference any issues that are potentially linked to your Pull Request using GitHub's [keywords](https://docs.github.com/en/issues/tracking-your-work-with-issues/using-issues/linking-a-pull-request-to-an-issue), either via comments or commit messages.

WinBoat generally uses the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) naming scheme. You should aim to stick to these conventions whenever possible.

## Review & Merging Process

After you've submitted your changes as a PR, a maintainer may request changes to your code before merging. If everything goes well, your PR will be merged and shipped in the next version of WinBoat.

In case we find issues later on, the PR commits might be reverted and additional discussion will take place in the PR thread or a separate issue.


## /README.md

<div align="left">
  <table>
    <tr>
      <td>
        <img src="icons/winboat_logo.svg" alt="WinBoat Logo" width="150">
      </td>
      <td>
        <h1 style="color: #7C86FF; margin: 0; font-size: 32px;">WinBoat</h1>
        <p style="color: oklch(90% 0 0); font-size: 14px; margin: 5px 0;">Windows for Penguins.<br>
        Run Windows apps on 🐧 Linux with ✨ seamless integration</p>
      </td>
    </tr>
  </table>
</div>

## Screenshots

<div align="center">
  <img src="gh_assets/features/feat_dash.png" alt="WinBoat Dashboard" width="45%">
  <img src="gh_assets/features/feat_apps.png" alt="WinBoat Apps" width="45%">
  <img src="gh_assets/features/feat_native.png" alt="Native Windows" width="45%">
</div>

## ⚠️ Work in Progress ⚠️

WinBoat is currently in beta, so expect to occasionally run into hiccups and bugs. You should be comfortable with some level of troubleshooting if you decide to try it, however we encourage you to give it a shot anyway.

## Features

- **🎨 Elegant Interface**: Sleek and intuitive interface that seamlessly integrates Windows into your Linux desktop environment, making it feel like a native experience
- **📦 Automated Installs**: Simple installation process through our interface - pick your preferences & specs and let us handle the rest
- **🚀 Run Any App**: If it runs on Windows, it can run on WinBoat. Enjoy the full range of Windows applications as native OS-level windows in your Linux environment
- **🖥️ Full Windows Desktop**: Access the complete Windows desktop experience when you need it, or run individual apps seamlessly integrated into your Linux workflow
- **📁 Filesystem Integration**: Your home directory is mounted in Windows, allowing easy file sharing between the two systems without any hassle
- **✨ And many more**: Smartcard passthrough, resource monitoring, and more features being added regularly

## How Does It Work?

WinBoat is an Electron app which allows you to run Windows apps on Linux using a containerized approach. Windows runs as a VM inside a Docker/Podman container, we communicate with it using the [WinBoat Guest Server](https://github.com/TibixDev/winboat/tree/main/guest_server) to retrieve data we need from Windows. For compositing applications as native OS-level windows, we use FreeRDP together with Windows's RemoteApp protocol.

## Prerequisites

Before running WinBoat, ensure your system meets the following requirements:

- **RAM**: At least 4 GB of RAM
- **CPU**: At least 2 CPU threads
- **Storage**: At least 32 GB free space on the drive your selected install folder corresponds to
- **Virtualization**: KVM enabled in BIOS/UEFI
    - [How to enable virtualization](https://duckduckgo.com/?t=h_&q=how+to+enable+virtualization+in+%3Cmotherboard+brand%3E+bios&ia=web)
- **In case of Docker:**
  - **Docker**: Required for containerization
      - [Installation Guide](https://docs.docker.com/engine/install/)
      - **⚠️ NOTE:** Docker Desktop is **not** supported, you will run into issues if you use it
  - **Docker Compose v2**: Required for compatibility with docker-compose.yml files
      - [Installation Guide](https://docs.docker.com/compose/install/#plugin-linux-only)
  - **Docker User Group**: Add your user to the `docker` group
      - [Setup Instructions](https://docs.docker.com/engine/install/linux-postinstall/#manage-docker-as-a-non-root-user)
- **In case of Podman:**
  - **Podman**: Required for containerization
      - [Installation Guide](https://podman.io/docs/installation#installing-on-linux)
      - On Debian/Ubuntu and forks, the Podman version installed with `apt install` could be too old. Make sure you have **Version 4.x.x** or higher to ensure the installation completes successfully.
  - **Podman Compose**: Required for compatibility with podman-compose.yml files
      - [Installation Guide](https://github.com/containers/podman-compose?tab=readme-ov-file#installation)
- **FreeRDP**: Required for remote desktop connection (Please make sure you have **Version 3.x.x** with sound support included)
    - [Installation Guide](https://github.com/FreeRDP/FreeRDP/wiki/PreBuilds)
- [OPTIONAL] **Kernel Modules**: The `iptables` / `nftables` and `iptable_nat` kernel modules can be loaded for network autodiscovery and better shared filesystem performance, but this is not obligatory in newer versions of WinBoat
    - [Module loading instructions](https://rentry.org/rmfq2e5e)

## Downloading

You can download the latest Linux builds under the [Releases](https://github.com/TibixDev/winboat/releases) tab. We currently offer four variants:

- **AppImage:** A popular & portable app format which should run fine on most distributions
- **Unpacked:** The raw unpacked files, simply run the executable (`linux-unpacked/winboat`)
- **.deb:** The intended format for Debian based distributions
- **.rpm:** The intended format for Fedora based distributions
- **Nix (Nixpkgs)**
    1. Add the winboat package to your config (ensure using nixpkgs-unstable)
    using `environment.systemPackages = [pkgs.winboat];` or `home.packages = [pkgs.winboat];` if using home manager.
    2. Add the following lines to your nix configuration
    ```nix
    virtualisation.docker.enable = true;
    users.users.{yourUser}.extraGroups = ["docker"];
    ```
## Known Issues About Container Runtimes

- Docker Desktop is **unsupported** for now
- USB passthrough via Podman is currently **unsupported**

## Building WinBoat

- For building you need to have Bun and Go installed on your system
- Clone the repo (`git clone https://github.com/TibixDev/WinBoat`)
- Install the dependencies (`bun i`)
- Build the app and the guest server using `bun run build:linux-gs`
- You can now find the built app under `dist` with an AppImage and an Unpacked variant

## Running WinBoat in development mode

- Make sure you meet the [prerequisites](#prerequisites)
- Additionally, for development you need to have Bun and Go installed on your system
- Clone the repo (`git clone https://github.com/TibixDev/WinBoat`)
- Install the dependencies (`bun i`)
- Build the guest server (`bun run build:gs`)
- Run the app (`bun run dev`)

## Contributing

Contributions are welcome! Whether it's bug fixes, feature improvements, or documentation updates, we appreciate your help making WinBoat better.

**Please note**: We maintain a focus on technical contributions only. Pull requests containing political/sexual content, or other sensitive/controversial topics will not be accepted. Let's keep things focused on making great software! 🚀

Feel free to:

- Report bugs and issues
- Submit feature requests
- Contribute code improvements
- Help with documentation
- Share feedback and suggestions

Check out our issues page to get started, or feel free to open a new issue if you've found something that needs attention.

## License

WinBoat is licensed under the [MIT](https://github.com/TibixDev/winboat/blob/main/LICENSE) license

## Inspiration / Alternatives

These past few years some cool projects have surfaced with similar concepts, some of which we've also taken inspirations from.\
They're awesome and you should check them out:

- [WinApps](https://github.com/winapps-org/winapps)
- [Cassowary](https://github.com/casualsnek/cassowary)
- [dockur/windows](https://github.com/dockur/windows) (🌟 Also used in WinBoat)

## Socials & Contact

- [![Website](https://img.shields.io/badge/Website-winboat.app-blue?style=flat&logo=googlechrome&logoColor=white)](https://www.winboat.app/)
- [![Twitter](https://img.shields.io/badge/Twitter-@winboat__app-1DA1F2?style=flat&logo=x&logoColor=white)](https://x.com/winboat_app)
- [![Mastodon](https://img.shields.io/badge/Mastodon-@winboat-6364FF?style=flat&logo=mastodon&logoColor=white)](https://fosstodon.org/@winboat)
- [![Bluesky](https://img.shields.io/badge/Bluesky-winboat.app-00A8E8?style=flat&logo=bluesky&logoColor=white)](http://bsky.app/profile/winboat.app)
- [![Discord](https://img.shields.io/badge/Discord-Join_Community-5865F2?style=flat&logo=discord&logoColor=white)](http://discord.gg/MEwmpWm4tN)
- [![Email](https://img.shields.io/badge/Email-staff@winboat.app-D14836?style=flat&logo=gmail&logoColor=white)](mailto:staff@winboat.app)
- [![Ask DeepWiki](https://deepwiki.com/badge.svg)](https://deepwiki.com/TibixDev/winboat)

## Star History

<a href="https://www.star-history.com/?repos=tibixdev%2Fwinboat&type=date&legend=top-left">
 <picture>
   <source media="(prefers-color-scheme: dark)" srcset="https://api.star-history.com/chart?repos=tibixdev/winboat&type=date&theme=dark&legend=top-left&sealed_token=zAULDxizn0P-jCXSLfkaIJCa-OhtmdFRKaZ3CfYqpk2L4um_02jxxY0_CVY_IrvpAJk8hph0kK0oyrlD0exFSrs4VDF86zOvTw3K0FVjggjuVR_v2gre9koLhhy8vTSR3UrY1xcINMWrbDurLh9Uiej8ZD2tsDDeksYZKnJMtaDSDp0zh2zJDGGrXBx4" />
   <source media="(prefers-color-scheme: light)" srcset="https://api.star-history.com/chart?repos=tibixdev/winboat&type=date&legend=top-left&sealed_token=zAULDxizn0P-jCXSLfkaIJCa-OhtmdFRKaZ3CfYqpk2L4um_02jxxY0_CVY_IrvpAJk8hph0kK0oyrlD0exFSrs4VDF86zOvTw3K0FVjggjuVR_v2gre9koLhhy8vTSR3UrY1xcINMWrbDurLh9Uiej8ZD2tsDDeksYZKnJMtaDSDp0zh2zJDGGrXBx4" />
   <img alt="Star History Chart" src="https://api.star-history.com/chart?repos=tibixdev/winboat&type=date&legend=top-left&sealed_token=zAULDxizn0P-jCXSLfkaIJCa-OhtmdFRKaZ3CfYqpk2L4um_02jxxY0_CVY_IrvpAJk8hph0kK0oyrlD0exFSrs4VDF86zOvTw3K0FVjggjuVR_v2gre9koLhhy8vTSR3UrY1xcINMWrbDurLh9Uiej8ZD2tsDDeksYZKnJMtaDSDp0zh2zJDGGrXBx4" />
 </picture>
</a>


## /SECURITY.md

# Security Policy

WinBoat takes security very seriously, if you believe you have found a security vulnerability within WinBoat, do the following steps:

1. Construct a PoC (Proof of Concept) showcasing how the vulnerability can be used in real world scenarios.
2. Make a video showcasing the vulnerability.
3. Send an email to `staff@winboat.app` with the subject line `[SECURITY] Example Vulnerability` or contact any maintainer on [Discord](https://discord.gg/MEwmpWm4tN) (faster).
4. We will respond as soon as possible and begin working on a fix if your vulnerability qualifies. During this time, please avoid disclosing it to any third parties or the general public for obvious reasons. Our commit messages will likely not indicate that it's a fix for a security vulnerability in specific, this is to minimize any N-day attacks.
5. A release will be made including the fix for the vulnerability, which will be mentioned in the patch notes.


## /build-guest-server.sh

```sh path="/build-guest-server.sh" 
#!/bin/bash
set -e

echo "Building guest server..."

# Variables
export GOOS=windows
export GOARCH=amd64
export PACKAGE=winboat-server
export VERSION="$(bun -p "require('./package.json').version")"
export COMMIT_HASH="$(git rev-parse --short HEAD)"
export BUILD_TIMESTAMP=$(date '+%Y-%m-%dT%H:%M:%S')
export LDFLAGS=(
  "-X 'main.Version=${VERSION}'"
  "-X 'main.CommitHash=${COMMIT_HASH}'"
  "-X 'main.BuildTimestamp=${BUILD_TIMESTAMP}'"
)

echo "Version: ${VERSION}"
echo "Commit Hash: ${COMMIT_HASH}"
echo "Build Timestamp: ${BUILD_TIMESTAMP}"

# Enter build directory
cd guest_server

# Verify nssm.exe integrity
echo "Verifying nssm.exe integrity..."
if [ -f "nssm.exe" ] && [ -f "nssm.sha1.txt" ]; then
    COMPUTED_HASH=$(sha1sum nssm.exe | cut -d' ' -f1)
    EXPECTED_HASH=$(cat nssm.sha1.txt | tr -d '[:space:]')
    
    if [ "$COMPUTED_HASH" = "$EXPECTED_HASH" ]; then
        echo "✓ nssm.exe integrity verified (SHA-1: $COMPUTED_HASH)"
    else
        echo "✗ nssm.exe integrity check FAILED!"
        echo "  Expected: $EXPECTED_HASH"
        echo "  Computed: $COMPUTED_HASH"
        exit 1
    fi
else
    echo "⚠ Warning: nssm.exe or nssm.sha1.txt not found, skipping integrity check"
fi

# Build the guest server
go build -ldflags="${LDFLAGS[*]}" -o winboat_guest_server.exe *.go
rm -f winboat_guest_server.zip
zip -r winboat_guest_server.zip .

echo "Guest server built: guest_server/winboat_guest_server.zip"
```

## /data/usb.ids

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/data/usb.ids

## /electron-builder.json

```json path="/electron-builder.json" 
{
    "appId": "com.teabox.winboat",
    "productName": "WinBoat",
    "directories": {
        "output": "dist"
    },
    "publish": null,
    "nsis": {
        "oneClick": false,
        "perMachine": false,
        "allowToChangeInstallationDirectory": true,
        "shortcutName": "WinBoat"
    },
    "win": {
        "target": "nsis"
    },
    "linux": {
        "icon": "icons/winboat_logo.svg",
        "target": ["appimage", "deb", "rpm", "tar.bz2"],
        "category": "Utility",
        "artifactName": "${name}-${version}-${arch}.${ext}"
    },
    "files": [
        {
            "from": "build/main",
            "to": "main",
            "filter": ["**/*"]
        },
        {
            "from": "build/renderer",
            "to": "renderer",
            "filter": ["**/*"]
        },
        {
            "from": "src/main/static",
            "to": "static",
            "filter": ["**/*"]
        },
        "!build",
        "!dist",
        "!scripts"
    ],
    "extraResources": [
        {
            "from": "./guest_server",
            "to": "guest_server",
            "filter": ["**/*"]
        },
        {
            "from": "./data",
            "to": "data",
            "filter": ["**/*"]
        }
    ]
}

```

## /gh_assets/features/feat_apps.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_apps.png

## /gh_assets/features/feat_dash.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_dash.png

## /gh_assets/features/feat_fs.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_fs.png

## /gh_assets/features/feat_install.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_install.png

## /gh_assets/features/feat_misc.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_misc.png

## /gh_assets/features/feat_native.png

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/gh_assets/features/feat_native.png

## /guest_server/RDPApps.reg

```reg path="/guest_server/RDPApps.reg" 
Windows Registry Editor Version 5.00

    ; Disable RemoteApp allowlist so all applications can be used in Remote Desktop sessions
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Terminal Server\TSAppAllowList]
    "fDisabledAllowList"=dword:00000001

    ; Allow unlisted programs to be run in Remote Desktop sessions
    [HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft\Windows NT\Terminal Services]
    "fAllowUnlistedRemotePrograms"=dword:00000001

    ; Disable automatic administrator logon at startup
    [HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon]
    "AutoAdminLogon"="0"

    ; Always use the server's keyboard layout, TODO: Investigate
    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Keyboard Layout]
    "IgnoreRemoteKeyboardLayout"=dword:00000001

    ; Disable "Do you want your PC to be discoverable" prompt after each host system reboot
    [HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Network\NewNetworkWindowOff]
```

## /guest_server/argon2.go

```go path="/guest_server/argon2.go" 
package main

import (
	"crypto/subtle"
	"encoding/base64"
	"errors"
	"fmt"
	"strings"

	"golang.org/x/crypto/argon2"
)

type Argon2Configuration struct {
	HashRaw    []byte
	Salt       []byte
	TimeCost   uint32
	MemoryCost uint32
	Threads    uint8
	KeyLength  uint32
}

func parseArgon2Hash(encodedHash string) (*Argon2Configuration, error) {
	components := strings.Split(encodedHash, "{{contextString}}quot;)
	if len(components) != 6 {
		return nil, errors.New("invalid hash format structure")
	}

	// Validate algorithm identifier
	if !strings.HasPrefix(components[1], "argon2id") {
		return nil, errors.New("unsupported algorithm variant")
	}

	// Extract version information
	var version int
	fmt.Sscanf(components[2], "v=%d", &version)

	// Parse configuration parameters
	config := &Argon2Configuration{}
	fmt.Sscanf(components[3], "m=%d,t=%d,p=%d",
		&config.MemoryCost, &config.TimeCost, &config.Threads)

	// Decode salt component
	salt, err := base64.RawStdEncoding.DecodeString(components[4])
	if err != nil {
		return nil, fmt.Errorf("salt decoding failed: %w", err)
	}
	config.Salt = salt

	// Decode hash component
	hash, err := base64.RawStdEncoding.DecodeString(components[5])
	if err != nil {
		return nil, fmt.Errorf("hash decoding failed: %w", err)
	}
	config.HashRaw = hash
	config.KeyLength = uint32(len(hash))

	return config, nil
}

func verifyPasswordSecure(storedHash, providedPassword string) (bool, error) {
	// Parse stored hash parameters
	config, err := parseArgon2Hash(storedHash)
	if err != nil {
		return false, fmt.Errorf("hash parsing failed: %w", err)
	}

	// Generate hash using identical parameters
	computedHash := argon2.IDKey(
		[]byte(providedPassword),
		config.Salt,
		config.TimeCost,
		config.MemoryCost,
		config.Threads,
		config.KeyLength,
	)

	// Perform constant-time comparison to prevent timing attacks
	match := subtle.ConstantTimeCompare(config.HashRaw, computedHash) == 1
	return match, nil
}

```

## /guest_server/go.mod

```mod path="/guest_server/go.mod" 
module winboat-server

go 1.24.2

require (
	github.com/gorilla/mux v1.8.1
	github.com/rs/cors v1.11.1
	github.com/shirou/gopsutil v3.21.11+incompatible
	golang.org/x/crypto v0.43.0
	golang.org/x/sys v0.37.0
)

require (
	github.com/go-ole/go-ole v1.2.6 // indirect
	github.com/stretchr/testify v1.10.0 // indirect
	github.com/tklauser/go-sysconf v0.3.15 // indirect
	github.com/tklauser/numcpus v0.10.0 // indirect
	github.com/yusufpapurcu/wmi v1.2.4 // indirect
)

```

## /guest_server/go.sum

```sum path="/guest_server/go.sum" 
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/cors v1.11.1 h1:eU3gRzXLRK57F5rKMGMZURNdIG4EoAmX8k94r9wXWHA=
github.com/rs/cors v1.11.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU=
github.com/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI=
github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

```

## /guest_server/install.bat

```bat path="/guest_server/install.bat" 
@echo off
set INSTALL_DIR=C:\Program Files\WinBoat
set EXE_PATH=%INSTALL_DIR%\winboat_guest_server.exe
set TIME_SYNC_SCRIPT_PATH=%INSTALL_DIR%\scripts\time-sync.bat
set NSSM_PATH=%INSTALL_DIR%\nssm.exe
set OEM_DIR=C:\OEM

:: Registry tweaks
reg import "%OEM_DIR%\RDPApps.reg"

:: Create install directory if it doesn't exist
if not exist "%INSTALL_DIR%" mkdir "%INSTALL_DIR%"

:: Copy files from OEM to install directory
xcopy "%OEM_DIR%\*" "%INSTALL_DIR%\" /Y /E

:: Install the service with NSSM
"%NSSM_PATH%" install WinBoatGuestServer "%EXE_PATH%"
"%NSSM_PATH%" set WinBoatGuestServer Start SERVICE_AUTO_START
"%NSSM_PATH%" set WinBoatGuestServer AppDirectory "%INSTALL_DIR%"
"%NSSM_PATH%" set WinBoatGuestServer Description "WinBoat Guest Server API on port 7148"
"%NSSM_PATH%" set WinBoatGuestServer ObjectName "NT AUTHORITY\SYSTEM"

:: Add firewall rule for port 7148
netsh advfirewall firewall add rule name="Allow WinBoat API 7148" dir=in action=allow protocol=TCP localport=7148

:: Start the service
"%NSSM_PATH%" start WinBoatGuestServer

:: Startup Tasks
schtasks /create /tn "TimeSyncTask" /sc ONSTART /RL HIGHEST /tr "\"%TIME_SYNC_SCRIPT_PATH%\"" /RU SYSTEM
```

## /guest_server/main.go

```go path="/guest_server/main.go" 
//go:build windows
// +build windows

package main

import (
	"bytes"
	"encoding/json"
	"io"
	"log"
	"net/http"
	"os"
	"os/exec"
	"path/filepath"
	"strings"
	"time"

	"github.com/gorilla/mux"
	"github.com/rs/cors"
	"github.com/shirou/gopsutil/cpu"
	"github.com/shirou/gopsutil/disk"
	"github.com/shirou/gopsutil/mem"
)

var (
	Version        = "0.0.0"
	CommitHash     = "n/a"
	BuildTimestamp = "n/a"
)

const AUTHKEY_HASH_REG = "WinBoatAuthHash"
const AUTHKEY_HASH_OEM_LOCATION = "C:\\OEM\\auth.hash"

type Metrics struct {
	CPU struct {
		Usage     float64 `json:"usage"`     // Percentage, 0-100
		Frequency uint64  `json:"frequency"` // MHz
	} `json:"cpu"`
	RAM struct {
		Used       uint64  `json:"used"`       // MB
		Total      uint64  `json:"total"`      // MB
		Percentage float64 `json:"percentage"` // %
	} `json:"ram"`
	Disk struct {
		Used       uint64  `json:"used"`       // MB
		Total      uint64  `json:"total"`      // MB
		Percentage float64 `json:"percentage"` // %
	} `json:"disk"`
}

type RDPStatusResponse struct {
	RdpConnection bool `json:"rdpConnected"`
}

func getApps(w http.ResponseWriter, r *http.Request) {
	cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-Command", "[Console]::OutputEncoding = [System.Text.Encoding]::UTF8; & '.\\scripts\\apps.ps1'")
	output, err := cmd.Output()
	if err != nil {
		http.Error(w, "Failed to execute script: "+err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json; charset=utf-8")
	w.WriteHeader(http.StatusOK)
	w.Write(output)
}

func getHealth(w http.ResponseWriter, r *http.Request) {
	// Simple health check
	response := map[string]string{"status": "ok"}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(response)
}

func getVersion(w http.ResponseWriter, r *http.Request) {
	response := map[string]string{
		"version":     Version,
		"commit_hash": CommitHash,
		"build_time":  BuildTimestamp,
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(response)
}

func getMetrics(w http.ResponseWriter, r *http.Request) {
	// CPU stats
	cpuPercent, err := cpu.Percent(time.Second/4, false)
	if err != nil {
		http.Error(w, "Failed to get CPU stats: "+err.Error(), http.StatusInternalServerError)
		return
	}
	cpuInfo, err := cpu.Info()
	if err != nil || len(cpuInfo) == 0 {
		http.Error(w, "Failed to get CPU info: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// RAM stats
	memInfo, err := mem.VirtualMemory()
	if err != nil {
		http.Error(w, "Failed to get RAM stats: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Disk stats (using first disk, adjust path if needed)
	diskInfo, err := disk.Usage("C:\\")
	if err != nil {
		http.Error(w, "Failed to get disk stats: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Build metrics struct
	metrics := Metrics{}
	metrics.CPU.Usage = cpuPercent[0]              // Total CPU usage
	metrics.CPU.Frequency = uint64(cpuInfo[0].Mhz) // First CPU's frequency
	metrics.RAM.Used = memInfo.Used / 1024 / 1024  // Bytes to MB
	metrics.RAM.Total = memInfo.Total / 1024 / 1024
	metrics.RAM.Percentage = float64(metrics.RAM.Used) / float64(metrics.RAM.Total) * 100
	metrics.Disk.Used = diskInfo.Used / 1024 / 1024
	metrics.Disk.Total = diskInfo.Total / 1024 / 1024
	metrics.Disk.Percentage = diskInfo.UsedPercent

	// Send it
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(metrics)
}

func getRdpConnectedStatus(w http.ResponseWriter, r *http.Request) {
	// Check for RDP Status via quser.exe
	cmd := exec.Command("quser.exe")
	output, err := cmd.Output()
	if err != nil {
		http.Error(w, "Failed to execute script: "+err.Error(), http.StatusInternalServerError)
		return
	}
	// Check if the output contains both "active" and "rdp" todo: Check for VNC Sessions
	hasRdpSession := strings.Contains(strings.ToLower(string(output)), "active") &&
		strings.Contains(strings.ToLower(string(output)), "rdp")

	response := RDPStatusResponse{
		RdpConnection: hasRdpSession,
	}

	// Convert the response from guest server to JSON
	// Expected output { "rdp_connected": "true" } if a session is active
	jsonResponse, err := json.Marshal(response)
	if err != nil {
		http.Error(w, "Failed to marshal JSON response: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	w.Write(jsonResponse)
}

func applyUpdate(w http.ResponseWriter, r *http.Request) {
	// Verify password
	expectedHash, err := getSecureRegKey(AUTHKEY_HASH_REG)
	if err != nil || expectedHash == nil {
		http.Error(w, "Unauthorized: failed to read auth hash", http.StatusUnauthorized)
		return
	}

	password := r.FormValue("password")
	if password == "" {
		http.Error(w, "Unauthorized: password is required", http.StatusUnauthorized)
		return
	}

	isValid, err := verifyPasswordSecure(*expectedHash, password)
	if err != nil || !isValid {
		http.Error(w, "Unauthorized: invalid password", http.StatusUnauthorized)
		return
	}

	r.ParseMultipartForm(100 << 20) // Limit upload size to 100MB
	var buf bytes.Buffer

	// Grab the file
	file, _, err := r.FormFile("updateFile")
	if err != nil {
		http.Error(w, "Failed to get update file: "+err.Error(), http.StatusBadRequest)
		return
	}

	// Copy to buffer & close the file
	io.Copy(&buf, file)
	file.Close()

	// Check zip magic bytes
	if !(buf.Bytes()[0] == 'P' && buf.Bytes()[1] == 'K' && buf.Bytes()[2] == 3 && buf.Bytes()[3] == 4) {
		http.Error(w, "Uploaded file is not a valid ZIP archive", http.StatusBadRequest)
		return
	}

	// Make temp directory
	tmpDir, err := os.MkdirTemp("", "winboat-update")
	if err != nil {
		http.Error(w, "Failed to create temp directory: "+err.Error(), http.StatusInternalServerError)
	}

	// Write to temp file
	fileName := "update.zip"
	tmpFilePath := filepath.Join(tmpDir, fileName)
	err = os.WriteFile(tmpFilePath, buf.Bytes(), 0644)

	if err != nil {
		http.Error(w, "Failed to write temp file: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Get current executable path & directory
	exePath, err := os.Executable()
	if err != nil {
		http.Error(w, "Failed to get executable path: "+err.Error(), http.StatusInternalServerError)
		return
	}
	appDir := filepath.Dir(exePath)
	origScriptPath := filepath.Join(appDir, "scripts\\update.ps1")
	scriptPath := filepath.Join(tmpDir, "update.ps1")

	// Copy the update script to the temp directory
	copyFile(origScriptPath, scriptPath)

	// Return success & the paths
	response := map[string]string{
		"status":    "updating",
		"temp_path": tmpFilePath,
		"filename":  fileName,
	}
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	json.NewEncoder(w).Encode(response)

	// Create & run the script
	cmd := exec.Command("cmd", "/C", "start", "/B", "powershell",
		"-ExecutionPolicy", "Bypass", "-File", scriptPath,
		"-AppPath", appDir,
		"-UpdateFilePath", tmpFilePath,
		"-ServiceName", "WinBoatGuestServer")

	cmd.Stdout = nil
	cmd.Stderr = nil
	cmd.Stdin = nil

	// The script will take care of the rest, including stopping this service
	cmd.Start()

}

func getIcon(w http.ResponseWriter, r *http.Request) {
	path := r.PostFormValue("path")
	if path == "" {
		http.Error(w, "path is required", http.StatusBadRequest)
		return
	}

	cmd := exec.Command("powershell", "-ExecutionPolicy", "Bypass", "-File", "scripts\\get-icon.ps1", "-path", path)
	output, err := cmd.Output()
	if err != nil {
		http.Error(w, "Failed to execute script: "+err.Error(), http.StatusInternalServerError)
		return
	}

	w.Header().Set("Content-Type", "text/plain")
	w.WriteHeader(http.StatusOK)
	w.Write(output)
}

func setAuthHash(w http.ResponseWriter, r *http.Request) {
	// If there's an existing hash, reject
	existingHash, err := getSecureRegKey(AUTHKEY_HASH_REG)
	if err != nil {
		http.Error(w, "Failed to read existing auth hash: "+err.Error(), http.StatusInternalServerError)
		return
	}
	if existingHash != nil {
		http.Error(w, "Auth hash already set", http.StatusBadRequest)
		return
	}

	// Get auth hash from POST body
	authHash := r.PostFormValue("authHash")
	if authHash == "" {
		http.Error(w, "authHash is required", http.StatusBadRequest)
		return
	}

	// Store it securely
	err = setSecureRegKey(AUTHKEY_HASH_REG, authHash)
	if err != nil {
		http.Error(w, "Failed to store auth hash: "+err.Error(), http.StatusInternalServerError)
		return
	}

	// Success
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	response := map[string]string{"status": "ok"}
	json.NewEncoder(w).Encode(response)
}

func main() {
	// Try to initialize auth key hash if not present
	existingHash, err := getSecureRegKey(AUTHKEY_HASH_REG)
	if err != nil {
		log.Println("Warning: failed to read auth hash from registry:", err)
	}

	// Hash not found in registry
	if existingHash == nil {
		// Check OEM location and try to read from there
		oemHashBytes, err := os.ReadFile(AUTHKEY_HASH_OEM_LOCATION)
		if err != nil {
			log.Println("Auth hash not found in registry or OEM location.")
		} else {
			oemHash := string(bytes.TrimSpace(oemHashBytes))
			err = setSecureRegKey(AUTHKEY_HASH_REG, oemHash)
			if err != nil {
				log.Println("Failed to initialize auth hash from OEM location:", err)
			} else {
				log.Println("Initialized auth hash from OEM location.")
			}
		}
	}

	// Setup routes
	r := mux.NewRouter()
	r.HandleFunc("/apps", getApps).Methods("GET")
	r.HandleFunc("/health", getHealth).Methods("GET")
	r.HandleFunc("/version", getVersion).Methods("GET")
	r.HandleFunc("/metrics", getMetrics).Methods("GET")
	r.HandleFunc("/rdp/status", getRdpConnectedStatus).Methods("GET")
	r.HandleFunc("/update", applyUpdate).Methods("POST")
	r.HandleFunc("/get-icon", getIcon).Methods("POST")
	r.HandleFunc("/auth/set-hash", setAuthHash).Methods("POST")
	handler := cors.Default().Handler(r)

	log.Println("Starting WinBoat Guest Server on :7148...")
	if err := http.ListenAndServe(":7148", handler); err != nil {
		log.Fatal("Server failed: ", err)
	}
}

```

## /guest_server/nssm.exe

Binary file available at https://raw.githubusercontent.com/TibixDev/winboat/refs/heads/main/guest_server/nssm.exe

## /guest_server/nssm.sha1.txt

f280858dcfefabc1a9a006a57f6b266a5d1fde8e

## /guest_server/scripts/apps.ps1

```ps1 path="/guest_server/scripts/apps.ps1" 
#Requires -Version 5.1
#Requires -RunAsAdministrator

#------------------------------------------------------------------------------
# WinBoat Application Discovery Script
#
# Purpose: Detects installed applications (System, Registry, Start Menu,
#          UWP, Chocolatey, Scoop) within a Windows KVM guest and outputs
#          them as a JSON list for the WinBoat host.
#------------------------------------------------------------------------------

# --- Setup ---
# Load System.Drawing for icon extraction, suppress errors if already loaded
Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue

# Default fallback icon (32x32 transparent PNG) if extraction fails
$defaultIconBase64 = "iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAMAAABEpIrGAAAAAXNSR0IB2cksfwAAAAlwSFlzAAALEwAACxMBAJqcGAAAASZQTFRFAAAA+vr65ubm4uLkhYmLvL7A7u7w+/r729vb4eHjFYPbFoTa5eXnGIbcG4jc+fn7Gofc7+/x7OzuF4Xb+fn54uLiC37Z5OTmEIHaIIjcEYHbDoDZFIPcJ43fHYjd9fX28PDy3d3fI4rd3d3dHojc19fXttTsJIve2dnZDX/YCn3Y09PTjL/p5+fnh7zo2traJYzfIYjdE4Pb6urrW6Tf9PT1Ioneir7otNPsCX3Zhbvn+Pj5YKfhJYfWMo7a39/gKIzeKo7eMI3ZNJDcXqbg4eHhuNTsB3zYIoncBXvZLIrXIYjbLJDgt7m6ubu+YqjiKYvYvr6+tba3rs/sz8/P1+byJonXv7/DiImLxsbGjo6Ra6reurq6io6QkJKVw8PD0tLSycnJq1DGywAAAGJ0Uk5TAP////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+BVJDaAAABY0lEQVR4nM2RaVOCUBSGr1CBgFZimppgoGnKopZSaYGmRpravq///0904IqOM9j00WeGT+9ztgtCS8Dzyh98fL6i2+HqQoaj0RPSzQNgzZc4F4wgvUuoqkr1er094MjlIeBCwRdFua9CqURQ51cty7Lykj0YCIIibnlEkS4TgCuky3nbTmSFsCKSHuso96N/Ox1aacjrlYQQ3gjNCYV7UlUJ6szCeRZyXmlkNjEZEPSuLIMAuYTreVYROQ8Y8SLTNAhlCdfzLMsaIhfHgEAT7pLtvFTH9QxTNWrmLsaEDu8558y2ZOP5LLNTNUQyiCFnHaRZnjTmzryhnR36FSdnIU9up7RGxAOuKJjOFX2vHvKU5jPiepbvxzR3BIffwROc++AAJy9qjQxQwz9rIjyGeN6tj8VACEyZCqfQn3H7F48vTvwEdlIP+aWvMNkPcl8h8DYeN5vNTqdzCNz5CIv4h7AE/AKcwUFbShJywQAAAABJRU5ErkJggg=="

# Define common system paths
$systemRoot = $env:SystemRoot
$system32Path = Join-Path -Path $systemRoot -ChildPath "System32"


# --- Helper Functions ---

# Adds spaces to CamelCase or PascalCase strings (e.g., "VisualStudio" -> "Visual Studio")
function Add-SpacesToCamelCase {
    param([string]$InputString)

    # Skip if null, already contains whitespace, is only numbers, or too short to need spacing
    if ($null -eq $InputString -or $InputString -match '\s' -or $InputString -match '^\d+{{contextString}}#39; -or $InputString.Length -lt 3) {
        return $InputString
    }

    try {
        # Regex patterns:
        # 1. (?<=[a-z])(?=[A-Z])   : Lowercase followed by Uppercase
        # 2. (?<=[A-Z])(?=[A-Z][a-z]) : Uppercase followed by Uppercase+Lowercase (handles acronyms like "MSPaint")
        # 3. (?<=[a-zA-Z])(?=[0-9])  : Letter followed by Digit
        $pattern = '((?<=[a-z])(?=[A-Z])|(?<=[A-Z])(?=[A-Z][a-z])|(?<=[a-zA-Z])(?=[0-9]))'
        $spaced = $InputString -replace $pattern, ' '
        return $spaced.Trim()
    } catch {
        # Return original string if regex fails for any reason
        return $InputString
    }
}

# Extracts an application's associated icon as a 32x32 PNG Base64 string.
function Get-ApplicationIcon {
    param ([string]$targetPath)

    # Pre-check: Ensure System.Drawing is loaded and path is a valid file
    if (-not [System.Drawing.Icon] -or -not (Test-Path -LiteralPath $targetPath -PathType Leaf -ErrorAction SilentlyContinue)) {
        return $defaultIconBase64
    }

    try {
        $icon = [System.Drawing.Icon]::ExtractAssociatedIcon($targetPath)
        if ($null -eq $icon) { throw "ExtractAssociatedIcon returned null for $targetPath" }

        # Get best available bitmap from icon
        $bmp = $icon.ToBitmap()

        # Resize to a standard 32x32 using high quality interpolation
        $resizedBmp = New-Object System.Drawing.Bitmap(32, 32)
        $graphics = [System.Drawing.Graphics]::FromImage($resizedBmp)
        $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
        $graphics.DrawImage($bmp, 0, 0, 32, 32)

        # Save as PNG to memory stream
        $stream = New-Object System.IO.MemoryStream
        $resizedBmp.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
        $base64 = [Convert]::ToBase64String($stream.ToArray())

        # Clean up GDI+ resources
        $stream.Dispose()
        $graphics.Dispose()
        $resizedBmp.Dispose()
        $bmp.Dispose()
        $icon.Dispose()

        return $base64
    } catch {
        # Fallback on any error during extraction/conversion
        return $defaultIconBase64
    }
}

# Gets the application name using a priority order:
# 1. Target EXE FileDescription
# 2. LNK file name (often the user-facing name)
# 3. Target file name
function Get-ApplicationName {
    param (
        [string]$targetPath,
        [string]$lnkPath = $null
    )

    $appName = $null

    # Priority 1: Target Executable's FileDescription
    if ($targetPath -and $targetPath.EndsWith('.exe', [System.StringComparison]::OrdinalIgnoreCase) -and (Test-Path $targetPath -PathType Leaf)) {
        try {
            $desc = (Get-Item $targetPath).VersionInfo.FileDescription
            # Only use description if it's not empty/whitespace
            if ($desc -and $desc.Trim()) {
                $appName = $desc.Trim() -replace '\s+', ' ' # Normalize whitespace
            }
        } catch { } # Ignore errors reading version info
    }

    # Priority 2: LNK Filename (BaseName without extension), attempt spacing
    if (-not $appName -and $lnkPath -and (Test-Path $lnkPath -PathType Leaf)) {
        $appName = Add-SpacesToCamelCase -InputString ([System.IO.Path]::GetFileNameWithoutExtension($lnkPath))
    }

    # Priority 3: Target Filename (BaseName without extension), attempt spacing
    if (-not $appName -and $targetPath -and (Test-Path $targetPath -PathType Leaf)) {
        $appName = Add-SpacesToCamelCase -InputString ([System.IO.Path]::GetFileNameWithoutExtension($targetPath))
    }

    # Final Cleanup: Remove common registered/trademark symbols and trim whitespace
    if ($appName) {
        # TODO: This breaks on CJK systems because of UTF-8, check out https://github.com/TibixDev/winboat/issues/40
        # Original line in Base64:
        # JGFwcE5hbWUgPSAkYXBwTmFtZSAtcmVwbGFjZSAnKD9pKVxzKlwoclwpfFwodG1cKXzCqXzCrnzihKInLCAnJw==
        $appName = $appName.Trim()
    }

    return $appName
}

function Get-PrettifyName {
    param (
        [string]$Name
    )

    if ($Name -match '\.([^\.]+){{contextString}}#39;) {
        $ProductName = $Matches[1]
    } else {
        # If no period is found, use the whole name.
        $ProductName = $Name
    }
    
    $PrettyName = ($ProductName -creplace  '([A-Z\W_]|\d+)(?<![a-z])',' {{contextString}}amp;').trim()

    return $PrettyName
}

# Gets the display name for a UWP app.
function Get-UWPApplicationName {
    param (
        $app # The AppxPackage object
    )
    # UWP properties are usually the best source
    if ($app.DisplayName) { 
        return $app.DisplayName.Trim()
        #Write-Host($app)
    }
    if ($app.Name) { 
        return Get-PrettifyName -Name $app.Name
    } # Often the package name, less ideal but a fallback

    return $null # Failed to get a name
}

function Get-ParseUWP {
    param ([string]$instLoc) # InstallLocation from Get-AppxPackage

    $manifestPath = Join-Path -Path $instLoc -ChildPath "AppxManifest.xml"
    if (-not (Test-Path $manifestPath -PathType Leaf)) {
        return $null # Manifest doesn't exist or isn't a file
    }

    $xmlContent = Get-Content $manifestPath -Raw -Encoding Default -ErrorAction Stop
    
    $appId = "App" # Default, as it works for most packages.
    if ($xmlContent -match '<Application[^>]*Id\s*=\s*"([^"]+)"') {
        $appId = $Matches[1]
    }

    $logoMatch = [regex]::Match($xmlContent, '<Properties.*?>.*?<Logo>(.*?)</Logo>.*?</Properties>', [System.Text.RegularExpressions.RegexOptions]::Singleline)
    if ($logoMatch.Success) {
        $logo = $logoMatch.Groups[1].Value
    }

    return $logo, $appId
}

function Get-UWPBase64Logo {
    param([string]$logo, [string]$instLoc)

    $logoPath = Join-Path -Path $instLoc -ChildPath $logo

    if (-not (Test-Path $logoPath)) {
        # if base file not exist, attempt to find scaled version.
        $scaledVersions = @("scale-100", "scale-200", "scale-400")
        foreach ($scale in $scaledVersions) {
            $scaledLogoPath = $logoPath -replace '\.png{{contextString}}#39;, ".$scale.png"
            if (Test-Path $scaledLogoPath) {
                $logoPath = $scaledLogoPath
                break
            }
        }

        # null if no valid file found.
        if (-not (Test-Path $logoPath)) {
            return $null
        }
    }

    try {
        $image = [System.Drawing.Image]::FromFile($logoPath)

        # resize to 32x32
        $resizedBmp = New-Object System.Drawing.Bitmap(32, 32)
        $graphics = [System.Drawing.Graphics]::FromImage($resizedBmp)
        $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
        $graphics.DrawImage($image, 0, 0, 32, 32)

        # Save as PNG to memory stream
        $stream = New-Object System.IO.MemoryStream
        $resizedBmp.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)

        $base64 = [Convert]::ToBase64String($stream.ToArray())

        # Clean up
        $stream.Dispose()
        $graphics.Dispose()
        $resizedBmp.Dispose()
        $image.Dispose()

        return $base64
    } catch {
        return $null
    }
}

# --- Main Application Logic ---

# Use efficient List and HashSet for collection and deduplication
$apps = [System.Collections.Generic.List[PSCustomObject]]::new()
# Store normalized (lowercase) full paths for case-insensitive duplicate checking
$addedPaths = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)

# Helper function to validate and add an app to the list if it's unique
function Add-AppToListIfValid {
    param(
        [string]$Name,
        [string]$InputPath, # The path discovered (can be relative or contain variables)
        [string]$Source,     # Source type (e.g., 'system', 'winreg', 'startmenu')

        [string]$logoBase64,  # Optional base64 logo, mainly for UWP logo discover system
        [string]$launchArg # optional arg
    )

    $resolved = $null
    $fullPath = $null
    $normalizedPathKey = $null

    # Step 1, handle UWP app.
    if ($Source -eq "uwp") {
        if (-not $launchArg) {
            return # launching UWP app requires Arg
        }
        
        $normalizedPathKey = $InputPath.ToLowerInvariant() + $launchArg.ToLowerInvariant()
        if ($addedPaths.Contains($normalizedPathKey)) {
            return # Already added this app
        }

        $icon = $null
        if ($logoBase64) {
            $icon = $logoBase64
        }

        # UWP object
        $apps.Add([PSCustomObject]@{
            Name   = $Name
            Path   = $InputPath
            Args   = $launchArg
            Icon   = $icon
            Source = 'uwp'
        })
        $addedPaths.Add($normalizedPathKey) | Out-Null
        return
    }

    # 2. Resolve and Validate Path Other
    try {
        $resolved = Resolve-Path -Path $InputPath -ErrorAction SilentlyContinue
    } catch { return } # Ignore if path resolution throws error

    # Ensure resolved path exists and is a file (Leaf)
    if ($resolved -and (Test-Path -LiteralPath $resolved.ProviderPath -PathType Leaf)) {
        $fullPath = $resolved.ProviderPath
        # Create a consistent key for the HashSet (lowercase)
        $normalizedPathKey = $fullPath.ToLowerInvariant()
    } else {
        return # Skip if path doesn't resolve to a valid file
    }

    # 3. Validate Name (Basic Check)
    if (-not $Name -or $Name.Trim().Length -eq 0 -or $Name -like 'Microsoft? Windows? Operating System*') {
        return # Skip if name is empty, invalid, or generic OS name
    }

    # 4. Check for Duplicates using normalized path
    if ($addedPaths.Contains($normalizedPathKey)) {
        return # Skip if this exact executable path has already been added
    }

    # 5. Get Icon
    $icon = Get-ApplicationIcon -targetPath $fullPath

    # 6. Add the Application Object (matching WinApp type)
    $apps.Add([PSCustomObject]@{
        Name   = $Name
        Path   = $fullPath # Use the resolved, non-normalized path for output
        Args    = ""
        Icon   = $icon
        Source = $Source
    })

    # 7. Mark Path as Added
    $addedPaths.Add($normalizedPathKey) | Out-Null
}


# --- Application Discovery Sections ---

# 1. Hardcoded Common System Tools
$systemTools = @(
    @{N = "Task Manager"; P = Join-Path $system32Path "Taskmgr.exe"},
    @{N = "Control Panel"; P = Join-Path $system32Path "control.exe"},
    @{N = "File Explorer"; P = Join-Path $env:WINDIR "explorer.exe"},
    @{N = "Command Prompt"; P = Join-Path $system32Path "cmd.exe"},
    @{N = "PowerShell"; P = Join-Path $system32Path "WindowsPowerShell\v1.0\powershell.exe"},
    @{N = "Notepad"; P = Join-Path $system32Path "notepad.exe"},
    @{N = "Paint"; P = Join-Path $system32Path "mspaint.exe"},
    @{N = "Registry Editor"; P = Join-Path $env:WINDIR "regedit.exe"},
    @{N = "Services"; P = Join-Path $system32Path "services.msc"},
    @{N = "Device Manager"; P = Join-Path $system32Path "devmgmt.msc"},
    @{N = "Computer Management"; P = Join-Path $system32Path "compmgmt.msc"},
    @{N = "Disk Management"; P = Join-Path $system32Path "diskmgmt.msc"},
    @{N = "Snipping Tool"; P = Join-Path $system32Path "SnippingTool.exe"}, # Legacy version
    @{N = "Calculator"; P = Join-Path $system32Path "win32calc.exe"},    # Legacy version
    @{N = "Remote Desktop Connection"; P = Join-Path $system32Path "mstsc.exe"}
)
foreach ($tool in $systemTools) {
    # Use the predefined name 'N' for system tools
    Add-AppToListIfValid -Name $tool.N -InputPath $tool.P -Source "system"
}


# 2. Windows Registry (App Paths - HKLM & HKCU)
try {
    $regRoots = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths",
                "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths"

    # Get properties from all matching keys under both roots
    $regEntries = foreach ($regRoot in $regRoots) {
        if (Test-Path $regRoot) {
            Get-ItemProperty "$regRoot\*" -ErrorAction SilentlyContinue
        }
    }

    # Process each found registry entry
    foreach ($entry in $regEntries) {
        $keyName = $entry.PSChildName # e.g., msedge.exe, devenv.exe
        $pathValue = $null

        # Check if the (default) value exists and is not empty
        if ($entry.PSObject.Properties['(default)'] -and $entry.'(default)') {
            try {
                # Expand environment variables and remove surrounding quotes
                $pathValue = $ExecutionContext.InvokeCommand.ExpandString($entry.'(default)'.Trim('"'))
            } catch { } # Ignore errors expanding variables
        }

        if ($pathValue) {
            # Get name using standard function (tries FileDescription first)
            $appName = Get-ApplicationName -targetPath $pathValue
            # Fallback: If Get-ApplicationName fails, use the spaced registry key name
            if (-not $appName) {
                $appName = Add-SpacesToCamelCase -InputString ([System.IO.Path]::GetFileNameWithoutExtension($keyName))
            }
            # Add if name is valid
            if ($appName) {
                Add-AppToListIfValid -Name $appName -InputPath $pathValue -Source "winreg"
            }
        }
    }
} catch { } # Ignore errors during registry scan


# 3. Start Menu Shortcuts (All Users + All User Profiles)
$startMenuPaths = @()

# Add global Start Menu path
$startMenuPaths += "C:\ProgramData\Microsoft\Windows\Start Menu\Programs"

# Add Start Menu paths for all user profiles (when running as SYSTEM)
try {
    $usersPath = "C:\Users"
    if (Test-Path $usersPath -PathType Container) {
        Get-ChildItem $usersPath -Directory -ErrorAction SilentlyContinue | 
            Where-Object { $_.Name -ne "Public" -and $_.Name -ne "All Users" -and $_.Name -ne "Default" -and $_.Name -ne "Default User" } |
            ForEach-Object {
                $userStartMenuPath = Join-Path $_.FullName "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
                if (Test-Path $userStartMenuPath -PathType Container) {
                    $startMenuPaths += $userStartMenuPath
                }
            }
    }
} catch {
    # Fallback: If C:\Users scan fails, try WMI approach
    try {
        Get-WmiObject Win32_UserProfile -ErrorAction SilentlyContinue | 
            Where-Object { -not $_.Special -and $_.LocalPath -and $_.LocalPath -notlike "*\Public" -and $_.LocalPath -notlike "*\Default*" } | 
            ForEach-Object {
                $userStartMenuPath = Join-Path $_.LocalPath "AppData\Roaming\Microsoft\Windows\Start Menu\Programs"
                if (Test-Path $userStartMenuPath -PathType Container) {
                    $startMenuPaths += $userStartMenuPath
                }
            }
    } catch { } # If both methods fail, just use global path
}

# Collect .lnk files from all valid Start Menu directories
$lnkFiles = @()
foreach ($startMenuPath in $startMenuPaths) {
    if (Test-Path $startMenuPath -PathType Container) {
        try {
            $pathLnkFiles = Get-ChildItem -Path $startMenuPath -Recurse -Filter *.lnk -File -ErrorAction SilentlyContinue
            if ($pathLnkFiles) {
                $lnkFiles += $pathLnkFiles
            }
        } catch { } # Ignore errors for individual paths
    }
}

if ($lnkFiles) {
    try {
        # Use strict mode for COM object for better error catching
        $shell = New-Object -ComObject WScript.Shell -Strict
        try {
            foreach ($lnk in $lnkFiles) {
                $target = $null
                $appName = $null

                    # Parse the shortcut file
                    try {
                        $link = $shell.CreateShortcut($lnk.FullName)
                        $rawTarget = $link.TargetPath
                        # Resolve path if contains environment variables
                        if ($rawTarget) {
                            $target = try { $ExecutionContext.InvokeCommand.ExpandString($rawTarget) } catch { $rawTarget }
                            
                            # Handle case where SYSTEM user resolves paths to SYSTEM profile instead of actual user
                            $systemProfilePath = "C:\WINDOWS\system32\config\systemprofile"
                            if ($target -like "$systemProfilePath*") {
                                # Extract the relative path after the profile directory
                                $relativePath = $target -replace [regex]::Escape($systemProfilePath), ""
                                
                                # Try to find this app in actual user profiles
                                $userBasePath = Split-Path (Split-Path $lnk.FullName) # Get user's start menu base path
                                if ($userBasePath -like "*\Users\*\AppData\Roaming\Microsoft\Windows\Start Menu\Programs") {
                                    # Extract username from the path
                                    if ($userBasePath -match "\\Users\\([^\\]+)\\") {
                                        $userName = $matches[1]
                                        $userTarget = "C:\Users\$userName$relativePath"
                                        if (Test-Path $userTarget -PathType Leaf) {
                                            $target = $userTarget
                                        }
                                    }
                                }
                            }
                        }
                    } catch { continue } # Skip malformed or unreadable shortcuts

                # Skip if no target or looks like an uninstaller
                if (-not $target -or $target -like '*uninstall*' -or $target -like '*unins000*') {
                    continue
                }

                # Get name using standard function (FileDesc > LNK Name > Target Name)
                $appName = Get-ApplicationName -targetPath $target -lnkPath $lnk.FullName

                # Refinement: If target is NOT an exe and name defaulted to target filename, prefer LNK filename
                if ($target -notlike '*.exe' -and $appName -eq (Add-SpacesToCamelCase ([System.IO.Path]::GetFileNameWithoutExtension($target))) ) {
                    $lnkNameOnly = Add-SpacesToCamelCase -InputString ([System.IO.Path]::GetFileNameWithoutExtension($lnk.FullName))
                    if ($lnkNameOnly -ne $appName) { $appName = $lnkNameOnly }
                }

                # Add if name is valid
                if ($appName) {
                    Add-AppToListIfValid -Name $appName -InputPath $target -Source "startmenu"
                }
            }
        } finally {
            # Ensure COM object is released
            if ($shell) {
                [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null
                Remove-Variable shell -ErrorAction SilentlyContinue # Clean up variable
                [System.GC]::Collect()
                [System.GC]::WaitForPendingFinalizers()
            }
        }
    } catch { } # Ignore errors during Start Menu scan
}


# 4. UWP Apps
if (Get-Command Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue) {
    try {
        Get-AppxPackage -AllUsers -ErrorAction SilentlyContinue |
            Where-Object {
                $_.IsFramework -eq $false -and
                $_.IsResourcePackage -eq $false -and
                $_.SignatureKind -ne 'System' -and       # Exclude core system packages
                $_.SignatureKind -ne 'Developer' -and     # Exclude "developer" packages
                $_.InstallLocation                       # Must have an install location
            } |
            ForEach-Object {
                $app = $_
                $pathValue = "explorer.exe"

                # Attempt to find logo and executable using Appxmanifest.xml
                $logo, $appId = Get-ParseUWP -instLoc $app.InstallLocation
                $launchArgs = "shell:AppsFolder\" + $app.PackageFamilyName + '!' + $appId

                if ($appId) { # check if we have appId
                    # Get the best display name (UWP properties preferred)
                    $name = Get-UWPApplicationName -exePath $exePath -app $app
                    if ($name) {
                        $base64 = Get-UWPBase64Logo -logo $logo -instLoc $app.InstallLocation
                        Add-AppToListIfValid -Name $name -InputPath $pathValue -Source "uwp" -logoBase64 $base64 -launchArg $launchArgs
                    }
                }
            }
    } catch { } # Ignore errors during UWP scan
}


# 5. Chocolatey Installed Apps (via Shims)
$chocoDir = "C:\ProgramData\chocolatey\bin"
if (Test-Path $chocoDir -PathType Container) {
    try {
        Get-ChildItem -Path $chocoDir -Filter *.exe -File -ErrorAction SilentlyContinue |
            ForEach-Object {
                $shim = $_
                $exePath = $null
                $name = $null

                # Resolve shim target using Get-Command
                try {
                    $cmdInfo = Get-Command $shim.FullName -ErrorAction SilentlyContinue
                    # Ensure it resolved to a different path (not the shim itself)
                    if ($cmdInfo -and $cmdInfo.Source -ne $shim.FullName) {
                        $exePath = $cmdInfo.Source
                    }
                } catch { } # Ignore resolution errors

                if ($exePath) { # Path must resolve
                    # Get name (FileDesc preferred)
                    $name = Get-ApplicationName -targetPath $exePath
                    # Fallback to spaced shim name if needed
                    if (-not $name) { $name = Add-SpacesToCamelCase -InputString $shim.BaseName }

                    if ($name) {
                        Add-AppToListIfValid -Name $name -InputPath $exePath -Source "choco"
                    }
                }
            }
    } catch { } # Ignore errors during Choco scan
}


# 6. Scoop Installed Apps (via Shims)
# Check both common user and global scoop shim paths
$scoopDir = @(
    Join-Path $env:USERPROFILE "scoop\shims"
    "C:\ProgramData\scoop\shims"
) | Where-Object { Test-Path $_ -PathType Container } | Select-Object -First 1

if ($scoopDir) {
    try {
        Get-ChildItem -Path $scoopDir -File -ErrorAction SilentlyContinue |
            Where-Object { $_.Name -ne 'scoop.ps1' } | # Exclude scoop runner itself
            ForEach-Object {
                $shim = $_
                $exePath = $null
                $name = $null

                # Attempt to resolve target using Get-Command first
                try {
                    $cmdInfo = Get-Command $shim.FullName -ErrorAction SilentlyContinue
                    if ($cmdInfo -and $cmdInfo.Source -ne $shim.FullName) {
                        $exePath = $cmdInfo.Source
                    }
                } catch { } # Ignore resolution errors

                # Fallback: Very basic content scan for text-based shims if Get-Command fails
                if (-not $exePath -and $shim.Extension -in '.cmd', '.ps1', '') {
                    try {
                        # Read first 5 lines, look for "path/to/something.exe" pattern
                        $content = Get-Content $shim.FullName -Raw -TotalCount 5 -ErrorAction SilentlyContinue
                        if ($content -match '(?<=")([^"]+?\.exe)(?=")') {
                            $relativePath = $Matches[1] -replace '%~dp0', $shim.DirectoryName
                            $exePath = try { (Resolve-Path $relativePath -ErrorAction SilentlyContinue).Path } catch {}
                        }
                    } catch {}
                }

                if ($exePath) { # Path must resolve
                    # Get name (FileDesc preferred)
                    $name = Get-ApplicationName -targetPath $exePath
                    # Fallback to spaced shim name
                    if (-not $name) { $name = Add-SpacesToCamelCase -InputString $shim.BaseName }

                    if ($name) {
                        Add-AppToListIfValid -Name $name -InputPath $exePath -Source "scoop"
                    }
                }
            }
    } catch { } # Ignore errors during Scoop scan
}


# --- Final Output ---

# Convert the final list of application objects to a compressed JSON string.
# This is the only output sent to the standard output stream.

$apps | ConvertTo-Json -Depth 5 -Compress



```

## /guest_server/scripts/get-icon.ps1

```ps1 path="/guest_server/scripts/get-icon.ps1" 
#Requires -Version 5.1

# WinBoat - Icon Extraction Helper
#
# Usage:
#   powershell -ExecutionPolicy Bypass -File icon.ps1 "C:\Path\To\Something.exe"
#   powershell -ExecutionPolicy Bypass -File icon.ps1 "C:\Path\To\Shortcut.lnk" -Size 48
#
# Output:
#   Writes a Base64-encoded PNG of the associated icon to stdout.
#   If extraction fails, outputs a small transparent PNG as fallback.

param(
    [Parameter(Mandatory = $true, Position = 0)]
    [string]$Path,

    [Parameter(Mandatory = $false)]
    [ValidateRange(16, 512)]
    [int]$Size = 128
)

# Suppress non-terminating errors to keep stdout clean (only base64 should be printed)
$ErrorActionPreference = 'SilentlyContinue'

# Load System.Drawing for icon extraction/conversion
Add-Type -AssemblyName System.Drawing -ErrorAction SilentlyContinue

# Default transparent 256x256 PNG as Base64 (used if extraction fails)
$defaultIconBase64 = "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAABiVBMVEUAAABzc3N5eXl0dHR5dnN5dnN4eHV5d3V6eHR5d3V6eHR5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3QGabwGar0Ha70Ha74HbL4HbL8Hbb8HbcAIbsAIbsEIb8EIcMIJccIJccMJcsMJcsQJc8QKc8UKdMUKdMYKdcYKdscLdscLd8cLd8gLeMgLeMkLeckMecoMesoMessMe8sMe8wMfMwNfMwNfcwNfc0Nfs0Nfs4Nf84Of88OgM8OgNAOgdAOgdEOgtEPgtEPg9IPhNIPhNMPhdMQhdQQhtQQhtUQh9UQh9YRiNYRidcRitcRitgSi9gSi9kSjNkSjNoSjdoSjdsTjtsTj9wed8IeeMMfecMpmd4qmd8qmt94rdl5d3R5rtp8end+wep/weqHhYOMi4iOjIqPjYqPjYuWlpbAv77BwL7Cwb/CwcDDwsHEw8LExMTPz87Q0M/S0dDX19bY19bk5OPl5eTx8PDx8fHz8/Lz8/P09PP09vj09/n4+Pj6+vr///+Z/ULnAAAAFXRSTlMAFBUWUlRVgYKDhM3Oz9Dw8fLz9P7FJ1F0AAAAAWJLR0SCi7P/RAAAAtFJREFUeNrt2s1qE1EYxvGZTGfSIEYKQhYigqtcSN2IVi/BO2z9WHotomIXorQS05JkpplmalyIbmrz2ff3h1C6aJnzz/O85+S0SQIAAAAAAAAAAAAAAG476eyVtbPiebiVvx1fTC6uBBR3XgR9918PyyRLiu5B1Pj3P5fTNOve9vd/b/Y6/cfXhqNBqx01/1e8LFo7sTeBnegC8vRV8HNAKyEg+BC4KkK3k4dbeTkaTOYC8vu7EcdfXnwrf1Wguxsz/e278xnQidr/zlxAHlVAYRcggAACCCCAAAIIIIAAAgggICZ//1ng6zV+uvfnt9Mv5WTpT5y3H7QW9cCLTsDx2fLXn5TD4+UmoPf/v2xcryS1o4U98KITMFlNbUtDcKkVuAE1Ac4BBKiABEiABEiABEhAzPuA9QjYoPuA21GB3rYtoWcGEGAbJIAAM0ACCFABAlSAABVY633AerjZA0uA+wDboF3ADFABJ0EzwAwwA8wAAgxBM0ACknD/H+AcsMiP1+sR0DMDCLANEqACBKiABEgAASpAgAq4D7jmJ0cVWPDH6627DzADnAPMABUgwAzwWcAQNAPMADPADDADtvM+QAU25z5AAgiwCxCgAgSoAAEEmAESIAEERLwPMAPcB5gBBJgBBKiABEiABEiABEgAASoQ7z7ADIh+H2AGqAABBJgBBKgAASpAgApIgARIgARIgAQQoAIEqEAkAfureeYnG1uBfDUdKDZWwKOnk/dLX/5+8XBjBWSPk74hSAABBBBAAAEEEEAAAVsjoIy6+MlcQBVVQDUXcBZVwPB3AuqY66+bBGSf+rMkdEIKODlPkqNWY2EQsgTDpgHVTiNg+n28l0bL/8lw2uyAWf1xdotVj0ejNI1zJqhGp4PzZvIdnqdJfu9Z2FPQux9VlkzLD/2g6z/8WSVN9VtF1j4It/o342pSJwAAAAAAAAAAAAAAIACXIfSz2zOYD54AAAAASUVORK5CYII="

function Write-FallbackBase64 {
    Write-Output $defaultIconBase64
}

function Resolve-LnkTargetPath {
    param([string]$lnkPath)

    if (-not (Test-Path -LiteralPath $lnkPath -PathType Leaf)) { return $null }
    try {
        $shell = New-Object -ComObject WScript.Shell -Strict
        try {
            $shortcut = $shell.CreateShortcut($lnkPath)
            if ($shortcut -and $shortcut.IconLocation) {
                # IconLocation can be "path,index"; prefer the icon file path if present
                $iconSpec = $shortcut.IconLocation
                $iconPath = $iconSpec.Split(',')[0]
                if ($iconPath -and (Test-Path -LiteralPath $iconPath -PathType Leaf)) {
                    return $iconPath
                }
            }
            if ($shortcut -and $shortcut.TargetPath) {
                return $shortcut.TargetPath
            }
        } finally {
            if ($shell) { [System.Runtime.InteropServices.Marshal]::ReleaseComObject($shell) | Out-Null }
        }
    } catch { }
    return $null
}

function Get-IconBase64FromFile {
    param(
        [string]$filePath,
        [int]$size
    )

    if (-not [System.Drawing.Icon]) { return $null }

    try {
        # Handle .ico files explicitly for best fidelity
        $ext = [System.IO.Path]::GetExtension($filePath)
        if ($ext -and $ext.Equals('.ico', [System.StringComparison]::OrdinalIgnoreCase)) {
            $ico = New-Object System.Drawing.Icon($filePath)
        } else {
            $ico = [System.Drawing.Icon]::ExtractAssociatedIcon($filePath)
        }
        if ($null -eq $ico) { return $null }

        $bmp = $ico.ToBitmap()
        $resizedBmp = New-Object System.Drawing.Bitmap($size, $size)
        $graphics = [System.Drawing.Graphics]::FromImage($resizedBmp)
        $graphics.InterpolationMode = [System.Drawing.Drawing2D.InterpolationMode]::HighQualityBicubic
        $graphics.DrawImage($bmp, 0, 0, $size, $size)

        $stream = New-Object System.IO.MemoryStream
        $resizedBmp.Save($stream, [System.Drawing.Imaging.ImageFormat]::Png)
        $base64 = [Convert]::ToBase64String($stream.ToArray())

        $stream.Dispose()
        $graphics.Dispose()
        $resizedBmp.Dispose()
        $bmp.Dispose()
        $ico.Dispose()

        return $base64
    } catch {
        return $null
    }
}

# Expand environment variables if any (e.g., %WINDIR%) without introducing noise
$expandedPath = try { $ExecutionContext.InvokeCommand.ExpandString($Path) } catch { $Path }

# Resolve .lnk shortcuts first if the given path ends with .lnk
$candidatePath = $expandedPath
if ($candidatePath -like '*.lnk') {
    $lnkTarget = Resolve-LnkTargetPath -lnkPath $candidatePath
    if ($lnkTarget) { $candidatePath = $lnkTarget }
}

# Resolve to a filesystem path if possible
$resolvedPath = try { (Resolve-Path -LiteralPath $candidatePath -ErrorAction SilentlyContinue).ProviderPath } catch { $null }
if (-not $resolvedPath) { $resolvedPath = $candidatePath }

# Ensure target exists and is a file
if (-not (Test-Path -LiteralPath $resolvedPath -PathType Leaf)) {
    Write-FallbackBase64
    exit 0
}

$result = Get-IconBase64FromFile -filePath $resolvedPath -size $Size
if ($result) {
    Write-Output $result
} else {
    Write-FallbackBase64
}

exit 0
```

## /guest_server/scripts/time-sync.bat

```bat path="/guest_server/scripts/time-sync.bat" 
@echo off
net stop w32time
net start w32time
w32tm /resync
```

## /guest_server/scripts/update.ps1

```ps1 path="/guest_server/scripts/update.ps1" 
param(
    [Parameter(Mandatory=$true)]
    [string]$AppPath,
    
    [Parameter(Mandatory=$true)]
    [string]$UpdateFilePath,
    
    [string]$ServiceName = "WinBoatGuestServer",
    
    [int]$InitialDelay = 2,
    [int]$StopTimeout = 30
)

# Suppress non-log output
$ProgressPreference = 'SilentlyContinue'
$VerbosePreference = 'SilentlyContinue'
$DebugPreference = 'SilentlyContinue'
$WarningPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'
$ErrorActionPreference = 'SilentlyContinue'

# Set up logging to temp folder
$TempDir = Split-Path $UpdateFilePath -Parent
$LogFile = Join-Path $TempDir "logs.txt"

function Write-Log {
    param([string]$Message, [string]$Level = "INFO")
    $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $logMessage = "[$timestamp] [$Level] $Message"
    
    # Use Write-Output instead of Write-Host for better compatibility
    Write-Output $logMessage
    
    # Log to temp folder
    try {
        Add-Content -Path $LogFile -Value $logMessage -Encoding UTF8
    } catch {
        Write-Output "Failed to write to log file: $($_.Exception.Message)"
    }
}

try {
    Write-Log "Update script started"
    Write-Log "App Path: $AppPath"
    Write-Log "Update File: $UpdateFilePath"
    Write-Log "Service Name: $ServiceName"
    Write-Log "Log File: $LogFile"
    Write-Log "Temp Directory: $TempDir"
    
    # Initial delay to let Go app finish its response and exit gracefully
    Write-Log "Waiting $InitialDelay seconds for app to finish..."
    Start-Sleep -Seconds $InitialDelay
    
    # Validate inputs
    if (-not (Test-Path $AppPath)) {
        throw "Application path does not exist: $AppPath"
    }
    
    if (-not (Test-Path $UpdateFilePath)) {
        throw "Update file does not exist: $UpdateFilePath"
    }
    
    # Check if running as administrator
    if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) {
        throw "Script must be run as Administrator to manage services"
    }
    
    # Get service
    $service = Get-Service -Name $ServiceName -ErrorAction SilentlyContinue
    if (-not $service) {
        throw "Service '$ServiceName' not found"
    }
    
    Write-Log "Current service status: $($service.Status)"
    
    # Force stop the service (the Go app should have already exited, but make sure)
    if ($service.Status -ne "Stopped") {
        Write-Log "Force stopping service '$ServiceName'..."
        Stop-Service -Name $ServiceName -Force -ErrorAction SilentlyContinue
        
        # Wait for it to actually stop
        $elapsed = 0
        do {
            Start-Sleep -Seconds 1
            $elapsed++
            $service = Get-Service -Name $ServiceName
            if ($elapsed % 5 -eq 0) {
                Write-Log "Waiting for service to stop... ($elapsed seconds)"
            }
        } while ($service.Status -ne "Stopped" -and $elapsed -lt $StopTimeout)
        
        if ($service.Status -ne "Stopped") {
            Write-Log "Service didn't stop gracefully, continuing anyway..." -Level "WARN"
        } else {
            Write-Log "Service stopped"
        }
    }
    
    # Extra delay to ensure all file handles are released
    Write-Log "Waiting for file handles to release..."
    Start-Sleep -Seconds 3
    
    # Create backup in a more appropriate location
    $backupDir = "$env:PROGRAMDATA\WinBoat\Backups"
    if (-not (Test-Path $backupDir)) {
        Write-Log "Creating backup directory: $backupDir"
        New-Item -ItemType Directory -Path $backupDir -Force | Out-Null
    }
    
    $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
    $backupPath = Join-Path $backupDir "backup-$timestamp"
    Write-Log "Creating backup: $backupPath"
    Copy-Item -Path $AppPath -Destination $backupPath -Recurse -Force
    
    # Clean up old backups (keep only last 5)
    try {
        $oldBackups = Get-ChildItem -Path $backupDir -Directory | 
                     Where-Object { $_.Name -like "backup-*" } | 
                     Sort-Object CreationTime -Descending | 
                     Select-Object -Skip 5
        
        if ($oldBackups) {
            Write-Log "Cleaning up $($oldBackups.Count) old backup(s)"
            $oldBackups | ForEach-Object {
                Write-Log "Removing old backup: $($_.Name)"
                Remove-Item $_.FullName -Recurse -Force -ErrorAction SilentlyContinue
            }
        }
    } catch {
        Write-Log "Failed to clean up old backups: $($_.Exception.Message)" -Level "WARN"
    }
    
    # Clean the application directory (but keep the directory itself)
    Write-Log "Cleaning application directory..."
    try {
        Get-ChildItem -Path $AppPath -Force | ForEach-Object {
            Write-Log "Removing: $($_.Name)"
            if ($_.PSIsContainer) {
                Remove-Item $_.FullName -Recurse -Force
            } else {
                Remove-Item $_.FullName -Force
            }
        }
        Write-Log "Application directory cleaned"
    } catch {
        Write-Log "Warning: Some files could not be removed: $($_.Exception.Message)" -Level "WARN"
        # Continue anyway - the -Force flag on Expand-Archive should overwrite
    }
    
    # Add Defender exclusions for app path and update folder
    $UpdateFolder = Split-Path -Path $UpdateFilePath -Parent
    Write-Log "Adding Defender exclusions for app path and update folder..."
    Write-Log "Excluding app path: $AppPath"
    Write-Log "Excluding update folder: $UpdateFolder"
    Add-MpPreference -ExclusionPath $AppPath
    Add-MpPreference -ExclusionPath $UpdateFolder

    # Extract update
    Write-Log "Extracting update files to clean directory..."
    Expand-Archive -Path $UpdateFilePath -DestinationPath $AppPath -Force
    Write-Log "Files extracted successfully"
    
    # Start service
    Write-Log "Starting service '$ServiceName'..."
    Start-Service -Name $ServiceName
    
    # Brief check
    Start-Sleep -Seconds 3
    $service = Get-Service -Name $ServiceName
    Write-Log "Service status after start: $($service.Status)"
    
    # Cleanup update file but keep temp directory and logs
    Write-Log "Cleaning up update file..."
    Remove-Item $UpdateFilePath -Force -ErrorAction SilentlyContinue
    
    Write-Log "Update completed successfully!" -Level "SUCCESS"
    Write-Log "Backup created at: $backupPath"
    Write-Log "Temp directory preserved for debugging: $TempDir"
    Write-Log "Log file location: $LogFile"
    
} catch {
    Write-Log "Update failed: $($_.Exception.Message)" -Level "ERROR"
    Write-Log "Stack trace: $($_.Exception.StackTrace)" -Level "ERROR"
    
    # Try to start the service anyway
    try {
        Write-Log "Attempting to start service after failure..." -Level "WARN"
        Start-Service -Name $ServiceName -ErrorAction SilentlyContinue
        $service = Get-Service -Name $ServiceName
        Write-Log "Service status after recovery attempt: $($service.Status)" -Level "WARN"
    } catch {
        Write-Log "Failed to start service: $($_.Exception.Message)" -Level "ERROR"
    }
    
    Write-Log "Update process completed with errors. Check log file: $LogFile" -Level "ERROR"
    exit 1
}
```

## /guest_server/securekey.go

```go path="/guest_server/securekey.go" 
package main

import (
	"fmt"
	"unsafe"

	"golang.org/x/sys/windows"
	"golang.org/x/sys/windows/registry"
)

var (
	advapi32                  = windows.NewLazySystemDLL("advapi32.dll")
	procSetNamedSecurityInfoW = advapi32.NewProc("SetNamedSecurityInfoW")
	procSetEntriesInAclW      = advapi32.NewProc("SetEntriesInAclW")
)

const (
	// Using a protected location under HKLM
	baseRegistryPath = `SOFTWARE\WinBoatSecureStore`
)

// setSecureRegKey stores a value that only SYSTEM can modify
// Must be run as NT AUTHORITY\SYSTEM
func setSecureRegKey(k string, v string) error {
	// Open/Create the registry key first with default permissions
	key, _, err := registry.CreateKey(registry.LOCAL_MACHINE, baseRegistryPath, registry.ALL_ACCESS)
	if err != nil {
		return fmt.Errorf("failed to create registry key: %w", err)
	}
	defer key.Close()

	// Write the value
	if err := key.SetStringValue(k, v); err != nil {
		return fmt.Errorf("failed to set registry value: %w", err)
	}

	// Now lock it down - get SYSTEM SID
	systemSID, err := windows.StringToSid("S-1-5-18")
	if err != nil {
		return fmt.Errorf("failed to get SYSTEM SID: %w", err)
	}

	// Get Administrators SID
	adminsSID, err := windows.StringToSid("S-1-5-32-544")
	if err != nil {
		return fmt.Errorf("failed to get Administrators SID: %w", err)
	}

	// Get Everyone SID
	everyoneSID, err := windows.StringToSid("S-1-1-0")
	if err != nil {
		return fmt.Errorf("failed to get Everyone SID: %w", err)
	}

	// Create ACL:
	// 1. SYSTEM gets FULL CONTROL (KEY_ALL_ACCESS)
	// 2. Everyone gets READ (KEY_READ) - so your service can read it back
	// 3. Admins explicitly DENIED write access
	explicitAccess := []windows.EXPLICIT_ACCESS{
		{
			AccessPermissions: windows.KEY_ALL_ACCESS,
			AccessMode:        windows.GRANT_ACCESS,
			Inheritance:       windows.CONTAINER_INHERIT_ACE | windows.OBJECT_INHERIT_ACE,
			Trustee: windows.TRUSTEE{
				TrusteeForm:  windows.TRUSTEE_IS_SID,
				TrusteeType:  windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
				TrusteeValue: windows.TrusteeValueFromSID(systemSID),
			},
		},
		{
			AccessPermissions: windows.KEY_READ,
			AccessMode:        windows.GRANT_ACCESS,
			Inheritance:       windows.CONTAINER_INHERIT_ACE | windows.OBJECT_INHERIT_ACE,
			Trustee: windows.TRUSTEE{
				TrusteeForm:  windows.TRUSTEE_IS_SID,
				TrusteeType:  windows.TRUSTEE_IS_WELL_KNOWN_GROUP,
				TrusteeValue: windows.TrusteeValueFromSID(everyoneSID),
			},
		},
		{
			AccessPermissions: windows.KEY_WRITE | windows.KEY_SET_VALUE | windows.DELETE | windows.WRITE_DAC | windows.WRITE_OWNER,
			AccessMode:        windows.DENY_ACCESS,
			Inheritance:       windows.CONTAINER_INHERIT_ACE | windows.OBJECT_INHERIT_ACE,
			Trustee: windows.TRUSTEE{
				TrusteeForm:  windows.TRUSTEE_IS_SID,
				TrusteeType:  windows.TRUSTEE_IS_GROUP,
				TrusteeValue: windows.TrusteeValueFromSID(adminsSID),
			},
		},
	}

	var acl *windows.ACL
	ret, _, err := procSetEntriesInAclW.Call(
		uintptr(len(explicitAccess)),
		uintptr(unsafe.Pointer(&explicitAccess[0])),
		0, // oldAcl is nil
		uintptr(unsafe.Pointer(&acl)),
	)
	if ret != 0 {
		return fmt.Errorf("failed to create ACL: %w", err)
	}
	defer windows.LocalFree(windows.Handle(unsafe.Pointer(acl)))

	// Apply the security descriptor to the registry key
	regPath := `MACHINE\` + baseRegistryPath
	regPathW, err := windows.UTF16PtrFromString(regPath)
	if err != nil {
		return fmt.Errorf("failed to convert path: %w", err)
	}

	ret, _, err = procSetNamedSecurityInfoW.Call(
		uintptr(unsafe.Pointer(regPathW)),
		uintptr(windows.SE_REGISTRY_KEY),
		uintptr(windows.DACL_SECURITY_INFORMATION|windows.OWNER_SECURITY_INFORMATION|windows.PROTECTED_DACL_SECURITY_INFORMATION),
		uintptr(unsafe.Pointer(systemSID)),
		0,
		uintptr(unsafe.Pointer(acl)),
		0,
	)

	if ret != 0 {
		return fmt.Errorf("SetNamedSecurityInfoW failed: %w", err)
	}

	return nil
}

// getSecureRegKey retrieves a stored value
// Returns nil if the key or value doesn't exist
func getSecureRegKey(k string) (*string, error) {
	key, err := registry.OpenKey(registry.LOCAL_MACHINE, baseRegistryPath, registry.QUERY_VALUE)
	if err != nil {
		// Registry key doesn't exist yet
		if err == registry.ErrNotExist {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to open registry key: %w", err)
	}
	defer key.Close()

	value, _, err := key.GetStringValue(k)
	if err != nil {
		// Value doesn't exist
		if err == registry.ErrNotExist {
			return nil, nil
		}
		return nil, fmt.Errorf("failed to read registry value: %w", err)
	}

	return &value, nil
}

```

## /guest_server/util.go

```go path="/guest_server/util.go" 
//go:build windows
// +build windows

package main

import (
	"log"
	"os"
)

func checkErr(err error) {
	if err != nil {
		log.Fatal(err)
	}
}

// Copy a file from src to dst
func copyFile(src string, dst string) {
	info, err := os.Stat(src)
	checkErr(err)

	// Read all content of src to data, may cause OOM for a large file.
	data, err := os.ReadFile(src)
	checkErr(err)
	// Write data to dst
	err = os.WriteFile(dst, data, info.Mode())
	checkErr(err)
}

```

## /icons/winboat_logo.svg

```svg path="/icons/winboat_logo.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1080" height="1080"><defs><linearGradient id="b"><stop offset="0" stop-color="#3c88f5"/><stop offset="1" stop-color="#4e77eb"/></linearGradient><linearGradient id="a"><stop offset="0" stop-color="#7550dc"/><stop offset=".299" stop-color="#7550dc"/><stop offset="1" stop-color="#3e87f5"/></linearGradient><linearGradient xlink:href="#a" id="c" x1="814.435" x2="399.131" y1="147.145" y2="814.537" gradientTransform="translate(4.012 -8.11)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#b" id="d" x1="97.108" x2="962.926" y1="915.934" y2="915.934" gradientTransform="translate(4.012 -8.11)" gradientUnits="userSpaceOnUse"/><linearGradient xlink:href="#b" id="e" x1="97.108" x2="962.926" y1="915.934" y2="915.934" gradientTransform="translate(4.012 78.458)" gradientUnits="userSpaceOnUse"/></defs><path fill="url(#c)" d="M276.737 42.338c-15.982.206-29.211 4.524-39.549 14.096-11.453 10.603-13.365 17.301-12.062 42.252.824 15.795 4.033 80.831 9.052 183.455l2.239 45.75h344.595v-2.975c0-5.757-5.708-104.161-9.619-179.025-2.032-38.895-1.529-53.39-2.803-53.643-100.284-19.846-181.662-36.16-274.959-48.787-5.934-.803-11.567-1.192-16.894-1.123m360.656 62.851c-.193.206 12.262 221.996 12.492 222.452.07.137 48.459.25 107.532.25h107.406l-.41-3.75c-2.48-22.702-5.046-130.29-11.338-148.565-4.753-13.801-19.17-31.232-34.627-36.54-27.815-9.555-81.604-14.23-173.9-32.659-3.831-.765-7.05-1.298-7.155-1.187m230.113 277.87c-.53-.541-213.568 3.574-214.13 4.136-.72.72 11.66 218.633 11.644 222.553-.006 1.78.396 2.202 1.64 1.725 2.107-.808 150.11-30.84 156.801-32.082 16.086-2.988 25.99-7.923 36.098-17.985 8.073-8.036 12.4-15.771 15.008-26.83 1.706-7.235 1.665-8.537-2.5-79.271-2.329-39.553-4.382-72.064-4.56-72.246m-282.908 4.832c-88.166.146-217.61 4.71-344.463 6.586-.538.476 10.728 229.785 11.914 242.513.775 8.303 1.618 11.895 3.889 16.584 7.315 15.1 23.723 25.704 43.574 28.157 10.265 1.268 15.874.57 49-6.09 488.681-98.264-111.97 22.511 248.5-50.155 0-7.089-1.84-44.335-6.957-135.595-1.888-33.997-3.639-68-5.457-102m375.877 215c-2.539 0-9.418 1.106-15.289 2.457-190.461 43.825-728.605 151.97-743.592 164.781-5.59 4.778-7.72 9.378-7.824 16.889-.064 4.645 1.01 8.398 6.152 21.5l10.748 27.373c8.233 20.96 7.935 20.462 13.743 22.869 23.627 9.793 33.477 31.551 45.916 24.976 1.201-.635 6.77-5.524 12.377-10.865 57.863-55.124 122.299-26.23 145.806-1.955 17.564 18.137 21.448 18.356 37.573 2.111 38.658-38.946 101.203-45.568 147.214-1.123 5.068 4.895 11.076 9.904 13.354 11.133 6.486 3.5 10.424 1.813 22.248-9.533 32.379-31.07 93.011-54.016 148.68-.404 16.668 16.052 18.759 15.99 36.25-1.067 8.986-8.763 16.572-14.13 30.048-21.27 7.53-3.988 4.824.596 30.032-50.872 26.004-53.088 52.048-106.156 78.074-159.233 3.662-7.437 6.476-14.561 6.766-17.13 1.272-11.292-7.004-20.637-18.276-20.637"/><path fill="url(#d)" d="M542.192 864.585c-20.616-.692-42.49 7.635-63.37 28.264-24.95 24.651-43.183 18.238-62.89-.707-56.52-54.332-105.68-14.323-121.752 2.105-24.796 25.344-46.797 11.372-55.668 3.53-20.686-18.287-40.176-26.234-64.5-26.297-21.057-.055-39.89 5.89-55.768 17.603-16.326 12.044-20.282 20.719-14.808 32.475 3.449 7.407 10.87 12.334 18.578 12.334 6.383 0 9.483-1.432 17.955-8.297 28.368-22.984 55.782-10.271 69.516 2.324 31.026 28.458 76.405 32.951 113.027-2.918 19.075-19.65 39.808-25.429 61.064-5.623 19.243 19.001 28.862 24.929 47.7 29.387 6.645 1.573 25.948 1.407 33.736-.289 16.764-3.65 26.21-9.348 44.764-27.004 25.934-24.68 43.815-15.813 62.736 2.555 10.45 10.144 12.162 11.539 19.406 15.81 11.797 6.956 23.79 10.053 38.904 10.053 25.248-.608 43.835-9.23 60.809-27.096 24.237-23.237 40.514-20.44 62.88 1.082 57.877 55.69 108.781 10.585 120.12-.99 14.645-16.24 35.598-20.929 54.125-7.24 10.132 8.273 14.799 9.661 23.604 7.023 5.727-1.715 12.165-8.19 13.644-13.722 2.893-10.815-.797-18.729-13.008-27.895-25.943-19.475-60.416-21.217-88.244-4.46-4.888 2.943-12.012 8.732-18.414 14.962-21.103 20.54-38.496 25.695-63.978.664-33.864-33.263-66.547-28.973-82.348-23.87-15.21 4.91-24.233 10.734-38.254 24.689-26.79 26.662-45.797 14.892-63.746-2.602-15.847-15.446-35.205-25.157-55.82-25.85"/><path fill="url(#e)" d="M542.192 951.152c-20.616-.692-42.49 7.635-63.37 28.264-24.95 24.651-43.183 18.238-62.89-.707-56.52-54.332-105.68-14.323-121.752 2.105-24.796 25.345-46.797 11.372-55.668 3.53-20.686-18.287-40.176-26.234-64.5-26.297-21.057-.055-39.89 5.89-55.768 17.603-16.326 12.044-20.282 20.719-14.808 32.475 3.448 7.407 10.87 12.334 18.578 12.334 6.383 0 9.483-1.432 17.955-8.297 28.368-22.985 55.782-10.271 69.515 2.324 31.027 28.458 76.406 32.952 113.028-2.918 19.075-19.65 39.808-25.429 61.064-5.623 19.242 19.002 28.862 24.929 47.7 29.387 6.645 1.573 25.948 1.407 33.736-.289 16.764-3.65 26.21-9.348 44.764-27.004 25.934-24.68 43.815-15.813 62.736 2.555 10.45 10.144 12.162 11.539 19.406 15.81 11.797 6.956 23.79 10.053 38.904 10.053 25.248-.608 43.835-9.23 60.809-27.096 24.237-23.237 40.514-20.44 62.88 1.082 57.877 55.69 108.781 10.585 120.12-.99 14.645-16.24 35.598-20.929 54.125-7.24 10.132 8.273 14.799 9.661 23.604 7.023 5.727-1.715 12.165-8.19 13.644-13.722 2.893-10.815-.797-18.729-13.008-27.895-25.943-19.475-60.416-21.217-88.244-4.46-4.888 2.943-12.012 8.732-18.414 14.962-21.103 20.54-38.496 25.695-63.978.664-33.864-33.263-66.547-28.973-82.348-23.87-15.21 4.91-24.233 10.734-38.254 24.689-26.79 26.662-45.797 14.892-63.746-2.602-15.847-15.446-35.205-25.157-55.82-25.85"/></svg>
```

## /package.json

```json path="/package.json" 
{
    "name": "winboat",
    "version": "0.9.0",
    "description": "Windows for Penguins",
    "type": "module",
    "main": "main/main.js",
    "engines": {
        "node": ">=23.6.0",
        "bun": ">=1.3.0"
    },
    "scripts": {
        "dev": "bun scripts/dev-server.ts",
        "build:gs": "bash build-guest-server.sh",
        "build:linux-gs": "bash build-guest-server.sh && bun scripts/build.ts && electron-builder --linux"
    },
    "repository": "https://github.com/TibixDev/winboat",
    "author": {
        "name": "TibixDev",
        "url": "https://github.com/TibixDev",
        "email": "staff@winboat.app"
    },
    "devDependencies": {
        "@types/node-fetch": "^2.6.13",
        "@types/turndown": "^5.0.6",
        "@vitejs/plugin-vue": "^6.0.4",
        "autoprefixer": "^10.4.24",
        "chalk": "^5.6.2",
        "chokidar": "^5.0.0",
        "electron": "^40.2.1",
        "electron-builder": "^26.7.0",
        "postcss": "^8.5.6",
        "prettier": "3.8.1",
        "tailwindcss": "^3.4.17",
        "typescript": "~5.9.3",
        "vite": "^7.3.1",
        "vue-tsc": "^3.2.4"
    },
    "dependencies": {
        "@electron/remote": "^2.1.3",
        "@iconify/vue": "^5.0.0",
        "@vueuse/core": "^14.2.0",
        "@vueuse/motion": "^3.0.3",
        "apexcharts": "^5.3.6",
        "argon2": "^0.44.0",
        "check-disk-space": "^3.4.0",
        "consola": "^3.4.2",
        "electron-store": "^11.0.2",
        "form-data": "^4.0.5",
        "jimp": "^1.6.0",
        "marked": "^17.0.1",
        "nanoevents": "^9.1.0",
        "node-fetch": "2",
        "path-browserify": "^1.0.1",
        "turndown": "^7.2.2",
        "usb": "^2.17.0",
        "vue": "^3.5.27",
        "vue-router": "^5.0.2",
        "vue3-apexcharts": "^1.10.0",
        "xel": "^0.40.3",
        "yaml": "^2.8.2"
    },
    "patchedDependencies": {
        "usb@2.17.0": "patches/usb@2.17.0.patch"
    }
}

```

## /patches/usb@2.17.0.patch

```patch path="/patches/usb@2.17.0.patch" 
diff --git a/binding.gyp b/binding.gyp
index db98bb83b50ecc75aea7ccddd9a20c99d3494c13..7f86dfd5c5511f5c331a14e47119e5f9a34f5cbe 100644
--- a/binding.gyp
+++ b/binding.gyp
@@ -32,7 +32,7 @@
         'src/thread_name.cc'
       ],
       'cflags_cc': [
-        '-std=c++14'
+        '-std=c++17'
       ],
       'defines': [
         '_FILE_OFFSET_BITS=64',

```

## /postcss.config.ts

```ts path="/postcss.config.ts" 
export default {
    plugins: {
        tailwindcss: {},
        autoprefixer: {},
    },
};

```

## /scripts/build.ts

```ts path="/scripts/build.ts" 
import Path from "path";
import Chalk from "chalk";
import FileSystem from "fs";
import * as Vite from "vite";
import compileTs from "./private/tsc.ts";
// ^ Extension can't be omitted because Node expects it
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = Path.dirname(__filename);

function buildRenderer() {
    return Vite.build({
        configFile: Path.join(__dirname, "..", "vite.config.ts"),
        base: "./",
        mode: "production",
    });
}

function buildMain() {
    const mainPath = Path.join(__dirname, "..", "src", "main");
    return compileTs(mainPath);
}

FileSystem.rmSync(Path.join(__dirname, "..", "build"), {
    recursive: true,
    force: true,
});

console.log(Chalk.blueBright("Transpiling renderer & main..."));

Promise.allSettled([buildRenderer(), buildMain()]).then(() => {
    console.log(
        Chalk.greenBright("Renderer & main successfully transpiled! (ready to be built with electron-builder)"),
    );
});

```

## /scripts/dev-server.ts

```ts path="/scripts/dev-server.ts" 
process.env.NODE_ENV = "development";

import * as Vite from "vite";
import ChildProcess, { type ChildProcessWithoutNullStreams } from "child_process";
import Path from "path";
import Chalk from "chalk";
import Chokidar from "chokidar";
import Electron from "electron";
import compileTs from "./private/tsc.ts";
// ^ Extension needed because no TSConfig in the root
import FileSystem from "fs";
import { EOL } from "os";
import { fileURLToPath } from "url";

const __filename = fileURLToPath(import.meta.url);
const __dirname = Path.dirname(__filename);

let viteServer: Vite.ViteDevServer | null = null;
let electronProcess: ChildProcessWithoutNullStreams | null = null;
let electronProcessLocker = false;
let rendererPort = 0;

async function startRenderer() {
    viteServer = await Vite.createServer({
        configFile: Path.join(__dirname, "..", "vite.config.ts"),
        mode: "development",
    });

    return viteServer.listen();
}

async function startElectron() {
    if (electronProcess) {
        // single instance lock
        return;
    }

    try {
        await compileTs(Path.join(__dirname, "..", "src", "main"));
    } catch {
        console.log(Chalk.redBright("Could not start Electron because of the above typescript error(s)."));
        electronProcessLocker = false;
        return;
    }

    const args = [Path.join(__dirname, "..", "build", "main", "main.js"), String(rendererPort)];

    electronProcess = ChildProcess.spawn(String(Electron), args);
    electronProcessLocker = false;

    electronProcess!.stdout.on("data", data => {
        if (data == EOL) {
            return;
        }

        process.stdout.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString()));
    });

    electronProcess!.stderr.on("data", data =>
        process.stderr.write(Chalk.blueBright(`[electron] `) + Chalk.white(data.toString())),
    );

    electronProcess!.on("exit", () => stop());
}

function restartElectron() {
    if (electronProcess) {
        electronProcess.removeAllListeners("exit");
        electronProcess.kill();
        electronProcess = null;
    }

    if (!electronProcessLocker) {
        electronProcessLocker = true;
        startElectron();
    }
}

function copyStaticFiles() {
    copy("static");
}

/*
The working dir of Electron is build/main instead of src/main because of TS.
tsc does not copy static files, so copy them over manually for dev server.
*/
function copy(path) {
    FileSystem.cpSync(
        Path.join(__dirname, "..", "src", "main", path),
        Path.join(__dirname, "..", "build", "main", path),
        { recursive: true },
    );
}

function stop() {
    viteServer!.close();
    process.exit();
}

async function start() {
    console.log(`${Chalk.greenBright("=======================================")}`);
    console.log(`${Chalk.greenBright("Starting Electron + Vite Dev Server...")}`);
    console.log(`${Chalk.greenBright("=======================================")}`);

    const devServer = await startRenderer();
    rendererPort = devServer.config.server.port;

    copyStaticFiles();
    startElectron();

    const path = Path.join(__dirname, "..", "src", "main");
    Chokidar.watch(path, {
        cwd: path,
    }).on("change", path => {
        console.log(Chalk.blueBright(`[electron] `) + `Change in ${path}. reloading... 🚀`);

        if (path.startsWith(Path.join("static", "/"))) {
            copy(path);
        }

        restartElectron();
    });
}

start();

```

## /scripts/private/tsc.ts

```ts path="/scripts/private/tsc.ts" 
import ChildProcess from "child_process";
import Chalk from "chalk";

export default function compile(directory) {
    return new Promise<void>((resolve, reject) => {
        const tscProcess = ChildProcess.exec("tsc", {
            cwd: directory,
        });

        tscProcess.stdout!.on("data", data =>
            process.stdout.write(Chalk.yellowBright(`[tsc] `) + Chalk.white(data.toString())),
        );

        tscProcess.on("exit", exitCode => {
            if (exitCode ?? 1 > 0) {
                reject(exitCode);
            } else {
                resolve();
            }
        });
    });
}

```

## /src/main/main.ts

```ts path="/src/main/main.ts" 
import { app, BrowserWindow, ipcMain, session, dialog } from "electron";
import { join } from "path";
import { initialize, enable } from "@electron/remote/main/index.js";
import Store from "electron-store";

initialize();

// Window Constants
const WINDOW_MIN_WIDTH = 1280;
const WINDOW_MIN_HEIGHT = 800;

// For electron-store Type-Safety
type SchemaType = {
    dimensions: {
        width: number;
        height: number;
    };
    position: {
        x: number;
        y: number;
    };
};

const windowStore = new Store<SchemaType>({
    schema: {
        dimensions: {
            type: "object",
            properties: {
                width: {
                    type: "number",
                    default: WINDOW_MIN_WIDTH,
                },
                height: {
                    type: "number",
                    default: WINDOW_MIN_HEIGHT,
                },
            },
            required: ["width", "height"],
        },
        position: {
            type: "object",
            properties: {
                x: {
                    type: "number",
                },
                y: {
                    type: "number",
                },
            },
            required: ["x", "y"],
        },
    },
});

let mainWindow: BrowserWindow | null = null;

function createWindow() {
    if (!app.requestSingleInstanceLock()) {
        // @ts-ignore property "window" is optional, see: [dialog.showMessageBoxSync](https://www.electronjs.org/docs/latest/api/dialog#dialogshowmessageboxsyncwindow-options)
        dialog.showMessageBoxSync(null, {
            type: "error",
            buttons: ["Close"],
            title: "WinBoat",
            message: "An instance of WinBoat is already running.\n\tMultiple Instances are not allowed.",
        });
        app.exit();
    }

    mainWindow = new BrowserWindow({
        minWidth: WINDOW_MIN_WIDTH,
        minHeight: WINDOW_MIN_HEIGHT,
        width: windowStore.get("dimensions.width"),
        height: windowStore.get("dimensions.height"),
        x: windowStore.get("position.x"),
        y: windowStore.get("position.y"),
        transparent: false,
        frame: false,
        webPreferences: {
            // preload: join(__dirname, 'preload.js'),
            nodeIntegration: true,
            contextIsolation: false,
        },
    });

    mainWindow.on("close", () => {
        const bounds = mainWindow?.getBounds();

        windowStore.set("dimensions", {
            width: bounds?.width,
            height: bounds?.height,
        });

        windowStore.set("position", {
            x: bounds?.x,
            y: bounds?.y,
        });
    });

    enable(mainWindow.webContents);

    if (process.env.NODE_ENV === "development") {
        const rendererPort = process.argv[2];
        mainWindow.loadURL(`http://localhost:${rendererPort}`);
    } else {
        mainWindow.loadFile(join(app.getAppPath(), "renderer", "index.html"));
    }
}

app.whenReady().then(() => {
    createWindow();

    session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
        callback({
            responseHeaders: {
                ...details.responseHeaders,
                // 'Content-Security-Policy': ['script-src \'self\'']
                "Content-Security-Policy": [
                    "script-src 'self' 'unsafe-eval' 'wasm-unsafe-eval' 'unsafe-inline'",
                    "worker-src 'self' blob:",
                    "media-src 'self' blob:",
                    "font-src 'self' 'unsafe-inline' https://fonts.gstatic.com;",
                    "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
                ],
            },
        });
    });

    app.on("activate", function () {
        // On macOS it's common to re-create a window in the app when the
        // dock icon is clicked and there are no other windows open.
        if (BrowserWindow.getAllWindows().length === 0) {
            createWindow();
        }
    });
});

app.on("window-all-closed", function () {
    if (process.platform !== "darwin") app.quit();
});

app.on("second-instance", _ => {
    if (mainWindow) {
        mainWindow.focus();
    }
});

ipcMain.on("message", (_event, message) => {
    console.log(message);
});

```

## /src/main/static/.gitkeep

```gitkeep path="/src/main/static/.gitkeep" 

```

## /src/main/tsconfig.json

```json path="/src/main/tsconfig.json" 
{
    "compilerOptions": {
        "target": "esnext",
        "module": "nodenext",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "outDir": "../../build/main",
        "allowJs": true,
        "noImplicitAny": false
    },
    "exclude": ["static"]
}

```

## /src/renderer/App.vue

```vue path="/src/renderer/App.vue" 
<template>
    <main 
        class="overflow-hidden relative w-screen h-screen"
        :class="{ animationsDisabled: 'disable-animations' }"
    >
        <!-- Decoration -->
        <div
            class="gradient-ball absolute -z-10 left-0 bottom-0 translate-x-[-50%] translate-y-[50%] w-[90vw] aspect-square opacity-15 blob-anim"
        ></div>
        <div
            class="gradient-ball absolute -z-10 right-0 top-0 translate-x-[50%] translate-y-[-50%] w-[90vw] aspect-square opacity-15 blob-anim"
        ></div>

        <!-- Stripes for experimental -->
        <div
            v-show="wbConfig?.config.experimentalFeatures"
            class="experimental-stripes absolute top-0 left-0 w-full h-[3rem] pointer-events-none z-[10] opacity-15 grayscale"
        ></div>

        <!-- Titlebar -->
        <x-titlebar
            @minimize="handleMinimize()"
            @buttonclick="handleTitleBarEvent"
            class="backdrop-blur-xl bg-neutral-900/50"
        >
            <x-label>WinBoat</x-label>
        </x-titlebar>

        <!-- Updater -->
        <dialog ref="updateDialog">
            <Icon class="text-indigo-400 size-12" icon="mdi:cloud-upload"></Icon>
            <template v-if="manualUpdateRequired">
                <h3 class="mt-2">Manual Guest Server Update Required</h3>
                <div class="max-w-[60vw]">
                    <strong
                        >WinBoat has encountered an issue while trying to update the Guest Server automatically. Please
                        follow the steps below to manually update it:</strong
                    >
                    <ol class="mt-2 list-decimal list-inside">
                        <li>
                            Use VNC over at
                            <a @click="openAnchorLink" :href="novncURL" target="_blank" rel="noopener noreferrer">
                                {{ novncURL }}
                            </a>
                            to access Windows
                        </li>
                        <li>Press Win + R or search for <code>Run</code>, type in <code>services.msc</code></li>
                        <li>Stop the <code>WinBoatGuestServer</code> service by right clicking and pressing "Stop"</li>
                        <li>
                            Download the new Guest Server from
                            <a
                                @click="openAnchorLink"
                                href="https://github.com/TibixDev/winboat/releases"
                                target="_blank"
                                rel="noopener noreferrer"
                            >
                                https://github.com/TibixDev/winboat/releases
                            </a>
                            , you should pick version <strong>{{ appVer }}</strong>
                        </li>
                        <li>Navigate to <code>C:\Program Files\WinBoat</code> and delete the contents</li>
                        <li>Extract the freshly downloaded zip into the same folder</li>
                        <li>
                            Start the <code>WinBoatGuestServer</code> service by right clicking and pressing "Start"
                        </li>
                        <li>If you were using VNC, log out of Windows and close it</li>
                        <li>Restart WinBoat</li>
                    </ol>
                    <p>We're sorry for the inconvenience. 😟</p>
                </div>
            </template>

            <template v-else>
                <h3 class="mt-2" v-if="winboat?.isUpdatingGuestServer.value">Updating Guest Server</h3>
                <h3 class="mt-2" v-else>Guest Server update successful!</h3>
                <p v-if="winboat?.isUpdatingGuestServer.value" class="max-w-[40vw]">
                    The guest is currently running an outdated version of the WinBoat Guest Server. Please wait while we
                    update it to the current version.
                </p>
                <p v-else class="max-w-[40vw]">
                    The WinBoat Guest Server has been updated successfully! You can now close this dialog and continue
                    using the application.
                </p>
            </template>
            <footer v-if="!manualUpdateRequired">
                <x-progressbar v-if="winboat?.isUpdatingGuestServer.value" class="my-4"></x-progressbar>
                <x-button v-else id="close-button" @click="updateDialog!.close()" toggled>
                    <x-label>Close</x-label>
                </x-button>
            </footer>
        </dialog>

        <!-- UI / SetupUI -->
        <div
            v-if="!['SetupUI', 'Migration'].includes($route.name?.toString() || '')"
            class="flex flex-row h-[calc(100vh-2rem)]"
        >
            <x-nav class="flex flex-col flex-none gap-0.5 w-72 backdrop-blur-xl bg-gray-500/10 backdrop-contrast-90">
                <div
                    v-if="winboat?.rdpConnected.value"
                    class="w-full bg-gradient-to-r from-indigo-500 via-indigo-400 to-blue-500 text-white !mt-0 py-1 shadow-md shadow-indigo-500/50 transition-all duration-300 hover:brightness-105 flex flex-row items-center justify-center gap-2"
                >
                    <Icon class="size-5" icon="mdi:remote-desktop"></Icon>
                    <span class="font-semibold text-center"> RDP Session Active </span>
                </div>
                <div class="flex flex-row gap-4 items-center p-4">
                    <img
                        class="w-16 rounded-full"
                        src="/img/pfp.svg"
                        alt="Profile"
                    />
                    <div>
                        <x-label class="text-lg font-semibold">{{ os.userInfo().username }}</x-label>
                        <x-label class="text-[0.8rem]">Local Account</x-label>
                    </div>
                </div>
                <RouterLink
                    v-for="route of routes.filter(
                        (r: RouteRecordRaw) => !['SetupUI', 'Loading', 'Migration'].includes(String(r.name)),
                    )"
                    :to="route.path"
                    :key="route.path"
                >
                    <x-navitem>
                        <Icon class="mr-4 w-5 h-5" :icon="(route.meta!.icon as string)" />
                        <x-label>{{ route.name }}</x-label>
                    </x-navitem>
                </RouterLink>
                <div class="flex flex-col justify-end items-center p-4 h-full">
                    <p class="text-xs text-neutral-500">WinBoat Beta v{{ appVer }} {{ isDev ? "Dev" : "Prod" }}</p>
                </div>
            </x-nav>
            <div class="px-5 flex-grow max-h-[calc(100vh-2rem)] overflow-y-auto py-4">
                <div class="flex flex-row gap-2 items-center my-6">
                    <Icon class="w-6 h-6 opacity-60" icon="icon-park-solid:toolkit"></Icon>
                    <h1 class="my-0 text-2xl font-semibold opacity-60">WinBoat</h1>
                    <Icon class="w-6 h-6" icon="bitcoin-icons:caret-right-filled"></Icon>
                    <Icon class="w-6 h-6" :icon="useRoute().meta.icon as string"></Icon>
                    <h1 class="my-0 text-2xl font-semibold">
                        {{ useRoute().name }}
                    </h1>
                </div>
                <router-view v-slot="{ Component }">
                    <transition mode="out-in" name="fade">
                        <component :is="Component" />
                    </transition>
                </router-view>
            </div>
        </div>

        <div v-else class="w-full h-[calc(100vh-2rem)]">
            <RouterView />
        </div>
    </main>
</template>

<script setup lang="ts">
import { RouteRecordRaw, RouterLink, useRoute, useRouter } from "vue-router";
import { routes } from "./router";
import { Icon } from "@iconify/vue";
import { onMounted, ref, useTemplateRef, watch, reactive, computed } from "vue";
import { isInstalled } from "./lib/install";
import { Winboat } from "./lib/winboat";
import { openAnchorLink } from "./utils/openLink";
import { WinboatConfig } from "./lib/config";
import { USBManager } from "./lib/usbmanager";
import { CommonPorts, getActiveHostPort } from "./lib/containers/common";
import { performAutoMigrations } from "./lib/migrate";
const { BrowserWindow }: typeof import("@electron/remote") = require("@electron/remote");
const os: typeof import("os") = require("node:os");

const $router = useRouter();
const $route = useRoute();
const appVer = import.meta.env.VITE_APP_VERSION;
const isDev = import.meta.env.DEV;
let winboat: Winboat | null;
let wbConfig: WinboatConfig | null;

let updateTimeout: NodeJS.Timeout | null = null;
const manualUpdateRequired = ref(false);
const MANUAL_UPDATE_TIMEOUT = 60000; // 60 seconds
const updateDialog = useTemplateRef("updateDialog");
const novncURL = ref("");

const animationsDisabled = computed(() => wbConfig?.config.disableAnimations);

onMounted(async () => {
    const winboatInstalled = await isInstalled();

    if (winboatInstalled) {
        wbConfig = reactive(WinboatConfig.getInstance()); // Instantiate singleton class
        winboat = Winboat.getInstance(); // Instantiate singleton class
        USBManager.getInstance(); // Instantiate singleton class

        // Migrations
        $router.push("/migration");
        await performAutoMigrations();

        // After migrations, go to home
        $router.push("/home");
    } else {
        console.log("Not installed, redirecting to setup...");
        $router.push("/setup");
    }

    // Watch for guest server updates and show dialog
    watch(
        () => winboat?.isUpdatingGuestServer.value,
        isUpdating => {
            if (isUpdating === true) {
                novncURL.value = `http://127.0.0.1:${getActiveHostPort(winboat?.containerMgr!, CommonPorts.NOVNC)}`;
                updateDialog.value!.showModal();
                // Prepare the timeout to show manual update required after 45 seconds
                updateTimeout = setTimeout(() => {
                    manualUpdateRequired.value = true;
                }, MANUAL_UPDATE_TIMEOUT);
            } else {
                // Clear the timeout if the update finished before the timeout
                if (updateTimeout) {
                    clearTimeout(updateTimeout);
                    updateTimeout = null;
                }
                manualUpdateRequired.value = false;
            }
        },
    );
});

function handleMinimize() {
    console.log("Minimize");
    window.electronAPI.minimizeWindow();
}

function handleTitleBarEvent(e: CustomEvent) {
    console.log("TitleBarEvt", e);
    switch (e.detail) {
        case "close":
            BrowserWindow.getFocusedWindow()!.close();
            break;
        case "maximize":
            if (BrowserWindow.getFocusedWindow()!.isMaximized()) {
                BrowserWindow.getFocusedWindow()!.unmaximize();
            } else {
                BrowserWindow.getFocusedWindow()!.maximize();
            }
            break;
        case "minimize":
            BrowserWindow.getFocusedWindow()!.minimize();
            break;
    }
}
</script>

<style>
dialog::backdrop {
    pointer-events: none;
    backdrop-filter: blur(8px);
}

.gradient-ball {
    border-radius: 99999px;
    background:
        linear-gradient(197.37deg, #7450db -0.38%, rgba(138, 234, 240, 0) 101.89%),
        linear-gradient(115.93deg, #3e88f6 4.86%, rgba(62, 180, 246, 0.33) 38.05%, rgba(62, 235, 246, 0) 74.14%),
        radial-gradient(
            56.47% 76.87% at 6.92% 7.55%,
            rgba(62, 136, 246, 0.7) 0%,
            rgba(62, 158, 246, 0.182) 52.16%,
            rgba(62, 246, 246, 0) 100%
        ),
        linear-gradient(306.53deg, #2ee4e3 19.83%, rgba(46, 228, 227, 0) 97.33%);
    background-blend-mode: normal, normal, normal, normal, normal, normal;
    filter: blur(200px);
}

@keyframes blob {
    from {
        filter: hue-rotate(0deg) blur(200px);
    }
    to {
        filter: hue-rotate(45deg) blur(200px);
    }
}

.blob-anim {
    animation: blob 5s linear infinite;
    animation-direction: alternate-reverse;
}

.fade-enter-active,
.fade-leave-active {
    transition: all 0.2s ease;
}

.fade-enter-from {
    opacity: 0;
}

.fade-leave-to {
    opacity: 0;
}

/* Stripes for the top of the window to indicate experimental features enabled */
.experimental-stripes {
    background: repeating-linear-gradient(
        45deg,
        #ffffff00,
        #ffffff00 25px,
        rgb(129 140 248) 25px,
        rgb(129 140 248) 50px
    );
    -webkit-mask-image: -webkit-gradient(linear, left 0%, left bottom, from(rgba(0, 0, 0, 1)), to(rgba(0, 0, 0, 0)));
}

/* Disable all animations when the setting is enabled */
body.disable-animations,
body.disable-animations *,
body.disable-animations *::before,
body.disable-animations *::after {
    animation: none !important;
    transition: none !important;
}

/* Specifically disable Vue transition components */
body.disable-animations .fade-enter-active,
body.disable-animations .fade-leave-active,
body.disable-animations .devices-move,
body.disable-animations .devices-enter-active,
body.disable-animations .devices-leave-active,
body.disable-animations .menu-move,
body.disable-animations .menu-enter-active,
body.disable-animations .menu-leave-active,
body.disable-animations .apps-move,
body.disable-animations .apps-enter-active,
body.disable-animations .apps-leave-active,
body.disable-animations .bounce-enter-active,
body.disable-animations .bounce-leave-active,
body.disable-animations .bouncedown-enter-active,
body.disable-animations .bouncedown-leave-active,
body.disable-animations .bounce-in,
body.disable-animations .bouncedown-in {
    transition: none !important;
    animation: none !important;
}

/* Disable keyframe animations */
body.disable-animations .blob-anim {
    animation: none !important;
}
</style>

```

## /src/renderer/assets/LICENSE.txt

MIT License

Copyright (c) 2026 TibixDev

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.


## /src/renderer/assets/podman.svg

```svg path="/src/renderer/assets/podman.svg" 
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
   width="24"
   height="24"
   viewBox="0 0 24 24"
   version="1.1"
   id="svg1"
   xml:space="preserve"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg"><defs
     id="defs1"><mask
       maskUnits="userSpaceOnUse"
       id="mask-powermask-path-effect46"><path
         id="mask-powermask-path-effect46_box"
         style="fill:#ffffff;fill-opacity:1"
         d="M -0.39081344,-0.57958372 H 24.37049 V 20.943075 H -0.39081344 Z" /><g
         id="g46"
         style=""><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 8.0798005,13.326682 c 0,0 1.2269327,-1.077306 2.4139655,-1.107231"
           id="path32" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 9.3366584,15.411471 c 0,0 0.8079796,-1.905237 1.8952616,-2.304239"
           id="path33" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 16.478803,15.361596 c 0,0 -1.147132,-2.164589 -2.074813,-2.304239"
           id="path34" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 17.316708,14.254364 c 0,0 -1.556109,-1.566085 -2.394015,-1.625935"
           id="path35" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 17.685786,13.047382 c 0,0 -1.566085,-0.927681 -2.533666,-0.897756"
           id="path36" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 8.478803,14.573566 c 0,0 1.0773067,-1.57606 2.36409,-1.965087"
           id="path37" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 15.700945,7.3214647 c 0,0 0.860519,-0.8605189 1.763358,-0.028214"
           id="path38" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 7.9421669,7.5753884 c 0,0 0.8040915,-1.0298014 1.7774655,-0.1692825"
           id="path39" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 7.6844051,13.87089 c 0,0 -3.6219568,-2.085638 -4.6792269,2.105762 C 2.1815704,19.24173 -13.645393,12.806516 -13.645393,12.806516"
           id="path40" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 14.00812,11.793342 c -0.06451,1.057653 -2.09416,0.174199 -0.691237,-0.775878 0.371421,-0.251531 0.331495,-0.901928 -0.451419,-0.916946 -0.873419,-0.01675 -1.005968,0.637315 -0.606595,0.924 1.436789,1.031382 -0.522447,1.771486 -0.627757,0.797038"
           id="path41" /><ellipse
           style="fill:#ff0000;fill-opacity:0;stroke:#000000;stroke-width:0.544331;stroke-dasharray:none;stroke-opacity:1"
           id="ellipse41"
           cx="12.820312"
           cy="11.390624"
           rx="2.3946545"
           ry="2.1211736"
           d="m 15.214966,11.390624 a 2.3946545,2.1211736 0 0 1 -2.394654,2.121174 2.3946545,2.1211736 0 0 1 -2.394655,-2.121174 2.3946545,2.1211736 0 0 1 2.394655,-2.1211736 2.3946545,2.1211736 0 0 1 2.394654,2.1211736 z" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 8.3427759,16.306928 c 0,0 -0.4907848,2.826626 -8.6901107,6.85542"
           id="path42" /><ellipse
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.655915;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           id="ellipse42"
           cx="8.9437943"
           cy="9.5238447"
           rx="1.2533367"
           ry="1.2588608"
           d="M 10.197131,9.5238447 A 1.2533367,1.2588608 0 0 1 8.9437943,10.782706 1.2533367,1.2588608 0 0 1 7.6904576,9.5238447 1.2533367,1.2588608 0 0 1 8.9437943,8.2649839 1.2533367,1.2588608 0 0 1 10.197131,9.5238447 Z" /><ellipse
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.694669;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           id="ellipse43"
           cx="16.804838"
           cy="9.4630756"
           rx="1.2323118"
           ry="1.2378361"
           d="M 18.03715,9.4630756 A 1.2323118,1.2378361 0 0 1 16.804838,10.700912 1.2323118,1.2378361 0 0 1 15.572526,9.4630756 1.2323118,1.2378361 0 0 1 16.804838,8.2252395 1.2323118,1.2378361 0 0 1 18.03715,9.4630756 Z" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 5.4840498,12.283146 c 0,0 -1.9385366,-6.7693493 4.3683931,-9.0011108 C 15.168453,1.4009194 17.68593,4.6346129 17.68593,4.6346129"
           id="path43" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.54375;stroke-linecap:round;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1;paint-order:normal"
           d="m 18.71875,15.608034 c 0,0 -1.539904,3.445464 -8.431021,6.604585"
           id="path44" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
           d="m 22.583223,8.0654367 c 0,0 -0.204383,-2.8681766 -1.451266,-4.0466778"
           id="path45" /><path
           style="fill:#000000;fill-opacity:0;stroke:#000000;stroke-width:0.5;stroke-linecap:round;stroke-dasharray:none;stroke-opacity:1"
           d="m 20.269531,8.515625 c 0,0 0.367187,-0.9609375 -0.07031,-2.0546875 -0.4375,-1.09375 -0.609375,-1.28125 -0.59375,-2.2578125"
           id="path46" /></g></mask></defs><path
     fill="currentColor"
     d="M 10.326733,3.07004 C 9.6733199,3.1189708 7.6032015,4.8196963 6.8943526,4.9966005 4.7600998,5.5292361 1.4810383,7.2257839 1.790227,12.669585 c 0.2008487,3.536288 1.950066,2.75927 0.8141564,4.129262 -5.1028776,2.993955 0.8763793,4.246302 3.5772265,1.985217 4.0674111,1.984542 9.2467651,0.496735 11.0838681,-1.538548 0.304897,-0.337788 1.137388,0.732437 1.795101,1.977581 0.473697,0.896776 5.177363,1.802794 0.657051,-4.414883 -0.325948,-0.44834 -0.06649,-0.625777 0.06952,-1.420027 0.284793,-1.663139 2.996816,-1.377977 3.525185,-4.8378246 C 23.653977,6.313234 22.373568,5.8116915 22.495967,6.3636216 22.64441,7.0329934 21.998891,5.9165644 21.670667,5.242359 20.855383,3.5676853 19.737925,4.1283869 19.812322,4.6139199 19.882309,5.0706715 19.474329,5.3484191 19.415863,4.7358761 19.3917,4.4827231 19.355221,3.5000513 19.362999,3.3027198 19.491686,0.0379417 17.648246,-0.52646089 17.096902,1.9846867 16.628777,4.1168034 16.84504,3.6407824 15.321696,3.2219451 10.93447,2.015695 11.829667,2.9574931 10.326733,3.07004 Z"
     id="path5"
     mask="url(#mask-powermask-path-effect46)" /></svg>

```

## /src/renderer/assets/vue.svg

```svg path="/src/renderer/assets/vue.svg" 
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
```

## /src/renderer/components/ConfigCard.vue

```vue path="/src/renderer/components/ConfigCard.vue" 
<template>
    <x-card
        class="flex flex-row justify-between items-center p-2 py-3 my-0 w-full backdrop-blur-xl backdrop-brightness-150 bg-neutral-800/20"
    >
        <div>
            <div class="flex flex-row gap-2 items-center mb-2">
                <Icon class="inline-flex text-violet-400 size-8" :icon="props.icon"></Icon>
                <h1 class="my-0 text-lg font-semibold">{{ props.title }}</h1>
            </div>
            <p class="text-neutral-400 text-[0.9rem] !pt-0 !mt-0">
                <slot name="desc">{{ props.desc }}</slot>
            </p>
        </div>
        <div class="flex flex-row gap-2 justify-center items-center">
            <slot v-if="props.type === 'custom'"/>
            <template v-else-if="props.type === 'number'">
                <x-button
                    v-if="props.step"
                    type="button"
                    class="size-8 !p-0"
                    @click="() => applyStep(-props.step!)"
                >
                    <Icon icon="mdi:minus" class="size-4"></Icon>
                    <x-label class="sr-only">Subtract</x-label>
                </x-button>
                <x-input
                    class="max-w-16 text-right text-[1.1rem]"
                    :min="props.min"
                    :max="props.max"
                    :value="value"
                    v-on:keydown="(e: any) => ensureNumericInput(e)"
                    @input="(e: any) => (value = Number(/^\d+$/.exec(e.target.value)![0] || props.min))"
                    required
                />
                <x-button
                    v-if="props.step"
                    type="button"
                    class="size-8 !p-0"
                    @click="() => applyStep(props.step!)"
                >
                    <Icon icon="mdi:plus" class="size-4"></Icon>
                    <x-label class="sr-only">Add</x-label>
                </x-button>
                <p class="text-neutral-100">{{ props.unit }}</p>
            </template>
            <template v-else-if="props.type === 'dropdown'">
                <x-select
                    class="w-20"
                    @change="(e: any) => (value = e.detail.newValue)"
                >
                    <x-menu>
                        <x-menuitem v-for="(opt, key) in props.options" :value="opt" :key="key" :toggled="value === opt">
                            <x-label>{{ opt }}{{ props.unit ?? '' }}</x-label>
                        </x-menuitem>
                    </x-menu>
                </x-select>
            </template>
            <template v-else-if="props.type === 'switch'">
                <x-switch
                    :toggled="value"
                    @toggle="(_: any) => { $emit('toggle'); (value = !value) }"
                    size="large"
                />
            </template>
        </div>
    </x-card>
</template>

<script setup lang="ts">
import { Icon } from "@iconify/vue";

type PropsType = {
    /**
     * The icon displayed in the top-left corner of the card. Only accepts Iconify icon name format.
     * @example "fluent:folder-link-32-filled"
     */
    icon: string;

    /**
     * The title text displayed next to the icon.
     */
    title: string;

    /**
     * The description of the card. It will be displayed as a <p> tag.
     * In case you need more control over how the description is displayed, use the `desc` slot instead.
     */
    desc?: string;

    /**
     * Specifies the nature of the input value.
     * - `number`: Shows a numeric input with optional Add/Subtract buttons in case `step` is specified.
     * - `dropdown`: Shows a dropdown menu with values defined by the `options` prop.
     * - `switch`: Shows a toggle switch.
     * - `custom`: Shows the default slot content.
     */
    type: "number" | "dropdown" | "switch" | "custom";
    
    /**
     * The minimum accepted value in case the `number` type is specified.
     */
    min?: number;

    /**
     * The maximum accepted value in case the `number` type is specified.
     */
    max?: number;

    /**
     * Specifies how much the Add/Subtract buttons change the input value.
     * Can be omitted, in which case the buttons won't be shown.
     */
    step?: number;

    /**
     * Can be used to append some text after dropdown selections or a number input.
     */
    unit?: string;

    /**
     * Defines dropdown entries in case the `dropdown` type is specified.
     */
    options?: any[];
};

const props = defineProps<PropsType>();
const value = defineModel("value");

function ensureNumericInput(e: any) {
    if (e.metaKey || e.ctrlKey || e.which <= 0 || e.which === 8 || e.key === "ArrowRight" || e.key === "ArrowLeft") {
        return;
    }

    if (!/\d/.test(e.key)) {
        e.preventDefault();
    }
}

function applyStep(step: number) {
    let tmp = Number.parseInt(value.value as string);

    if (Number.isNaN(tmp)) return;

    tmp += step;

    if(!props.min && !props.max) {
        value.value = tmp;
        return;
    }

    value.value = Math.min(Math.max(props.min ?? Number.MIN_SAFE_INTEGER, tmp), props.max ?? Number.MAX_SAFE_INTEGER);
}
</script>
```

## /src/renderer/components/WBContextMenu.vue

```vue path="/src/renderer/components/WBContextMenu.vue" 
<!--
  WBContextMenu - Replacement for broken xel contextmenu component
  This is a working reimplementation of the xel toolkit's x-contextmenu component
  which has bugs in the library that prevent proper functionality.
-->
<template>
    <!-- Context menu popup -->
    <teleport to="body">
        <div
            v-if="isVisible"
            ref="menuRef"
            class="wb-contextmenu-menu"
            :style="menuStyle"
            @click.stop="handleMenuClick"
        >
            <slot></slot>
        </div>
    </teleport>
</template>

<script setup lang="ts">
import { ref, computed, onUnmounted, nextTick } from "vue";

interface Props {
    trigger?: "contextmenu" | "click" | "none";
}

const props = withDefaults(defineProps<Props>(), {
    trigger: "contextmenu",
});

const emit = defineEmits<{
    show: [];
    hide: [];
}>();

const triggerRef = ref<HTMLElement>();
const menuRef = ref<HTMLElement>();

const isVisible = ref(false);
const menuPosition = ref({ x: 0, y: 0 });

const hasTrigger = computed(() => props.trigger !== "none");

const menuStyle = computed(() => ({
    left: `${menuPosition.value.x}px`,
    top: `${menuPosition.value.y}px`,
}));

const showMenu = (event: MouseEvent) => {
    if (props.trigger === "none") return;

    event.preventDefault();
    event.stopPropagation();

    // Position the menu slightly offset from the mouse cursor for better UX
    menuPosition.value.x = event.clientX + 2;
    menuPosition.value.y = event.clientY + 2;

    isVisible.value = true;
    emit("show");

    // prevent layout jump
    adjustPosition();
    document.addEventListener("click", hideMenu);
    document.addEventListener("contextmenu", hideMenu);
    window.addEventListener("scroll", hideMenu);
    window.addEventListener("resize", hideMenu);

    // Close on next tick to allow menu to render
    nextTick(() => {
        adjustPosition();
    });
};

const handleMenuClick = (event: MouseEvent) => {
    // Close menu when a menu item is clicked
    // The @click.stop on the menu div prevents this from firing on menu background clicks
    const target = event.target as HTMLElement;
    if (target.closest(".wb-menuitem")) {
        hideMenu();
    }
};

const hideMenu = () => {
    isVisible.value = false;
    emit("hide");

    document.removeEventListener("click", hideMenu);
    document.removeEventListener("contextmenu", hideMenu);
    window.removeEventListener("scroll", hideMenu);
    window.removeEventListener("resize", hideMenu);
};

const adjustPosition = () => {
    if (!menuRef.value) return;

    const menuRect = menuRef.value.getBoundingClientRect();
    const viewportWidth = window.innerWidth;
    const viewportHeight = window.innerHeight;

    // Adjust horizontal position if menu goes off screen
    if (menuPosition.value.x + menuRect.width > viewportWidth) {
        menuPosition.value.x = viewportWidth - menuRect.width - 10;
    }

    // Adjust vertical position if menu goes off screen
    if (menuPosition.value.y + menuRect.height > viewportHeight) {
        menuPosition.value.y = viewportHeight - menuRect.height - 10;
    }

    // Ensure minimum position
    menuPosition.value.x = Math.max(10, menuPosition.value.x);
    menuPosition.value.y = Math.max(10, menuPosition.value.y);
};

// Expose methods for programmatic control
defineExpose({
    show: showMenu,
    hide: hideMenu,
});

onUnmounted(() => {
    hideMenu();
});
</script>

<style scoped>
.wb-contextmenu-trigger {
    position: absolute;
    inset: 0;
    z-index: 1;
    cursor: context-menu;
    pointer-events: auto;
}

.wb-contextmenu-menu {
    position: fixed;
    z-index: 9999;
    background: var(--x-menu-background, #ffffff);
    border: 1px solid var(--x-menu-border, #e0e0e0);
    border-radius: var(--x-menu-border-radius, 4px);
    box-shadow: var(--x-menu-shadow, 0 4px 12px rgba(0, 0, 0, 0.15));
    padding: var(--x-menu-padding, 4px 0);
    min-width: 120px;
    max-width: 300px;
    pointer-events: auto;
}

/* Dark theme support */
@media (prefers-color-scheme: dark) {
    .wb-contextmenu-menu {
        background: var(--x-menu-background-dark, #2a2a2a);
        border-color: var(--x-menu-border-dark, #404040);
        box-shadow: var(--x-menu-shadow-dark, 0 4px 12px rgba(0, 0, 0, 0.3));
    }
}

/* Match xel theming if available */
:global(.x-theme-material) .wb-contextmenu-menu {
    background: var(--x-surface-background, #ffffff);
    border-color: var(--x-outline-color, #e0e0e0);
    border-radius: var(--x-border-radius, 4px);
}

:global(.x-theme-material.x-theme-dark) .wb-contextmenu-menu {
    background: var(--x-surface-background, #1e1e1e);
    border-color: var(--x-outline-color, #404040);
}
</style>

```

## /src/renderer/components/WBMenuItem.vue

```vue path="/src/renderer/components/WBMenuItem.vue" 
<!--
  WBMenuItem - Replacement for broken xel menuitem component
  This is a working reimplementation of the xel toolkit's x-menuitem component
  which has bugs in the library that prevent proper functionality.
-->
<template>
    <div
        class="wb-menuitem"
        :class="{ 'wb-menuitem--disabled': disabled }"
        @click="handleClick"
        @mouseenter="isHovered = true"
        @mouseleave="isHovered = false"
    >
        <slot></slot>
    </div>
</template>

<script setup lang="ts">
import { ref } from "vue";

interface Props {
    disabled?: boolean;
}

const props = withDefaults(defineProps<Props>(), {
    disabled: false,
});

const emit = defineEmits<{
    click: [event: MouseEvent];
}>();

const isHovered = ref(false);

const handleClick = (event: MouseEvent) => {
    if (props.disabled) return;

    emit("click", event);
};
</script>

<style scoped>
.wb-menuitem {
    padding: var(--x-menuitem-padding, 8px 16px);
    cursor: pointer;
    user-select: none;
    display: flex;
    align-items: center;
    gap: 8px;
    transition: background-color 0.15s ease;
    font-size: var(--x-menuitem-font-size, 14px);
    line-height: 1.4;
}

.wb-menuitem:hover:not(.wb-menuitem--disabled) {
    background-color: var(--x-menuitem-hover-background, #f0f0f0);
}

.wb-menuitem--disabled {
    cursor: not-allowed;
    opacity: 0.5;
}

/* Dark theme support */
@media (prefers-color-scheme: dark) {
    .wb-menuitem:hover:not(.wb-menuitem--disabled) {
        background-color: var(--x-menuitem-hover-background-dark, #404040);
    }
}

/* Match xel theming if available */
:global(.x-theme-material) .wb-menuitem:hover:not(.wb-menuitem--disabled) {
    background-color: var(--x-surface-variant-background, #f5f5f5);
}

:global(.x-theme-material.x-theme-dark) .wb-menuitem:hover:not(.wb-menuitem--disabled) {
    background-color: var(--x-surface-variant-background, #2a2a2a);
}
</style>

```

## /src/renderer/data/appicons.ts

```ts path="/src/renderer/data/appicons.ts" 
import { InternalApps } from "../data/internalapps";

export const DEFAULT_ICON = "DEFAULT_ICON";

export const AppIcons = {
    [InternalApps.WINDOWS_DESKTOP]:
        "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAsTAAALEwEAmpwYAAAFLUlEQVR4nO2WWVMcVRzF+RSZXpgFvw1lWT7mxaoExBgFJHswJIZFxCSCGCPikkVJTIxiojGLWwwxMXH5ArIMAzMBDDAwwLAF61i3+97uf9/uBrofFKrmVp2qefydU797e4qKCqdwCqdwNuSJbHvydGT7ckbZ/gSOPL8M9YUlqC8uQX1pEWrlAtTqBag189B2z0Pbm4e2fw7awTlotbPQDs1Cq5uBdmQG+msz0Otz0Bty0BunoTdNQ2+ehv7GFPSWKehv8hzLQj+eRfEJI2n9xGRpiALLaQd4GYffsQR15yLUlxehVi1AfWUe2q55aHvy0PbloR1g4HMcfNYEP0rAGfTrDJxAHyPAb2VR3Dpppm0SxW+zTAwHLuCCr+Dwa63+KgEXizfm3Gs7oCedwO0TKH6H56SZ8AXKlxEpn0Vk2wgiZY8QqcggsiMDtXpKWp3rclhShYLztR1LtxkL29AM+N1xM6d43hsPWaDc9J3BT839A3GycytQKoddq+uGLmuBeyx9kkNzWCMdjxF9307wAhye+R4py0A+StWQ7TpdvZE7LoOLtf2gOwhw52NEP2D5G9EPzQQuIODZZY1UpN0FalLkkkqrt3gsLoMb0OMSNAf+iOdjljFET4+FKMDgKxeNyxrZmTa0cSi0O+W8pHT149xx4bcAl9fulKAZ8GkTOHqG5yzLaIgCHJ49kWpNFkrVMJTqISi7UlD2sPXH/Vdv5Y77gTM9ZOgzAngU0XNmYp+MIvYpy0iIAhzeeGnEZRW+U2UcqxNdHKp4gZOlz7qBY10jiJ3nuRCigANeXFbhu6SMa3VxOYXjnRK4UOMcgZaAY589MnPRTPACBF49OAFlbwrq/kGotYNQDyWhNY3Zynis7nDcF3zEBr9AgC+xZBD7nOdyJkQBsjyDn8qTS5xfgXq0376oAv4U1YVfTqEKBe+Slr7IoQlw/IsM4l+mzXSnQxQg2qgHkq5nVKvvdSrDVu+QdBGO08Vd4Bk3dHca8a94rgwbCV5AOF+fg1o74C7Q1OuEp5fU0sUHXKx92QOaA8evDiP+tZ3gBawLOw21LmloQxXSWnqt59FSRl5dXE66uBf4FQn6G5YhxK/ZCVzAem2ap40Lq9b3Q2vohdb0lwGvt2dM3+lF5a47VyeqUHCx9lUP6G95rtsJXkA8lS38jTeeSemyCt/Zx4gq0+Why2rg1wg4A74xhMSNFBI37QQv4IKf9IH3UkbShapigUvQ1yXoWykkvrMTvAD7SB3jX9f1wp93uu7QRV6crn3TA/r7QSR+sBO8gAGftZ/KVeG570IZw3VpdQncWvsWAafQPw4i8ZNIMngB6wsrnsp1wRNlhOuyLmJxCzplg1Po20kkfrYTvACFZ0+lF7y4rMJ3L2X8VvcCv02g7ySR6DFT0jMQokC79JHyWl6G91PGtboP+B0buOTuAEp+sRO8AL+01kdKPJUe8HEKT5Whq68H/C6Bvsdzn6U/RAEG3yF9pKjzq8ELZeTV1wK/J6D7UfIrzwMzwQuIS2t9YaWnUtbGD96x+irg9wdc0CUPRfqCF3BcWvH3gL42fvDCd6qMtLon+AMJ+rc+O7+HKUC8t/4e0Hd+PfBUGXl1T/A+BzTLU3+YCV6AqeP1XFrv/Drhxeo9dHWiysN+H+heM3+aCVxAebYZGylFYQpEnmnAltI6I+z3piuwpbQO2Zk8srm88XtzFsht1gJb21Y2ikLqc60rgQto+7qPKFtbV/7P1RUOX9x86XDgAoVTOIVTOEX/xfkXW1DiieUqJZgAAAAASUVORK5CYII=",
    [InternalApps.WINDOWS_EXPLORER]:
        "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",
    [InternalApps.WINDOWS_SETTINGS]:
        "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",
    [InternalApps.NOVNC_BROWSER]:
        "AAABAAEAQEAAAAEAIAAoQgAAFgAAACgAAABAAAAAgAAAAAEAIAAAAAAAAEIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAODg4zDExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/ODg4/0FBQf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSc8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MjIy/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9GRkYhAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEBAQAQxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/OTk5/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSYkAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJiQAAAAAAAAAAAAAAAAAAAAAyMjL+MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/QEBA/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9LS0siAAAAAAAAAAAxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/QEBA/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/wAAAAAAAAAAMTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8AAAAAAAAAADExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJzjo6OowxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/QkJC/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf80NDT7MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MjIy/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/z8/P/9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9ISEj/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zQ0NP9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/NjY2/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTMz/wD///8A////ANDQ/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/HWpq/wD///8A////AAAA/zExMf8xMzP/APr6/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAP9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wbj4/8A////AP///wD///8ACAj/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/KUtL/wD///8A////AP///wAAAP8xMTH/AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAD/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wH39/8A////AP///wD///8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP///wD///8AAAD/MTEx/wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wD///8A+/v/AP///wD///8AAAD/JCQk/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD8/P8A////AP///wD///8A////AAAA/zExMf8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8ffHz/AP///wD///8JExP/MTEx/wD///8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP///wK1tf8A////AP///wAAAP8xMTH/AP///wD///8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9ERET/SUlJ/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8A////MTEx/zExMf8A////AP///wD///8Ac3P/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8AAAD/MTEx/zExMf8xMTH/MTEx/w+env8A////AP///wDd3f8xMTH/AP///wD///8AAAD/MTEx/wD///8A////AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8A////AJSU/zExMf8xMTH/MTEx/wD///8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8xMTH/MTEx/wD///8A////AP///wD///8xMTH/MTEx/wD///8A////AAAA/zExMf8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8yMjL/SUlJ/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP///yAgIP8xMTH/MTEx/zExMf8akpL/AP///wD///8AAAD/CQkJ/zExMf8xMTH/MTEx/zExMf8A////AP///wAAAP8xMTH/MTEx/y84OP8A////AP///wD///8sLy//MTEx/zExMf8A////AP///wAAAP8xMTH/AP///wD///8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wbj4/8A////AP///yoqKv8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP///wAICP8xMTH/MTEx/zExMf8xMTH/AP///wD///8AAAD/MTEx/wD8/P8A////AP///wD///8xMTH/MTEx/zExMf8xMTH/AP///wD///8AAAD/MTEx/wD///8A////AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/SUlJ/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/y4+Pv8A////AP///wD///8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8B+vr/AP///wD///8A7u7/Kysr/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8A////AP///wD///8IeHj/MTEx/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8A////AP///wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/PDw8/0lJSf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wD///8LEhL/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wfi4v8A////AP///wAAAP8kJCT/MTEx/zExMf8A////AP///wCPj/8A////AP///wD///8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wAAAP8xMTH/AP///wD///8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9ISEj/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8AyMj/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMjL/AP///wD///8ARUX/AAAA/zExMf8xMTH/AP///wD///8A////AP///wD///8A9/f/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8AAAD/MTEx/wD///8A////AAAA/y8vL/8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/NjY2/0lJSf9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8A////MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP///wBzc/8xMTH/MTEx/wD///8A////AP///wD///8sLy//MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AAAA/zExMf8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wAAAP8xMTH/MTEx/zExMf9JSUn/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/FqGh/wD///8A////AP///zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8bi4v/AP///wD///8A////EhIS/zExMf8A////AP///wD///8A////MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8A////AP///wAAAP8xMTH/AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8AAAD/MTEx/zExMf8xMTH/SUlJ/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wD///8A////AP7+/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/yddXf8A////AP///zExMf8xMTH/AP///wD///8bi4v/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AP///wD///8xMTH/MTEx/zExMf8qTEz/AP///wD///8A////AP///wD///8A////AP///wD///8A////AP///wD///8A////MTEx/zExMf8xMTH/MTEx/0dHR/9JSUn/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf87Ozv/SUlJ/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9ERET/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AAAD/AAAA/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AAAA/wAAAP8xMTH/MTEx/zExMf8BAQH/AAAA/wAAAP8AAAD/AAAA/wAAAP8AAAD/AAAA/wAAAP8aGhr/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/MTEx/zExMf8AfQD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AAAA/x4eHv8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/zExMf8aVxr/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8xMTH/BHkE/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0lJSf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/MTEx/wCAAP8AgAD/AIAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf9JSUn/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/zExMf8AgAD/AIAA/wCAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/SUlJ/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8xMTH/AIAA/wCAAP8AgAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/0JCQv8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/MTEx/wCAAP8AgAD/AIAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/wAAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf88PDz/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/zExMf8AgAD/AIAA/wCAAP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8AAAD/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MjIy/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wAAAP8vLy//MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8xMTH/AIAA/wCAAP8AgAD/Ly8v/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/AAAA/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/MTEx/wxtDP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wMDA/8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AHgA/zExMf8tNy3/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wB4AP8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AIAA/wCAAP8CewL/MTEx/zExMf8xMTH/MTEx/yo7Kv8Icgj/AIAA/wCAAP8AgAD/AIAA/wCAAP8AgAD/AnsC/xdbF/8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zMzM/4xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf80NDT1MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/OTk5LTExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/wAAAAAxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zg4OMwAAAAAMTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8AAAAAAAAAADIyMv4xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AAAAAAAAAAAAAAAAOjo6ljExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AAAAAAAAAAAAAAAAAAAAAAAAAAAxMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/VVVVBgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADo6OpExMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAMjIy/TExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADo6Oiw1NTX1MzMz/jExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf8xMTH/MTEx/zExMf80NDT7Ojo6jAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA/gAAAAAAAH/4AAAAAAAAD+AAAAAAAAAH4AAAAAAAAAPAAAAAAAAAAYAAAAAAAAABgAAAAAAAAAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAAAAAAAAAgAAAAAAAAAGAAAAAAAAAAcAAAAAAAAAD4AAAAAAAAAPwAAAAAAAAD/gAAAAAAAAf/wAAAAAAAP8=",
    [DEFAULT_ICON]:
        "iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAABiVBMVEUAAABzc3N5eXl0dHR5dnN5dnN4eHV5d3V6eHR5d3V6eHR5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3R5d3QGabwGar0Ha70Ha74HbL4HbL8Hbb8HbcAIbsAIbsEIb8EIcMIJccIJccMJcsMJcsQJc8QKc8UKdMUKdMYKdcYKdscLdscLd8cLd8gLeMgLeMkLeckMecoMesoMessMe8sMe8wMfMwNfMwNfcwNfc0Nfs0Nfs4Nf84Of88OgM8OgNAOgdAOgdEOgtEPgtEPg9IPhNIPhNMPhdMQhdQQhtQQhtUQh9UQh9YRiNYRidcRitcRitgSi9gSi9kSjNkSjNoSjdoSjdsTjtsTj9wed8IeeMMfecMpmd4qmd8qmt94rdl5d3R5rtp8end+wep/weqHhYOMi4iOjIqPjYqPjYuWlpbAv77BwL7Cwb/CwcDDwsHEw8LExMTPz87Q0M/S0dDX19bY19bk5OPl5eTx8PDx8fHz8/Lz8/P09PP09vj09/n4+Pj6+vr///+Z/ULnAAAAFXRSTlMAFBUWUlRVgYKDhM3Oz9Dw8fLz9P7FJ1F0AAAAAWJLR0SCi7P/RAAAAtFJREFUeNrt2s1qE1EYxvGZTGfSIEYKQhYigqtcSN2IVi/BO2z9WHotomIXorQS05JkpplmalyIbmrz2ff3h1C6aJnzz/O85+S0SQIAAAAAAAAAAAAAAG476eyVtbPiebiVvx1fTC6uBBR3XgR9918PyyRLiu5B1Pj3P5fTNOve9vd/b/Y6/cfXhqNBqx01/1e8LFo7sTeBnegC8vRV8HNAKyEg+BC4KkK3k4dbeTkaTOYC8vu7EcdfXnwrf1Wguxsz/e278xnQidr/zlxAHlVAYRcggAACCCCAAAIIIIAAAgggICZ//1ng6zV+uvfnt9Mv5WTpT5y3H7QW9cCLTsDx2fLXn5TD4+UmoPf/v2xcryS1o4U98KITMFlNbUtDcKkVuAE1Ac4BBKiABEiABEiABEhAzPuA9QjYoPuA21GB3rYtoWcGEGAbJIAAM0ACCFABAlSAABVY633AerjZA0uA+wDboF3ADFABJ0EzwAwwA8wAAgxBM0ACknD/H+AcsMiP1+sR0DMDCLANEqACBKiABEgAASpAgAq4D7jmJ0cVWPDH6627DzADnAPMABUgwAzwWcAQNAPMADPADDADtvM+QAU25z5AAgiwCxCgAgSoAAEEmAESIAEERLwPMAPcB5gBBJgBBKiABEiABEiABEgAASoQ7z7ADIh+H2AGqAABBJgBBKgAASpAgApIgARIgARIgAQQoAIEqEAkAfureeYnG1uBfDUdKDZWwKOnk/dLX/5+8XBjBWSPk74hSAABBBBAAAEEEEAAAVsjoIy6+MlcQBVVQDUXcBZVwPB3AuqY66+bBGSf+rMkdEIKODlPkqNWY2EQsgTDpgHVTiNg+n28l0bL/8lw2uyAWf1xdotVj0ejNI1zJqhGp4PzZvIdnqdJfu9Z2FPQux9VlkzLD/2g6z/8WSVN9VtF1j4It/o342pSJwAAAAAAAAAAAAAAIACXIfSz2zOYD54AAAAASUVORK5CYII=",
};

```

## /src/renderer/data/docker.ts

```ts path="/src/renderer/data/docker.ts" 
import { ComposeConfig } from "../../types";
import { RESTART_ON_FAILURE } from "../lib/constants";

export const DOCKER_DEFAULT_COMPOSE: ComposeConfig = {
    name: "winboat",
    volumes: {
        data: null,
    },
    services: {
        windows: {
            image: "ghcr.io/dockur/windows:6.00",
            container_name: "WinBoat",
            environment: {
                VERSION: "11",
                RAM_SIZE: "4G",
                CPU_CORES: "4",
                DISK_SIZE: "64G",
                USERNAME: "MyWindowsUser",
                PASSWORD: "MyWindowsPassword",
                HOME: "${HOME}",
                LANGUAGE: "English",
                USER_PORTS: "7148",
                HOST_PORTS: "7149",
                ARGUMENTS: "-qmp tcp:0.0.0.0:7149,server,wait=off",
            },
            cap_add: ["NET_ADMIN"],
            privileged: true,
            ports: [
                "127.0.0.1:47270-47279:8006", // VNC Web Interface
                "127.0.0.1:47280-47289:7148", // Winboat Guest Server API
                "127.0.0.1:47290-47299:7149", // QEMU QMP Port
                "127.0.0.1:47300-47309:3389/tcp", // RDP
                "127.0.0.1:47310-47319:3389/udp", // RDP
            ],
            stop_grace_period: "120s",
            restart: RESTART_ON_FAILURE,
            volumes: [
                "data:/storage",
                "${HOME}:/shared",
                "/dev/bus/usb:/dev/bus/usb", // QEMU Dynamic USB Passthrough
                "./oem:/oem",
            ],
            devices: ["/dev/kvm"],
        },
    },
};

```

## /src/renderer/data/internalapps.ts

```ts path="/src/renderer/data/internalapps.ts" 
export const InternalApps = {
    WINDOWS_DESKTOP: "WINDOWS_DESKTOP",
    WINDOWS_EXPLORER: "WINDOWS_EXPLORER",
    WINDOWS_SETTINGS: "WINDOWS_SETTINGS",
    NOVNC_BROWSER: "NOVNC_BROWSER",
};

```

## /src/renderer/data/podman.ts

```ts path="/src/renderer/data/podman.ts" 
import { ComposeConfig } from "../../types";
import { RESTART_ON_FAILURE } from "../lib/constants";

export const PODMAN_DEFAULT_COMPOSE: ComposeConfig = {
    name: "winboat",
    volumes: {
        data: null,
    },
    services: {
        windows: {
            image: "ghcr.io/dockur/windows:6.00",
            container_name: "WinBoat",
            environment: {
                VERSION: "11",
                RAM_SIZE: "4G",
                CPU_CORES: "4",
                DISK_SIZE: "64G",
                USERNAME: "MyWindowsUser",
                PASSWORD: "MyWindowsPassword",
                HOME: "${HOME}",
                LANGUAGE: "English",
                NETWORK: "user",
                USER_PORTS: "7148",
                HOST_PORTS: "7149",
                ARGUMENTS: "-qmp tcp:0.0.0.0:7149,server,wait=off",
            },
            cap_add: ["NET_ADMIN"],
            ports: [
                "127.0.0.1::8006", // VNC Web Interface
                "127.0.0.1::7148", // Winboat Guest Server API
                "127.0.0.1::7149", // QEMU QMP Port
                "127.0.0.1::3389/tcp", // RDP
                "127.0.0.1::3389/udp", // RDP
            ],
            stop_grace_period: "120s",
            restart: RESTART_ON_FAILURE,
            privileged: true,
            volumes: [
                "data:/storage",
                "${HOME}:/shared",
                "./oem:/oem",
            ],
            devices: ["/dev/kvm", "/dev/bus/usb"],
        },
    },
};

```

## /src/renderer/directives/autoscroll.ts

```ts path="/src/renderer/directives/autoscroll.ts" 
// auto-scroll.ts
import type { Directive, DirectiveBinding } from "vue";

interface ScrollOptions {
    behavior?: ScrollBehavior;
    top?: number;
    left?: number;
}

interface AutoScrollElement extends HTMLElement {
    _autoScrollObserver?: MutationObserver;
}

export const autoScroll: Directive<AutoScrollElement, ScrollOptions> = {
    mounted(el: AutoScrollElement, binding: DirectiveBinding<ScrollOptions>) {
        // Store the mutation observer instance on the element
        el._autoScrollObserver = new MutationObserver(() => {
            // Get scroll options from binding value or use defaults
            const options: ScrollOptions = {
                behavior: "smooth",
                ...binding.value,
            };

            // Scroll to bottom
            el.scrollTo({
                top: el.scrollHeight,
                ...options,
            });
        });

        // Configure the observer to watch for changes
        const config: MutationObserverInit = {
            childList: true, // Watch for changes in child elements
            subtree: true, // Watch all descendants, not just direct children
            characterData: true, // Watch for changes in text content
        };

        // Start observing
        el._autoScrollObserver.observe(el, config);

        // Initial scroll
        el.scrollTo({
            top: el.scrollHeight,
            behavior: binding.value?.behavior ?? "smooth",
        });
    },

    beforeUnmount(el: AutoScrollElement): void {
        // Clean up the observer when directive is removed
        if (el._autoScrollObserver) {
            el._autoScrollObserver.disconnect();
            delete el._autoScrollObserver;
        }
    },

    // Re-apply when component updates
    updated(el: AutoScrollElement, binding: DirectiveBinding<ScrollOptions>): void {
        const options: ScrollOptions = {
            behavior: "smooth",
            ...binding.value,
        };

        el.scrollTo({
            top: el.scrollHeight,
            ...options,
        });
    },
};

```

## /src/renderer/index.css

```css path="/src/renderer/index.css" 
@import url("https://fonts.googleapis.com/css2?family=Open+Sans:ital,wght@0,300..800;1,300..800&display=swap");
@tailwind base;
@tailwind components;
@tailwind utilities;

body {
    @apply bg-transparent overflow-hidden text-white;
    font-family: "Open Sans", serif;
    font-optical-sizing: auto;
    font-weight: 400;
    font-style: normal;
}

/* Xel reset, why do they modify default tags anyway...? */
body img {
    display: unset;
    height: unset;
    margin: unset;
    max-width: unset;
    box-sizing: unset;
    background: unset;
    border-width: unset;
    border-style: unset;
    border-color: unset;
    user-select: unset;
    -webkit-user-modify: unset;
}

.generic-hover {
    @apply hover:brightness-90 transition duration-200;
}

```

## /src/renderer/index.html

```html path="/src/renderer/index.html" 
<!doctype html>
<html lang="en">
    <head>
        <meta charset="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>WinBoat</title>
        <script src="./public/xel/xel.js" type="module"></script>
        <meta name="xel-theme" content="./xel/themes/fluent-dark.css" />
        <meta name="xel-icons" content="./xel/icons/fluent.svg" />
        <meta name="xel-accent-color" content="#A78AF9" />
    </head>

    <body>
        <div id="app"></div>
        <script type="module" src="./main.ts"></script>
    </body>
</html>

```

## /src/renderer/lib/config.ts

```ts path="/src/renderer/lib/config.ts" 
const fs: typeof import("fs") = require("node:fs");
const path: typeof import("path") = require("node:path");
import { type WinApp } from "../../types";
import { WINBOAT_DIR } from "./constants";
import { type PTSerializableDeviceInfo } from "./usbmanager";
import { ContainerRuntimes } from "./containers/common";
import { logger } from "./winboat";

export type RdpArg = {
    original?: string;
    newArg: string;
    isReplacement: boolean;
};

export class WinboatVersion {
    public readonly generation: number;
    public readonly major: number;
    public readonly minor: number;
    public readonly alpha: boolean;

    constructor(public readonly versionToken: string) {
        const versionTags = versionToken.split("-");
        const versionNumbers = versionTags[0].split(".").map(value => {
            const parsedValue = parseInt(value);

            if(Number.isNaN(parsedValue)) {
                throw new Error(`Invalid winboat version format: '${versionToken}'`);
            }

            return parsedValue;
        });

        this.alpha = !!versionTags[1]?.includes("alpha");
        this.generation = versionNumbers[0];
        this.major = versionNumbers[1];
        this.minor = versionNumbers[2];
    }

    toString(): string {
        return this.versionToken;
    }

    toJSON(): string {
        return this.toString();
    }
}

type WinboatVersionData = {
    previous: WinboatVersion,
    current: WinboatVersion
}

export enum MultiMonitorMode {
    None = "None",
    MultiMon = "MultiMon",
    Span = "Span"
};

export type WinboatConfigObj = {
    scale: number;
    scaleDesktop: number;
    smartcardEnabled: boolean;
    rdpMonitoringEnabled: boolean;
    passedThroughDevices: PTSerializableDeviceInfo[];
    customApps: WinApp[];
    experimentalFeatures: boolean;
    advancedFeatures: boolean;
    multiMonitor: MultiMonitorMode;
    rdpArgs: RdpArg[];
    disableAnimations: boolean;
    containerRuntime: ContainerRuntimes;
    versionData: WinboatVersionData;
    appsSortOrder: string;
};

const currentVersion = new WinboatVersion(import.meta.env.VITE_APP_VERSION);

const defaultConfig: WinboatConfigObj = {
    scale: 100,
    scaleDesktop: 100,
    smartcardEnabled: false,
    rdpMonitoringEnabled: false,
    passedThroughDevices: [],
    customApps: [],
    experimentalFeatures: false,
    advancedFeatures: false,
    multiMonitor: MultiMonitorMode.None,
    rdpArgs: [],
    disableAnimations: false,
    // TODO: Ideally should be podman once we flesh out everything
    containerRuntime: ContainerRuntimes.DOCKER,
    versionData: {
        previous: currentVersion, // As of 0.9.0 this won't exist on the filesystem, so we just set it to the current version
        current: currentVersion
    },
    appsSortOrder: 'name',
};

export class WinboatConfig {
    private static readonly configPath: string = path.join(WINBOAT_DIR, "winboat.config.json");
    private static instance: WinboatConfig | null = null;
    
    // Due to us wrapping WinboatConfig in reactive, this can't be private
    configData: WinboatConfigObj = { ...defaultConfig };

    static getInstance() {
        WinboatConfig.instance ??= new WinboatConfig();
        return WinboatConfig.instance;
    }

    private constructor() {
        this.configData = WinboatConfig.readConfigObject()!;

        // Set correct versionData
        if(this.config.versionData.current.versionToken !== currentVersion.versionToken) {
            this.config.versionData.previous = this.config.versionData.current;
            this.config.versionData.current = currentVersion;

            logger.info(`Updated version data from '${this.config.versionData.previous.toString()}' to '${currentVersion.toString()}'`);
        }

        console.log("Reading current config", this.configData);
    }

    get config(): WinboatConfigObj {
        // Return a proxy to intercept property sets
        return new Proxy(this.configData, {
            get: (target, key) => Reflect.get(target, key),
            set: (target, key, value: WinboatConfigObj) => {
                const result = Reflect.set(target, key, value);

                WinboatConfig.writeConfigObject(target);
                console.info("Wrote modified config to disk");

                return result;
            },
        });
    }

    set config(newConfig: WinboatConfigObj) {
        this.configData = { ...newConfig };
        WinboatConfig.writeConfigObject(newConfig);
        console.info("Wrote modified config to disk");
    }

    static writeConfigObject(configObj: WinboatConfigObj): void {
        console.log("writing data: ", configObj);
        fs.writeFileSync(WinboatConfig.configPath, JSON.stringify(configObj, null, 4), "utf-8");
    }

    static readConfigObject(writeDefault = true): WinboatConfigObj | null {
        if (!fs.existsSync(WinboatConfig.configPath)) {
            if (!writeDefault) return null;
            // Also the create the directory because we're not guaranteed to have it
            if (!fs.existsSync(WINBOAT_DIR)) {
                fs.mkdirSync(WINBOAT_DIR);
            }

            fs.writeFileSync(WinboatConfig.configPath, JSON.stringify(defaultConfig, null, 4), "utf-8");
            return { ...defaultConfig };
        }

        try {
            const rawConfig = fs.readFileSync(WinboatConfig.configPath, "utf-8");
            const configObjRaw = JSON.parse(rawConfig);

            // Parse winboat version data
            if(configObjRaw.versionData) {
                configObjRaw.versionData.current = new WinboatVersion(configObjRaw.versionData.current);
                configObjRaw.versionData.previous = new WinboatVersion(configObjRaw.versionData.previous);
            }

            const configObj = configObjRaw as WinboatConfigObj;

            console.log("Successfully read the config file");

            // Some fields might be missing after an update, so we merge them with the default config
            let hasMissing = false;
            for (const key in defaultConfig) {
                if (!(key in configObj)) {
                    // @ts-expect-error This is valid
                    configObj[key] = defaultConfig[key];
                    hasMissing = true;
                    console.log(
                        `Added missing config key: ${key} with default value: ${
                            JSON.stringify(defaultConfig[key as keyof WinboatConfigObj])
                        }`,
                    );
                }
            }

            // If we have any missing keys, we should just write the config back to disk so those new keys are saved
            // We cannot use this.writeConfig() here since #configData is not populated yet
            if (hasMissing) {
                fs.writeFileSync(WinboatConfig.configPath, JSON.stringify(configObj, null, 4), "utf-8");
                console.log("Wrote updated config with missing keys to disk");
            }

            return { ...configObj };
        } catch (e) {
            console.error("Config’s borked, outputting the default:", e);
            return { ...defaultConfig };
        }
    }
}

```

## /src/renderer/lib/constants.ts

```ts path="/src/renderer/lib/constants.ts" 
const os: typeof import("os") = require("node:os");
const path: typeof import("path") = require("node:path");

// Should be {home}/.winboat
export const WINBOAT_DIR = path.join(os.homedir(), ".winboat");
export const DEFAULT_HOMEBREW_DIR = path.join(os.homedir(), "../linuxbrew/.linuxbrew/bin");

export const WINDOWS_VERSIONS = {
    "11": "Windows 11 Pro",
    "11l": "Windows 11 LTSC 2024",
    "11e": "Windows 11 Enterprise",
    "10": "Windows 10 Pro",
    "10l": "Windows 10 LTSC 2021",
    "10e": "Windows 10 Enterprise",
    custom: "Custom Windows",
};

export type WindowsVersionKey = keyof typeof WINDOWS_VERSIONS;

export const WINDOWS_LANGUAGES = {
    "🇦🇪 Arabic": "Arabic",
    "🇧🇬 Bulgarian": "Bulgarian",
    "🇨🇳 Chinese": "Chinese",
    "🇭🇷 Croatian": "Croatian",
    "🇨🇿 Czech": "Czech",
    "🇩🇰 Danish": "Danish",
    "🇳🇱 Dutch": "Dutch",
    "🇬🇧 English": "English",
    "🇪🇪 Estonian": "Estonian",
    "🇫🇮 Finnish": "Finnish",
    "🇫🇷 French": "French",
    "🇩🇪 German": "German",
    "🇬🇷 Greek": "Greek",
    "🇮🇱 Hebrew": "Hebrew",
    "🇭🇺 Hungarian": "Hungarian",
    "🇮🇹 Italian": "Italian",
    "🇯🇵 Japanese": "Japanese",
    "🇰🇷 Korean": "Korean",
    "🇱🇻 Latvian": "Latvian",
    "🇱🇹 Lithuanian": "Lithuanian",
    "🇳🇴 Norwegian": "Norwegian",
    "🇵🇱 Polish": "Polish",
    "🇵🇹 Portuguese": "Portuguese",
    "🇷🇴 Romanian": "Romanian",
    "🇷🇺 Russian": "Russian",
    "🇷🇸 Serbian": "Serbian",
    "🇸🇰 Slovak": "Slovak",
    "🇸🇮 Slovenian": "Slovenian",
    "🇪🇸 Spanish": "Spanish",
    "🇸🇪 Swedish": "Swedish",
    "🇹🇭 Thai": "Thai",
    "🇹🇷 Turkish": "Turkish",
    "🇺🇦 Ukrainian": "Ukrainian",
};

// Ports
export const GUEST_RDP_PORT = 3389;
export const GUEST_QMP_PORT = 7149;
export const DEFAULT_HOST_QMP_PORT = 8149;

// USB
export const USB_CLASS_IMAGING = 6;
export const USB_INTERFACE_MTP = 5;
export const USB_VID_BLACKLIST = [
    // Linux Foundation VID
    "1d6b:",
];

// Docker Restart Policies
export const RESTART_UNLESS_STOPPED = "unless-stopped";
export const RESTART_ON_FAILURE = "on-failure";
export const RESTART_ALWAYS = "always";
export const RESTART_NO = "no";

```

## /src/renderer/lib/containers/common.ts

```ts path="/src/renderer/lib/containers/common.ts" 
import { PortEntryProtocol } from "../../../types";
import { ContainerManager } from "./container";
import { DockerContainer, DockerSpecs } from "./docker";
import { PodmanContainer, PodmanSpecs } from "./podman";

// For convenience
export { type DockerSpecs } from "./docker";
export { type PodmanSpecs } from "./podman";
export { ContainerStatus } from "./container";

export enum ContainerRuntimes {
    DOCKER = "Docker",
    PODMAN = "Podman",
}

// NOTE: These are container port values, and should be used as such
export enum CommonPorts {
    RDP = 3389,
    NOVNC = 8006,
    API = 7148,
    QMP = 7149,
}

export const ContainerImplementations = {
    [ContainerRuntimes.DOCKER]: DockerContainer,
    [ContainerRuntimes.PODMAN]: PodmanContainer,
} as const satisfies Record<ContainerRuntimes, any>; // this makes it so ContainerImplementations has to map ContainerRuntimes to something exhaustively

type ContainerSpecMap = {
    [ContainerRuntimes.DOCKER]: DockerSpecs;
    [ContainerRuntimes.PODMAN]: PodmanSpecs;
};

export type ContainerSpecs = ContainerSpecMap[ContainerRuntimes];

export async function getContainerSpecs<T extends ContainerRuntimes>(type: T): Promise<ContainerSpecMap[T]> {
    return (await ContainerImplementations[type]._getSpecs()) as ContainerSpecMap[T];
}

export function createContainer<T extends ContainerRuntimes>(
    type: T,
    ...params: ConstructorParameters<(typeof ContainerImplementations)[T]>
) {
    return new ContainerImplementations[type](...(params as []));
}

export function getActiveHostPort(
    container: ContainerManager,
    port: CommonPorts,
    protocol: PortEntryProtocol = "tcp",
): number | undefined {
    return container.cachedPortMappings?.find(
        mapping => typeof mapping.container === "number" && mapping.container === port && mapping.protocol === protocol,
    )?.host as number;
}

```

## /src/renderer/lib/containers/container.ts

```ts path="/src/renderer/lib/containers/container.ts" 
import { ComposeConfig } from "../../../types";
import { WINBOAT_DIR } from "../constants";
import { createLogger } from "../../utils/log";
import { ComposePortEntry } from "../../utils/port";

const path: typeof import("node:path") = require("node:path");

export const containerLogger = createLogger(path.join(WINBOAT_DIR, "container.log"));

export type ComposeDirection = "up" | "down";
export type ComposeArguments = "--no-start";
export type ContainerAction = "start" | "stop" | "pause" | "unpause" | "restart";

export abstract class ContainerManager {
    abstract readonly defaultCompose: ComposeConfig;
    abstract readonly composeFilePath: string;
    abstract readonly executableAlias: string;

    abstract cachedPortMappings: ComposePortEntry[] | null;

    abstract writeCompose(compose: ComposeConfig): void;
    abstract compose(direction: ComposeDirection, extraArgs?: ComposeArguments[]): Promise<void>;
    abstract container(action: ContainerAction): Promise<void>;
    abstract port(): Promise<ComposePortEntry[]>;
    abstract remove(): Promise<void>;
    abstract getStatus(): Promise<ContainerStatus>;
    abstract exists(): Promise<boolean>;

    abstract get containerName(): string;

    // static "abstract" function
    static async _getSpecs(): Promise<any> {
        throw new Error("Can't get specs of abstract class ContainerManager");
    }
}

export enum ContainerStatus {
    CREATED = "Created", // unused
    RUNNING = "Running",
    PAUSED = "Paused",
    EXITED = "Exited",
    UNKNOWN = "Unknown",
}

```

## /src/renderer/lib/containers/docker.ts

```ts path="/src/renderer/lib/containers/docker.ts" 
import { ComposeConfig } from "../../../types";
import { DOCKER_DEFAULT_COMPOSE } from "../../data/docker";
import { capitalizeFirstLetter } from "../../utils/capitalize";
import { ComposePortEntry } from "../../utils/port";
import { WINBOAT_DIR } from "../constants";
import {
    ComposeArguments,
    ComposeDirection,
    ContainerAction,
    containerLogger,
    ContainerManager,
    ContainerStatus,
} from "./container";
import YAML from "yaml";
import { execFileAsync, stringifyExecFile } from "../exec-helper";

const path: typeof import("node:path") = require("node:path");
const fs: typeof import("node:fs") = require("node:fs");

export type DockerSpecs = {
    dockerInstalled: boolean;
    dockerComposeInstalled: boolean;
    dockerIsRunning: boolean;
    dockerIsInUserGroups: boolean;
};

export class DockerContainer extends ContainerManager {
    defaultCompose = DOCKER_DEFAULT_COMPOSE;
    composeFilePath = path.join(WINBOAT_DIR, "docker-compose.yml"); // TODO: If/when we support multiple VM's we need to put this in the constructor
    executableAlias = "docker";

    cachedPortMappings: ComposePortEntry[] | null = null;

    constructor() {
        super();
    }

    writeCompose(compose: ComposeConfig): void {
        const composeContent = YAML.stringify(compose, { nullStr: "" });
        fs.writeFileSync(this.composeFilePath, composeContent, { encoding: "utf-8" });

        containerLogger.info(`Wrote to compose file at: ${this.composeFilePath}`);
        containerLogger.info(`Compose file content: ${JSON.stringify(composeContent, null, 2)}`);
    }

    async compose(direction: ComposeDirection, extraArgs: ComposeArguments[] = []): Promise<void> {
        const args = ["compose", "-f", this.composeFilePath, direction, ...extraArgs];

        if (direction === "up") {
            // Run compose in detached mode if we are running compose up
            args.push("-d");
        }

        try {
            const { stderr } = await execFileAsync(this.executableAlias, args);
            if (stderr) {
                containerLogger.error(stderr);
            }
        } catch (e) {
            containerLogger.error(`Failed to run compose command '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }
    }

    async container(action: ContainerAction): Promise<void> {
        const args = ["container", action, this.containerName];
        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);
            containerLogger.info(`Container action '${action}' response: '${stdout}'`);
        } catch (e) {
            containerLogger.error(`Failed to run container action '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }
    }

    async port(): Promise<ComposePortEntry[]> {
        const args = ["port", this.containerName];
        const ret = [];

        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);

            for (const line of stdout.trim().split("\n")) {
                const parts = line.split("->").map(part => part.trim());
                const hostPart = parts[1];
                const containerPart = parts[0];

                ret.push(new ComposePortEntry(`${hostPart}:${containerPart}`));
            }
        } catch (e) {
            containerLogger.error(`Failed to run container action '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }

        containerLogger.info("Docker container active port mappings: ", JSON.stringify(ret));
        this.cachedPortMappings = ret;
        return ret;
    }

    async remove(): Promise<void> {
        const args = ["rm", this.containerName];

        try {
            await execFileAsync(this.executableAlias, args);
        } catch (e) {
            containerLogger.error(`Failed to remove container '${this.containerName}'`);
            containerLogger.error(e);
        }
    }

    async getStatus(): Promise<ContainerStatus> {
        const statusMap = {
            created: ContainerStatus.CREATED,
            restarting: ContainerStatus.UNKNOWN,
            removing: ContainerStatus.UNKNOWN,
            running: ContainerStatus.RUNNING,
            paused: ContainerStatus.PAUSED,
            exited: ContainerStatus.EXITED,
            dead: ContainerStatus.UNKNOWN,
        } as const;
        const args = ["inspect", "--format={{.State.Status}}", this.containerName];
        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);
            const status = stdout.trim() as keyof typeof statusMap;
            return statusMap[status];
        } catch (e) {
            containerLogger.error(`Failed to get status of docker container ${e}'`);
            return ContainerStatus.UNKNOWN;
        }
    }

    async exists(): Promise<boolean> {
        const args = ["ps", "-a", "--filter", `name=${this.containerName}`, "--format", "{{.Names}}"];
        try {
            const { stdout: exists } = await execFileAsync(this.executableAlias, args);
            return exists.includes("WinBoat");
        } catch (e) {
            containerLogger.error(
                `Failed to get container status, is ${capitalizeFirstLetter(this.executableAlias)} installed?`,
            );
            containerLogger.error(e);
            return false;
        }
    }

    get containerName(): string {
        return this.defaultCompose.services.windows.container_name; // TODO: investigate whether we should use the compose on disk
    }

    static override async _getSpecs(): Promise<DockerSpecs> {
        let specs: DockerSpecs = {
            dockerInstalled: false,
            dockerComposeInstalled: false,
            dockerIsRunning: false,
            dockerIsInUserGroups: false,
        };

        try {
            const { stdout: dockerOutput } = await execFileAsync("docker", ["--version"]);
            specs.dockerInstalled = !!dockerOutput;
        } catch (e) {
            console.error("Error checking for Docker installation:", e);
        }

        // Docker Compose plugin check with version validation
        try {
            const { stdout: dockerComposeOutput } = await execFileAsync("docker", ["compose", "version"]);
            if (dockerComposeOutput) {
                // Example output: "Docker Compose version v2.35.1"
                // Example output 2: "Docker Compose version 2.36.2"
                const versionMatch = /(\d+\.\d+\.\d+)/.exec(dockerComposeOutput);
                if (versionMatch) {
                    const majorVersion = Number.parseInt(versionMatch[1].split(".")[0], 10);
                    specs.dockerComposeInstalled = majorVersion >= 2;
                } else {
                    specs.dockerComposeInstalled = false; // No valid version found
                }
            } else {
                specs.dockerComposeInstalled = false; // No output, plugin not installed
            }
        } catch (e) {
            console.error("Error checking Docker Compose version:", e);
        }

        // Docker is running check
        try {
            const { stdout: dockerOutput } = await execFileAsync("docker", ["ps"]);
            specs.dockerIsRunning = !!dockerOutput;
        } catch (e) {
            console.error("Error checking if Docker is running:", e);
        }

        // Docker user group check
        try {
            const { stdout: userGroups } = await execFileAsync("id", ["-Gn"]);
            specs.dockerIsInUserGroups = userGroups.split(/\s+/).includes("docker");
        } catch (e) {
            console.error("Error checking user groups for docker:", e);
        }

        return specs;
    }
}

```

## /src/renderer/lib/containers/podman.ts

```ts path="/src/renderer/lib/containers/podman.ts" 
import { ComposeConfig } from "../../../types";
import { PODMAN_DEFAULT_COMPOSE } from "../../data/podman";
import { WINBOAT_DIR } from "../constants";
import {
    ComposeArguments,
    ComposeDirection,
    ContainerAction,
    containerLogger,
    ContainerManager,
    ContainerStatus,
} from "./container";
import YAML from "yaml";
import { capitalizeFirstLetter } from "../../utils/capitalize";
import { ComposePortEntry } from "../../utils/port";
import { concatEnv, execFileAsync, stringifyExecFile } from "../exec-helper";

const path: typeof import("node:path") = require("node:path");
const fs: typeof import("node:fs") = require("node:fs");
const process: typeof import("process") = require("node:process");

export type PodmanSpecs = {
    podmanInstalled: boolean;
    podmanComposeInstalled: boolean;
};

export enum PodmanAPIStatus {
    AVAILABLE = "Available",
    UNAVAILABLE = "Unavailable",
}

type PodmanInfo = {
    host: {
        remoteSocket: {
            exists: boolean;
            path: string;
        };
        [Key: string]: any;
    };
    plugins: object;
    registries: {
        search: string[];
    };
    store: object;
    version: object;
};

const COMPOSE_ENV_VARS = { PODMAN_COMPOSE_PROVIDER: "podman-compose", PODMAN_COMPOSE_WARNING_LOGS: "false" };

export class PodmanContainer extends ContainerManager {
    defaultCompose = PODMAN_DEFAULT_COMPOSE;
    composeFilePath = path.join(WINBOAT_DIR, "podman-compose.yml");
    executableAlias = "podman";

    cachedPortMappings: ComposePortEntry[] | null = null;

    constructor() {
        super();
    }

    writeCompose(compose: ComposeConfig): void {
        const composeContent = YAML.stringify(compose, {
            nullStr: "",
            defaultStringType: "QUOTE_DOUBLE",
            defaultKeyType: "PLAIN",
        });
        fs.writeFileSync(this.composeFilePath, composeContent, { encoding: "utf-8" });

        containerLogger.info(`Wrote to compose file at: ${this.composeFilePath}`);
        containerLogger.info(`Compose file content: ${JSON.stringify(composeContent, null, 2)}`);
    }

    async compose(direction: ComposeDirection, extraArgs: ComposeArguments[] = []): Promise<void> {
        const args = ["compose", "-f", this.composeFilePath, direction, ...extraArgs];

        if (direction === "up") {
            // Run compose in detached mode if we are running compose up
            args.push("-d");
        }

        try {
            const { stderr } = await execFileAsync(this.executableAlias, args, {
                env: concatEnv(process.env as { [key: string]: string }, COMPOSE_ENV_VARS),
            });
            if (stderr) {
                containerLogger.error(stderr);
            }
        } catch (e) {
            containerLogger.error(`Failed to run compose command '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }
    }

    async container(action: ContainerAction): Promise<void> {
        const args = ["container", action, this.containerName];
        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);
            containerLogger.info(`Container action '${action}' response: '${stdout}'`);
        } catch (e) {
            containerLogger.error(`Failed to run container action '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }
    }

    async port(): Promise<ComposePortEntry[]> {
        const args = ["port", this.containerName];
        const ret = [];

        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);

            for (const line of stdout.trim().split("\n")) {
                const parts = line.split("->").map(part => part.trim());
                const hostPart = parts[1];
                const containerPart = parts[0];

                ret.push(new ComposePortEntry(`${hostPart}:${containerPart}`));
            }
        } catch (e) {
            containerLogger.error(`Failed to run container action '${stringifyExecFile(this.executableAlias, args)}'`);
            containerLogger.error(e);
            throw e;
        }

        containerLogger.info("Podman container active port mappings: ", JSON.stringify(ret));
        this.cachedPortMappings = ret;
        return ret;
    }

    async remove(): Promise<void> {
        const args = ["rm", this.containerName];

        try {
            await execFileAsync(this.executableAlias, args);
        } catch (e) {
            containerLogger.error(`Failed to remove container '${this.containerName}'`);
            containerLogger.error(e);
        }
    }

    async getStatus(): Promise<ContainerStatus> {
        const statusMap = {
            created: ContainerStatus.CREATED,
            restarting: ContainerStatus.UNKNOWN,
            initialized: ContainerStatus.UNKNOWN,
            removing: ContainerStatus.UNKNOWN,
            stopping: ContainerStatus.EXITED,
            stopped: ContainerStatus.EXITED,
            running: ContainerStatus.RUNNING,
            paused: ContainerStatus.PAUSED,
            exited: ContainerStatus.EXITED,
            dead: ContainerStatus.UNKNOWN,
        } as const;
        const args = ["inspect", "--format={{.State.Status}}", this.containerName];

        try {
            const { stdout } = await execFileAsync(this.executableAlias, args);
            const status = stdout.trim() as keyof typeof statusMap;
            return statusMap[status];
        } catch (e) {
            containerLogger.error(`Failed to get status of podman container ${e}'`);
            return ContainerStatus.UNKNOWN;
        }
    }

    async exists(): Promise<boolean> {
        const args = ["ps", "-a", "--filter", `name=${this.containerName}`, "--format", "{{.Names}}"];
        try {
            const { stdout: exists } = await execFileAsync(this.executableAlias, args);
            return exists.includes(this.containerName);
        } catch (e) {
            containerLogger.error(
                `Failed to get container status, is ${capitalizeFirstLetter(this.executableAlias)} installed?`,
            );
            containerLogger.error(e);
            return false;
        }
    }

    get containerName(): string {
        return this.defaultCompose.services.windows.container_name; // TODO: investigate whether we should use the compose on disk
    }

    static override async _getSpecs(): Promise<PodmanSpecs> {
        let specs: PodmanSpecs = {
            podmanInstalled: false,
            podmanComposeInstalled: false,
        };

        try {
            const { stdout: podmanOutput } = await execFileAsync("podman", ["--version"]);
            specs.podmanInstalled = !!podmanOutput;
        } catch (e) {
            containerLogger.error("Error checking podman version");
            containerLogger.error(e);
        }

        try {
            const { stdout: podmanComposeOutput } = await execFileAsync("podman", ["compose", "--version"], {
                env: concatEnv(process.env as { [key: string]: string }, COMPOSE_ENV_VARS),
            });
            specs.podmanComposeInstalled = !!podmanComposeOutput;
        } catch (e) {
            containerLogger.error("Error checking podman compose version");
            containerLogger.error(e);
        }

        return specs;
    }
}

```

## /src/renderer/lib/exec-helper.ts

```ts path="/src/renderer/lib/exec-helper.ts" 
const { execFile }: typeof import("child_process") = require("node:child_process");
const { promisify }: typeof import("util") = require("node:util");

export type ExecFileAsyncError = {
    cmd: string;
    code: number;
    killed: boolean;
    signal?: string | number;
    stderr: string;
    stdout: string;
    message: string;
    stack: string;
};

export const execFileAsync = promisify(execFile);

export function stringifyExecFile(file: string, args: string[]): string {
    let result = `${file}`;
    for (const arg of args) {
        result += `  ${escapeString(arg)}`;
    }
    return result;
}

function escapeString(str: string): string {
    let fixed_string = "";
    let index = 0;
    let safe = /^[a-zA-Z0-9,._+:@%/-]$/;
    while (index < str.length) {
        let char = str[index];
        if (safe.exec(char) == null) {
            fixed_string += "\\";
        }
        fixed_string += char;
        index++;
    }
    return fixed_string;
}

type EnvMap = {
    [key: string]: string;
};

export function concatEnv(a: EnvMap, b?: EnvMap) {
    if (b !== undefined) {
        for (const key of Object.keys(b)) {
            a[key] = b[key];
        }
    }
    return a;
}

```

## /src/renderer/lib/install.ts

```ts path="/src/renderer/lib/install.ts" 
import { type InstallConfiguration } from "../../types";
import { WINBOAT_DIR } from "./constants";
import { createLogger } from "../utils/log";
import { createNanoEvents, type Emitter } from "nanoevents";
import { Winboat } from "./winboat";
import { ContainerManager } from "./containers/container";
import { WinboatConfig } from "./config";
import { CommonPorts, createContainer, getActiveHostPort } from "./containers/common";

const fs: typeof import("fs") = require("fs");
const path: typeof import("path") = require("path");
const nodeFetch: typeof import("node-fetch").default = require("node-fetch");
const remote: typeof import("@electron/remote") = require("@electron/remote");
const argon2: typeof import("argon2") = require("argon2");
const logger = createLogger(path.join(WINBOAT_DIR, "install.log"));

export enum InstallStates {
    IDLE = "Preparing",
    CREATING_COMPOSE_FILE = "Creating Compose File",
    CREATING_OEM = "Creating OEM Assets",
    STARTING_CONTAINER = "Starting Container",
    MONITORING_PREINSTALL = "Monitoring Preinstall",
    INSTALLING_WINDOWS = "Installing Windows",
    COMPLETED = "Completed",
    INSTALL_ERROR = "Install Error",
};

interface InstallEvents {
    stateChanged: (state: InstallStates) => void;
    preinstallMsg: (msg: string) => void;
    error: (error: Error) => void;
    vncPortChanged: (port: number) => void;
}

export class InstallManager {
    conf: InstallConfiguration;
    emitter: Emitter<InstallEvents>;
    state: InstallStates;
    preinstallMsg: string;
    container: ContainerManager;

    constructor(conf: InstallConfiguration) {
        this.conf = conf;
        this.state = InstallStates.IDLE;
        this.preinstallMsg = "";
        this.emitter = createNanoEvents<InstallEvents>();
        this.container = createContainer(conf.container);
    }

    changeState(newState: InstallStates) {
        this.state = newState;
        this.emitter.emit("stateChanged", newState);
        logger.info(`New state: "${newState}"`);
    }

    setPreinstallMsg(msg: string) {
        if (msg === this.preinstallMsg) return;
        this.preinstallMsg = msg;
        this.emitter.emit("preinstallMsg", msg);
        logger.info(`Preinstall: "${msg}"`);
    }

    sleep(ms: number) {
        return new Promise(resolve => setTimeout(resolve, ms));
    }

    async createComposeFile() {
        this.changeState(InstallStates.CREATING_COMPOSE_FILE);

        // Ensure the .winboat directory exists
        if (!fs.existsSync(WINBOAT_DIR)) {
            fs.mkdirSync(WINBOAT_DIR);
            logger.info(`Created WinBoat directory: ${WINBOAT_DIR}`);
        }

        // Ensure the installation directory exists
        if (!fs.existsSync(this.conf.installFolder)) {
            fs.mkdirSync(this.conf.installFolder, { recursive: true });
            logger.info(`Created installation directory: ${this.conf.installFolder}`);
        }

        // Configure the compose file
        const composeContent = this.container.defaultCompose;

        composeContent.services.windows.environment.RAM_SIZE = `${this.conf.ramGB}G`;
        composeContent.services.windows.environment.CPU_CORES = `${this.conf.cpuCores}`;
        composeContent.services.windows.environment.DISK_SIZE = `${this.conf.diskSpaceGB}G`;
        composeContent.services.windows.environment.VERSION = this.conf.windowsVersion;
        composeContent.services.windows.environment.LANGUAGE = this.conf.windowsLanguage;
        composeContent.services.windows.environment.USERNAME = this.conf.username;
        composeContent.services.windows.environment.PASSWORD = this.conf.password;

        // Boot image mapping
        if (this.conf.customIsoPath) {
            composeContent.services.windows.volumes.push(`${this.conf.customIsoPath}:/boot.iso`);
        }

        // Storage folder mapping
        const storageFolderIdx = composeContent.services.windows.volumes.findIndex(vol => vol.includes("/storage"));
        
        if (storageFolderIdx === -1) {
            logger.warn("No /storage volume found in compose template, adding one...");
            composeContent.services.windows.volumes.push(`${this.conf.installFolder}:/storage`);
        } else {
            composeContent.services.windows.volumes[storageFolderIdx] = `${this.conf.installFolder}:/storage`;
        }

        // Shared folder mapping
        const sharedFolderIdx = composeContent.services.windows.volumes.findIndex(vol => vol.includes("/shared"));
        
        if (!this.conf.sharedFolderPath) {
            // Remove shared folder if not enabled
            if (sharedFolderIdx !== -1) {
                composeContent.services.windows.volumes.splice(sharedFolderIdx, 1);
                logger.info("Removed shared folder as per user configuration");
            }
        } else {
            // Add or update shared folder
            const volumeStr = `${this.conf.sharedFolderPath}:/shared`;
            
            if (sharedFolderIdx === -1) {
                composeContent.services.windows.volumes.push(volumeStr);
                logger.info(`Added shared folder: ${this.conf.sharedFolderPath}`);
            } else {
                composeContent.services.windows.volumes[sharedFolderIdx] = volumeStr;
                logger.info(`Updated shared folder to: ${this.conf.sharedFolderPath}`);
            }
        }

        // Write the compose file
        this.container.writeCompose(composeContent);
    }

    async createOEMAssets() {
        this.changeState(InstallStates.CREATING_OEM);
        logger.info("Creating OEM assets");

        const oemPath = path.join(WINBOAT_DIR, "oem"); // Fixed the path separator

        // Create OEM directory if it doesn’t exist
        if (!fs.existsSync(oemPath)) {
            fs.mkdirSync(oemPath, { recursive: true });
            logger.info(`Created OEM directory: ${oemPath}`);
        }

        // Determine the source path based on whether the app is bundled
        const appPath = remote.app.isPackaged
            ? path.join(process.resourcesPath, "guest_server") // For packaged app
            : path.join(remote.app.getAppPath(), "..", "..", "guest_server"); // For dev mode

        logger.info(`Guest server source path: ${appPath}`);

        // Check if the source directory exists
        if (!fs.existsSync(appPath)) {
            const error = new Error(`Guest server directory not found at: ${appPath}`);
            logger.error(error.message);
            throw error;
        }

        const copyRecursive = (src: string, dest: string) => {
            const stats = fs.statSync(src);

            if (stats.isDirectory()) {
                // Create directory if it doesn't exist
                if (!fs.existsSync(dest)) {
                    fs.mkdirSync(dest, { recursive: true });
                }

                // Copy all contents
                fs.readdirSync(src).forEach(entry => {
                    const srcPath = path.join(src, entry);
                    const destPath = path.join(dest, entry);
                    copyRecursive(srcPath, destPath);
                });

                logger.info(`Copied directory ${src} to ${dest}`);
            } else {
                // Copy file
                fs.copyFileSync(src, dest);
                logger.info(`Copied file ${src} to ${dest}`);
            }
        };

        // Copy all files from guest_server to oemPath
        try {
            fs.readdirSync(appPath).forEach(entry => {
                const srcPath = path.join(appPath, entry);
                const destPath = path.join(oemPath, entry);
                copyRecursive(srcPath, destPath);
            });
            logger.info("OEM assets created successfully");
        } catch (error) {
            logger.error(`Failed to copy OEM assets: ${error}`);
            throw error;
        }

        // Create password hash file in oemPath
        try {
            const hash = await argon2.hash(this.conf.password);
            fs.writeFileSync(path.join(oemPath, "auth.hash"), hash, { encoding: "utf8" });
        } catch (error) {
            logger.error(`Failed to create password hash: ${error}`);
            throw error;
        }
    }

    async startContainer() {
        this.changeState(InstallStates.STARTING_CONTAINER);
        logger.info("Starting container...");

        // Start the container
        await this.container.compose("up");

        // Cache ports
        await this.container.port();

        // emit vnc port event
        this.emitter.emit("vncPortChanged", getActiveHostPort(this.container, CommonPorts.NOVNC)!);

        logger.info("Container started successfully.");
    }

    async monitorContainerPreinstall() {
        // Sleep a bit to make sure the webserver is up in the container
        await this.sleep(3000);

        this.changeState(InstallStates.MONITORING_PREINSTALL);
        logger.info("Starting preinstall monitoring...");

        const re = new RegExp(/>([^<]+)</);
        while (true) {
            try {
                const vncHostPort = getActiveHostPort(this.container, CommonPorts.NOVNC)!;
                const response = await nodeFetch(`http://127.0.0.1:${vncHostPort}/msg.html`, {
                    signal: AbortSignal.timeout(500),
                });

                if (response.status === 404) {
                    logger.info("Received 404, preinstall completed");
                    return; // Exit the method when we get 404
                }

                const message = await response.text();
                const messageFormatted = re.exec(message)?.[1] || message;
                this.setPreinstallMsg(messageFormatted);
            } catch (error) {
                if (error instanceof Error && error.message.includes("404")) {
                    logger.info("Received 404, preinstall completed");
                    return; // Exit the method when fetch throws 404
                }

                logger.error(`Error monitoring container: ${error}`);
                throw error;
            }

            // Wait 500ms before next check
            await this.sleep(500);
        }
    }

    async monitorAPIHealth() {
        this.changeState(InstallStates.INSTALLING_WINDOWS);
        logger.info("Waiting for WinBoat Guest Server to wrap up installation...");

        let attempts = 0;

        while (true) {
            const start = performance.now();

            try {
                const apiHostPort = getActiveHostPort(this.container, CommonPorts.API)!;
                const res = await nodeFetch(`http://127.0.0.1:${apiHostPort}/health`, {
                    signal: AbortSignal.timeout(5000),
                });

                if (res.status === 200) {
                    logger.info("WinBoat Guest Server is up and healthy!");
                    this.changeState(InstallStates.COMPLETED);

                    const compose = Winboat.readCompose(this.container.composeFilePath);
                    const filteredVolumes = compose.services.windows.volumes.filter(
                        volume => !volume.endsWith("/boot.iso"),
                    );

                    if (compose.services.windows.volumes.length !== filteredVolumes.length) {
                        compose.services.windows.volumes = filteredVolumes;
                        this.container.writeCompose(compose);
                    }

                    return;
                }

                logger.log(`API request status: ${res.status}`);
            } catch (error) {
                // We can ignore the AbortError resulting from the timeout
                if (!(error instanceof nodeFetch.AbortError)) {
                    logger.error(error);
                }
            }

            if (++attempts % 12 === 0) {
                logger.info(`API not responding yet, still waiting after ${(attempts * 5) / 60} minutes...`);
            }

            await this.sleep(5000 - (performance.now() - start));
        }
    }

    async install() {
        logger.info("Starting installation...");

        try {
            await this.createComposeFile();
            await this.createOEMAssets();
            await this.startContainer();
            await this.monitorContainerPreinstall();
            await this.monitorAPIHealth();
        } catch (e) {
            this.changeState(InstallStates.INSTALL_ERROR);
            logger.error("Errors encountered, could not complete the installation steps.");
            logger.error(e);
            return;
        }
        this.changeState(InstallStates.COMPLETED);

        logger.info("Installation completed successfully.");
    }
}

export async function isInstalled(): Promise<boolean> {
    // Check if a winboat container exists
    const config = WinboatConfig.readConfigObject(false);

    if (!config) return false;

    const containerRuntime = createContainer(config.containerRuntime);

    return await containerRuntime.exists();
}

```

## /src/renderer/lib/migrate.ts

```ts path="/src/renderer/lib/migrate.ts" 
import { createLogger } from "../utils/log";
import { ComposePortMapper, Range } from "../utils/port";
import { WinboatConfig } from "./config";
import { WINBOAT_DIR } from "./constants";
import { CommonPorts, createContainer } from "./containers/common";
import { ContainerManager } from "./containers/container";
import { Winboat } from "./winboat";

const path: typeof import("path") = require("path");
const logger = createLogger(path.join(WINBOAT_DIR, "migrations.log"));

/**
 * This function performs the necessary automatic migrations
 * when updating to newer versions of WinBoat
 */
export async function performAutoMigrations(): Promise<void> {
    logger.info("[performAutoMigrations]: Starting automatic migrations");

    const wbConfig = WinboatConfig.getInstance(); // Get WinboatConfig instance
    const containerManager = createContainer(wbConfig.config.containerRuntime);
    const composeMapper = new ComposePortMapper(Winboat.readCompose(containerManager.composeFilePath))
    
    try {
        // In case of a version prior to 0.9.0, the NoVNC port will be set to the default 8006
        // which is how we know we need to perform the migration, because from 0.9.0 we can rely
        // on the stored version strings
        const novncMapping = composeMapper.getShortPortMapping(CommonPorts.NOVNC);
        console.log(composeMapper);
        if (!Range.isRange(novncMapping!.host) && novncMapping!.host === CommonPorts.NOVNC) {
            await migrateComposePorts_Pre090(containerManager);
        }
    }
    catch (e: any) {
        logger.error("[performAutoMigrations]: Automatic migrations failed");
        logger.error(e.message ?? e);
        return;
    }

    logger.info("[performAutoMigrations]: Finished automatic migrations");
}

/**
 * Perform compose port migrations for pre-0.9.0 installations
 */
async function migrateComposePorts_Pre090(containerManager: ContainerManager): Promise<void> {
    logger.info("[migrateComposePorts_Pre090]: Performing migrations for 0.9.0");

    // Compose migration
    if (await containerManager.exists()) {
        logger.info("[migrateComposePorts_Pre090]: Composing down current WinBoat container");
        await containerManager.compose("down");
    }

    const currentCompose = Winboat.readCompose(containerManager.composeFilePath);
    const defaultCompose = containerManager.defaultCompose;

    currentCompose.services.windows.ports = defaultCompose.services.windows.ports;
    currentCompose.services.windows.image = defaultCompose.services.windows.image;
    currentCompose.services.windows.environment["USER_PORTS"] = defaultCompose.services.windows.environment["USER_PORTS"];

    containerManager.writeCompose(currentCompose);

    logger.info("[migrateComposePorts_Pre090]: Composing up WinBoat container");
    await containerManager.compose("up", ["--no-start"]);
}
```

## /src/renderer/main.ts

```ts path="/src/renderer/main.ts" 
import { createApp } from "vue";
import App from "./App.vue";
import { router } from "./router";
import { MotionPlugin } from "@vueuse/motion";
import "./index.css";
import { autoScroll } from "./directives/autoscroll";
import { DEFAULT_HOMEBREW_DIR } from "./lib/constants";
import VueApexCharts from "vue3-apexcharts";

const process: typeof import("process") = require("node:process");

/**
 * @note A big chunk of our userbase uses WinBoat under an immutable distro through GearLever.
 * In case it's the flatpak version of GearLever, PATH, and some other environment variables are stripped by default.
 * We include the default homebrew bin directory for exactly this reason.
 * It's not WinBoat's responsibility if the PATH envvar is incomplete, but in this case it affects a lot of users.
 */
process.env.PATH && (process.env.PATH += `:${DEFAULT_HOMEBREW_DIR}`);

createApp(App)
    .directive("auto-scroll", autoScroll)
    .use(router)
    .use(MotionPlugin)
    .use(VueApexCharts as any) // TODO: See https://github.com/apexcharts/vue3-apexcharts/issues/141
    .mount("#app");

```

## /src/renderer/public/img/pfp.svg

```svg path="/src/renderer/public/img/pfp.svg" 
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32"><title>ProfileIcon</title><circle cx="16" cy="16" r="16" fill="#666"/><path d="M12.73 13.1a3.271 3.271 0 1 1 3.27 3.2 3.237 3.237 0 0 1-3.27-3.2zm-2.73 9.069h1.088a4.91 4.91 0 0 1 9.818 0h1.094a5.884 5.884 0 0 0-3.738-5.434 4.238 4.238 0 0 0 2.1-3.635 4.366 4.366 0 0 0-8.73 0 4.238 4.238 0 0 0 2.1 3.635 5.878 5.878 0 0 0-3.732 5.434z" fill="#eee"/><path fill="none" d="M0 0h32v32h-32z"/></svg>

```


The content has been capped at 50000 tokens. The user could consider applying other filters to refine the result. The better and more specific the context, the better the LLM can follow instructions. If the context seems verbose, the user can refine the filter using uithub. Thank you for using https://uithub.com - Perfect LLM context for any GitHub repo.
Copied!