Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions .github/workflows/alpha.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,8 @@ jobs:
id: ver
shell: pwsh
run: |
$h = Get-Content src/version.h -Raw
$maj = [regex]::Match($h, '#define\s+WIND_VER_MAJOR\s+(\d+)').Groups[1].Value
$min = [regex]::Match($h, '#define\s+WIND_VER_MINOR\s+(\d+)').Groups[1].Value
$pat = [regex]::Match($h, '#define\s+WIND_VER_PATCH\s+(\d+)').Groups[1].Value
if (-not $maj) { throw 'could not read WIND_VER_MAJOR from src/version.h' }
$v = "$maj.$min.$pat"
. ./tools/version.ps1 # Get-WindVersion: the one src/version.h reader
$v = Get-WindVersion (Get-Location).Path
$sha = '${{ github.sha }}'.Substring(0,7)
"version=$v" >> $env:GITHUB_OUTPUT
"alpha=$v-alpha.$sha" >> $env:GITHUB_OUTPUT
Expand Down
8 changes: 2 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,12 +51,8 @@ jobs:
id: ver
shell: pwsh
run: |
$h = Get-Content src/version.h -Raw
$maj = [regex]::Match($h, '#define\s+WIND_VER_MAJOR\s+(\d+)').Groups[1].Value
$min = [regex]::Match($h, '#define\s+WIND_VER_MINOR\s+(\d+)').Groups[1].Value
$pat = [regex]::Match($h, '#define\s+WIND_VER_PATCH\s+(\d+)').Groups[1].Value
if (-not $maj) { throw 'could not read WIND_VER_MAJOR from src/version.h' }
"version=$maj.$min.$pat" >> $env:GITHUB_OUTPUT
. ./tools/version.ps1 # Get-WindVersion: the one src/version.h reader
"version=$(Get-WindVersion (Get-Location).Path)" >> $env:GITHUB_OUTPUT

- name: Install NSIS
shell: pwsh
Expand Down
5 changes: 3 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ are frozen history cited from code, not kept in step with it.
- `build.bat`: `Wind.exe` + `WindTray.exe` (uiAccess=false). Wind refuses to start without UIAccess;
run a plain build with `WIND_ALLOW_NO_UIACCESS=1` (or `WIND_SELFTEST=1`).
- `build.bat test`: builds and runs the doctest binary; exit 0 = pass.
- `build.bat check`: compile-only pass over `src\*.cpp`.
- `build.bat check`: compile-only pass over `src\*.cpp`, the tray sources and WindConfig's host.
- `build.bat uiaccess`: the uiAccess=true build; works only signed and run from `C:\Program Files\Wind`.
- `build.bat config`: builds the Svelte UI (`ui/` -> `ui/dist/`) and `WindConfig.exe`.
- `build.bat tray`: `WindTray.exe` alone. It always uses the plain manifest; never make it uiAccess.
Expand Down Expand Up @@ -58,7 +58,8 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
Never add `QS_RAWINPUT`/`QS_INPUT` to the mask.
- Tick counts depend on the refresh rate; derive new ones through `TicksAtHz`/`setTickRate`.
- A config reload rebuilds `ZoomController`; UI-only keys are stripped by `StripUiOnlyKeys` so they
never reload. Add new UI-only keys there.
never reload. Add new UI-only keys to `kUiOnlyKeys` (`src/profiles.h`) and
`tests/fixtures/global_keys.txt` (the UI's `GLOBAL_KEYS` is tested against it).

**Cursor (07)**
- In a transform session the lock (`lockApps`, tells) applies only while the pointer is hidden
Expand Down
30 changes: 16 additions & 14 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -29,16 +29,7 @@ if /i "%1"=="installer" goto :installer
if /i "%1"=="tray" goto :tray

rem --- App build (normal: uiAccess=false, runs from anywhere) ----------------
rem Compile the app-icon resource (rc.exe ships with the Windows SDK, on PATH via vcvars).
rc /nologo /fo "%ROOT%src\wind.res" "%ROOT%src\wind.rc"
if errorlevel 1 (echo [build] rc.exe failed & exit /b 1)
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE ^
src\*.cpp src\wind.res ^
/Fe:Wind.exe ^
/link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib Dbghelp.lib ^
d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib oleaut32.lib uuid.lib advapi32.lib ^
/MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.manifest /SUBSYSTEM:WINDOWS ^
/DEBUG /OPT:REF /OPT:ICF
call :core_exe "" Wind.manifest
if errorlevel 1 exit /b 1
call :tray_exe
exit /b %errorlevel%
Expand All @@ -48,17 +39,24 @@ rem Embeds Wind.uiaccess.manifest so the overlay CAN use a high z-band (opt-i
rem to cover the Start menu / taskbar / tray. Shipped default is 0 - see issue #162.
rem Deploy via tools\uiaccess_setup.ps1.
:uiaccess
call :core_exe /DWIND_UIACCESS Wind.uiaccess.manifest
if errorlevel 1 exit /b 1
call :tray_exe
exit /b %errorlevel%

rem --- Wind.exe itself, shared by the app and uiaccess targets. %1 = extra define ("" for none),
rem %2 = the manifest to embed. Everything else (sources, flags, libraries) is the same.
:core_exe
rem Compile the app-icon resource (rc.exe ships with the Windows SDK, on PATH via vcvars).
rc /nologo /fo "%ROOT%src\wind.res" "%ROOT%src\wind.rc"
if errorlevel 1 (echo [build] rc.exe failed & exit /b 1)
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE /DWIND_UIACCESS ^
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE %~1 ^
src\*.cpp src\wind.res ^
/Fe:Wind.exe ^
/link Magnification.lib Dwmapi.lib user32.lib shell32.lib gdi32.lib Dbghelp.lib ^
d3d11.lib dxgi.lib dxguid.lib d3dcompiler.lib windowscodecs.lib ole32.lib oleaut32.lib uuid.lib advapi32.lib ^
/MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.uiaccess.manifest /SUBSYSTEM:WINDOWS ^
/MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:%~2 /SUBSYSTEM:WINDOWS ^
/DEBUG /OPT:REF /OPT:ICF
if errorlevel 1 exit /b 1
call :tray_exe
exit /b %errorlevel%

rem --- Tray helper (WindTray.exe, issue #291). ALWAYS the plain manifest (asInvoker, NO uiAccess):
Expand Down Expand Up @@ -124,6 +122,10 @@ cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c src\*.cpp
if errorlevel 1 exit /b 1
rem WindTray.exe sources (issue #291); own object dir, its main.cpp would overwrite Wind's main.obj.
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%ROOT%src\tray_app\\" src\tray_app\*.cpp
if errorlevel 1 exit /b 1
rem WindConfig.exe's host (same flags as :config); own object dir for the same reason.
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /I third_party\webview2\include ^
/Fo"%ROOT%src\config_ui\\" src\config_ui\main.cpp
exit /b %errorlevel%

rem --- Installer (needs NSIS; winget install NSIS.NSIS) ---------------------
Expand Down
6 changes: 3 additions & 3 deletions docs/architecture/01-overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,17 +79,17 @@ build. See [11](11-build-test-release.md).

| Area | Files |
|---|---|
| Loop and session state | `main.cpp` (`wWinMain`, `RunTick`), `uiaccess_gate.h` (refuse to start without UIAccess), `idle_policy.h`, `sched_priority.h`, `tick_stats.h` |
| Loop and session state | `main.cpp` (`wWinMain`, `RunTick`), `uiaccess_gate.h` (refuse to start without UIAccess), `uiaccess_token.h`, `idle_policy.h`, `sched_priority.h`, `tick_stats.h` |
| Engine contract and pick | `magnifier_model.h`, `engine_pick.h`, `shell_desktop.h`, `launch_quiesce.h` |

| Render engine | `render_engine.*`, `render_model.*`, `render_params.cpp`, `render_shaders.h`, `upscale_pass.*`, `upscale_shaders.h`, `upscale_nis.h`, `hdr_info.*`, `hdr_scale.h`, `band_window.h`, `png_dump.*` |
| Render engine | `render_engine.*`, `render_model.*`, `render_params.cpp`, `render_shaders.h`, `hlsl_compile.h`, `upscale_pass.*`, `upscale_shaders.h`, `upscale_nis.h`, `hdr_info.*`, `hdr_scale.h`, `band_window.h`, `png_dump.*` |
| Transform engine | `transform_model.*`, `transform.*`, `mag_host.*`, `tx_warm.h`, `comp_pin.*`, `mpo.h`, `mpo_boot.h`, `mpo_boot_logic.h`, `mpo_guard.h`, `native_cursor.h`, `zoom_ladder.h`, `dwm_watch.*` |
| Colour | `color_filter.*`, `color_matrix.h`, `cursor_tint.*`, `cursor_tint_pixels.h` |
| Input | `input_router.*`, `keybind_rules.h`, `pointer_binds.h`, `keyboard_pan.h`, `typing_key.h`, `event_order.h`, `swallow_ledger.h` |
| Cursor and lock | `cursor_mapper.*`, `lock_detector.*`, `drag_follow.h`, `gain_learner.h`, `cursor_sprite.*`, `cursor_blanker.*`, `cursor_decode.*`, `sprite_layer.h`, `crosshair.*`, `cursor_lock.*`, `inspect_focus.h`, `game_cursor.h`, `pan_glide.h` |
| Tracking | `focus_track.*`, `view_target.h`, `view_glide.h`, `detached_view.h`, `edge_pan.h`, `caret_rect.h`, `track_filter.h`, `java_bridge*`, `focus_identity.h` |
| Zoom | `zoom_controller.*` |
| Config and profiles | `config.*`, `config_path.h`, `ini_edit.*`, `profiles.*`, `profiles_io.h` |
| Config and profiles | `config.*`, `config_path.h`, `ini_edit.*`, `profiles.*`, `profiles_io.h`, `profile_switch.h` |
| Tray | `tray_host.*`, `tray_ipc.h`, `tray_items.*`, `tray_status.h`, `tray_app/` |
| Settings host | `config_ui/` (`main.cpp`, `bridge_json.h`, `wind_watchdog.h`, `webview_recover.h`) |
| Logging and diagnostics | `logging.*`, `log_queue.h`, `test_telemetry.h`, `hitch_record.*`, `tick_span.h`, `com_util.h`, `reload_gate.h` |
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/02-tick-loop.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ There is no settings IPC. `WindConfig.exe` writes `magnifier.ini` and the core n
**UI-only writes never reload.** A reload rebuilds `ZoomController` and `CursorMapper`, which is
wasted work and risks a visible hitch mid-zoom (the live level and mapper centre are carried over
since #234, so the view no longer collapses to 1x). `StripUiOnlyKeys` (`src/config.cpp`) drops
`uiTheme`, `uiPalette`,
the `kUiOnlyKeys` list (`src/profiles.h`): `uiTheme`, `uiPalette`,
`showAdvanced`, `onboarded`, `trayPinned`, `uiHighResNoticeOff` and the five tray layout keys
(`trayPerf`, `traySliders`, `traySliderOrder`, `trayToggles`, `trayToggleOrder`), and the result is compared with the fingerprint of the last applied
config (`t.lastCoreIni`). An identical fingerprint skips the reload. The fingerprint is seeded at startup;
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/05-transform-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,7 +300,7 @@ welded cursor has hover dead zones. Identity or no publish both produce dead zon
[../history/POINTER-HITTEST-FINDINGS.md](../history/POINTER-HITTEST-FINDINGS.md).

- **It needs UIAccess.** Availability is read from the process token's `TokenUIAccess` bit at
`initialize`, with zero Magnification calls; an acquire/release probe would run an identity
`initialize` (`ProcessHasUiAccess`, `src/uiaccess_token.h`), with zero Magnification calls; an acquire/release probe would run an identity
write. A verified-failed publish clears `inputTransformAvailable_`, which stops Auto picking
Transform for the desktop. Only `ERROR_ACCESS_DENIED` is permanent; any other failure is re-armed
from the token probe when the session ends.
Expand Down
32 changes: 20 additions & 12 deletions docs/architecture/08-config-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ others are not).
`LoadConfig` creates the defaults only for a missing file; the hot-reload keeps its settings and
retries (`config ini unreadable on reload`); writers that read the live ini first use
`ReadLiveIni` and stop when it fails.
- The only cross-process kernel objects are the single-instance mutexes and the
`Local\Wind_QuitRequest` event (quit, restart handshake, installer). A window message would not
work: UIPI drops `PostMessage` from a normal process to a UIAccess one.
- The only cross-process kernel objects are the single-instance mutexes, the ini write lock and
the `Local\Wind_QuitRequest` event (quit, restart handshake, installer). A window message would not
work: UIPI drops `PostMessage` from a normal process to a UIAccess one. Their names are constants
in `src/tray_ipc.h` (`kQuitEventName` and the rest); the NSIS and PowerShell copies point there.
- The tray's status block (`Local\Wind_TrayState_v1`) carries status, not settings
([01](01-overview.md)).

Expand All @@ -59,6 +60,9 @@ flowchart LR
- **Never hardcode `L"magnifier.ini"`.** Program Files is read-only for the non-admin runtime, and a
write next to the exe fails silently. The same applies to logs (`ResolveLogDir`) and the WebView2
user-data folder (`%LOCALAPPDATA%\Wind\WebView2`).
- `config_path.h` also holds the two folder lookups every binary uses: `ExeDir()` (the folder of the
running exe, which also holds the other two) and `UserDataDir()` (`%LOCALAPPDATA%\Wind`, created
on first use; `session.keep` and `running.model` live there).

## Parsing

Expand All @@ -74,8 +78,8 @@ as an initializer, so a missing or malformed key keeps the default; parsing neve

The reload mechanics are in [02](02-tick-loop.md). A reload rebuilds `ZoomController`, so it
collapses an active zoom; therefore `StripUiOnlyKeys` removes the eleven keys the core never reads
(`uiTheme`, `uiPalette`, `showAdvanced`, `onboarded`, `trayPinned`, `uiHighResNoticeOff` and
the five tray layout keys `trayPerf`,
(`kUiOnlyKeys` in `src/profiles.h`: `uiTheme`, `uiPalette`, `showAdvanced`, `onboarded`, `trayPinned`,
`uiHighResNoticeOff` and the five tray layout keys `trayPerf`,
`traySliders`, `traySliderOrder`, `trayToggles`, `trayToggleOrder`) and the reload is skipped when the stripped
text is unchanged. `profile` stays in the fingerprint, so a profile switch reloads.

Expand All @@ -100,12 +104,14 @@ A profile is a named full snapshot of the settings, keybinds included, stored as
`profiles\<Name>.ini` next to the resolved ini, with `profile=<name>` in the live ini. Pure logic is
in `src/profiles.*` (tested); I/O in `src/profiles_io.h`.

**Global keys never travel with a profile.** `IsGlobalProfileKey` covers `profile`, `onboarded`,
`uiTheme`, `uiPalette`, `showAdvanced`, `trayPinned`, `uiHighResNoticeOff` and the five tray
layout keys (`trayPerf`, `traySliders`,
**Global keys never travel with a profile.** `IsGlobalProfileKey` covers `profile` plus the
`kUiOnlyKeys` list (`src/profiles.h`): `onboarded`, `uiTheme`, `uiPalette`, `showAdvanced`,
`trayPinned`, `uiHighResNoticeOff` and the five tray layout keys (`trayPerf`, `traySliders`,
`traySliderOrder`, `trayToggles`, `trayToggleOrder`). `MakeProfileText` strips them from profile
files; `MakeLiveText` carries them over from the old live text. Both work line by line and keep
comments and order.
comments and order. `kUiOnlyKeys` is the only C++ copy of the list; the UI's `GLOBAL_KEYS`
(`ui/src/session.js`) is pinned to it through `tests/fixtures/global_keys.txt`, which a doctest and
a node test (`npm run test:rules`) both read.

**Session model: the live ini is the session, the profile file is the saved state.**

Expand All @@ -123,7 +129,8 @@ comments and order.
consumed once.
- Tray Quit compares the files and prompts Save, Discard or Cancel.

**Switching** (Settings or the tray flyout, the same sequence):
**Switching** (Settings or the tray flyout, one implementation: `wind::SwitchProfile` in
`src/profile_switch.h`; each surface only maps its result to its own messages):

1. Read and check the profile with `ProfileTextError`, which rejects binary, oversized or
unparseable text. A read failure is distinct from an empty file; treating a locked file as empty
Expand All @@ -133,8 +140,9 @@ comments and order.
3. Write `MakeLiveText(profile, oldLive, name)` over the live ini.
4. The core hot-reloads it, `model` included (`running.model` reads `live`). Only when Wind runs
one engine (`running.model` names it) and the parsed `model` differs does the surface relaunch
`Wind.exe` (`EngineSwitchesLive`, `src/profiles_io.h`). If that relaunch fails, it writes the
old `model` back, so the ini always matches the running engine.
`Wind.exe` (`EngineSwitchesLive`, `src/profiles_io.h`), writing `session.keep` first. If that
relaunch fails, it writes the old `model` back (`RevertModelAfterFailedRelaunch`, also used by
the tray's engine pick), so the ini always matches the running engine.

**That relaunch uses the eviction handshake, never a kill.** The new instance finds the
single-instance mutex held, sets `Local\Wind_QuitRequest` and waits for the old one to exit. Only a
Expand Down
4 changes: 3 additions & 1 deletion docs/architecture/09-settings-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,9 @@ and offers New (defaults) or Duplicate current; Enter duplicates. There is no re
- Components use tokens (`--sel`, `--onfill`, `--focus`, `--danger*`, `--scrim`, the radii), never
fixed colours.
- WindConfig's pre-paint window colour follows `uiPalette` (`ThemeBackground` in
`src/config_ui/main.cpp`); update it when a theme is added.
`src/config_ui/main.cpp`), read from the generated flyout palettes (`src/tray_app/flyout_palettes.h`).
`tests/test_flyout_palettes.cpp` checks each palette's menu colour against that theme's `--bg` in
`ui/src/design/themes.css`, so rerun both generators when a theme is added.

## Onboarding

Expand Down
8 changes: 5 additions & 3 deletions docs/architecture/11-build-test-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ deploy, the installer's elevation rules and the release workflows.
|---|---|---|
| (none) | `Wind.exe` + `WindTray.exe` | The app with the `uiAccess=false` manifest. It refuses to start without UIAccess (issue #460) unless `WIND_ALLOW_NO_UIACCESS=1` or `WIND_SELFTEST=1` is set |
| `test` | `wind_tests.exe` | The doctest binary over the pure sources; runs it and returns its exit code |
| `check` | – | Compile-only pass over `src\*.cpp` |
| `check` | – | Compile-only pass over `src\*.cpp`, `src\tray_app\*.cpp` and `src\config_ui\main.cpp` |
| `uiaccess` | `Wind.exe` + `WindTray.exe` | `Wind.uiaccess.manifest` and `/DWIND_UIACCESS`; useful only signed and in Program Files. `WindTray.exe` always keeps the plain manifest |
| `tray` | `WindTray.exe` | The tray helper alone, objects in `src\tray_app\` |
| `config` | `WindConfig.exe` | Builds the Svelte app to `ui/dist/`, then the WebView2 host against `third_party/webview2` |
Expand Down Expand Up @@ -44,7 +44,8 @@ headlessly, so the unit tests are the only verification loop that runs everywher
temp files. They need no Wind runtime, so they still run headless.
- `tests/fixtures/keybind_cases.txt` is shared by the C++ and the JavaScript bind rules. The JS half
is `ui/tests/keybind-rules.nodetest.mjs`, run by `npm run test:rules` in `ui/` (`node --test`, no
browser) and by CI.
browser) and by CI. `tests/fixtures/global_keys.txt` does the same for the profile-global keys
(`ui/tests/global-keys.nodetest.mjs`).
- Config defaults are checked from both sides. `tests/test_config_defaults.cpp` walks every key of
`DefaultIniText()` against `Config{}` (a new template key needs an `F(key)` line there) and compares
`tests/fixtures/config_defaults.txt` (key, clamp min and max, default; generated from `Config{}` and
Expand Down Expand Up @@ -122,7 +123,8 @@ flowchart TD

- **`src/version.h` is the only version declaration.** Bumping it cuts a new release `v<ver>`; a
push that leaves it alone refreshes the current release's assets in place with `--clobber`, in
this repo.
this repo. Scripts and both workflows read it through `Get-WindVersion` (`tools/version.ps1`);
`tests/test_version.cpp` fails when `WIND_VERSION_STR` drifts from the three numbers.
- Each release carries the installer twice: `Wind-Setup-x64-<ver>.exe` and the stable name
`Wind-Setup-x64.exe`, so `releases/latest/download/Wind-Setup-x64.exe` always serves the newest.
- Release notes come from `.github/release-notes.md`; the workflow fills `__VERSION__`,
Expand Down
2 changes: 1 addition & 1 deletion installer/app.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

; Wind's own two kernel objects. Both are Local\, i.e. session-scoped, and UAC elevation
; stays inside the session, so the elevated installer and the user's Wind see the same
; objects. Names must match src\main.cpp:1911 and src\main.cpp:2199 exactly.
; objects. Names must match kInstanceMutexName and kQuitEventName in src\tray_ipc.h exactly.
!define WIND_MUTEX "Local\Wind_Magnifier_SingleInstance"
!define WIND_QUIT "Local\Wind_QuitRequest"

Expand Down
Loading
Loading