feat(mac): add keyman command-line interface (stage 1)#15946
Closed
boazy wants to merge 2 commits into
Closed
Conversation
Extend processURL: in KMInputMethodAppDelegate to recognise a new 'select' action that carries a percent-encoded canonical keyboard id in a 'path' query parameter. The URL is parsed, the matching index in self.activeKeyboards is looked up, and selectKeyboardFromMenu: is dispatched on the main queue so the same code path that runs when a user picks a keyboard from Keyman's menu fires — loading the kmx into Core, updating the OSK, persisting KMSelectedKeyboardKey, and applying persisted options. Required by the new keyman command-line tool in mac/keyman-cli/, which dispatches keyman:select?path=… via LaunchServices (open(1)). The existing keyman:download URL handling is untouched. No changes to entitlements, Info.plist, or the build pipeline; the existing kAEGetURL registration in initCompletion already routes these URLs to processURL:.
Introduces mac/keyman-cli/, a standalone Rust crate providing a
'keyman' binary that controls Keyman from the shell.
Subcommands (all support --json for machine-readable output):
keyman list every keyboard in KMActiveKeyboardsKey,
with the currently-selected one starred
and display names resolved from kmp.json
keyman status OS-level (im_registered / im_selected /
im_process_running) + Keyman-level state
keyman activate enable + select Keyman via Carbon TIS
(idempotent)
keyman select <id> activate if needed, then dispatch a
keyman:select?path=… URL via LaunchServices
and verify the round-trip via prefs
Resolution rules for <id> (case-insensitive): exact canonical id,
package/keyboard, filename[.kmx], stem. Ambiguity is reported with the
list of candidates; the resolver lives in src/resolver.rs and has 14
unit tests covering each input form.
Architecture is shaped for future Windows / Linux backends: every
operation goes through the KeymanClient trait in src/client.rs, the
value types in src/keyboard.rs use OS-neutral names (im_registered
rather than 'enabled in AppleEnabledInputSources'), and non-macOS
targets compile against src/backend/unsupported.rs returning a clear
'not yet implemented' error.
macOS internals:
src/backend/macos/prefs.rs CFPreferencesCopyAppValue against
the keyman.inputmethod.Keyman domain
src/backend/macos/tis.rs Carbon TIS bindings (filters input
sources by kTISPropertyBundleID)
src/backend/macos/packages.rs serde-driven kmp.json reader for
keyboards[].name display names
src/backend/macos/ipc.rs 'open keyman:select?path=…' dispatch
with percent-encoding
IPC for select reuses the existing keyman:// URL surface (see the
matching feat(mac): handle keyman:select URL commit). Alternative
options considered (distributed notifications, XPC, Mach ports,
AppleScript) are documented in the crate README with rejection
rationale. Smallest combined diff of the candidates.
Build: cargo build --release (in mac/keyman-cli/). MSRV 1.75; the
binary is ~900KB arm64 Mach-O; no codesign step required (the macOS
linker emits the ad-hoc signature local execution needs).
Out of scope for this PR (called out in the README):
- enable/disable/install (no mutation of KMActiveKeyboardsKey)
- embedding the binary in Keyman.app
- changes to mac/build.sh, Xcode targets, or the .pkg installer
- Windows / Linux backends
cargo fmt clean. cargo clippy --all-targets -- -D warnings clean.
14/14 unit tests pass.
scripts/smoke.sh provides a guided manual smoke test covering both
shell-verifiable and visual checks (menu-bar IM picker, live TextEdit
typing) once the matching IMK patch is built into Keyman.app.
User Test ResultsTest specification and instructions ERROR: user tests have not yet been defined |
Collaborator
|
This pull request is from an external repo and will not automatically be built. The build must still be passed before it can be merged. Ask one of the team members to make a manual build of this PR. |
Member
|
Thank you for your contribution. Unfortunately we cannot accept contributions in Rust at this time. It would be wise to engage with the team before investing in making a contribution of an entirely new component, to ensure that your work aligns with the project roadmap. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
mac/keyman-cli/, a standalone Rust CLI (keyman list | status | activate | select) for controlling Keyman from the shell on macOS, and the minimal Keyman input-method change required to makeselectswitch keyboards live without restarting the IM. Two commits, both scopedmac:feat(mac): handle keyman:select?path=… URL for live keyboard switching— 30 lines added toKMInputMethodAppDelegate.processURL:extending the existingkeyman://URL scheme with aselectaction that dispatchesselectKeyboardFromMenu:(the exact same code path the in-app menu uses).feat(mac): add keyman command-line interface (stage 1)— new Rust crate.Stage 1 supports macOS only; the crate is shaped behind a
KeymanClienttrait so Windows / Linux backends can land later without an API break. The Rust crate is not embedded inKeyman.appfor this PR — it's intentionally acargo build --releaseartefact — and no existing build-pipeline files (mac/build.sh, Xcode targets, the.pkginstaller,codesignsteps) are touched. The macOS linker already emits an ad-hoc signature sufficient for local execution.IPC for
selectThe CLI's
activateruns entirely in-process via the Carbon TIS API.selectis harder: the running IMK process keepsKMSelectedKeyboardKeycached in memory and does not observeNSUserDefaultschanges (verified —rg 'NSUserDefaultsDidChangeNotification|addObserver.*NSUserDefaults' mac/Keyman4MacIM/returns zero hits). Some form of IPC is therefore required for a live switch without a Keyman restart.keyman://URL schemekeyman:(Info.plistCFBundleURLSchemes) and routes Apple Events viakInternetEventClass:kAEGetURLinKMInputMethodAppDelegate -initCompletion. One newelse ifinprocessURL:reuses the existing surface; LaunchServices handles the app-not-running case for free. Smallest combined diff..sdefcurrently shipped; would need a scripting dictionary.The CLI dispatches
keyman:select?path=<percent-encoded id>via/usr/bin/open -g, then pollsKMSelectedKeyboardKeyviaCFPreferencesAppSynchronizefor up to 1.5 s to confirm the round-trip. On timeout it exits 9 with a diagnostic explaining that the runningKeyman.applacks the matching IMK patch — observed directly in the smoke transcript below, where the unpatched installed Keyman 18.0.248 ignores the URL.Cross-platform readiness
KeymanClient(mac/keyman-cli/src/client.rs) is the seam between the CLI dispatcher and per-OS backends. Value types (Keyboard,KeyboardId,Status,ImState,ActivateOutcome,SelectOutcome) use OS-neutral names (e.g.im_registeredrather than "inAppleEnabledInputSources"). The dispatcher insrc/main.rsonly deals in already-resolvedKeyboardIdvalues, with the free-form string → canonical id resolver living insrc/resolver.rsas a CLI-level concern.Non-macOS targets compile against
src/backend/unsupported.rs, which returnsstd::io::ErrorKind::Unsupportedwith a clear "stage 1 is macOS only" message. The crate's README documents the integration points each future backend will hook:activatemaps toITfInputProcessorProfileMgr::ActivateProfile;selectlikely to a profile exchange with the running Keyman Engine.ibus/fcitxD-Bus interfaces map cleanly ontoKeymanClient. The current URL approach has no direct analogue, but a small named socket or D-Bus signal inside the engine process fits.Both deferred to follow-up PRs.
Manual smoke-test transcript
Captured against the installed Keyman 18.0.248 on the dev machine. All 7 expected keyboards (
KMActiveKeyboardsKey) are present.The last line is the expected result against an unpatched Keyman: the URL is dispatched (LaunchServices
openexits 0), but the IMK has no handler forselect, so the prefs round-trip times out and the diagnostic guides the user to rebuild. Building and installing the IMK from this branch promotes that exit-9 case to a successful live switch.Live-switch verification (post-IMK-rebuild) is a visual check — see
mac/keyman-cli/scripts/smoke.shfor the guided manual procedure covering steps that need a human (menu-bar IM picker state; TextEdit typing on the very next keystroke; menu checkmark movement).Deviations from the handoff
keyman.inputmethod.Keyman. The Xcode project currently setsPRODUCT_BUNDLE_IDENTIFIER = "keyman.inputmethod.$(PRODUCT_NAME:rfc1034identifier)"which would resolve tokeyman.inputmethod.Keyman4MacIM, but the installed runtime artefact still reportskeyman.inputmethod.Keyman(confirmed viadefaults read "~/Library/Input Methods/Keyman.app/Contents/Info.plist" CFBundleIdentifier). The CLI hard-codeskeyman.inputmethod.Keyman, matching the runtime constant inKMInputMethodLifecycle.m:44. No action needed; flagging because it surprised me.tools/keyman-cli/,core/keyman-cli/, or new top-levelcli/. I chosemac/keyman-cli/to mirror the existing precedent ofmac/setup/textinputsource/(a similar small single-purpose helper insidemac/). When Windows / Linux backends arrive the path may want to migrate totools/keyman-cli/; doing it now would touch the build pipeline (labeler.yml etc.) without payoff.im_registered/im_selected/im_process_runninginstead of the handoff's suggested phrasing. The handoff explicitly invited OS-neutral names; these read cleanly for the Windows TSF and Linux ibus/fcitx maps.Verification
cargo fmt -- --checkclean.cargo clippy --all-targets -- -D warningsclean (with#[allow(clippy::struct_excessive_bools)]on the two wire-schema structs — bools are the schema there).cargo test— 14 / 14 unit tests pass (keyboard-id resolver covers each accepted input form, ambiguity, unknown, case-insensitivity, leading-slash handling, package disambiguation; URL percent-encoding round-trip).Out of scope (explicit)
enable/disablesubcommands; any mutation ofKMActiveKeyboardsKey.Keyman.app/Contents/MacOS/.mac/build.sh, Xcode targets, the.pkginstaller, or code-signing pipelines./usr/local/binfrom any installer.