WindowDeck Keyboard binding conflict authority Techlosoft - Workspace Center WHAT IT IS ========== WindowDeck answers the question a team of developers eventually has to answer out loud: "whose Ctrl+Shift+P is it?" You hand it everyone's hotkey binding sets. It normalises every chord in every file to one canonical spelling, then finds every conflict: - two people binding the same chord to different actions - the same action bound to different chords on different machines - multi-chord sequences where one binding is a strict prefix of another - collisions that are invisible in the files and only appear after normalisation, because one person typed ^⇧P and another typed ctrl-shift-p - bindings that land on a chord the operating system has already claimed Then it merges everything into one conflict-free deck with a documented precedence, explains every single decision it made, and proposes genuinely free chords for the bindings it had to drop. The merge is order-independent. Merging the same sets in a different order produces a byte-identical deck; that property is enforced by a test that tries every permutation of the inputs. Its sibling WorkspaceForge computes window GEOMETRY - sizes, positions, monitors, layouts. WindowDeck owns BINDINGS and nothing else. The two do not overlap and do not share a file format. INSTALL ======= Pre-built binaries are in dist/. There is nothing to install - copy the one for your platform anywhere on your PATH and run it. dist/windowdeck-linux-amd64 Linux, x86-64 dist/windowdeck-darwin-arm64 macOS, Apple Silicon dist/windowdeck-darwin-amd64 macOS, Intel dist/windowdeck-windows-amd64.exe Windows, x86-64 On macOS and Linux you may need to mark it executable: chmod +x windowdeck-linux-amd64 To build from source you need Go 1.24 or newer. There are no dependencies of any kind, so no network access is required: go build -o windowdeck . go test ./... COMMANDS ======== windowdeck check --set [--set ...] [--base ] [--strict] [--suggest N] [--json] windowdeck merge --set [...] [--base ] --out [--force] [--suggest N] [--json] windowdeck explain --set [...] [--base ] [--json] windowdeck export --set [...] [--base ] --format json|csv|md [--out ] [--force] windowdeck help | -h | --help check Loads every set, normalises every chord, and prints every conflict it can find, sorted. Exits 2 if any error-severity conflict exists, so it drops straight into CI. Warnings alone exit 0 unless you pass --strict. merge Resolves the conflicts and writes ONE deck to a NEW file. It refuses to write over any file it just read, and refuses to replace an existing output file unless you pass --force. Every kept, dropped and duplicate binding gets a written explanation. explain Everything known about one chord: who binds it in which set, how each file spells it, what Windows and macOS do with it, which longer sequences it is a prefix of, and who ended up owning it in the merged deck. The chord may be written in any accepted notation. export Renders the merged deck as json, csv or md. Writes to stdout unless you give it --out. FLAGS --set A binding-set file. Repeat for each person. Bare positional arguments are treated as sets too. --base The team standard. See PRECEDENCE below. --out Where to write. Required by merge. --format export only: json, csv or md. --ledger Append one JSON-lines audit record for this run. --suggest Free replacement chords to propose per unresolved conflict. Default 3. 0 disables suggestions. --strict check only: warnings fail the run too. --force Allow --out to replace an existing (non-input) file. --json Machine-readable output. Available on every command. Short forms -s, -b, -o, -l, -f and -n are accepted for --set, --base, --out, --ledger, --format and --suggest. Flags may appear before or after positional arguments; either order works. CHORD NOTATION ============== WindowDeck accepts the notations people actually write and normalises all of them to one canonical form. what you write canonical form ------------------------------ --------------------- Ctrl+Shift+P Ctrl+Shift+P ctrl-shift-p Ctrl+Shift+P SHIFT+CTRL+P Ctrl+Shift+P ^⇧P (caret, U+21E7) Ctrl+Shift+P ⌃⇧P (U+2303 U+21E7) Ctrl+Shift+P Cmd+Opt+4 Alt+Meta+4 ⌘⇧4 (U+2318 U+21E7) Shift+Meta+4 Super+Space / Win+Space Meta+Space F13 F13 Ctrl+Alt+NumpadAdd Ctrl+Alt+NumpadAdd ctrl-alt-kp_add Ctrl+Alt+NumpadAdd Ctrl++ Ctrl+Plus Ctrl+- Ctrl+Minus Ctrl+K Ctrl+S Ctrl+K Ctrl+S (a two-chord sequence) MODIFIER ALIASES Ctrl ctrl, control, ctl, ctrl_l, ctrl_r, U+2303, ^ Alt alt, opt, option, altgr, alt_l, alt_r, U+2325, U+2387 Shift shift, shft, shift_l, shift_r, U+21E7 Meta meta, cmd, command, super, win, windows, hyper, U+2318, U+229E Cmd, Super, Win and Meta are ONE modifier. That is deliberate: the physical key in that position is the same key, and a team with mixed machines needs Cmd+Shift+4 and Win+Shift+4 to collide, not to pass each other silently. CANONICAL ORDER Ctrl, Alt, Shift, Meta - the order macOS itself uses when it renders a chord (U+2303 U+2325 U+21E7 U+2318). Input order is irrelevant: Shift+Ctrl+P and Ctrl+Shift+P are the same chord. SEPARATORS Both + and - separate a chord's parts. A separator that begins a token is the key itself, which is how Ctrl++ means Ctrl plus the plus key and Ctrl+- means Ctrl plus the minus key. SEQUENCES Chords in a sequence are separated by WHITESPACE: "Ctrl+K Ctrl+S". A consequence is that you may not put spaces around the + inside one chord; "Ctrl + Shift + P" is rejected, not silently read as three chords. REJECTIONS A malformed chord is refused with an error that names the offending token: chord "Ctrl+Frobnicate": unknown key "Frobnicate" chord "Ctrl+Zork+P": unknown modifier "Zork" chord "Ctrl+Shift": "Shift" is a modifier, not a key - a chord must end in a key chord "Ctrl+Ctrl+P": modifier "Ctrl" is repeated chord "Ctrl+": separator "+" at the end with no key after it Key names come from a curated table (letters, digits, F1-F24, the numpad, the navigation cluster, the punctuation keys). A key that is not in the table is rejected by name rather than silently accepted, so a typo can never quietly become its own binding. BINDING SET FILE FORMAT ======================= One JSON object per person. Unknown fields are rejected, so a typo in a field name is an error rather than a silently ignored binding. { "set": "alice", "platform": "macos", "bindings": [ {"action": "command.palette", "chord": "^⇧P"}, {"action": "pane.split.right", "chord": "Cmd+Opt+D", "override": true, "note": "Ctrl+Alt is taken by her IME"}, {"action": "workspace.save", "chord": "Ctrl+K Ctrl+S"} ] } set The set name. Defaults to the file's base name. Names must be unique across the run, because merge ties break on them. platform Optional and informational: windows, macos, linux or any. action A stable identifier for what the binding does. Required. An action may appear only once per file. chord Any accepted notation. Required. override Optional. Marks this binding as beating the team standard. note Optional free text, carried through into the deck and exports. PRECEDENCE - HOW MERGE DECIDES ============================== Every candidate binding is ranked, then processed in a fixed order. The rank depends ONLY on the binding's own properties, never on the order the files appeared on the command line. That is what makes the merge order-independent. rank 0 a binding marked "override": true rank 1 a binding from the --base team standard rank 2 an ordinary personal-set binding Within a rank, ties break on set name, then action, then chord - all alphabetical. So when two personal sets claim one chord for different actions and neither is marked override, the set whose NAME sorts first wins. It is an arbitrary rule, but it is a stated, stable, reproducible arbitrary rule. Bindings are then taken in that order and accepted unless: the chord is already bound to a different action -> dropped the action is already bound to a different chord -> dropped the sequence is a strict prefix of a kept binding, or a kept binding is a strict prefix of it -> dropped the chord AND action are already present -> noted as an identical duplicate Every one of those outcomes is written into the deck's resolutions list in plain English, naming what was kept, what was dropped, which file it came from and why: kept command.palette -> Ctrl+Shift+P from team-standard (team standard), dropped build.run -> Ctrl+Shift+P from bob.json because the chord is already bound to a different action WHY OVERRIDE OUTRANKS THE BASE The team standard is the default, not a straitjacket. A person on a different OS or a different keyboard layout has a legitimate reason to move one binding, and the honest way to record that is an explicit flag in their own file that shows up in the audit - not a silent local edit nobody can see. CONFLICT KINDS AND SEVERITIES ============================= duplicate-chord error One chord, two actions, spelled identically in every file. alias-collision error One chord, two actions, spelled DIFFERENTLY - the collision only exists after normalisation. These are the ones code review misses. prefix-sequence error One binding is a strict prefix of another, so the shorter one cannot resolve without a timeout and one of the two will never fire. divergent-action warning One action, different chords in different sets. Not broken, but muscle memory will not transfer between machines. reserved-chord warning The chord collides with the built-in Windows or macOS reserved table. Reported with the OS that claims it, what it does there, and the published source for the claim. `check` exits 2 when any error exists. Warnings alone exit 0 unless --strict. THE RESERVED TABLES =================== Two tables are compiled into the binary, one for Windows and one for macOS. Each entry carries the chord, what the OS does with it, and a source citation (Microsoft's "Keyboard shortcuts in Windows"; Apple's "Mac keyboard shortcuts"). Examples: Windows Ctrl+Alt+Delete open the security screen - an application can never intercept this one Win+L lock the workstation Alt+Tab switch between open windows Alt+F4 close the active window Win+Space switch the input language F11 toggle full screen in Explorer and browsers macOS Cmd+Space open Spotlight Cmd+Tab switch applications Cmd+Q / Cmd+W quit / close Cmd+Shift+4 capture a selected area to a screenshot Ctrl+Cmd+Q lock the screen F11 show the desktop A chord may be reserved on both, with different meanings, and both are reported. Chords inside a multi-chord sequence are checked individually and the position in the sequence is reported. SUGGESTIONS =========== For every conflict it cannot resolve, WindowDeck proposes replacement chords. A proposal is only offered if it is: - not used by ANY loaded set, in any position of any sequence - not the first chord of any existing sequence (which would create a new prefix conflict) - not already in the merged deck - not reserved on Windows AND not reserved on macOS - not modifier-free The search is ordered so the suggestions stay close to what the author wanted: first the same key with different modifiers, then the same modifiers with a different key, then the rest of the space. The order is fixed, so the same conflict always produces the same proposals. QUICK START =========== # what is broken? windowdeck check --set alice.json --set bob.json --base team.json # fix it, and write the result somewhere new windowdeck merge --set alice.json --set bob.json --base team.json \ --out deck.json --ledger audit.jsonl # who on earth is using this chord? windowdeck explain "Ctrl+Shift+P" --set alice.json --set bob.json # publish it windowdeck export --set alice.json --base team.json --format md --out DECK.md # in CI windowdeck check --set *.json --base team.json --strict --json EXAMPLE OUTPUT ============== WindowDeck conflict report base : team.json (set "team-standard") sets : 3 (team-standard, alice, bob) bindings : 23 conflicts: 11 (3 error, 8 warning) #2 [error] alias-collision chord : Ctrl+Shift+P detail : Ctrl+Shift+P is bound to 2 different actions ("build.run", "command.palette"); the collision is only visible after normalisation, the files spell it "Ctrl+Shift+P", "^⇧P", "ctrl-shift-p" involved: alice command.palette Ctrl+Shift+P involved: bob build.run Ctrl+Shift+P involved: team-standard command.palette Ctrl+Shift+P free : Ctrl+Alt+P, Alt+Shift+P, Ctrl+Alt+Shift+P #6 [error] prefix-sequence chord : Ctrl+K detail : "Ctrl+K" is a strict prefix of "Ctrl+K Ctrl+S" - pressing the shorter binding can never resolve without a timeout, and one of the two will not fire free : Ctrl+Shift+K, Ctrl+Alt+K, Alt+Shift+K #8 [warning] reserved-chord chord : Meta+L detail : Meta+L is reserved by Windows - the OS uses it to lock the workstation / switch user source : Microsoft, "Keyboard shortcuts in Windows" THE LEDGER ========== Every run given --ledger appends exactly one JSON line. Existing lines are never rewritten, so it is safe to point several runs at the same file. {"ts":"2026-08-11T07:05:09.320157778Z","tool":"windowdeck","version":"1.0.0", "command":"merge","args":["--set","alice.json",...],"base":"team.json", "inputs":[{"set":"team-standard","file":"team.json","sha256":"be1b...", "bindings":7,"base":true}, ...], "bindings_in":23,"conflicts":11,"conflicts_error":3,"conflicts_warning":8, "deck_bindings":14,"output":"deck.json","status":"ok"} The SHA-256 of each input file's exact bytes is recorded, so you can prove later which version of whose bindings produced a given deck. SCOPE / WHAT THIS DOES NOT DO ============================= Read this section before you file a bug. IT DOES NOT TOUCH YOUR KEYBOARD. WindowDeck never installs, registers, intercepts, grabs or tests a hotkey. It does not call RegisterHotKey, it does not talk to the Carbon or Cocoa event APIs, it does not open an X11 or Wayland connection, it does not read or write any application's real keymap. It reads JSON, thinks about it, and writes JSON. If you want the deck applied to your editor, YOU have to write the exporter that feeds it into that editor's own config. There is no daemon, no agent, no background process, and nothing is ever installed on the host. THE RESERVED TABLES ARE CURATED, NOT EXHAUSTIVE. They contain the chords that actually bite teams - roughly 25 for Windows and 22 for macOS. They are NOT a dump of every shortcut either OS ships, and there are thousands. They do not know about: - your Windows edition, version, region, or keyboard layout - macOS System Settings > Keyboard shortcuts, which every user can change, including turning built-in chords off entirely - third-party grabbers already running on the machine: Alfred, Raycast, Rectangle, PowerToys, AutoHotkey, Karabiner, IME switchers, the vendor utilities that ship with the laptop - Linux desktop environments. Linux is a supported PLATFORM to run the binary on and a valid value for "platform" in a set file, but there is no reserved table for GNOME, KDE or anything else, because there is no single answer to give. "No reserved-chord conflict" therefore means "not in our table", not "safe". The absence of a warning is not a guarantee. IT DOES NOT KNOW WHAT YOUR APPLICATION ALREADY BINDS. The chord space it searches for suggestions is free with respect to the loaded sets and the two reserved tables. VS Code, IntelliJ, your browser and your terminal emulator all have their own crowded keymaps that WindowDeck has never seen. Feed those in as a binding set if you want them respected. ONE ACTION GETS ONE CHORD. The merged deck is a bijection: one chord per action, one action per chord. Secondary or alternate bindings for the same action are dropped, deliberately and visibly, and reported as such. If your target application supports several chords for one command, WindowDeck's deck is not the right shape for it. CONTEXT AND MODES ARE NOT MODELLED. There is no "when" clause, no editor focus condition, no modal state, no per-application scoping. Two bindings that would never be active at the same time are still reported as a conflict, because WindowDeck has no way to know that. Real editors resolve a lot of apparent conflicts with context; this tool cannot. CHORDING SEMANTICS ARE SIMPLIFIED. A sequence is an ordered list of chords compared for exact equality. There is no timeout model, no partial-match resolution, no notion of which runtime is interpreting the sequence. The prefix rule is the honest general case: if one binding is a strict prefix of another, at least one of them is unreliable. KEY COVERAGE IS FINITE. Media keys, laptop function-row keys, mouse buttons, gamepad inputs, trackpad gestures, dead keys, and non-US layout keys are not in the key table and are rejected. IsoSection, Yen, Ro and friends are not supported. Modifier-only "chords" like a bare Shift are rejected by design. NO NETWORK, NO STATE, NO INSTALL. It makes no network calls of any kind. It has no config file, no cache, no registry keys, no dotfiles. It reads exactly the files you name and writes exactly the files you ask for. There are no environment-variable switches and no hidden or debug-only flags: everything the program can do is in --help. EXIT CODES ========== 0 Success. For check this means no error-severity conflicts (and, with --strict, no warnings either). 1 Bad invocation (unknown command, missing required flag, unparseable chord, duplicate set names) or an I/O error (missing file, malformed JSON, refusing to overwrite an input). Explicit help exits 0. 2 check only: conflicts were found. This is a report, not a crash - the full report is still printed on stdout. DECK FILE FORMAT ================ merge writes one JSON object: { "tool": "windowdeck", "version": "1.0.0", "generated_at": "2026-08-11T07:05:09Z", "deck": { "base": "team-standard", "sets": ["team-standard", "alice", "bob"], "bindings": [ ... one entry per kept binding ... ], "resolutions": [ ... one entry per decision, with an explanation ... ], "warnings": [ ... surviving OS-reserved collisions ... ], "suggestions": [ ... free chords for the dropped bindings ... ] } } Everything inside "deck" is sorted and contains no timestamps, so two merges of the same inputs in any order produce an identical "deck" object. Only "generated_at" differs between runs.