From af115e4fdab56657616a69373be2eb882e92adf5 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:10:58 -0230 Subject: [PATCH 01/16] improve popup update flow with platform installer links --- extension/src/popup/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/extension/src/popup/index.ts b/extension/src/popup/index.ts index 9447ecf..a9a4d2c 100644 --- a/extension/src/popup/index.ts +++ b/extension/src/popup/index.ts @@ -159,7 +159,11 @@ btnOpenDiscord?.addEventListener('click', () => { }); btnUpdate?.addEventListener('click', () => { - void chrome.tabs.create({ url: 'https://github.com/ClickSentinel/FreeMiD/releases/latest' }); + const isWindows = /Win/i.test(navigator.platform); + const scriptUrl = isWindows + ? 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/install.ps1' + : 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/install.sh'; + void chrome.tabs.create({ url: scriptUrl }); }); btnInstallHost?.addEventListener('click', () => { From ee0ade0089dc4860a3ff8bb6d685863fc2109a09 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:21:29 -0230 Subject: [PATCH 02/16] harden windows installer release artifacts and checksum parsing --- .github/workflows/ci.yml | 29 +++++++++ .github/workflows/release.yml | 30 +++++++++ docs/WINDOWS-INSTALLER-PLAN.md | 115 +++++++++++++++++++++++++++++++++ install/install.ps1 | 17 +++-- 4 files changed, 186 insertions(+), 5 deletions(-) create mode 100644 docs/WINDOWS-INSTALLER-PLAN.md diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e590fd9..0fee3ac 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,35 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: + installer-defaults: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Verify installer default extension ID consistency + run: | + set -euo pipefail + + sh_id=$(sed -n 's/^DEFAULT_EXTENSION_ID="\([a-z0-9]\+\)"$/\1/p' install/install.sh) + ps_id=$(sed -n 's/^\$DefaultExtensionId = "\([a-z0-9]\+\)"$/\1/p' install/install.ps1) + rs_id=$(sed -n 's/^ const DEFAULT_EXTENSION_ID: &str = "\([a-z0-9]\+\)";$/\1/p' installer/src/main.rs) + + if [[ -z "$sh_id" || -z "$ps_id" || -z "$rs_id" ]]; then + echo "Failed to parse one or more default extension IDs" + exit 1 + fi + + if [[ "$sh_id" != "$ps_id" || "$sh_id" != "$rs_id" ]]; then + echo "Installer default extension IDs are inconsistent" + echo "install.sh: $sh_id" + echo "install.ps1: $ps_id" + echo "installer/src/main.rs: $rs_id" + exit 1 + fi + + echo "Default extension ID is consistent: $sh_id" + extension: runs-on: ubuntu-latest defaults: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c0341df..d31d08f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,11 +97,21 @@ jobs: run: copy target\release\freemid.exe ${{ matrix.artifact }} if: runner.os == 'Windows' + - name: Collect setup binary (Windows) + run: copy target\release\freemid-setup.exe freemid-setup.exe + if: runner.os == 'Windows' + - uses: actions/upload-artifact@v7 with: name: ${{ matrix.artifact }} path: ${{ matrix.artifact }} + - uses: actions/upload-artifact@v7 + if: runner.os == 'Windows' + with: + name: freemid-setup.exe + path: freemid-setup.exe + # Create the GitHub release with all artifacts release: needs: [audit-rust, build-extension, build-native] @@ -120,6 +130,7 @@ jobs: cp artifacts/freemid-macos-arm64/freemid-macos-arm64 . cp artifacts/freemid-macos-x86_64/freemid-macos-x86_64 . cp artifacts/freemid-windows-x86_64.exe/freemid-windows-x86_64.exe . + cp artifacts/freemid-setup.exe/freemid-setup.exe . chmod +x freemid-linux-x86_64 freemid-macos-arm64 freemid-macos-x86_64 - name: Generate checksums @@ -129,9 +140,27 @@ jobs: freemid-macos-arm64 \ freemid-macos-x86_64 \ freemid-windows-x86_64.exe \ + freemid-setup.exe \ freemid-extension.zip \ > checksums.sha256 + - name: Validate checksum coverage + run: | + required=( + freemid-linux-x86_64 + freemid-macos-arm64 + freemid-macos-x86_64 + freemid-windows-x86_64.exe + freemid-setup.exe + freemid-extension.zip + ) + for f in "${required[@]}"; do + grep -q "[[:space:]]$f$" checksums.sha256 || { + echo "Missing checksum entry for $f" + exit 1 + } + done + - uses: softprops/action-gh-release@v2 with: prerelease: ${{ contains(github.ref_name, '-') }} @@ -141,6 +170,7 @@ jobs: freemid-macos-arm64 freemid-macos-x86_64 freemid-windows-x86_64.exe + freemid-setup.exe checksums.sha256 install/install.sh install/install.ps1 diff --git a/docs/WINDOWS-INSTALLER-PLAN.md b/docs/WINDOWS-INSTALLER-PLAN.md new file mode 100644 index 0000000..029299b --- /dev/null +++ b/docs/WINDOWS-INSTALLER-PLAN.md @@ -0,0 +1,115 @@ +# Windows Installer and Uninstaller Plan + +## Goals + +1. Ship a dead-simple Windows GUI installer experience. +2. Provide a matching dead-simple uninstall experience. +3. Keep installation non-admin (HKCU + LOCALAPPDATA). +4. Ensure release artifacts and checksums are always in sync. +5. Ensure all installers use the production extension ID by default. + +## Current Gaps + +1. `freemid-setup.exe` is documented but not published by the release workflow. +2. Windows install can fail if `checksums.sha256` does not contain the expected Windows artifact line. +3. Uninstall is script-only (`uninstall.ps1`) and not exposed as a GUI workflow. +4. Extension ID consistency must be validated across all installer entrypoints on every release. + +## Phase 1: Make Release Artifacts Reliable (First) + +1. Update `.github/workflows/release.yml` to build and upload `freemid-setup.exe` from the `installer` crate. +2. Include `freemid-setup.exe` in release assets uploaded by `softprops/action-gh-release`. +3. Generate `checksums.sha256` from the final release file set and include all expected artifacts: + - `freemid-windows-x86_64.exe` + - `freemid-setup.exe` + - existing Linux/macOS binaries + - `freemid-extension.zip` +4. Add a release validation step that fails if `checksums.sha256` is missing any required filenames. +5. Add a post-release smoke test job that downloads `install.ps1` + `checksums.sha256` and verifies the artifact lookup succeeds for Windows. + +## Phase 2: Unify and Harden Installer Defaults + +1. Keep production extension ID default in all installer paths: + - `install/install.ps1` + - `install/install.sh` + - `installer/src/main.rs` +2. Add a CI check script that asserts these defaults are identical. +3. Add an install script runtime warning when non-default extension ID is used (for clarity, not failure). +4. Improve checksum parser robustness in PowerShell installer: + - normalize line endings + - tolerate extra whitespace + - fail with explicit diagnostics showing artifact name and first lines of checksum file +5. Keep checksum mismatch behavior strict (delete downloaded binary and exit non-zero). + +## Phase 3: GUI Installer UX (Dead Simple) + +1. Convert `freemid-setup.exe` to true GUI mode (`windows_subsystem = "windows"`). +2. Implement one lightweight window with these actions: + - Install or Update + - Uninstall + - Open troubleshooting docs +3. Display simple progress states: + - Stopping existing process + - Downloading + - Verifying checksum + - Registering host + - Complete or failed +4. Show actionable error dialogs with copyable details. +5. Keep advanced options hidden by default: + - custom extension ID + - release tag override + +## Phase 4: GUI Uninstaller Support + +1. Add uninstall execution path to Rust installer binary (same logic as `uninstall.ps1`). +2. Ensure uninstall removes: + - browser host registry entries (HKCU) + - manifest + - `freemid.exe` (unless user chooses keep-binary) +3. Add confirmation dialog and completion status. +4. Preserve script uninstall for automation and fallback. + +## Phase 5: Windows App Lifecycle Integration + +1. Register in Apps and Features (ARP) under HKCU: + - DisplayName + - DisplayVersion + - Publisher + - UninstallString (pointing to GUI uninstaller mode) +2. Add Start Menu shortcuts: + - FreeMiD Setup + - Uninstall FreeMiD +3. Optional: add file version metadata and code signing preparation notes. + +## Phase 6: Docs and Extension UX Alignment + +1. Update README to match actual published files and exact install paths. +2. Update popup Update and Uninstall behavior on Windows: + - prefer GUI setup executable when available + - fallback to script URLs when not available +3. Add troubleshooting section for checksum/artifact mismatch. + +## Validation Checklist (Release Gate) + +1. `install.ps1` succeeds on clean Windows profile. +2. `freemid-setup.exe` install succeeds on clean Windows profile. +3. GUI uninstall removes all expected artifacts and registry keys. +4. `checksums.sha256` contains all required release files. +5. Default extension ID is identical across all installer entrypoints. +6. Reinstall over existing version works without admin rights. +7. Browser restart guidance appears after successful install. + +## Delivery Sequence + +1. PR 1: release artifact and checksum reliability (Phase 1). +2. PR 2: default consistency checks + script hardening (Phase 2). +3. PR 3: GUI installer shell + install flow (Phase 3). +4. PR 4: GUI uninstall + ARP integration (Phases 4 and 5). +5. PR 5: docs and popup polish (Phase 6). + +## Owner Notes + +1. Keep all Windows paths under `%LOCALAPPDATA%\\FreeMiD`. +2. Keep registry writes under `HKCU` only. +3. Keep `uninstall.ps1` as permanent fallback even after GUI uninstall is shipped. +4. Keep checksum verification mandatory for downloaded binaries. diff --git a/install/install.ps1 b/install/install.ps1 index 0726c78..6f1c7d5 100644 --- a/install/install.ps1 +++ b/install/install.ps1 @@ -75,14 +75,21 @@ if ($Binary) { Remove-Item $BinDst -Force -ErrorAction SilentlyContinue exit 1 } - $ExpectedHash = ($ChecksumData -split "`n" | - Where-Object { $_ -match "\s+$([regex]::Escape($Artifact))$" } | - Select-Object -First 1) -split '\s+' | Select-Object -First 1 - if (-not $ExpectedHash) { - Write-Error "Could not find checksum for $Artifact in checksums.sha256" + $ChecksumLines = $ChecksumData -split "`n" | + ForEach-Object { $_.Trim() } | + Where-Object { $_ } + + $ChecksumEntry = $ChecksumLines | + Where-Object { $_ -match "(^|\s)\*?$([regex]::Escape($Artifact))$" } | + Select-Object -First 1 + + if (-not $ChecksumEntry) { + $Preview = ($ChecksumLines | Select-Object -First 5) -join "`n" + Write-Error "Could not find checksum for $Artifact in checksums.sha256.`nFirst entries:`n$Preview" Remove-Item $BinDst -Force exit 1 } + $ExpectedHash = ($ChecksumEntry -split '\s+')[0] $ActualHash = (Get-FileHash $BinDst -Algorithm SHA256).Hash.ToLower() if ($ActualHash -ne $ExpectedHash.ToLower()) { Write-Error "Checksum mismatch!`n Expected: $ExpectedHash`n Actual: $ActualHash" From 5c64c3bf1f35086cbb8f9eedf4f11a4015d51f2f Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:32:06 -0230 Subject: [PATCH 03/16] add repo yamllint config for workflow files --- .yamllint | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .yamllint diff --git a/.yamllint b/.yamllint new file mode 100644 index 0000000..18d10c7 --- /dev/null +++ b/.yamllint @@ -0,0 +1,9 @@ +extends: default + +rules: + document-start: disable + line-length: + max: 140 + level: warning + truthy: + check-keys: false \ No newline at end of file From e1494fa323aabbb96d3acf01220d572e654c8c2e Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:34:13 -0230 Subject: [PATCH 04/16] make windows setup app show graphical completion dialogs --- installer/src/main.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index 241c8fc..c927336 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -9,6 +9,8 @@ // To install a specific release tag: // set FREEMID_RELEASE_TAG=v0.3.1 && freemid-setup.exe +#![cfg_attr(target_os = "windows", windows_subsystem = "windows")] + #[cfg(not(target_os = "windows"))] fn main() { eprintln!("freemid-setup is a Windows-only installer."); @@ -17,7 +19,6 @@ fn main() { #[cfg(target_os = "windows")] mod win { - use std::io::{self, Read, Write}; use std::path::PathBuf; use std::process::Command; @@ -36,14 +37,18 @@ mod win { Ok(()) => { println!(); println!("Installation complete. Restart Chrome or Edge to activate."); + show_message( + "FreeMiD Setup", + "Installation complete. Restart Chrome or Edge to activate.", + false, + ); } Err(e) => { eprintln!(); eprintln!("ERROR: {}", e); + show_message("FreeMiD Setup - Error", &e, true); } } - - pause(); } fn run() -> Result<(), String> { @@ -215,11 +220,19 @@ mod win { } } - fn pause() { - print!("\nPress Enter to exit..."); - let _ = io::stdout().flush(); - let mut buf = [0u8; 1]; - let _ = io::stdin().read(&mut buf); + fn show_message(title: &str, body: &str, is_error: bool) { + let icon = if is_error { "Error" } else { "Information" }; + let cmd = format!( + "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('{body}','{title}','OK','{icon}') | Out-Null", + body = ps_quote(body), + title = ps_quote(title), + icon = icon, + ); + let _ = ps_run(&cmd); + } + + fn ps_quote(s: &str) -> String { + s.replace('\\', "\\\\").replace('\'', "''") } } From 7dffe7b22a200cde729a25234a4a95194f4a1770 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:37:07 -0230 Subject: [PATCH 05/16] register FreeMiD in windows apps and features --- installer/src/main.rs | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index c927336..61d23d1 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -27,6 +27,7 @@ mod win { const HOST_NAME: &str = "com.clicksentinel.freemid"; const DEFAULT_EXTENSION_ID: &str = "gaonohfjfpdlfapccfaanenfcojfknli"; const VERSION: &str = env!("CARGO_PKG_VERSION"); + const UNINSTALL_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\FreeMiD"; pub fn run_main() { println!("FreeMiD Setup v{}", VERSION); @@ -144,7 +145,7 @@ mod win { std::fs::write(&manifest_path, &manifest) .map_err(|e| format!("Cannot write manifest: {}", e))?; - println!("[5/5] Registering native messaging host..."); + println!("[5/6] Registering native messaging host..."); let manifest_str = manifest_path.display().to_string(); for (name, parent) in [ ("Chrome", r"HKCU\Software\Google\Chrome\NativeMessagingHosts"), @@ -157,10 +158,14 @@ mod win { } } + println!("[6/6] Registering Apps & Features entry..."); + register_arp(&install_dir, &bin_dst)?; + println!(); println!(" Binary: {}", bin_dst.display()); println!(" Manifest: {}", manifest_path.display()); println!(" Extension: {}", extension_id); + println!(" ARP Key: {}", UNINSTALL_KEY); Ok(()) } @@ -220,6 +225,38 @@ mod win { } } + fn reg_set_named(key: &str, name: &str, typ: &str, value: &str) -> Result<(), String> { + let status = Command::new("reg") + .args(["add", key, "/v", name, "/t", typ, "/d", value, "/f"]) + .status() + .map_err(|e| format!("Failed to spawn reg.exe: {}", e))?; + if status.success() { + Ok(()) + } else { + Err(format!("reg add failed for key {} value {}", key, name)) + } + } + + fn register_arp(install_dir: &PathBuf, bin_dst: &PathBuf) -> Result<(), String> { + let install_location = install_dir.display().to_string(); + let display_icon = bin_dst.display().to_string(); + let uninstall_cmd = format!( + "powershell -NoProfile -ExecutionPolicy Bypass -Command \"irm https://github.com/{}/releases/latest/download/uninstall.ps1 | iex\"", + GITHUB_REPO + ); + + reg_set_named(UNINSTALL_KEY, "DisplayName", "REG_SZ", "FreeMiD")?; + reg_set_named(UNINSTALL_KEY, "DisplayVersion", "REG_SZ", VERSION)?; + reg_set_named(UNINSTALL_KEY, "Publisher", "REG_SZ", "ClickSentinel")?; + reg_set_named(UNINSTALL_KEY, "InstallLocation", "REG_SZ", &install_location)?; + reg_set_named(UNINSTALL_KEY, "DisplayIcon", "REG_SZ", &display_icon)?; + reg_set_named(UNINSTALL_KEY, "UninstallString", "REG_SZ", &uninstall_cmd)?; + reg_set_named(UNINSTALL_KEY, "NoModify", "REG_DWORD", "1")?; + reg_set_named(UNINSTALL_KEY, "NoRepair", "REG_DWORD", "1")?; + + Ok(()) + } + fn show_message(title: &str, body: &str, is_error: bool) { let icon = if is_error { "Error" } else { "Information" }; let cmd = format!( From f68bd21c1a1c18a794505173ee3f360cc3a4db2a Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:42:13 -0230 Subject: [PATCH 06/16] allow windows setup to install local binary for dev testing --- installer/src/main.rs | 99 ++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 43 deletions(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index 61d23d1..16a8d38 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -27,6 +27,7 @@ mod win { const HOST_NAME: &str = "com.clicksentinel.freemid"; const DEFAULT_EXTENSION_ID: &str = "gaonohfjfpdlfapccfaanenfcojfknli"; const VERSION: &str = env!("CARGO_PKG_VERSION"); + const LOCAL_BINARY_ENV: &str = "FREEMID_BINARY"; const UNINSTALL_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\FreeMiD"; pub fn run_main() { @@ -89,50 +90,62 @@ mod win { } } - let tag = std::env::var("FREEMID_RELEASE_TAG").unwrap_or_else(|_| "latest".to_string()); - let (download_url, checksums_url) = build_urls(&tag); - - println!("[2/5] Downloading {} ...", ARTIFACT); - println!(" From: {}", download_url); - ps_run(&format!( - "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing", - download_url, - bin_dst.display() - ))?; - - let size_mb = std::fs::metadata(&bin_dst) - .map(|m| m.len() as f64 / 1_048_576.0) - .unwrap_or(0.0); - println!(" Downloaded ({:.2} MB)", size_mb); - - println!("[3/5] Verifying SHA256 checksum..."); - // .Content is byte[] on PS5 when Content-Type is octet-stream; decode explicitly. - let checksums_raw = ps_output(&format!( - "[System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -Uri '{}' -UseBasicParsing).Content)", - checksums_url - ))?; - - let expected = checksums_raw - .lines() - .find(|l| l.trim_end().ends_with(ARTIFACT)) - .and_then(|l| l.split_whitespace().next()) - .ok_or_else(|| format!("Entry for {} not found in checksums.sha256", ARTIFACT))? - .to_lowercase(); - - let actual = ps_output(&format!( - "(Get-FileHash '{}' -Algorithm SHA256).Hash.ToLower()", - bin_dst.display() - ))?; - let actual = actual.trim().to_lowercase(); - - if actual != expected { - let _ = std::fs::remove_file(&bin_dst); - return Err(format!( - "Checksum mismatch!\n Expected: {}\n Actual: {}", - expected, actual - )); + if let Ok(local_binary) = std::env::var(LOCAL_BINARY_ENV) { + println!("[2/6] Installing from local binary..."); + println!(" From: {}", local_binary); + std::fs::copy(&local_binary, &bin_dst) + .map_err(|e| format!("Failed to copy local binary {}: {}", local_binary, e))?; + let size_mb = std::fs::metadata(&bin_dst) + .map(|m| m.len() as f64 / 1_048_576.0) + .unwrap_or(0.0); + println!(" Installed ({:.2} MB)", size_mb); + println!("[3/6] Skipping checksum (local binary mode)..."); + } else { + let tag = std::env::var("FREEMID_RELEASE_TAG").unwrap_or_else(|_| "latest".to_string()); + let (download_url, checksums_url) = build_urls(&tag); + + println!("[2/6] Downloading {} ...", ARTIFACT); + println!(" From: {}", download_url); + ps_run(&format!( + "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing", + download_url, + bin_dst.display() + ))?; + + let size_mb = std::fs::metadata(&bin_dst) + .map(|m| m.len() as f64 / 1_048_576.0) + .unwrap_or(0.0); + println!(" Downloaded ({:.2} MB)", size_mb); + + println!("[3/6] Verifying SHA256 checksum..."); + // .Content is byte[] on PS5 when Content-Type is octet-stream; decode explicitly. + let checksums_raw = ps_output(&format!( + "[System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -Uri '{}' -UseBasicParsing).Content)", + checksums_url + ))?; + + let expected = checksums_raw + .lines() + .find(|l| l.trim_end().ends_with(ARTIFACT)) + .and_then(|l| l.split_whitespace().next()) + .ok_or_else(|| format!("Entry for {} not found in checksums.sha256", ARTIFACT))? + .to_lowercase(); + + let actual = ps_output(&format!( + "(Get-FileHash '{}' -Algorithm SHA256).Hash.ToLower()", + bin_dst.display() + ))?; + let actual = actual.trim().to_lowercase(); + + if actual != expected { + let _ = std::fs::remove_file(&bin_dst); + return Err(format!( + "Checksum mismatch!\n Expected: {}\n Actual: {}", + expected, actual + )); + } + println!(" OK {}...", &actual[..16]); } - println!(" OK {}...", &actual[..16]); println!("[4/5] Writing native messaging manifest..."); let bin_path_json = bin_dst.display().to_string().replace('\\', "\\\\"); From 150e3ab4155a2e8169c35567da372444731ab1be Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 10:56:28 -0230 Subject: [PATCH 07/16] add windows artifact build workflow for dev testing --- .github/workflows/windows-artifacts.yml | 61 +++++++++++++++++++++++++ 1 file changed, 61 insertions(+) create mode 100644 .github/workflows/windows-artifacts.yml diff --git a/.github/workflows/windows-artifacts.yml b/.github/workflows/windows-artifacts.yml new file mode 100644 index 0000000..b7653a1 --- /dev/null +++ b/.github/workflows/windows-artifacts.yml @@ -0,0 +1,61 @@ +name: Windows Artifacts + +on: + workflow_dispatch: + push: + branches: + - dev/ux-improvements-auto-update + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + build-windows-artifacts: + runs-on: windows-latest + permissions: + contents: read + + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + + - name: Cache Cargo + uses: actions/cache@v5 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + target + key: windows-artifacts-${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} + + - name: Ensure DISCORD_CLIENT_ID fallback + shell: pwsh + env: + DISCORD_CLIENT_ID: ${{ secrets.DISCORD_CLIENT_ID }} + run: | + if ([string]::IsNullOrWhiteSpace($env:DISCORD_CLIENT_ID)) { + "DISCORD_CLIENT_ID=placeholder-ci-build" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + } else { + "DISCORD_CLIENT_ID=$env:DISCORD_CLIENT_ID" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append + } + + - name: Build Windows binaries + run: cargo build --release -p freemid -p freemid-installer + + - name: Collect artifacts + shell: pwsh + run: | + New-Item -ItemType Directory -Path artifacts -Force | Out-Null + Copy-Item target/release/freemid.exe artifacts/freemid-windows-x86_64.exe + Copy-Item target/release/freemid-setup.exe artifacts/freemid-setup.exe + Copy-Item install/install.ps1 artifacts/install.ps1 + Copy-Item install/uninstall.ps1 artifacts/uninstall.ps1 + + - name: Upload Windows artifacts + uses: actions/upload-artifact@v7 + with: + name: freemid-windows-artifacts-${{ github.sha }} + path: artifacts/* From 339687ba72ca711aefaa9f3bbec1807e133625cd Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 11:20:33 -0230 Subject: [PATCH 08/16] add windows installer gui window --- installer/Cargo.toml | 3 + installer/src/main.rs | 252 ++++++++++++++++++++++++++++++++++-------- 2 files changed, 210 insertions(+), 45 deletions(-) diff --git a/installer/Cargo.toml b/installer/Cargo.toml index e3936a4..9f29866 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -9,3 +9,6 @@ description = "FreeMiD Windows installer" [[bin]] name = "freemid-setup" path = "src/main.rs" + +[target.'cfg(windows)'.dependencies] +native-windows-gui = "1" diff --git a/installer/src/main.rs b/installer/src/main.rs index 16a8d38..4116ad9 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -1,7 +1,7 @@ // FreeMiD Windows Installer // -// Double-click freemid-setup.exe to install the FreeMiD native messaging host. -// Re-running is safe — kills the running process first so the file is never locked. +// Double-click freemid-setup.exe to open a small Windows GUI with install and +// uninstall actions. // // To use a custom extension ID: // set FREEMID_EXTENSION_ID=yourextensionid && freemid-setup.exe @@ -22,6 +22,8 @@ mod win { use std::path::PathBuf; use std::process::Command; + use native_windows_gui as nwg; + const GITHUB_REPO: &str = "ClickSentinel/FreeMiD"; const ARTIFACT: &str = "freemid-windows-x86_64.exe"; const HOST_NAME: &str = "com.clicksentinel.freemid"; @@ -29,36 +31,153 @@ mod win { const VERSION: &str = env!("CARGO_PKG_VERSION"); const LOCAL_BINARY_ENV: &str = "FREEMID_BINARY"; const UNINSTALL_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\FreeMiD"; + const README_URL: &str = "https://github.com/ClickSentinel/FreeMiD#installation"; pub fn run_main() { - println!("FreeMiD Setup v{}", VERSION); - println!("{}", "-".repeat(38)); - println!(); + if let Err(e) = run_gui() { + eprintln!("ERROR: {}", e); + } + } - match run() { - Ok(()) => { - println!(); - println!("Installation complete. Restart Chrome or Edge to activate."); - show_message( - "FreeMiD Setup", - "Installation complete. Restart Chrome or Edge to activate.", - false, - ); - } - Err(e) => { - eprintln!(); - eprintln!("ERROR: {}", e); - show_message("FreeMiD Setup - Error", &e, true); + fn run_gui() -> Result<(), String> { + nwg::init().map_err(|e| format!("Failed to initialize GUI: {}", e))?; + nwg::Font::set_global_family("Segoe UI").map_err(|e| format!("Failed to set UI font: {}", e))?; + + let ui = Ui::build().map_err(|e| format!("Failed to build UI: {}", e))?; + let install_handle = ui.install_button.handle.clone(); + let uninstall_handle = ui.uninstall_button.handle.clone(); + let docs_handle = ui.docs_button.handle.clone(); + let window_handle = ui.window.handle.clone(); + + let ui = std::rc::Rc::new(std::cell::RefCell::new(ui)); + let ui_events = std::rc::Rc::clone(&ui); + + let evt_handler = nwg::full_bind_event_handler(&window_handle, move |evt, _evt_data, handle| { + use nwg::Event as E; + + match evt { + E::OnWindowClose => { + nwg::stop_thread_dispatch(); + } + E::OnButtonClick if handle == install_handle => { + let extension_id = std::env::var("FREEMID_EXTENSION_ID") + .unwrap_or_else(|_| DEFAULT_EXTENSION_ID.to_string()); + let result = run_install(&extension_id); + match result { + Ok(()) => nwg::simple_message( + "FreeMiD Setup", + "Installation complete. Restart Chrome or Edge to activate.", + ), + Err(e) => nwg::simple_message("FreeMiD Setup - Error", &e), + } + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Ready"); + } + E::OnButtonClick if handle == uninstall_handle => { + let result = run_uninstall(); + match result { + Ok(()) => nwg::simple_message( + "FreeMiD Setup", + "FreeMiD native host uninstalled.", + ), + Err(e) => nwg::simple_message("FreeMiD Setup - Error", &e), + } + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Ready"); + } + E::OnButtonClick if handle == docs_handle => { + open_docs(); + } + _ => {} } + }); + + ui.borrow_mut().evt_handler = Some(evt_handler); + nwg::dispatch_thread_events(); + Ok(()) + } + + struct Ui { + window: nwg::Window, + title: nwg::Label, + status: nwg::Label, + install_button: nwg::Button, + uninstall_button: nwg::Button, + docs_button: nwg::Button, + evt_handler: Option, + } + + impl Ui { + fn build() -> Result { + let mut window = nwg::Window::default(); + let mut title = nwg::Label::default(); + let mut status = nwg::Label::default(); + let mut install_button = nwg::Button::default(); + let mut uninstall_button = nwg::Button::default(); + let mut docs_button = nwg::Button::default(); + + nwg::Window::builder() + .size((420, 210)) + .position((300, 300)) + .title(&format!("FreeMiD Setup v{}", VERSION)) + .flags(nwg::WindowFlags::MAIN_WINDOW | nwg::WindowFlags::VISIBLE) + .build(&mut window)?; + + nwg::Label::builder() + .text("Choose what you want to do with FreeMiD.") + .parent(&window) + .position((16, 16)) + .size((388, 24)) + .build(&mut title)?; + + nwg::Label::builder() + .text("Ready") + .parent(&window) + .position((16, 48)) + .size((388, 24)) + .build(&mut status)?; + + nwg::Button::builder() + .text("Install or Update") + .parent(&window) + .position((16, 92)) + .size((120, 36)) + .build(&mut install_button)?; + + nwg::Button::builder() + .text("Uninstall") + .parent(&window) + .position((148, 92)) + .size((120, 36)) + .build(&mut uninstall_button)?; + + nwg::Button::builder() + .text("Troubleshooting") + .parent(&window) + .position((280, 92)) + .size((120, 36)) + .build(&mut docs_button)?; + + Ok(Self { + window, + title, + status, + install_button, + uninstall_button, + docs_button, + evt_handler: None, + }) } } - fn run() -> Result<(), String> { - let extension_id = std::env::var("FREEMID_EXTENSION_ID") - .unwrap_or_else(|_| DEFAULT_EXTENSION_ID.to_string()); + fn run_install(extension_id: &str) -> Result<(), String> { + set_status("Installing..."); + + println!("FreeMiD Setup v{}", VERSION); + println!("{}", "-".repeat(38)); + println!(); println!("[1/5] Stopping any running FreeMiD process..."); - // taskkill exits non-zero if the process is not running — that is fine. let _ = Command::new("taskkill") .args(["/F", "/IM", "freemid.exe", "/T"]) .output(); @@ -72,7 +191,6 @@ mod win { std::fs::create_dir_all(&install_dir) .map_err(|e| format!("Cannot create install directory: {}", e))?; - // Wait up to 5 s for freemid.exe to release its file handle. if bin_dst.exists() { let mut unlocked = false; for _ in 0..10 { @@ -84,8 +202,7 @@ mod win { } if !unlocked { return Err(format!( - "freemid.exe is still locked after 5 s. \ - Close any application using it and re-run the installer." + "freemid.exe is still locked after 5 s. Close any application using it and re-run the installer." )); } } @@ -118,7 +235,6 @@ mod win { println!(" Downloaded ({:.2} MB)", size_mb); println!("[3/6] Verifying SHA256 checksum..."); - // .Content is byte[] on PS5 when Content-Type is octet-stream; decode explicitly. let checksums_raw = ps_output(&format!( "[System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -Uri '{}' -UseBasicParsing).Content)", checksums_url @@ -147,7 +263,7 @@ mod win { println!(" OK {}...", &actual[..16]); } - println!("[4/5] Writing native messaging manifest..."); + println!("[4/6] Writing native messaging manifest..."); let bin_path_json = bin_dst.display().to_string().replace('\\', "\\\\"); let manifest = format!( "{{\n \"name\": \"{host}\",\n \"description\": \"FreeMiD native messaging host\",\n \"path\": \"{path}\",\n \"type\": \"stdio\",\n \"allowed_origins\": [\n \"chrome-extension://{ext_id}/\"\n ]\n}}", @@ -162,7 +278,7 @@ mod win { let manifest_str = manifest_path.display().to_string(); for (name, parent) in [ ("Chrome", r"HKCU\Software\Google\Chrome\NativeMessagingHosts"), - ("Edge", r"HKCU\Software\Microsoft\Edge\NativeMessagingHosts"), + ("Edge", r"HKCU\Software\Microsoft\Edge\NativeMessagingHosts"), ] { let key = format!(r"{}\{}", parent, HOST_NAME); match reg_set(&key, &manifest_str) { @@ -180,9 +296,58 @@ mod win { println!(" Extension: {}", extension_id); println!(" ARP Key: {}", UNINSTALL_KEY); + set_status("Ready"); Ok(()) } + fn run_uninstall() -> Result<(), String> { + set_status("Uninstalling..."); + + let local_app_data = std::env::var("LOCALAPPDATA") + .map_err(|_| "%LOCALAPPDATA% not set".to_string())?; + let install_dir = PathBuf::from(local_app_data).join("FreeMiD"); + let manifest_path = install_dir.join(format!("{}.json", HOST_NAME)); + let bin_dst = install_dir.join("freemid.exe"); + + let _ = Command::new("taskkill") + .args(["/F", "/IM", "freemid.exe", "/T"]) + .output(); + + for key in [ + UNINSTALL_KEY.to_string(), + r"HKCU\Software\Google\Chrome\NativeMessagingHosts\com.clicksentinel.freemid".to_string(), + r"HKCU\Software\Microsoft\Edge\NativeMessagingHosts\com.clicksentinel.freemid".to_string(), + ] { + let _ = reg_delete(&key); + } + + let _ = std::fs::remove_file(&manifest_path); + let _ = std::fs::remove_file(&bin_dst); + + if install_dir.exists() { + let has_remaining = std::fs::read_dir(&install_dir) + .ok() + .and_then(|mut it| it.next()) + .is_some(); + if !has_remaining { + let _ = std::fs::remove_dir(&install_dir); + } + } + + set_status("Ready"); + Ok(()) + } + + fn open_docs() { + let _ = Command::new("cmd") + .args(["/C", "start", "", README_URL]) + .status(); + } + + fn set_status(status: &str) { + let _ = status; + } + fn build_urls(tag: &str) -> (String, String) { let base = if tag == "latest" { format!( @@ -250,6 +415,18 @@ mod win { } } + fn reg_delete(key: &str) -> Result<(), String> { + let status = Command::new("reg") + .args(["delete", key, "/f"]) + .status() + .map_err(|e| format!("Failed to spawn reg.exe: {}", e))?; + if status.success() { + Ok(()) + } else { + Err(format!("reg delete failed for key: {}", key)) + } + } + fn register_arp(install_dir: &PathBuf, bin_dst: &PathBuf) -> Result<(), String> { let install_location = install_dir.display().to_string(); let display_icon = bin_dst.display().to_string(); @@ -269,24 +446,9 @@ mod win { Ok(()) } - - fn show_message(title: &str, body: &str, is_error: bool) { - let icon = if is_error { "Error" } else { "Information" }; - let cmd = format!( - "Add-Type -AssemblyName PresentationFramework; [System.Windows.MessageBox]::Show('{body}','{title}','OK','{icon}') | Out-Null", - body = ps_quote(body), - title = ps_quote(title), - icon = icon, - ); - let _ = ps_run(&cmd); - } - - fn ps_quote(s: &str) -> String { - s.replace('\\', "\\\\").replace('\'', "''") - } } #[cfg(target_os = "windows")] fn main() { win::run_main(); -} +} \ No newline at end of file From bdefaa92bb6225f122d5482f381202b572afcd11 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 11:24:36 -0230 Subject: [PATCH 09/16] lock native windows gui dependency for installer --- Cargo.lock | 250 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 250 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 4718ba4..e3049f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,30 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + [[package]] name = "freemid" version = "0.3.11" @@ -13,6 +37,33 @@ dependencies = [ [[package]] name = "freemid-installer" version = "0.3.11" +dependencies = [ + "native-windows-gui", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] [[package]] name = "itoa" @@ -20,12 +71,100 @@ version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" +[[package]] +name = "js-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11" +dependencies = [ + "cfg-if", + "futures-util", + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libm" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fc7aa29613bd6a620df431842069224d8bc9011086b1db4c0e0cd47fa03ec9a" + [[package]] name = "memchr" version = "2.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8" +[[package]] +name = "native-windows-gui" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f7003a669f68deb6b7c57d74fff4f8e533c44a3f0b297492440ef4ff5a28454" +dependencies = [ + "bitflags", + "lazy_static", + "newline-converter", + "plotters", + "plotters-backend", + "stretch", + "winapi", + "winapi-build", +] + +[[package]] +name = "newline-converter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -44,6 +183,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + [[package]] name = "serde" version = "1.0.228" @@ -87,6 +232,22 @@ dependencies = [ "zmij", ] +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "stretch" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b0dc6d20ce137f302edf90f9cd3d278866fd7fb139efca6f246161222ad6d87" +dependencies = [ + "lazy_static", + "libm", +] + [[package]] name = "syn" version = "2.0.117" @@ -104,6 +265,95 @@ version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" +[[package]] +name = "unicode-segmentation" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" + +[[package]] +name = "wasm-bindgen" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.122" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.99" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "zmij" version = "1.0.21" From b5e7c6179f5e5e363d7b21c240a1587e9a88f8cc Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 11:27:45 -0230 Subject: [PATCH 10/16] fix windows installer gui message box match arms --- installer/src/main.rs | 28 ++++++++++++++++++---------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/installer/src/main.rs b/installer/src/main.rs index 4116ad9..f8e908e 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -64,11 +64,15 @@ mod win { .unwrap_or_else(|_| DEFAULT_EXTENSION_ID.to_string()); let result = run_install(&extension_id); match result { - Ok(()) => nwg::simple_message( - "FreeMiD Setup", - "Installation complete. Restart Chrome or Edge to activate.", - ), - Err(e) => nwg::simple_message("FreeMiD Setup - Error", &e), + Ok(()) => { + nwg::simple_message( + "FreeMiD Setup", + "Installation complete. Restart Chrome or Edge to activate.", + ); + } + Err(e) => { + nwg::simple_message("FreeMiD Setup - Error", &e); + } } let mut ui = ui_events.borrow_mut(); ui.status.set_text("Ready"); @@ -76,11 +80,15 @@ mod win { E::OnButtonClick if handle == uninstall_handle => { let result = run_uninstall(); match result { - Ok(()) => nwg::simple_message( - "FreeMiD Setup", - "FreeMiD native host uninstalled.", - ), - Err(e) => nwg::simple_message("FreeMiD Setup - Error", &e), + Ok(()) => { + nwg::simple_message( + "FreeMiD Setup", + "FreeMiD native host uninstalled.", + ); + } + Err(e) => { + nwg::simple_message("FreeMiD Setup - Error", &e); + } } let mut ui = ui_events.borrow_mut(); ui.status.set_text("Ready"); From 7da18f612d484c110226d704759005476df985d1 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 11:38:57 -0230 Subject: [PATCH 11/16] embed explicit asInvoker manifest in windows setup exe --- Cargo.lock | 235 +++++++++++++++++++++++++++++++ installer/Cargo.toml | 3 + installer/build.rs | 9 ++ installer/freemid-setup.manifest | 18 +++ installer/freemid-setup.rc | 1 + 5 files changed, 266 insertions(+) create mode 100644 installer/build.rs create mode 100644 installer/freemid-setup.manifest create mode 100644 installer/freemid-setup.rc diff --git a/Cargo.lock b/Cargo.lock index e3049f9..ba26588 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20,12 +20,48 @@ version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "cc" +version = "1.2.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f" +dependencies = [ + "find-msvc-tools", + "shlex", +] + [[package]] name = "cfg-if" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" +[[package]] +name = "embed-resource" +version = "2.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d506610004cfc74a6f5ee7e8c632b355de5eca1f03ee5e5e0ec11b77d4eb3d61" +dependencies = [ + "cc", + "memchr", + "rustc_version", + "toml", + "vswhom", + "winreg", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + [[package]] name = "freemid" version = "0.3.11" @@ -38,6 +74,7 @@ dependencies = [ name = "freemid-installer" version = "0.3.11" dependencies = [ + "embed-resource", "native-windows-gui", ] @@ -65,6 +102,22 @@ dependencies = [ "slab", ] +[[package]] +name = "hashbrown" +version = "0.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a" + +[[package]] +name = "indexmap" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" +dependencies = [ + "equivalent", + "hashbrown", +] + [[package]] name = "itoa" version = "1.0.18" @@ -89,6 +142,12 @@ version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + [[package]] name = "libm" version = "0.1.4" @@ -183,12 +242,27 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + [[package]] name = "rustversion" version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" +[[package]] +name = "semver" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" + [[package]] name = "serde" version = "1.0.228" @@ -232,6 +306,21 @@ dependencies = [ "zmij", ] +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "shlex" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba" + [[package]] name = "slab" version = "0.4.12" @@ -259,6 +348,47 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + [[package]] name = "unicode-ident" version = "1.0.24" @@ -271,6 +401,26 @@ version = "1.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6f5d3c3b1bf09027a88a6bc961fc00497d651009560b5463668dc81b0fa87a8" +[[package]] +name = "vswhom" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be979b7f07507105799e854203b470ff7c78a1639e330a58f183b5fea574608b" +dependencies = [ + "libc", + "vswhom-sys", +] + +[[package]] +name = "vswhom-sys" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb067e4cbd1ff067d1df46c9194b5de0e98efd2810bbc95c5d5e5f25a3231150" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "wasm-bindgen" version = "0.2.122" @@ -354,6 +504,91 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "winnow" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" +dependencies = [ + "cfg-if", + "windows-sys", +] + [[package]] name = "zmij" version = "1.0.21" diff --git a/installer/Cargo.toml b/installer/Cargo.toml index 9f29866..522ac88 100644 --- a/installer/Cargo.toml +++ b/installer/Cargo.toml @@ -12,3 +12,6 @@ path = "src/main.rs" [target.'cfg(windows)'.dependencies] native-windows-gui = "1" + +[build-dependencies] +embed-resource = "2" diff --git a/installer/build.rs b/installer/build.rs new file mode 100644 index 0000000..506ac75 --- /dev/null +++ b/installer/build.rs @@ -0,0 +1,9 @@ +fn main() { + println!("cargo:rerun-if-changed=freemid-setup.manifest"); + println!("cargo:rerun-if-changed=freemid-setup.rc"); + + let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap_or_default(); + if target_os == "windows" { + embed_resource::compile("freemid-setup.rc", embed_resource::NONE); + } +} diff --git a/installer/freemid-setup.manifest b/installer/freemid-setup.manifest new file mode 100644 index 0000000..b3561e3 --- /dev/null +++ b/installer/freemid-setup.manifest @@ -0,0 +1,18 @@ + + + + FreeMiD Windows Installer + + + + + + + + + diff --git a/installer/freemid-setup.rc b/installer/freemid-setup.rc new file mode 100644 index 0000000..cb0a366 --- /dev/null +++ b/installer/freemid-setup.rc @@ -0,0 +1 @@ +1 24 "freemid-setup.manifest" From 176e654388b2f3f52162b5b2e2b9b49268e2d72f Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 12:01:59 -0230 Subject: [PATCH 12/16] reduce installer window flashing and show status checkmarks --- docs/WINDOWS-INSTALLER-PLAN.md | 2 ++ installer/src/main.rs | 37 +++++++++++++++++++++++----------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/docs/WINDOWS-INSTALLER-PLAN.md b/docs/WINDOWS-INSTALLER-PLAN.md index 029299b..a62fe88 100644 --- a/docs/WINDOWS-INSTALLER-PLAN.md +++ b/docs/WINDOWS-INSTALLER-PLAN.md @@ -88,6 +88,7 @@ - prefer GUI setup executable when available - fallback to script URLs when not available 3. Add troubleshooting section for checksum/artifact mismatch. +4. Update installer completion copy to avoid browser restart requirement and instead guide users to install or enable the extension. ## Validation Checklist (Release Gate) @@ -113,3 +114,4 @@ 2. Keep registry writes under `HKCU` only. 3. Keep `uninstall.ps1` as permanent fallback even after GUI uninstall is shipped. 4. Keep checksum verification mandatory for downloaded binaries. +5. Do not require browser restart in installer messaging; guide extension install/enable actions instead. diff --git a/installer/src/main.rs b/installer/src/main.rs index f8e908e..e5ef26e 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -19,6 +19,7 @@ fn main() { #[cfg(target_os = "windows")] mod win { + use std::os::windows::process::CommandExt; use std::path::PathBuf; use std::process::Command; @@ -32,6 +33,7 @@ mod win { const LOCAL_BINARY_ENV: &str = "FREEMID_BINARY"; const UNINSTALL_KEY: &str = r"HKCU\Software\Microsoft\Windows\CurrentVersion\Uninstall\FreeMiD"; const README_URL: &str = "https://github.com/ClickSentinel/FreeMiD#installation"; + const CREATE_NO_WINDOW: u32 = 0x08000000; pub fn run_main() { if let Err(e) = run_gui() { @@ -67,15 +69,17 @@ mod win { Ok(()) => { nwg::simple_message( "FreeMiD Setup", - "Installation complete. Restart Chrome or Edge to activate.", + "Installation complete. Install or enable the FreeMiD extension, then reload the extension page if needed.", ); + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Status: \u{2714} Installed"); } Err(e) => { nwg::simple_message("FreeMiD Setup - Error", &e); + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Status: Failed"); } } - let mut ui = ui_events.borrow_mut(); - ui.status.set_text("Ready"); } E::OnButtonClick if handle == uninstall_handle => { let result = run_uninstall(); @@ -85,13 +89,15 @@ mod win { "FreeMiD Setup", "FreeMiD native host uninstalled.", ); + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Status: \u{2714} Uninstalled"); } Err(e) => { nwg::simple_message("FreeMiD Setup - Error", &e); + let mut ui = ui_events.borrow_mut(); + ui.status.set_text("Status: Failed"); } } - let mut ui = ui_events.borrow_mut(); - ui.status.set_text("Ready"); } E::OnButtonClick if handle == docs_handle => { open_docs(); @@ -186,7 +192,7 @@ mod win { println!(); println!("[1/5] Stopping any running FreeMiD process..."); - let _ = Command::new("taskkill") + let _ = hidden_command("taskkill") .args(["/F", "/IM", "freemid.exe", "/T"]) .output(); @@ -318,6 +324,7 @@ mod win { let bin_dst = install_dir.join("freemid.exe"); let _ = Command::new("taskkill") + .creation_flags(CREATE_NO_WINDOW) .args(["/F", "/IM", "freemid.exe", "/T"]) .output(); @@ -347,7 +354,7 @@ mod win { } fn open_docs() { - let _ = Command::new("cmd") + let _ = hidden_command("cmd") .args(["/C", "start", "", README_URL]) .status(); } @@ -373,7 +380,7 @@ mod win { } fn ps_run(cmd: &str) -> Result<(), String> { - let status = Command::new("powershell") + let status = hidden_command("powershell") .args(["-NoProfile", "-NonInteractive", "-Command", cmd]) .status() .map_err(|e| format!("Failed to spawn PowerShell: {}", e))?; @@ -385,7 +392,7 @@ mod win { } fn ps_output(cmd: &str) -> Result { - let out = Command::new("powershell") + let out = hidden_command("powershell") .args(["-NoProfile", "-NonInteractive", "-Command", cmd]) .output() .map_err(|e| format!("Failed to spawn PowerShell: {}", e))?; @@ -400,7 +407,7 @@ mod win { } fn reg_set(key: &str, value: &str) -> Result<(), String> { - let status = Command::new("reg") + let status = hidden_command("reg") .args(["add", key, "/ve", "/t", "REG_SZ", "/d", value, "/f"]) .status() .map_err(|e| format!("Failed to spawn reg.exe: {}", e))?; @@ -412,7 +419,7 @@ mod win { } fn reg_set_named(key: &str, name: &str, typ: &str, value: &str) -> Result<(), String> { - let status = Command::new("reg") + let status = hidden_command("reg") .args(["add", key, "/v", name, "/t", typ, "/d", value, "/f"]) .status() .map_err(|e| format!("Failed to spawn reg.exe: {}", e))?; @@ -424,7 +431,7 @@ mod win { } fn reg_delete(key: &str) -> Result<(), String> { - let status = Command::new("reg") + let status = hidden_command("reg") .args(["delete", key, "/f"]) .status() .map_err(|e| format!("Failed to spawn reg.exe: {}", e))?; @@ -454,6 +461,12 @@ mod win { Ok(()) } + + fn hidden_command(program: &str) -> Command { + let mut cmd = Command::new(program); + cmd.creation_flags(CREATE_NO_WINDOW); + cmd + } } #[cfg(target_os = "windows")] From c282fc8c54f00211fbe4f2da6c1eb1aaa097dc14 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 13:41:13 -0230 Subject: [PATCH 13/16] refine installation instructions and button labels for clarity on Windows platform --- README.md | 4 +- extension/public/popup/index.html | 2 +- extension/src/popup/index.ts | 33 ++++++++++---- installer/src/main.rs | 75 +++++++++++++++++++++++-------- 4 files changed, 84 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index b4454b4..d7e0037 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,9 @@ $env:FREEMID_EXTENSION_ID = ""; irm .../install.ps1 | iex | macOS | `~/.local/bin/freemid` | `~/Library/Application Support//NativeMessagingHosts/` | | Windows | `%LOCALAPPDATA%\FreeMiD\freemid.exe` | `HKCU\Software\\NativeMessagingHosts\` | -### Step 3 — Restart your browser +### Step 3 — Reload extension (restart only if needed) -Chrome caches native messaging manifests at startup. A full restart (not just extension reload) is required after installation. +After installing the native host, reload the FreeMiD extension page once. If the host is not detected right away, restart your browser and check again. ### Step 4 — Verify diff --git a/extension/public/popup/index.html b/extension/public/popup/index.html index e3cd7c4..9d6671c 100644 --- a/extension/public/popup/index.html +++ b/extension/public/popup/index.html @@ -611,7 +611,7 @@

FreeMiD

- +
diff --git a/extension/src/popup/index.ts b/extension/src/popup/index.ts index a9a4d2c..d0d6f83 100644 --- a/extension/src/popup/index.ts +++ b/extension/src/popup/index.ts @@ -36,6 +36,19 @@ const timelineTotal = document.getElementById('timeline-total') as HTMLEleme if (versionEl) versionEl.textContent = `v${chrome.runtime.getManifest().version}`; +// Clarify button behavior by platform so Windows users know these actions open Setup. +const isWindowsPlatform = /Win/i.test(navigator.platform); +if (isWindowsPlatform) { + if (btnUpdate) { + btnUpdate.textContent = 'Open Setup ↗'; + btnUpdate.title = 'Open the latest FreeMiD setup executable'; + } + if (btnUninstall) { + btnUninstall.textContent = 'Open Setup'; + btnUninstall.title = 'Open setup and choose Uninstall'; + } +} + // ── Uptime ──────────────────────────────────────────────────────────────────── let uptimeInterval: ReturnType | null = null; @@ -159,11 +172,10 @@ btnOpenDiscord?.addEventListener('click', () => { }); btnUpdate?.addEventListener('click', () => { - const isWindows = /Win/i.test(navigator.platform); - const scriptUrl = isWindows - ? 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/install.ps1' + const url = isWindowsPlatform + ? 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/freemid-setup.exe' : 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/install.sh'; - void chrome.tabs.create({ url: scriptUrl }); + void chrome.tabs.create({ url }); }); btnInstallHost?.addEventListener('click', () => { @@ -172,11 +184,10 @@ btnInstallHost?.addEventListener('click', () => { }); btnUninstall?.addEventListener('click', () => { - const isWindows = /Win/i.test(navigator.platform); - const scriptUrl = isWindows - ? 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/uninstall.ps1' + const url = isWindowsPlatform + ? 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/freemid-setup.exe' : 'https://github.com/ClickSentinel/FreeMiD/releases/latest/download/uninstall.sh'; - void chrome.tabs.create({ url: scriptUrl }); + void chrome.tabs.create({ url }); }); // ── Render ──────────────────────────────────────────────────────────────────── @@ -246,7 +257,11 @@ function render(status: Status | null): void { if (updateBanner) { if (status.updateAvailable && status.latestVersion) { updateBanner.classList.remove('hidden'); - if (updateText) updateText.textContent = `Host update available: v${status.latestVersion}`; + if (updateText) { + updateText.textContent = isWindowsPlatform + ? `Host update available: v${status.latestVersion} (opens Setup)` + : `Host update available: v${status.latestVersion}`; + } } else { updateBanner.classList.add('hidden'); } diff --git a/installer/src/main.rs b/installer/src/main.rs index e5ef26e..0027a67 100644 --- a/installer/src/main.rs +++ b/installer/src/main.rs @@ -64,25 +64,37 @@ mod win { E::OnButtonClick if handle == install_handle => { let extension_id = std::env::var("FREEMID_EXTENSION_ID") .unwrap_or_else(|_| DEFAULT_EXTENSION_ID.to_string()); - let result = run_install(&extension_id); + let result = run_install(&extension_id, |msg| { + let mut ui = ui_events.borrow_mut(); + ui.status.set_text(msg); + }); match result { Ok(()) => { nwg::simple_message( "FreeMiD Setup", - "Installation complete. Install or enable the FreeMiD extension, then reload the extension page if needed.", + "Installation complete. Make sure the FreeMiD extension is installed and enabled. If the host is not detected right away, reload the extension page or restart the browser.", ); let mut ui = ui_events.borrow_mut(); ui.status.set_text("Status: \u{2714} Installed"); } Err(e) => { - nwg::simple_message("FreeMiD Setup - Error", &e); + nwg::simple_message( + "FreeMiD Setup - Error", + &format!( + "Install failed. Check your internet connection and verify you can access GitHub Releases.\n\nDetails:\n{}", + e + ), + ); let mut ui = ui_events.borrow_mut(); ui.status.set_text("Status: Failed"); } } } E::OnButtonClick if handle == uninstall_handle => { - let result = run_uninstall(); + let result = run_uninstall(|msg| { + let mut ui = ui_events.borrow_mut(); + ui.status.set_text(msg); + }); match result { Ok(()) => { nwg::simple_message( @@ -93,7 +105,13 @@ mod win { ui.status.set_text("Status: \u{2714} Uninstalled"); } Err(e) => { - nwg::simple_message("FreeMiD Setup - Error", &e); + nwg::simple_message( + "FreeMiD Setup - Error", + &format!( + "Uninstall failed. Close any running FreeMiD process and try again.\n\nDetails:\n{}", + e + ), + ); let mut ui = ui_events.borrow_mut(); ui.status.set_text("Status: Failed"); } @@ -115,6 +133,7 @@ mod win { window: nwg::Window, title: nwg::Label, status: nwg::Label, + note: nwg::Label, install_button: nwg::Button, uninstall_button: nwg::Button, docs_button: nwg::Button, @@ -126,12 +145,13 @@ mod win { let mut window = nwg::Window::default(); let mut title = nwg::Label::default(); let mut status = nwg::Label::default(); + let mut note = nwg::Label::default(); let mut install_button = nwg::Button::default(); let mut uninstall_button = nwg::Button::default(); let mut docs_button = nwg::Button::default(); nwg::Window::builder() - .size((420, 210)) + .size((420, 246)) .position((300, 300)) .title(&format!("FreeMiD Setup v{}", VERSION)) .flags(nwg::WindowFlags::MAIN_WINDOW | nwg::WindowFlags::VISIBLE) @@ -145,7 +165,7 @@ mod win { .build(&mut title)?; nwg::Label::builder() - .text("Ready") + .text("Status: Ready") .parent(&window) .position((16, 48)) .size((388, 24)) @@ -166,16 +186,24 @@ mod win { .build(&mut uninstall_button)?; nwg::Button::builder() - .text("Troubleshooting") + .text("Help / Latest Setup") .parent(&window) .position((280, 92)) .size((120, 36)) .build(&mut docs_button)?; + nwg::Label::builder() + .text("Setup installs the latest host by default. Use Help / Latest Setup to download the newest setup binary.") + .parent(&window) + .position((16, 142)) + .size((388, 62)) + .build(&mut note)?; + Ok(Self { window, title, status, + note, install_button, uninstall_button, docs_button, @@ -184,14 +212,18 @@ mod win { } } - fn run_install(extension_id: &str) -> Result<(), String> { - set_status("Installing..."); + fn run_install(extension_id: &str, mut set_status: F) -> Result<(), String> + where + F: FnMut(&str), + { + set_status("Status: Starting installation..."); println!("FreeMiD Setup v{}", VERSION); println!("{}", "-".repeat(38)); println!(); println!("[1/5] Stopping any running FreeMiD process..."); + set_status("Status: Stopping existing FreeMiD process..."); let _ = hidden_command("taskkill") .args(["/F", "/IM", "freemid.exe", "/T"]) .output(); @@ -223,6 +255,7 @@ mod win { if let Ok(local_binary) = std::env::var(LOCAL_BINARY_ENV) { println!("[2/6] Installing from local binary..."); + set_status("Status: Installing local binary..."); println!(" From: {}", local_binary); std::fs::copy(&local_binary, &bin_dst) .map_err(|e| format!("Failed to copy local binary {}: {}", local_binary, e))?; @@ -231,11 +264,13 @@ mod win { .unwrap_or(0.0); println!(" Installed ({:.2} MB)", size_mb); println!("[3/6] Skipping checksum (local binary mode)..."); + set_status("Status: Skipping checksum (local mode)..."); } else { let tag = std::env::var("FREEMID_RELEASE_TAG").unwrap_or_else(|_| "latest".to_string()); let (download_url, checksums_url) = build_urls(&tag); println!("[2/6] Downloading {} ...", ARTIFACT); + set_status("Status: Downloading native host..."); println!(" From: {}", download_url); ps_run(&format!( "Invoke-WebRequest -Uri '{}' -OutFile '{}' -UseBasicParsing", @@ -249,6 +284,7 @@ mod win { println!(" Downloaded ({:.2} MB)", size_mb); println!("[3/6] Verifying SHA256 checksum..."); + set_status("Status: Verifying checksum..."); let checksums_raw = ps_output(&format!( "[System.Text.Encoding]::UTF8.GetString((Invoke-WebRequest -Uri '{}' -UseBasicParsing).Content)", checksums_url @@ -278,6 +314,7 @@ mod win { } println!("[4/6] Writing native messaging manifest..."); + set_status("Status: Writing native messaging manifest..."); let bin_path_json = bin_dst.display().to_string().replace('\\', "\\\\"); let manifest = format!( "{{\n \"name\": \"{host}\",\n \"description\": \"FreeMiD native messaging host\",\n \"path\": \"{path}\",\n \"type\": \"stdio\",\n \"allowed_origins\": [\n \"chrome-extension://{ext_id}/\"\n ]\n}}", @@ -289,6 +326,7 @@ mod win { .map_err(|e| format!("Cannot write manifest: {}", e))?; println!("[5/6] Registering native messaging host..."); + set_status("Status: Registering browser host entries..."); let manifest_str = manifest_path.display().to_string(); for (name, parent) in [ ("Chrome", r"HKCU\Software\Google\Chrome\NativeMessagingHosts"), @@ -302,6 +340,7 @@ mod win { } println!("[6/6] Registering Apps & Features entry..."); + set_status("Status: Registering Apps and Features entry..."); register_arp(&install_dir, &bin_dst)?; println!(); @@ -310,12 +349,15 @@ mod win { println!(" Extension: {}", extension_id); println!(" ARP Key: {}", UNINSTALL_KEY); - set_status("Ready"); + set_status("Status: \u{2714} Installed"); Ok(()) } - fn run_uninstall() -> Result<(), String> { - set_status("Uninstalling..."); + fn run_uninstall(mut set_status: F) -> Result<(), String> + where + F: FnMut(&str), + { + set_status("Status: Starting uninstall..."); let local_app_data = std::env::var("LOCALAPPDATA") .map_err(|_| "%LOCALAPPDATA% not set".to_string())?; @@ -328,6 +370,7 @@ mod win { .args(["/F", "/IM", "freemid.exe", "/T"]) .output(); + set_status("Status: Removing registry entries..."); for key in [ UNINSTALL_KEY.to_string(), r"HKCU\Software\Google\Chrome\NativeMessagingHosts\com.clicksentinel.freemid".to_string(), @@ -349,7 +392,7 @@ mod win { } } - set_status("Ready"); + set_status("Status: \u{2714} Uninstalled"); Ok(()) } @@ -359,10 +402,6 @@ mod win { .status(); } - fn set_status(status: &str) { - let _ = status; - } - fn build_urls(tag: &str) -> (String, String) { let base = if tag == "latest" { format!( From 09226f46eb7a46295e2d9f4f8e6e7dc6dee945b2 Mon Sep 17 00:00:00 2001 From: Lucas Gillingham Date: Tue, 2 Jun 2026 14:01:42 -0230 Subject: [PATCH 14/16] update installer to link to the latest setup executable and improve button labels for clarity --- extension/public/popup/index.html | 173 ++++++++---------------------- installer/src/main.rs | 42 ++++---- 2 files changed, 65 insertions(+), 150 deletions(-) diff --git a/extension/public/popup/index.html b/extension/public/popup/index.html index 9d6671c..578f729 100644 --- a/extension/public/popup/index.html +++ b/extension/public/popup/index.html @@ -14,6 +14,7 @@ --text-1: #c4c8d4; --text-2: #7d8395; --accent: #5865f2; + --accent-strong: #7983f5; --accent-soft: rgba(88, 101, 242, 0.18); --success: #2dd17a; --warning: #f0b232; @@ -47,7 +48,7 @@ width: 36px; height: 36px; border-radius: 10px; - background: linear-gradient(135deg, var(--accent) 0%, #7983f5 100%); + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); display: grid; place-items: center; color: #fff; @@ -106,12 +107,12 @@ @keyframes pulse { 0%, 100% { opacity: 1; } - 50% { opacity: 0.35; } + 50% { opacity: 0.35; } } .status-label { flex: 1; - font-weight: 500; + font-weight: 600; color: var(--text-0); font-size: 13px; } @@ -124,33 +125,24 @@ .status-text { min-width: 0; flex: 1; } - .panel { - padding: 14px 18px; + .panel, + .control-row, + .help, + .now-playing, + .update-banner, + footer { + padding: 12px 18px; border-bottom: 1px solid var(--border); } - .panel-label { - font-size: 10px; - font-weight: 600; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-2); - margin-bottom: 6px; - } - - .panel-value { - font-size: 13px; - color: var(--text-0); - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; + .panel, + .control-row, + footer { + background: var(--bg-1); } .help { - padding: 14px 18px; background: var(--bg-2); - border-bottom: 1px solid var(--border); font-size: 12px; color: var(--text-1); } @@ -172,16 +164,14 @@ } .help p { margin-top: 2px; } - .help.hidden { display: none; } footer { - padding: 10px 18px; display: flex; justify-content: space-between; align-items: center; - background: var(--bg-1); font-size: 11px; + border-bottom: 0; } footer a { @@ -202,7 +192,7 @@ gap: 5px; padding: 4px 10px; background: var(--accent-soft); - border: 1px solid rgba(88,101,242,0.35); + border: 1px solid rgba(88, 101, 242, 0.35); border-radius: 6px; color: var(--accent); font-size: 11px; @@ -211,13 +201,12 @@ transition: background 0.15s, opacity 0.15s; flex-shrink: 0; } - .btn-reconnect:hover { background: rgba(88,101,242,0.28); } + .btn-reconnect:hover { background: rgba(88, 101, 242, 0.28); } .btn-reconnect:disabled { opacity: 0.45; cursor: not-allowed; } .btn-reconnect svg { flex-shrink: 0; } @keyframes spin { to { transform: rotate(360deg); } } .btn-reconnect.spinning svg { animation: spin 0.7s linear infinite; } - /* Toggle switch */ .toggle-btn { position: relative; width: 36px; @@ -243,100 +232,27 @@ } .toggle-btn[aria-checked="true"] .toggle-thumb { transform: translateX(16px); background: #fff; } - /* Pause control row */ .control-row { display: flex; align-items: center; justify-content: space-between; - padding: 11px 18px; - border-bottom: 1px solid var(--border); - background: var(--bg-1); } .control-label { display: flex; flex-direction: column; gap: 2px; } - .control-label > span:first-child { font-size: 13px; font-weight: 500; color: var(--text-0); } + .control-label > span:first-child { font-size: 13px; font-weight: 600; color: var(--text-0); } .control-sub { font-size: 11px; color: var(--text-2); } .control-row[data-paused="true"] .control-sub { color: var(--warning); } - /* Site toggles */ .site-toggles { display: flex; flex-direction: column; gap: 10px; } .site-row { display: flex; align-items: center; justify-content: space-between; } .site-row span { font-size: 12px; color: var(--text-1); } - /* Activity sub-label */ - .panel-sub { - font-size: 11px; - color: var(--text-2); - margin-top: 1px; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .preview-meta { - margin-top: 5px; - display: flex; - align-items: center; - gap: 6px; - font-size: 11px; - color: var(--text-2); - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .preview-meta-icon { - flex-shrink: 0; - opacity: 0.9; - } - - .preview-meta-text { - min-width: 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .preview-extra { - margin-top: 6px; - display: flex; - align-items: center; - gap: 6px; - } - - .preview-art { - width: 18px; - height: 18px; - border-radius: 4px; - object-fit: cover; - border: 1px solid var(--border); - background: var(--bg-1); - opacity: 0.9; - } - - .preview-button { - display: inline-flex; - align-items: center; - min-width: 0; - max-width: 100%; - padding: 2px 7px; - border-radius: 999px; - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--border); - font-size: 10px; - color: var(--text-1); - line-height: 1.25; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - /* Open Discord button */ - .btn-discord { + .btn-discord, + .btn-install { display: inline-flex; align-items: center; margin-top: 8px; padding: 5px 12px; - background: var(--accent); + background: linear-gradient(135deg, var(--accent) 0%, var(--accent-strong) 100%); border: none; border-radius: 6px; color: #fff; @@ -345,32 +261,32 @@ cursor: pointer; transition: opacity 0.15s; } - .btn-discord:hover { opacity: 0.85; } + .btn-discord:hover, + .btn-install:hover { opacity: 0.85; } - .btn-install { - display: inline-flex; - align-items: center; - margin-top: 8px; - padding: 5px 12px; - background: var(--accent); - border: none; - border-radius: 6px; - color: #fff; - font-size: 12px; + .panel-label { + font-size: 10px; font-weight: 600; - cursor: pointer; - transition: opacity 0.15s; + text-transform: uppercase; + letter-spacing: 0.08em; + color: var(--text-2); + margin-bottom: 6px; + } + + .panel-value { + font-size: 13px; + color: var(--text-0); + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .btn-install:hover { opacity: 0.85; } - /* Update banner */ .update-banner { display: flex; align-items: center; justify-content: space-between; - padding: 8px 18px; background: rgba(240, 178, 50, 0.1); - border-top: 1px solid rgba(240, 178, 50, 0.25); font-size: 12px; color: var(--warning); gap: 8px; @@ -390,9 +306,9 @@ } .btn-update:hover { opacity: 0.82; } - /* host-version is now inside .version-group — see .version-host */ - - .now-playing { padding: 12px 18px; border-bottom: 1px solid var(--border); background: var(--bg-0); } + .now-playing { + background: var(--bg-0); + } .now-playing.hidden { display: none; } .now-top { display: flex; @@ -478,7 +394,6 @@ font-variant-numeric: tabular-nums; } - /* GitHub icon link */ .gh-link { color: var(--text-2); display: flex; align-items: center; transition: color 0.15s; } .gh-link:hover { color: var(--text-0); } @@ -536,7 +451,7 @@

FreeMiD

@@ -601,7 +516,7 @@

FreeMiD