Skip to content

feat(input): --alt-q-quit delivers Option+Q as Cmd+Q - #200

Open
antonmos wants to merge 3 commits into
clintcan:mainfrom
antonmos:claude/alt-q-close-apps-switch-cf7eef
Open

antonmos wants to merge 3 commits into
clintcan:mainfrom
antonmos:claude/alt-q-close-apps-switch-cf7eef

Conversation

@antonmos

@antonmos antonmos commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

What

A new opt-in switch, --alt-q-quit (config ALT_Q_QUIT=1, plus an Option+Q quits apps toggle in the menu-bar Settings → App switching), that delivers Option+Q (Alt+Q from a Windows client) to the focused app as Cmd+Q.

Why

Quitting a Mac app from a Windows client currently needs Win+Q. mstsc swallows Win-combos when windowed ("Apply Windows key combinations"), and the macOS Windows App forwards Cmd only as a tap, so there was no reliable way to quit. This is the Option-key counterpart to --alt-tab-switch / --alt-backtick-switch.

How

  • Caught in try_symbolic_hotkey (src/input.rs), next to the Option+Tab/Option+` triggers, so it runs before the Ctrl→Cmd remap and layout translation.
  • Posts a full Cmd+Q tap with the same FlagsChanged-on-both-event-sources pattern as post_ctrl_as_cmd, then restores the real held-Option state. The Q key-up is swallowed via consumed_keys.
  • Plain Option only. Option+Shift+Q is deliberately not turned into Cmd+Shift+Q (log out), and Cmd/Ctrl combos pass through unchanged. The gate is a pure function, should_alt_q_quit, covered by an exhaustive decision-table test (alt_q_quit_decision_table).
  • Off by default. With the switch off, Option+Q reaches apps as a normal key (œ on US layouts). With it on, Option+Q is lost in every app, including a terminal that uses Option as Meta.

Q in the switcher. With Cmd+Tab (or Option+Tab under --alt-tab-switch) still held, Q quits the highlighted app, as in the native switcher, rather than whichever app is frontmost. The app is removed from the switcher's list (and the HUD), asked to quit via NSRunningApplication.terminate (a normal quit, so save prompts still appear), and the switcher stays open on the next app. Quitting the last app ends the switcher session. This needs no flag, since the session only exists while the switcher is held. The index logic is a pure function with a test (switcher_quit_moves_selection_to_the_next_app).

Docs: docs/cli.md, docs/configuration.md, docs/features.md, README, packaging/config.env.example.

Testing

  • cargo build clean; cargo test coord_tests passes, including the new decision-table test.
  • Live-verified on a real RDP client against the installed /Applications/macrdp.app: Alt+Q quits the focused app, and Q pressed in the Alt+Tab switcher quits the highlighted app (log: app switcher: quit the selected app pid=… sent=true).
  • The Settings toggle wasn't compiled locally: swift build in gui/ fails on this machine's Command Line Tools (SwiftUI @State macro plugin not found), with or without this change. The change there is one Toggle line that matches its neighbours.

🤖 Generated with Claude Code

antonmos and others added 2 commits October 10, 2026 19:47
Lets a Windows client quit the focused Mac app with Alt+Q, no Windows key
needed (mstsc gates Win-combos when windowed; the macOS Windows App sends
Cmd only as a tap). Off by default; config ALT_Q_QUIT=1, plus a Settings
toggle in the menu-bar controller.

Plain Option only: Option+Shift+Q is deliberately NOT turned into
Cmd+Shift+Q (log out); Cmd/Ctrl combos pass through. The gate is a pure,
unit-tested decision table (should_alt_q_quit). The tap reuses the
Ctrl->Cmd remap's FlagsChanged-on-both-sources pattern and restores the
held Option afterwards; the Q key-up is swallowed via consumed_keys.

Live-verified on a real client: Alt+Q quits the focused app.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Like the native Cmd+Tab switcher: with Cmd+Tab (or Option+Tab under
--alt-tab-switch) still held, Q quits the selected app instead of the
frontmost one. The app is dropped from the frozen snapshot (and the HUD),
asked to quit via NSRunningApplication.terminate (a normal quit, so
save prompts still appear), and the selection moves to the next app,
which is activated like a Tab step. Quitting the last app ends the
session. No flag: a session only exists while the switcher is held.

Previously Q here reached the frontmost app (racing the per-step AX
activation) and left the quit app in the switcher's list.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
@clintcan

Copy link
Copy Markdown
Owner

Thanks @antonmos. This now conflicts with main in six files (src/input.rs, src/main.rs, README, docs/configuration.md, docs/features.md, packaging/config.env.example). Since it was opened, #192, #208, #209, #210 and #212 have landed and touch the same places, mostly the flag lists and try_symbolic_hotkey.

#197 changes the same switcher code, so I'll review that one first. Could you rebase this one once #197 is settled? That way you only resolve the conflicts once. I'll review it after the rebase.

…ps-switch-cf7eef

* origin/main: (21 commits)
  chore(deps): bump libz-sys 1.1.28 -> 1.1.31 (RUSTSEC-2026-0418) (clintcan#243)
  test(auth_guard): serialize the audit-capturing tests with the ones that emit audit events (clintcan#239)
  docs(readme): explain controller setup and connecting after a reboot (clintcan#238)
  fix(stats): count shipped EGFX frames and clear them when the client leaves (clintcan#218)
  fix(clipboard): keep polling the pasteboard while a format list awaits its answer (clintcan#217)
  fix(input): close the app switcher when stale modifiers are cleared (clintcan#234)
  fix(auth_guard): don't count failures after authentication toward lockout (clintcan#233)
  fix(keyboard_layout): serialize TIS calls so concurrent callers can't abort the process (clintcan#227)
  chore(deps): bump zstd-safe to 8.2 in the vendored ironrdp-server (clintcan#224)
  fix(server): read input while a write blocks, and disable Nagle (clintcan#222)
  fix(capture): apply --max-client-size to the connect-time size too (clintcan#216)
  feat: read the password from a file with --password-file (clintcan#212)
  fix(input): drop the scroll backlog the 5-line cap leaves behind (clintcan#210)
  fix(input): post each modifier change once (clintcan#208)
  docs: fold hard-won dev lessons into CLAUDE.md and the topic docs (clintcan#202)
  fix(capture): mirror the main display on a multi-monitor Mac (clintcan#215)
  docs(readme): add the Homebrew cask install (clintcan#214)
  feat(input): add --alt-space-spotlight (clintcan#209)
  perf(capture): retain the flush frame's pixel buffer instead of copying it (clintcan#203)
  test(input): pure resync_plan + tests for the connection-edge reset (clintcan#192)
  ...

# Conflicts:
#	README.md
#	docs/configuration.md
#	docs/features.md
#	packaging/config.env.example
#	src/input.rs
#	src/main.rs
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