Presets — named, switchable configurations for different keyboards #1818
Replies: 2 comments
|
Thanks for asking beforehand, and for looking to contribute! I do have some planned work from a previous PR that was closed that could touch some of the same areas (#1782), but from what I can tell it should be perfectly fine to roll in your pull request and continue. I'm up for reviewing it either way, and I suspect that I would be inclined to move forward with it. I think it's worth having in the UI, but my preference is to have it in the popover in the Extras button and the bottom of the General tab, not in the Shortcuts tab. |
|
Thanks — that's great to hear, and thanks for the pointer to #1782. I've moved the picker into the Extras popover, under a "Presets" header after the split-ratio rows, so the Shortcuts tab is untouched. Opened it as #1819. One small check on your wording: I read "the popover in the Extras button and the bottom of the General tab" as the one popover reached from the button at the bottom of General, so that's where it went. If you also wanted something directly on the General tab, say the word and I'll add it. On #1782 — the only shared seam is |
Uh oh!
There was an error while loading. Please reload this page.
I've implemented presets for Rectangle and would like to check whether it's something you'd
consider before opening a PR, as CONTRIBUTING.md suggests. If it isn't a direction you want,
no problem at all — I'd rather hear that now than have you spend review time on it.
What I ran into
I switch between a full-size keyboard, a tenkeyless one, and the MacBook's built-in
keyboard. Most of my shortcuts sit on the numeric keypad, so on the other two they're
unreachable, and the bindings I'd want instead collide with the numpad set. Import/Export
can move a configuration between machines, but using it to switch several times a day means
two file dialogs each time, and there's no way to keep named sets around.
There's also one thing a config file can't express. MASShortcut distinguishes three states
per shortcut key: absent (the registered default applies), an empty dictionary (explicitly
unbound), and a real shortcut.
Defaults.encoded()reads throughShortcutCycle.shortcut(for:), which returns nil for the empty dictionary, so an explicitlyunbound shortcut isn't written to the file. With #1805 making import authoritative, the
omitted key is then removed on import and the built-in default applies again.
That matters here because a "tenkeyless" configuration is mostly defined by which bindings
should be off. #1805 did close the other half of what I originally hit — imports used to
leave omitted shortcuts alone — so this proposal is narrower than it would have been a week
ago.
What it does
A preset is a named snapshot of the shortcuts plus the settings that describe window
behavior. Settings about the app itself — launch at login, update checks, menu bar icon,
per-app exclusions, transient Todo state — stay global. The exclusion list is explicit and
tested against
Defaults.array, so a future rename can't silently pull a key in.Shortcuts are stored in all three states above, so switching restores unbound bindings as
unbound. Snapshots read
persistentDomain(forName:)rather thandictionary(forKey:)totell an untouched shortcut from an assigned one.
The active preset simply mirrors the live settings, so there's no save button or dirty
state. Applying one writes settings first (so
alternateDefaultShortcutsis current beforeregisterDefaults()re-runs), then shortcuts, then postsconfigImportedand lets theexisting observers reconcile. Presets can be created from the built-in defaults or by
duplicating the current one, and renamed or deleted.
Footprint
One
NSPopUpButtonat the top right of the Shortcuts tab, added programmatically to theexisting stack view the way
initializeAutoMaximizeCheckbox()does — no storyboard changes,and no behavior change for anyone who ignores it. 11 new strings. 13 files, +949/−22, of
which 327 lines are two new files under
Rectangle/Presets/and 266 are tests.If you'd rather it not be visible by default, I'm happy to put it behind a hidden preference
the way Todo mode is. Just say which you prefer and I'll adjust before opening the PR.
Presets ride along in Import/Export through a new optional
Config.presetsfield ratherthan another entry in
defaults, which would have put the whole library into the file asone escaped JSON string. It's the one place this steps outside the "every new key goes in
Defaults.array" convention; the convention's purpose — never silently dropping a key fromexport — is still met. Both directions stay compatible and there are tests for it, and
Configkeeps a memberwise initializer withpresetsdefaulting to nil so existing callsites are untouched.
Two small things came along the way:
Defaults.load(fileUrl:)'s settings loop is extractedto
Defaults.apply(defaults:)so import and preset switching share a path, andJSONDefault.load(from:)now clears its decoded value when a config sets the underlyingstring to nil (previously footprint colors and snap areas couldn't be reset through import).
That second one is a different path from the
init(key:defaultValue:)fix in #1811.Testing
29 new tests, all passing;
xcodebuild archiveis green. For reference,f6279e2runs 223tests with 22 failing assertions across
ActiveSideSplitRatiosCooperativeTests,CooperativeCornerResizeTestsandHalfSplitCornerCalculationTests, and my branch runs 252with the same 22. I checked an unmodified checkout to be sure they aren't mine. Since
build.ymlonly archives, they wouldn't show up in CI — happy to open a separate issue ifthat's useful.
All reactions