Skip to content

Add opt-in global brightness shortcuts - #2

Draft
LinkaiQi wants to merge 5 commits into
mainfrom
linkaiqi-global-brightness-hotkeys
Draft

Add opt-in global brightness shortcuts#2
LinkaiQi wants to merge 5 commits into
mainfrom
linkaiqi-global-brightness-hotkeys

Conversation

@LinkaiQi

@LinkaiQi LinkaiQi commented Aug 3, 2026

Copy link
Copy Markdown
Owner

Adds two system-wide keyboard shortcuts that step every connected Apple display's brightness by 10%, without stealing focus from whatever you're doing. Along the way, Settings and About become one window.

Action Default
Increase brightness by 10% Win + Ctrl +
Decrease brightness by 10% Win + Ctrl +

Off by default — nothing is taken over until you turn it on in tray → Settings.

Why these defaults

Win+Ctrl+↑/↓ is one of the few arrow combinations Windows leaves free. Ruled out along the way: Win+↑/↓ (snap), Win+Shift+↑/↓ (stretch across monitors), Win+Alt+↑/↓ (Win11 22H2 half-snap), Ctrl+Alt+arrows (Intel GPU rotation), Ctrl+Shift+arrows (text selection), Win + Plus/Minus (Magnifier). Only Win+Ctrl+←/→ is taken, by virtual desktops.

Both combinations are user-configurable anyway, and a collision is reported rather than swallowed.

How the shortcuts work

  • RegisterHotKey binds to the main window's HWND — the same window that already receives WM_DEVICECHANGE, so the shortcuts keep working while the window is hidden in the tray. WM_HOTKEY is dispatched through an HwndSource hook, exactly like the device-change watch.
  • MOD_NOREPEAT is set, so one press is one 10% step. Without it a held key auto-repeats ~30×/sec, crossing the whole range in about a third of a second.
  • Registration failures surface in the UI. ERROR_HOTKEY_ALREADY_REGISTERED (1409) becomes an inline warning naming the combination; anything else is logged with its Win32 error.
  • Recording a shortcut suspends the live ones, otherwise Windows would deliver the combination as WM_HOTKEY and it would never reach the settings window as a key press.
  • Settings persist to %LOCALAPPDATA%\NitTray\settings.json, beside the diagnostic log. Load/Save never throw.
  • A click-through overlay near the bottom of the primary screen reports the new level, because with the window in the tray there's no other feedback. It never activates and fades out on its own.

One settings window instead of two

Two separate windows for a few hundred pixels of content — each with its own title bar — was clutter. SettingsWindow now hosts a NavigationView rail with Shortcuts in MenuItems and About in FooterMenuItems, mirroring where Windows 11 Settings puts its own About page. AboutWindow is gone; both tray entries and both footer links still work, they just land on different pages of the same window.

About used to be centred while the settings body was a left-aligned form. Putting them one click apart exposed the clash, so About is re-laid-out into the same left-aligned cards.

Notes for reviewers

Two non-obvious framework behaviours drove the design, both verified against source rather than assumed:

  • Keyboard.Modifiers never reports the Windows key — KeyboardDevice.cs only computes Alt/Ctrl/Shift. The recorder probes Key.LWin/Key.RWin directly; without that, Win+Ctrl+Up silently records as Ctrl+Up.
  • NavigationView constructs pages itself, so each page sets its own DataContext and navigation passes no dataContextNavigationViewActivator only overwrites DataContext when handed a non-null one.

That second point makes SettingsViewModel a singleton on App, where it used to be rebuilt on every open. Its constructor was the only thing that ever surfaced a registration collision, and CancelCapture blanked the status message unconditionally — so backing out of a recording erased the very warning that prompted it. It now reports the live registration state and refreshes on open.

Known limitation, documented in the UI: Windows doesn't deliver shortcuts to ordinary apps while an elevated window has focus.

⚠️ Not yet tested on Windows. Builds clean (Release + Debug, 0 warnings/errors) and the XAML compiler validates every property name, but no runtime path has been exercised. Worth checking before merge: shortcuts fire while the window is hidden in the tray; the recorder captures Win+Ctrl+↑ intact; a deliberate collision shows the warning; the nav window opens on the right page from each entry point; and the overlay lands correctly on a scaled or multi-monitor setup.

Copilot AI added 3 commits August 2, 2026 17:34
Step every connected Apple display by 1% from any app. Shortcuts are
registered with RegisterHotKey against the main window's HWND — the same
window that already receives WM_DEVICECHANGE — so they keep working while
NitTray sits in the tray.

- Defaults to Win+Ctrl+Up / Win+Ctrl+Down, the least contended arrow
  combination Windows leaves unassigned, and they are configurable.
- Off until enabled, persisted to %LOCALAPPDATA%\NitTray\settings.json.
- MOD_NOREPEAT is not set so holding a key ramps; DisplayViewModel already
  coalesces the writes.
- A combination another app owns (ERROR_HOTKEY_ALREADY_REGISTERED) is
  reported inline in the new Settings window instead of failing silently,
  and live registrations are suspended while recording a new one.
- A click-through, never-activated overlay reports the new level, since the
  window is normally hidden.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
WPF's Keyboard.Modifiers only reports Alt, Ctrl, and Shift — it never sets
ModifierKeys.Windows — so the recorder silently downgraded Win+Ctrl+Up to
plain Ctrl+Up and registered that instead, without telling the user. Probe
Key.LWin/Key.RWin directly.

Also require Ctrl, Alt, or Win in a binding: Shift alone would have taken
over ordinary text-selection chords system-wide.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
One press is now one 10% step, so ten presses covers the full range. Register
the shortcuts with MOD_NOREPEAT to match: a held key auto-repeats about 30
times a second, which at this step size would cross the whole range in roughly
a third of a second.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Copilot AI added 2 commits August 4, 2026 12:58
Two windows for a few hundred pixels of content was clutter, and each one
carried its own title bar. Settings now hosts a NavigationView rail with
Shortcuts in MenuItems and About in FooterMenuItems, which is where Windows 11
Settings puts its own About page. Both tray items and both footer links still
work; they just land on different pages of one window.

About was centred while the settings body was a left-aligned form, and putting
them a click apart exposed the clash, so About is re-laid-out into the same
left-aligned cards.

NavigationView constructs pages itself, so each page sets its own DataContext
and navigation deliberately passes no dataContext — WPF-UI's activator only
overwrites DataContext when handed a non-null one. That makes SettingsViewModel
a singleton on App, which it wasn't before: it used to be rebuilt on every open,
and its constructor was the only thing that ever surfaced a registration
collision. CancelCapture also blanked the message unconditionally, so backing
out of a recording erased the very warning that prompted it. Reporting the live
registration state instead, and refreshing on open, keeps the InfoBar honest.

The key recorder stays on the window rather than the page so it fires wherever
focus sits, and capture is now cancelled on page change as well as on
deactivation and close.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants