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: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ and run it. It always points at the latest release.
- Adds the WebView2 runtime if Settings needs it.
- Settings, profiles and logs stay in `%LOCALAPPDATA%\Wind`, also after an uninstall unless you
choose otherwise.
- Works with screen readers: setup switches to plain, standard pages when a screen reader (NVDA,
JAWS or Narrator) or high contrast is on. To ask for them yourself, run `Wind-Setup-x64.exe /ACCESSIBLE`.

The installer is not signed, so SmartScreen warns on first run: choose **More info**, then **Run
anyway**. Some browsers and work computers block the download. Setup signs Wind locally for your PC,
Expand Down
2 changes: 2 additions & 0 deletions docs/VERIFICATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ 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.
- [ ] Accessible setup (`/ACCESSIBLE`, or with NVDA running): stock pages with a title bar; NVDA reads each page, Tab reaches every button and box, Space ticks the licence box, Install shows the log, and Finish starts an unelevated Wind only when "Open Wind now" is ticked.
- [ ] The picture's licence screen: Space ticks the box; Enter before ticking shows the hint message box.
- [ ] 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
Expand Down
6 changes: 6 additions & 0 deletions docs/architecture/11-build-test-release.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,12 @@ and local signing are described in `installer/README.md`; the design spec is
[../specs/2026-08-20-installer-design.md](../specs/2026-08-20-installer-design.md). There is no
install-location chooser, because UIAccess is granted only in a secure location.

The picture has no controls a screen reader or the keyboard can use, so setup also carries an
accessible plain mode of stock MUI pages (`installer/plain.nsh`, issue #513). `.onInit` picks it on
`/ACCESSIBLE`, a running NVDA, JAWS or Narrator, or high contrast, never under `/S` (not
`SPI_GETSCREENREADER`: other apps leave it set); both page
sets are listed and each skips itself when the other runs. Details in `installer/README.md`.

**Elevation rules** (`installer/app.nsh`):

1. **HKCU and `%LOCALAPPDATA%` belong to the wrong user under elevation.** An elevated process's
Expand Down
31 changes: 30 additions & 1 deletion installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@
Wind's installer is NSIS with a custom-drawn UI: a looping video with the screens composited over
it. NSIS cannot play video, so every tick it decodes a JPEG frame through GDI+, alpha-blends the
screen's overlay on top and copies the result into one static control; the same tick hit-tests the
pointer. There are no native buttons.
pointer. There are no native buttons, so setup also carries an accessible plain mode built from stock
pages (see [Accessible setup](#accessible-setup)).

Wind installs per-machine and elevated, and there is no install-location screen: UIAccess is granted
only to a signed binary in a secure location, so an install elsewhere could not get it, and Wind refuses to start without it.
Expand All @@ -18,6 +19,7 @@ The elevation rules (HKLM autostart, launching through `explorer.exe`, stopping
| `wind.nsi` | Entry point: metadata, page order, install and uninstall sections |
| `app.nsh` | Wind-specific logic: quitting a running Wind, WebView2, autostart, launching |
| `screens.nsh` | The five screens and what each click does |
| `plain.nsh` | The accessible plain mode: stock MUI pages for screen readers and the keyboard |
| `kit.nsh` | The frameless window: size, DPI, GDI+, unpacking |
| `video.nsh` | The player: decode, composite, hover, clicks, dragging |
| `over.html` | The foreground: type, buttons, caption. The only place copy lives |
Expand Down Expand Up @@ -69,6 +71,33 @@ and opens it through `explorer.exe`, so the viewer is not elevated (an elevated
`$PLUGINSDIR` to Administrators). A silent install skips the page; `LICENSE.txt` is installed next to
`Wind.exe` either way.

## Accessible setup

The picture is pixels: a screen reader finds nothing to name on it and the keyboard has nothing to
focus (issue #513). So `wind.nsi` lists two page sets and `.onInit` (`WIND_PICK_PLAIN` in `kit.nsh`)
picks one per run. The plain set is stock NSIS: MUI's welcome page, MUI's licence page with the full
`LICENSE` text and a real "I accept" checkbox, an nsDialogs page with the install folder as a label
and a "Start Wind when I sign in" checkbox, the stock install page with its status line and log
shown, and MUI's finish page with "Open Wind now" (launched through `explorer.exe`, like the picture)
and "Create desktop shortcut". NVDA, JAWS and Narrator read these, and Tab, Space and Enter work.

Setup runs plain when any of these holds:

- `/ACCESSIBLE` is on the command line (any case): `Wind-Setup-x64-<ver>.exe /ACCESSIBLE`.
- A screen reader is running: NVDA (its `wxWindowClassNR` / `NVDA` window, or `nvda.exe`), JAWS
(`jfw.exe`) or Narrator (`Narrator.exe`). The system flag `SPI_GETSCREENREADER` is deliberately
not used: other apps leave it set with no screen reader running.
- High contrast is on (the art ignores the user's colours).

A silent install (`/S`) never runs plain mode and behaves exactly as before. Plain mode skips
everything the picture needs: the frameless window, the media unpack, GDI+ and the timer, the hidden
log and the autoclose. Each plain MUI page has `windPlainOnly` as its PRE function, and each picture
creator aborts when `$Plain` is 1, so NSIS walks past the other set in both directions.

The picture itself answers one key: Space ticks the licence box, and pressing Enter on the licence
screen before ticking it shows a message box saying so and pointing at `/ACCESSIBLE`, rather than
doing nothing.

## Editing the overlay

Edit `over.html`, then regenerate both overlay sets straight into `media/<size>/o`:
Expand Down
50 changes: 50 additions & 0 deletions installer/kit.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,50 @@ Var ArtDir
Var Dialog
Var Gdip ; GDI+ token, started once and never stopped: setup outlives it
Var Font
Var Plain ; 1: the accessible stock pages (plain.nsh) instead of the picture

; ---- accessible plain mode (issue #513) ----------------------------------------------------
; The picture has no controls a screen reader can name and no keyboard operation, so setup
; also carries a plain set of stock MUI pages (plain.nsh) and picks one set at startup. Plain
; when any of these holds:
; /ACCESSIBLE on the command line (GetOptions is case-insensitive)
; a screen reader is running: NVDA's own window exists (the FindWindow nvda.exe uses to find
; a running copy), or tasklist shows nvda.exe, jfw.exe (JAWS) or Narrator.exe
; high contrast is on: the art is fixed colours and ignores the user's scheme
; Never under /S: a silent install shows no pages, and keeping $Plain at 0 there keeps every
; silent code path exactly as it was.
!macro WIND_PICK_PLAIN
StrCpy $Plain 0
${IfNot} ${Silent}
${GetParameters} $0
ClearErrors
${GetOptions} $0 "/ACCESSIBLE" $1
${IfNot} ${Errors}
StrCpy $Plain 1
${EndIf}
; A running screen reader. Not SPI_GETSCREENREADER: other apps leave that flag set with no
; screen reader running, which would hide the animated UI from sighted users (#513).
FindWindow $1 "wxWindowClassNR" "NVDA"
${If} $1 <> 0
StrCpy $Plain 1
${Else}
nsExec::Exec 'cmd /c tasklist /NH | findstr /I /B "nvda.exe jfw.exe Narrator.exe"'
Pop $1
${If} $1 == 0
StrCpy $Plain 1
${EndIf}
${EndIf}
; HIGHCONTRAST { cbSize, dwFlags, lpszDefaultScheme }, 12 bytes in this 32-bit process
System::Call '*(i 12, i 0, p 0) p .r0'
System::Call 'user32::SystemParametersInfoW(i 0x42, i 12, p r0, i 0)'
System::Call '*$0(i, i .r1)'
System::Free $0
IntOp $1 $1 & 1 ; HCF_HIGHCONTRASTON
${If} $1 <> 0
StrCpy $Plain 1
${EndIf}
${EndIf}
!macroend

!macro HIDE_CHILD ID
GetDlgItem $0 $HWNDPARENT ${ID}
Expand All @@ -49,6 +93,12 @@ Var Font
!define MUI_CUSTOMFUNCTION_GUIINIT windGuiInit

Function windGuiInit
; Plain mode keeps NSIS's own window as it is: frame, caption, header and buttons are what
; a screen reader and the keyboard use. None of the picture's setup below is needed.
${If} $Plain = 1
Return
${EndIf}

; --- what are we drawing at? ------------------------------------------------------------
System::Call 'user32::GetDpiForSystem() i .r0 ? e'
Pop $1
Expand Down
92 changes: 92 additions & 0 deletions installer/plain.nsh
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
;
; Wind setup, the accessible plain pages (issue #513).
;
; The picture (kit.nsh, video.nsh, screens.nsh) is pixels: a screen reader finds nothing to name
; on it and the keyboard has nothing to focus. These pages are the stock alternative, real
; dialog controls with text, tab order and default buttons: MUI's welcome, licence (with a real
; checkbox), install and finish pages, plus one nsDialogs page for the autostart choice. They
; offer exactly what the picture offers. kit.nsh's WIND_PICK_PLAIN decides which set runs;
; wind.nsi lists both, each set skipping itself when the other is chosen.
;
; Included after the page list, because the finish page's show function reads
; $mui.FinishPage.Run, which MUI only declares when that page is inserted.
;

Var PlainAutostart ; the setup page's checkbox

; PRE for every MUI page that belongs to plain mode only: skip it when the picture runs.
Function windPlainOnly
${If} $Plain <> 1
Abort
${EndIf}
FunctionEnd

; ---- setup: where Wind goes, and autostart -----------------------------------
; The path is shown, not chosen, for the reason screens.nsh gives: UIAccess is only granted in a
; secure location.
Function windPlainSetupCreate
${If} ${Silent}
${OrIf} $Plain <> 1
Abort
${EndIf}
!insertmacro MUI_HEADER_TEXT "Where Wind goes" "Wind installs to Program Files."
nsDialogs::Create 1018
Pop $0
${If} $0 == error
Abort
${EndIf}
${NSD_CreateLabel} 0 0 100% 30u "Wind installs for everyone on this PC. Windows only grants the access its shortcuts and desktop zoom need to apps in a protected folder, so the folder cannot be changed."
Pop $0
${NSD_CreateLabel} 0 36u 100% 12u "Install folder: $INSTDIR"
Pop $0
${NSD_CreateCheckbox} 0 60u 100% 12u "&Start Wind when I sign in"
Pop $PlainAutostart
${If} $WantAutostart = 1
${NSD_Check} $PlainAutostart
${EndIf}
; Back runs no leave function, so the box writes its choice as it changes, or Back and
; Next again would show the old state.
${NSD_OnClick} $PlainAutostart windPlainSetupClick
; NSIS only names the button Install on its own pages; this one starts the install too
GetDlgItem $0 $HWNDPARENT 1
SendMessage $0 ${WM_SETTEXT} 0 "STR:$(^InstallBtn)"
; focus on the one choice, so a screen reader announces it as the page opens
${NSD_SetFocus} $PlainAutostart
nsDialogs::Show
FunctionEnd

; The box's click handler: nsDialogs pushes the control, so it is popped here.
Function windPlainSetupClick
Pop $0
Call windPlainSetupLeave
FunctionEnd

; Read before the section runs, which applies it (WIND_APPLY_AUTOSTART), as on the picture.
Function windPlainSetupLeave
${NSD_GetState} $PlainAutostart $0
${If} $0 = ${BST_CHECKED}
StrCpy $WantAutostart 1
${Else}
StrCpy $WantAutostart 0
${EndIf}
FunctionEnd

; ---- finish ------------------------------------------------------------------
; "Open Wind now" starts ticked, unless local signing failed: that Wind has no UIAccess and
; cannot start (the section has already said so and cleared $RunAfter), as on the picture.
Function windPlainFinishShow
${If} $RunAfter <> 1
${NSD_Uncheck} $mui.FinishPage.Run
${EndIf}
FunctionEnd

; MUI calls these on Finish when their box is ticked. Through explorer.exe, never Exec, so
; Wind does not inherit setup's admin token (app.nsh).
Function windPlainRun
!insertmacro WIND_LAUNCH_DEELEVATED
FunctionEnd

Function windPlainDesktop
SetShellVarContext all
CreateShortcut "$DESKTOP\Wind.lnk" "$INSTDIR\Wind.exe"
FunctionEnd
19 changes: 19 additions & 0 deletions installer/screens.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
; differs is $Screen, which picks the overlay and decides what the clicks mean. There is not a
; single button control in this file.
;
; In plain mode (kit.nsh, plain.nsh, issue #513) every page here skips itself.
;
; Prism has a "where it goes" page with a Browse button. Wind does not, and the omission is
; deliberate: UIAccess is only granted to a signed binary in a secure location, so an install
; to D:\Apps\Wind would silently disable the elevated-window shortcuts and the desktop
Expand All @@ -19,6 +21,11 @@ Function windPageStart
${If} ${Silent}
Abort
${EndIf}
; Plain mode (kit.nsh, plain.nsh) shows the stock pages instead; skipping here skips every
; picture screen that runs through this.
${If} $Plain = 1
Abort
${EndIf}
; Back runs no leave function, so the page we came from may still hold its canvas: the
; bitmaps, the DC and the GDI+ images, one of which keeps its PNG locked. Free it here so
; nothing leaks and the next unpack can write (issue #263). Idempotent after a Next.
Expand Down Expand Up @@ -47,6 +54,7 @@ FunctionEnd
; ---- 1. welcome --------------------------------------------------------------
Function windWelcomeCreate
${If} ${Silent}
${OrIf} $Plain = 1
Abort
${EndIf}
; once per run: Back returns here, and the art is already unpacked
Expand All @@ -70,6 +78,11 @@ Function windLicenceCreate
${EndIf}
StrCpy $Screen 4
Call windPageStart
; NSIS leaves the keyboard focus on its Next button, hidden but still live. Space ticks the
; box (WindInput), and the same Space would then click that Next on key-up and leave the
; page the moment the box is ticked (or show the hint after an untick). The canvas is a
; static, which ignores keys; Enter still reaches Next through the dialog's default button.
System::Call 'user32::SetFocus(p $Canvas)'
nsDialogs::Show
FunctionEnd

Expand All @@ -81,6 +94,10 @@ Function windLicenceLeave
; staying on this page: undo what the click set up for leaving it
StrCpy $Leaving 0
!insertmacro WIND_THAW
; Not silently (issue #513): someone on the keyboard pressed Enter and got nothing. A
; message box is a real dialog, so it is also the one thing on this screen a screen
; reader can read. Space ticks the box (WindInput).
MessageBox MB_ICONINFORMATION|MB_OK "To continue, accept the licence agreement: press Space or click the box.$\r$\n$\r$\nFor a setup that works with a screen reader and the keyboard, close Setup and run it again with /ACCESSIBLE."
Abort
${EndIf}
Call windPageLeave
Expand Down Expand Up @@ -125,7 +142,9 @@ FunctionEnd
; script while files are being written. This screen draws one frame and hands the motion to
; the progress bar, which Windows paints for us.
Function windCopyShow
; Plain mode keeps the stock page: status line, log and buttons, all readable.
${If} ${Silent}
${OrIf} $Plain = 1
Return
${EndIf}
FindWindow $R4 "#32770" "" $HWNDPARENT
Expand Down
24 changes: 24 additions & 0 deletions installer/video.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ Var OverImg
Var BackImg ; the shade and caption scrim, loaded once per page
Var Hot ; control under the pointer
Var WasDown
Var SpaceDown ; Space's state last tick, so one press ticks the licence box once
Var Leaving ; an action is posted; this page is finished
Var BoxOn ; the two states of a checkbox, stamped rather than baked
Var BoxOff
Expand Down Expand Up @@ -161,6 +162,7 @@ Function WindCanvas
; assume the button may still be down from the click that brought us here, so the next page
; needs a real release before it accepts anything
StrCpy $WasDown 1
StrCpy $SpaceDown 1
FunctionEnd

Function WindCanvasFree
Expand Down Expand Up @@ -369,6 +371,28 @@ Function WindInput
${Else}
StrCpy $WasDown 1
${EndIf}

; The one key this picture answers (issue #513): Space ticks the licence box, so the
; keyboard is not stuck on the one screen that cannot be passed without it. Edge-triggered
; on the high bit like the click, and only while setup is the foreground window ($2).
${If} $Screen = 4
System::Call 'user32::GetAsyncKeyState(i 0x20) i .r1'
IntOp $3 $1 & 0x8000
${If} $3 <> 0
${AndIf} $SpaceDown = 0
${AndIf} $2 = $HWNDPARENT
${If} $Accepted = 1
StrCpy $Accepted 0
${Else}
StrCpy $Accepted 1
${EndIf}
${EndIf}
${If} $3 = 0
StrCpy $SpaceDown 0
${Else}
StrCpy $SpaceDown 1
${EndIf}
${EndIf}
FunctionEnd

; Post, never send. SendMessage would run the whole page change inside this callback, and NSIS
Expand Down
Loading
Loading