karabiner.ts Documentation

website·Indexed 32 days ago

https://karabiner.ts.evanliu.dev/

A TypeScript-based configuration tool for Karabiner-Elements. It provides a type-safe way to define complex keyboard manipulators, layers (including hyper, duo, and simlayers), and rules, with utility methods like writeToProfile() to export configurations to JSON.

Tokens
8.9K
Snippets
59
Records
62
Agent score
51%

What's inside karabiner.ts

  1. Use karabiner.ts to define Karabiner-Elements complex modifications

    karabiner.ts is a TypeScript library that provides a fluent API for writing Karabiner-Elements complex_modifications. It abstracts the verbose JSON structure of karabiner.json into a chainable TypeScript syntax, allowing developers to define rules, manipulators, key mappings, and conditions more efficiently.
    writeToProfile('Default', [
      rule('Demo Rule')
        .manipulators([
          map('⇪')
            .to('⌫', '⌘')
            .condition(ifVar('test')),
        ]),
    ])
  2. Trigger multiple layers with a single key

    A single key can be used to trigger multiple layers by defining multiple layer() calls with the same trigger key. The system will combine the variable settings into a single manipulator to activate all specified modes simultaneously.
    layer('a', 'a-mode').manipulators([
      map(1).to(2),
    ])
    layer('a', 'b-mode').manipulators([
      map(3).to(4),
    ])
    // Holding 'a' activates both a-mode and b-mode
  3. Create a keyboard layer with layer()

    The layer() function creates a group of manipulators that are only active when a specific key is pressed and held. It works by setting a Karabiner variable to 1 when the trigger key is pressed and resetting it to 0 when released. The trigger key remains functional if tapped alone (to_if_alone).
    layer('a', 'a-mode').manipulators([
      map(1).to(2), // Only active when key 'a' is pressed and held
    ])
  4. Define conditions for Karabiner-Elements manipulators

    Conditions determine whether a manipulator should be active based on the current state of the system. In karabiner.ts, conditions are created using if*() helper methods. You can invert any condition by chaining the .unless() method to create the corresponding _unless variant (e.g., frontmost_application_if becomes frontmost_application_unless).
    // Example: Only trigger if the current app is NOT Finder
    ifApp('Finder').unless()
    
    // Example: Apply a condition to a specific rule and a specific manipulator
    rule('Finder', ifApp('Finder')).manipulators([
      map(1).to(2).condition(ifInputSource({ language: 'en' }).unless())
    ])
  5. Implement a simultaneous-activation layer using simlayer

    The simlayer function creates a layer that is activated when a specific 'layer key' is pressed and held, and another key within the layer is pressed within a threshold time (default 200ms). Once activated, the layer variable remains 1 until the layer key is released. This differs from a standard layer because it uses from.simultaneous functionality to trigger the initial activation.

    If the layer key is held longer than the threshold without another key being pressed, the layer key will repeat normally.

    simlayer('a', 'a-mode').manipulators([
      map(1).to(','),
      map(2).to('.'),
    ])
  6. Implement Vim-like leader mode using leaderMode()

    The leaderMode() method can be added to layer(), hyperLayer(), modifierLayer(), or duoLayer() to create a leader key behavior. In this mode, the layer remains active after the layer key is released until either a defined action key or an escape key is pressed. It is highly recommended to use .notification() when implementing leader mode to provide visual feedback to the user.
    hyperLayer('o')
      .description('Open App')
      .leaderMode()
      .notification()
      .manipulators({
        f: toApp('Finder'),
      })
  7. Implement nested leader keys using variables

    To create nested leader keys (sequences of keys that trigger different modes), use a variable to track the current state. The logic follows these steps:

    1. Initialize the Leader: Set a variable (e.g., leader) to a specific value (e.g., 1) when the primary leader key is pressed.
    2. Define Nested Leaders: When the primary leader is active, map specific keys to set the variable to a new unique value (e.g., o or r), effectively entering a sub-menu.
    3. Define Actions: Use withCondition to check for the specific nested leader variable value and map keys to their final actions.
    4. Reset State: Ensure all final action keys and the escape key unset the variable and clear any notifications to return to the default state.
    let raycastEmoji = 'raycast/emoji-symbols/search-emoji-symbols'
    let escape = [toUnsetVar('leader'), toRemoveNotificationMessage('leader')]
    
    let rules = [
      rule('Leader Key').manipulators([
        // 1. Primary Leader Key: Triggered when leader is 0 (unset)
        withCondition(ifVar('leader', 0))([
          map('l', 'Hyper')
            .toVar('leader', 1)
            .toNotificationMessage('leader', 'Leader Key: Open, Raycast, ...'),
        ]),
    
        // 2. Global Escape: Reset when any leader state is active
        withCondition(ifVar('leader', 0).unless())([
          map('escape').to(escape),
        ]),
    
        // 3. Nested Leader Keys: Triggered when primary leader (1) is active
        withCondition(ifVar('leader', 1))([
          withMapper(['o', 'r'])((x) =>
            map(x)
              .toVar('leader', x)
              .toNotificationMessage('leader', `leader ${x}`),
          ),
        ]),
    
        // 4. Actions for 'leader o' (Open)
        withCondition(ifVar('leader', 'o'))(
          [
            map('f').toApp('Finder'),
          ].map((x) => x.to(escape)),
        ),
    
        // 5. Actions for 'leader r' (Raycast)
        withCondition(ifVar('leader', 'r'))(
          [
            map('e').to$(`open raycast://extensions/${raycastEmoji}`),
          ].map((x) => x.to(escape)),
        ),
      ]),
    ]
  8. Prevent accidental layer activation using delay mode

    The .delay() method can be added to layer(), hyperLayer(), modifierLayer(), or duoLayer(). This prevents accidental layer activation during fast typing: if keys are pressed and released quickly, the original keys are output; if held down, the layer activates after a short delay. It is recommended to use .notification() alongside delay mode to provide visual feedback when the layer activates.
    duoLayer('j', 'k')
      .delay()
      .notification() // Notification is recommended with delay mode
      .manipulators({
        h: toKey('left_arrow'),
        l: toKey('right_arrow'),
      })
  9. Implement Duo-modifiers in Karabiner-Elements

    Duo-modifiers allow you to activate modifier keys (like Command, Option, Control, or Shift) by pressing two keys simultaneously instead of a single key. This approach is designed to mitigate the accidental triggers common with single-key home row mods.

    Key considerations:

    • Timing: Adjust the basic.simultaneous_threshold_milliseconds setting to match your typing speed and habits to ensure modifiers trigger reliably.
    • Feedback: It is recommended to use notifications when modifiers are activated during the initial setup phase to verify the configuration is working as intended.
    let rules = [
      rule('duo-modifiers').manipulators(
        duoModifiers({
          '⌘': ['fd', 'jk'], // Command: triggered by f+d or j+k
          '⌃': ['fs', 'jl'], // Control: triggered by f+s or j+l
          '⌥': ['fa', 'j;'], // Option: triggered by f+a or j+;
    
          '⇧': ['ds', 'kl'], // Shift
    
          '⌘⇧': ['gd', 'hk'], // Cmd+Shift
          '⌃⇧': ['gs', 'hl'], // Ctrl+Shift
          '⌥⇧': ['ga', 'h;'], // Opt+Shift
    
          '⌘⌥': ['vc', 'm,'], // Cmd+Opt
          '⌘⌃': ['vx', 'm.'], // Cmd+Ctrl
          '⌥⌃': ['cx', ',.'], // Opt+Ctrl
    
          '⌘⌥⌃': ['vz', 'm/'], // Cmd+Opt+Ctrl
        }),
      ),
    ]
    
    function duoModifiers(
      v: Partial<
        Record<
          '⌘' | '⌥' | '⌃' | '⇧' | MultiModifierAlias,
          `${LetterKeyCode | KeyAlias}${LetterKeyCode | KeyAlias}`[]
        >
      >,
    ) {
      let result = []
    
      for (let [m, k] of Object.entries(v)) {
        for (let keys of k) {
          let id = k + m
          let [firstMod, ...restMods] = (
            m in modifierKeyAliases
              ? [modifierKeyAliases[m as ModifierKeyAlias]]
              : multiModifierAliases[m as MultiModifierAlias]
          ) as Array<'command' | 'control' | 'option' | 'shift'>
    
          let to_after_key_up = [toRemoveNotificationMessage(id)]
          result.push(
            mapSimultaneous(keys.split('') as (LetterKeyCode | KeyAlias)[], {
              to_after_key_up,
            })
              .toNotificationMessage(id, m) // Must go first or to() doesn't work
              .to(`left_${firstMod}`, restMods),
          )
        }
      }
    
      return result
    }
  10. Import rules from another profile using importProfile()

    Use importProfile() to import all rules from a different existing Karabiner-Elements profile. This allows you to merge rules created by other tools into your karabiner.ts configuration. You can place importProfile() within the writeToProfile array to determine the order in which rules are applied (e.g., adding new rules before the imported profile).
    writeToProfile('karabiner.ts', [
      // ... karabiner.ts rules
    
      // Import all rules from the profile named 'ByAnotherTool'
      importProfile('ByAnotherTool'),
    ])
  11. Import JSON rules into karabiner.ts using importJson()

    Use importJson() to import existing JSON configuration files—such as those from shared rules or other tools—directly into your karabiner.ts setup without rewriting them. This is useful for importing rules stored in the default Karabiner assets directory (~/.config/karabiner/assets/complex_modifications/) or local project files.
    import { homedir } from 'node:os'
    import { resolve } from 'node:path'
    
    writeToProfile('Default', [
      // ... karabiner.ts rules
    
      // Import a shared rule from the default Karabiner assets directory
      importJson(
        resolve(
          homedir(),
          '.config/karabiner/assets/complex_modifications/1703535155.json',
        ),
      ),
    
      // Import a local JSON file within the project directory
      importJson(resolve(__dirname, './my-rules.json')),
    ])
  12. Apply shared conditions to all manipulators in a rule

    Conditions added directly to the rule() function (or via the .condition() method) are automatically applied to every manipulator within that group. This avoids repeating the same condition for every individual mapping.
    rule('Demo', ifVar('test')).manipulators([
      map(1).to(2),
    ])