Skip to content
/ karaml Public

Write and update your Karabiner-Elements config in YAML

License

Notifications You must be signed in to change notification settings

al-ce/karaml

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

karaml 🍮

karaml (Karabiner in yaml) lets you write and maintain a virtual layers-based Karabiner-Elements keyboard customization configuration in YAML. It uses Python to translate the YAML into Karabiner-compatible JSON.

karaml is based on the philosophy of mxstbr's layer-centered Karabiner config, and my thanks goes to to mxstbr and yqrashawn for the inspiration for this project. Immense thanks to tekezo for Karabiner-Elements (consider sponsoring/donating).

# Default layer, does not require activation
/base/:
  caps_lock: [escape, /nav/]  # Escape when tapped, /nav/ layer when held
  # Separate modifiers and keycodes with a pipe `|`
  # Lowercase modifiers = left, uppercase = right
  oc | n: /nav/               # Tap left opt + left ctrl + n to toggle /nav/
  O  | w: <o-backspace>       # right opt + w to left opt + Backspace

  # Enter when tapped, Left Control when held, lazy flag, any optional modifiers
  (x) | enter:
    - enter
    - left_control
    - null # No event when released
    - [+lazy]

  j+k: [escape, button1] # j+k to escape when tapped, left click when held

  # option (either side) + o/O to create new line below/above
  a | o: m |right + return
  a | O: up + m | right + return

  a | h: string(hello world) # Send multiple chars without concatenating with +

  # backspace/left on tap, MacOS/Kitty hints on hold/release, depending on frontmost app
  c | h: {
      # Notification with Karabiner-Style popup
      unless Terminal$ kitty$:
        [backspace, "notify(idh, My MacOS Shortcut Hints)", "notifyOff(idh)"],
      # Notification with AppleScript popup
      if Terminal$ kitty$: [left, "shnotify(My Kitty Shortcuts, ==Kitty==)"],
    }

  # Other syntax for modifiers is also supported
  ms   a: app(Alacritty) # left_command + left_shift + a to launch Alacritty
  <ms-c>: app(CotEditor) # left_command + left_shift + c to launch CotEditor

  # You can use Unicode symbols for modifiers instead of letters
  ⌘ ⇧    | g: string(lazygit)    # command + shift + g to send string 'lazygit'
  ⌃› ‹⌥  | s: string(git status) # right_control + left_option + s to send string 'git status'
  ☆      | o: /open/             # hyper + o to toggle /open/ layer

  # Utilize user-defined aliases
  tab  : [tab, ⁙]      # tab to left opt, ctrl, and shift when held
  ⁙ | ⏎: screen_saver  # left opt, ctrl, shift, and enter starts screen saver

  # Utilize user-defined templates
  ⌘ ⇧ | f: rectangle(fullscreen)

# condition 'nav_layer' must be true for the following maps
/nav/:
  (x) | h: left # vim navigation with any optional mods
  (x) | j: down
  (x) | k: up
  (x) | l: right

  s: [app(Safari), /sys/]     # Launch Safari on tap, /sys/ layer when held
  g: open(https://github.com) # Open link in default browser

# etc.
? /sys/
  System Layer
: # ↑ inserts a description for the layer into karabiner.json

  o | m: [mute, null, mute] # To mute if held/key down, unmute if released/key up
  k  : play_or_pause
  "}": fastforward
  u  : shell(open -b com.apple.ScreenSaver.Engine) # Start Screen Saver

# User defined aliases
aliases:
  : ⌥ ⌃ ⇧
  : return_or_enter
  screen_saver: shell(open -b com.apple.ScreenSaver.Engine)

# User defined templates
templates:
  rectangle: open -g "rectangle-pro://execute-action?name=%s"


# JSON integration - any Karabiner JSON can be added here
json:
  - {
      description: "Right Shift to ) if tapped, Shift if held",
      from: { key_code: right_shift },
      to: { key_code: right_shift, lazy: true },
      to_if_alone: { key_code: 9, modifiers: [shift] },
      type: basic,
    }

✨ Features

  • Complex modifications on a single line of YAML so you can create and update keymaps quickly
  • Events for when a key is tapped, held, and released defined by position in an array (rather than k/v pairs)
  • Simple schema for requiring mandatory or optional modifiers in a pseudo-Vim style
  • Multiple frontmost-app conditional remaps in a single YAML map
  • Aliases for symbols, shifted keys, and complex names (e.g. grave_accent_and_tildegrave, left_shift + [{ )
  • Define your own aliases for keycodes and modifiers!
  • Template for app launchers, shell commands, notifications, and more by default
  • Define your own templates for shell commands!
  • Accepts regular Karabiner JSON in an 'appendix' table so all cases Karaml can't or doesn't plan to handle can still be in one config
  • Automatically update your karabiner.json or write to the complex modifications folder and import with the Karabiner GUI - no need to handle any files other than your .yaml config
  • Checks and formatting hints for your .yaml file - karaml will try not to let you upload a config that doesn't create a working modification, and tell you why (no more hunting for typos or missing/extraneous commas in a large JSON object)

❓ Why this project

The karabiner.json file can be hard to manage and visualize as it grows, as the JSON format requires a lot of lines, carefully managed opening and closing quotes, brackets, and commas, and repetitive conditional logic.

karaml tries to simplify this by providing a more readable, maintainable, and easy to adjust format in YAML. This means making some trade-offs in completeness of features, but I try to come close with the goal of balancing features and configuration simplicity. To prevent the need for maintaining multiple configurations if karaml falls short of your needs, karaml also supports an 'appendix' table for any Karabiner-compatible JSON that can't be expressed in karaml.

Why YAML?

  • Easy to maintain: no mandatory quotes, but quotes can simplify escaping troublesome characters; minimal or no use of brackets around keys/values
  • Easy to learn and easy to read
  • A superset of JSON which allows us to fall back to JSON if wanted/needed
  • Lets you leave comments for descriptions or notes to yourself!

⚡️ Quickstart

If you're unfamiliar with YAML, take a look at this handy guide.

After installing, take the sample YAML configuration and use it as a template for your own. The sample config has comments to explain karaml syntax.

For more detailed explanations, follow the configuration guide and read the YAML Configuration section of this document.

For a cleaner, less commented on example, see one of my configurations here. Follow the usage instructions below to convert your karaml config to Karabiner-JSON with the command-line tool (written in Python).

⚙️ YAML Configuration

Basic Mapping and Layer Structure

All keys belong to a layer. At minimum, a map requires a from: to structure and must be in a layer:

/layer_name/:
  from_key: to_key

This is equivalent to:

{
  "from": { "key_code": "from_key" },
  "to": { "key_code": "to_key" } "type": "basic",
  "conditions": [ { "name": "layer_name_layer", "type": "variable_if", "value": 1 } ]
}

In your karaml config, you need to add a /base/ layer, but only as a matter of convention. Maps in the /base/ layer don't check for conditions.

To add additional events or options to your maps, put them in a YAML array or sequence.

/layer_name/: # All mappings below require the 'layer_name' layer enabled
  from_key(s): [when_tapped, when_held, when_released, [to_opts], { params }]

As shown above, you can add as many or as few items to the array as you like.

Enabling Layers

In the 'to' part of the map, enable layers with /layer_name/. In the first position, the layer will be tap-toggled by the 'from'. In the second position, the layer will be enabled when the 'from' key is held.

/base/:
  oc | n: /nav/ # enabled/disabled on tap
  caps_lock: [escape, /nav/] # enabled when held

# The maps indented in this layer will only work when the layer is enabled
/nav/:
  h: left
  j: down
  k: up
  l: right

Modifiers

To add modifiers to a primary key, follow the format <modifiers-primary_key>. Wrap optional modifiers in parens.

To add modifiers to a primary key, there are a few available formats. The general format is to have modifiers followed by a delimiter followed by primary keys. Multiple modifiers can have whitespace between them if they help readability, or they can be joined without any whitespace. Optional modifiers are indicated by wrapping them in parens.

The original format for modifiers was <modifiers-primary_key>, but others have been added to give the user a choice in whatever format they find most readable.

The following are all examples of the mapping for left_control + left_shift + a to open the Terminal app:

/base/:
  <cs-a>   : app(Terminal)  # A vim-ish syntax
  cs-a     : app(Terminal)  # Angle brackets are optional
  c s  - a : app(Terminal)  # Whitespace is flexible
  c s  | a : app(Terminal)  # Use a pipe or a dash as delimiter
  c s    a : app(Terminal)  # Or use the final whitespace as the delimiter
  'c s | a': app(Terminal)  # Quotes may help you with escape characters

The guides and the remainder of the README (mostly) use the modifiers | primary_key format, with the pipe char | as the delimiter.

Whether the optional set in parens comes first or last doesn't matter, e.g. (c)os | g and os(c) | g are both valid. But a single set of optional modifiers in parens must be to the right or left of all mandatory modifiers (if there are any mandatory modifiers).

Single letters are one way of specifying modifiers. Left side modifiers use lowercase, right side modifiers use uppercase.

karaml alias key_code --- karaml alias key_code
c left_control --- C right_control
s left_shift --- S right_shift
o left_option --- O right_option
m left_command --- M right_command
r control --- R control
h shift --- H shift
a option --- A option
g command --- G command
f fn --- F fn
l caps_lock --- L caps_lock
x any --- X any

Examples:

  • c | hleft_ctrl + h
  • (x) | hh (with any optional modifiers)
  • mOC(s) | hleft_cmd + right_opt + right_ctrl + left_shift (optional) + h
  • (s)mOC | h → (same as above)
  • g(arh) | hcmd (mandatory) + option + control + shift (optional) + h
  • (arh)g | h → (same as above)

The mnemonics for the less-obvious modifiers are:

  • m: Mac
  • r: contRol
  • h: sHift
  • a: Alt
  • g: Gui

An alternative system for modifiers uses Unicode symbols: ⌘ ⌥ ⌃ ⇧ The Unicode symbols and denote left and right side modifiers.

Unicode symbol key_code
command
option
control
shift
‹⌘ left_command
‹⌥ left_option
‹⌃ left_control
‹⇧ left_shift
⌘› right_command
⌥› right_option
⌃› right_control
⇧› right_shift

Examples:

  • ⌘ ⇧ | gcommand + shift + g
  • ⌘ (⇧) | gcommand + shift (optional) + g
  • ‹⌃ | hleft_ctrl + h
  • ⌃› ‹⌥ | hleft_opt + right_ctrl + h

Of course you can use another valid syntax for the Unicode characters as well, e.g. <⌘⇧-g> or ⌘⇧ g, etc.

If you need an easy way to type these Unicode characters, you can use a text-expander or snippet tool like Typinator, the built in expanders in Raycast or Alfred, TextExpander, etc. Or, set your own alias with the user-defined aliases feature.

Or, see this suggestion in this Karabiner issue.

Key-Code Aliases

You can follow the explicit mapping for any key (e.g. s | 1!), or use these available aliases. Be mindful in your YAML config that some characters need to be escaped or wrapped in quotes to be recognized as strings.

karaml alias Karabiner key_code
enter return_or_enter
CR return_or_enter
ESC escape
backspace delete_or_backspace
BS delete_or_backspace
delete delete_forward
space spacebar
(a space) spacebar
SPC spacebar
spc spacebar
- hyphen
underscore hyphen + shift
_ hyphen + shift
= equal_sign
plus equal_sign + shift
( 9 + shift
) 0 + shift
[ open_bracket
{ open_bracket + shift
] close_bracket
} close_bracket + shift
\ backslash
| backslash + shift
; semicolon
: semicolon + shift
' quote
" quote + shift
grave grave_accent_and_tilde
` grave_accent_and_tilde
~ grave_accent_and_tilde + shift
, comma
< comma + shift
. period
> period + shift
/ slash
? slash + shift
! 1 + shift
@ 2 + shift
# 3 + shift
$ 4 + shift
% 5 + shift
^ 6 + shift
& 7 + shift
* 8 + shift
up up_arrow
down down_arrow
left left_arrow
right right_arrow
up_arrow
down_arrow
left_arrow
right_arrow
pgup page_up
pgdn page_down
kp- keypad_hyphen
kp* keypad_asterisk
kp/ keypad_slash
kp= keypad_equal_sign
kp. keypad_period
kp, keypad_comma
kpenter keypad_enter
kp1 keypad_1
kp2 keypad_2
kp3 keypad_3
kp4 keypad_4
kp5 keypad_5
kp6 keypad_6
kp7 keypad_7
kp8 keypad_8
kp9 keypad_9
kp0 keypad_0
kpnum keypad_num_lock
left_command
‹⌘ left_command
⌘› right_command
left_option
‹⌥ left_option
⌥› right_option
left_control
‹⌃ left_control
⌃› right_control
left_shift
‹⇧ left_shift
⇧› right_shift
lcmd left_command
rcmd right_command
lopt left_option
ropt right_option
lctrl left_control
rctrl right_control
lshift left_shift
rshift right_shift

Mutli-Modifier aliases:

karaml alias Karabiner key_code
hyper right_shift + right_option + right_command + right_control
ultra right_shift + right_option + right_command + right_control + fn
super right_shift + right_command + right_control
shift + option + command + control + fn

MISSING ALIASES:

  • +, since it's used to join multiple key codes and I need to figure out a way around that. Use plus in the meantime
  • kp+ for the same reason, so use kpplus as an alternate

Defining your own aliases

You can define your own aliases for singular events by adding a top level key named aliases anywhere in your config file. This YAML map will be merged with the default aliases.

An alias has the following format:

aliases:            # Include this top-level key anywhere in your config
  alias_name: /{modifiers (optional)} {delimiter (optional)} {key_code}/

Example:

aliases:

  # No modifier
  : return_or_enter
  screen_saver: shell(open -b com.apple.ScreenSaver.Engine) # Start Screen Saver
  # With modifiers (any of the following)
  tilde: s | grave_accent_and_tilde  # left_shift + grave
  tilde: ⇧ | grave_accent_and_tilde  # left_shift + grave

  : o c | s  # With an explicit delimiter (`|` or `-`), the final `s` is
              # interpreted as the # key code it represents,
              # So this is an alias for `left_option` + `left_control` + `s`

  : o c s    # Without an explicit delimiter, this alias is all mods, since
              # `s` is one of the single-character modifier aliases.
              # So this is an alias for `left_option` + `left_control` + `left_shift`

  : ⌥ ⌃ ⇧    # An alias of all modifier symbols is added to the modifier
              # alias dict regardless of its syntax

/base/:
  ⌘ | ⏎: app(WezTerm)
  tilde: '`'
  '`'  : tilde
  ⁙ | ⏎: screen_saver

/sys:
  m | s: screen_saver

In the above example, the Unicode character can be used as an alias for return_or_enter, and tilde can be used as an alias for a tilde ~ character, and screen_saver is aliased to a shell command to enable the screen saver.

Now these can be used in any layer in the config. In the example, ⌘ | ⏎ (return_or_enter with the command modifier) is used to launch WezTerm, and tilde key's usual function has been reversed so that now you have to press shift + the backtick character to get a backtick instead of vice versa. And finally, the left_command and s key combination is used to start the screen saver if the /sys/ layer is active.

Above, is an alias for left_option + left_control + left_shift that we can use as a modifier. In the /base/ layer, we use it and the alias for return_or_enter to make a rule for starting the screen saver, which also uses the alias screen_saver that we defined above. It's a rule composed entirely of user-defined aliases!

The purpose is to let you visualize your config in whatever way you find most readable. Say for example you're creating a layer for your window-management commands. Instead of a messy series of mappings like <coms-1>, <coms-2, or a series of long and similar shell commands, now you can create aliases like window_eights, window_center, move_to_space_1 etc. This makes for a cleaner config and makes it easier to manage when you want to change the keybindings.

Currently, only single-character aliases for modifier aliases are supported. We're working on a way to support multi-character aliases for modifier aliases!

Rules for defining your own modifier aliases

If all the key codes in an alias are valid modifiers, then the alias may be treated as a (multi-)modifier alias and added to the dict of modifiers aliases.

If the alias is composed entirely of modifier symbol characters (⌥, ⌘, ⇧, ⌃, etc.), then it will be added to the dict of modifier aliases, regardless of the syntax used to define it.

If the alias is composed entirely of single characters that are all valid modifier aliases (see the modifiers section), AND there are no explicit delimiters in the alias definition, then it will be added to the dict of modifier aliases. Otherwise, the final character will be treated as the key code it represents. See the examples above.

Simultaneous from-keys, multiple to-events, requiring multiple layers

To get simultaneous from events or multiple to events, i.e. in Karabiner-JSON:

{
  "from": {
    "simultaneous": [{ "key_code": "j" }, { "key_code": "k" }]
  },
  "to": [{ "key_code": "h" }, { "key_code": "l" }]
}

Join valid key codes or aliases in any part of the YAML map with a +. This is 'whitespace agnostic', so j+k, j + k, and j+k + l etc. are all valid.

/base/:
  j+k: h+l
  c | j + k: [escape, "/nav/ + notify(idn, Nav Layer on!)", "notifyOff(idn)"]

This also applies to joining layers if you want to enable two layers at once. Note that whichever layer comes later in the config will take priority if there are conflicting keys. In the following example, the s of the /sys/ layer will take priority over the s of the /fn/ layer, and the non-conflicting keys will all work as intended.

/base/:
  <a-caps_lock>: [null, /fn/ + /sys/]
/fn/:
  a: f1
  s: f2
  d: f3
/sys/:
  e: volume_down
  r: volume_up
  s: app(System Preferences)

Alternatively, for sending multiple singe characters, you can use string(). See: string special event function

Templates for common actions

karaml provides some templates for common-use actions. Some are just to.shell_command events in disguise. You can define your own templates for shell scripts - see user-defined templates

You can use these as if they were to.events, e.g.:

/base/:
  o | g: open(https://github.com)
Function Description
app Launch an app from the Applications folder
open Open a URL in your default browser
shell Run a shell command
input Switch input source
mouse Move the mouse cursor in the x or y directions
mousePos Move the mouse cursor to a specific position
notify Trigger a Karabiner-Elements notification
notifyOff Turn off a Karabiner-Elements notification
shnotify Trigger a notification in macOS's Notification Center
softFunc Karabiner-Elements software function
sticky Set a sticky modifier
string Send a sequence of characters
var Set a value for a variable/condition

App Launchers

app(app_name)

Pass an app name (as it appears in your Applications folder) as an argument to app(). I like using these in my /nav/ layer for quick access, but you might want these in a standalone /apps/ layer.

/nav/:
  f: app(Firefox)

Open Browser Link

open(url)

Open a URL with your default browser.

/base/:
  o | g: open(https://github.com)

Shell Commands

shell(shell command)

Pass a shell command as an argument to shell(). This is what the app() and open() 'functions' are actually doing under the hood. Please suggest other useful shorthands of shell commands that we could add!

/sys/:
  grave: shell(open ~)

Input Sources

input(lang_regex)
input({"language": "regex", "input_source_id": "regex", "input_mode_id": "regex" })

Either pass a language regex as an argument to input() to select the first available input source that matches the regex, or pass a string representing a JSON object with all the valid Karabiner fields as specified here.

/sys/:
  o | k + e:
    input(en) # Set English input source
    # Set a Greek keyboard based on source id
  o | k + g:
    'input({
    "input_source_id": "com.apple.keylayout.GreekPolytonic",
    "language": "el"
    })' # note the use of single quotes around the rhs to escape
    # double quotes and commas

Mouse Movement

mouse(action, speed|multiplier)

Using the to.mouse_key event, you can pass a two arguments that represent a key/value pair of movement/speed, or the speed_multiplier key and its multiplier value. If you want to assign multiple mouse events to the mapping, you can pass a string representing a JSON object matching the Karabiner specs, just like with input sources.

/mouse/:
  m: mouse(x, -2000)
  n: mouse(y, 2000)
  e: mouse(y, -2000)
  i: mouse(x, 2000)
  c | m: mouse(horizontal_wheel, 100)
  c | n: mouse(vertical_wheel, -100)
  c | e: mouse(vertical_wheel, 100)
  c | i: mouse(horizontal_wheel, -100)
  s: mouse(speed_multiplier, 2.5)

Set Mouse Cursor Position

mousePos(x, y, screen)

Wraps around the to.software_function.set_mouse_cursor_position event. The x and y arguments are mandatory, the screen argument is optional, and all three must be integers. The screen argument is the screen number, starting from 0. If you don't specify a screen, the cursor will be moved to the same screen as the current mouse position.

/mouse/:
  "0": mousePos(0, 0)
  ⌥ | 2: mousePos(500, 500, 1)

Notifications (Karabiner Style)

notify(id, message) notifyOff(id)

Shorthand for to.set_notification_message

The id is the reference for updating the notification with the message on subsequent calls.

/sys/:
  s:
    - app(System Preferences)
    - /sys/ + notify(sysNotification,"System Layer Enabled")
    - notifyOff(sysNotification)

There are a few ways to disable a notification. The easiest to use and remember is to pass the notification id as the only arg to notifyOff().

notifyOff(id)

Otherwise, you can pass an empty string or null as the message arg to notify().

notify(id, null)
notify(id, "")

Notifications (AppleScript Style)

shnotify(msg, title, subtitle, sound)

shnotify({"msg": "message", "title": "title", "subtitle": "subtitle", "sound": "sound"})

Displays a notification using AppleScript by running a shell command in the format:

osascript -e 'display notification "message" with title "title" subtitle "subtitle" sound name "sound"'

See the scriping documentation here.

There are two ways to pass arguments to shnotify(). The first is to pass positional arguments in the order of message, title, subtitle, and sound.

/sys/:
  o | 1: shnotify(text) # message only
  o | 2: shnotify(text, title) # message and title
  o | 3: shnotify(text, title, subtitle) # message, title, and subtitle
  o | 4: shnotify(text, title, subtitle, sound) # message, title, subtitle, sound
  o | 5: shnotify(text, null, null, sound) # message and sound only

The second is to pass a string representing a JSON object with all the valid AppleScript notification fields.

/sys/:
  o | 6: 'shnotify({
    "msg": "text",
    "title": "title",
    "subtitle": "subtitle",
    "sound": "sound"
    })'
  # With a dict, you can omit any fields you don't want to use
  o | 7: 'shnotify({"msg": "text", "sound": "sound"})'

See the sample configuration for an example with keyboard input source switching.

Sound names can be found at /System/Library/Sounds/ For more advanced notifications, try terminal-notifier or alerter and pass the command as a shell() command.

Software Functions

softFunc( {"function name": nested_dict} )

Takes a string representing a JSON object with all the valid Karabiner fields as specified here.

/mouse/:
  "p+1": 'softFunc("set_mouse_cursor_position": {"x": 0, "y": 0, "screen": 0 })'

Sticky Modifiers

sticky(modifier, toggle)

Pass two arguments: the modifier to be held on the next keypress (must be a valid modifier key code), and whether to toggle the modifier, turn it on, or turn it off (on, off, or toggle).

```yaml
/base/:
  ⌥ | ['right_shift: sticky(left_shift, toggle)', right_shift]
  ⌘ | ['right_shift: sticky(left_shift, on)', right_shift]
  ⌃ | ['right_shift: sticky(left_shift, off)', right_shift]

Strings

string(a string of valid key codes or aliases)

Instead of mapping a key to type out a string of characters using the + joining method, e.g. git checkout like so:

/base/:
  a | g: g+i+t+space+c+h+e+c+k+o+u+t

You can use string() (though the above method is valid):

/base/:
  a | g: string(git checkout )

The string must contain valid key codes or valid single-character aliases. So, [, ?, a blank space for spacebar, etc. will get interpreted as characters, and spacebar, kp-, left etc. will get interpreted as a literal string of chars, not their alias counterparts.

You can use a combination if you want to send non-single character events:

/base/:
  a | g: string(git checkout main) + enter

Variables

The var() function takes two arguments: a variable name and a value (0 or 1). See the to.set_variable documentation

karaml intends variables to be handled as layers and automates some mappings under the hood to streamline toggling and enabling layers, but the var() function allows more granular control.

/base/:
  o | s:
    var(sys_layer, 1) # This does not automatically create a corresponding
    # toggle-off mapping. Don't get stuck in another layer!

User Defined Shell Command Templates

This feature copies much of the design of the template feature in GokuRakuJoudo, though not as fully featured.

You can define your own templates for shell commands in the templates section of your configuration file. This may be useful if you want to create a lot of maps for similar long and complex shell commands.

templates:
  template_name: "command template %s || another command %s"

/base/:
  ⌥ | 1: template(arg1, arg2)

All %s will be replaced by the arguments passed to any instances of a template in your layers. You can have as many args as you want, but karaml will check that the amount of args passed to a template instance matches the amount of %s in the template definition.

As an example of a good use case, here is a template for triggering Rectangle Pro actions using urls.

templates:
  rectangle: open -g "rectangle-pro://execute-action?name=%s"

/rectangle/:
  ⌘ ⇧ | 1: rectangle( fullscreen       )
  ⌘ ⇧ | 2: rectangle( left-half        )
  ⌘ ⇧ | 3: rectangle( right-half       )
  ⌘ ⇧ | 4: rectangle( next-display     )
  ⌘ ⇧ | 5: rectangle( previous-display )
  # etc.

This looks much cleaner than having a few dozen lines of the same long command.

Note that the templates map in your configuration file is loaded BEFORE your aliases map (regardless of where you place those maps in your config), so you can use templates in your aliases, but not the other way around.

templates:
  rectangle: open -g "rectangle-pro://execute-action?name=%s"

aliases:
  fullscreen : rectangle( fullscreen )
  left-half  : rectangle( left-half  )
  right-half : rectangle( right-half )

/rectangle/:
  ⌘ ⇧ | 1: fullscreen
  ⌘ ⇧ | 2: left-half
  ⌘ ⇧ | 3: right-half
  # etc.

Layer descriptions

If no description is provided for a layer, karaml will add a default one to the karabiner.json file in the form of {layer name} layer. You can override this by adding some text to your layer's key after the layer (or layers).

/mouse/     Mouse Movements:  # Excess whitespace is ignored
  m       : mouse(x, -2000)
  n       : mouse(y, 2000)
  e       : mouse(y, -2000)
  i       : mouse(x, 2000)

# Using YAML's complex-key syntax for multi-line keys
? /symnum/

  Numpad + Symbols Layer
:
  (x) j: "0"
  (x) k: ","

# Layers with multiple conditions can also be split across multiple lines
? /symnum/ +
  /nav/

  Sym & Nav Multi-layer
:
  m   : left
  n   : down
  e   : up
  i   : right

The parser will replace all the newline chars in the complex key with a space and parse it as if it were a single line, as in the /mouse/ layer example.

This is an unconventional way to use YAML, but since a major part of karaml's design is to make a compact Karabiner-Elements configuration file and avoid dictionaries and excess brackets where possible and sensible, this was a good compromise.

The advantage of this over simply adding YAML comments near the layer is that the description makes it into the karabiner.json file and is displayed in the Karabiner-Elements GUI.

NOTE: The only restriction on layer descriptions is that they must not contain a forward slash (/). This was done to simplify the parsing of muti-conditional layers. karaml will raise an error if it detects a forward slash in a layer description.

Frontmost-App Conditions

To set frontmost-app conditions, the from-key part of your map remains the same, but your value becomes a dictionary (instead of the usual single string or list/sequence).

/layer/:
  from-key:
    {
      if regex regex ...: [when-tapped, when-held, etc.],
      unless regex regex ...: [etc.],
    }

The dictionary's keys must be in the form of either if regex regex ... or unless regex regex ... where regex is a regex expression that matches the bundle identifier of the app you want to match (read the Karabiner docs for more info). karaml will yell at you if your conditional term isn't if or unless. Every following substring separated by a space will be interpreted by karaml as a list (YAML supports lists in keys, but Python doesn't, so use a string here).

The dictionary's values are your usual karaml map values. Each k/v pair represents a new map that includes the frontmost_app_if/unless conditional.

You can create as many unique keys as you want here, all of which will be tied to the original 'from' key. Mind your regex and your capitalization!

/base/:
  c | u: { unless Terminal$ iterm2$ kitty$: g-backspace }
  a | O: { unless Terminal$ iterm2$ kitty$: up + g-right + enter }
  a | o: { unless Terminal$ iterm2$ kitty$: g-right + enter }
  a | g: {
      # types 'git ' when tapped, 'checkout' when held if any of these apps are focused
      if Terminal$ iterm2$ kitty$: [string(git ), string(checkout)],
      if CotEditor$: g | l, # 'go to line' alternate shortcut if CotEditor is focused
    }

Global parameters

Add a parameters map to your .yaml file to set global parameters for your profile. These parameters will be applied to all mappings in the profile unless overridden by a mapping's own parameters object.

parameters:
  {
    "basic.to_if_alone_timeout_milliseconds": 100,
    "basic.to_if_held_down_threshold_milliseconds": 101,
    basic.to_delayed_action_delay_milliseconds: 150,
    "basic.simultaneous_threshold_milliseconds": 75,
    "mouse_motion_to_scroll.speed": 100,
  }

/base/:
  # ...

Profile Name and Complex-Modification Title

Add a profile map to your .yaml file to set the profile name and a title map to set the complex-modification title. Both are optional, and you can leave them in your config even if you don't use the profile-creation or the complex-modification writing feature. If you don't provide either, karaml will generate default placeholders before it writes the file.

# If a profile name is not provided, one will be generated from the
# current Unix timestamp
profile_name: Karaml Config

title: KaramlRules

JSON Extension Map

Add a json map to the .yaml file to include Karabiner-compatible JSON. This is intended for cases where karaml doesn't support a feature you need, or when you think the JSON looks easier to read than the karaml syntax, but you still want to use karaml for the rest of your config.

Be mindful of the slight differences in syntax between YAML and JSON. Namely, quotes are optional unless needed for escaping characters, elements of the json map are separated as sequence item (a properly indented dash - followed by a space), and no commas are used to separate those items.

WARNING!: in the layer mappings, karaml 'inspects' your configuration for proper formatting - not just whether you wrote it in a syntax karaml can intrepret, but also whether you used valid key codes, valid modifiers, etc. Currently, karaml doesn't support these 'health checks' for the JSON extension map, so karaml will just append whatever you put in there to the rule-set.

/base/:
  # ...

/nav/:
  # ...

json:
  - {
      # No need for quotes since there are no chars that need escaping
      description: Right Control to > if tapped and control if held,
      from: { key_code: right_control },
      to: { key_code: right_control, lazy: true },
      to_if_alone: { key_code: period, modifiers: [right_control] },
      type: basic,
    }
  - {
      # Quotes needed in the description because of the comma.
      # We can also add quotes just if we want to
      "description": "Left Control to < if tapped, control if held",
      "from": { "key_code": "left_control" },
      "to": { "key_code": "left_control", "lazy": true },
      "to_if_alone": { "key_code": "comma", "modifiers": ["left_control"] },
      "type": "basic",
    }

🐍️ Python backend for handling your files

The program reads your .yaml config with the PyYAML module, interprets your layers and keybindings, checking that they are all well-formed along the way, and creates 'KaramlizedKey' objects with all the relevant attributes (from events, to events, options, modifiers, etc.) that are eventually converted into Karabiner-compatible JSON.

karaml then either updates your existing karabiner.json file or your complex-modifications folder.

  • If you elect to update karabiner.json directly:
    • karaml makes a copy of the current karabiner.json file and adds that copy to ~/.config/karabiner/automatic_backups/ as karabiner.backup.TIMESTAMP.json, every time (thirty of my ~6000 line Karabiner JSON backups add up to ~10MB, so check in every once in a while)
    • karaml then searches the profiles list in karabiner.json for a profile name that matches the one in your config (or generates a new one if you didn't specify it - add one!). If it finds a match, it will replace that profile with the new config generated from the .yaml file, otherwise it just appends a new profile to the list
  • If you elect to add your karaml config as a ruleset to the complex-modifications folder:
    • If you passed the -c flag in the command line, karaml expects a filename to write to. If you're using the CLI menu, karaml will default to creating/overwriting karaml_complex_mods.json. If the file already exists, you'll get a confirmation prompt. karaml doesn't backup your complex-mods files
    • If you have a title map in your .yaml file, karaml will use that as the title of your complex-modification ruleset. If you don't, karaml will title your ruleset as KaramlRules

🎨 About the Design

Understanding a standard Karabiner configuration is useful for leveraging the most out of karaml, but at its core karaml is designed to reduce events to either when_tapped and when_held actions (also when_released, but that's automated when layers are enabled when held).

when_tapped is mapped to either to or to_if_alone, and when_held is mapped to either to or to_if_held_down depending on whether the map enables a layer, a modifier, or a notification ('non-chatty' events) when held.

karaml was designed around toggling layers or enabling modifiers when a key is held down, and letting the key have some other function when tapped. I found that, for my typing style, a 100ms to_if_alone_timeout_milliseconds setting, mapping the 'when tapped' function to to_if_alone and the 'when held' functions to a to dictionary (instead of to_if_held_down) provided immediate switching between layers or enabling of modifier keys, since the to dictionary doesn't wait for the to_if_held_down_threshold_milliseconds timeout. This means sending the to event even when to_if_alone is sent , but so long as the event is 'harmless', or not 'chatty', i.e. it is a layer, a modifier, or a notification, and so long as we set the lazy opt when necessary (e.g. for my mapping of entercontrol when held), this doesn't cause any significant side effects.

When a 'chatty' event would have side-effects, or when sending an event on 'if held down' following the relevant parameters is an explicit goal (rather than just a way of distinguishing a tap from a hold), karaml can handle that by making the distinction between 'chatty/harmless' events and otherwise. In these cases, karml will place the 'when-held' event in the to_if_held_down dictionary to prevent 'chatter' caused by events in the to dictionary.

In short, karaml is opinionated about layers and modifiers, and is designed to act with zero delay given that most maps can be handled by reaching another layer via tap-to-toggle or hold-to-enable. With this in mind, it tries to reduce the need to manually specify 'companion' mappings for layer toggling, e.g. for an 'x sets y = 1 if variable y = 0' mapping, karaml will automatically generate a 'x sets y = 0 if variable y = 1' mapping, and for 'x sets y = 1 if x is held down', karaml will automatically generate 'x sets y = 0 if x is released'.

I try to handle as many other cases as possible in a concise manner in the karaml style, but as a fallback, a json: map can be added to the config to integrate a regular Karabiner JSON config (with minor YAML modifications). In fact, you could just use karaml as a way to write a regular Karabiner config in YAML but with less commas and little to no quotation marks.

🔩 Requirements

  • Python >= 3.10

This program was written while using Karabiner-Elements 14.11.0 and MacOS 12.5.1

📦 Installation

Clone this repo and install with pip in your terminal:

git clone https://github.com/al-ce/karaml.git
cd karaml
pip install .

🚀 Usage

After you've made a well-formed karaml config, open your terminal app.

The karaml command requires one positional argument: the name of the YAML file with your karaml config in the current folder (or the relative/absolute path to that file).

karaml my_karaml_config.yaml

If your karaml maps aren't mapped correctly, the program will raise an exception and try to give you some information about what went wrong. The error system needs improvement - working on it!

CLI prompt + modes

Without any optional arguments, you will be prompted to make a configuration choice:

$ karaml my_karaml_config.yaml
Reading from: my_karaml_config.yaml...

1. Update karabiner.json with my_karaml_config.yaml
2. Update complex modifications folder with my_karamlconfig_.yaml.
Writes to: karaml_complex_mods.json
3. Quit

1. updates your karabiner.json file directly, either creating a new profile or updating an existing one depending on the profile_name key in your config. Before every update, karaml makes a backup copy of your previous karabiner.json in the automatic_backups folder. This can add up! But I didn't want to risk a bug in the program destroying your config.

2. writes a JSON file to your karabiner complex modifications folder which you can import with the Karabiner-Elements GUI. Then you can enable or disable layers individually, but you should enable them all at once to ensure your layer and mapping priority is setup as intended. The file is named karaml_complex_mods.json by default, but you can change this in your karaml config with the title key, (and by doing so, you can easily switch between rulesets).

3. quits the program.

-k mode

By passing the -k flag, you will bypass the usual CLI prompt and karaml will update your karabiner.json file directly (as always, creating a backup beforehand).

karaml my_karaml_config.yaml -k

-c mode

By passing the -c flag, you will bypass the usual CLI prompt and karaml will update your complex modifications folder directly. If you're updating an old complex rule set, you'll have to remove the old complex modifications and re-enable the updated ones, as you would normally. No backup files will be created for complex modifications, but you will get a confirmation prompt to confirm the overwrite/update. If you do not provide a title map in your YAML config, karaml will default to writing/updating the 'Karaml rules' ruleset.

karaml my_karaml_config.yaml -c

-d (debug) mode

If there are malformed maps in your config, by default karaml prints you an error message and quits without making any changes to karbiner.json. By adding the -d flag, you can see the full stack trace of the error. If you're making a map that should work, either because it's a feature you think should be implemented or because you found a bug, please add the stack trace in an issue!

🪲 Known Issues / Bugs / Limitations

  • Can't toggle layer in 'when-tapped' position if also set in 'when-held' position (e.g. <a-f>: [/fn/, /fn/] only enables the /fn/ layer when held)
  • from.simultaneous_options not yet supported (this one will be tricky)
  • to_delayed_action not yet supported
  • halt option for to_if_held_down and to_if_alone not yet supported
  • Can't add multiple shell-based templates in one mapping (see this issue)

🌱 TODOs

  • More condition types (device_if, etc.)
  • More helpful configuration error messages

🔭 Alternatives

Releases

No releases published

Packages

No packages published

Languages