Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions docs/adr/0016-use-kanary-for-keyboard-remapping.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# ADR 0016: Use Kanary for Keyboard Remapping

## Status

Accepted

## Context

Karabiner Elements has been used for local macOS keyboard behavior:

- Caps Lock to left Control.
- JIS virtual keyboard behavior.
- cmux-scoped IME shortcuts for Google Japanese Input.

Kanary provides a macOS 14+ Apple Silicon menu bar app for keyboard-focused
workflows, including modifier-key taps, app hotkeys, window movement, input
switching, per-app input defaults, and a built-in Caps Lock to Control remap for
JIS users.

Kanary is distributed from `https://kanary.download/download`, which currently
redirects to a versioned ZIP file such as `Kanary-2.3.8.zip`. It is not available
as a standard Homebrew cask in the public Homebrew cask API at the time of this
decision.

## Decision

Use Kanary as the primary local keyboard remapping tool instead of Karabiner
Elements.

- Stop installing Karabiner Elements through the nix-darwin Homebrew cask list.
- Stop generating `~/.config/karabiner/karabiner.json` through home-manager.
- Keep Google Japanese Input installed.
- Install Kanary manually from `https://kanary.download/download` until a stable
package-manager source exists.
- Make nix-darwin system checks fail before Homebrew activation when
`Kanary.app` is missing from `/Applications` or `~/Applications`.
- Configure Kanary manually for:
- Caps Lock to Control.
- Left Command tap to alphanumeric input.
- Right Command tap to kana input.
- Per-app default input modes where useful.

## Consequences

`darwin-rebuild switch --flake ~/develop/github.com/keito4/config/nix` no longer
installs or configures Karabiner. The system check requires Kanary to already be
installed before Homebrew cleanup runs, so keyboard remapping does not silently
disappear when Karabiner is removed.

Kanary installation and app-level settings are intentionally manual for now. The
repository avoids adding a bespoke ZIP download, checksum, and install flow while
Kanary is not available through the normal package sources used by this
configuration.

The previous cmux-specific `Ctrl+Shift+J`, `Ctrl+Shift+;`, and `Ctrl+Shift+'`
IME mappings are no longer generated by this repository. Use Kanary's modifier
tap input switching and per-app input defaults instead.
1 change: 0 additions & 1 deletion nix/home/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
./dotfiles.nix
./agent-commands.nix
./cmux.nix
./karabiner.nix
];

home = {
Expand Down
126 changes: 0 additions & 126 deletions nix/home/karabiner.nix

This file was deleted.

1 change: 1 addition & 0 deletions nix/hosts/darwin/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

{
imports = [
../../modules/kanary.nix
../../modules/homebrew.nix
];

Expand Down
1 change: 0 additions & 1 deletion nix/modules/homebrew.nix
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@
"aerospace"
"alfred"
"bettertouchtool"
"karabiner-elements"
"notion"
"notion-calendar"
"raycast"
Expand Down
11 changes: 11 additions & 0 deletions nix/modules/kanary.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{ config, ... }:

{
system.checks.text = ''
if [ ! -d "/Applications/Kanary.app" ] && [ ! -d "/Users/${config.system.primaryUser}/Applications/Kanary.app" ]; then
echo "error: Kanary.app is required for keyboard remapping." >&2
echo "Install it from https://kanary.download/download before running darwin-rebuild." >&2
exit 2
fi
'';
}
Empty file modified script/macos/agent-select-input-source.sh
100755 → 100644
Empty file.
56 changes: 28 additions & 28 deletions test/nix-darwin-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,23 @@ function readRepoFile(relativePath) {
}

describe('nix-darwin and home-manager macOS configuration', () => {
test('home-manager imports cmux and Karabiner modules', () => {
test('nix-darwin imports Kanary guard before Homebrew', () => {
const darwinHost = readRepoFile('nix/hosts/darwin/default.nix');

expect(darwinHost).toContain('../../modules/kanary.nix');
expect(darwinHost).toContain('../../modules/homebrew.nix');
expect(darwinHost.indexOf('../../modules/kanary.nix')).toBeLessThan(
darwinHost.indexOf('../../modules/homebrew.nix'),
);
});

test('home-manager imports cmux without Karabiner', () => {
const homeDefault = readRepoFile('nix/home/default.nix');

expect(homeDefault).toContain('./dotfiles.nix');
expect(homeDefault).toContain('./agent-commands.nix');
expect(homeDefault).toContain('./cmux.nix');
expect(homeDefault).toContain('./karabiner.nix');
expect(homeDefault).not.toContain('./karabiner.nix');
});

test('Homebrew casks install cmux and input tooling', () => {
Expand All @@ -23,9 +33,9 @@ describe('nix-darwin and home-manager macOS configuration', () => {
expect(homebrewModule).toContain('"android-studio"');
expect(homebrewModule).toContain('"cmux"');
expect(homebrewModule).toContain('"flutter"');
expect(homebrewModule).toContain('"karabiner-elements"');
expect(homebrewModule).toContain('"mattermost"');
expect(homebrewModule).toContain('"google-japanese-ime"');
expect(homebrewModule).not.toContain('"karabiner-elements"');
expect(homebrewModule).not.toContain('"bartender"');
expect(homebrewModule).not.toContain('"rancher"');
expect(homebrewModule).not.toContain('"google-cloud-sdk"');
Expand Down Expand Up @@ -71,31 +81,21 @@ describe('nix-darwin and home-manager macOS configuration', () => {
expect(cmuxModule).toContain('split-divider-color = #3e4451');
});

test('Karabiner maps Caps Lock and scopes cmux IME shortcuts', () => {
const karabinerModule = readRepoFile('nix/home/karabiner.nix');

expect(karabinerModule).toContain('home.file.".config/karabiner/karabiner.json"');
expect(karabinerModule).not.toContain('keyboard_type_v2 = "jis"');
expect(karabinerModule).toContain('key_code = "caps_lock"');
expect(karabinerModule).toContain('key_code = "left_control"');
expect(karabinerModule).toContain('^com\\\\.cmuxterm\\\\.app$');
expect(karabinerModule).toContain('cmuxJapaneseInputSource = "com.google.inputmethod.Japanese.base";');
expect(karabinerModule).toContain('cmuxEnglishInputSource = "com.google.inputmethod.Japanese.Roman";');
expect(karabinerModule).toContain('shell_command = "${inputSourceCommand} ${inputSourceID}"');
expect(karabinerModule).toContain('cmuxImeShortcut "j" cmuxJapaneseInputSource');
expect(karabinerModule).toContain('cmuxImeShortcut "semicolon" cmuxEnglishInputSource');
expect(karabinerModule).toContain('cmuxImeShortcut "quote" cmuxEnglishInputSource');
expect(karabinerModule).toContain('cmuxCapsLockImeShortcut "j" cmuxJapaneseInputSource');
expect(karabinerModule).toContain('cmuxImeSimultaneousShortcut "left_control" "j" cmuxJapaneseInputSource');
expect(karabinerModule).toContain('home.file.".local/bin/agent-select-input-source"');
expect(karabinerModule).toContain('source = ../../script/macos/agent-select-input-source.sh;');

const selectInputSourceScript = readRepoFile('script/macos/select-input-source.swift');
expect(selectInputSourceScript).toContain('TISSelectInputSource');
expect(selectInputSourceScript).toContain('TISCopyCurrentKeyboardInputSource');

const selectInputSourceWrapper = readRepoFile('script/macos/agent-select-input-source.sh');
expect(selectInputSourceWrapper).toContain('exec /usr/bin/xcrun swift "$src" "$@"');
test('Kanary replacement is documented without home-manager Karabiner state', () => {
const adr = readRepoFile('docs/adr/0016-use-kanary-for-keyboard-remapping.md');
const kanaryModule = readRepoFile('nix/modules/kanary.nix');

expect(fs.existsSync(path.join(repoPath, 'nix/home/karabiner.nix'))).toBe(false);
expect(adr).toContain('Use Kanary as the primary local keyboard remapping tool');
expect(adr).toContain('Stop installing Karabiner Elements');
expect(adr).toContain('Stop generating `~/.config/karabiner/karabiner.json`');
expect(adr).toContain('Make nix-darwin system checks fail');
expect(adr).toContain('Left Command tap to alphanumeric input');
expect(adr).toContain('Right Command tap to kana input');
expect(kanaryModule).toContain('system.checks.text');
expect(kanaryModule).toContain('/Applications/Kanary.app');
expect(kanaryModule).toContain('config.system.primaryUser');
expect(kanaryModule).toContain('https://kanary.download/download');
});

test('portable user dotfiles are managed without credential state', () => {
Expand Down