Karules: Ruby DSL For Karabiner JSONs - A Goku Alternative

by Alex Johnson 59 views

Karabiner-Elements is a powerful keyboard customizer for macOS, allowing you to remap keys and create complex macros. However, configuring Karabiner can be a daunting task, especially when dealing with complex setups. This is where Karules comes in – a Ruby DSL (Domain Specific Language) that simplifies the creation of Karabiner JSON configurations. If you're familiar with Goku but looking for a different style, Karules might be the perfect solution for you.

What is Karules?

Karules is a Ruby DSL designed to streamline the process of building Karabiner JSON configurations. It offers a more readable and maintainable syntax compared to manually writing JSON, making it easier to manage complex keyboard mappings. Karules is inspired by tools like Goku, but it offers a different approach to configuration, focusing on a more Ruby-centric style.

Why Use a DSL for Karabiner?

Configuring Karabiner-Elements directly with JSON can become cumbersome and difficult to manage, especially as your configuration grows. A DSL like Karules provides several advantages:

  • Readability: DSLs offer a more human-readable syntax, making your configuration easier to understand and modify.
  • Maintainability: With a structured approach, your configuration becomes easier to maintain and debug.
  • Reusability: DSLs allow you to define reusable components and functions, reducing code duplication.
  • Abstraction: DSLs abstract away the complexities of the underlying JSON structure, allowing you to focus on the logic of your key mappings.

Karules vs. Goku

Goku is another popular tool for building Karabiner configurations using a DSL. While both Karules and Goku aim to simplify Karabiner configuration, they differ in their approach. The creator of Karules found Goku to be challenging to understand and learn, which led to the development of Karules with a more intuitive Ruby-style syntax. If you've found Goku difficult to grasp, Karules might offer a more accessible alternative.

Key Features of Karules

Karules provides a set of features that make it a powerful tool for Karabiner configuration:

  • Ruby-based Syntax: Karules leverages the expressiveness of Ruby, allowing you to write configurations in a familiar and readable style. This makes it easier for Ruby developers to get started with Karabiner customization.
  • Modularity: Karules encourages modularity by allowing you to define groups of mappings and modes, making your configuration more organized and reusable. This is crucial for complex setups that involve multiple layers of key remappings and conditional behaviors.
  • Application-Specific Mappings: You can define mappings that are specific to certain applications, allowing you to tailor your keyboard layout to different workflows. This ensures that your key remappings are context-aware and don't interfere with other applications.
  • Modes: Karules supports the concept of modes, which allow you to create different layers of key mappings that can be toggled on and off. This is useful for creating function layers, mouse modes, and other advanced behaviors. Modes enable you to significantly expand the functionality of your keyboard without sacrificing the usability of standard keys.
  • Conditions: Karules allows you to define conditions for your mappings, so they are only active under certain circumstances. This is useful for creating mappings that are only active in certain applications or when certain modifiers are held down. Conditions add a layer of precision to your keyboard customization, ensuring that your remappings behave as expected in different contexts.

Getting Started with Karules

To get started with Karules, you'll need to have Ruby installed on your system. Once you have Ruby set up, you can install the karules gem:

gem install karules

After installing Karules, you can create a configuration file (e.g., ~/.config/karules/config.rb) and start defining your key mappings. Here's a basic example:

# frozen_string_literal: true

require "karules"

class MyKaRules < KaRules
  def config
    m("caps_lock -any", "left_control", to_if_alone: "escape")
  end
end

MyKaRules.new.call

This example remaps the Caps Lock key to Left Control, and if pressed alone, it acts as Escape. This is a common remapping that many users find helpful for improving keyboard ergonomics and workflow.

Example Configuration: A Deeper Dive

Let's explore a more comprehensive example to showcase the capabilities of Karules:

# frozen_string_literal: true

require "karules"

class MyKaRules < KaRules
  def key_mode(key, mode)
    m(
      key,
      to_if_alone: { key_code: key, halt: true },
      to_after_key_up: mode_off(mode),
      to_delayed_action: { to_if_canceled: { key_code: key }, to_if_invoked: mode_on(mode) },
      parameters: {
        "basic.to_if_held_down_threshold_milliseconds": 300,
        "basic.to_delayed_action_delay_milliseconds": 300
      }
    )
  end

  def config
    # Optional: specify custom Karabiner config path
    # Default: $XDG_CONFIG_HOME/karabiner/karabiner.json or ~/.config/karabiner/karabiner.json
    # karabiner_path "~/custom/path/karabiner.json"

    apps(slack: "^com\\.tinyspeck\\.slackmacgap{{content}}quot;, ghostty: "^com\\.mitchellh\\.ghostty{{content}}quot;)

    group("Caps Lock") do
      # m("caps_lock -any", "left_control", to_if_alone: "escape")
      m("caps_lock -any", "left_control")
    end

    group("Mouse buttons") do
      m("pointing_button:button5 -any", "f3") # mission control
      # m("pointing_button:button5 -any", "tab +right_command")
    end

    group("Tmux") do
      app_unless(:ghostty) do
        # Example: Focus terminal app, wait, then send Ctrl+A
        # Replace with your own terminal focus script
        m("a +control", ["!open -a 'Terminal'", { key_code: "vk_none", hold_down_milliseconds: 100 }, "a +control"])
      end
    end

    group("Tab mode") do
      m("tab", "right_option lazy", to_if_alone: "tab")

      m("j +right_option", "down_arrow")
      m("k +right_option", "up_arrow")

      app_if(:slack) do
        m("h +right_option", "f6 +shift")
        m("l +right_option", "f6")
        m("semicolon +right_option", "right_arrow")
      end

      m("h +right_option", "left_arrow")
      m("l +right_option", "right_arrow")

      m("w +right_option", "right_arrow +right_option")
      m("b +right_option", "left_arrow +right_option")
      m("u +right_option", "page_up")
      m("d +right_option", "page_down")
    end

    group("Mouse mode", enabled: false) do
      default_mode("mouse-mode")
      scroll = "mouse-scroll"

      step = 1000
      mult1 = 0.5
      mult2 = 2
      wheel = 50

      # m("fn -any", mode_on, to_if_alone: "fn", to_after_key_up: mode_off)

      key_mode("d", "mouse-mode")
      mode_if do
        m("left_shift +right_shift", mode_off)
        m("right_shift +left_shift", mode_off)
      end
      m("left_shift +right_shift", mode_on)
      m("right_shift +left_shift", mode_on)

      mode_if do
        mode_if(scroll) do
          m("j -any", { mouse_key: { vertical_wheel: wheel } })
          m("k -any", { mouse_key: { vertical_wheel: -wheel } })
          m("h -any", { mouse_key: { horizontal_wheel: wheel } })
          m("l -any", { mouse_key: { horizontal_wheel: -wheel } })
        end

        # normal movement
        m("j -any", { mouse_key: { y: step } })
        m("k -any", { mouse_key: { y: -step } })
        m("h -any", { mouse_key: { x: -step } })
        m("l -any", { mouse_key: { x: step } })

        # mode modifiers
        m("s -any", mode_on(scroll), to_after_key_up: mode_off(scroll))
        m("c -any", { mouse_key: { speed_multiplier: mult1 } })
        m("f -any", { mouse_key: { speed_multiplier: mult2 } })

        # buttons
        m("b -any", { pointing_button: "button1" })
        m("spacebar -any", { pointing_button: "button1" })
        m("n -any", { pointing_button: "button2" })

        # position
        m("u -any", { software_function: { set_mouse_cursor_position: { x: "20%", y: "20%" } } })
        m("i -any", { software_function: { set_mouse_cursor_position: { x: "80%", y: "20%" } } })
        m("o -any", { software_function: { set_mouse_cursor_position: { x: "20%", y: "80%" } } })
        m("p -any", { software_function: { set_mouse_cursor_position: { x: "80%", y: "80%" } } })
        m("m -any", { software_function: { set_mouse_cursor_position: { x: "50%", y: "50%" } } })
      end
    end

    group("MacOS double CmdQ") do
      default_mode("macos-q-command")
      m("q +command", "q +command", conditions: mode_if)
      m("q +command", mode_on, to_delayed_action: { to_if_canceled: mode_off, to_if_invoked: mode_off })
    end

    # Example: Switch between terminal windows/tabs
    # Replace with your own terminal switching script
    max = 9
    group("terminal 1-" + max.to_s) do
      app_if(:ghostty) do
        (1..max).each { |i| m("#{i} +left_command", "#{i} +command") }
      end

      (1..max).each { |i| m("#{i} +left_option", "#{i} +command") }
    end

    # Example: Application launcher shortcuts
    group("Apps") do
      m("j +right_command", "!open -a 'Terminal'")
      m("k +right_command", "!open -a 'Safari'")
      m("semicolon +right_command", "!open -a 'Mail'")

      m("f +right_command", "!open -a 'Finder'")
      m("s +right_command", "!open -a 'Slack'")
      m("c +right_command", "!open -a 'Google Chrome'")
      m("n +right_command", "!open -a 'Notes'")

      # You can also map to other key combinations
      m("t +right_command", "t +control +command +option")
    end
  end
end

MyKaRules.new.call

This configuration demonstrates several key features of Karules:

  • Key Modes: The key_mode method defines a reusable pattern for creating key mappings that toggle modes. This is used in the Mouse Mode section to define a key that activates mouse control when held down.
  • Application-Specific Mappings: The apps, app_if, and app_unless methods allow you to define mappings that are specific to certain applications. For example, the Tmux group includes mappings that are only active when Ghostty is not the active application.
  • Groups: The group method allows you to organize your mappings into logical groups. This makes your configuration more readable and maintainable. Groups can also be enabled or disabled, allowing you to easily toggle sections of your configuration.
  • Modes: The Mouse Mode section demonstrates the use of modes to create a separate layer of key mappings that are only active when the mode is enabled. This allows you to create complex behaviors without interfering with your standard key mappings.

Generating the Karabiner JSON

Once you've defined your configuration in Karules, you need to generate the Karabiner JSON file. You can do this by running your Ruby script:

ruby ~/.config/karules/config.rb

This will generate a karabiner.json file in the default Karabiner configuration directory (~/.config/karabiner/karabiner.json or $XDG_CONFIG_HOME/karabiner/karabiner.json). You can then load this configuration into Karabiner-Elements.

Community and Contributions

Karules is an open-source project, and contributions are welcome. If you have any comments, suggestions, or bug reports, feel free to submit them to the Karules GitHub repository. The Karules community is dedicated to making keyboard customization more accessible and efficient for everyone.

Conclusion

Karules offers a powerful and flexible way to build Karabiner JSON configurations using a Ruby DSL. Its readable syntax, modularity, and support for application-specific mappings and modes make it a great choice for both simple and complex keyboard customizations. If you're looking for an alternative to Goku or simply prefer a Ruby-style DSL, Karules is definitely worth exploring.

For more information on Karabiner-Elements and keyboard customization, visit the official Karabiner-Elements website.