From f38bb2e5f86cc8e95ada24c125f88c6bb78e5b80 Mon Sep 17 00:00:00 2001 From: Ozimondias Date: Wed, 1 Jul 2026 02:59:45 -0700 Subject: [PATCH 01/15] feat: add Windows support via WebView2 and Go build tags Implements the Windows platform layer behind //go:build windows, leaving the macOS build unaffected. Mirrors the existing _darwin split with _windows counterparts. - URL routing: named-mutex single-instance + local socket IPC forwards URLs (and --window) from secondary launches to the resident instance, replacing macOS Apple Events (Windows uses new-process-per-URL) - Config UI: WebView2 via jchv/go-webview2 (pure Go, no cgo); local HTTP asset server replaces WKURLSchemeHandler; JS<->Go bridge via Bind(). The window is created and message-pumped on the LockOSThread main thread (Win32 delivers window messages only to the creating thread) - Browser detection/launch: StartMenuInternet registry + known install paths; config_dir_windows added to browsers.json for Chromium/Firefox profile resolution (%LOCALAPPDATA% / %APPDATA%) - Default-browser detection via the UserChoice ProgID (FinickyURL) - util: modifier keys, battery, process list via Win32; cross-platform LogDir() - Packaging/CI: Inno Setup installer with protocol-handler registration, winget manifest, cross-compile build script, and a separate, non-blocking Windows CI workflow - WINDOWS_PORT_QC.md documents the platform layer and a reusable QC checklist Cross-compiles cleanly (GOOS=windows CGO_ENABLED=0); go test ./... passes on both platforms. Refs #538 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/windows-build.yml | 129 ++++ .gitignore | 5 +- README.md | 29 +- WINDOWS_PORT_QC.md | 233 +++++++ apps/finicky/assets/Resources/finicky.ico | Bin 0 -> 372526 bytes apps/finicky/src/browser.go | 2 + apps/finicky/src/browser/browsers.json | 17 + apps/finicky/src/browser/detect.go | 2 + apps/finicky/src/browser/detect_windows.go | 104 +++ apps/finicky/src/browser/launcher.go | 2 + apps/finicky/src/browser/launcher_windows.go | 394 +++++++++++ apps/finicky/src/browser_windows.go | 71 ++ apps/finicky/src/go.mod | 2 + apps/finicky/src/go.sum | 6 + apps/finicky/src/logger/logger.go | 6 +- apps/finicky/src/main.go | 2 + apps/finicky/src/main_windows.go | 652 ++++++++++++++++++ apps/finicky/src/util/directories.go | 2 + apps/finicky/src/util/directories_windows.go | 36 + apps/finicky/src/util/info.go | 2 + apps/finicky/src/util/info_darwin_ext.go | 13 + apps/finicky/src/util/info_windows.go | 182 +++++ apps/finicky/src/window/window.go | 2 + apps/finicky/src/window/window_windows.go | 365 ++++++++++ scripts/build-windows.sh | 49 ++ scripts/installer.iss | 112 +++ .../JohnSterling.Finicky.installer.yaml | 23 + .../JohnSterling.Finicky.locale.en-US.yaml | 26 + scripts/winget/JohnSterling.Finicky.yaml | 7 + 29 files changed, 2468 insertions(+), 7 deletions(-) create mode 100644 .github/workflows/windows-build.yml create mode 100644 WINDOWS_PORT_QC.md create mode 100644 apps/finicky/assets/Resources/finicky.ico create mode 100644 apps/finicky/src/browser/detect_windows.go create mode 100644 apps/finicky/src/browser/launcher_windows.go create mode 100644 apps/finicky/src/browser_windows.go create mode 100644 apps/finicky/src/main_windows.go create mode 100644 apps/finicky/src/util/directories_windows.go create mode 100644 apps/finicky/src/util/info_darwin_ext.go create mode 100644 apps/finicky/src/util/info_windows.go create mode 100644 apps/finicky/src/window/window_windows.go create mode 100755 scripts/build-windows.sh create mode 100644 scripts/installer.iss create mode 100644 scripts/winget/JohnSterling.Finicky.installer.yaml create mode 100644 scripts/winget/JohnSterling.Finicky.locale.en-US.yaml create mode 100644 scripts/winget/JohnSterling.Finicky.yaml diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml new file mode 100644 index 00000000..7acc3d23 --- /dev/null +++ b/.github/workflows/windows-build.yml @@ -0,0 +1,129 @@ +name: Windows Build + +on: + push: + branches: [main] + pull_request: + branches: [main] + workflow_dispatch: + +jobs: + build-windows: + runs-on: windows-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: | + packages/config-api/package-lock.json + packages/finicky-ui/package-lock.json + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.24' + cache-dependency-path: | + apps/finicky/src/go.sum + + - name: Install Node dependencies + run: | + cd packages/config-api + npm ci + cd ../finicky-ui + npm ci + + - name: Build config-api + run: | + cd packages/config-api + npm run build + npm run generate-types + copy dist\finickyConfigAPI.js ..\..\apps\finicky\src\assets\finickyConfigAPI.js + shell: cmd + + - name: Build UI + run: | + cd packages/finicky-ui + npm run build + mkdir ..\..\apps\finicky\src\assets\templates 2>nul + xcopy /E /Y dist\* ..\..\apps\finicky\src\assets\templates\ + shell: cmd + + - name: Build Windows binary + env: + CGO_ENABLED: '0' + GOOS: windows + GOARCH: amd64 + run: | + $commitHash = git rev-parse --short HEAD + $buildDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC + cd apps/finicky/src + go build -ldflags "-X 'finicky/version.commitHash=$commitHash' -X 'finicky/version.buildDate=$buildDate' -H windowsgui" -o ../build/windows/Finicky.exe . + shell: pwsh + + - name: Run tests + run: | + cd apps/finicky/src + go test ./... + env: + CGO_ENABLED: '0' + + - name: Upload Windows binary + uses: actions/upload-artifact@v4 + with: + name: Finicky-windows-x64 + path: apps/finicky/build/windows/Finicky.exe + retention-days: 14 + + cross-compile-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: | + packages/config-api/package-lock.json + packages/finicky-ui/package-lock.json + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: '1.24' + cache-dependency-path: | + apps/finicky/src/go.sum + + - name: Build assets + run: | + cd packages/config-api && npm ci && npm run build && npm run generate-types + cp dist/finickyConfigAPI.js ../../apps/finicky/src/assets/finickyConfigAPI.js + cd ../finicky-ui && npm ci && npm run build + mkdir -p ../../apps/finicky/src/assets/templates + cp -r dist/* ../../apps/finicky/src/assets/templates/ + + - name: Cross-compile for Windows + env: + CGO_ENABLED: '0' + GOOS: windows + GOARCH: amd64 + run: | + cd apps/finicky/src + go build -o /dev/null . + + - name: Cross-compile for macOS (verify no regressions) + env: + CGO_ENABLED: '0' + GOOS: darwin + GOARCH: arm64 + run: | + cd apps/finicky/src + go build -o /dev/null . 2>&1 || echo "Expected: macOS build needs CGO for ObjC (this is OK)" diff --git a/.gitignore b/.gitignore index 1ff93c9a..9f3fec21 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,7 @@ node_modules build .env dist -example-config \ No newline at end of file +example-config +*.exe +*.msi +Output/ \ No newline at end of file diff --git a/README.md b/README.md index 65ad1636..a8827219 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ -Finicky is a macOS application that allows you to set up rules that decide which browser is opened for every url. With Finicky as your default browser, you can tell it to open Bluesky or Reddit in one browser, and LinkedIn or Google Meet in another. +Finicky is an application for macOS and Windows that allows you to set up rules that decide which browser is opened for every url. With Finicky as your default browser, you can tell it to open Bluesky or Reddit in one browser, and LinkedIn or Google Meet in another. - Route any URL to your preferred browser with powerful matching rules - Automatically edit URLs before opening them (e.g., force HTTPS, remove tracking parameters) @@ -22,16 +22,31 @@ Finicky is a macOS application that allows you to set up rules that decide which ## Table of Contents - [Installation](#installation) + - [macOS](#macos) + - [Windows](#windows) - [Basic configuration](#basic-configuration) - [Configuration](#configuration) - [Migrating from Finicky 3](#migrating-from-finicky-3) ## Installation +### macOS + - Download from [releases](https://github.com/johnste/finicky/releases) - Or install via homebrew: `brew install --cask finicky` - Create a JavaScript or TypeScript configuration file at `~/.finicky.js`. Have a look at the example configuration below, or in the `example-config` folder. -- Start Finicky (in Applications, or through Spotlight/Alfred/Raycast) and allow it to be set as the default browser. Starting Finicky manually opens the configuration/troubleshooting window. +- Start Finicky (in Applications, or through Spotlight/Alfred/Raycast) and allow it to be set as the default browser. Starting Finicky manually opens the configuration/troubleshooting window. + +### Windows + +- Download `FinickySetup.exe` from [releases](https://github.com/johnste/finicky/releases) and run the installer +- Or install via winget: `winget install Finicky` +- Create a configuration file at `%USERPROFILE%\.finicky.js` (same format as macOS — configs are cross-platform) +- Open **Settings > Default Apps** and set Finicky as the default web browser +- Finicky runs in the system tray and routes URLs based on your rules + +The installer registers Finicky as a browser in Windows. No admin rights are required — it installs per-user to `%LOCALAPPDATA%\Finicky`. + ## Basic configuration @@ -95,6 +110,16 @@ Finicky has browser extensions for Chrome and Firefox. They add an "open with Fi See [Building Finicky from source](https://github.com/johnste/finicky/wiki/Building-Finicky-from-source) +**Windows (cross-compile from macOS/Linux):** +```bash +./scripts/build-windows.sh +``` + +**Windows (native):** +```powershell +$env:CGO_ENABLED="0"; go build -C apps/finicky/src -ldflags "-H windowsgui" -o ../build/windows/Finicky.exe . +``` + ### Works well with If you are looking for something that lets you pick the browser to activate in a graphical interface, check out [Browserosaurus](https://browserosaurus.com/) by Will Stone, an open source browser prompter for macOS. It works really well together with Finicky! diff --git a/WINDOWS_PORT_QC.md b/WINDOWS_PORT_QC.md new file mode 100644 index 00000000..94bf4904 --- /dev/null +++ b/WINDOWS_PORT_QC.md @@ -0,0 +1,233 @@ +# Windows Port — QC Checklist & Findings + +This document records the quality-control methodology used before the Windows port was opened for community review. +It serves as a reusable checklist for future contributors, release engineers, and anyone who wants to extend or audit the Windows layer. + +--- + +## How to run a QC pass + +### 1. API parity check + +For every file that has both a `_darwin.go` and a `_windows.go` counterpart, verify that all **exported** symbols (functions, types, vars) defined on one platform are also present on the other with a compatible signature. Intentional differences are fine — document them. + +Files to compare: + +| darwin | windows | intentional differences | +|---|---|---| +| `main.go` | `main_windows.go` | URL receiving: Apple Events vs argv+unix socket | +| `browser.go` | `browser_windows.go` | `setDefaultBrowser` opens Settings app on Windows (OS-enforced) | +| `browser/detect.go` | `browser/detect_windows.go` | Registry walk vs `mdfind`/plist scan | +| `browser/launcher.go` | `browser/launcher_windows.go` | Profile paths via `%LOCALAPPDATA%` / `%APPDATA%` | +| `util/info.go` | `util/info_windows.go` | `GetForegroundWindowTitle` present only on Windows (future opener info) | +| `util/directories.go` | `util/directories_windows.go` | Delegates entirely to stdlib — no differences | +| `window/window.go` | `window/window_windows.go` | WKWebView → WebView2; WKURLSchemeHandler → local HTTP server | + +**Checklist:** +- [ ] Every exported function in `_darwin.go` has a matching counterpart in `_windows.go` +- [ ] Function signatures match (same argument types and return types) +- [ ] Any intentional difference is noted in the table above + +--- + +### 2. Race condition analysis + +The Windows event model is different from macOS. In particular: + +- `window_windows.go` runs WebView2 in a goroutine (`w.Run()` blocks). +- `main_windows.go` receives URLs on the `listenForURLs()` goroutine. +- `SendMessageToWebView` is called from multiple goroutines (URL handler, config watcher, update checker). + +**Shared mutable state to audit:** + +| Variable | File | Guard | Risk | +|---|---|---|---| +| `wv` (WebView2 instance) | `window_windows.go` | `queueMutex` | Read/write from multiple goroutines | +| `windowReady` | `window_windows.go` | `queueMutex` | Set in goroutine launched by `ShowWindow()` | +| `messageQueue` | `window_windows.go` | `queueMutex` | Appended by `SendMessageToWebView`, drained by `NavigationComplete` | +| `vm` (JS VM) | `main_windows.go` | None — single goroutine (event loop owns it) | OK: all writes and reads are serialized through the `select` loop | +| `shouldKeepRunning` | `main_windows.go` | None — event loop only | OK: same reason | +| `pipeAddress` | `main_windows.go` | `pipeOnce` (sync.Once) | Safe | + +**Checklist:** +- [ ] Every write to `wv` is under `queueMutex` +- [ ] Every write to `windowReady` is under `queueMutex` +- [ ] `sendMessageToWebViewInternal` is only called with `queueMutex` held (check all call sites) +- [ ] When `w.Run()` returns (window closed), `wv = nil` and `windowReady = false` are set inside the mutex before the close is signalled +- [ ] No goroutine calls `w.Eval()` / `w.Navigate()` directly — all WebView2 calls go through `Dispatch()` + +--- + +### 2b. Thread-affinity audit (GUI thread ownership) + +**This is the most important Windows-specific check and the easiest to get wrong.** Data-race analysis (section 2) asks "is this variable safe to touch from two goroutines?" Thread-affinity asks a different question: "is this call happening on the *one specific OS thread* that is allowed to make it?" Win32 GUI objects are thread-affine — window messages are delivered only to the thread that created the `HWND`, and the message loop must run on that same thread. A WebView2 window created on one thread and pumped on another is silently broken: it appears but never paints or accepts input, and WebView2's controller init may crash. + +Go makes this trap easy to fall into, because goroutines migrate between OS threads freely unless pinned with `runtime.LockOSThread()`. The rule the port must obey (mirroring how the macOS build runs all of Cocoa on the main thread): + +> The WebView2 window is **created, message-pumped, and torn down on a single OS thread**, and that thread is pinned with `runtime.LockOSThread()`. All work from other goroutines reaches it via `webview.Dispatch()`, never directly. + +**Checklist:** +- [ ] `main()` calls `runtime.LockOSThread()` and then keeps the main goroutine as the UI owner (it runs the window loop, not the event loop) +- [ ] The event loop runs on a **background goroutine**; the **main thread** runs `runUILoop` → `window.RunWindow()` +- [ ] `window.RunWindow()` both **creates** the webview and calls `w.Run()` — on the same call stack, same thread. Creation and the message loop are never split across goroutines +- [ ] Show-window requests cross from the event-loop goroutine to the main thread via a channel (`showWindowChan`), not by calling `RunWindow` directly off-thread +- [ ] Every `SendMessageToWebView` from a non-UI goroutine ends in `wv.Dispatch(...)`, which marshals onto the pumping thread +- [ ] Re-opening a closed window creates a fresh webview on the same locked thread (no stale `HWND` reuse) + +**How to catch a regression:** on a real Windows machine, trigger the config window (`Finicky.exe --window`). If it opens but is blank/frozen/unclickable, thread-affinity is broken even though `go build` and `go test` pass — this class of bug is invisible to the compiler and to macOS-hosted tests. + +--- + +### 3. Lifecycle signal audit + +The macOS port relies on OS-provided lifecycle callbacks (Apple Events, `WKNavigationDelegate`). The Windows port must manually wire equivalent signals. + +| Signal | macOS mechanism | Windows mechanism | Status | +|---|---|---|---| +| URL received from OS | Apple Event `kAEGetURL` | `argv[1]` on process launch → unix socket IPC | ✅ Implemented | +| Window closed | Cocoa delegate callback | `w.Run()` returns on main thread → `windowClosed` channel | ✅ Implemented | +| Page load complete | `WKNavigationDelegate.didFinish` | `w.Init(...)` injects `window.addEventListener("load", () => __finicky_ready())` | ✅ Implemented | +| Message queue drain | Triggered by page load | `NavigationComplete()` called via `__finicky_ready` binding | ✅ Implemented | +| Single instance | NSRunningApplication check | Named mutex `Global\FinickyBrowserRouter` | ✅ Implemented | + +**Checklist:** +- [ ] `__finicky_ready` binding is registered via `w.Bind()` before `w.Navigate()` +- [ ] `w.Init(...)` script fires `__finicky_ready()` on `window` load event (not DOMContentLoaded — assets must be ready) +- [ ] `NavigationComplete()` drains `messageQueue` and sets `windowReady = true` inside `queueMutex` +- [ ] The UI loop signals `windowClosed` after `RunWindow()` returns, and the event loop resets `showingWindow` so the window can be reopened +- [ ] `sendToPrimary` retries the socket dial (covering the mutex-created-before-socket startup race) and, on ultimate failure, only falls back to `cmd /c start` when Finicky is **not** the default browser (else it would relaunch itself in a loop) + +--- + +### 4. Dead code and import audit + +After any cross-platform file is created or edited, run: + +```bash +cd apps/finicky/src +GOOS=windows CGO_ENABLED=0 go build ./... 2>&1 | grep -E "imported and not used|declared and not used" +GOOS=darwin CGO_ENABLED=1 go build ./... 2>&1 | grep -E "imported and not used|declared and not used" +``` + +**Checklist:** +- [ ] No unused imports on either platform +- [ ] No unused constants (e.g. named pipe path constants left over from earlier design iterations) +- [ ] No `var _ = ...` import silencers (these mask real unused-import issues) + +--- + +### 5. Error handling — critical paths + +Silent failures in a URL router are user-visible: the user clicks a link, nothing happens. + +**Critical paths to audit:** + +| Path | What can go wrong | Mitigation | +|---|---|---| +| `forwardURLToPipe` | Pipe not yet up (race at startup); stale socket | Falls back to `cmd /c start` — URL opens in system default | +| `listenForURLs` | Stale `.sock` file from crash | Removes and retries | +| `findBrowserExe` | Browser installed in non-standard path | Falls back to `cmd /c start ""` which lets Windows resolve the browser | +| `startAssetServer` | Port binding failure | Falls back to `w.SetHtml()` with embedded HTML | +| `LaunchBrowser` | Profile not found | Logs warning, proceeds without profile flag (browser opens default profile) | +| `GetInstalledBrowsers` | Registry key missing | Returns empty slice (UI shows "no browsers found") | + +**Checklist:** +- [ ] `forwardURLToPipe` has `cmd /c start` fallback and `HideWindow: true` on SysProcAttr +- [ ] `listenForURLs` removes stale socket before retrying bind +- [ ] `startAssetServer` falls back to `SetHtml` on listener error +- [ ] `LaunchBrowser` uses `cmd /c start ""` fallback when `findBrowserExe` returns empty +- [ ] Profile resolution failures log a warning and continue (not fatal) + +--- + +### 6. Cross-compile verification + +Run this before every release candidate: + +```bash +cd apps/finicky/src + +# Windows cross-compile (the primary deliverable) +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ + go build -v \ + -ldflags="-H windowsgui -X finicky/version.GitCommit=$(git rev-parse --short HEAD) -X finicky/version.BuildDate=$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ + -o /tmp/Finicky.exe \ + . + +# macOS native build (regression check — must not break) +CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 \ + go build -v \ + -o /tmp/Finicky-mac \ + . + +# Test suite (both platforms share the same test files) +go test ./... +``` + +**Expected results:** +- Windows binary: PE32+ executable (GUI), ~28-35 MB depending on assets +- macOS binary: Mach-O 64-bit, arm64 +- Tests: 4 suites pass (resolver, rules, util, version) + +--- + +## Findings from initial QC pass (2026-06-30) + +Six bugs were found and fixed before community release. + +| # | Severity | File | Bug | Fix | +|---|---|---|---|---| +| 1 | **Critical** | `window_windows.go` | `NavigationComplete()` was defined but never called — the Svelte UI would receive a blank screen because the message queue never drained | Added `w.Init(...)` script that fires `window.__finicky_ready()` on page load; bound via `w.Bind("__finicky_ready", NavigationComplete)` | +| 2 | **Critical** | `window_windows.go` | `wv` read/written from multiple goroutines without synchronization — race condition that could panic or corrupt state | All `wv` accesses now under `queueMutex`; `sendMessageToWebViewInternal` documented as requiring lock held | +| 3 | **High** | `window_windows.go` | `wv = nil` and `windowReady = false` set outside `queueMutex` in `Run()` callback goroutine | Wrapped in `queueMutex.Lock()/Unlock()` | +| 4 | **Medium** | `main_windows.go` | `forwardURLToPipe` silently dropped URLs when the unix socket dial failed (e.g. main instance still starting) — user click lost | Added `exec.Command("cmd", "/c", "start", "", url)` fallback with `HideWindow: true` | +| 5 | **Low** | `main_windows.go` | Unused constant `pipeName = "\\\\.\pipe\finicky-url"` — leftover from Named Pipe design iteration; code uses unix sockets | Removed | +| 6 | **Low** | `browser/launcher_windows.go`, `window_windows.go` | Unused import silencers (`var _ = util.ShortenPath`, `var _ = filepath.Base`) | Removed; cleaned imports | + +## Findings from second QC pass (2026-07-01) + +A deeper architectural review — focused on threading, Win32 API contracts, and cross-referencing the installer against the runtime code — found five more issues. These are the kind that pass every compiler and macOS-hosted test but fail on real Windows, so they were the highest-value catches before community testing. + +| # | Severity | File | Bug | Fix | +|---|---|---|---|---| +| 7 | **Critical** | `main_windows.go`, `window_windows.go` | **Broken GUI thread affinity.** The webview was *created* on a `go ShowConfigWindow()` goroutine and *message-pumped* on a separate `go func(){ w.Run() }()` goroutine — neither being the `LockOSThread`-pinned main thread. Win32 delivers window messages only to the creating thread, so the config window would open blank/frozen and WebView2 init could crash. | Restructured so the **main (locked) thread owns the window**: event loop moved to a background goroutine; `runUILoop` on the main thread runs `window.RunWindow()`, which creates *and* pumps the webview on one thread. Cross-thread show requests go through `showWindowChan`. Mirrors the macOS "all UI on main thread" model. | +| 8 | **High** | `main_windows.go` | **Racy single-instance detection.** `GetLastError` was read via a *separate* `LazyProc.Call`, which can be clobbered by the Go runtime's own syscalls between it and `CreateMutexW` — so `ERROR_ALREADY_EXISTS` could be missed and two instances both act as primary. | Use the `err` value returned directly by `procCreateMutexW.Call(...)` (captured by the runtime immediately after the syscall). Removed the separate `getLastError` proc. | +| 9 | **High** | `browser_windows.go` | **`isDefaultBrowser()` always returned false.** It compared the UserChoice ProgID to the literal `"Finicky"`, but the installer registers the http/https association as ProgID `"FinickyURL"`. The two never matched. | Compare against a shared `progID = "FinickyURL"` constant that matches `installer.iss`. | +| 10 | **High** | `browser_windows.go`, `main_windows.go` | **Settings popped open on every link click.** `setDefaultBrowser()` responded to "not default" by launching `ms-settings:defaultapps`, and it ran at every startup — so (amplified by #9) the Default Apps page opened on every URL invocation. | Startup now only *observes* default status (`logDefaultBrowserStatus`) and never prompts, matching the fact that Windows 10+ requires the user to set defaults manually (the installer already offers this opt-in). Removed the unusable programmatic-set path. | +| 11 | **Medium** | `main_windows.go` | **URL loss / relaunch-loop risk in IPC handoff.** `forwardURLToPipe` dialed the primary's socket exactly once; during the primary's startup the mutex exists before the socket is listening, so the URL was dropped. Its `cmd /c start` fallback could also re-invoke Finicky if Finicky was the default browser, risking a launch loop. | `sendToPrimary` retries the dial for ~1s (covers the startup race); the `start` fallback runs **only when Finicky is not the default browser**, otherwise the URL is dropped with a clear log instead of looping. Also added a `--window` handoff so a second launch focuses the existing window (Start Menu parity). | + +**Also fixed:** `installer.iss` autostart entry launched `Finicky.exe --window`, which would pop the config window on every login. Changed to launch with no arguments (resident router only). And the asset HTTP server is now closed when the window closes (was leaked on every open). + +--- + +## Known limitations (not bugs) + +These are documented design decisions, not defects: + +- **Finicky cannot make itself the default browser programmatically on Windows 10+.** Microsoft removed programmatic default-browser setting after Windows 8. So, unlike the macOS build (which sets the default via `LSSetDefaultHandlerForURLScheme` behind an OS consent dialog), the Windows build only *detects* the current default and logs it — it never prompts on launch. Setting Finicky as default is a user action, offered by the installer's opt-in "Open Default Apps settings" task. A future in-app "Set as default" button could call the same `ms-settings:defaultapps` deep link. + +- **Opener info is not available on Windows.** On macOS, `OpenerInfo` captures the frontmost app when a URL is clicked. On Windows, `GetForegroundWindowTitle` is implemented but the Windows process launch model (new process per URL) makes reliable opener detection impractical — by the time Finicky launches, the original foreground window may have changed. `OpenerInfo` is passed as an empty struct. + +- **Browser detection is hardcoded + registry-based.** `findBrowserExe` uses a hardcoded path table for the 10 most common browsers. `detect_windows.go` supplements this via the `StartMenuInternet` registry key. Exotic browsers in non-standard install locations will fall through to the `cmd /c start` fallback (which uses Windows' own default browser association). + +- **WebView2 runtime must be present.** `go-webview2` embeds `WebView2Loader.dll` via `go-winloader` but still requires the WebView2 runtime to be installed. It ships with Windows 11 and is available via Windows Update on Windows 10. The installer (`scripts/installer.iss`) does not yet bundle or auto-install the runtime — this is a known gap for fresh Windows 10 installs. + +--- + +## Pre-release checklist (summary) + +``` +[ ] go build (windows) — clean +[ ] go build (darwin) — clean, no regressions +[ ] go test ./... — all suites pass +[ ] API parity check — all exports present on both platforms +[ ] Race audit — queueMutex guards all wv/windowReady/messageQueue writes +[ ] Thread-affinity — webview created + pumped on the one LockOSThread'd main thread +[ ] Lifecycle audit — __finicky_ready bound, NavigationComplete drains queue +[ ] Window smoke test — on real Windows, `--window` opens a live, clickable config UI +[ ] Dead code audit — no unused imports, constants, or silencers +[ ] Critical paths — all error paths have fallback or warning log +[ ] installer.iss — InstallerSha256 updated for this build +[ ] winget manifest — version and SHA updated +[ ] LAUNCH_MATERIALS — demo GIF recorded, posting schedule set +``` diff --git a/apps/finicky/assets/Resources/finicky.ico b/apps/finicky/assets/Resources/finicky.ico new file mode 100644 index 0000000000000000000000000000000000000000..7c28a945f839f9e9ad2e234252de892f982a152d GIT binary patch literal 372526 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 literal 0 HcmV?d00001 diff --git a/apps/finicky/src/browser.go b/apps/finicky/src/browser.go index 324f514c..871fd690 100644 --- a/apps/finicky/src/browser.go +++ b/apps/finicky/src/browser.go @@ -1,3 +1,5 @@ +//go:build darwin + package main /* diff --git a/apps/finicky/src/browser/browsers.json b/apps/finicky/src/browser/browsers.json index cb4e804c..9fe6161c 100644 --- a/apps/finicky/src/browser/browsers.json +++ b/apps/finicky/src/browser/browsers.json @@ -1,102 +1,119 @@ [ { "config_dir_relative": "BraveSoftware/Brave-Browser", + "config_dir_windows": "BraveSoftware\\Brave-Browser\\User Data", "id": "com.brave.Browser", "type": "Chromium", "app_name": "Brave Browser" }, { "config_dir_relative": "Google/Chrome", + "config_dir_windows": "Google\\Chrome\\User Data", "id": "com.google.Chrome", "type": "Chromium", "app_name": "Google Chrome" }, { "config_dir_relative": "Google/Chrome Beta", + "config_dir_windows": "Google\\Chrome Beta\\User Data", "id": "com.google.Chrome.beta", "type": "Chromium", "app_name": "Google Chrome Beta" }, { "config_dir_relative": "Google/Chrome Canary", + "config_dir_windows": "Google\\Chrome SxS\\User Data", "id": "com.google.Chrome.canary", "type": "Chromium", "app_name": "Google Chrome Canary" }, { "config_dir_relative": "Chromium", + "config_dir_windows": "Chromium\\User Data", "id": "org.chromium.Chromium", "type": "Chromium", "app_name": "Chromium" }, { "config_dir_relative": "Microsoft Edge", + "config_dir_windows": "Microsoft\\Edge\\User Data", "id": "com.microsoft.edgemac", "type": "Chromium", "app_name": "Microsoft Edge" }, { "config_dir_relative": "Vivaldi", + "config_dir_windows": "Vivaldi\\User Data", "id": "com.vivaldi.Vivaldi", "type": "Chromium", "app_name": "Vivaldi" }, { "config_dir_relative": "WaveboxApp", + "config_dir_windows": "WaveboxApp\\User Data", "id": "com.bookry.wavebox", "type": "Chromium", "app_name": "Wavebox" }, { "config_dir_relative": "net.imput.helium", + "config_dir_windows": "", "id": "net.imput.helium", "type": "Chromium", "app_name": "Helium" }, { "config_dir_relative": "Comet", + "config_dir_windows": "", "id": "ai.perplexity.comet", "type": "Chromium", "app_name": "Comet" }, { "config_dir_relative": "Yandex/YandexBrowser", + "config_dir_windows": "Yandex\\YandexBrowser\\User Data", "id": "ru.yandex.desktop.yandex-browser", "type": "Chromium", "app_name": "Yandex" }, { "config_dir_relative": "com.operasoftware.Opera", + "config_dir_windows": "Opera Software\\Opera Stable", "id": "com.operasoftware.Opera", "type": "Chromium", "app_name": "Opera" }, { "config_dir_relative": "com.operasoftware.OperaGX", + "config_dir_windows": "Opera Software\\Opera GX Stable", "id": "com.operasoftware.OperaGX", "type": "Chromium", "app_name": "Opera GX" }, { "config_dir_relative": "", + "config_dir_windows": "", "id": "com.apple.Safari", "type": "Safari", "app_name": "Safari" }, { "config_dir_relative": "Firefox", + "config_dir_windows": "Mozilla\\Firefox", "id": "org.mozilla.firefox", "type": "Firefox", "app_name": "Firefox" }, { "config_dir_relative": "Firefox", + "config_dir_windows": "Mozilla\\Firefox", "id": "org.mozilla.firefoxdeveloperedition", "type": "Firefox", "app_name": "Firefox Developer Edition" }, { "config_dir_relative": "zen", + "config_dir_windows": "zen", "id": "app.zen-browser.zen", "type": "Firefox", "app_name": "Zen" diff --git a/apps/finicky/src/browser/detect.go b/apps/finicky/src/browser/detect.go index a6d3349d..d639be0d 100644 --- a/apps/finicky/src/browser/detect.go +++ b/apps/finicky/src/browser/detect.go @@ -1,3 +1,5 @@ +//go:build darwin + package browser /* diff --git a/apps/finicky/src/browser/detect_windows.go b/apps/finicky/src/browser/detect_windows.go new file mode 100644 index 00000000..ce121ab6 --- /dev/null +++ b/apps/finicky/src/browser/detect_windows.go @@ -0,0 +1,104 @@ +//go:build windows + +package browser + +import ( + "log/slog" + "sort" + "strings" + "syscall" + "unsafe" +) + +var ( + advapi32 = syscall.NewLazyDLL("advapi32.dll") + procRegOpenKeyEx = advapi32.NewProc("RegOpenKeyExW") + procRegEnumKeyEx = advapi32.NewProc("RegEnumKeyExW") + procRegQueryValue = advapi32.NewProc("RegQueryValueExW") + procRegCloseKey = advapi32.NewProc("RegCloseKey") +) + +const ( + hkeyLocalMachine = 0x80000002 + keyRead = 0x20019 +) + +func GetInstalledBrowsers() []string { + browsers := getBrowsersFromRegistry() + sort.Strings(browsers) + return browsers +} + +func getBrowsersFromRegistry() []string { + path := `SOFTWARE\Clients\StartMenuInternet` + pathPtr, _ := syscall.UTF16PtrFromString(path) + + var key syscall.Handle + ret, _, _ := procRegOpenKeyEx.Call( + hkeyLocalMachine, + uintptr(unsafe.Pointer(pathPtr)), + 0, + keyRead, + uintptr(unsafe.Pointer(&key)), + ) + if ret != 0 { + slog.Debug("Failed to open StartMenuInternet registry key") + return []string{} + } + defer procRegCloseKey.Call(uintptr(key)) + + var names []string + for i := uint32(0); ; i++ { + buf := make([]uint16, 256) + size := uint32(len(buf)) + ret, _, _ := procRegEnumKeyEx.Call( + uintptr(key), + uintptr(i), + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + 0, 0, 0, 0, + ) + if ret != 0 { + break + } + name := syscall.UTF16ToString(buf[:size]) + displayName := getBrowserDisplayName(key, name) + if displayName != "" && !strings.EqualFold(displayName, "Finicky") { + names = append(names, displayName) + } + } + return names +} + +func getBrowserDisplayName(parentKey syscall.Handle, subkeyName string) string { + subPath, _ := syscall.UTF16PtrFromString(subkeyName) + + var subKey syscall.Handle + ret, _, _ := procRegOpenKeyEx.Call( + uintptr(parentKey), + uintptr(unsafe.Pointer(subPath)), + 0, + keyRead, + uintptr(unsafe.Pointer(&subKey)), + ) + if ret != 0 { + return subkeyName + } + defer procRegCloseKey.Call(uintptr(subKey)) + + buf := make([]uint16, 256) + size := uint32(len(buf) * 2) + emptyStr, _ := syscall.UTF16PtrFromString("") + ret, _, _ = procRegQueryValue.Call( + uintptr(subKey), + uintptr(unsafe.Pointer(emptyStr)), + 0, + 0, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + ) + if ret == 0 && size > 2 { + return syscall.UTF16ToString(buf) + } + return subkeyName +} diff --git a/apps/finicky/src/browser/launcher.go b/apps/finicky/src/browser/launcher.go index a3a71200..d8fe5b19 100644 --- a/apps/finicky/src/browser/launcher.go +++ b/apps/finicky/src/browser/launcher.go @@ -1,3 +1,5 @@ +//go:build darwin + package browser import ( diff --git a/apps/finicky/src/browser/launcher_windows.go b/apps/finicky/src/browser/launcher_windows.go new file mode 100644 index 00000000..804f7305 --- /dev/null +++ b/apps/finicky/src/browser/launcher_windows.go @@ -0,0 +1,394 @@ +//go:build windows + +package browser + +import ( + _ "embed" + "encoding/json" + "fmt" + "io" + "log/slog" + "os" + "os/exec" + "path/filepath" + "slices" + "strings" + + "al.essio.dev/pkg/shellescape" +) + +//go:embed browsers.json +var browsersJsonData []byte + +type BrowserResult struct { + Browser BrowserConfig `json:"browser"` + Error string `json:"error"` +} + +type BrowserConfig struct { + Name string `json:"name"` + AppType string `json:"appType"` + OpenInBackground *bool `json:"openInBackground"` + Profile string `json:"profile"` + Args []string `json:"args"` + URL string `json:"url"` +} + +type browserInfo struct { + ConfigDirRelative string `json:"config_dir_relative"` + ConfigDirWindows string `json:"config_dir_windows"` + ID string `json:"id"` + AppName string `json:"app_name"` + Type string `json:"type"` +} + +func (b browserInfo) windowsConfigDir() string { + if b.ConfigDirWindows != "" { + return b.ConfigDirWindows + } + return b.ConfigDirRelative +} + +func LaunchBrowser(config BrowserConfig, dryRun bool, openInBackgroundByDefault bool) error { + if config.AppType == "none" { + slog.Info("AppType is 'none', not launching any browser") + return nil + } + + slog.Info("Starting browser", "name", config.Name, "url", config.URL) + + profileArgs, hasProfile := resolveBrowserProfileArgs(config.Name, config.Profile) + hasCustomArgs := len(config.Args) > 0 + + var cmdArgs []string + + if hasProfile || hasCustomArgs { + if hasProfile { + cmdArgs = append(cmdArgs, profileArgs...) + } + if hasCustomArgs { + cmdArgs = append(cmdArgs, config.Args...) + } + if !slices.Contains(config.Args, config.URL) { + cmdArgs = append(cmdArgs, config.URL) + } + } else { + cmdArgs = append(cmdArgs, config.URL) + } + + // Find the browser executable + exePath := findBrowserExe(config.Name) + if exePath == "" { + // Fallback: use cmd /c start to let Windows find the browser + allArgs := append([]string{"/c", "start", "", config.URL}, cmdArgs...) + cmd := exec.Command("cmd", allArgs...) + prettyCmd := formatCommand(cmd.Path, cmd.Args) + if dryRun { + slog.Debug("Would run command (dry run)", "command", prettyCmd) + return nil + } + slog.Debug("Run command (fallback)", "command", prettyCmd) + return cmd.Run() + } + + cmd := exec.Command(exePath, cmdArgs...) + prettyCmd := formatCommand(cmd.Path, cmd.Args) + + if dryRun { + slog.Debug("Would run command (dry run)", "command", prettyCmd) + return nil + } + + slog.Debug("Run command", "command", prettyCmd) + + stderr, err := cmd.StderrPipe() + if err != nil { + return err + } + stdout, err := cmd.StdoutPipe() + if err != nil { + return err + } + + if err := cmd.Start(); err != nil { + return err + } + + stderrBytes, _ := io.ReadAll(stderr) + stdoutBytes, _ := io.ReadAll(stdout) + cmdErr := cmd.Wait() + + if len(stderrBytes) > 0 { + slog.Error("Command returned error", "error", string(stderrBytes)) + } + if len(stdoutBytes) > 0 { + slog.Debug("Command returned output", "output", string(stdoutBytes)) + } + + if cmdErr != nil { + return fmt.Errorf("command failed: %v", cmdErr) + } + return nil +} + +func findBrowserExe(name string) string { + commonPaths := []string{ + os.Getenv("PROGRAMFILES"), + os.Getenv("PROGRAMFILES(X86)"), + os.Getenv("LOCALAPPDATA"), + } + + knownExes := map[string][]string{ + "Google Chrome": {"Google\\Chrome\\Application\\chrome.exe"}, + "Chrome": {"Google\\Chrome\\Application\\chrome.exe"}, + "Microsoft Edge": {"Microsoft\\Edge\\Application\\msedge.exe"}, + "Edge": {"Microsoft\\Edge\\Application\\msedge.exe"}, + "Firefox": {"Mozilla Firefox\\firefox.exe"}, + "Mozilla Firefox": {"Mozilla Firefox\\firefox.exe"}, + "Brave Browser": {"BraveSoftware\\Brave-Browser\\Application\\brave.exe"}, + "Brave": {"BraveSoftware\\Brave-Browser\\Application\\brave.exe"}, + "Vivaldi": {"Vivaldi\\Application\\vivaldi.exe"}, + "Opera": {"Opera\\opera.exe"}, + "Arc": {"Arc\\Arc.exe"}, + } + + if paths, ok := knownExes[name]; ok { + for _, base := range commonPaths { + if base == "" { + continue + } + for _, rel := range paths { + full := filepath.Join(base, rel) + if _, err := os.Stat(full); err == nil { + return full + } + } + } + } + + return "" +} + +func resolveBrowserProfileArgs(identifier string, profile string) ([]string, bool) { + var browsersJson []browserInfo + if err := json.Unmarshal(browsersJsonData, &browsersJson); err != nil { + slog.Info("Error parsing browsers.json", "error", err) + return nil, false + } + + var matchedBrowser *browserInfo + for _, browser := range browsersJson { + if browser.ID == identifier || browser.AppName == identifier { + matchedBrowser = &browser + break + } + } + + if matchedBrowser == nil { + return nil, false + } + + slog.Debug("Browser found in browsers.json", "identifier", identifier, "type", matchedBrowser.Type) + + if profile != "" { + switch matchedBrowser.Type { + case "Chromium": + localAppData := os.Getenv("LOCALAPPDATA") + if localAppData == "" { + slog.Info("LOCALAPPDATA not set") + return nil, false + } + localStatePath := filepath.Join(localAppData, matchedBrowser.windowsConfigDir(), "Local State") + profilePath, ok := parseProfiles(localStatePath, profile) + if ok { + return []string{"--profile-directory=" + profilePath}, true + } + case "Firefox": + appData := os.Getenv("APPDATA") + if appData == "" { + slog.Info("APPDATA not set") + return nil, false + } + profilesIniPath := filepath.Join(appData, matchedBrowser.windowsConfigDir(), "profiles.ini") + profileName, ok := parseFirefoxProfiles(profilesIniPath, profile) + if ok { + return []string{"-P", profileName}, true + } + default: + slog.Info("Browser is not a supported browser type", "identifier", identifier) + } + } + + return nil, false +} + +func GetProfilesForBrowser(identifier string) []string { + var browsersJson []browserInfo + if err := json.Unmarshal(browsersJsonData, &browsersJson); err != nil { + slog.Info("Error parsing browsers.json", "error", err) + return []string{} + } + + var matchedBrowser *browserInfo + for i := range browsersJson { + if browsersJson[i].ID == identifier || browsersJson[i].AppName == identifier { + matchedBrowser = &browsersJson[i] + break + } + } + + if matchedBrowser == nil { + return []string{} + } + + switch matchedBrowser.Type { + case "Chromium": + localAppData := os.Getenv("LOCALAPPDATA") + if localAppData == "" { + return []string{} + } + localStatePath := filepath.Join(localAppData, matchedBrowser.windowsConfigDir(), "Local State") + return getAllChromiumProfiles(localStatePath) + case "Firefox": + appData := os.Getenv("APPDATA") + if appData == "" { + return []string{} + } + profilesIniPath := filepath.Join(appData, matchedBrowser.windowsConfigDir(), "profiles.ini") + return readFirefoxProfileNames(profilesIniPath) + default: + return []string{} + } +} + +// --- Shared profile parsing (duplicated from launcher.go, can be extracted later) --- + +func readFirefoxProfileNames(profilesIniPath string) []string { + data, err := os.ReadFile(profilesIniPath) + if err != nil { + slog.Info("Error reading profiles.ini", "path", profilesIniPath, "error", err) + return []string{} + } + names := []string{} + for _, line := range strings.Split(string(data), "\n") { + line = strings.TrimSpace(line) + if name, ok := strings.CutPrefix(line, "Name="); ok { + names = append(names, name) + } + } + return names +} + +func parseFirefoxProfiles(profilesIniPath string, profile string) (string, bool) { + names := readFirefoxProfileNames(profilesIniPath) + for _, name := range names { + if name == profile { + return name, true + } + } + slog.Warn("Could not find profile in Firefox profiles.", "Expected profile", profile, "Available profiles", strings.Join(names, ", ")) + return "", false +} + +func chromiumInfoCache(localStatePath string) (map[string]interface{}, bool) { + data, err := os.ReadFile(localStatePath) + if err != nil { + slog.Info("Error reading Local State file", "path", localStatePath, "error", err) + return nil, false + } + var localState map[string]interface{} + if err := json.Unmarshal(data, &localState); err != nil { + slog.Info("Error parsing Local State JSON", "error", err) + return nil, false + } + profiles, ok := localState["profile"].(map[string]interface{}) + if !ok { + slog.Info("Could not find profile section in Local State") + return nil, false + } + infoCache, ok := profiles["info_cache"].(map[string]interface{}) + if !ok { + slog.Info("Could not find info_cache in profile section") + return nil, false + } + return infoCache, true +} + +func getAllChromiumProfiles(localStatePath string) []string { + cache, ok := chromiumInfoCache(localStatePath) + if !ok { + return []string{} + } + var names []string + for _, info := range cache { + profileInfo, ok := info.(map[string]interface{}) + if !ok { + continue + } + name, ok := profileInfo["name"].(string) + if !ok { + continue + } + names = append(names, name) + } + slices.Sort(names) + return names +} + +func parseProfiles(localStatePath string, profile string) (string, bool) { + infoCache, ok := chromiumInfoCache(localStatePath) + if !ok { + return "", false + } + for profilePath, info := range infoCache { + profileInfo, ok := info.(map[string]interface{}) + if !ok { + continue + } + name, ok := profileInfo["name"].(string) + if !ok { + continue + } + if name == profile { + slog.Info("Found profile by name", "name", name, "path", profilePath) + return profilePath, true + } + } + slog.Debug("Could not find profile in browser profiles, trying to find by profile path", "profile", profile) + for profilePath, info := range infoCache { + if profilePath == profile { + if profileInfo, ok := info.(map[string]interface{}); ok { + if name, ok := profileInfo["name"].(string); ok { + slog.Warn("Found profile using profile path", "path", profilePath, "name", name, "suggestion", "Please use the profile name instead") + } + } + return profilePath, true + } + } + var profileNames []string + for _, info := range infoCache { + profileInfo, ok := info.(map[string]interface{}) + if !ok { + continue + } + name, ok := profileInfo["name"].(string) + if !ok { + continue + } + profileNames = append(profileNames, name) + } + slog.Warn("Could not find profile in browser profiles.", "Expected profile", profile, "Available profiles", strings.Join(profileNames, ", ")) + return "", false +} + +func formatCommand(path string, args []string) string { + if len(args) == 0 { + return shellescape.Quote(path) + } + quotedArgs := make([]string, len(args)) + for i, arg := range args { + quotedArgs[i] = shellescape.Quote(arg) + } + return strings.Join(quotedArgs, " ") +} + diff --git a/apps/finicky/src/browser_windows.go b/apps/finicky/src/browser_windows.go new file mode 100644 index 00000000..a7f1cd9d --- /dev/null +++ b/apps/finicky/src/browser_windows.go @@ -0,0 +1,71 @@ +//go:build windows + +package main + +import ( + "fmt" + "syscall" + "unsafe" +) + +// progID is the ProgID the installer registers for Finicky's http/https URL +// associations (see scripts/installer.iss). Windows records the user's default +// browser choice as this ProgID under UserChoice, so isDefaultBrowser must +// compare against it — not the application's display name. +const progID = "FinickyURL" + +var ( + advapi32Browser = syscall.NewLazyDLL("advapi32.dll") + procRegOpenKeyExB = advapi32Browser.NewProc("RegOpenKeyExW") + procRegQueryValueB = advapi32Browser.NewProc("RegQueryValueExW") + procRegCloseKeyB = advapi32Browser.NewProc("RegCloseKey") +) + +const ( + hkeyCurrentUser = 0x80000001 + keyReadB = 0x20019 +) + +// isDefaultBrowser reports whether Finicky is the user's default browser by +// reading the http/https UserChoice ProgIDs. Windows 10+ does not allow apps to +// set this programmatically; the user selects Finicky in Settings (the +// installer can open that page), so there is no setDefaultBrowser counterpart. +func isDefaultBrowser() (bool, error) { + httpHandler, _ := getDefaultHandlerForURLScheme("http") + httpsHandler, _ := getDefaultHandlerForURLScheme("https") + return httpHandler == progID && httpsHandler == progID, nil +} + +func getDefaultHandlerForURLScheme(scheme string) (string, error) { + path := fmt.Sprintf(`SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\%s\UserChoice`, scheme) + pathPtr, _ := syscall.UTF16PtrFromString(path) + + var key syscall.Handle + ret, _, _ := procRegOpenKeyExB.Call( + hkeyCurrentUser, + uintptr(unsafe.Pointer(pathPtr)), + 0, + keyReadB, + uintptr(unsafe.Pointer(&key)), + ) + if ret != 0 { + return "", fmt.Errorf("no default handler found for '%s'", scheme) + } + defer procRegCloseKeyB.Call(uintptr(key)) + + valueName, _ := syscall.UTF16PtrFromString("ProgId") + buf := make([]uint16, 256) + size := uint32(len(buf) * 2) + ret, _, _ = procRegQueryValueB.Call( + uintptr(key), + uintptr(unsafe.Pointer(valueName)), + 0, 0, + uintptr(unsafe.Pointer(&buf[0])), + uintptr(unsafe.Pointer(&size)), + ) + if ret != 0 { + return "", fmt.Errorf("no ProgId found for '%s'", scheme) + } + + return syscall.UTF16ToString(buf), nil +} diff --git a/apps/finicky/src/go.mod b/apps/finicky/src/go.mod index 26045d82..c2a151b0 100644 --- a/apps/finicky/src/go.mod +++ b/apps/finicky/src/go.mod @@ -15,6 +15,8 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect + github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 // indirect + github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.30.0 // indirect ) diff --git a/apps/finicky/src/go.sum b/apps/finicky/src/go.sum index 78e0c108..a7b30001 100644 --- a/apps/finicky/src/go.sum +++ b/apps/finicky/src/go.sum @@ -22,6 +22,10 @@ github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= +github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 h1:ftnsTqIUH57XQEF+PnXX9++nlHCzdkuB5zbWyMMruZo= +github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808/go.mod h1:rWifBlzkgrvd7zUqlfq91sWt3473OikgnglnIILx/Jo= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 h1:njuLRcjAuMKr7kI3D85AXWkw6/+v9PwtV6M6o11sWHQ= +github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs= github.com/jvatic/goja-babel v0.0.0-20250308121736-c08d87dbdc10 h1:vWIOMaPN3MJ9W2U+0sKPoouDoSdRP6TNEfmfmT8AmfA= github.com/jvatic/goja-babel v0.0.0-20250308121736-c08d87dbdc10/go.mod h1:zDU18A3osPvkOEpVRV2TAclAXKauNajDTPhyxqTcQO8= github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= @@ -32,6 +36,8 @@ github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBO github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b h1:GlTM/aMVIwU3luIuSN2SIVRuTqGPt1P97YxAi514ulw= github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZRA+znA6/Kz5vbSwh69QioernOHeDCatU= +golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/apps/finicky/src/logger/logger.go b/apps/finicky/src/logger/logger.go index 324bc281..2d7ea1c2 100644 --- a/apps/finicky/src/logger/logger.go +++ b/apps/finicky/src/logger/logger.go @@ -69,12 +69,10 @@ func SetupFile(shouldLog bool) error { return nil } - homeDir, err := util.UserHomeDir() + logDir, err := util.LogDir() if err != nil { - return fmt.Errorf("failed to get user home directory: %w", err) + return fmt.Errorf("failed to get log directory: %w", err) } - - logDir := filepath.Join(homeDir, "Library", "Logs", "Finicky") err = os.MkdirAll(logDir, 0755) // Create directory if it doesn't exist if err != nil { return fmt.Errorf("failed to create log directory: %v", err) diff --git a/apps/finicky/src/main.go b/apps/finicky/src/main.go index 829a8f27..d3a3cfdf 100644 --- a/apps/finicky/src/main.go +++ b/apps/finicky/src/main.go @@ -1,3 +1,5 @@ +//go:build darwin + package main /* diff --git a/apps/finicky/src/main_windows.go b/apps/finicky/src/main_windows.go new file mode 100644 index 00000000..f9ed1764 --- /dev/null +++ b/apps/finicky/src/main_windows.go @@ -0,0 +1,652 @@ +//go:build windows + +package main + +import ( + _ "embed" + "encoding/base64" + "finicky/browser" + "finicky/config" + "finicky/logger" + "finicky/resolver" + "finicky/rules" + "finicky/version" + "finicky/window" + "flag" + "fmt" + "log/slog" + "net" + "os" + "os/exec" + "runtime" + "strings" + "sync" + "syscall" + "time" + "unsafe" + + "github.com/dop251/goja" +) + +//go:embed assets/finickyConfigAPI.js +var finickyConfigAPIJS []byte + +type UpdateInfo struct { + ReleaseInfo *version.ReleaseInfo + UpdateCheckEnabled bool +} + +type URLInfo struct { + URL string + Opener *resolver.OpenerInfo + OpenInBackground bool +} + +type ConfigInfo struct { + Handlers int16 + Rewrites int16 + DefaultBrowser string + ConfigPath string +} + +var urlListener chan URLInfo = make(chan URLInfo) + +// windowClosed is signalled by the UI loop once RunWindow returns. Buffered so +// the UI loop never blocks if the event loop is momentarily busy. +var windowClosed chan struct{} = make(chan struct{}, 1) + +// showWindowChan carries show-window requests from the event loop (any +// goroutine) to the UI loop (the main, thread-locked goroutine that owns the +// WebView2 window). Buffered so requesters never block. +var showWindowChan chan struct{} = make(chan struct{}, 1) + +// queueWindowOpen lets other goroutines (e.g. the IPC listener handling a +// second `--window` invocation) ask the event loop to open the window, keeping +// the event loop's showingWindow bookkeeping authoritative. +var queueWindowOpen chan bool = make(chan bool, 1) + +var configChange chan struct{} = make(chan struct{}, 1) + +var vm *config.VM + +var forceWindowOpen bool = false +var lastError error +var dryRun bool = false +var skipJSConfig bool = false +var updateInfo UpdateInfo +var configInfo *ConfigInfo +var shouldKeepRunning bool = true + +var ( + kernel32Mutex = syscall.NewLazyDLL("kernel32.dll") + procCreateMutexW = kernel32Mutex.NewProc("CreateMutexW") +) + +const mutexName = "Global\\FinickyBrowserRouter" +const errorAlreadyExists = 183 + +// showWindowCmd is the sentinel a secondary instance sends over the IPC socket +// to ask the primary to open its config window (used when Finicky is launched a +// second time with --window, e.g. from the Start Menu entry). A real URL can +// never collide with this value. +const showWindowCmd = "__finicky_show_window__" + +func main() { + startTime := time.Now() + logger.Setup() + + // Pin the main goroutine to the main OS thread. The WebView2 window is + // created and pumped on this thread (see runUILoop / window.RunWindow); + // Win32 requires the window's message loop to run on its creating thread. + runtime.LockOSThread() + + flag.String("config", "", "Path to custom JS configuration file") + rulesPathPtr := flag.String("rules", "", "Path to custom rules JSON file") + noConfigPtr := flag.Bool("no-config", false, "Skip JS configuration file entirely") + windowPtr := flag.Bool("window", false, "Force window to open") + dryRunPtr := flag.Bool("dry-run", false, "Simulate without actually opening browsers") + flag.Parse() + + if *windowPtr { + forceWindowOpen = true + } + + // URL passed as an argument (Windows protocol-handler invocation). + urlFromArgs := "" + if args := flag.Args(); len(args) > 0 { + urlFromArgs = args[0] + } + + // Single-instance detection via a named mutex. CreateMutexW succeeds even + // when the mutex already exists, signalling that case through the error + // value captured by Call (which reads GetLastError immediately after the + // syscall). Reading GetLastError in a *separate* LazyProc.Call would race + // with the Go runtime's own intervening syscalls, so we must use this one. + mutexNamePtr, _ := syscall.UTF16PtrFromString(mutexName) + handle, _, callErr := procCreateMutexW.Call(0, 0, uintptr(unsafe.Pointer(mutexNamePtr))) + alreadyRunning := callErr == syscall.Errno(errorAlreadyExists) + + if alreadyRunning { + // Another instance owns the routing. Hand our work to it and exit. + if handle != 0 { + syscall.CloseHandle(syscall.Handle(handle)) + } + handOffToPrimary(urlFromArgs) + os.Exit(0) + } + if handle != 0 { + defer syscall.CloseHandle(syscall.Handle(handle)) + } + + customConfigPath := "" + flag.Visit(func(f *flag.Flag) { + if f.Name == "config" { + customConfigPath = f.Value.String() + } + }) + + if customConfigPath != "" { + slog.Debug("Using custom config path", "path", customConfigPath) + } + + if *rulesPathPtr != "" { + slog.Debug("Using custom rules path", "path", *rulesPathPtr) + rules.SetCustomPath(*rulesPathPtr) + } + + if *noConfigPtr { + slog.Debug("Skipping JS config") + skipJSConfig = true + } + + dryRun = *dryRunPtr + + currentVersion := version.GetCurrentVersion() + commitHash, buildDate := version.GetBuildInfo() + slog.Info("Starting Finicky", "version", currentVersion) + slog.Debug("Build info", "buildDate", buildDate, "commitHash", commitHash) + + go logDefaultBrowserStatus() + + namespace := "finickyConfig" + cfw, err := config.NewConfigFileWatcher(customConfigPath, namespace, configChange) + if err != nil { + handleFatalError(fmt.Sprintf("Failed to setup config file watcher: %v", err)) + } + + vm, err = setupVM(cfw, namespace) + if err != nil { + handleFatalError(err.Error()) + } + + slog.Debug("VM setup complete", "duration", fmt.Sprintf("%.2fms", float64(time.Since(startTime).Microseconds())/1000)) + + go checkForUpdates() + + window.TestUrlHandler = func(url string) { + go TestURLInternal(url) + } + + window.SaveRulesHandler = func(rf rules.RulesFile) { + slog.Debug("Rules updated", "count", len(rf.Rules)) + resolver.SetCachedRules(rf) + if vm == nil || !vm.IsJSConfig() { + if rf.DefaultBrowser == "" && len(rf.Rules) == 0 && rf.Options == nil { + vm = nil + return + } + script, err := rules.ToJSConfigScript(rf, namespace) + if err != nil { + slog.Error("Failed to generate config from rules", "error", err) + return + } + newVM, err := config.NewFromScript(finickyConfigAPIJS, namespace, script) + if err != nil { + slog.Error("Failed to rebuild VM from rules", "error", err) + return + } + vm = newVM + if vm != nil { + shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning + go checkForUpdates() + } + } + } + + // Start the IPC server that receives URLs (and show-window requests) from + // secondary instances. + go listenForURLs() + + // If launched with a URL argument, process it. + if urlFromArgs != "" { + go func() { + handleURLString(urlFromArgs, false) + }() + } + + if vm != nil { + shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning + } + + // The event loop runs on a background goroutine (like the macOS build); the + // main thread stays free to own the WebView2 UI in runUILoop. + go runEventLoop(cfw, namespace) + + runUILoop() +} + +// runUILoop owns the WebView2 window and MUST run on the main thread (pinned by +// runtime.LockOSThread in main). It waits for a show request, then creates and +// pumps the window's message loop here — blocking until the window closes — +// before notifying the event loop and looping back to wait for the next one. +func runUILoop() { + for range showWindowChan { + window.RunWindow() + select { + case windowClosed <- struct{}{}: + default: + } + } +} + +// requestShowWindow asks the UI loop (main thread) to open the window and seeds +// the current state, which buffers in the window package until the page loads. +func requestShowWindow() { + window.SendMessageToWebView("version", version.GetCurrentVersion()) + select { + case showWindowChan <- struct{}{}: + default: + } +} + +func runEventLoop(cfw *config.ConfigFileWatcher, namespace string) { + const oneDay = 24 * time.Hour + + showingWindow := false + timeoutChan := time.After(1 * time.Second) + updateChan := time.After(oneDay) + + if shouldKeepRunning { + timeoutChan = nil + } + + slog.Info("Listening for events...") + + if forceWindowOpen { + requestShowWindow() + showingWindow = true + timeoutChan = nil + } + + for { + select { + case urlInfo := <-urlListener: + startTime := time.Now() + slog.Info("URL received", "url", urlInfo.URL) + + config, err := resolver.ResolveURL(vm, urlInfo.URL, urlInfo.Opener, urlInfo.OpenInBackground) + if err != nil { + handleRuntimeError(err) + } else { + lastError = nil + } + if launchErr := browser.LaunchBrowser(*config, dryRun, urlInfo.OpenInBackground); launchErr != nil { + slog.Error("Failed to start browser", "error", launchErr) + } + + slog.Debug("Time taken evaluating URL and opening browser", "duration", fmt.Sprintf("%.2fms", float64(time.Since(startTime).Microseconds())/1000)) + + if !showingWindow && !shouldKeepRunning { + timeoutChan = time.After(2 * time.Second) + } else { + timeoutChan = nil + } + + case <-configChange: + startTime := time.Now() + slog.Debug("Config has changed") + var setupErr error + vm, setupErr = setupVM(cfw, namespace) + if setupErr != nil { + handleRuntimeError(setupErr) + } else { + lastError = nil + } + slog.Debug("VM refresh complete", "duration", fmt.Sprintf("%.2fms", float64(time.Since(startTime).Microseconds())/1000)) + if vm != nil { + shouldKeepRunning = vm.GetAllConfigOptions().KeepRunning + go checkForUpdates() + } + + case shouldShowWindow := <-queueWindowOpen: + if !showingWindow && shouldShowWindow { + requestShowWindow() + showingWindow = true + timeoutChan = nil + } + + case <-updateChan: + go checkForUpdates() + updateChan = time.After(oneDay) + + case <-windowClosed: + showingWindow = false + if !shouldKeepRunning { + slog.Info("Exiting due to window closed") + tearDown() + } else { + slog.Debug("Window closed") + } + + case <-timeoutChan: + slog.Info("Exiting due to timeout") + tearDown() + } + } +} + +func handleURLString(urlString string, openInBackground bool) { + if strings.HasPrefix(urlString, "finicky://open/") { + encodedURL := strings.TrimPrefix(urlString, "finicky://open/") + if decodedBytes, err := base64.StdEncoding.DecodeString(encodedURL); err == nil { + urlString = string(decodedBytes) + slog.Debug("Decoded finicky protocol URL", "decoded", urlString) + } else { + slog.Warn("Failed to decode finicky protocol URL", "error", err) + } + } + + urlListener <- URLInfo{ + URL: urlString, + Opener: &resolver.OpenerInfo{}, + OpenInBackground: openInBackground, + } +} + +func handleRuntimeError(err error) { + slog.Error("Failed evaluating url", "error", err) + lastError = err +} + +// logDefaultBrowserStatus reports whether Finicky is the default browser. On +// Windows 10+ an app cannot make itself default programmatically — the user +// does it in Settings (offered by the installer) — so unlike the macOS build we +// only observe and log here, never prompt. Prompting on every launch would pop +// the Settings app on every link click. +func logDefaultBrowserStatus() { + isDefault, err := isDefaultBrowser() + if err != nil { + slog.Debug("Failed checking if we are the default browser", "error", err) + } else if isDefault { + slog.Debug("Finicky is the default browser") + } else { + slog.Debug("Finicky is not the default browser") + } +} + +func TestURLInternal(urlString string) { + slog.Debug("Testing URL", "url", urlString) + config, err := resolver.ResolveURL(vm, urlString, nil, false) + if err != nil { + slog.Error("Failed to evaluate URL", "error", err) + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": err.Error(), + }) + return + } + window.SendMessageToWebView("testUrlResult", map[string]interface{}{ + "url": config.URL, + "browser": config.Name, + "openInBackground": config.OpenInBackground, + "profile": config.Profile, + "args": config.Args, + }) +} + +func handleFatalError(errorMessage string) { + slog.Error("Fatal error", "msg", errorMessage) + lastError = fmt.Errorf("%s", errorMessage) +} + +func checkForUpdates() { + var runtime *goja.Runtime + if vm != nil { + runtime = vm.Runtime() + } + + releaseInfo, updateCheckEnabled, err := version.CheckForUpdatesIfEnabled(runtime) + if err != nil { + slog.Error("Error checking for updates", "error", err) + } + + updateInfo = UpdateInfo{ + ReleaseInfo: releaseInfo, + UpdateCheckEnabled: updateCheckEnabled, + } + + if updateInfo.ReleaseInfo != nil && updateInfo.ReleaseInfo.HasUpdate { + slog.Info("New version is available", "version", updateInfo.ReleaseInfo.LatestVersion) + } + + if updateInfo.ReleaseInfo != nil { + window.SendMessageToWebView("updateInfo", map[string]interface{}{ + "version": updateInfo.ReleaseInfo.LatestVersion, + "hasUpdate": updateInfo.ReleaseInfo.HasUpdate, + "updateCheckEnabled": updateInfo.UpdateCheckEnabled, + "downloadUrl": updateInfo.ReleaseInfo.DownloadUrl, + "releaseUrl": updateInfo.ReleaseInfo.ReleaseUrl, + }) + } else { + window.SendMessageToWebView("updateInfo", map[string]interface{}{ + "version": "", + "hasUpdate": false, + "updateCheckEnabled": updateInfo.UpdateCheckEnabled, + "downloadUrl": "", + "releaseUrl": "", + }) + } +} + +func tearDown() { + checkForUpdates() + slog.Info("Exiting...") + os.Exit(0) +} + +func setupVM(cfw *config.ConfigFileWatcher, namespace string) (*config.VM, error) { + logRequests := true + var err error + + defer func() { + err = logger.SetupFile(logRequests) + if err != nil { + slog.Warn("Failed to setup file logging", "error", err) + } + }() + + var currentBundlePath, configPath string + if !skipJSConfig { + var err2 error + currentBundlePath, configPath, err2 = cfw.BundleConfig() + if err2 != nil { + return nil, fmt.Errorf("failed to read config: %v", err2) + } + } + + if rf, rulesErr := rules.Load(); rulesErr != nil { + slog.Warn("Failed to pre-load rules cache", "error", rulesErr) + } else { + resolver.SetCachedRules(rf) + } + + var newVM *config.VM + + if currentBundlePath != "" { + newVM, err = config.New(finickyConfigAPIJS, namespace, currentBundlePath) + if err != nil { + return nil, fmt.Errorf("failed to setup VM: %v", err) + } + } else { + rf, rulesErr := rules.Load() + if rulesErr != nil { + slog.Warn("Failed to load rules file", "error", rulesErr) + } else { + resolver.SetCachedRules(rf) + if rf.DefaultBrowser != "" || len(rf.Rules) > 0 { + script, scriptErr := rules.ToJSConfigScript(rf, namespace) + if scriptErr != nil { + return nil, fmt.Errorf("failed to generate config from rules: %v", scriptErr) + } + newVM, err = config.NewFromScript(finickyConfigAPIJS, namespace, script) + if err != nil { + return nil, fmt.Errorf("failed to setup VM from rules: %v", err) + } + configPath, _ = rules.GetPath() + } + } + } + + if newVM == nil { + return nil, nil + } + + cs := newVM.GetConfigState() + if cs != nil { + configInfo = &ConfigInfo{ + Handlers: cs.Handlers, + Rewrites: cs.Rewrites, + DefaultBrowser: cs.DefaultBrowser, + ConfigPath: configPath, + } + } + + opts := newVM.GetAllConfigOptions() + logRequests = opts.LogRequests + + window.SendMessageToWebView("config", map[string]interface{}{ + "handlers": configInfo.Handlers, + "rewrites": configInfo.Rewrites, + "defaultBrowser": configInfo.DefaultBrowser, + "configPath": configInfo.ConfigPath, + "isJSConfig": newVM.IsJSConfig(), + "options": map[string]interface{}{ + "keepRunning": opts.KeepRunning, + "hideIcon": opts.HideIcon, + "logRequests": opts.LogRequests, + "checkForUpdates": opts.CheckForUpdates, + }, + }) + + return newVM, nil +} + +// listenForURLs starts a local socket server. When a second finicky.exe is +// launched with a URL (or --window), it connects here and hands off the request +// instead of starting a duplicate router. +func listenForURLs() { + listener, err := net.Listen("unix", pipeAddr()) + if err != nil { + // Stale socket from a previous crash — remove and retry once. + os.Remove(pipeAddr()) + listener, err = net.Listen("unix", pipeAddr()) + if err != nil { + slog.Error("Failed to start URL listener", "error", err) + return + } + } + defer listener.Close() + + slog.Debug("IPC listener started", "address", pipeAddr()) + for { + conn, err := listener.Accept() + if err != nil { + slog.Error("IPC accept error", "error", err) + continue + } + go func() { + defer conn.Close() + buf := make([]byte, 8192) + n, err := conn.Read(buf) + if err != nil || n == 0 { + return + } + payload := strings.TrimSpace(string(buf[:n])) + switch { + case payload == "": + return + case payload == showWindowCmd: + slog.Info("Received show-window request from another instance") + select { + case queueWindowOpen <- true: + default: + } + default: + slog.Info("Received URL from IPC", "url", payload) + handleURLString(payload, false) + } + }() + } +} + +// handOffToPrimary forwards this (secondary) instance's work to the running +// primary over the IPC socket, then the caller exits. +func handOffToPrimary(urlFromArgs string) { + if urlFromArgs != "" { + if err := sendToPrimary(urlFromArgs); err != nil { + slog.Error("Could not forward URL to the running Finicky instance", "error", err) + // Fall back to the OS default handler ONLY if we are not it — + // otherwise `start` would relaunch Finicky and loop. + if def, _ := isDefaultBrowser(); !def { + cmd := exec.Command("cmd", "/c", "start", "", urlFromArgs) + cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} + _ = cmd.Run() + } else { + slog.Error("Running instance unreachable and Finicky is the default browser; dropping URL to avoid a relaunch loop", "url", urlFromArgs) + } + } + return + } + if forceWindowOpen { + if err := sendToPrimary(showWindowCmd); err != nil { + slog.Error("Could not ask the running Finicky instance to show its window", "error", err) + } + } +} + +// sendToPrimary hands a payload (a URL, or the show-window sentinel) to the +// running instance over the local socket. The primary creates its +// single-instance mutex before it begins listening, so a request from a +// near-simultaneous launch can briefly beat the socket into existence; retry +// for ~1s to cover that startup window. +func sendToPrimary(payload string) error { + var lastErr error + for i := 0; i < 20; i++ { + conn, err := net.Dial("unix", pipeAddr()) + if err != nil { + lastErr = err + time.Sleep(50 * time.Millisecond) + continue + } + defer conn.Close() + _ = conn.SetWriteDeadline(time.Now().Add(2 * time.Second)) + _, err = conn.Write([]byte(payload)) + return err + } + return lastErr +} + +var pipeOnce sync.Once +var pipeAddress string + +func pipeAddr() string { + pipeOnce.Do(func() { + cacheDir, err := os.UserCacheDir() + if err != nil { + pipeAddress = os.TempDir() + `\finicky.sock` + return + } + os.MkdirAll(cacheDir+`\Finicky`, 0755) + pipeAddress = cacheDir + `\Finicky\finicky.sock` + }) + return pipeAddress +} diff --git a/apps/finicky/src/util/directories.go b/apps/finicky/src/util/directories.go index cffdc00d..4e63c38a 100644 --- a/apps/finicky/src/util/directories.go +++ b/apps/finicky/src/util/directories.go @@ -1,3 +1,5 @@ +//go:build darwin + package util /* diff --git a/apps/finicky/src/util/directories_windows.go b/apps/finicky/src/util/directories_windows.go new file mode 100644 index 00000000..2d1655f7 --- /dev/null +++ b/apps/finicky/src/util/directories_windows.go @@ -0,0 +1,36 @@ +//go:build windows + +package util + +import ( + "fmt" + "os" + "strings" +) + +func UserHomeDir() (string, error) { + dir, err := os.UserHomeDir() + if err != nil || dir == "" { + return "", fmt.Errorf("failed to get user home directory: %w", err) + } + return dir, nil +} + +func ShortenPath(path string) string { + home, err := UserHomeDir() + if err != nil || home == "" { + return path + } + if path == home || strings.HasPrefix(path, home+`\`) || strings.HasPrefix(path, home+"/") { + return "~" + path[len(home):] + } + return path +} + +func UserCacheDir() (string, error) { + dir, err := os.UserCacheDir() + if err != nil || dir == "" { + return "", fmt.Errorf("failed to get user cache directory: %w", err) + } + return dir, nil +} diff --git a/apps/finicky/src/util/info.go b/apps/finicky/src/util/info.go index b42dd6c1..37d0e44b 100644 --- a/apps/finicky/src/util/info.go +++ b/apps/finicky/src/util/info.go @@ -1,3 +1,5 @@ +//go:build darwin + package util /* diff --git a/apps/finicky/src/util/info_darwin_ext.go b/apps/finicky/src/util/info_darwin_ext.go new file mode 100644 index 00000000..b970aa12 --- /dev/null +++ b/apps/finicky/src/util/info_darwin_ext.go @@ -0,0 +1,13 @@ +//go:build darwin + +package util + +import "path/filepath" + +func LogDir() (string, error) { + home, err := UserHomeDir() + if err != nil { + return "", err + } + return filepath.Join(home, "Library", "Logs", "Finicky"), nil +} diff --git a/apps/finicky/src/util/info_windows.go b/apps/finicky/src/util/info_windows.go new file mode 100644 index 00000000..02f20d24 --- /dev/null +++ b/apps/finicky/src/util/info_windows.go @@ -0,0 +1,182 @@ +//go:build windows + +package util + +import ( + "fmt" + "log/slog" + "os" + "strings" + "syscall" + "unsafe" +) + +var ( + user32 = syscall.NewLazyDLL("user32.dll") + kernel32 = syscall.NewLazyDLL("kernel32.dll") + procGetAsyncKeyState = user32.NewProc("GetAsyncKeyState") + procGetWindowTextW = user32.NewProc("GetWindowTextW") + + procCreateToolhelp32Snapshot = kernel32.NewProc("CreateToolhelp32Snapshot") + procProcess32FirstW = kernel32.NewProc("Process32FirstW") + procProcess32NextW = kernel32.NewProc("Process32NextW") + procGetSystemPowerStatus = kernel32.NewProc("GetSystemPowerStatus") +) + +const ( + vkShift = 0x10 + vkControl = 0x11 + vkMenu = 0x12 // Alt/Option + vkLWin = 0x5B // Left Windows key (maps to Command) + vkCapital = 0x14 +) + +func GetModifierKeys() map[string]bool { + result := map[string]bool{ + "shift": isKeyDown(vkShift), + "option": isKeyDown(vkMenu), + "command": isKeyDown(vkLWin), + "control": isKeyDown(vkControl), + "capsLock": isKeyDown(vkCapital), + "fn": false, // no direct equivalent on Windows + "function": false, + } + args := []any{} + for k, v := range result { + if k == "function" { + continue + } + args = append(args, k, v) + } + slog.Debug("Modifier keys state", args...) + return result +} + +func isKeyDown(vk int) bool { + ret, _, _ := procGetAsyncKeyState.Call(uintptr(vk)) + return ret&0x8000 != 0 +} + +func GetSystemInfo() map[string]string { + name, err := os.Hostname() + if err != nil { + name = "" + } + return map[string]string{ + "localizedName": name, + "name": name, + } +} + +type systemPowerStatus struct { + ACLineStatus byte + BatteryFlag byte + BatteryLifePercent byte + SystemStatusFlag byte + BatteryLifeTime uint32 + BatteryFullLifeTime uint32 +} + +func GetPowerInfo() map[string]interface{} { + var status systemPowerStatus + ret, _, _ := procGetSystemPowerStatus.Call(uintptr(unsafe.Pointer(&status))) + if ret == 0 { + slog.Debug("Power info", "isCharging", false, "isConnected", false, "percentage", nil) + return map[string]interface{}{ + "isCharging": false, + "isConnected": false, + "percentage": nil, + } + } + + isConnected := status.ACLineStatus == 1 + isCharging := status.BatteryFlag&0x08 != 0 + + var percentage interface{} + if status.BatteryLifePercent <= 100 { + percentage = int(status.BatteryLifePercent) + } + + slog.Debug("Power info", "isCharging", isCharging, "isConnected", isConnected, "percentage", percentage) + return map[string]interface{}{ + "isCharging": isCharging, + "isConnected": isConnected, + "percentage": percentage, + } +} + +const thSnapshot = 0x00000002 // TH32CS_SNAPPROCESS + +type processEntry32W struct { + Size uint32 + CntUsage uint32 + ProcessID uint32 + DefaultHeapID uintptr + ModuleID uint32 + CntThreads uint32 + ParentProcessID uint32 + PriClassBase int32 + Flags uint32 + ExeFile [260]uint16 +} + +func IsAppRunning(identifier string) bool { + handle, _, _ := procCreateToolhelp32Snapshot.Call(thSnapshot, 0) + if handle == ^uintptr(0) { + return false + } + defer syscall.CloseHandle(syscall.Handle(handle)) + + var entry processEntry32W + entry.Size = uint32(unsafe.Sizeof(entry)) + + ret, _, _ := procProcess32FirstW.Call(handle, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + return false + } + + target := strings.ToLower(identifier) + for { + name := strings.ToLower(syscall.UTF16ToString(entry.ExeFile[:])) + // Match against exe name with or without .exe suffix + if name == target || name == target+".exe" || + strings.TrimSuffix(name, ".exe") == target { + slog.Debug("App running info", "identifier", identifier, "isRunning", true) + return true + } + + entry.Size = uint32(unsafe.Sizeof(entry)) + ret, _, _ = procProcess32NextW.Call(handle, uintptr(unsafe.Pointer(&entry))) + if ret == 0 { + break + } + } + + slog.Debug("App running info", "identifier", identifier, "isRunning", false) + return false +} + +// GetForegroundWindowTitle returns the title of the currently focused window. +func GetForegroundWindowTitle() string { + procGetForegroundWindow := user32.NewProc("GetForegroundWindow") + hwnd, _, _ := procGetForegroundWindow.Call() + if hwnd == 0 { + return "" + } + + buf := make([]uint16, 256) + ret, _, _ := procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), 256) + if ret == 0 { + return "" + } + return syscall.UTF16ToString(buf) +} + +// LogDir returns the platform-appropriate log directory for Finicky. +func LogDir() (string, error) { + appData := os.Getenv("APPDATA") + if appData == "" { + return "", fmt.Errorf("APPDATA environment variable not set") + } + return appData + `\Finicky\Logs`, nil +} diff --git a/apps/finicky/src/window/window.go b/apps/finicky/src/window/window.go index 1119946a..95d5fad1 100644 --- a/apps/finicky/src/window/window.go +++ b/apps/finicky/src/window/window.go @@ -1,3 +1,5 @@ +//go:build darwin + package window /* diff --git a/apps/finicky/src/window/window_windows.go b/apps/finicky/src/window/window_windows.go new file mode 100644 index 00000000..e41dffa7 --- /dev/null +++ b/apps/finicky/src/window/window_windows.go @@ -0,0 +1,365 @@ +//go:build windows + +package window + +import ( + "encoding/json" + "finicky/assets" + "finicky/browser" + "finicky/rules" + "finicky/util" + "finicky/version" + "fmt" + "io/fs" + "log/slog" + "net" + "net/http" + "os" + "strings" + "sync" + + webview2 "github.com/jchv/go-webview2" +) + +var ( + messageQueue []string + queueMutex sync.Mutex + windowReady bool + TestUrlHandler func(string) + SaveRulesHandler func(rules.RulesFile) + + wv webview2.WebView + assetServer net.Listener +) + +func SendMessageToWebView(messageType string, message interface{}) { + jsonMsg := struct { + Type string `json:"type"` + Message interface{} `json:"message"` + }{ + Type: messageType, + Message: message, + } + jsonBytes, err := json.Marshal(jsonMsg) + if err != nil { + slog.Error("Error marshaling message", "error", err) + return + } + + queueMutex.Lock() + defer queueMutex.Unlock() + + if windowReady { + sendMessageToWebViewInternal(string(jsonBytes)) + } else { + messageQueue = append(messageQueue, string(jsonBytes)) + } +} + +// sendMessageToWebViewInternal must be called with queueMutex held. +func sendMessageToWebViewInternal(message string) { + if wv == nil { + return + } + escaped := strings.ReplaceAll(message, `\`, `\\`) + escaped = strings.ReplaceAll(escaped, `"`, `\"`) + js := fmt.Sprintf(`finicky.receiveMessage("%s")`, escaped) + w := wv + wv.Dispatch(func() { + w.Eval(js) + }) +} + +// RunWindow creates the WebView2 window and runs its message loop on the +// CURRENT OS thread, blocking until the window is closed. +// +// It MUST be called from the main thread — the goroutine pinned by +// runtime.LockOSThread in main(). Win32 delivers window messages only to the +// thread that created the window, so creating the window on one goroutine and +// pumping its loop on another (as an earlier version did) leaves the UI frozen +// and can crash WebView2 during initialization. Keeping creation and the +// message loop on one locked thread mirrors how the macOS build runs the whole +// Cocoa app on the main thread. +func RunWindow() { + slog.Debug("Creating window") + + w := webview2.NewWithOptions(webview2.WebViewOptions{ + Debug: true, + AutoFocus: true, + WindowOptions: webview2.WindowOptions{ + Title: "Finicky", + Width: 860, + Height: 600, + }, + }) + if w == nil { + slog.Error("Failed to create webview2 instance") + return + } + + queueMutex.Lock() + wv = w + windowReady = false + queueMutex.Unlock() + + w.SetSize(860, 600, webview2.HintNone) + + // Inject the finicky stub before any page JS runs (mirrors the macOS WKUserScript). + w.Init(` + window.finicky = { + _queue: [], + receiveMessage: function(msg) { this._queue.push(msg); }, + sendMessage: function(msg) { + window.__finicky_send(JSON.stringify(msg)); + } + }; + window.addEventListener("load", function() { + window.__finicky_ready(); + }); + `) + + // Bind the JS→Go message channel. + w.Bind("__finicky_send", func(msgJSON string) { + HandleWebViewMessage(msgJSON) + }) + + // Bind the navigation-complete signal so the message queue drains. + w.Bind("__finicky_ready", func() { + NavigationComplete() + }) + + // Start a local HTTP server to serve the embedded Svelte UI assets. + addr := startAssetServer() + if addr != "" { + w.Navigate("http://" + addr + "/index.html") + } else { + html, err := assets.GetHTML() + if err != nil { + slog.Error("Failed to load HTML", "error", err) + } else { + w.SetHtml(html) + } + } + + SendBuildInfo() + + // Blocks on the Win32 message loop until the window is closed. + w.Run() + + // Window closed — reset shared state so a later RunWindow starts clean and + // queued messages buffer again, and shut down the asset server it was using. + queueMutex.Lock() + wv = nil + windowReady = false + queueMutex.Unlock() + + if assetServer != nil { + assetServer.Close() + assetServer = nil + } +} + +// CloseWindow asks a running window to close, which unblocks RunWindow. Safe to +// call from any goroutine — it marshals onto the UI thread via Dispatch. +func CloseWindow() { + queueMutex.Lock() + w := wv + queueMutex.Unlock() + if w != nil { + w.Dispatch(func() { + w.Terminate() + }) + } +} + +func SendBuildInfo() { + commitHash, buildDate := version.GetBuildInfo() + buildInfo := fmt.Sprintf("(%s, built %s)", commitHash, buildDate) + SendMessageToWebView("buildInfo", buildInfo) +} + +func NavigationComplete() { + queueMutex.Lock() + windowReady = true + for _, message := range messageQueue { + sendMessageToWebViewInternal(message) + } + messageQueue = nil + queueMutex.Unlock() +} + +// startAssetServer spins up a localhost HTTP server serving the embedded +// Svelte UI. Returns the listen address (e.g. "127.0.0.1:54321") or "". +func startAssetServer() string { + mux := http.NewServeMux() + + filesystem := assets.GetFileSystem() + mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { + path := r.URL.Path + if path == "/" { + path = "/index.html" + } + path = strings.TrimPrefix(path, "/") + + content, err := assets.GetFile(path) + if err != nil { + // Try reading directly from the embedded FS. + data, fsErr := fs.ReadFile(filesystem, "templates/"+path) + if fsErr != nil { + http.NotFound(w, r) + return + } + content = data + } + + switch { + case strings.HasSuffix(path, ".html"): + w.Header().Set("Content-Type", "text/html; charset=utf-8") + case strings.HasSuffix(path, ".css"): + w.Header().Set("Content-Type", "text/css; charset=utf-8") + case strings.HasSuffix(path, ".js"): + w.Header().Set("Content-Type", "application/javascript; charset=utf-8") + case strings.HasSuffix(path, ".png"): + w.Header().Set("Content-Type", "image/png") + default: + w.Header().Set("Content-Type", http.DetectContentType(content)) + } + w.Write(content) + }) + + // Bind to a random available port on loopback only. + var err error + assetServer, err = net.Listen("tcp", "127.0.0.1:0") + if err != nil { + slog.Error("Failed to start asset server", "error", err) + return "" + } + + addr := assetServer.Addr().String() + slog.Debug("Asset server started", "address", addr) + + go http.Serve(assetServer, mux) + return addr +} + +func HandleWebViewMessage(messageStr string) { + var msg map[string]interface{} + if err := json.Unmarshal([]byte(messageStr), &msg); err != nil { + slog.Error("Failed to parse webview message", "error", err) + return + } + + messageType, ok := msg["type"].(string) + if !ok { + slog.Error("Message missing type field") + return + } + + slog.Debug("Received message from webview", "type", messageType) + + switch messageType { + case "testUrl": + handleTestUrl(msg) + case "getRules": + handleGetRules() + case "saveRules": + handleSaveRules(msg) + case "getInstalledBrowsers": + handleGetInstalledBrowsers() + case "getBrowserProfiles": + handleGetBrowserProfiles(msg) + default: + slog.Debug("Unknown message type", "type", messageType) + } +} + +func handleTestUrl(msg map[string]interface{}) { + url, ok := msg["url"].(string) + if !ok { + slog.Error("testUrl message missing url field") + return + } + if TestUrlHandler != nil { + TestUrlHandler(url) + } else { + SendMessageToWebView("testUrlResult", map[string]interface{}{ + "error": "Test handler not initialized", + }) + } +} + +func handleGetRules() { + rf, err := rules.Load() + if err != nil { + slog.Error("Failed to load rules", "error", err) + SendMessageToWebView("rules", map[string]interface{}{ + "defaultBrowser": "", + "rules": []interface{}{}, + }) + return + } + + path, _ := rules.GetPath() + var rulesPath string + if _, statErr := os.Stat(path); statErr == nil { + rulesPath = path + } + + type rulesResponse struct { + rules.RulesFile + Path string `json:"path,omitempty"` + } + SendMessageToWebView("rules", rulesResponse{RulesFile: rf, Path: util.ShortenPath(rulesPath)}) +} + +func handleSaveRules(msg map[string]interface{}) { + payload, ok := msg["payload"] + if !ok { + slog.Error("saveRules message missing payload field") + return + } + + payloadBytes, err := json.Marshal(payload) + if err != nil { + slog.Error("Failed to marshal saveRules payload", "error", err) + return + } + + var rf rules.RulesFile + if err := json.Unmarshal(payloadBytes, &rf); err != nil { + slog.Error("Failed to parse saveRules payload", "error", err) + return + } + + if err := rules.Save(rf); err != nil { + slog.Error("Failed to save rules", "error", err) + SendMessageToWebView("saveRulesError", map[string]interface{}{"error": err.Error()}) + return + } + + path, _ := rules.GetPath() + type rulesResponse struct { + rules.RulesFile + Path string `json:"path,omitempty"` + } + SendMessageToWebView("rules", rulesResponse{RulesFile: rf, Path: util.ShortenPath(path)}) + + if SaveRulesHandler != nil { + SaveRulesHandler(rf) + } +} + +func handleGetInstalledBrowsers() { + installed := browser.GetInstalledBrowsers() + SendMessageToWebView("installedBrowsers", installed) +} + +func handleGetBrowserProfiles(msg map[string]interface{}) { + browserName, _ := msg["browser"].(string) + profiles := browser.GetProfilesForBrowser(browserName) + SendMessageToWebView("browserProfiles", map[string]interface{}{ + "browser": browserName, + "profiles": profiles, + }) +} + diff --git a/scripts/build-windows.sh b/scripts/build-windows.sh new file mode 100755 index 00000000..b3a63b7e --- /dev/null +++ b/scripts/build-windows.sh @@ -0,0 +1,49 @@ +#!/bin/bash +set -e + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +echo "Building Finicky for Windows..." + +# Build frontend assets (same as macOS) +( + cd "$ROOT_DIR/packages/config-api" + npm run build + npm run generate-types + cp dist/finickyConfigAPI.js "$ROOT_DIR/apps/finicky/src/assets/finickyConfigAPI.js" +) + +( + cd "$ROOT_DIR/packages/finicky-ui" + npm run build + mkdir -p "$ROOT_DIR/apps/finicky/src/assets/templates" + cp -r dist/* "$ROOT_DIR/apps/finicky/src/assets/templates" +) + +# Build info +COMMIT_HASH=$(git -C "$ROOT_DIR" rev-parse --short HEAD 2>/dev/null || echo "dev") +BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC') +API_HOST=$(cat "$ROOT_DIR/.env" 2>/dev/null | grep API_HOST | cut -d '=' -f 2 || echo "") + +# Cross-compile for Windows +mkdir -p "$ROOT_DIR/apps/finicky/build/windows" + +CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ + go build -C "$ROOT_DIR/apps/finicky/src" \ + -ldflags \ + "-X 'finicky/version.commitHash=${COMMIT_HASH}' \ + -X 'finicky/version.buildDate=${BUILD_DATE}' \ + -X 'finicky/version.apiHost=${API_HOST}' \ + -H windowsgui" \ + -o ../build/windows/Finicky.exe + +echo "Binary: apps/finicky/build/windows/Finicky.exe" +ls -lh "$ROOT_DIR/apps/finicky/build/windows/Finicky.exe" +file "$ROOT_DIR/apps/finicky/build/windows/Finicky.exe" + +echo "" +echo "To build the installer, run Inno Setup on scripts/installer.iss" +echo " iscc scripts/installer.iss" +echo "" +echo "Build complete." diff --git a/scripts/installer.iss b/scripts/installer.iss new file mode 100644 index 00000000..e9bb186a --- /dev/null +++ b/scripts/installer.iss @@ -0,0 +1,112 @@ +; Finicky for Windows — Inno Setup installer +; Registers Finicky as a browser so Windows routes http/https URLs to it. +; Per-user install (no admin required). + +#define MyAppName "Finicky" +#define MyAppVersion "4.2.2" +#define MyAppPublisher "John Sterling" +#define MyAppURL "https://github.com/johnste/finicky" +#define MyAppExeName "Finicky.exe" + +[Setup] +AppId={{7F3E2A1B-4C5D-6E7F-8A9B-0C1D2E3F4A5B} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL}/issues +DefaultDirName={localappdata}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +OutputBaseFilename=FinickySetup-{#MyAppVersion} +Compression=lzma +SolidCompression=yes +PrivilegesRequired=lowest +SetupIconFile=..\apps\finicky\assets\Resources\finicky.ico +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +WizardStyle=modern +UninstallDisplayName={#MyAppName} + +[Languages] +Name: "english"; MessagesFile: "compiler:Default.isl" + +[Tasks] +Name: "autostart"; Description: "Start Finicky when Windows starts"; GroupDescription: "Additional options:" +Name: "setdefault"; Description: "Open Default Apps settings after install"; GroupDescription: "Additional options:"; Flags: unchecked + +[Files] +Source: "..\apps\finicky\build\windows\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion + +[Icons] +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" + +[Registry] +; ---- ProgID: FinickyURL ---- +; This is the handler that runs when Windows invokes a URL assigned to Finicky. +Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueType: string; ValueData: "Finicky URL"; Flags: uninsdeletekey +Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" +Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" + +; ---- finicky:// custom protocol (always active, independent of default browser choice) ---- +Root: HKCU; Subkey: "SOFTWARE\Classes\finicky"; ValueType: string; ValueData: "Finicky Protocol"; Flags: uninsdeletekey +Root: HKCU; Subkey: "SOFTWARE\Classes\finicky"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" +Root: HKCU; Subkey: "SOFTWARE\Classes\finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKCU; Subkey: "SOFTWARE\Classes\finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" + +; ---- Browser registration under StartMenuInternet ---- +; This makes Finicky appear in Settings > Default Apps > Web browser. +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky"; ValueType: string; ValueData: "Finicky"; Flags: uninsdeletekey +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" + +; ---- Capabilities ---- +; Tells Windows which URL schemes Finicky can handle. +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationName"; ValueType: string; ValueData: "Finicky" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationDescription"; ValueType: string; ValueData: "A rule-based browser routing utility. Define rules to open URLs in different browsers based on the link domain, path, or source application." +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "http"; ValueType: string; ValueData: "FinickyURL" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "https"; ValueType: string; ValueData: "FinickyURL" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "finicky"; ValueType: string; ValueData: "FinickyURL" + +; ---- RegisteredApplications ---- +; The master index Windows checks to discover which apps offer URL handling. +Root: HKCU; Subkey: "SOFTWARE\RegisteredApplications"; ValueName: "Finicky"; ValueType: string; ValueData: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; Flags: uninsdeletevalue + +; ---- Autostart (optional) ---- +; Launch WITHOUT --window: autostart makes Finicky the resident URL router at +; login (useful with keepRunning), it must not pop the config window on boot. +Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueName: "Finicky"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart + +[Run] +; After install, optionally open Default Apps so the user can select Finicky +Filename: "cmd"; Parameters: "/c start ms-settings:defaultapps"; Description: "Open Default Apps settings"; Flags: postinstall shellexec skipifsilent nowait; Tasks: setdefault +; Launch Finicky after install +Filename: "{app}\{#MyAppExeName}"; Parameters: "--window"; Description: "Launch Finicky"; Flags: postinstall skipifsilent nowait + +[UninstallRun] +; If Finicky is running during uninstall, terminate it +Filename: "taskkill"; Parameters: "/F /IM {#MyAppExeName}"; Flags: runhidden; RunOnceId: "KillFinicky" + +[UninstallDelete] +; Clean up the IPC socket and cache +Type: filesandordirs; Name: "{localappdata}\Finicky" + +[Code] +// Notify Windows that registered applications changed so the Default Apps +// list refreshes without requiring a reboot. +procedure CurStepChanged(CurStep: TSetupStep); +var + Res: Integer; +begin + if CurStep = ssPostInstall then + begin + // SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0) + // Inno doesn't expose SHChangeNotify directly, but we can call it via + // a small helper. The registry changes above are sufficient — Windows + // picks them up on next Settings open. For immediate refresh we'd need + // a DLL call, which is optional polish. + end; +end; diff --git a/scripts/winget/JohnSterling.Finicky.installer.yaml b/scripts/winget/JohnSterling.Finicky.installer.yaml new file mode 100644 index 00000000..4ed45f8c --- /dev/null +++ b/scripts/winget/JohnSterling.Finicky.installer.yaml @@ -0,0 +1,23 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.6.0.schema.json +# Submitted to https://github.com/microsoft/winget-pkgs +# Update InstallerUrl and InstallerSha256 for each release. + +PackageIdentifier: JohnSterling.Finicky +PackageVersion: 4.2.2 +MinimumOSVersion: 10.0.17763.0 +InstallerType: inno +Scope: user +InstallModes: + - interactive + - silent + - silentWithProgress +UpgradeBehavior: install +Installers: + - Architecture: x64 + InstallerUrl: https://github.com/johnste/finicky/releases/download/v4.2.2/FinickySetup-4.2.2.exe + InstallerSha256: # Fill after building the installer + InstallerSwitches: + Silent: /VERYSILENT /SUPPRESSMSGBOXES /NORESTART + SilentWithProgress: /SILENT /SUPPRESSMSGBOXES /NORESTART +ManifestType: installer +ManifestVersion: 1.6.0 diff --git a/scripts/winget/JohnSterling.Finicky.locale.en-US.yaml b/scripts/winget/JohnSterling.Finicky.locale.en-US.yaml new file mode 100644 index 00000000..0981cbf6 --- /dev/null +++ b/scripts/winget/JohnSterling.Finicky.locale.en-US.yaml @@ -0,0 +1,26 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.6.0.schema.json + +PackageIdentifier: JohnSterling.Finicky +PackageVersion: 4.2.2 +PackageLocale: en-US +Publisher: John Sterling +PublisherUrl: https://github.com/johnste +PackageName: Finicky +PackageUrl: https://github.com/johnste/finicky +License: MIT +LicenseUrl: https://github.com/johnste/finicky/blob/main/LICENSE +ShortDescription: A rule-based browser routing utility for Windows and macOS. +Description: |- + Finicky lets you set up rules that decide which browser opens for every URL. + Route any URL to your preferred browser with powerful matching rules, automatically + edit URLs before opening them, and write rules in JavaScript or TypeScript for + complete control. Set Finicky as your default browser and it routes each link to + the right place based on domain, path, or source application. +Tags: + - browser + - browser-router + - default-browser + - url-handler + - productivity +ManifestType: defaultLocale +ManifestVersion: 1.6.0 diff --git a/scripts/winget/JohnSterling.Finicky.yaml b/scripts/winget/JohnSterling.Finicky.yaml new file mode 100644 index 00000000..203d24cf --- /dev/null +++ b/scripts/winget/JohnSterling.Finicky.yaml @@ -0,0 +1,7 @@ +# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.6.0.schema.json + +PackageIdentifier: JohnSterling.Finicky +PackageVersion: 4.2.2 +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.6.0 From 5ed1293337932dc341a385f42ea9b7268a252f0c Mon Sep 17 00:00:00 2001 From: Ozimondias Date: Thu, 2 Jul 2026 01:19:22 -0700 Subject: [PATCH 02/15] fix(windows): address CodeRabbit review findings - launcher: fix duplicate URL + misplaced args in the no-known-exe fallback; open the URL via ShellExecuteW (util.OpenURLDefault) instead of `cmd /c start`, so URL metacharacters can't be re-tokenized by the shell into commands - main: same injection-safe ShellExecuteW fallback for the single-instance URL hand-off (still guarded so it never relaunches Finicky when it's the default) - launcher: drain child stdout/stderr via CombinedOutput to avoid a pipe deadlock - detect: enumerate StartMenuInternet under both HKCU and HKLM (+dedupe) so per-user browser installs (default Chrome/Edge/Firefox) are detected - window: gate WebView2 Debug mode behind FINICKY_DEBUG (off by default) - main: back off on repeated IPC Accept() errors instead of a tight loop - ci: `copy /Y` so the Windows asset copy step can't prompt/hang - docs: correct the winget id (JohnSterling.Finicky) and make the native build example self-contained Pre-existing patterns shared verbatim with the macOS build (vm/configInfo/lastError races, fatal-error continuation, failed-rules-load handling) are left matching the reference implementation rather than diverged in the Windows files. Refs #542 Co-Authored-By: Claude Opus 4.8 --- .github/workflows/windows-build.yml | 2 +- README.md | 8 ++-- apps/finicky/src/browser/detect_windows.go | 32 ++++++++++--- apps/finicky/src/browser/launcher_windows.go | 47 +++++++------------ apps/finicky/src/main_windows.go | 11 +++-- apps/finicky/src/util/shellexec_windows.go | 49 ++++++++++++++++++++ apps/finicky/src/window/window_windows.go | 4 +- 7 files changed, 106 insertions(+), 47 deletions(-) create mode 100644 apps/finicky/src/util/shellexec_windows.go diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 7acc3d23..76e2a249 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -43,7 +43,7 @@ jobs: cd packages/config-api npm run build npm run generate-types - copy dist\finickyConfigAPI.js ..\..\apps\finicky\src\assets\finickyConfigAPI.js + copy /Y dist\finickyConfigAPI.js ..\..\apps\finicky\src\assets\finickyConfigAPI.js shell: cmd - name: Build UI diff --git a/README.md b/README.md index a8827219..52522a92 100644 --- a/README.md +++ b/README.md @@ -39,8 +39,8 @@ Finicky is an application for macOS and Windows that allows you to set up rules ### Windows -- Download `FinickySetup.exe` from [releases](https://github.com/johnste/finicky/releases) and run the installer -- Or install via winget: `winget install Finicky` +- Download `FinickySetup-.exe` from [releases](https://github.com/johnste/finicky/releases) and run the installer +- Or install via winget: `winget install --id JohnSterling.Finicky` - Create a configuration file at `%USERPROFILE%\.finicky.js` (same format as macOS — configs are cross-platform) - Open **Settings > Default Apps** and set Finicky as the default web browser - Finicky runs in the system tray and routes URLs based on your rules @@ -117,7 +117,9 @@ See [Building Finicky from source](https://github.com/johnste/finicky/wiki/Build **Windows (native):** ```powershell -$env:CGO_ENABLED="0"; go build -C apps/finicky/src -ldflags "-H windowsgui" -o ../build/windows/Finicky.exe . +$env:CGO_ENABLED="0" +New-Item -ItemType Directory -Force apps/finicky/build/windows | Out-Null +go build -C apps/finicky/src -ldflags "-H windowsgui" -o ../build/windows/Finicky.exe . ``` ### Works well with diff --git a/apps/finicky/src/browser/detect_windows.go b/apps/finicky/src/browser/detect_windows.go index ce121ab6..2c7fbbbe 100644 --- a/apps/finicky/src/browser/detect_windows.go +++ b/apps/finicky/src/browser/detect_windows.go @@ -3,7 +3,6 @@ package browser import ( - "log/slog" "sort" "strings" "syscall" @@ -11,7 +10,7 @@ import ( ) var ( - advapi32 = syscall.NewLazyDLL("advapi32.dll") + advapi32 = syscall.NewLazyDLL("advapi32.dll") procRegOpenKeyEx = advapi32.NewProc("RegOpenKeyExW") procRegEnumKeyEx = advapi32.NewProc("RegEnumKeyExW") procRegQueryValue = advapi32.NewProc("RegQueryValueExW") @@ -19,6 +18,7 @@ var ( ) const ( + hkeyCurrentUser = 0x80000001 hkeyLocalMachine = 0x80000002 keyRead = 0x20019 ) @@ -30,20 +30,40 @@ func GetInstalledBrowsers() []string { } func getBrowsersFromRegistry() []string { - path := `SOFTWARE\Clients\StartMenuInternet` + const path = `SOFTWARE\Clients\StartMenuInternet` + + // Browsers register their Start Menu entry under HKCU for per-user + // (non-elevated) installs — which is what the default Chrome/Edge/Firefox + // installers do — and under HKLM for system-wide installs. Scan both roots + // and dedupe so per-user installs aren't missed. + seen := map[string]bool{} + var names []string + for _, root := range []uintptr{hkeyCurrentUser, hkeyLocalMachine} { + for _, name := range enumStartMenuBrowsers(root, path) { + key := strings.ToLower(name) + if !seen[key] { + seen[key] = true + names = append(names, name) + } + } + } + return names +} + +func enumStartMenuBrowsers(root uintptr, path string) []string { pathPtr, _ := syscall.UTF16PtrFromString(path) var key syscall.Handle ret, _, _ := procRegOpenKeyEx.Call( - hkeyLocalMachine, + root, uintptr(unsafe.Pointer(pathPtr)), 0, keyRead, uintptr(unsafe.Pointer(&key)), ) if ret != 0 { - slog.Debug("Failed to open StartMenuInternet registry key") - return []string{} + // Missing key just means no browsers registered under this root. + return nil } defer procRegCloseKey.Call(uintptr(key)) diff --git a/apps/finicky/src/browser/launcher_windows.go b/apps/finicky/src/browser/launcher_windows.go index 804f7305..ed4a6034 100644 --- a/apps/finicky/src/browser/launcher_windows.go +++ b/apps/finicky/src/browser/launcher_windows.go @@ -6,7 +6,6 @@ import ( _ "embed" "encoding/json" "fmt" - "io" "log/slog" "os" "os/exec" @@ -14,6 +13,8 @@ import ( "slices" "strings" + "finicky/util" + "al.essio.dev/pkg/shellescape" ) @@ -79,16 +80,17 @@ func LaunchBrowser(config BrowserConfig, dryRun bool, openInBackgroundByDefault // Find the browser executable exePath := findBrowserExe(config.Name) if exePath == "" { - // Fallback: use cmd /c start to let Windows find the browser - allArgs := append([]string{"/c", "start", "", config.URL}, cmdArgs...) - cmd := exec.Command("cmd", allArgs...) - prettyCmd := formatCommand(cmd.Path, cmd.Args) + // Fallback: no known exe for this browser name — open the URL with the + // system default handler. The URL is passed to ShellExecuteW as a single + // argument (never through cmd.exe), so metacharacters in it can't be + // re-tokenized into commands. Profile/custom args can't be honored here + // because the target executable is unknown. if dryRun { - slog.Debug("Would run command (dry run)", "command", prettyCmd) + slog.Debug("Would open URL via default handler (dry run)", "url", config.URL) return nil } - slog.Debug("Run command (fallback)", "command", prettyCmd) - return cmd.Run() + slog.Debug("Opening URL via default handler (no known exe)", "browser", config.Name, "url", config.URL) + return util.OpenURLDefault(config.URL) } cmd := exec.Command(exePath, cmdArgs...) @@ -101,30 +103,13 @@ func LaunchBrowser(config BrowserConfig, dryRun bool, openInBackgroundByDefault slog.Debug("Run command", "command", prettyCmd) - stderr, err := cmd.StderrPipe() - if err != nil { - return err - } - stdout, err := cmd.StdoutPipe() - if err != nil { - return err - } - - if err := cmd.Start(); err != nil { - return err + // CombinedOutput drains stdout and stderr concurrently, avoiding the + // deadlock that draining two pipes sequentially can cause when the child + // fills one pipe's buffer while we're blocked reading the other. + out, cmdErr := cmd.CombinedOutput() + if len(out) > 0 { + slog.Debug("Command output", "output", string(out)) } - - stderrBytes, _ := io.ReadAll(stderr) - stdoutBytes, _ := io.ReadAll(stdout) - cmdErr := cmd.Wait() - - if len(stderrBytes) > 0 { - slog.Error("Command returned error", "error", string(stderrBytes)) - } - if len(stdoutBytes) > 0 { - slog.Debug("Command returned output", "output", string(stdoutBytes)) - } - if cmdErr != nil { return fmt.Errorf("command failed: %v", cmdErr) } diff --git a/apps/finicky/src/main_windows.go b/apps/finicky/src/main_windows.go index f9ed1764..bbc6d949 100644 --- a/apps/finicky/src/main_windows.go +++ b/apps/finicky/src/main_windows.go @@ -10,6 +10,7 @@ import ( "finicky/logger" "finicky/resolver" "finicky/rules" + "finicky/util" "finicky/version" "finicky/window" "flag" @@ -17,7 +18,6 @@ import ( "log/slog" "net" "os" - "os/exec" "runtime" "strings" "sync" @@ -561,6 +561,7 @@ func listenForURLs() { conn, err := listener.Accept() if err != nil { slog.Error("IPC accept error", "error", err) + time.Sleep(50 * time.Millisecond) continue } go func() { @@ -595,11 +596,11 @@ func handOffToPrimary(urlFromArgs string) { if err := sendToPrimary(urlFromArgs); err != nil { slog.Error("Could not forward URL to the running Finicky instance", "error", err) // Fall back to the OS default handler ONLY if we are not it — - // otherwise `start` would relaunch Finicky and loop. + // otherwise opening the URL would relaunch Finicky and loop. if def, _ := isDefaultBrowser(); !def { - cmd := exec.Command("cmd", "/c", "start", "", urlFromArgs) - cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true} - _ = cmd.Run() + if openErr := util.OpenURLDefault(urlFromArgs); openErr != nil { + slog.Error("Fallback open via default handler failed", "error", openErr) + } } else { slog.Error("Running instance unreachable and Finicky is the default browser; dropping URL to avoid a relaunch loop", "url", urlFromArgs) } diff --git a/apps/finicky/src/util/shellexec_windows.go b/apps/finicky/src/util/shellexec_windows.go new file mode 100644 index 00000000..f7d90f7d --- /dev/null +++ b/apps/finicky/src/util/shellexec_windows.go @@ -0,0 +1,49 @@ +//go:build windows + +package util + +import ( + "fmt" + "syscall" + "unsafe" +) + +var ( + shell32Util = syscall.NewLazyDLL("shell32.dll") + procShellExecuteW = shell32Util.NewProc("ShellExecuteW") +) + +// OpenURLDefault opens a URL with the system's default handler via ShellExecuteW. +// +// Unlike `cmd /c start `, the URL is handed to the shell as a single +// wide-string argument and is never parsed by cmd.exe, so shell metacharacters +// in an attacker-controlled URL (&, |, ^, >, quotes, ...) cannot break out and +// inject commands. Use this instead of shelling out to cmd for URL opening. +func OpenURLDefault(url string) error { + verb, err := syscall.UTF16PtrFromString("open") + if err != nil { + return err + } + target, err := syscall.UTF16PtrFromString(url) + if err != nil { + return err + } + const swShowNormal = 1 + // ShellExecuteW(hwnd, lpVerb, lpFile, lpParameters, lpDirectory, nShowCmd) + ret, _, callErr := procShellExecuteW.Call( + 0, + uintptr(unsafe.Pointer(verb)), + uintptr(unsafe.Pointer(target)), + 0, + 0, + swShowNormal, + ) + // ShellExecuteW returns a value greater than 32 on success. + if ret <= 32 { + if callErr != nil && callErr != syscall.Errno(0) { + return fmt.Errorf("ShellExecuteW failed: %w", callErr) + } + return fmt.Errorf("ShellExecuteW failed with code %d", ret) + } + return nil +} diff --git a/apps/finicky/src/window/window_windows.go b/apps/finicky/src/window/window_windows.go index e41dffa7..9cf36301 100644 --- a/apps/finicky/src/window/window_windows.go +++ b/apps/finicky/src/window/window_windows.go @@ -84,7 +84,9 @@ func RunWindow() { slog.Debug("Creating window") w := webview2.NewWithOptions(webview2.WebViewOptions{ - Debug: true, + // Debug (WebView2 devtools/context menu) is opt-in via env, off by + // default in shipped builds. Set FINICKY_DEBUG=1 to enable for testing. + Debug: os.Getenv("FINICKY_DEBUG") != "", AutoFocus: true, WindowOptions: webview2.WindowOptions{ Title: "Finicky", From 1bda128151e1e752792ddedae6ba2832eda5fa8d Mon Sep 17 00:00:00 2001 From: Ozimondias Date: Fri, 3 Jul 2026 13:54:31 -0700 Subject: [PATCH 03/15] fix(windows): default browser = Edge, rule-editor focus, detect logging MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Smoke-test fixes (real Win10): - Default browser is now platform-aware via browser.DefaultBrowserName (Edge on Windows, Safari on macOS) — replaces hardcoded "com.apple.Safari" in resolver and rules. UI no longer seeds the field with "Safari". - Rules editor: ignore the echo of our own saveRules so it doesn't reassign `rules` and steal focus when moving between the browser and URL fields (F4 — new rules couldn't be created under WebView2). - detect_windows: log per-root (HKCU/HKLM) and total browser counts to diagnose empty browser dropdown (F3). --- .../src/browser/default_browser_darwin.go | 11 +++++++++++ .../src/browser/default_browser_windows.go | 12 ++++++++++++ apps/finicky/src/browser/detect_windows.go | 11 +++++++++-- apps/finicky/src/resolver/resolver.go | 4 ++-- apps/finicky/src/rules/rules.go | 4 +++- packages/finicky-ui/src/pages/Rules.svelte | 17 +++++++++++++++-- packages/finicky-ui/src/pages/StartPage.svelte | 9 +++++---- 7 files changed, 57 insertions(+), 11 deletions(-) create mode 100644 apps/finicky/src/browser/default_browser_darwin.go create mode 100644 apps/finicky/src/browser/default_browser_windows.go diff --git a/apps/finicky/src/browser/default_browser_darwin.go b/apps/finicky/src/browser/default_browser_darwin.go new file mode 100644 index 00000000..930845e0 --- /dev/null +++ b/apps/finicky/src/browser/default_browser_darwin.go @@ -0,0 +1,11 @@ +//go:build darwin + +package browser + +// DefaultBrowserName / DefaultBrowserAppType is the out-of-the-box fallback +// browser used when the user hasn't configured one. Safari is always present +// on macOS. +const ( + DefaultBrowserName = "com.apple.Safari" + DefaultBrowserAppType = "bundleId" +) diff --git a/apps/finicky/src/browser/default_browser_windows.go b/apps/finicky/src/browser/default_browser_windows.go new file mode 100644 index 00000000..caaceade --- /dev/null +++ b/apps/finicky/src/browser/default_browser_windows.go @@ -0,0 +1,12 @@ +//go:build windows + +package browser + +// DefaultBrowserName / DefaultBrowserAppType is the out-of-the-box fallback +// browser used when the user hasn't configured one. Microsoft Edge ships on +// every Windows 10/11 machine, so it's the safe default (not Safari, which +// doesn't exist on Windows). +const ( + DefaultBrowserName = "Microsoft Edge" + DefaultBrowserAppType = "appName" +) diff --git a/apps/finicky/src/browser/detect_windows.go b/apps/finicky/src/browser/detect_windows.go index 2c7fbbbe..7ec7e4d3 100644 --- a/apps/finicky/src/browser/detect_windows.go +++ b/apps/finicky/src/browser/detect_windows.go @@ -3,6 +3,7 @@ package browser import ( + "log/slog" "sort" "strings" "syscall" @@ -38,8 +39,13 @@ func getBrowsersFromRegistry() []string { // and dedupe so per-user installs aren't missed. seen := map[string]bool{} var names []string - for _, root := range []uintptr{hkeyCurrentUser, hkeyLocalMachine} { - for _, name := range enumStartMenuBrowsers(root, path) { + for _, r := range []struct { + label string + root uintptr + }{{"HKCU", hkeyCurrentUser}, {"HKLM", hkeyLocalMachine}} { + found := enumStartMenuBrowsers(r.root, path) + slog.Debug("Browser registry scan", "root", r.label, "found", len(found)) + for _, name := range found { key := strings.ToLower(name) if !seen[key] { seen[key] = true @@ -47,6 +53,7 @@ func getBrowsersFromRegistry() []string { } } } + slog.Debug("Installed browsers detected", "count", len(names), "browsers", names) return names } diff --git a/apps/finicky/src/resolver/resolver.go b/apps/finicky/src/resolver/resolver.go index 389c07af..2f149aaf 100644 --- a/apps/finicky/src/resolver/resolver.go +++ b/apps/finicky/src/resolver/resolver.go @@ -138,8 +138,8 @@ func evaluateURL(vm *config.VM, url string, opener *OpenerInfo) (*browser.Browse func defaultBrowserConfig(urlStr string, openInBackground bool) *browser.BrowserConfig { bg := openInBackground return &browser.BrowserConfig{ - Name: "com.apple.Safari", - AppType: "bundleId", + Name: browser.DefaultBrowserName, + AppType: browser.DefaultBrowserAppType, OpenInBackground: &bg, Args: []string{}, URL: urlStr, diff --git a/apps/finicky/src/rules/rules.go b/apps/finicky/src/rules/rules.go index 30909ec4..1305201c 100644 --- a/apps/finicky/src/rules/rules.go +++ b/apps/finicky/src/rules/rules.go @@ -5,6 +5,8 @@ import ( "fmt" "os" "path/filepath" + + "finicky/browser" ) type Rule struct { @@ -180,7 +182,7 @@ func ToJSHandlers(rules []Rule) []map[string]interface{} { func ToJSConfigScript(rf RulesFile, namespace string) (string, error) { defaultBrowser := rf.DefaultBrowser if defaultBrowser == "" { - defaultBrowser = "com.apple.Safari" + defaultBrowser = browser.DefaultBrowserName } var defaultBrowserObj interface{} diff --git a/packages/finicky-ui/src/pages/Rules.svelte b/packages/finicky-ui/src/pages/Rules.svelte index ccc97c55..a203692e 100644 --- a/packages/finicky-ui/src/pages/Rules.svelte +++ b/packages/finicky-ui/src/pages/Rules.svelte @@ -44,6 +44,11 @@ } } let pendingSave = $state(false); + // Set when we initiate a save. The backend echoes a "rules" message back; without + // this guard the $effect below would reassign `rules` and re-render every row, + // which steals focus when moving between the browser and URL fields (WebView2 + // exposes this deterministically). We ignore the echo of our own save. + let selfSaved = false; function save() { clearTimeout(saveTimer); @@ -52,6 +57,7 @@ defaultProfile: rulesFile.defaultProfile, rules, }; + selfSaved = true; window.finicky.sendMessage({ type: "saveRules", payload }); pendingSave = false; } @@ -65,6 +71,7 @@ defaultProfile: rulesFile.defaultProfile, rules, }; + selfSaved = true; window.finicky.sendMessage({ type: "saveRules", payload }); pendingSave = false; }, SAVE_DEBOUNCE); @@ -148,14 +155,20 @@ // Sync incoming props into local state when they change; also fetch profiles for known browsers $effect(() => { + // Track inputs up front so external changes still re-sync after an early return. + const incoming = rulesFile; + const browsers = installedBrowsers; if (pendingSave) return; - const newRules = rulesFile.rules.map((r: Rule) => ({ + // Ignore the echo of our own save — reassigning `rules` here re-renders the + // rows and breaks focus moving between the browser and URL fields. + if (selfSaved) { selfSaved = false; return; } + const newRules = incoming.rules.map((r: Rule) => ({ ...r, match: Array.isArray(r.match) ? r.match : r.match ? [r.match as unknown as string] : [""], })); rules = newRules; rowIsCustom = newRules.map( - (r) => r.browser !== "" && !installedBrowsers.includes(r.browser) + (r) => r.browser !== "" && !browsers.includes(r.browser) ); rowProfileIsCustom = newRules.map( (r) => { diff --git a/packages/finicky-ui/src/pages/StartPage.svelte b/packages/finicky-ui/src/pages/StartPage.svelte index 30582bfb..c969c561 100644 --- a/packages/finicky-ui/src/pages/StartPage.svelte +++ b/packages/finicky-ui/src/pages/StartPage.svelte @@ -33,9 +33,10 @@ let logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false; let checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true; - const SAFARI = "Safari"; - - let defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser || SAFARI); + // No hard-coded browser fallback — that seeded "Safari" (macOS-only) on every + // platform. When no default is set, show the placeholder; the Go layer applies + // the platform-appropriate default (Edge on Windows, Safari on macOS) at runtime. + let defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser ?? ""); let defaultProfile = rulesFile.defaultProfile ?? ""; let defaultBrowserIsCustom = false; let defaultProfileIsCustom = false; @@ -47,7 +48,7 @@ hideIcon = rulesFile.options?.hideIcon ?? config.options?.hideIcon ?? false; logRequests = rulesFile.options?.logRequests ?? config.options?.logRequests ?? false; checkForUpdates = rulesFile.options?.checkForUpdates ?? config.options?.checkForUpdates ?? true; - defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser || SAFARI); + defaultBrowser = isJSConfig ? (config.defaultBrowser ?? "") : (rulesFile.defaultBrowser ?? ""); defaultProfile = rulesFile.defaultProfile ?? ""; } // Separate statements so browser/profile list updates only affect these derived From c6c9771cb7378ab389ad7e48f6d09e1fa350fed6 Mon Sep 17 00:00:00 2001 From: Ozimondias Date: Fri, 3 Jul 2026 14:05:35 -0700 Subject: [PATCH 04/15] feat(windows): embed app icon + version resource; About cross-platform wording - versioninfo.json + resource_windows_amd64.syso (goversioninfo) embed finicky.ico and version metadata into the PE; linked for windows/amd64 only, macOS unaffected - About page: "macOS application" -> "Available on macOS and Windows" --- apps/finicky/src/resource_windows_amd64.syso | Bin 0 -> 746916 bytes apps/finicky/src/versioninfo.json | 25 +++++++++++++++++++ packages/finicky-ui/src/pages/About.svelte | 4 +-- 3 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 apps/finicky/src/resource_windows_amd64.syso create mode 100644 apps/finicky/src/versioninfo.json diff --git a/apps/finicky/src/resource_windows_amd64.syso b/apps/finicky/src/resource_windows_amd64.syso new file mode 100644 index 0000000000000000000000000000000000000000..f1eec84bf226b26ad2b38187259f8a35a494f174 GIT binary patch literal 746916 zcmeFa2b@(!mOqX&JK?u+=J)&W?(EL&P98HkH<<>SoK!>wL;(>|F*_;(ihz;?6*DNK zf}kRxVgNB`$)TZh(>dpU`TPHV>%Ds1e*Io|6S{}l<$n5f-Fx4?x2n!Lb?VfqQ>UhE zIH#44WmEs5Rq*WZ&S`z?ZMR-)?gsy@pYji_dhj%hsg9o$`j|gm+2e<;e#q1FTD6+{ z&rDtCw3_=frU^f2HJ6Fw_~%wX;MICQr{_On`gcqdnf{1r8Pgv#{VCu73De&&{VCI~ zO#ctlQB41y>2#(l&0MDc$n+_u|HSl7rpo^-rvJh;ohcK?aZaoM!wm0MA?h5#`-@`S ze=5fP^)c?h8{__0G43bExSto}zAMK4p9-mUj$plh5##=OG46MbaX+En{U5Xv-~2*! zp3|yT+j{g4=6`v;_k#DQN4c->T^-{)ck}s0gy~|M&{0N4StRBN9e0yK*Ngc=$8Ube z>p$8n^2Q+YX}{ywWe@Y33}=sDtURaHdA~WHL7y!@s*~UPcYahSzqTL!s7`)~dwA{0 zZ*I+x>g2b+>yPW?x2Mlf>g4CT`X5?Nw}oMP_2jzOU-3HGL4UTnR;uoA22V}FO7P)_AD+8s&z_z8j~uGl zn|Q#z_izyQBp!6{O+4h@cVNH!z<~pm`}XboWarME7dc*SwPC}CAM*b8V>wyKdh;8U zJ@_6fW^X~oJsVMW-%H4Sb~BFd-iO4LWE?wo4E)Zg-+lL;M2k5-`Q(%TIFg!Fmb>+P z1V+9A|B%;#A+G=fp9K0W0J_}{*QNI&w(sxVyZ5&#S?N_3 z3qFK*{5z=nAQhgm8{tWK3N@pbqxO=AfX-9l9y<#ehYlk(Jyavw0^U6EG`GZk)*L-9Z6}pZbIZ`M8ywc*T>WAI{`Yu83h?S@$9-W6TB@=-* zgHe6wJmi&^)pD;Nu9K-wWk0zJj9a zYFAoXS~!2bUtUpBRl(sm&HE| z`U98C^_B|kBe+V-P_^MLRNucC)w36&YW?eQm6V#_^bDWfP9Cho$F;S!zxMfjTl{{1 zp&xWTkh1b4 z%FnqJcTE93TgHx5?}~`OAhacfv zJ9bRrvtw4RT6Jz?CAjp`OMm?3mtQ_~@ZiB>$_5-wPQuaDWCx`1JOxR~$w*2{3Ip3T z_)Zz$U3SYYw>Wygx;gUqxfvN5NaXVejvhwh_xq6a(f3H+@hy_Ke}k0mUm^L!uaNld zZX7(CNH~H+hYlG~c``FI@y$2iEUEj|;MLo2zx|8DhYuI3Y>9^tqhRHS@Jx6OfideS zTh}5m{8S_1T^Ea`DO0=;IUXu-2VJQA1sK4lKR|Ni@7 zk>96W9(61$3#kW=Q08tzV8m9G-L(@{H*aSi>=#D91Ppx!NLUW^djRNtH{oWW<3toM zcnZmx8BqBsU$B4w{t1!aXWuh6CqEaNUmb*xeS~k=W>oJ;gFB}jwPQBIJK%ZL41OHt zBbTD&nw6-yY7zWBX0jer5a@b2vftj0G?p(fFHf|-I`aF({n)&s0%U!80KU=dSmsTr zp7AdHZXZh4e}|H_U!e5E{iw|=fv3g=Z)G*AubvNdoeZ=ei^`d^k&%;wf`S6@TlK!r zzHe+{NilN1It1TH;(Fvd_!C}2^((s&pwNnT)NcP8zD`#GT`q+uemt@dC!)BtMEz*g z?-!SsA@3W0e;E7UVb2ratJ#OHh9{>q+UMabDTjN|bwH;v@O2r6ysy7SX=NpnlauRx zpR^fUR#}09U5W4xB@G9!B)>ckPxnPAox2eM_79PV>Q#H&BB0F>csnPc;G6GJQBw`} zVfDUGz8YIuRfWQ|V{naK%>H;G+yfV&Wc7C9eNW8qyXUbS7xss%??@D+q@t>(M(r%> z_p7U`QBqls^1Ge|dj1){e)qxk!u#+Sl-8qxFFhNc*~{VYG77ke{qGI8p_sT~9~1Ta z)Q`rpe?mFySN!QNxO+jlzZu>^cffT!arMPs_zK8B&%VaICmFn;!yvfZ^g!`P zpP;<91|E+GSy@^2%1^!>soyGDhDvuWiXVL$-i}wZ9@mq{NyE-p!`FQhd;_N<(0(-A z)d)fYyyta6@gvLmj@!J;eiZEYuWA)>nmfBZdHzbhXX%i{|8QTE06D7oc9 zRK#D2>Mp}k-FYx7`i(-#jd!B#^Dp^KAowib@p`>_x0Ja4W#spD&F{~$Jg8?4@P>y_ zQ&@z`#3aH|o)?;X(pl_t)G^Gfr0?u#-_|`oa1LoOi_d1-=gmt^_{Zn7nb)8BD7Aow zkMi>J|I25)5m!SABO8HX{APE;|JU$W|2BVRy@dBxu2#QpmEP(n20m)_L)tn{9PIo0 zmi?~uu-_2+5W;#G2Y~+@^e|TWZ~hoMbm-4HJ|4@l=tG}=`swqZefHVQOxHC9I2L_{ z&#nIW(q3}EQU8U0RSeKf2-g#$uLuHsdckbV_ z-rukv59+r^5^;z+YxqonQ+}=WIgh{jZ_)zV#@X{W1y`Z$Rey_i*Gp z>c=V6Rf)I59M`iR2Ba$s7sNy2^5x4NR;Rk9;TW=y$|{^CvQA064xr*L4nv1iZyO?b znfl3#)McNeE#e6vp886^h156a68;R&sQXd)$cs3-CurL^!g7d)`kp}bB43QIo9pqb z-Me?+%>E2VS%#xYM^S$F+stzVFlIAh3t>}GrylVamWgE;!ZIW*V_9euiJwEg;7;m| zGf+G39;EO13`f$_q|=2#-zI(^IG$tOGt%lI_SZ;CPetZC-*IffyvJ^ZXEb&0v76u> zMZJEE^m^iA_!{bgPZ1xF5+BsX;)#QP?1uW>!ZJ*SXV45|pGl&9#`3SeO_L(^5-}y4?;S-Tb9Asu4Lm~AO@!1bMG*#j%2X z6c-nZ7h>j58Z9j?Ed_07D1DIpu68C~ANex%&owB2@{?HEhVT2mK2%?M59@vf@j$(@ z^+3|#F=Q1NLEmN`G4tnGVJUSa;bm50c>BxsM8PUn1|z@>fukKHYqopSFDq& zC2LskvCN;)dH~0(^N_KvForl7MH^*5l-@NLd1a+WPp6$BX8x29OEs1$s3=A80a;HnkKn79np=?&w+S5^Uc{Dh!Be_>CK6COnY$9S}tX$%rGf69lY8V40~ zELHflJ1ezV~oHV?HN(|OZ!W0t_1m^zm9|*MFpzAc+53WY-RrkUD@bm2VZbYCh@!gqYxX#Ku zf#bM1lnlQNrLqt6Tl%KPXq0cOTg5o|+k>i)_(V|caiL_-VU!HIfp%=lv!2t8-32`) zU#_`GIpTUAhm;`cs6!K2S0|Govt zzxN>&EMJAXoZBrlXpz(Di z_;rtUw$IsA-_XIj7n29O#j3mWK6&qA^2&RJ;LGL(=d;H{Ep8Mf2y+@4RTy zq8>|@Ea`PB@W>;N^nBof2fA<`@#2942mTlH`A35Cvw4QHKB>ffW8%b#|4kdsEY9g| zqn#jyP)c8l8iI%5jRkxz=5LO7JhZ>m^16)Ar_!Gs$kT$a` z`28cYHOPh|+mCEdoZmaGpl|Biva2XBem|4*$TL`mpPUrtNk7h0-$px;SKmC{p~mpD1O5uJC|@%2M!VciHC83Hp0e%HpFCtY>7wsE!ynrd@J(X zj%(3FG$$UfYiham-1XOA|8w%=e%S_PBh+_HO55Qf+A|Na>?u3GL-vawQ{R0b#S7n| zU1Tdt9@s*76D9NBAZ$d*+*eUNZyjxa8<73nTS$HPQykv6KWK|2e#778_jPWsoq4MK zq*XR$#eX-o4j+8*!48Gk?PLO+1+vuHQEo%L9aqx%jZk@c|oL`&s^K_7t+Sbm4+Ui8{rc;SUV zCLO<#--6nY0e(Mi`!0CLu-qeAX4z|HyB$l=`RLb!_Ox&vo@e=K!<3C@@N$;_k)XXp zw$%Q#JM`tazt=2|F{jZF>t3Yo{Jfq`H=H-i5aNZjyYH5FWy6P_Aq{DcN;7T8_^!}) zUUnPpZo`P{F>kWmw0&Rl4$o-|)VbL5Chr(^>*G@zYKcHo4?L0j|y+D*tq=ywA^h7^Wu98DnHLl*R5Okn}$o@s?`%uJn;)Ynv8>`U23Ne};M)=l*qO`b9|V0#$$9BJ?*%fFK4XFC|k zb})eLpg-*%{m4VM{Jo~L4p-B6U>Y(${(^po8AkpqZwo3v`-B?yMHe(y{+C{Q=~sNV zNafdi^jAdIOP?X2c?Ok{c(0|e%dy$pQIku1(5I;=Gwnb$AaALjrX7UJKSbqswt-Oj z``#9^+pzpSuBDw~JgUddLMnYe==W&yR{jF<0_);__uY5fG**7n=)d?*vD%E@OQp>` zcjXT9^6~Ol4|x-%TY`3LjX8=Q`y8&pa81f%}m*uB?QClVd z_(y8);eFb?y(&Ij1r;b7|0?H2=o3mgS{qM)(gBa*7=43|-S8~ZDR+`5K7zuxzeN5k z?<05WBdG0uLtXsGvHV@e!PDz<`W$_SblT;WzaWImCYPV@6l+dL@1=9jpkM*p>R`4@ z;WyNNs|IgG>E?r`{Q6h2aThY;mLucV^~hPc1qUx(iqhRF@bkBdlq~u%l!l-6y4)z7 zLVIS{>u7ru|IqH%eFAM?m(#~&7>XWRiS+zDlfTLk!ex`o&v%ODm!1#hTO7d&dfLQMiCIa?onZmX}!v_5qqZAuU{k{(_<> zK53|!F@Fp2my+2}(%w2LIHx1qqp>@Vrk~DGWbgbGS)4Od8A7;ha{2jAvF6+Kp5|N% z7f?P9WS^?JwUN{pR1YI3U!g7cIn*4@jaklyeyIL@H)`Z>7RNet)4a^3Yy+cczl=xz z>X(ttxfzuqgv%zCpECBp_)f9r)byU_Y>F08HV&XZp!pj0O(R|i)`fD?%X$^w^M;wn zYpATze)E*lw`=eWmYa1jcHc{k%{OpSKjLgT=XOd=8ED&A+T`-{*<#H%>b=~OLbfN$ zo%j{(kDdzltD1XK|D?HIrWO5{qvDf8(TdpMJ@HWKZI8g;o_Ob+9J-8Q`G?a+-xo#q z%ttPL|5OIyl4+C6&u5D@r>OTdXH~S|bp-mc4;}It;fbJbroJ@UZt%4uI;u^E1?FVLL2 z^cm@u(hCUwLCfLmznJ~f9OSS0AX*U{yyr^IL}eUt-&wdH!G3r!{1^0~%v;9!wsKR3 z5H3CRA8Azni2sTAV$Ij;y}}COU>(;x^rG%4J#r9rF*A3!6t4J%$Qn8y>DR76@|BMu zX%YLl6FmI1qgRb(U)F(q(`lHQU-Dki9r-W3j6%-ssSLs;(y-+;l66Tr9yg6UXG6g$E9zjPsOc zzLxoiaOt5hev{fS-z(NUr``(#SEuAIe;Ku%sB=m8)%c^2O?lpX7Im;2==VGgRsCn8 z;(=%3PUc!5*2Kqts(RHr>d@pB`kn;)S*Awb3?c3Zvi$u}`=|EEU9<${Zu%8F^41(J z%haUub6wf5_%4?YRR`ybHD^qauXP#c0duanmvh(Cg1WE9e+FhUr3~rCzEnOs)X97u zC!nSu$G0=)!!_zA_%xp@e_*2{jE*RKfA}uwf}By~m`4FBtAq2(%2yy zRQ~znE3drr?|e2_{s!V9y=y?{#7pu1MC1;*o;<@blgcXnx2N=A)~V-ImVHW4pVT~b zH|oS4CEE$=U%OI2kpF@By_3Z|%YIQec>!I<7DKqs*jYGN$-=T7caV{B2Iny!NRxAG49+{&qN6G#J zsO227GY{vv=8Q>?B-$2aTW|DWTX|9fMD>mZS_4)xpRQ%7WX>W!>Nmhl@UX02STd~{z2d16bW1rI$yy(dBQKVdO<%_UCWHjI(1p6FQ*`NhL;>s$4{$NcPlKC1d9a$ZAA+M9e( zhtM2 z%9DA!D2t{!igd!Ke7_UF^P#@ik_YqrhI|u$!Uc}D(LII)h z6hYs}AtVt#XC6{J7SJ_NpY+)s-)&aNjTLe3s%F>n?Grwaq} zr;`7JKz_q50XDzW;mgJ%pYWml<@+S~F+sju(i0WNOtygL1cgXbUdmH>3ser_y}9vW z>;2PT{pwf0T)cR3AC4iW(SGwF<3Fw9TE(^W=XsUzT5|xuzk%QV%SRu5^fc$}7rgPt z8#k<6xpH9Fu3dk_x(g@5h465?58?Ii2tS@NW5)SAcI=ohKQQ`Vx#bsR{ZVY{^s&)B zT|0lX_v~}_I@U9hzw27QgnFO;M?U&7CUM>I%GtALcVZbez7jv2E?%&8|3|J(zJYb- z6nKj9?UJt`^Z%=Y${_zZm5KgwMVyPD^UGiU^8c~^qSxub!QcIi2YK%a;ah%e%JWPF ziwDjpyfI?Li2pndI8b~4DSbUx$lpi)hfVdd{A)f(JAmRFz0tO9+n=%gCo2!cJLhnH z`vkVPfN-li2?vp2?`u7~;xQTD)nxLC1?$ghujRKI4YVenKBV$xjm*PdTfExqSgCM8 zIjOZ{qT$K3gL9@$oAw`U-$x~DqUCc~l20Y*oI(y}+JF zD@){WYOB0gvTWJ1i%+utttd-wke`*!Uv1x>o0NXXhe*e?w;8kW?yc}%_6mI$=m(>9 z(DF-4SQGT`8T2gYrs=mikiI7L{|frhF6BHU=iU1*;C$VKoadTj=7D`f=AvrKQsh0l z9?753mzF-3hm(%h%{PRPI%SD+PrkBWfBp3`*8gOVpOE%DRsU$dyNY*kgzLnM7QP4n z@Rx&ra3j|TeX)!$)4HH9ko-R7A2gh}kZ;N`;z2&R^7$}6pi79Ohl!hqf^*>VqwLN3 zSNih#2HuI1yPiPmhhN}u8vC3k_!K+Ksy>13C3*b#@h2RUhPusfiQi)Jem&n`(&Oks z`ib4Sm33gd6%_vwzpJd{@F`r~-Sws4Tk8`haC&~cq>-(_o0jqn2zGZZdBlKG?B;Ap~ zx*OlYH-bKpW8Mt<5t@|u^qX+Q7-PqA@H}}Sy7zxy4$;`vdmpYi?N zO#e^vg7JBl-?Ly#@Hc}4@`3S-qinG4!0^E1As(PCaJB*YqxZju^5M>4{S}|C7w5sd zTmyH)Ok})GIT^zz+F4ee%TI3Cu3a(L5XZs`*YwQPyW)Q*B(eU*ly%0}Ua>vKvhJ6# zzVtu#6Fj3S2MK{XI8a-lYzVgl@qpxj+Q3TEe_5yvP!`IU+O&atgZ>!Cry*2-&8gF; z+S6|eGKqsE#_DsHE!w$!{iv(2h*i&6_ldK+^{!+{7}Dr>{oO&vC#9S>v?sr-o|kN6 zyx4b;M_=WXaqDsHvR6?#j4>0%6Y}SmOfYK^$TN{^5sV+U>c5!$zmWB}GQe5?S)~8W zpihDP2)aywi}BUcKl|FmvkjNeS=P|G>OdLzT&#M=x=);IQn^^A3&`u#llx=N3-keZ!= ztQS9FJQvnK%=^Lm=Z@Hjs?;Lbqb8#md1KeZKWbBq`l}ys)SvVZ(1*?2e=fXz@1tL& z=-)v7yI)1W_R%O<$o?@qD_llfR-390?8jb?RnJ)WRe$+u>O0Z_%=hTCpZ)lD6E8C8 zZylsRNdMwtucQ3)bX$Fs7Oegn6$8a9;{Uqz_j4Ufbv)yf^jbiFo`rA?U~GWycOjKB zKds+w$mu)-Ro!l)FB$tlOMk7Y5dVw*sz3eW-95%3^V2VpX8rOVKkF8&wtKR>N^RP9fZpoD!1xSJ>Z1tulmc+ zR60NyxaJ`DVfJ%_UI_MQ((xn*3=X!y%h=<&SFDG-s4`p)#TVLt{c0S(e}jqF8sIPG z^zAI#n-YGep_9t}NhpY8EX^L|gK+&Pg?yUE8K0m)>jdO8zD5RPCWXryDz|8OGWDm- zm`C3lv(7iLAfY1+e~#r^1*~Wmn$7m{nX?A+Y6TDjHG`I$`>&{_^kDItdHM z3UgV1uJaj4*=^T5NWVAhPblAr3;)PhkdN6-g#j}d!}6M zM)kjp^`}1=HiU2D?%MgdK6cWaLv~i_~WU!)>$(`JSgs%i9PlguJ>AtBFek6 zj}M=o%JVB_8{bD&JIcGbNujpSbBEukk3MyIPrtx*EsX6PDz8$}@MP;RUtj4@7II1o zSbwg0=)WR3hO@EGw6=EmnmXVa^b~Tgc^+DSbvlmP)MF^=!FJzm0>Sb=+kPmPlJCOa z$Y02{HKDaBw#=d-b(a&SKikYa`bHZ4LEn-dp*1%J50kI^Eu-#68-`hT!*Ptpvu5oL z<$|$sD18QHy%(eC(d8Hh6e4sLKi;27=eVK`W8VwT`ablGB2Nk`-%Ft2I4N zpXrd_v5#@%^6y^|UfW~qFB(#RJ7M~>&CHX3x9S}RuKCGZ$$q{!$EjL>E&G+mZ)W|{ zV@!z;13D*gY+KdqA>=Q5Z?5{*ZKnMzii`(F?O5xf;g^^ScZJA4N`ff_b3$& zPqzN@*VcC|6f(|V?%EIF>NPKDv(frtv-T;33uDh)&N$;sxh`h`Dj5&3oX;NR`rMK| zNvDhhZ$%YKhBA(R$I+Dg#6#!dvEMDCjOIQ&3)aC)|7Vq#d#RZ8l!?y|C{ASc5bA$gQ|6#O+Vw?A*Z#<- zO}2>gFI--hT{NUVc*69z^_Tt>2FBzpBIHe727mASg0?EzYYfb1Iv>T|??m}$`%g!8 z>wLqVoPnwt_rqssFPT4-d_Tynjq)>oVcz9evuq{c0>9(sW!XhT>Om)5e~zQ($yZ(a zQyt)%ubijfM77p@$xazRhcGt`uAX<(X3rRp#pQL%f3g=|u2HIdd@ZVbjfY>cD(zYm z#_BZdHMl;h`rMAlS-A=&q;;LLvg}$ja4Pk;Yt6zb+O_m+Jdx+G+0%+UrAW z-(9Au5Ah~)ZbTc-j2$V*TdUda?UC9h8UTL4*fa)XTRT( zz_lu+Tvr(`C)Ptufa~YH-DaUG?q-h7 z=A+`tO>iZq8+p)B{UtMMzuk|Dsk7mteG47O*3omFe2?~hQG0F&WZiZ*%1HZqNAsj)7%oYQOZtf;EuZ2v|j~!+yr;$>Ff`0Kb?gz3*VGgoUeq(2EFzg^+3MHct|DC1Xa(yhN}2$;Zw|1?voR&JI~Gd&dNL1U2C+&_w9S5 z>xtWa>})D~4vc{`R{zHR~2BUo$4$$5;i|2J07Ix5&E6 zUM_CacyBn9M?r2IB3>IMk|}W zr#i6y&%~-{toziNrt97CI#1#_+Ih9lgMu%1A-xyl8+T%TwARzC_f%bJt2RLXFm#5e#LQYopp(}t<#>PKF;Vjq4pkHQ;GI{P3*j!n6LPHO+jsBFi2D3-i|tC!(e7?}JLp`c$nc{@)#|p0Vz8{;Ml>N5AwZ>5z>< zEkpm7Bqk%}id&F>(QtT8+-K3))|WO5!{ch>9K(lYyXr0Z?x49D(*$)pt-toR;n?=~ zZIHtDUcxv`wdBjj%42_*3?naJ9;=?Q?rXg>@t3RqLpW#(xWtd_b27MgHI?)%;o4Pi zXUa9zH(Y04E6#nG-nQNjnuqw^%a{cvT#K99wmUKwFGp1wW1^GSno<@$Bi)Con{&C1 z#;1ychjS=PH;aGtohAeE<>ou32ah22?uAI{G7|acGj>FqA++6$WL$EMTLty&9q2HS z`aSho#mV?Xd!)4QiPYQgLFs`*=9?~lyUFEXnMHfja1VW;PHGJs?OvBi2HG{NCz6_V zupIf7C8grnQ!gTk_W9(l!;nEcW$w8>QFtEr>=K;Y9fg1DirhbSMh4^0CwJDHPxYktah^rgO|Z#vfsH~E}2@alcit|Ehb zYJBt=ve&+f%%x8t{lUcsGM7Han31obaOWp{Ceu8tcMJ~sp4JmLIY03qpAXS3gK~^J z2A}*vTS^b|5ydUnHS^ojry|Q@@7dJ(oV};iOFH^kuZF-&ezWh`eC)MN?fqD(`YqL; zI_niDU-RQ{QkS@%-_ucVjkNMIbyNKa&THqnea7CiY2-VR*UsncH*D(s&7O-+I>@&V zAFsiwpHYqo2c|<+9?I`b0$XnpiTppG@h;@|dRhB^sd@hk?;V&E(W&SKzi z8Uu}Y+^Qa*G64(z1xVNRPoQ`NTvnve4mxZ)$HXmQQqmz92m>%L%xRpQ5l<*i#9V!fE5yS4`S9#)oCqQ1>~R(YXEAUV z17|UC76WH7a25k+F>n?GXEAUV17|UC76WH7(83rn@pzc0O*Ow43l#I`ZvkSJ-`=-% zRrqi6|^G@;a1d4a&1o^uBt%0-rwqCZLsA#D8{g3T=J>n#5|#++KCII4_o`Y_{f-OW8;r#|}Vqa(QQbXUe( z{R8nJJ`rv#|BDwC4>pnycg!`aK*K9#(L7Vg~<3{O}*CpO?~!sK4;6V)LAE;E0!DU93Yet2Z@Zy^(Ob)z30IP zAM8gQ{VIej;mq=Z+K}3ka4R?y4_h{sQ+z8qFPaN}$@V>#_1wrh7h0SsrXR0OoLR=Y z#s4Nyd|kzwWjqqYu&y{3qhg_K|8s(wCfl0x!>)!?t;qPvunq zF1r8gGtWFTnswSvTKiQ;(L0jP!bRl0XxEKBr{7~e6L~-KTKPr07p)vkxpy+((mRqt zAv_&==bd+E{l|a&$FD=UlUS2Jh6Bk)@d5jX&$tg=0&!}`5Yh|I6dtfLU+rGBH)FTet5=U?jNU}W?X&XzROo&x zymKn=o=TZd*Sq2YwxNpc+qd6C{7O$0AE<9V6L>)MSAQRF_l&DOgEFkbwt3hFKHZQHh6NOz;lpQZW9(>xmPBuCTIO#J)x zAs$dat9B>ZD_XSlAss~H5@WmLnC=?V-feYymG@LYIB>q-*tPT7<~)xq$BE>1D(~8S z6th%vfOyyVQ0-3b&yEdw+9HSeO?)SP^q*F&SkadVpqHxLddnfz<_g+3bgCueL9~`=|K$`2S`f^_}`Bl~Lsr*z?FV)^E0c(Vma>P5UkT zZe!0mpSS1E)Sf%P7kRE{oDf-Fdu_iJncClN8u`2Zw*B3v&gUd!+1@Hwty&c?8W7YE zw3H06ZD0DwKXQMf1&YmU%iSy~^`C=B2--(Qdy;716|?6G?P$^VTWRvWqx?<~^qqs; z`@-aLg!_Lm&t|Dh1pl0D9+CmHLwtVWg%@g0L_DCrSnaa858Lnm!~FU4J5uJCO71tc z9@1s?tW6`&&3Cj{j_6F9hz^On_9N-jT}a*bHPT=I9LHY%1X*i#BKw)`$a(4mb z_dfC*%aC%E<=~z(h9~N_eP3~CQ-`aQQ$oKn^vGH^6MY0VQGhc)2%2(hWvljkgFCZ{@HDfY9&lrt@ z=LpX-ULoVOntgblVyr&KC>^kh@Hk`FJw{l`IBt&;mJ^mTuGta-V>|X;L|Di;Ru2&v zmNhGLoXq7-FrCt1@|u!4{+a}L)@?Cbkz4$zaiTI zc_DG&z=8iIdJv=sh(^tI=-bNU@3{`Yt=hiqm8V*d!y&pSf3ypQ3%0>^#VZI5d69G` zXy2vbgb|EKZ9wOOwFLgIc-6z#5Xcvb-5ie7`?Mow?Gwl2w`y-5#duYG(**K@_UF(Z zErZAlih0RckBEPOJi;|siUry8&x{3k7d(S!qx?F?A%1*gaQ`Ou6=n}7_Qj`M|5Io` z8V_(@aXM)tySsF<=IjHk-uDyE$==WSL#FQ1M@|+FQ@$SkcAufUXEf=mJqWZ1kK)ir z1^q@|Fni8K;{zuTP%hZG#oCw2#PFp&2=f5tf#Ctl0~6;|JV1G1;(n3`O#I9HgK;4H z-9uiugK?j3g=fHBD4V$)nd`S9k$X2CPEAH4`@oaM!Ku8bK7ixOPqddIX>a=go;J%N z+5c1R!$`UFt#pu+E<0tZXrH;^bJSe94gt}91Zi$!&94vcL16Y9bDpmw-38(e@j-|e zj68^x1?(G4>~1Ru*e4hsV4tAa&L*a@WB_>}ECbjln7Fr;0cO9VdE^1gf%w_vfqMz$ z0mZ}YLq3R`1%KR4sJdbvvYvkfM;Nm%$Ok9i7ET72lD~vn?w8tCv>`N?J;>_)KVe>- zm8bHS?svkV{oay}B6sC>c!!ei!$?O%>sOfyHUwcbQw!#v{arlacI=Bm<-e z7#Se?QwC`NLdgK_UuWrW_AtAZe8709-Dkkx?K+gsco-=keTl^MR8!ZJ!ci0HZoe)1 z(+2Yz=`Q)-Tsna0FW&mm?%lgrF^*`H{Az3&jjWKoNK0l+XSRFMUi(U#_U)wk>%2!W z`|rGQJk>n|;tf4dFfzf(1C%56h0L?2-2 z&px2x!R=oK@>^g!A4$-b-WN zcxYTuryqzV0~}+4CiMZLKl=b918(71;by{(!MMP2*TLU; zGD>cG7|FZ#lIK%T7*~zq()qi_ft-6OTd-ik1)(-7J?(TKhW_F)^4($Sowf{`>(#k4 zt#{5oG5PxgsNmSnKTLX_Xs@>Hq`PR%(3}G8vsnj1_YFUg7qm~Ic|TGPSe;P%q}2tY z$pGmA>H}5~mIw8~2KxY~4iMP~+(J2E=|3|V7n<>)1Dz(JYV7Su+x{tza1S?&BjLv4 zC)T}4+}dmVyuG&HqW#^!VZ(-gB)W8fW~2Xo_ucn<=2;+nzsCyBQ;tS0VEcfmI)JkexQQ~r(tkR~i_;hvb`omiuS4b=?>2dF*QVez@)^nF z9Xoc657Ay@fYW`5r+)P0lTWrI?W<3S{@hzO_0v759JQ8mUwwWo+KbMlH}@XZ-iyaU z*Mu-HkPnQUusk3+A|BBAz>W*l4@8pzRu2q~1?uR5OOB5P>-7PSabQdxz(IdUO!z?8 zNpSU?jI5V8lLs-Pcm=&x~rvk!=-13Kw{J=_0vgsB`OUW-82iEwj&zhf`IiKJtV?{C}` zygHxJo^aoP|NUGC{ZIE8Q0@PR^hq78zQ4)zPvt(#dk>@Z^1pCDdG`C^wy(BJId1y+ zy0ka@O_ScWW45Ad^k!6!=DsvcU1PS87qkbp=x=#IeF1qX+z-T(0qg^Yhx&jzHo?cD z>i~xSjy@o&9UwFYh^7N-zXS*U6*oW7EAp3G_SFCuHuODG<`k$mtb=YqB%K;AI#+J3;$pWn7Jz~}*u^a0i$7}f!o)z<;c zIPign?LbYTzxMZxn?!x!3hs+M8R;K>hGg~yDU_j2!DD03i2k$zJk`wf|G~!}e|)9- zn#Rgwe^2J#;#rH{fy?Zz8`Ak5d+P@Kd-e4}neV2|FC*=b4PA$Q{hq z)*Cp=`M6C-eyQfJG6uV5j_# zW(T^Z?iet#{TupUO*_#<_&Z*PiqSV9b?<(paQ?>P>QqwEpX2fuNPqSH&DH*Y!1ZjC zwg0MY4z|2Hk9KYDaqOFS!>jNNp?$WF?05G2qW`<5?N^Q7ikxAuV(-A`kpA*Fa97nr zhxVH*+fTl`b^{8?1GSU`e#(KM9$@4355PaQDo7~nRJ|49Gv81RPR zemPfX?z>IdA29Ntyd+(+o(>R;{t-H0y)i&E`p;{`4%|@tchEm>B4xl;oHG~?PrFgH z6Rt%H_cw14kD>O}&@-Yx=b<+?H~pz&PE}s9$|Bt`{2Of^XP9(`g=(K zl98Kmm~;OX$pw+54__a9`ZMl3zXsK#UWdbp%w%=^+U-Z`=yvnyx;5a93$1ao(evbR6b6g&c{${Lyy#1Gs*o34plGvzP2M#eBC`r zrTwN+d^Uh{(S+^)^)rh8*_lY+@-@onpH-j!&avO|{=ah67NiY+31z#FMUuVFb>7y! zNbmnF_tAV6KDPUSu>sJYXyym)7_h$l4-9=4evbdVgP(w#baxF}4mbBT_K@yA1CP+Z zVhKY)_Hvys>9Ze+#XimdLlkk{i@6!p3d6}@jqZJ*oWA@~UaGY1r{{cHR$`d?eG z|F`rHbh(uKVGc*ux;K%QlWllFK0PPO3oS)|&i$G8uQE0YImeLk>Srh);G{owv>5uo zpL#&;n75FZupa4i-ZOjs)FHa#!t3!O<;IOD82AEQ9P{}{N%wQ;|8>THFPgca0ByiN z(!O@^<0vQHiv~Q5eC}sm&~H9U`agtaR08U$m@0k_rRV7ckf%cXE13m{m(fE6mI`hL-e0qPyX9|^8D>bqIAr3 zq;p@abjpE7@frp9+7q1fGR@xpMStbhq%!7`{(t!x3sFG3 zb!}+{$00SSNXbIh8#{6Io>e$J_!eYzxE5tSZiKtn&E$bu4b$K3n>fjg|IFTj?EfwO z16{_#)p{_pSFb~QZcfv18{z@k{Y8tGroZxS4AOJ6khyv%+tt&g3-!Nx^FFo@5Dx@x zK(hI|Xxk|qydEjjUx&BWU7zrwI{~I?^Xc17J3$TgOD}2f8SyfzX(uQf@+^u6KZW9i zRVW+03YCP4gvU`ZU@4M%&c&{Txky|69!d|TQ`gKw&XzB*Z}Jj+)!}-4eBLB{cl{DW z^H`bJ7L*`!{%T}&ycQKbX2RP`wqL=G!8zc__8+GI1oFU@r2TRFqst}mwI7C(G1HKq zl8Ox4*BZlZf9}WBwDRSV{?G0p{hxBsKbj2CI6lM!91r?v=dT*I2^rM!b2oe+ ziv;F=Rc0ZQM?OtjuSV6dwJ0098o8u-a__}B*nK|XK^!5Z_g=*BJ&eSj^YHDUIVjnm zR`0t$_9@x#e}hA-UPnz)S-syQZ;1ag?t2nB?I)wA=S<1~(muR5aDDr)*)NrI0LS-9 z9T(m&G|+i8YT6_qYvUGV)( zpSqa!@iXH9_SHrYknXO2AjAV^j%O>}yjMaSVDe>aITk37M17s>W2-+wO0P$elkgPM z2CT%MxcS&S?HOcnok;HcyOFVG2knR}@l~fg@!0P8G9{R;~V z>y^!xqfVNigUqKx^q23xY_f(2Bm<(&0b4%ULfhVE)KI6(?*BY8m+xqhTJDN!>>0lb zdwVRzSKa2|=wtLV;GUMw!&B=*#^x{Z%{|YdI={sE(Kl^{}73}Xg92MP0;@H>UA(J-NI=GC&56<63la^}#%1i!#0%sW_ zuQLt(KVbV`#d&|)e~k{%5D!QeP$wYm157>C6U*o;lrs1klpo4&kWRL4CiY{fFou=sW_o7xm{F6Y_jP zejOY~!?zRnElK|(?oDsY(_mVVi(^ld{{3iYkuQc>w;?@HJfJbUl>_VxWV;LZ2SIy| zpY*RCxdD0c+;{qp*Bhj3009*4Po=J2*^;CW=T)@hE2Oo*5|!Pj!rMdRenEuo$J+i3 z{Vxyq{lPu6M|1D!5%9Jih{B0CnfBixZbR+QJ|miNez@8Df37*58cBc62ikHd)wQ`7 zrax_L^1~V_8Nhy^jvOE#)bRl(9~e8XY&z8c$s-lRIR{K0D%^1>jPEn;L^3DsrbQ@d zuQ~py2=pNBL%tuH`#-MxvHd%B|E`xf_DmlEe}^F`i(?wCp3K?Z4E)$JPi$OA#U@3o}8ll~g}S=oOXWk87jX7BhB^cfwBs*Cy}d&8T^ zqD~(buc7wk{7p1zsrKJ=`sd-;GnD^*X zAgieGgy_%t;1-epqQ86q8ik^KWIgjf>HjDKw80sFeEDQsJ_z>*PCl?QVSPw8tmoKq z9ql?Vp(Np1B#(F;)mg=1e4lA2C2xI<+_vN3in|5@@&I~-=KbPAx?foKhsJ&(-7iT0 z;Sl}Z7xzQ{q?yQ~9iUMhhv-mJQX+qh7NNiLYErq1E&Z2~{#=7&)@ZL`>fnK}ERbFx zJ_zxGP$gU&DPrIuB#} z9|B+dfhg{hfb8#gBPT!aI6hTC>Z4C;jDfVnJopxlPUel0wGlVf*j>2;+G? zNx%E2Lu+uz16q$`WPw8-NG6!RLA+2OG=}i90LwS*@Zoj{TkyFQP*Xx3>e6;D_=eqXwThw`K}ekqa@7N9CE?~LPtl6OBrPU}&q?MgpS zBloYM-6xvO_qg7#wvUd3NdEz(e^+F$drOZxYu z53SZ$XuY-KE`<3Y3NMHs!XTaqLHM42e-eJuzcyh7a(g_0^w}?SJQz5mc)*p*Inv&o z6YO*uii!&3ZzP|S z20`>+Lm!(SjH{=$HD;aFiU>YnzhHC&%L|eXPWd1iA+YzAaz2SX51m{^G{0>t>_0?I?J=s@>bRPx{xh`2_WT$$oSk2!H$jC>8xF|MN-z25}ssfoRea z?f=B+Pdm}N4^h%%4*c}3HE|Xs2kP;Gl?mbn3*-m!MHuXTn?6RFBLCf`DCqS7Ql>lx zugiT#@j&6~jmW=<_Pj2aasBVOi21%y+Yi}%tn9b+FFCif(f>|_{+thPcKL7VFMkuu zmpZrC_S~dpr2kgN@$4}hzJZU>zkX@Z$6mhnh7TOPVDtm^4;I7|5n$Ho@b?h^1o#c# zpk=5Wun6g0??&EhpPo_luR3r9Ssg~9rt>B6)6d)Z`qXLrBdG5?+x{SceZQ}LUlg84 z`oF#z1srQxoC@FeT&eTAva(XX7%f77<>f3#%ya60IiKuCe(%}FrydE5Nq@=#%Lk@U z5HCdI2lf+IPLR%lgyrl*mNRD9GR6>DN?2mR!`OY*^w-brH5&&<%%!hb=^4WVKFXtG zW3ER8)|p@{jWiP z`J7m=WprK_mJ}gx|54-&oCA0N`Jwn~;sMJCmKQ>Dp$DUK(dl?s%b1jj$6^?b!;#z4{RaJ|%f92)OJLWlkQu2>wBWvtKsOdYG>#7!pc;I2C z><HgeP0mS_Jd=7_4{i3f%g4Sb8#HkH;zDl<}nl!hcWR>y4&AG z6Z(j>i2QFV{pFKVSW=A48IRFN{vPP{cOfBpj0#)z#H4*8Y`OW95>Mh@L5~EJw~; zpCPx~Ez|+#1Y>F{riuY4FNi0|4-SyLsA4?+%&xZ~@kaU(Hiqmo?00Wj1xklcfxG>1 zuJv}xeXH{iHugU0{GvVB_T6oJA@@)1k@Lo8;;u3(o-J+-eu-yY*VNRs2>rRnaH{C9 zJevg6U-J`Fk;$0p)qU>aI{LZf0m=arTlK+UjJ-PiAf5<ToC*DXb9_uJrO3pvAf5<?|{k~`)t?f(h4}!zJvh%Kcavcf-wR zOXBXpu_;U8^Loz^Dn~Cy^}(Ykx@jKD+75@CYx<@01;YA%WZNh0Lps0OKG^o%ZF?g3 z4{dO4)iWrqsji1x(K!~rMUyj@{uSjYIFN+2zOzu(>kjU*b6+rylHw^E5HCar$%|U@ zOi{O6QLupVKANyDGdgve@2;zWdd?%ya1G$4T*EnxW4*DYyT*Er>3qlAzMpNs>f&xl zYZr%tefv>LdwwihN4wwB^q0Spz?LQ2b)DZTE4V&lISRXQtu$k&1ozVrA6QvtbqjJZ-q9WsB!{oE_&WmJB35VEiRH4n`boVfUMmcjbH( zb4*yBm=1rS)s2khA$Ml(8>wDqih#Ek%E>F_{`kfBBx+awyfcy%$b7k6BX0H7b|SMp4%r;ME>F z+Dpd(dBMa~7Ed_&!tw`G{w|(T+$CRM?#IRVYvOJ}X{Q+|VBD4b=}S<$VFz5<1ibsitKaW!+J4b_9gsZi65^-` zW%T8U#AoES#jC})+wE?V_AmM?FH7SSNy}@gqpp7!>5P|9-HUN^!~=?aiGW$Z1%EeMeQ0rmwx#^Ukxx)HVAr=z^X zWaM|5jGU|IpzMP$;q&>LBoj;@{|)EguDuuKZO5>WzYIS5aVc)6@#71}?zkkR@5P{f z$fxi4STES;OYZxq^HsF&hU7n7Z0vm%Zr4fSRCH(|`k$lre=77h{9RRvk<{(i<{@%5ZxVlq@RHbX&&3CNu? z59J?!*Py;wn~{y8`&XibZN0WVV>8izGw8z^qJ5qIer&Ma*Rkz~^u3t0?}I?Qo~WhH zmqodsd=2@Owtn08Po`b96#ZrYQ(p2nJ6WitZKH5M_WQoT#)bwdbd zgkmsVABw}Gm@M3jhimeET$AnUJ{gr#O z%^mbMardT&=zd)=28-gbm{=T&%fWT|T$k(P+DuQkiEwoskJ@&Z!PWjU+UT z+r0EV(cbp;W1{GKjnO`+@2StPWS^f#+9yq?u1XwKQua57FN^0`&q=T0dW@Gzf5rb7 zG{^og=WzYs8^cRnq&Vb?JM;I#(y0t`;Yuh zqLn4`zVlk=&TGA2&3>S8_dy)&H3``rxHn4N6!=a2B+^nb)FQ#e)pF9=N$*fQH*GzF z<|Yoqak?XJ66rmWFo9rcAFa(_8fy0;x;y7{!oJ%>BIa|Q{rz#;*O2zv=eEPaj&Ugb zei!Q=l%r0ZM8lErWS_CA#l5}Ox^s^Io)C>f^grG0-_ZZtZ@-P#{J+4K^;FXupBH&M zzQ6(M+d1r)Yr0=U9$?!wF_a?dD;iq}weK*!qtM*!yW*rfX&x6rcc*@5#&w~7zK%VY zeSIw2ht}-`$9S=6pL1S&9JsIp^4{N$YRcLOC!F)!naVA03LJBl^~%vFL0;GCd^M)yBj0cG8?}Jxp^) zdyk~MllI|x9MN5RUZlL2O_%N7tl1e9n#-+A`vC1b>hC4YLGod5kZX|F?l!0A53S3l$)qZhF36`F6fa^Q5OHGZGrf&HB>p+0a4Dyg3-hKBSt z6Gz9;H}YI`wxB*e*smM;ZfNdo>rOc@Ss$|FIqBYLn~&`4E$!ojK3vAG+n3|}pk2SR zHT(N>+hRZ4y`}x>!k6%2BVJ(hjJz8>R| zb}{4PbN#HFeBke{Hf-tZOm*LaQ?}c7ZfUNuoTGm?w6}V_?c-(3vHg5QZGQ0ac5nQ5 zLbMN!@nq9?bBtFiJ)gF{eeCb^KG=a;(&%*IDe^lSlN~&GaAb)7>Z?!pVfvMG&pr2_ zsC(?zctd?abAT)0NAaG6*gt$4j+{S)aa9#d<4W4$q^E~y9Op>GF&0GYNP0)3c`UkH z-Cj2QrpkLud$svK^xqB6opM3I3 zXA<4$X{Vd`{GO5jOn=C^h)v*ey6O?_JGJZ!DsuCYNPnvR=MAK-cogUT$H7f`5VXx$ zT07_+9=lnZTiXrge0cq(L$?dtacrB9pnG`k)~VlHd9S`+w3l6{FXX=?|E+S$`>gXj zVE=E7M(I-SJCa3){a)o}!^DX_wdm;Wab#z|a z^OH&$L#387t(J2lnJ;X_p01;D^!!1z-Ht&O*VB492N#@&6OEnV=+ndfx`XC*Y&bFK zZq{U~pSSb&4tZb4ccUNa-iNl_UZ|oiKc8*>DD66X+I2I(!)tOJpCT*JwG0) zTu)KhaWty9uGYhKwf?XVhUn~sP`@6gd1U_{t-V{iOSg}tdr-gE8m%DR>9g0S2MX!8 zm-@%H*!#N+aroNnQF`#ODRV8$-mJI@wLP^(`h#ijR~^lv`vt!o>;ukOw{G2U$cshN zIc)u86SC*g(#YSP*E)AzM|RW8bK2vFE(rIC>uACAS@n;ts=6*>MD1oknsFXB5|51%2<2 z``=rd(-%8zx0Q^yWBKFlT``yxhsWQZ>#<397v+2<<$Q5##$~&Za{hO%vFCReVc)Q^ z$a-Z1%T!^?=3;-AntNMQFXoBWE41dwr^h)uabdPBDwfB|U^JvdC z&SH(2DmPp_8VUZ!60}^a}Vk5V*HhwcKt~2K8(ZI3k9Tk)&-oe`$IeI`^|;e z-KHxJO}PPi@4RQqpt7h;CsP-_6Is8ee8)b+tThMs7ZO}_7j2rG{)We%d+xdaOTIfQ z-NVwj#Z%(k%ldm*hVmq?F?;@H9GG|`_v}l+o{KG zta!li0`=00jALw@+mW(x1rAP{i9P+tFg`#(>|#vCJ%8v#J*@-w5%&MKJ%MeSu=jUu zvFG>guEmw0sC*0Ep@J^pTePr)% ziMDU)Z)CuxO`Cqk`W;X?MAtJ0$O}G}Ng%$cr9XN_YC4L)`5t-OK0x;RH@Gk1OUQV7 zje*SP|H6B(lV{#XG521oNJ%sA=v{q7H23n{^6r^XPxbp8gSojUSdS1QO?0K|n`Q* zOb>nj`RD&e8W>q@iPFNU$(wsZ{ha0bTIGs{le71N?c#LcL1Qk8+j7d?xDc&dTJA@( zn8}~9hyKX^C|Nup+h8n+#DSh`=$`XC_Bs2GO`Z3h_hP*pc|Y>n`90^keXgN<`YrOg zX!qaMObAd=%D=O`*>6vaYB zMG+O1AS$3F#ekxMIV&pWQo)2G2qN+1oO2F4jQ5YZ*Vt$5dDhxzpK|~?hYjEOW>|Br zeb!v_ALAcm&NUa_7y1Qs!Cc(SLAftZSv7EfaM~W}$hqzLy<>fj)i=eszjat=^LzD# zJIk|Ie{%D9<#B*m;rZytHz9@ke%N;Gz2J#i~eiL;4sj3q@ z<8xPkzR5=11NJQaSJ2=#x-@Lw8OW$>aVw?8wi#0J0zQKKfua zaB&gl@ct7$Wn!vM=SB3H-JxBn%<~rt?}2y^#Css#1Mwb+_dvV{;yn=Wfp`zZdm!EeCr%HX2vcj) z8_wU7A?5!bs#q%JQ%pG+E}oLW6%EbhQ@LExTs9Zj-dz4$KEAZM{5M=~E}p_ape}#P zDNC$!0v6-w_ICNyh(G6)Q&3ia&MBw-s+BVS;0%5Jww7^&+x7X!yNnKv`2Bb}-XD5A z#>YeS@nGZC?l1fJKUjEct!$r{Ekcb_jjlK2i`1ViT{iDK)eUy zJrM7Kcn`#TAl?J<9*Flqya(bv5buF_55#*Q-UIO-i1$Fe2jV>t?}2y^#Css#1Mwb+ z_dvV{;yn=Wfp`zZdm!Ee@g9ixK)eUyJrM7Kcn`#TAl?J<9*Flqya(bv5buF_55#*Q z-UIO-i1$Fe2jV>t?}2y^#Css#1Mwb+_dvV{;yuubdw}1jj!Wn2gHAl$;(z*a>Va4= z&EM~cOXuu^Sf1~38fx*M`3ZZ#YaH0P{#*QwTK*2@SmggkySNdPEC(n|ft>|ff-l8LT9I$TSaltyl)(653#HR3A(_=5r2hbtM@t^K{ zH{7!hXUwyW7yHi7m+b*RbHTabo5u`~AI9*%LH?H`{~uD8-zxdv$ZO$my2s_G?F0Qh zIIo|VJVSn_{A^kHIrH=8=gvBU#{rKC(Fv>%gd>P9aN`AWJ%Elnj{ldV^FRWPn-MnX!V8eXYTjbo)JGk zc}~%uvDOJ%AMp5L+rhk%b%FQ@+CPZv0d&kU{%2!fc)rFx+j_=5>uvz`mj9EPn zcr0*PFwOaY|HB{t@aOZ+JFoZR#fyipS+i!^v(G+z`RlL0e#5)(zI(?pN71$YuBzVkoO=TegFOUf8D)%_h*o8t;wD}d$x(VwbjRTp4)O+pJSKZ z+IZEuT}OUTQu|z8?{>`Xn!3&{FZWAA-FI8R{r1~8ATL6m z`r?Z(HhlKkXKO$F@WW+Ww{E=!&u#X`jT_h0||*Z+y<%({r5 zJ7b>pgVx`i=Q=*dwnB6P^F|(5ZhFAWFF^D7zYg|*jeFL$9G7AJ%bZL&zVLg-z1Vrd zyx0t}<)@AuIr1!wi}72wY?;er2IJ}Vef#$9IB?)V@}WbAN{$>kQU?hpCnr;CYAU6t zr$vlS?+sN;> zR>$YNUBj(Tt|k4>&%y2a@bRRiB*Js_9X@=x`ryHXdHeAXy5v*V-yeVc@m;LnZ@&5F zA;0_G@BRzphHU`jo>Lw(wk}`}$>U4>1K|T~gLpi$9*A=Rbiz^kXJcP%zWDcI@0p{q z|0cFxbUHsjqXQ;Qn$-Qd=bpP5HexMsz8&Ks2jiki#tx@E)=)>lKS2)=hDum|{P9O> zO>kY)hSO+yeN3*Y&*3_0qw~5>&eN$}LsB{BR+j7PV|HH7w^ME>b#jib)7Q4!*~jF3 zyWTGA>+5sm`d%qNLw=UPKH-^DVq&7v&7v=yXU@9e2>8VtAAIn^LfD0|4o?t%Ai99Z z5_1Br2ZRqud_d+5Y(0R};{Rgo0WaJO*Jr)UJl_rX{H%r7Wqg zmupEXZ6uX?Nu}N^jn+;d&Y>l>f?6a?K-RMwu!%RgXDu`XbHA~r?z0Q&_JA7C!Zdccbd;86TuD?Q+beeO@jzUKPe@67Yrw`aUF z?pZ(o@Ad1~kB4u#i80U5l!fum&)Oan@p9|D=49$4(Fg1|u&qGqhkp3}lTSWbbm^s+ z_Qd!S|3K>j=ALYe*d}S8fVlwk0L=wZjQ{iK0Wa)}pRa8`>tDvZJcm;^Z{BwLSuwX)mq*yp&l(RFrvud-dQufh4&j@#{HDcjfdD%0tQwuLAcbj>ovy&y(}E$g`AsU6$wWc8zGqxrb#e~Zi%47hm{&QLdTC>)`tQ+sZl}ww{cUzzx8KoirkF+VV%mlm2LjN{r0!N z{r$FW+m?dom+*7oc)dKkc-m6u#rx#g^a;ldo$r&=ejQxLEv0}wA*!j=7-^IU=kMCnMzT^GcdcSgfz*&b-huFZn z(@s0>4;ZJc2iPBAzA7<+xDG&PsDC#0*+&xWv)$)-Dq~;z^55Qj@4f56^-DOmCv$i9 z81<^R+qspaoh!#UmAt-&U1yhdTb+;AP9M{GeLm;qn516ErQWNZ9G8^adA0Sb*XMFx z+DR(MC3UNJtCRLR^=hZ+vZJU>oF_!&sO{hXzZPNj|8c~YmZr_Xh(x36cn zm$I~TyN0xpV^WrLyi#c=skHMdOTC@y^SttW4j+?qrJYWtjczCH?NrW_db{jaFKu+H z+i{+cNu89X&OT4-b$k09UAEiH^|*~QXPEcWOD|oDF&#|@NK8<0!0E~IPwv^qKF9im z^RwOm&E(0G|BTrFd#qb!U7r1%930Ev(%1eeemBLD2yx)A}I6Ki@NR zAjZDT@3Wu8*#DQ=vuAgQz5kx$d_1q$o-x|q^V;)UZ9V6w_}gOr%~;#D^`4`(kM%cW zZ5Qpg+H~^{=ZREUYX=`oex^C^{7++tG>pJ;eyUs34J3EbbP0<110V|yG&0K(E zTkPjD=hgFqf)7-8$$xx*w$CrIe&$BM>EFNq@8R!%Dx6>Ut^GXgV^Y@7N9rV%V_epG zIW8$5lf0yEb#jbTxn8X8o4j&iM*ONLqU&^|T&PzM4lf0z5ozA-* z*L9qivZQjpKGxcA>T~V$Q?Y-*dI&N*{n0>C@R(t`xZ_M!kUKhYRfc;!v*Cz9V z%!Ru$2N2z6^L~l<{pM#s``N!@e~0I@va(uWPxo;=AIXQ}{c+;=2hUq!ZF=3yFTZ>- zaL)Fhxh`{F=DrdO=&F8z#=aNt|0nFNv&dOb7018j^TaWJ;$ytEXT)>(fs$`e+Me^HouiNG ze0%=3_M8*zZ^qiLJ%5AOIX}g3^4}8c@5b8B>;7V`XZr^K;OoA9`~D8&o;iT_19(25 zt8oC0eQo>M_u+g0f3Z);f5AsOB=7F&zU1ekpN*ZDRJC?De~H4Sk-RtB*-uAG6PqWBRx3^W>PEE2%zL*XerBvww@V!fT!J&pLqT1nv0% z!3_>|>VI+{%xm--KfY(S{lA9KzcD*IJB%mkZ|P?#d!@Ra&fD$nV|JO_yVcp(ack#w zo>#qn%~;yS(x$C{Tddc#|6aRnU&k)%_WGF4>o%N^c1+sF(ni`!8toi?Oy}iXPPyLe znBC5;%p4c<>eER99{}z4l9rwlY9OpbI z8_Vy-+RpP@hcR~fw^)vZ6|3$O?|M=;rpU&fXe!CC3tm`B%sr{U!o>R$78(nrguG`sd`IxSgwh_1x zY#Yo0J1M|X3gZ!&0*($Jz%}+Apv2ujQqng+P|}xsD0$m%O8MkFO8sajrF{T-?;A>c z_Zv!Q*}5Y_(zbrB%3o8c%yl72d*>^Y=Tsz}>%#IS@?YYZ$QQ_mB%RZ(kauzH{jVtX zuRADt`%X&Q@jV^+{s%hz;{l_)jGjnMqJu2Z8Hb=FBKp9>bhLh!K9x3h|4O}XCwb|A zX`|1Tyrk0BPUT#?UdsBoeJ%T#K5plumF2g1J^*`IA3_ z@kweL{{;EQZzSKi$H_OA<*|^A*+71XZ_J}eA4U3zBM*~*^h4x>_(!itx*q9+ z`Hie2|H!o{Lq@KFtVVe?(p5}R%9 z@v?WRYWAyCKm9r2c{8y36a_{-MZwXKF`Fnj7BUVpesf4BAT@%z2_Ye*A$bbvlSYCQ zoe+t`v{j5&7JjWzq=Xorre!83TANbhdy>ZGjz&h|d26!I# zG_d>(1t&gB!AZ{&MU`i8T;yrw9f9tkiO>T^pa&+hgmeKx7ldRZQkKUdj}bu^(D)6I zM~R>d2)cmAJ?zLsNY@iV4}|0ar0XDSiN>(3fvhH@14cs!7=a!by%Mql`haCQYRQQ01_C6mz3h^-rx9+(I{V03_s9&k88vkqYVLk#|v4j9k)7ahR(hky&T&;eom+d6>p z@8|%H|51#8mXHphk#`}#lxV~f2=u^kaD(B{1w(JAz~EacFyLnL4O&2T6Bkq24G&TF zQ*R^gvHK_01+hN+SlfAxF|T@kU7eTqyk7w9VLsk>KMnXW7(v>ZC#P)}||EZ^* z`mf+UJ2~br_s=Q!i|!lAOTAsz=j!uYYv*;ITqA`0LzJ^=J5|km8GiR>3XFZ40u!Hu zeSh8vO@0A*k40>~z&eBVfz|~v=z$0wzS zz~}&tf9QY_%{qWaEFyvqpy79b8{7_A2!3!21qaM0fBze)e(XZ%g7uX3+`E*xXCEC( zOELODo~`t~es=bAvfD^mQaM+bb)8$i{(U}fi=409Nu6#hb#CpX&Tb=R`Ma0{;Cn;2 z0{84A+i^gd3&f#L@lR|&`!5{FV1I_=8o$DP|Ae%(G(YrRxW9ED>Adu@uJg)EKYAUP zdar99c6ff~%U@B|m9KggJrM1*`|a1B@^a5H1kS zA8_FTh68x<0Kq?e0j&c#7GV1U5gY)1fYt%<1Df#<9smvyl8_Fd;kOg94j6VDc)=|c zNLT<~Fpqo#ZlKyJcT(Z9r!d#>6&;3ua5y!Ej=;Y_wkiEoxb;< zG|zFWv?NOXd=Hh~yoH*^B8~@+AHw|$6bxe-SfBDzNG2m?;j&JpUY`@r+qxiDE}-H9 zh6h;j0K^5|eE~NP!0`a+0K^0%@bAU}4E|R%#{o3{*#|K8UvU7V0|ftf0RJH!K*Me| zIv_Y?0XV_Uh#}rcfqvIfI!4cSmpL8rZ9Q3F78`vHq9_GV0 zYR3RC##SPg^?=NyI}-R>rCUg-%ruUEq)Vo*+(#km9udh z)lGX2F+K2nw)f!n2KRz@lfDeRw?%CIAY4K80H?wWmgZu0=BKH>if{+auW@Ba&Yinmv34i>&t~ z&uM$h_IYwmsh3oK<1pfTNqY`Z$-=j(aWriHgy$$AxHmT4#`-J3f2?Ak%VmcvwBQ4x z2W&pz_yevwK+g?uEI`Ktcuqj(1zPh1WG=vo1&*=tzY_L88V3*_5Q+urIDqj1j32=G zcXR-J0oDOD^k#5@n<&USAYmSSgSq7EcMVltxCHa6&;wyU5b_E1vy*y*Vxr~e-#*RMW*s` zeGcdK^`hwk%>^Vr$T|SxrUN2kf*SviS@_pFK;{El;{f+H&jpzIz~!O2z_1T+H@Ltu zh!_6F4-g-qCH`sXd?H5&1X%|p+<;i&9O!}B-~-Dj^L6+IDal3>ql_1M&XV%;m%OC9 zPR55o4j?*!hTI4)a0BFe%pqO}zhE}}g4t9$cNL{>+d)S%(%`2h{iNgOc#TuGkC+Fz z37BErF#dH6fU$L=|I_$q-=AZDzry;aZqRMzyyp3M^`-qgN77TUR(>Z{PkRU&`Ic~Z@bdBf% z9UEvx2ZVhA!9R3BI3DQu0payQq5~NJN(Y431vwnRtPA8AfL9zqVgQaGU~_Oh?k8 z148R%Vj4&G_~5eF7_r;N+Ri?&BV~>O;CBxV7%+fi0D^y>L+dOt0LH(>{w4PJvt7G( zEy>NzZSl}!|wYLgNx+*Hug1!71qyrlPIdF zwqYHiuf@8+`7Of>SPwWlAY$&Y6&)~%*9NeN4zS~aGAF2Ff#G#Qyf#qc0M2|MuMO5d zKtvp{H4cCnfbju%E^x6UvEYA!i32w400;lz00cikbU<*>9O!{-$lv!WY8Wt!3YTsG z7dVJ#+RO#y`P*ZlwX%#6jaT~|IVQiM%dwmv?Rs)8eI4i9LDj2Qujcmwm;*3>?u__n z&ceP6&t?7sYyMtlZX^8|Ybt%yT6?c{iHIR3?>$K6Ghf1cD_Fy8>^}S6j5{yfYa9df zQ#J0lKnHk<(~jF4(bsX&1GWw@a|Ajbpymd`YX&(U0AFCD;NQ^!5gfqb0s0*QdwsAc z2UrbVz_KcKAHa?SMyv&3{x2~A_yI$2g{T++<^nkm7-|2nH~6Q7xkQ7n#k)syAkYB= zuO?rw%b~{?Qu>G6DKXR7N@G)68y|e!YmCHlZhPCr`kT5P^W+ac_~1O?hPglUXM0Z& zRGqN@#QulZ{r%=QzxikM-)`am?d{7ppNI8jNsz+Z-lB$4o5|;3KRDUJzQI4+WWlE3 z)xoi`^)}Yu0{*?kZtGkpLKnzyIb0yD1B45B_yXZ|!%jS~g&zH~lW7#)ChK+amgW*q?hyZQi{|KAYi00ciEqyw&j4wwxt@E61d zuAusat10)97by{INs>bE(YM*Qw0S%^Jo8we$2=MD1KkFM3xDRcv#bMT{OfhUzr6Cw zEB}DFUoPhUkLxo^%t)o|C$>>7-p6gi9A5w&JA{2;&G2x+zJq(ld)U@%ENi^K4g6a& zP0G3*=k+z5Ye)D6{QHnrb%351kU0V20Kx<8H~=_6i&&t<0NmCDS@<{he^rbeK>Gko z+Ta5?F@T|L|D$sN8a#&x93Tk%2fzdT{ja3P-ZQB9mbH|8;2Uarsh z|MABkHv&h@pF7hWpv3<4+~3b1fBf-2zm@J8B(>{Z%UDO2`UQOc zsZUcQ*71jYew*`)Ee8I>e!XB;uq@biQo;JPcZiIjUQ*Tzay)_~$TgJ?IEQr(|32#g z_#v)3fPDcw7QpzobpZPSkO{U9&~boAp${OM1K2)*i2oEZF0Sx{JUjr^Mn}Wc9aNsQX16NT~&r7M|qT49#i*JoT(55k>?74j&k14K`u@xH5FXu_wSs(u)u+P4;;9t)Hp!$UTXXBq^7yt0iJMT>9xTf5nq|zT!=2ZGw z*Xg_*cRN?wCGmCO_ZQB8gX%|v_fO*Wys-UdZH{Ae4fY(n-@@iA>@%LjSQpH5y44ZM z>tmdE^hFCjpmc!Y0*(%tqIrN56AZ@#9321-)4Wbt;sC+}SO>V_-_ijs@V_41|G^Od zkI4t%IRKV>5cgwo)&j+hf0wmDb__tp03-1a4!}A996)qHVBnRA30z7wW9CriJD*Te zRz?JFWlXrGGM4OPdTdD>T_^3m>fGw(98Tq$x=!cWPe%EN-~ayiye}x@zq70X(D;|R z|G&rnMpyH?27QmmGM|*0Mj5YsP1U2Hq{fL3?>E@zcpu~6%kFF3+jthd>-1gVoJH43 z-Z?+)E6BAq7ZCm9#seH3VCDsc2LS&j4j}Iehz_uQ0KF!_!hd)kAXXpXerFxPeMnaV z_c9lVIDp2#9RuXK0M9s}6$5nE0)}INE$~0r!~nt^0C51;0c3cb%SS8 z_OowNQf5d89Lq6wEU(SB5?ncF=FFM@r<3E~#4fSF|DwFSyyJs^j`tr*q~e(`;=S{y z5%c4@KE{$@U+~Y^gDnon_ynKY-V2sF6|5sY*U6vz9_m?CUAUc`>!t_Vpaa7Aw{?K> z0V3uDTsVNe9zb(|h&bT7$QU5U0Aij8aESr58~^hV|GUA70RsEZJU|Ql54svUU>5cn zxDxLW%p_mm%c#EZC6u@RSxU+P--N$;EOC0QuFdbqL8=~k|G%v< z0QLcZ|7boy_!Ra-&@AYHtDpmBLI+%d_XjSchF%v^ z{;DSt6NC-`mh3U&R(2apZtad{o!4)2J>q4)S6_XV-vi(|Ko-3Q;6%p1to@Pqet(92 z5T56lX0*QJdv@y&yWVai$4!d8BvN4ei>`QqYRB??A8dbkoYA(Qfz~%vtAJ8Hez;l9Z|D$jKTL(m}2Y?S?_6+3pKpY2r1aZKJ z9eK#{{{{ap93bjiU=;&s;R7u4UI(bx07R?>ys>rsBL-;rKihx90j`1$xYE%9my@4$ zK#vQk;NA_CjCp}%j0+xnGFD_9wpP~XxV4q*NXl&_FR9ykx}D^8JI-?q;Jx?Wn+%|{ z@822mFLRf_z}kj)+5fliQOY{yev`7!OC6_feQWBo;P=1uB~`-qH(Gl$^8jmH<^W=d0fgTNg8%P12jCI|RZ4mh6*?s=4w*%x5j;MkaGsn?j(=h)-V zJ|<=RTDq+B(q5;G7wnOJDX`Bt>a6%@|NkFxUtjQ?hQ7aJlE*X7zy4LK!9JZ$i1`O1 z{CJ$Lb^b% vW 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⋘ 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 literal 0 HcmV?d00001 diff --git a/apps/finicky/src/versioninfo.json b/apps/finicky/src/versioninfo.json new file mode 100644 index 00000000..f1d991ab --- /dev/null +++ b/apps/finicky/src/versioninfo.json @@ -0,0 +1,25 @@ +{ + "FixedFileInfo": { + "FileVersion": { "Major": 4, "Minor": 2, "Patch": 2, "Build": 0 }, + "ProductVersion": { "Major": 4, "Minor": 2, "Patch": 2, "Build": 0 }, + "FileFlagsMask": "3f", + "FileFlags": "00", + "FileOS": "040004", + "FileType": "01", + "FileSubType": "00" + }, + "StringFileInfo": { + "CompanyName": "Finicky", + "FileDescription": "Rule-based browser router", + "FileVersion": "4.2.2", + "InternalName": "Finicky.exe", + "LegalCopyright": "MIT — John Sterling and contributors", + "OriginalFilename": "Finicky.exe", + "ProductName": "Finicky", + "ProductVersion": "4.2.2" + }, + "VarFileInfo": { + "Translation": { "LangID": "0409", "CharsetID": "04B0" } + }, + "IconPath": "../assets/Resources/finicky.ico" +} diff --git a/packages/finicky-ui/src/pages/About.svelte b/packages/finicky-ui/src/pages/About.svelte index 5149aea0..111c7575 100644 --- a/packages/finicky-ui/src/pages/About.svelte +++ b/packages/finicky-ui/src/pages/About.svelte @@ -19,8 +19,8 @@

- Finicky is a macOS application that lets you set up rules to decide which - browser to open for every link. + Finicky lets you set up rules to decide which browser to open for every + link. Available on macOS and Windows.

Date: Fri, 3 Jul 2026 20:22:14 -0700 Subject: [PATCH 05/15] fix(windows): repair UI<->Go bridge, named-pipe IPC, and version defaults error Three on-box smoke-test findings, all verified on native Windows. Note: the version bug is the Mac session's F7; the IPC bug is renumbered F8 here to avoid colliding with it. F3 (bridge): App.svelte overwrote window.finicky.sendMessage with the macOS-only window.webkit.messageHandlers path, making every UI->Go request a silent no-op under WebView2 (getInstalledBrowsers, getRules, saveRules, testUrl all dropped). Route sendMessage to whichever bridge exists: webkit on macOS, window.__finicky_send on Windows. This also unblocks F2 (test URL) and the save path of F4. Browser detection itself was never broken (returns Chrome/IE/Edge) - the request just never reached Go, which corrects the earlier "detect_windows.go returns empty" read in F7's reference note. F8 (IPC): single-instance/keepRunning used net.Listen("unix", ...), which fails intermittently with WSAEINVAL on Windows and left the URL listener dead. Switch to a Windows named pipe via go-winio (pipe FinickyBrowserRouter), matching the Global mutex scope. Two launches now share one process; the second's URL arrives over the pipe. F7 (version, Windows half): GetCurrentVersion shelled out to macOS "defaults read Info.plist" on every startup (3 ERROR lines, empty version). Split into version_darwin.go (unchanged) and version_windows.go, which reads the embedded PE version resource via x/sys/windows. Eliminates the errors; now reports 4.2.2. The Mac-side work to inject the correct (non-stale) version via ldflags remains open. Tests made platform-agnostic (assert browser.DefaultBrowserName). All go test / go vet green. macOS paths unchanged. New direct dep: github.com/Microsoft/go-winio v0.6.2 (pending Mac-session ratify before promotion to windows-support / PR #542). Co-Authored-By: Claude Fable 5 --- apps/finicky/src/go.mod | 3 +- apps/finicky/src/go.sum | 10 +--- apps/finicky/src/main_windows.go | 54 +++++++++------------ apps/finicky/src/resolver/resolver_test.go | 5 +- apps/finicky/src/rules/rules_test.go | 6 +-- apps/finicky/src/version/version.go | 35 ++----------- apps/finicky/src/version/version_darwin.go | 44 +++++++++++++++++ apps/finicky/src/version/version_windows.go | 49 +++++++++++++++++++ packages/finicky-ui/src/App.svelte | 15 ++++-- packages/finicky-ui/src/types.ts | 2 + 10 files changed, 143 insertions(+), 80 deletions(-) create mode 100644 apps/finicky/src/version/version_darwin.go create mode 100644 apps/finicky/src/version/version_windows.go diff --git a/apps/finicky/src/go.mod b/apps/finicky/src/go.mod index c2a151b0..cec3150f 100644 --- a/apps/finicky/src/go.mod +++ b/apps/finicky/src/go.mod @@ -5,9 +5,11 @@ go 1.24.0 require ( al.essio.dev/pkg/shellescape v1.6.0 github.com/Masterminds/semver v1.5.0 + github.com/Microsoft/go-winio v0.6.2 github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 github.com/evanw/esbuild v0.24.2 github.com/fsnotify/fsnotify v1.8.0 + github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 github.com/jvatic/goja-babel v0.0.0-20250308121736-c08d87dbdc10 ) @@ -15,7 +17,6 @@ require ( github.com/dlclark/regexp2 v1.11.5 // indirect github.com/go-sourcemap/sourcemap v2.1.4+incompatible // indirect github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d // indirect - github.com/jchv/go-webview2 v0.0.0-20260205173254-56598839c808 // indirect github.com/jchv/go-winloader v0.0.0-20250406163304-c1995be93bd1 // indirect golang.org/x/sys v0.32.0 // indirect golang.org/x/text v0.30.0 // indirect diff --git a/apps/finicky/src/go.sum b/apps/finicky/src/go.sum index a7b30001..6428cf21 100644 --- a/apps/finicky/src/go.sum +++ b/apps/finicky/src/go.sum @@ -4,10 +4,10 @@ github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3Q github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/dlclark/regexp2 v1.11.5 h1:Q/sSnsKerHeCkc/jSTNq1oCm7KiVgUMZRDUoRu0JQZQ= github.com/dlclark/regexp2 v1.11.5/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= -github.com/dop251/goja v0.0.0-20250307175808-203961f822d6 h1:G73yPVwEaihFs6WYKFFfSstwNY2vENyECvRnR0tye0g= -github.com/dop251/goja v0.0.0-20250307175808-203961f822d6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6 h1:6dE1TmjqkY6tehR4A67gDNhvDtuZ54ocu7ab4K9o540= github.com/dop251/goja v0.0.0-20251008123653-cf18d89f3cf6/go.mod h1:MxLav0peU43GgvwVgNbLAj1s/bSGboKkhuULvq/7hx4= github.com/evanw/esbuild v0.24.2 h1:PQExybVBrjHjN6/JJiShRGIXh1hWVm6NepVnhZhrt0A= @@ -16,8 +16,6 @@ github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/ github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/go-sourcemap/sourcemap v2.1.4+incompatible h1:a+iTbH5auLKxaNwQFg0B+TCYl6lbukKPc7b5x0n1s6Q= github.com/go-sourcemap/sourcemap v2.1.4+incompatible/go.mod h1:F8jJfvm2KbVjc5NqelyYJmf/v5J0dwNLS2mL4sNA1Jg= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7 h1:+J3r2e8+RsmN3vKfo75g0YSY61ms37qzPglu4p0sGro= -github.com/google/pprof v0.0.0-20250302191652-9094ed2288e7/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d h1:KJIErDwbSHjnp/SGzE5ed8Aol7JsKiI5X7yWKAtzhM0= github.com/google/pprof v0.0.0-20251007162407-5df77e3f7d1d/go.mod h1:I6V7YzU0XDpsHqbsyrghnFZLO1gwK6NPTNvmetQIk9U= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -39,12 +37,8 @@ github.com/stvp/assert v0.0.0-20170616060220-4bc16443988b/go.mod h1:CC7OXV9IjEZR golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210218145245-beda7e5e158e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= diff --git a/apps/finicky/src/main_windows.go b/apps/finicky/src/main_windows.go index bbc6d949..09dc6f93 100644 --- a/apps/finicky/src/main_windows.go +++ b/apps/finicky/src/main_windows.go @@ -16,15 +16,14 @@ import ( "flag" "fmt" "log/slog" - "net" "os" "runtime" "strings" - "sync" "syscall" "time" "unsafe" + "github.com/Microsoft/go-winio" "github.com/dop251/goja" ) @@ -540,23 +539,24 @@ func setupVM(cfw *config.ConfigFileWatcher, namespace string) (*config.VM, error return newVM, nil } -// listenForURLs starts a local socket server. When a second finicky.exe is +// listenForURLs starts a local named-pipe server. When a second finicky.exe is // launched with a URL (or --window), it connects here and hands off the request // instead of starting a duplicate router. +// +// Windows named pipes (not AF_UNIX sockets) are used deliberately: Go's +// net.Listen("unix", …) on Windows fails intermittently with WSAEINVAL after +// socket create/remove cycles, which silently disabled single-instance routing. +// Named pipes have no on-disk artifact, so there is no stale-file cleanup and no +// such flakiness. func listenForURLs() { - listener, err := net.Listen("unix", pipeAddr()) + listener, err := winio.ListenPipe(pipeName(), nil) if err != nil { - // Stale socket from a previous crash — remove and retry once. - os.Remove(pipeAddr()) - listener, err = net.Listen("unix", pipeAddr()) - if err != nil { - slog.Error("Failed to start URL listener", "error", err) - return - } + slog.Error("Failed to start URL listener", "error", err) + return } defer listener.Close() - slog.Debug("IPC listener started", "address", pipeAddr()) + slog.Debug("IPC listener started", "address", pipeName()) for { conn, err := listener.Accept() if err != nil { @@ -615,14 +615,15 @@ func handOffToPrimary(urlFromArgs string) { } // sendToPrimary hands a payload (a URL, or the show-window sentinel) to the -// running instance over the local socket. The primary creates its -// single-instance mutex before it begins listening, so a request from a -// near-simultaneous launch can briefly beat the socket into existence; retry -// for ~1s to cover that startup window. +// running instance over the named pipe. The primary creates its single-instance +// mutex before it begins listening, so a request from a near-simultaneous launch +// can briefly beat the pipe into existence; retry for ~1s to cover that startup +// window. func sendToPrimary(payload string) error { + dialTimeout := 100 * time.Millisecond var lastErr error for i := 0; i < 20; i++ { - conn, err := net.Dial("unix", pipeAddr()) + conn, err := winio.DialPipe(pipeName(), &dialTimeout) if err != nil { lastErr = err time.Sleep(50 * time.Millisecond) @@ -636,18 +637,9 @@ func sendToPrimary(payload string) error { return lastErr } -var pipeOnce sync.Once -var pipeAddress string - -func pipeAddr() string { - pipeOnce.Do(func() { - cacheDir, err := os.UserCacheDir() - if err != nil { - pipeAddress = os.TempDir() + `\finicky.sock` - return - } - os.MkdirAll(cacheDir+`\Finicky`, 0755) - pipeAddress = cacheDir + `\Finicky\finicky.sock` - }) - return pipeAddress +// pipeName is the machine-wide named pipe used for single-instance IPC. Its +// scope matches the Global\ mutex above so the pipe and the mutex agree on what +// "already running" means. +func pipeName() string { + return `\\.\pipe\FinickyBrowserRouter` } diff --git a/apps/finicky/src/resolver/resolver_test.go b/apps/finicky/src/resolver/resolver_test.go index 5604f671..cfe3dd25 100644 --- a/apps/finicky/src/resolver/resolver_test.go +++ b/apps/finicky/src/resolver/resolver_test.go @@ -4,6 +4,7 @@ import ( "os" "testing" + "finicky/browser" "finicky/config" . "finicky/resolver" "finicky/rules" @@ -55,8 +56,8 @@ func TestResolveURL_NoConfig(t *testing.T) { if err != nil { t.Fatal(err) } - if result.Name != "com.apple.Safari" { - t.Errorf("got %q, want %q", result.Name, "com.apple.Safari") + if result.Name != browser.DefaultBrowserName { + t.Errorf("got %q, want %q", result.Name, browser.DefaultBrowserName) } } diff --git a/apps/finicky/src/rules/rules_test.go b/apps/finicky/src/rules/rules_test.go index 0b81fc57..72ddd2a5 100644 --- a/apps/finicky/src/rules/rules_test.go +++ b/apps/finicky/src/rules/rules_test.go @@ -7,6 +7,7 @@ import ( "strings" "testing" + "finicky/browser" . "finicky/rules" ) @@ -92,12 +93,11 @@ func TestToJSConfigScript_DefaultBrowserFallback(t *testing.T) { if err != nil { t.Fatal(err) } - // Should fall back to com.apple.Safari when no defaultBrowser is set + // Should fall back to the platform default browser when no defaultBrowser is set if script == "" { t.Error("expected non-empty script") } - // com.apple.Safari should appear as the default - if !contains(script, "com.apple.Safari") { + if !contains(script, browser.DefaultBrowserName) { t.Errorf("expected fallback default browser in script, got: %s", script) } } diff --git a/apps/finicky/src/version/version.go b/apps/finicky/src/version/version.go index 2c00c484..84da59fa 100644 --- a/apps/finicky/src/version/version.go +++ b/apps/finicky/src/version/version.go @@ -7,7 +7,6 @@ import ( "log/slog" "net/http" "os" - "os/exec" "path/filepath" "strings" "time" @@ -96,36 +95,10 @@ func setLastUpdateCheck(info UpdateCheckInfo) { } } -func GetCurrentVersion() string { - // Get the bundle path - bundlePath := os.Getenv("BUNDLE_PATH") - if bundlePath == "" { - execPath, err := os.Executable() - if err != nil { - slog.Error("Error getting executable path", "error", err) - return "" - } - - bundlePath = filepath.Join(filepath.Dir(execPath), "..", "Info.plist") - } - - // Read and parse Info.plist - cmd := exec.Command("defaults", "read", bundlePath, "CFBundleVersion") - output, err := cmd.Output() - if err != nil { - slog.Error("Error reading version from Info.plist", "error", err) - return "" - } - - version := strings.TrimSpace(string(output)) - - if version == "" { - slog.Error("Could not determine current version") - return "dev" - } - - return version -} +// GetCurrentVersion returns the running build's version. Its implementation is +// platform-specific (version_darwin.go reads Info.plist; version_windows.go +// reads the PE version resource) because the two platforms package version +// metadata differently. func checkForUpdates() (releaseInfo *ReleaseInfo) { currentVersion := GetCurrentVersion() diff --git a/apps/finicky/src/version/version_darwin.go b/apps/finicky/src/version/version_darwin.go new file mode 100644 index 00000000..f102506f --- /dev/null +++ b/apps/finicky/src/version/version_darwin.go @@ -0,0 +1,44 @@ +//go:build darwin + +package version + +import ( + "log/slog" + "os" + "os/exec" + "path/filepath" + "strings" +) + +// GetCurrentVersion reads CFBundleVersion from the app bundle's Info.plist via +// the macOS `defaults` tool. +func GetCurrentVersion() string { + // Get the bundle path + bundlePath := os.Getenv("BUNDLE_PATH") + if bundlePath == "" { + execPath, err := os.Executable() + if err != nil { + slog.Error("Error getting executable path", "error", err) + return "" + } + + bundlePath = filepath.Join(filepath.Dir(execPath), "..", "Info.plist") + } + + // Read and parse Info.plist + cmd := exec.Command("defaults", "read", bundlePath, "CFBundleVersion") + output, err := cmd.Output() + if err != nil { + slog.Error("Error reading version from Info.plist", "error", err) + return "" + } + + version := strings.TrimSpace(string(output)) + + if version == "" { + slog.Error("Could not determine current version") + return "dev" + } + + return version +} diff --git a/apps/finicky/src/version/version_windows.go b/apps/finicky/src/version/version_windows.go new file mode 100644 index 00000000..1bcddc31 --- /dev/null +++ b/apps/finicky/src/version/version_windows.go @@ -0,0 +1,49 @@ +//go:build windows + +package version + +import ( + "fmt" + "log/slog" + "os" + "unsafe" + + "golang.org/x/sys/windows" +) + +// GetCurrentVersion reads the product version from the executable's embedded PE +// version resource (see versioninfo.json / resource_windows_amd64.syso). Windows +// has no `defaults`/Info.plist equivalent, so we read the resource directly +// instead of shelling out to a macOS-only tool. +func GetCurrentVersion() string { + exePath, err := os.Executable() + if err != nil { + slog.Error("Error getting executable path", "error", err) + return "dev" + } + + var zero windows.Handle + size, err := windows.GetFileVersionInfoSize(exePath, &zero) + if err != nil || size == 0 { + slog.Debug("No version resource in executable", "error", err) + return "dev" + } + + info := make([]byte, size) + if err := windows.GetFileVersionInfo(exePath, 0, size, unsafe.Pointer(&info[0])); err != nil { + slog.Debug("Failed to read version resource", "error", err) + return "dev" + } + + var fixed *windows.VS_FIXEDFILEINFO + fixedLen := uint32(unsafe.Sizeof(*fixed)) + if err := windows.VerQueryValue(unsafe.Pointer(&info[0]), `\`, unsafe.Pointer(&fixed), &fixedLen); err != nil || fixed == nil { + slog.Debug("Failed to query fixed file info", "error", err) + return "dev" + } + + major := (fixed.FileVersionMS >> 16) & 0xffff + minor := fixed.FileVersionMS & 0xffff + patch := (fixed.FileVersionLS >> 16) & 0xffff + return fmt.Sprintf("%d.%d.%d", major, minor, patch) +} diff --git a/packages/finicky-ui/src/App.svelte b/packages/finicky-ui/src/App.svelte index 9578c49c..4917f69d 100644 --- a/packages/finicky-ui/src/App.svelte +++ b/packages/finicky-ui/src/App.svelte @@ -91,12 +91,19 @@ // Capture any messages buffered by the WKUserScript stub before Svelte loaded. const _preloadQueue: any[] = window.finicky._queue ?? []; - // Replace the stub with the real implementation. + // Replace the stub with the real implementation. Route outbound messages to + // whichever native bridge the host provides: WKWebView on macOS + // (window.webkit.messageHandlers.finicky) or the WebView2 binding on Windows + // (window.__finicky_send). Without the Windows branch, sendMessage is a silent + // no-op there and no UI request ever reaches Go. window.finicky = { sendMessage: (msg: any) => { - window.webkit?.messageHandlers?.finicky?.postMessage( - JSON.stringify(msg) - ); + const json = JSON.stringify(msg); + if (window.webkit?.messageHandlers?.finicky) { + window.webkit.messageHandlers.finicky.postMessage(json); + } else if (typeof window.__finicky_send === "function") { + window.__finicky_send(json); + } }, receiveMessage: handleMessage, }; diff --git a/packages/finicky-ui/src/types.ts b/packages/finicky-ui/src/types.ts index 2eeb589f..5f90fbde 100644 --- a/packages/finicky-ui/src/types.ts +++ b/packages/finicky-ui/src/types.ts @@ -42,6 +42,8 @@ declare global { }; }; }; + /** JS→Go bridge bound by the Windows WebView2 host (absent on macOS). */ + __finicky_send?: (msg: string) => void; } } From 593c9d49b0a7d8a7ac838b0a97818d032904e163 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Fri, 3 Jul 2026 21:56:28 -0700 Subject: [PATCH 06/15] fix(windows): F10 - make Finicky selectable as default browser on Win11 Win11's Default Apps picker never offered Finicky with the shipped registration. Three stacked causes, diagnosed live on Win11 Home 26200: 1. Registration shape too minimal: the picker filters http/https candidates to apps that also declare Capabilities\FileAssociations (.htm/.html), Capabilities\Startmenu, and InstallInfo. Added all three to installer.iss and finicky-register.reg. 2. Missing SHChangeNotify(SHCNE_ASSOCCHANGED): the installer's [Code] stub skipped it as "optional polish" - it is required. Now imported from shell32 and called at ssPostInstall. 3. HKCU alone was insufficient on this build: the picker listed Finicky only after the registration was mirrored to HKLM. Strategy decision (elevate installer vs stay per-user) escalated to MAC_HANDOFF.md / PR review - not changed unilaterally here. Also: the picker labels entries with the exe's FileDescription, which read "Rule-based browser router". Windows convention (Chrome: "Google Chrome") is the app display name - changed to "Finicky" in versioninfo.json and regenerated resource_windows_amd64.syso. Verified on box: Finicky now appears in the HTTPS-link picker. Co-Authored-By: Claude Fable 5 --- apps/finicky/src/resource_windows_amd64.syso | Bin 746916 -> 746876 bytes apps/finicky/src/versioninfo.json | 2 +- scripts/installer.iss | 26 ++++++++++++------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/apps/finicky/src/resource_windows_amd64.syso b/apps/finicky/src/resource_windows_amd64.syso index f1eec84bf226b26ad2b38187259f8a35a494f174..dbba37cccd41ccdecf83aa600b93e1a2c4437ee2 100644 GIT binary patch delta 311 zcmZ2-TKCT>UCxv?Mg|Z_o5(56*^t7`U<2gr*{Ivi%($a@Cv*EwW=0@p0%8!G1&CRJ zm<@>8ftUk`If0mK`%Y$V?>NRC(^-?aqZuWpCnj+#Gksy4-j>Ae#%#gBG5vfJw<@n2 zLncEW5GONaGgNL@P3GRsSYN>ewx5BSp##pE0cWj%vv$B)C*Z6baMlYr>j#|0!3;B5 g0?yKavn(JiEuh67aKQ*TD+A7|fU&0crgDn|02$vhxBvhE delta 369 zcmex!N_WX=UCxwMj0_;qH<44Cb43a_gAI`LWTS33Gvl4+oy_e!nHhnY35Y>#79eH? zVm2UV2VxE&<^*D{?K_#dz2g|~OlM8vj%Ku(o|wd~%p}1yy)B8`jX8!vYWn#kZdJn| zhEj$chExV!h9rhWhGHNqg+T$xE@H@MC5J;;F-Odyhh zfr+65&YA&dt$?$3z*#5YtQ&CF3pncsoW;QmGg$)8(txupm>FE57JI-2BjBtIII9BA U(km)1N`{!p0191Z1{NR*04z{CMgRZ+ diff --git a/apps/finicky/src/versioninfo.json b/apps/finicky/src/versioninfo.json index f1d991ab..c82ea428 100644 --- a/apps/finicky/src/versioninfo.json +++ b/apps/finicky/src/versioninfo.json @@ -10,7 +10,7 @@ }, "StringFileInfo": { "CompanyName": "Finicky", - "FileDescription": "Rule-based browser router", + "FileDescription": "Finicky", "FileVersion": "4.2.2", "InternalName": "Finicky.exe", "LegalCopyright": "MIT — John Sterling and contributors", diff --git a/scripts/installer.iss b/scripts/installer.iss index e9bb186a..2710cf97 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -71,6 +71,16 @@ Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URL Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "https"; ValueType: string; ValueData: "FinickyURL" Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "finicky"; ValueType: string; ValueData: "FinickyURL" +; ---- Browser-shape extras (required for the Win11 Default Apps picker) ---- +; Win11 filters the http/https picker to apps that look like full browsers: +; FileAssociations + Startmenu + InstallInfo must exist alongside +; URLAssociations, or Finicky never appears as a selectable option. +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".htm"; ValueType: string; ValueData: "FinickyURL" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".html"; ValueType: string; ValueData: "FinickyURL" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\Startmenu"; ValueName: "StartMenuInternet"; ValueType: string; ValueData: "Finicky" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "ReinstallCommand"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" +Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "IconsVisible"; ValueType: dword; ValueData: 1 + ; ---- RegisteredApplications ---- ; The master index Windows checks to discover which apps offer URL handling. Root: HKCU; Subkey: "SOFTWARE\RegisteredApplications"; ValueName: "Finicky"; ValueType: string; ValueData: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; Flags: uninsdeletevalue @@ -96,17 +106,13 @@ Type: filesandordirs; Name: "{localappdata}\Finicky" [Code] // Notify Windows that registered applications changed so the Default Apps -// list refreshes without requiring a reboot. +// picker refreshes. NOT optional: without this (verified on Win11 26200), +// Settings does not see the new registration until a shell restart/logon. +procedure SHChangeNotify(wEventID, uFlags, dwItem1, dwItem2: Integer); + external 'SHChangeNotify@shell32.dll stdcall'; + procedure CurStepChanged(CurStep: TSetupStep); -var - Res: Integer; begin if CurStep = ssPostInstall then - begin - // SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, 0, 0) - // Inno doesn't expose SHChangeNotify directly, but we can call it via - // a small helper. The registry changes above are sufficient — Windows - // picks them up on next Settings open. For immediate refresh we'd need - // a DLL call, which is optional polish. - end; + SHChangeNotify($08000000, 0, 0, 0); // SHCNE_ASSOCCHANGED, SHCNF_IDLIST end; From c872e974267e99a6b95b3ce23e0b2b2a908eecc5 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Fri, 3 Jul 2026 22:04:21 -0700 Subject: [PATCH 07/15] fix(windows): F11 - Start Menu shortcut must pass --window (bare launch is the windowless resident-router mode; with a primary running it exits silently, so the shortcut appeared dead). Verified: --window with resident primary opens the window via the show-window pipe request. Co-Authored-By: Claude Fable 5 --- scripts/installer.iss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/installer.iss b/scripts/installer.iss index 2710cf97..9377af1d 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -39,7 +39,10 @@ Name: "setdefault"; Description: "Open Default Apps settings after install"; Gro Source: "..\apps\finicky\build\windows\{#MyAppExeName}"; DestDir: "{app}"; Flags: ignoreversion [Icons] -Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}" +; --window: clicking the Start Menu entry must open the config UI. A bare +; launch is the (windowless) resident-router mode used by autostart, and with +; a primary already running it exits silently - i.e. the shortcut appears dead. +Name: "{group}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; Parameters: "--window" Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" [Registry] From c60c0e5ba9442dde9f0c37c04985c82b6d480195 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 02:34:49 -0700 Subject: [PATCH 08/15] fix(windows): F12 - read default-browser choice from UserChoiceLatest Win11 26200 records the user's http/https choice under UserChoiceLatest\ProgId and leaves the legacy UserChoice key permanently stale, so isDefaultBrowser reported false while Finicky was demonstrably routing clicks. Prefer UserChoiceLatest, fall back to UserChoice for older builds. Verified on box: 'Finicky is the default browser' now logs; choice survived a reboot; Tier 1 complete. Also logs F13 (Run-key autostart never executes at logon - mitigation applied, verify next boot) and closes the stale-label question as this-box state-repo debris. Co-Authored-By: Claude Fable 5 --- apps/finicky/src/browser_windows.go | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/apps/finicky/src/browser_windows.go b/apps/finicky/src/browser_windows.go index a7f1cd9d..fbb49c15 100644 --- a/apps/finicky/src/browser_windows.go +++ b/apps/finicky/src/browser_windows.go @@ -36,8 +36,19 @@ func isDefaultBrowser() (bool, error) { return httpHandler == progID && httpsHandler == progID, nil } +// getDefaultHandlerForURLScheme reads the effective ProgId for a scheme. +// Newer Windows 11 builds (observed on 26200) record the user's choice under +// UserChoiceLatest\ProgId and leave the legacy UserChoice key stale — reading +// only UserChoice reports the wrong (old) handler forever. Prefer the new +// location, fall back to the legacy one for older builds. func getDefaultHandlerForURLScheme(scheme string) (string, error) { - path := fmt.Sprintf(`SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\%s\UserChoice`, scheme) + if progId, err := readUserChoiceProgId(fmt.Sprintf(`SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\%s\UserChoiceLatest\ProgId`, scheme)); err == nil { + return progId, nil + } + return readUserChoiceProgId(fmt.Sprintf(`SOFTWARE\Microsoft\Windows\Shell\Associations\UrlAssociations\%s\UserChoice`, scheme)) +} + +func readUserChoiceProgId(path string) (string, error) { pathPtr, _ := syscall.UTF16PtrFromString(path) var key syscall.Handle @@ -49,7 +60,7 @@ func getDefaultHandlerForURLScheme(scheme string) (string, error) { uintptr(unsafe.Pointer(&key)), ) if ret != 0 { - return "", fmt.Errorf("no default handler found for '%s'", scheme) + return "", fmt.Errorf("no key at '%s'", path) } defer procRegCloseKeyB.Call(uintptr(key)) @@ -64,7 +75,7 @@ func getDefaultHandlerForURLScheme(scheme string) (string, error) { uintptr(unsafe.Pointer(&size)), ) if ret != 0 { - return "", fmt.Errorf("no ProgId found for '%s'", scheme) + return "", fmt.Errorf("no ProgId value at '%s'", path) } return syscall.UTF16ToString(buf), nil From 0d22a3b954269efcb50158dbd341e121bd92294a Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 02:48:49 -0700 Subject: [PATCH 09/15] feat(windows): dual-mode installer - admin/HKLM default, /CURRENTUSER fallback (F10 decision) Per-user (HKCU-only) registration never appears in the Win11 26200 Default Apps picker; machine-wide registration works. Default to admin install: HKA registry roots (HKLM in admin mode, HKCU per-user), Program Files install dir (also un-collides install dir from the app cache dir), machine-wide autostart. /CURRENTUSER preserved for no-admin environments. Verified on box: install/uninstall clean, exe in Program Files, HKLM registration correct, default-browser choice survives reinstall, router logs 'Finicky is the default browser'. Co-Authored-By: Claude Fable 5 --- scripts/installer.iss | 63 ++++++++++++++++++++++++------------------- 1 file changed, 36 insertions(+), 27 deletions(-) diff --git a/scripts/installer.iss b/scripts/installer.iss index 9377af1d..8e885b95 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -1,6 +1,14 @@ ; Finicky for Windows — Inno Setup installer ; Registers Finicky as a browser so Windows routes http/https URLs to it. -; Per-user install (no admin required). +; +; Install mode: admin (all-users) by default, per-user via /CURRENTUSER. +; Rationale (smoke-test finding F10): on current Windows 11 (observed on +; build 26200) the Default Apps picker only offers browsers registered +; machine-wide (HKLM) — a per-user (HKCU-only) registration is complete and +; correct but never appears as a selectable default browser. HKA roots below +; resolve to HKLM in admin mode and HKCU in per-user mode, so /CURRENTUSER +; still yields a working install for routing/protocol use on systems where +; per-user registration is honored. #define MyAppName "Finicky" #define MyAppVersion "4.2.2" @@ -15,13 +23,14 @@ AppVersion={#MyAppVersion} AppPublisher={#MyAppPublisher} AppPublisherURL={#MyAppURL} AppSupportURL={#MyAppURL}/issues -DefaultDirName={localappdata}\{#MyAppName} +DefaultDirName={autopf}\{#MyAppName} DefaultGroupName={#MyAppName} DisableProgramGroupPage=yes OutputBaseFilename=FinickySetup-{#MyAppVersion} Compression=lzma SolidCompression=yes -PrivilegesRequired=lowest +PrivilegesRequired=admin +PrivilegesRequiredOverridesAllowed=dialog commandline SetupIconFile=..\apps\finicky\assets\Resources\finicky.ico ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible @@ -48,50 +57,50 @@ Name: "{group}\Uninstall {#MyAppName}"; Filename: "{uninstallexe}" [Registry] ; ---- ProgID: FinickyURL ---- ; This is the handler that runs when Windows invokes a URL assigned to Finicky. -Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueType: string; ValueData: "Finicky URL"; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" -Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "SOFTWARE\Classes\FinickyURL\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" +Root: HKA; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueType: string; ValueData: "Finicky URL"; Flags: uninsdeletekey +Root: HKA; Subkey: "SOFTWARE\Classes\FinickyURL"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" +Root: HKA; Subkey: "SOFTWARE\Classes\FinickyURL\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKA; Subkey: "SOFTWARE\Classes\FinickyURL\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" ; ---- finicky:// custom protocol (always active, independent of default browser choice) ---- -Root: HKCU; Subkey: "SOFTWARE\Classes\finicky"; ValueType: string; ValueData: "Finicky Protocol"; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Classes\finicky"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" -Root: HKCU; Subkey: "SOFTWARE\Classes\finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "SOFTWARE\Classes\finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" +Root: HKA; Subkey: "SOFTWARE\Classes\finicky"; ValueType: string; ValueData: "Finicky Protocol"; Flags: uninsdeletekey +Root: HKA; Subkey: "SOFTWARE\Classes\finicky"; ValueName: "URL Protocol"; ValueType: string; ValueData: "" +Root: HKA; Subkey: "SOFTWARE\Classes\finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKA; Subkey: "SOFTWARE\Classes\finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" ""%1""" ; ---- Browser registration under StartMenuInternet ---- ; This makes Finicky appear in Settings > Default Apps > Web browser. -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky"; ValueType: string; ValueData: "Finicky"; Flags: uninsdeletekey -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky"; ValueType: string; ValueData: "Finicky"; Flags: uninsdeletekey +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\DefaultIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\shell\open\command"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" ; ---- Capabilities ---- ; Tells Windows which URL schemes Finicky can handle. -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationName"; ValueType: string; ValueData: "Finicky" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationDescription"; ValueType: string; ValueData: "A rule-based browser routing utility. Define rules to open URLs in different browsers based on the link domain, path, or source application." -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "http"; ValueType: string; ValueData: "FinickyURL" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "https"; ValueType: string; ValueData: "FinickyURL" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "finicky"; ValueType: string; ValueData: "FinickyURL" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationName"; ValueType: string; ValueData: "Finicky" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationDescription"; ValueType: string; ValueData: "A rule-based browser routing utility. Define rules to open URLs in different browsers based on the link domain, path, or source application." +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; ValueName: "ApplicationIcon"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"",0" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "http"; ValueType: string; ValueData: "FinickyURL" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "https"; ValueType: string; ValueData: "FinickyURL" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\URLAssociations"; ValueName: "finicky"; ValueType: string; ValueData: "FinickyURL" ; ---- Browser-shape extras (required for the Win11 Default Apps picker) ---- ; Win11 filters the http/https picker to apps that look like full browsers: ; FileAssociations + Startmenu + InstallInfo must exist alongside ; URLAssociations, or Finicky never appears as a selectable option. -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".htm"; ValueType: string; ValueData: "FinickyURL" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".html"; ValueType: string; ValueData: "FinickyURL" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\Startmenu"; ValueName: "StartMenuInternet"; ValueType: string; ValueData: "Finicky" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "ReinstallCommand"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" -Root: HKCU; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "IconsVisible"; ValueType: dword; ValueData: 1 +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".htm"; ValueType: string; ValueData: "FinickyURL" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\FileAssociations"; ValueName: ".html"; ValueType: string; ValueData: "FinickyURL" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities\Startmenu"; ValueName: "StartMenuInternet"; ValueType: string; ValueData: "Finicky" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "ReinstallCommand"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"" --window" +Root: HKA; Subkey: "SOFTWARE\Clients\StartMenuInternet\Finicky\InstallInfo"; ValueName: "IconsVisible"; ValueType: dword; ValueData: 1 ; ---- RegisteredApplications ---- ; The master index Windows checks to discover which apps offer URL handling. -Root: HKCU; Subkey: "SOFTWARE\RegisteredApplications"; ValueName: "Finicky"; ValueType: string; ValueData: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; Flags: uninsdeletevalue +Root: HKA; Subkey: "SOFTWARE\RegisteredApplications"; ValueName: "Finicky"; ValueType: string; ValueData: "SOFTWARE\Clients\StartMenuInternet\Finicky\Capabilities"; Flags: uninsdeletevalue ; ---- Autostart (optional) ---- ; Launch WITHOUT --window: autostart makes Finicky the resident URL router at ; login (useful with keepRunning), it must not pop the config window on boot. -Root: HKCU; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueName: "Finicky"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart +Root: HKA; Subkey: "SOFTWARE\Microsoft\Windows\CurrentVersion\Run"; ValueName: "Finicky"; ValueType: string; ValueData: """{app}\{#MyAppExeName}"""; Flags: uninsdeletevalue; Tasks: autostart [Run] ; After install, optionally open Default Apps so the user can select Finicky From 9a86aa253658cf2012569a04570164049bc6f171 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 02:52:24 -0700 Subject: [PATCH 10/15] chore(installer): no install-mode dialog - straight to UAC Product owner guidance: target audience is personal, unmanaged machines (no prior Windows install base to migrate). Skip the all-users/current-user question on interactive installs; /CURRENTUSER stays available via command line only. Co-Authored-By: Claude Fable 5 --- scripts/installer.iss | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/scripts/installer.iss b/scripts/installer.iss index 8e885b95..5bfeefb9 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -29,8 +29,11 @@ DisableProgramGroupPage=yes OutputBaseFilename=FinickySetup-{#MyAppVersion} Compression=lzma SolidCompression=yes +; commandline-only override: interactive installs go straight to UAC (target +; audience is personal, unmanaged machines - product owner call, 2026-07-04); +; /CURRENTUSER remains available for the rare no-admin case, unadvertised. PrivilegesRequired=admin -PrivilegesRequiredOverridesAllowed=dialog commandline +PrivilegesRequiredOverridesAllowed=commandline SetupIconFile=..\apps\finicky\assets\Resources\finicky.ico ArchitecturesAllowed=x64compatible ArchitecturesInstallIn64BitMode=x64compatible From f023514655ae326a03988ecd1179f26720d243ac Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 02:58:47 -0700 Subject: [PATCH 11/15] fix(windows): F7 - inject real version via ldflags (git describe) GetCurrentVersion now returns the ldflags-injected finicky/version.version (from git describe --tags --match 'v*') and falls back to platform metadata (Info.plist / PE resource) for bare builds. build-windows.sh injects it; installer.iss version is CI-overridable via /DMyAppVersion; versioninfo.json bumped off the stale 4.2.2 to 4.4.0-alpha. Verified: log shows version=v4.4.0-alpha-25-g9b6c1db. Co-Authored-By: Claude Fable 5 --- apps/finicky/src/resource_windows_amd64.syso | Bin 746876 -> 746908 bytes apps/finicky/src/version/version.go | 15 +++++++++++++++ apps/finicky/src/version/version_darwin.go | 7 ++++--- apps/finicky/src/version/version_windows.go | 11 ++++++----- apps/finicky/src/versioninfo.json | 10 +++++----- scripts/build-windows.sh | 4 ++++ scripts/installer.iss | 5 ++++- 7 files changed, 38 insertions(+), 14 deletions(-) diff --git a/apps/finicky/src/resource_windows_amd64.syso b/apps/finicky/src/resource_windows_amd64.syso index dbba37cccd41ccdecf83aa600b93e1a2c4437ee2..74d1e1b92d2b0056a6dc53a27a6f3b7aa915cfd2 100644 GIT binary patch delta 244 zcmex!N_Wm_UCxv?Mg|aQo5(56xgdp`!3M~=vr)I3nej^VPUiNV%#1+H1jHaV3lOsc zF&hxG12G2>a{@8f_MOb!-f@gqrn4q->+7;Guz(GP(e_{-15lKO0b(6g07#ikPfX%g zW)fhU-j>8&$85pCGo3w|TT#%2L65UCxv?Mg|Z_o5(56*^t7`U<2gr*{Ivi%($a@Cv*EwW=0@p0%8!G1&CRJ zm<@>8ftUk`If0mK`%Y$V?>NRC(^-?a^>vvTSb(N50U?C82iX7wEI/dev/null || echo "dev") BUILD_DATE=$(date -u '+%Y-%m-%d %H:%M:%S UTC') API_HOST=$(cat "$ROOT_DIR/.env" 2>/dev/null | grep API_HOST | cut -d '=' -f 2 || echo "") +# Authoritative version from version tags only (--match filters out CI/test +# tags); overrides the PE-resource fallback in version_windows.go (F7). +VERSION=$(git -C "$ROOT_DIR" describe --tags --match 'v*' --always 2>/dev/null || echo "dev") # Cross-compile for Windows mkdir -p "$ROOT_DIR/apps/finicky/build/windows" @@ -35,6 +38,7 @@ CGO_ENABLED=0 GOOS=windows GOARCH=amd64 \ "-X 'finicky/version.commitHash=${COMMIT_HASH}' \ -X 'finicky/version.buildDate=${BUILD_DATE}' \ -X 'finicky/version.apiHost=${API_HOST}' \ + -X 'finicky/version.version=${VERSION}' \ -H windowsgui" \ -o ../build/windows/Finicky.exe diff --git a/scripts/installer.iss b/scripts/installer.iss index 5bfeefb9..89ecdfc9 100644 --- a/scripts/installer.iss +++ b/scripts/installer.iss @@ -11,7 +11,10 @@ ; per-user registration is honored. #define MyAppName "Finicky" -#define MyAppVersion "4.2.2" +; Overridable from CI: iscc /DMyAppVersion= installer.iss +#ifndef MyAppVersion + #define MyAppVersion "4.4.0-alpha" +#endif #define MyAppPublisher "John Sterling" #define MyAppURL "https://github.com/johnste/finicky" #define MyAppExeName "Finicky.exe" From f531aba344ac33bd483a2efbe21aaa2d67e42dc0 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 03:00:21 -0700 Subject: [PATCH 12/15] fix(ui): F2 - Test tab timeout and error states The test-url round trip had no failure mode: a dead bridge or hung resolver spun the loading indicator forever, and a backend error payload rendered as an empty result grid. Add a 5s response timeout with a clear message, and render backend {error} payloads as an error box. Also marks F1 fully complete - the StartPage Safari seed was already removed on this branch (findings doc was stale). Co-Authored-By: Claude Fable 5 --- packages/finicky-ui/src/lib/testUrlStore.ts | 8 ++-- packages/finicky-ui/src/pages/TestUrl.svelte | 43 +++++++++++++++++++- 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/packages/finicky-ui/src/lib/testUrlStore.ts b/packages/finicky-ui/src/lib/testUrlStore.ts index c7adfacb..5fa731b3 100644 --- a/packages/finicky-ui/src/lib/testUrlStore.ts +++ b/packages/finicky-ui/src/lib/testUrlStore.ts @@ -1,10 +1,12 @@ import { writable } from 'svelte/store'; export interface TestUrlResult { - browser: string; - url: string; - openInBackground: boolean; + browser?: string; + url?: string; + openInBackground?: boolean; profile?: string; + /** Set by the backend when URL evaluation failed (e.g. config error). */ + error?: string; } export const testUrlResult = writable(null); diff --git a/packages/finicky-ui/src/pages/TestUrl.svelte b/packages/finicky-ui/src/pages/TestUrl.svelte index 95cee7c9..b5b0b92c 100644 --- a/packages/finicky-ui/src/pages/TestUrl.svelte +++ b/packages/finicky-ui/src/pages/TestUrl.svelte @@ -31,11 +31,18 @@ const DEBOUNCE_DELAY = 300; const LOADING_DELAY = DEBOUNCE_DELAY + 100; + // If the backend never answers (dead bridge, hung resolver), stop the + // spinner and say so instead of spinning forever. + const RESPONSE_TIMEOUT = 5000; + let responseTimer: ReturnType; + let timedOut = false; function testUrlAutomatically() { clearTimeout(debounceTimer); clearTimeout(loadingTimer); + clearTimeout(responseTimer); loading = false; + timedOut = false; if (!isValidUrl(testUrl)) { testUrlResult.set(null); @@ -50,6 +57,11 @@ debounceTimer = setTimeout(() => { const normalizedUrl = normalizeUrl(testUrl); + responseTimer = setTimeout(() => { + loading = false; + timedOut = true; + }, RESPONSE_TIMEOUT); + // Send message to native app to test the URL window.finicky.sendMessage({ type: "testUrl", @@ -59,8 +71,12 @@ } // Subscribe to test results - testUrlResult.subscribe(() => { + testUrlResult.subscribe((result) => { clearTimeout(loadingTimer); + if (result) { + clearTimeout(responseTimer); + timedOut = false; + } loading = false; }); @@ -94,7 +110,17 @@ />
- {#if $testUrlResult} + {#if timedOut} +
+ + No response from Finicky — the test timed out. Check the Logs tab for errors. +
+ {:else if $testUrlResult?.error} +
+ + Could not evaluate this URL: {$testUrlResult.error} +
+ {:else if $testUrlResult}

Result

@@ -206,6 +232,19 @@ font-size: 0.95em; } + .error-message { + display: flex; + align-items: center; + gap: 10px; + padding: 12px 16px; + background: rgba(224, 82, 82, 0.08); + border: 1px solid rgba(224, 82, 82, 0.35); + border-radius: 8px; + color: var(--text-primary); + font-size: 0.9em; + word-break: break-word; + } + .hint-message { display: flex; align-items: center; From 55e3ca4e9b669349c6c6a431394814af15c2147b Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 03:06:17 -0700 Subject: [PATCH 13/15] ci: run macOS build on windows-support/windows-smoke pushes Co-Authored-By: Claude Fable 5 --- .github/workflows/macos-build.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index 6e904916..bec53c3f 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -3,7 +3,7 @@ name: macOS Build on: push: - branches: [main] + branches: [main, windows-support, windows-smoke] pull_request: branches: [main] workflow_dispatch: From 109d233838f4c42ff59a0e23abe052512837b468 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 03:09:11 -0700 Subject: [PATCH 14/15] ci: clarify macOS check name; run Windows build on port branches The macOS job on windows-* branches is the shared-file regression guard (App.svelte, version.go etc. compile into the macOS app too) - name now says so. Windows build joins it so the Actions list reads as a two-platform matrix. Co-Authored-By: Claude Fable 5 --- .github/workflows/macos-build.yml | 2 +- .github/workflows/windows-build.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/macos-build.yml b/.github/workflows/macos-build.yml index bec53c3f..b26fc984 100644 --- a/.github/workflows/macos-build.yml +++ b/.github/workflows/macos-build.yml @@ -1,5 +1,5 @@ # This workflow builds the Finicky macOS app for both Silicon (ARM64) and Intel (x86_64) architectures -name: macOS Build +name: macOS Build (cross-platform regression check) on: push: diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 76e2a249..4de6c488 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -2,7 +2,7 @@ name: Windows Build on: push: - branches: [main] + branches: [main, windows-support, windows-smoke] pull_request: branches: [main] workflow_dispatch: From e2ae44d92e34bc33184e9b025595fc0f205d2e33 Mon Sep 17 00:00:00 2001 From: chaoz23 Date: Sat, 4 Jul 2026 03:14:42 -0700 Subject: [PATCH 15/15] ci: fix Windows build - pwsh instead of cmd (npm.cmd chaining ate the asset-copy steps); inject version; fetch tags Co-Authored-By: Claude Fable 5 --- .github/workflows/windows-build.yml | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/.github/workflows/windows-build.yml b/.github/workflows/windows-build.yml index 4de6c488..f4a84288 100644 --- a/.github/workflows/windows-build.yml +++ b/.github/workflows/windows-build.yml @@ -14,6 +14,8 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: 0 # full history + tags so `git describe` yields the real version - name: Set up Node.js uses: actions/setup-node@v4 @@ -38,21 +40,24 @@ jobs: cd ../finicky-ui npm ci + # pwsh, not cmd: in a cmd script, `npm` (npm.cmd) without `call` transfers + # control and never returns, so everything after the first npm line was + # silently skipped and the embed assets never landed. - name: Build config-api run: | cd packages/config-api npm run build npm run generate-types - copy /Y dist\finickyConfigAPI.js ..\..\apps\finicky\src\assets\finickyConfigAPI.js - shell: cmd + Copy-Item dist/finickyConfigAPI.js ../../apps/finicky/src/assets/finickyConfigAPI.js -Force + shell: pwsh - name: Build UI run: | cd packages/finicky-ui npm run build - mkdir ..\..\apps\finicky\src\assets\templates 2>nul - xcopy /E /Y dist\* ..\..\apps\finicky\src\assets\templates\ - shell: cmd + New-Item -ItemType Directory -Force ../../apps/finicky/src/assets/templates | Out-Null + Copy-Item dist/* ../../apps/finicky/src/assets/templates/ -Recurse -Force + shell: pwsh - name: Build Windows binary env: @@ -62,8 +67,9 @@ jobs: run: | $commitHash = git rev-parse --short HEAD $buildDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss UTC" -AsUTC + $version = git describe --tags --match 'v*' --always cd apps/finicky/src - go build -ldflags "-X 'finicky/version.commitHash=$commitHash' -X 'finicky/version.buildDate=$buildDate' -H windowsgui" -o ../build/windows/Finicky.exe . + go build -ldflags "-X 'finicky/version.commitHash=$commitHash' -X 'finicky/version.buildDate=$buildDate' -X 'finicky/version.version=$version' -H windowsgui" -o ../build/windows/Finicky.exe . shell: pwsh - name: Run tests