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
2 changes: 1 addition & 1 deletion .github/actions/build-installer/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ runs:
working-directory: ui
run: npx playwright install chromium && npx playwright test --reporter=line

# Builds Wind.exe + WindConfig.exe + ui/dist, then compiles the installer and runs
# Builds build/Wind.exe + WindConfig.exe + ui/dist, then compiles the installer and runs
# tools/installer_check.ps1. The runner is administrator, so the gate's silent
# install/uninstall round-trip actually executes rather than skipping itself.
- name: Build installer
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
shell: cmd
working-directory: ui
run: npm run test:rules
- name: Build settings host and UI (WindConfig.exe, ui/dist)
- name: Build settings host and UI (build/WindConfig.exe, ui/dist)
shell: cmd
run: build.bat config
- name: Build the UIAccess variant
Expand Down
15 changes: 11 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
# Build output
# Build output: build.bat writes everything to build/ (binaries, plus build/obj/<target>/), and
# a dev build keeps its magnifier.ini, profiles and logs next to its exe there too. The installer
# goes to dist/.
build/
dist/
# Anywhere else: clones that predate build/ still hold root-level .obj/.exe/.pdb files, and
# tools/testenv builds gamesim.exe in place. Kept so none of those can be committed by accident.
*.obj
*.exe
*.pdb
*.ilk
*.lib
*.exp
*.res
build/
dist/
# Vendored binaries that are part of the tree, not build output
!third_party/webview2/x64/*.lib
!installer/MicrosoftEdgeWebview2Setup.exe
Expand All @@ -26,7 +30,10 @@ Thumbs.db
.superpowers/
docs/superpowers/

# Runtime and diagnostic output (a dev build keeps its ini, profiles and logs next to the exe)
# Runtime and diagnostic output. A dev build keeps its ini, profiles and logs next to the exe,
# now build/ (ignored above); the root entries cover clones that ran a pre-build/ Wind.exe.
# A self-test (WIND_SELFTEST=1) writes wind_selftest.png and wind_hdr_diag.txt to its working
# directory, normally the repo root.
/magnifier.ini
/profiles/
/logs/
Expand Down
5 changes: 4 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ New plans and brainstorm output are not committed. `docs/specs`, `docs/design` a
are frozen history cited from code, not kept in step with it.

## Commands
- All build output goes to `build\` (exes, PDBs, `ui\dist` copy; objects in `build\obj\<target>\`),
never the repo root. A dev `build\Wind.exe` keeps its ini, profiles and logs in `build\`. See 11.
- `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.
Expand Down Expand Up @@ -121,7 +123,8 @@ Chapter numbers refer to `docs/architecture/NN-*.md`.
session is the floor.
- The reveal is gated on the present fence (and composite evidence over fullscreen apps).
- Always restore the OS cursor and release `ClipCursor` on every exit path, including the crash filter.
- Verify the overlay only from inside: `WIND_SELFTEST=1 Wind.exe` writes `wind_selftest.png`.
- Verify the overlay only from inside: `WIND_SELFTEST=1 build\Wind.exe` writes `wind_selftest.png`
to its working directory (the repo root when started from there).
- `zorderBand` ships 0: band 16 covers the shell but loses the cursor under the Snipping Tool.
Re-test both before changing it. Never let a refused band be silent (`CreateBandedWindow`).

Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,9 @@ profile. Preferences > Show advanced settings shows the rest. Every setting is a
## Development

`build.bat` builds Wind, `build.bat test` runs the unit tests, and `build.bat config` builds the
Settings app. A plain `build.bat` build has no UIAccess, so Wind refuses to start; run it with
Settings app. Everything they build lands in `build\` (`build\Wind.exe` and friends, objects in
`build\obj\`), never the repo root; a dev build keeps its `magnifier.ini` and logs there too.
A plain `build.bat` build has no UIAccess, so Wind refuses to start; run it with
`WIND_ALLOW_NO_UIACCESS=1` set, or deploy the signed build with `tools\uiaccess_setup.ps1`. In `ui/`,
`npm run test:rules` runs the JS bind-rule and config-defaults tests and `npx playwright test` the Settings UI tests.
The developer docs are in [docs/architecture](docs/architecture/README.md).
Expand Down
71 changes: 51 additions & 20 deletions build.bat
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ call "%VSPATH%\VC\Auxiliary\Build\vcvars64.bat" >nul 2>&1
if errorlevel 1 (echo [build] vcvars64.bat failed & exit /b 1)
cd /d "%ROOT%"

rem --- Build output ------------------------------------------------------------
rem Everything the compiler, linker and rc.exe produce goes under build\, never the repo root:
rem the binaries in build\ itself (the install layout, so a dev Wind.exe finds WindTray.exe,
rem WindConfig.exe and ui\dist next to it the way an installed one does), and objects, compiler
rem PDBs and .res files in build\obj\<target>\. One object dir per target, because targets compile
rem the same sources with different defines (WIND_UIACCESS, WIND_TESTS), and the tray and settings
rem hosts each have their own main.cpp. The working directory stays the repo root, so the test
rem binary finds tests\fixtures and a self-test writes its files where it is started from.
set "BUILD=%ROOT%build"
set "OBJ=%BUILD%\obj"
if not exist "%BUILD%" mkdir "%BUILD%"

if /i "%1"=="test" goto :test
if /i "%1"=="check" goto :check
if /i "%1"=="uiaccess" goto :uiaccess
Expand All @@ -29,7 +41,7 @@ if /i "%1"=="installer" goto :installer
if /i "%1"=="tray" goto :tray

rem --- App build (normal: uiAccess=false, runs from anywhere) ----------------
call :core_exe "" Wind.manifest
call :core_exe "" Wind.manifest wind
if errorlevel 1 exit /b 1
call :tray_exe
exit /b %errorlevel%
Expand All @@ -39,20 +51,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
call :core_exe /DWIND_UIACCESS Wind.uiaccess.manifest wind_uiaccess
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.
rem %2 = the manifest to embed, %3 = its object dir under build\obj. Everything else (sources,
rem flags, libraries) is the same. Output: build\Wind.exe and build\Wind.pdb.
:core_exe
set "CORE_OBJ=%OBJ%\%~3"
if not exist "%CORE_OBJ%" mkdir "%CORE_OBJ%"
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"
rc /nologo /fo "%CORE_OBJ%\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 %~1 ^
src\*.cpp src\wind.res ^
/Fe:Wind.exe ^
/Fo"%CORE_OBJ%\\" /Fd"%CORE_OBJ%\\" ^
src\*.cpp "%CORE_OBJ%\wind.res" ^
/Fe:"%BUILD%\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:%~2 /SUBSYSTEM:WINDOWS ^
Expand All @@ -67,14 +83,15 @@ call :tray_exe
exit /b %errorlevel%

:tray_exe
rc /nologo /fo "%ROOT%src\tray_app\wind_tray.res" "%ROOT%src\tray_app\wind_tray.rc"
if not exist "%OBJ%\tray" mkdir "%OBJ%\tray"
rc /nologo /fo "%OBJ%\tray\wind_tray.res" "%ROOT%src\tray_app\wind_tray.rc"
if errorlevel 1 (echo [build] rc.exe failed for WindTray & exit /b 1)
rem Objects go to src\tray_app\ so the shared sources never overwrite Wind.exe's .obj files.
rem Own object dir (build\obj\tray) so the shared sources never overwrite Wind.exe's .obj files.
cl /nologo /std:c++17 /EHsc /O2 /W4 /Zi /DUNICODE /D_UNICODE ^
/Fo"%ROOT%src\tray_app\\" /Fd"%ROOT%WindTray.pdb" ^
/Fo"%OBJ%\tray\\" /Fd"%OBJ%\tray\\" ^
src\tray_app\*.cpp src\tray_items.cpp src\profiles.cpp src\config.cpp src\logging.cpp src\ini_edit.cpp ^
src\tray_app\wind_tray.res ^
/Fe:WindTray.exe ^
"%OBJ%\tray\wind_tray.res" ^
/Fe:"%BUILD%\WindTray.exe" ^
/link user32.lib shell32.lib gdi32.lib Dwmapi.lib Dbghelp.lib shlwapi.lib ole32.lib version.lib ^
advapi32.lib ntdll.lib ^
/MANIFEST:EMBED /MANIFESTUAC:NO /MANIFESTINPUT:Wind.manifest /SUBSYSTEM:WINDOWS ^
Expand All @@ -94,38 +111,52 @@ if exist "%ROOT%ui\package.json" (
if not exist dist\index.html (popd & echo [build] ui build produced no dist\index.html & exit /b 1)
popd
)
if not exist "%OBJ%\config" mkdir "%OBJ%\config"
rem Same app-icon resource as Wind.exe (rc.exe ships with the Windows SDK, on PATH via vcvars).
rc /nologo /fo "%ROOT%src\wind.res" "%ROOT%src\wind.rc"
rc /nologo /fo "%OBJ%\config\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 /DUNICODE /D_UNICODE ^
/I third_party\webview2\include ^
src\config_ui\main.cpp src\ini_edit.cpp src\profiles.cpp src\config.cpp src\logging.cpp src\wind.res ^
/Fe:WindConfig.exe ^
/Fo"%OBJ%\config\\" /Fd"%OBJ%\config\\" ^
src\config_ui\main.cpp src\ini_edit.cpp src\profiles.cpp src\config.cpp src\logging.cpp "%OBJ%\config\wind.res" ^
/Fe:"%BUILD%\WindConfig.exe" ^
/link third_party\webview2\x64\WebView2LoaderStatic.lib ^
user32.lib shell32.lib shlwapi.lib ole32.lib version.lib advapi32.lib ntdll.lib Dwmapi.lib /SUBSYSTEM:WINDOWS
exit /b %errorlevel%
if errorlevel 1 exit /b 1
rem WindConfig.exe loads its page from <exe dir>\ui\dist, so mirror the UI into build\ui\dist, the
rem install layout. ui\dist stays the copy the installer and tools\uiaccess_setup.ps1 ship.
rem robocopy exit codes below 8 mean success.
if exist "%ROOT%ui\dist\index.html" (
robocopy "%ROOT%ui\dist" "%BUILD%\ui\dist" /MIR /NJH /NJS /NFL /NDL /NP >nul
if errorlevel 8 (echo [build] copying ui\dist to build\ui\dist failed & exit /b 1)
)
exit /b 0

rem --- Test build (pure-logic sources only; no <windows.h>) -----------------
:test
rem /wd5285 silences a known doctest 2.4.11 header warning under MSVC /W4.
if not exist "%OBJ%\test" mkdir "%OBJ%\test"
cl /nologo /std:c++17 /EHsc /W4 /wd5285 /DWIND_TESTS /I third_party ^
/Fo"%OBJ%\test\\" /Fd"%OBJ%\test\\" ^
tests\*.cpp ^
src\transform.cpp src\zoom_controller.cpp src\config.cpp src\profiles.cpp src\cursor_mapper.cpp src\lock_detector.cpp src\cursor_lock.cpp src\crosshair.cpp src\ini_edit.cpp src\logging.cpp src\tray_items.cpp src\hitch_record.cpp src\render_params.cpp src\tray_app\tray_pin_logic.cpp ^
/Fe:wind_tests.exe
/Fe:"%BUILD%\wind_tests.exe"
if errorlevel 1 exit /b 1
"%ROOT%wind_tests.exe"
rem Runs from the repo root (the current directory): the tests find tests\fixtures from there.
"%BUILD%\wind_tests.exe"
exit /b %errorlevel%

rem --- Compile-only check (no link; verifies all sources compile) -----------
:check
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c src\*.cpp
for %%D in (core tray config) do if not exist "%OBJ%\check\%%D" mkdir "%OBJ%\check\%%D"
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\core\\" 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
cl /nologo /std:c++17 /EHsc /W4 /DUNICODE /D_UNICODE /c /Fo"%OBJ%\check\tray\\" 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
/Fo"%OBJ%\check\config\\" src\config_ui\main.cpp
exit /b %errorlevel%

rem --- Installer (needs NSIS; winget install NSIS.NSIS) ---------------------
Expand Down
2 changes: 1 addition & 1 deletion docs/VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ install/uninstall round trip (`tools\installer_check.ps1`).
- [ ] Upgrade while Wind runs zoomed: no "file in use" error, the OS cursor is visible afterwards. Upgrade with Settings open: WindConfig closes.
- [ ] Uninstall keeping settings: `magnifier.ini` survives. Uninstall removing settings: it does not.
- [ ] At 100% and 225% DPI the setup window is sharp and its hit targets line up.
- [ ] Without UIAccess Wind refuses to start: a plain `build.bat` Wind.exe (no `WIND_ALLOW_NO_UIACCESS`) shows the "Wind can't start" dialog telling you to run the installer again, then exits with no tray icon and no magnifier.
- [ ] Without UIAccess Wind refuses to start: a plain `build.bat` `build\Wind.exe` (no `WIND_ALLOW_NO_UIACCESS`) shows the "Wind can't start" dialog telling you to run the installer again, then exits with no tray icon and no magnifier.

## Desktop zoom
- [ ] First launch runs the guided setup; the chosen binds work.
Expand Down
3 changes: 2 additions & 1 deletion docs/architecture/04-render-engine.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,8 @@ monitor. Every style is required:
| `DwmSetWindowAttribute(DWMWA_EXCLUDED_FROM_PEEK)` | Keeps the view during a taskbar-thumbnail Aero Peek. |

Because the overlay is capture-excluded, external screenshots cannot see it. Verify from inside the
process: `WIND_SELFTEST=1 Wind.exe` writes `wind_selftest.png` (`RenderEngine::dumpFrame`).
process: `WIND_SELFTEST=1 build\Wind.exe` writes `wind_selftest.png` to its working directory
(`RenderEngine::dumpFrame`).

## Present: blt model only

Expand Down
2 changes: 1 addition & 1 deletion docs/architecture/08-config-profiles.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ flowchart LR
`wind::ResolveIniPath()` (`src/config_path.h`) probes whether the exe's directory is writable
(a sentinel file opened with `FILE_FLAG_DELETE_ON_CLOSE`).

- Writable (dev build): the ini sits next to the exe.
- Writable (dev build): the ini sits next to the exe, in `build\` (see [11](11-build-test-release.md)).
- Read-only (`C:\Program Files\Wind`): `%LOCALAPPDATA%\Wind\magnifier.ini`, seeded from a template
next to the exe if one exists, otherwise created by `LoadConfig` with a commented header.
- **Never hardcode `L"magnifier.ini"`.** Program Files is read-only for the non-admin runtime, and a
Expand Down
35 changes: 24 additions & 11 deletions docs/architecture/11-build-test-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,30 @@ deploy, the installer's elevation rules and the release workflows.

## build.bat targets

`build.bat` locates MSVC with vswhere, runs `vcvars64.bat` and dispatches on its first argument:
`build.bat` locates MSVC with vswhere, runs `vcvars64.bat` and dispatches on its first argument.
**Nothing is written to the repo root.** Binaries and their linker PDBs go to `build\`, which is
laid out like an install (`Wind.exe`, `WindTray.exe`, `WindConfig.exe` and `ui\dist` side by side,
so `ExeDir()` lookups work unchanged). Objects, compiler PDBs and `.res` files go to
`build\obj\<target>\` (`wind`, `wind_uiaccess`, `tray`, `config`, `test`, `check\*`): targets
compile the same sources with different defines, and the tray and settings hosts each have a
`main.cpp`. The working directory stays the repo root.

| Target | Output | What it is |
|---|---|---|
| (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`, `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` |
| `installer` | `dist\Wind-Setup-x64-<ver>.exe` | `makensis /WX installer\wind.nsi`, then `tools\installer_check.ps1` |
| (none) | `build\Wind.exe` + `build\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` | `build\wind_tests.exe` | The doctest binary over the pure sources; runs it from the repo root (it finds `tests\fixtures` there) and returns its exit code |
| `check` | – | Compile-only pass over `src\*.cpp`, `src\tray_app\*.cpp` and `src\config_ui\main.cpp`, objects in `build\obj\check\` |
| `uiaccess` | `build\Wind.exe` + `build\WindTray.exe` | `Wind.uiaccess.manifest` and `/DWIND_UIACCESS`; useful only signed and in Program Files. `WindTray.exe` always keeps the plain manifest |
| `tray` | `build\WindTray.exe` | The tray helper alone |
| `config` | `build\WindConfig.exe` | Builds the Svelte app to `ui/dist/`, then the WebView2 host against `third_party/webview2`, then mirrors `ui\dist` to `build\ui\dist` (the host loads `<exe dir>\ui\dist`) |
| `installer` | `dist\Wind-Setup-x64-<ver>.exe` | `makensis /WX installer\wind.nsi` (packs `build\*.exe` and `ui\dist`), then `tools\installer_check.ps1` |

**A dev build's runtime files live in `build\`.** `build\` is writable, so `ResolveIniPath()` and
`ResolveLogDir()` keep a dev build's `magnifier.ini`, `profiles\` and `logs\` next to
`build\Wind.exe`, as they did at the root before. Deleting `build\` deletes them too; an ini left at
the root by an older clone is no longer read (move it into `build\` to keep it). A self-test writes
`wind_selftest.png` and `wind_hdr_diag.txt` to its working directory, so
`WIND_SELFTEST=1 build\Wind.exe` from the repo root leaves them at the root (gitignored).

The installer treats NSIS warnings as errors: an unreferenced define in generated layout data is
how a page ends up wired to nothing.
Expand Down Expand Up @@ -74,8 +87,8 @@ locally trusted certificate and run from a secure location, in practice `C:\Prog
Without it, Wind reads `TokenUIAccess` at init and Auto keeps the desktop on Render.

`tools/uiaccess_setup.ps1` (elevated) stops Wind, builds `uiaccess` and `config`, creates or reuses
a self-signed "Wind Dev Test Cert", trusts it, signs all three exes (unsigned fresh builds trip a
Defender false positive) and copies them with `ui\dist` to Program Files. It does not deploy an
a self-signed "Wind Dev Test Cert", trusts it, signs all three exes in `build\` (unsigned fresh
builds trip a Defender false positive) and copies them with `ui\dist` to Program Files. It does not deploy an
ini. It logs to `tools\uiaccess_setup.log`; a good deploy ends with `status=Valid` and `DONE`.

- Pass an **absolute** `-File` path: the elevated process starts in System32.
Expand Down Expand Up @@ -145,7 +158,7 @@ environment-driven: `WIND_SIGN_THUMBPRINT`, or `WIND_SIGN_PFX` plus `WIND_SIGN_P
- With a certificate it signs the three exes and the installer, signing the payload
before makensis packs it (UIAccess is granted on `Wind.exe`'s own signature).
- Without one (CI today) it packs both variants: the ordinary build and the UIAccess build as
`WindUA.exe`. Setup signs `WindUA.exe` on each PC with a per-machine certificate whose private key
`build\WindUA.exe` (the `uiaccess` build's `build\Wind.exe`, renamed). Setup signs `WindUA.exe` on each PC with a per-machine certificate whose private key
it deletes at once (`installer/local-sign.ps1`). If that fails, setup keeps the ordinary build, says signing failed and does not start Wind: without UIAccess, Wind refuses to start ("Wind can't start", `src/uiaccess_gate.h`).
- **Never put the self-signed dev certificate in CI**: it is trusted by nobody and looks worse than
no signature.
Expand Down
Loading
Loading