kar lets you write your Karabiner-Elements keyboard config in TypeScript instead of editing raw JSON. It compiles a config.ts into Karabiner's complex modifications format and writes it directly into your karabiner.json.
This gist walks through setup, explains the key concepts, and includes my full config as a working example you can copy and customize.
- macOS with Karabiner-Elements installed
- Deno or Bun (kar uses one of these to evaluate your TypeScript)
# Install Deno (recommended)
curl -fsSL https://deno.land/install.sh | sh
# Or install Bun
curl -fsSL https://bun.sh/install | bash# Clone and build with Cargo
git clone https://github.com/nikivdev/kar.git
cd kar
cargo build --release
# Copy the binary somewhere on your PATH
cp target/release/kar /usr/local/bin/kar initThis creates ~/.config/kar/config.ts with a starter template. Open it in your editor and start customizing.
# Build once — writes to a "kar" profile in karabiner.json
kar
# Watch mode — rebuilds on every save
kar watch
# Dry run — prints generated JSON without writing
kar --dry-runkar creates a separate Karabiner profile called kar and auto-selects it. Your original profile stays untouched.
The most basic thing — remap one key to another with no conditions:
simple: [
{ from: "caps_lock", to: "escape" },
]This is kar's killer feature, inspired by Goku. A simlayer turns any key into a layer trigger when pressed simultaneously with another key. Tap the key normally and it types as usual.
// Define the layer
simlayers: {
"s-mode": { key: "s" },
},
// Use it in rules
rules: [
{
description: "s-mode (navigation)",
layer: "s-mode",
mappings: [
{ from: "h", to: "left_arrow" },
{ from: "j", to: "down_arrow" },
{ from: "k", to: "up_arrow" },
{ from: "l", to: "right_arrow" },
],
},
]How it works: Press s + h together (within the simultaneous threshold) and you get left_arrow. Press s alone and you get a normal s.
Attach modifiers to any to key:
// Single modifier
{ from: "b", to: { key: "left_arrow", modifiers: "left_command" } }
// Multiple modifiers
{ from: "w", to: { key: "right_arrow", modifiers: ["left_option", "left_shift"] } }Fire multiple keystrokes from one mapping:
// Select current word: Option+Left then Option+Shift+Right
{ from: "w", to: [
{ key: "left_arrow", modifiers: "option" },
{ key: "right_arrow", modifiers: ["option", "shift"] },
]}Trigger an action by pressing two keys at the same time — no layer definition needed:
{ from: ["j", "k"], to: "escape" }Run any shell command from a key press:
import { shell, open, zed } from "kar/types"
{ from: "t", to: shell("open -a Terminal") }
{ from: "s", to: open("/Applications/Safari.app") }
{ from: "z", to: zed("~/projects/my-app") }Scope rules to specific apps:
{
description: "Safari-only shortcuts",
condition: { app: "^com\\.apple\\.Safari$" },
mappings: [
{ from: "n", to: { key: "l", modifiers: "left_command" } },
],
}Fine-tune the feel of your layers:
profile: {
alone: 80, // ms before a held key fires to_if_alone (tap detection)
sim: 200, // ms window for simultaneous key detection
},See config.ts in this gist for a complete, working config that demonstrates:
- s-mode: Vim-style navigation (hjkl), word jumps, clipboard ops, tab switching, text selection — all from the home row
- semicolon-mode: A full shift layer so you never need to reach for Shift
- Simultaneous combos: j+k, k+n, j+l for app launching and system shortcuts
- Colon/semicolon swap: Types
:by default (useful for Vim users and programmers)
# 1. Install kar (see above)
# 2. Initialize
kar init
# 3. Replace the default config with mine
cp config.ts ~/.config/kar/config.ts
# 4. Build and apply
kar
# 5. (Optional) Watch for changes
kar watchThe config is plain TypeScript — edit it however you want:
- Change navigation keys: Swap
hjklforwasdor anything else - Add app launchers: Use
shell("open -a AppName")oropen("/path/to/app") - Add layers: Define a new simlayer (e.g.,
"d-mode": { key: "d" }) and add rules for it - Remove the semicolon swap: Delete that rule block if you prefer standard
;behavior - Remove Keyboard Maestro integration: The
km()calls require Keyboard Maestro. Replace them withshell()commands or remove those mappings
| Layer | Trigger | What It Does |
|---|---|---|
| s-mode | Hold s + key |
Navigation, editing, clipboard |
| semicolon-mode | Hold ; + key |
Shift layer (capital letters, symbols) |
| Combo | Keys | Action |
|---|---|---|
| j+k | Simultaneous | Keyboard Maestro: open Safari tab |
| k+n | Simultaneous | Keyboard Maestro: open Comet tab |
| j+l | Simultaneous | Cmd+Shift+Space |
| k+l | Simultaneous | Cmd+Option+Ctrl+Space |
| j+; | Simultaneous | Cmd+Option+Shift+9 |
- Start small. Add one simlayer, use it for a week, then expand.
- Use
kar --dry-runto inspect the generated JSON before applying. - Your original Karabiner profile is safe. kar writes to its own profile.
- Layer keys still type normally when tapped alone — you don't lose any keys.
If you're coming from Goku (EDN-based Karabiner config), kar supports importing your existing rules:
import { importProfile } from "kar/types"
export default {
imports: [
importProfile("your-goku-profile"),
],
// Then incrementally move rules to native kar format
} satisfies Config- kar GitHub repo — full docs, types reference, and more examples
- Karabiner-Elements — the underlying engine
- Karabiner complex modifications docs