karabiner.ts

repository·main·Indexed 43 days ago

https://github.com/evan-liu/karabiner.ts

A TypeScript and JavaScript framework for writing Karabiner-Elements configurations. It provides strongly-typed abstractions, high-level APIs for complex patterns like hyperLayer and leaderMode, and tools to manage key remapping more efficiently than raw JSON. Supports Node.js and Deno environments, and includes an online editor for generating configurations.

Tokens
23.5K
Snippets
93
Records
109
Agent score
51%

What's inside karabiner.ts

  1. What is karabiner.ts

    main

    karabiner.ts is a tool that allows you to write Karabiner-Elements configurations using TypeScript or JavaScript.

    It provides several advantages over the standard JSON format:

    • Strongly-typed abstractions: Use key aliases and abstractions with IDE support.
    • Structured configuration: Organize your mappings into multiple files instead of one large JSON file.
    • Advanced features: Built-in support for complex patterns like hyperLayer, duoLayer, and leaderMode.

    While TypeScript is recommended for its type safety, basic JavaScript is also supported.

  2. Use delayed layers to prevent accidental activation

    main

    Layers created via layer(), hyperLayer(), modifierLayer(), or duoLayer() support a "delay mode". This mode prevents accidental layer activation during fast typing.

    • Fast taps: If keys are pressed and released quickly, the layer is not activated and the original keys are outputted.
    • Holds: If a key is held down, the layer activates after a short delay.

    It is recommended to use .notification() alongside .delay() so you receive visual feedback when the layer actually activates.

    duoLayer('j', 'k')
      .delay()
      .notification() // Notification is recommended with delay mode
      .manipulators({
        h: toKey('left_arrow'),
        l: toKey('right_arrow'),
      })
  3. How duo layer works

    main

    A duoLayer is a hybrid abstraction that combines the mechanics of a layer and a simlayer:

    1. Simultaneous Triggering: Like a simlayer, it uses Karabiner's from.simultaneous functionality to detect when the two specified keys are pressed together to toggle a layer variable.
    2. Stateful Activation: Like a layer, the mappings within the layer are only active while the layer variable is set to 1 (which occurs when the trigger keys are pressed and held).
  4. How simlayer works

    main

    A simlayer is a specialized layer that uses Karabiner's simultaneous functionality to toggle a variable. Unlike a standard layer which toggles a variable immediately upon key press/release, a simlayer only sets the variable to 1 if the simlayer key is pressed and held, and then another key is pressed within a specific threshold time (defaulting to 200 milliseconds).

    Once the variable is set to 1, it remains active as long as the simlayer key is held down. When the simlayer key is released, the variable is set back to 0. This allows you to trigger specific mappings by pressing a key combination, and then use those mappings as a persistent layer until the first key is released.

    Note: If the second key is not pressed within the threshold time, the simlayer key will trigger its normal behavior (e.g., repeating if held).

    simlayer('a', 'a-mode').manipulators([
      map(1).to(','),
      map(2).to('.'),
    ])
  5. How doubleTap() works

    main

    The doubleTap() mechanism uses a variable-based state machine and a to_delayed_action to manage timing:

    1. First Tap: A variable (e.g., double-tap-up_arrow) is set to 1.
    2. Timer: A timer starts (defaulting to 200 milliseconds).
    3. Second Tap: If the key is pressed again while the variable is still 1, the .to() action is triggered.
    4. Timeout/Cancellation:
      • If a second tap occurs within the delay, the variable is reset to 0 after the action.
      • If no second tap occurs within the delay, the variable is reset to 0 and the original key (the from key) is triggered so that single taps still work.
      • If another key is pressed during the delay, the variable is reset to 0 (canceling the double-tap window).
  6. How Leader Mode works in karabiner.ts

    main

    Leader Mode allows a layer (created via layer(), hyperLayer(), modifierLayer(), or duoLayer()) to stay activated even after the initial layer key is released. This behavior is similar to Vim leader keys: the layer remains active until the user presses a defined action key or an escape key.

    When using Leader Mode, it is highly recommended to use .notification() so you have visual feedback that the layer is active.

    hyperLayer('o')
      .description('Open App')
      .leaderMode()
      .notification() // Notification is highly recommended when using leader mode
      .manipulators({
        f: toApp('Finder'),
      })
  7. How `layer()` works

    main

    A layer() creates a group of manipulators that are only active when a specific key is pressed and held.

    Internally, layer() performs three main actions:

    1. Adds a condition: It adds a variable_if condition to all manipulators within the group, checking if a specific variable (the layer name) is set to 1.
    2. Sets the variable: It configures the layer key to set that variable to 1 when pressed (to).
    3. Clears the variable: It configures the layer key to set the variable back to 0 when released (to_after_key_up).

    By default, the layer key remains functional as its original key if tapped alone via to_if_alone.

    layer('a', 'a-mode').manipulators([
      map(1).to(2), // Only active when key 'a' is pressed and held
    ])
    layer('a', 'a-mode').manipulators([
      map(1).to(2), // Only when key 'a' is pressed and held
    ])
  8. Use key aliases in karabiner.ts

    main

    To make configuration more readable, karabiner.ts supports several key aliases. These are particularly useful when used within layers or simlayers.

    Common Aliases:

    • Modifiers: ⌘ (command), ⌥ (option), ⌃ (control), ⇧ (shift), ⇪ (caps_lock)
    • Arrows: ↑ (up), ↓ (down), ← (left), → (right), ⇞ (page_up), ⇟ (page_down)
    • Navigation/System: ↖ (home), ↘ (end), ⏎ (return_or_enter), ⎋ (escape), ⌫ (delete_or_backspace), ⌦ (delete_forward), ⇥ (tab), ␣ (spacebar)

    Shorthand Tips:

    • map('‹⌘') is equivalent to left_command. Other variations include <⌘, l⌘, or left⌘.
    • map(1, '??') is equivalent to optionalAny. Other variations include ?any or { optional: 'any' }.
    • map(1, '?›⌘⌥') is equivalent to { optional: ['right_command', 'right_option'] }.
    // Example of using aliases in a layer
    layer(['z', '/'], 'emoji-mode').manipulators([
      withMapper(['⌘', '⌥', '⌃', '⇧', '⇪'])((k, i) =>
        map((i + 1) as NumberKeyValue).toPaste(k),
      ),
      map(',').toPaste('‹'),
    ])
  9. Set up karabiner.ts with Node.js (Scaffolding)

    main

    The fastest way to start a new project is using the create-karabiner-config CLI tool.

    To create a new project in a directory named karabiner-config:

    npx create-karabiner-config@latest

    To specify a custom project name:

    npx create-karabiner-config@latest [project-name]

    Workflow:

    1. Edit your key mappings in src/index.ts.
    2. Set your desired Karabiner-Elements profile name (ensure the profile exists in Karabiner-Elements).
    3. Run npm run build to generate the configuration. This will write directly to ~/.config/karabiner/karabiner.json, which Karabiner-Elements will automatically reload.

    To update your project dependencies, run npm run update or npm update karabiner.ts.

    npx create-karabiner-config@latest [project-name]
    
    # After editing src/index.ts
    npm run build
  10. Configure the simultaneous threshold

    main

    The simultaneous threshold (the time window in which keys must be pressed to count as simultaneous) can be configured in two ways:

    1. Per-call: Pass a numeric value as the third argument to mapSimultaneous().
    2. Globally for a profile: Set the basic.simultaneous_threshold_milliseconds parameter within writeToProfile().
    writeToProfile(
      '--dry-run', // profile name 
      [], // rules
      { 'basic.simultaneous_threshold_milliseconds': 100 }, // parameters 
    )