├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── bin ├── karabiner └── karabiner-dsl ├── img ├── disabled.png └── enabled.png ├── karabiner.gemspec ├── lib ├── karabiner.rb └── karabiner │ ├── appdef.rb │ ├── cli.rb │ ├── config.rb │ ├── dsl │ ├── group.rb │ ├── item.rb │ └── root.rb │ ├── group.rb │ ├── history.rb │ ├── item.rb │ ├── key.rb │ ├── namespace.rb │ ├── property.rb │ ├── remap.rb │ ├── root.rb │ ├── version.rb │ ├── vkopenurldef.rb │ └── xml_tree.rb └── spec ├── lib ├── karabiner │ ├── appdef_spec.rb │ ├── item_spec.rb │ ├── karabiner_spec.rb │ ├── key_spec.rb │ └── remap_spec.rb └── karabiner_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | .yardoc 6 | Gemfile.lock 7 | InstalledFiles 8 | _yardoc 9 | coverage 10 | doc/ 11 | lib/bundler/man 12 | pkg 13 | rdoc 14 | spec/reports 15 | test/tmp 16 | test/version_tmp 17 | tmp 18 | *.bundle 19 | *.so 20 | *.o 21 | *.a 22 | mkmf.log 23 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | -c 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | rvm: 2 | - 2.3.0 3 | branches: 4 | only: 5 | - master 6 | script: 7 | - 'bundle exec rspec .' 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## [v0.4.0](https://github.com/k0kubun/karabiner-dsl/tree/v0.4.0) (2016-07-12) 4 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.3.1...v0.4.0) 5 | 6 | **Closed issues:** 7 | 8 | - Add support for consumer key [\#6](https://github.com/k0kubun/karabiner-dsl/issues/6) 9 | 10 | **Merged pull requests:** 11 | 12 | - Improve keycode support [\#7](https://github.com/k0kubun/karabiner-dsl/pull/7) ([ardecvz](https://github.com/ardecvz)) 13 | 14 | ## [v0.3.1](https://github.com/k0kubun/karabiner-dsl/tree/v0.3.1) (2016-05-17) 15 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.3.0...v0.3.1) 16 | 17 | ## [v0.3.0](https://github.com/k0kubun/karabiner-dsl/tree/v0.3.0) (2015-12-31) 18 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.2.1...v0.3.0) 19 | 20 | **Merged pull requests:** 21 | 22 | - Can invoke the application of any place [\#5](https://github.com/k0kubun/karabiner-dsl/pull/5) ([elim](https://github.com/elim)) 23 | 24 | ## [v0.2.1](https://github.com/k0kubun/karabiner-dsl/tree/v0.2.1) (2015-08-30) 25 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.2.0...v0.2.1) 26 | 27 | ## [v0.2.0](https://github.com/k0kubun/karabiner-dsl/tree/v0.2.0) (2015-07-08) 28 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.1.2...v0.2.0) 29 | 30 | ## [v0.1.2](https://github.com/k0kubun/karabiner-dsl/tree/v0.1.2) (2015-05-21) 31 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.1.1...v0.1.2) 32 | 33 | **Merged pull requests:** 34 | 35 | - Add supporting BACKQUOTE [\#4](https://github.com/k0kubun/karabiner-dsl/pull/4) ([cnosuke](https://github.com/cnosuke)) 36 | 37 | ## [v0.1.1](https://github.com/k0kubun/karabiner-dsl/tree/v0.1.1) (2014-11-16) 38 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.0.8...v0.1.1) 39 | 40 | ## [v0.0.8](https://github.com/k0kubun/karabiner-dsl/tree/v0.0.8) (2014-11-16) 41 | [Full Changelog](https://github.com/k0kubun/karabiner-dsl/compare/v0.1.0...v0.0.8) 42 | 43 | ## [v0.1.0](https://github.com/k0kubun/karabiner-dsl/tree/v0.1.0) (2014-11-16) 44 | 45 | 46 | \* *This Change Log was automatically generated by [github_changelog_generator](https://github.com/skywinder/Github-Changelog-Generator)* -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in karabiner.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2014 Takashi Kokubun 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Karabiner DSL [![Build Status](https://travis-ci.org/k0kubun/karabiner-dsl.svg?branch=master)](https://travis-ci.org/k0kubun/karabiner-dsl) 2 | 3 | Lightweight keyremap configuration DSL for [Karabiner](https://pqrs.org/osx/karabiner/index.html.en) 4 | 5 | ## Why Karabiner DSL? 6 | 7 | Original [Karabiner's configuration](https://pqrs.org/osx/karabiner/xml.html.en) is very hard to write. 8 | Karabiner DSL is its wrapper, which is easy-to-write and readable. 9 | 10 | If you write Karabiner's config with Karabiner DSL, you can update your keyremap configuration quickly. 11 | 12 | ## Installation 13 | 14 | First of all, you have to install [Karabiner](https://pqrs.org/osx/karabiner/index.html.en). 15 | Karabiner is a keyboard remap utility for Mac OSX. 16 | 17 | Then execute: 18 | 19 | ```bash 20 | $ gem install karabiner 21 | ``` 22 | 23 | Then `karabiner` executable will be installed. 24 | This gem provides only `karabiner dsl` subcommand and other subcommands are delegated to original CLI for Karabiner.app. 25 | 26 | ## Usage 27 | ### 1. Create ~/.karabiner 28 | 29 | ```rb 30 | item "Command+G to open Google Chrome" do 31 | remap "Cmd-g", to: invoke("Google Chrome") 32 | end 33 | ``` 34 | 35 | ### 2. Execute karabiner dsl command 36 | 37 | ```bash 38 | $ karabiner dsl 39 | ``` 40 | 41 | Then `karabiner dsl` will update Karabiner's config as you expected. 42 | 43 | ![](https://raw.githubusercontent.com/k0kubun/karabiner-dsl/master/img/disabled.png) 44 | 45 | ### 3. Enable your favorite configurations 46 | 47 | ![](https://raw.githubusercontent.com/k0kubun/karabiner-dsl/master/img/enabled.png) 48 | 49 | Enjoy! 50 | 51 | ## How to write ~/.karabiner 52 | ### Basics 53 | 54 | karabiner-dsl's DSL is a superset of Ruby. 55 | So you can use any Ruby methods in ~/.karabiner. 56 | 57 | #### item 58 | 59 | ```rb 60 | item "configuration unit" do 61 | ... 62 | end 63 | ``` 64 | 65 | In karabiner-dsl, any Karabiner's configuration unit is expressed in `item` and its `do ~ end` block. 66 | You can group some remap configurations in one item and enable them in one click. 67 | 68 | #### remap 69 | 70 | ```rb 71 | item "remap example" do 72 | remap "Cmd-a", to: "C-a" 73 | end 74 | ``` 75 | 76 | If you want to add remap configuration, you have to call `remap` method. 77 | In this example, Command+A will be remapped to Control+A. 78 | 79 | You have to write "key expression" to specify keys to remap. 80 | 81 | #### key expression 82 | 83 | - `a`, `A`, `1`, `;`, `tab`, `Tab`, `space`, `up`, `down` 84 | - any string without `-` or `+` will be regarded as a single key 85 | - strings are case insensitive 86 | - `Volume Mute`, `Mute`, `Brightness Down`, `Brightness_Down` 87 | - support system (consumer) keys 88 | - words could be separated by space ` ` or underscore `_` 89 | - nil, `nil`, `none` 90 | - you can drop any key by using nil or the corresponding aliases 91 | 92 | #### key combination 93 | 94 | | Key combination | Regarded as | Notes | 95 | | ---------------------------------------------------------- | --------------------- | ------ | 96 | | `C-a`, `Ctrl-a`, `C+a`, `Ctrl+a` | Control + A | `C-` is a short alias for `Ctrl-` | 97 | | `M-a`, `Opt-a`, `Alt-a`, `M+a`, `Opt+a`, `Alt+a` | Option + A | `M-` and `Alt-` are aliases for `Opt-` | 98 | | `Shift-a`, `Shift+a` | Shift + A (capital A) | if you write just `A`, it will be regarded as small a | 99 | | `Cmd-a`, `Cmd+a` | Command + A | | 100 | | `Cmd-Shift-a`, `Cmd+Shift-a`, `Cmd-Shift+a`, `Cmd+Shift+a` | Command + Shift + A | you can use any combination of Ctrl, Opt, Shift, Cmd | 101 | 102 | #### available single keys 103 | 104 | [Karabiner DSL keycode and alias reference](https://github.com/k0kubun/karabiner-dsl/blob/master/lib/karabiner/key.rb) 105 | 106 | [Karabiner full keycode reference](https://pqrs.org/osx/karabiner/xml.html.en#keycode-list) 107 | 108 | *Note:* Karabiner DSL is designed to work with all keycodes which are supported by Karabiner 109 | (listed in the full keycode reference). 110 | 111 | ``` 112 | a b c ... x y z 113 | 0 1 2 ... 7 8 9 114 | 115 | F1 F2 ... F11 F12 116 | \ [ ] ; ' ` , . / - = 117 | Up Down Right Left 118 | Space Tab Delete ... Forward_Delete Esc Capslock 119 | Mute Volume_Down Volume_Up ... Prev Play Next 120 | 121 | nil none 122 | 123 | Ctrl_R Ctrl_L 124 | Opt_R Opt_L Alt_R Alt_L 125 | Cmd_R Cmd_L 126 | Shift_R Shift_L 127 | ``` 128 | 129 | ## Sample 130 | 131 | ```rb 132 | item "Application shortcuts" do 133 | remap "C-o", to: invoke("YoruFukurou") # Invoke the app under the /Applications. 134 | remap "C-u", to: invoke("Google Chrome") # ditto. 135 | remap "C-h", to: invoke("/Users/johndoe/Applications/iTerm.app") # Invoke the app of the specified path. 136 | end 137 | 138 | item "Copy date" do 139 | remap "Cmd-d", to: execute("date|pbcopy") 140 | end 141 | 142 | item "Control+PNBF to Up/Down/Left/Right" do 143 | remap "C-p", to: "Up" 144 | remap "C-n", to: "Down" 145 | remap "C-b", to: "Left" 146 | remap "C-f", to: "Right" 147 | end 148 | 149 | appdef "HIPCHAT", equal: "com.hipchat.HipChat" 150 | item "HipChat Room Change", only: "HIPCHAT" do 151 | remap "Cmd-K", to: "Cmd-Shift-[" 152 | remap "Cmd-J", to: "Cmd-Shift-]" 153 | end 154 | 155 | item "Window change in the same app" do 156 | remap "Opt-tab", to: "Cmd-F1" 157 | end 158 | ``` 159 | 160 | ## Linux alternative 161 | You could achieve the same functionality on Linux/X11 with a sister project [xkremap](https://github.com/k0kubun/xkremap). 162 | It has a similar syntax and doesn't need any Ruby interpreter to run. 163 | 164 | ## Contributing 165 | 166 | 1. Fork it ( https://github.com/k0kubun/karabiner-dsl/fork ) 167 | 2. Create your feature branch (`git checkout -b my-new-feature`) 168 | 3. Commit your changes (`git commit -am 'Add some feature'`) 169 | 4. Push to the branch (`git push origin my-new-feature`) 170 | 5. Create a new Pull Request 171 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require "bundler/gem_tasks" 2 | 3 | task :spec do 4 | system("bundle exec rspec") 5 | end 6 | 7 | task default: :spec 8 | -------------------------------------------------------------------------------- /bin/karabiner: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.dirname(__FILE__) + "/../lib" 4 | $:.unshift lib unless $:.include? lib 5 | 6 | require "karabiner" 7 | 8 | DOTFILE_PATH = File.expand_path("~/.karabiner") 9 | 10 | if ARGV.first == "dsl" 11 | karabiner = Karabiner.new(DOTFILE_PATH) 12 | karabiner.apply_configuration 13 | else 14 | system("/Applications/Karabiner.app/Contents/Library/bin/karabiner #{ARGV.join(' ')}") 15 | end 16 | -------------------------------------------------------------------------------- /bin/karabiner-dsl: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | lib = File.dirname(__FILE__) + "/../lib" 4 | $:.unshift lib unless $:.include? lib 5 | 6 | require "karabiner" 7 | 8 | DOTFILE_PATH = File.expand_path("~/.karabiner") 9 | 10 | karabiner = Karabiner.new(DOTFILE_PATH) 11 | karabiner.apply_configuration 12 | -------------------------------------------------------------------------------- /img/disabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/karabiner-ruby/638a01112afd71c8158521a4a69198ec2710dd3e/img/disabled.png -------------------------------------------------------------------------------- /img/enabled.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/k0kubun/karabiner-ruby/638a01112afd71c8158521a4a69198ec2710dd3e/img/enabled.png -------------------------------------------------------------------------------- /karabiner.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | require 'karabiner/version' 5 | 6 | Gem::Specification.new do |spec| 7 | spec.name = "karabiner" 8 | spec.version = Karabiner::VERSION 9 | spec.authors = ["Takashi Kokubun"] 10 | spec.email = ["takashikkbn@gmail.com"] 11 | spec.summary = %q{Lightweight keyremap configuration DSL} 12 | spec.description = %q{Lightweight keyremap configuration DSL for Karabiner} 13 | spec.homepage = "https://github.com/k0kubun/karabiner-dsl" 14 | spec.license = "MIT" 15 | 16 | spec.files = `git ls-files -z`.split("\x0") 17 | spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) } 18 | spec.test_files = spec.files.grep(%r{^(test|spec|features)/}) 19 | spec.require_paths = ["lib"] 20 | 21 | spec.add_development_dependency "bundler" 22 | spec.add_development_dependency "rake", "~> 10.3.2" 23 | spec.add_development_dependency "rspec", "~> 3.0.0" 24 | spec.add_development_dependency "pry" 25 | spec.add_dependency 'unindent', '~> 1.0' 26 | end 27 | -------------------------------------------------------------------------------- /lib/karabiner.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/cli" 2 | require "karabiner/version" 3 | require "karabiner/root" 4 | require "unindent" 5 | require "fileutils" 6 | 7 | class Karabiner 8 | XML_FILE_NAME = "private.xml" 9 | XML_DIR = File.expand_path("~/Library/Application Support/Karabiner") 10 | 11 | def initialize(config_path) 12 | @config_path = config_path 13 | Karabiner::History.clear_histroy 14 | end 15 | attr_reader :config_path 16 | 17 | def apply_configuration 18 | replace_private_xml 19 | CLI.reload_xml 20 | 21 | puts "Successfully updated Karabiner configuration" 22 | end 23 | 24 | private 25 | 26 | def replace_private_xml 27 | FileUtils.mkdir_p(XML_DIR) 28 | 29 | xml_path = File.join(XML_DIR, XML_FILE_NAME) 30 | File.write(xml_path, new_xml) 31 | end 32 | 33 | def new_xml 34 | return @new_xml if defined?(@new_xml) 35 | validate_config_existence 36 | 37 | root = Root.new 38 | config = File.read(config_path) 39 | root.instance_eval(config) 40 | @new_xml = root.to_xml.gsub(/ *$/, "").concat("\n") 41 | end 42 | 43 | def validate_config_existence 44 | return if File.exists?(config_path) 45 | 46 | File.write(config_path, <<-EOS.unindent) 47 | #!/usr/bin/env ruby 48 | 49 | # # Example 50 | # item "Command+E to Command+W", not: "TERMINAL" do 51 | # identifier "option.not_terminal_opt_e" 52 | # autogen "__KeyToKey__ KeyCode::E, VK_COMMAND, KeyCode::W, ModifierFlag::COMMAND_L" 53 | # end 54 | EOS 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/karabiner/appdef.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/xml_tree" 2 | 3 | class Karabiner::Appdef 4 | include Karabiner::XmlTree 5 | 6 | AVAILABLE_OPTIONS = %i[ 7 | equal 8 | prefix 9 | suffix 10 | ].freeze 11 | 12 | def initialize(appname, options) 13 | property = Karabiner::Property.new("appname", appname) 14 | add_child(property) 15 | 16 | options.each do |option, value| 17 | raise "Unavailable option: #{property}" unless AVAILABLE_OPTIONS.include?(option) 18 | 19 | property = Karabiner::Property.new(option, value) 20 | add_child(property) 21 | end 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /lib/karabiner/cli.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/namespace" 2 | 3 | module Karabiner::CLI 4 | CLI_PATH = "/Applications/Karabiner.app/Contents/Library/bin/karabiner" 5 | 6 | def self.reload_xml 7 | system("#{CLI_PATH} reloadxml") 8 | end 9 | 10 | def self.current_config 11 | changed = `#{CLI_PATH} changed` 12 | config_by_changed(changed) 13 | end 14 | 15 | private 16 | 17 | def self.config_by_changed(changed) 18 | config = {} 19 | changed.each_line do |line| 20 | property, value = line.strip.split("=") 21 | config[property] = value 22 | end 23 | config 24 | end 25 | end 26 | -------------------------------------------------------------------------------- /lib/karabiner/config.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | require "karabiner/namespace" 3 | 4 | class Karabiner::Config 5 | extend Forwardable 6 | 7 | def_delegator :@parent, :item 8 | def_delegator :@parent, :group 9 | 10 | def initialize(name) 11 | @name = name 12 | end 13 | 14 | attr_writer :parent 15 | end 16 | -------------------------------------------------------------------------------- /lib/karabiner/dsl/group.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/namespace" 2 | require "karabiner/item" 3 | 4 | module Karabiner::DSL::Group 5 | def item(name = nil, options = {}, &block) 6 | item = Karabiner::Item.new(name, options) 7 | item.instance_exec(&block) 8 | add_child(item) 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /lib/karabiner/dsl/item.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/history" 2 | require "karabiner/namespace" 3 | require "karabiner/property" 4 | require "karabiner/remap" 5 | require "karabiner/vkopenurldef" 6 | 7 | module Karabiner::DSL::Item 8 | AVAILABLE_PROPERTIES = %i[ 9 | name 10 | identifier 11 | autogen 12 | ].freeze 13 | 14 | def remap(target, options = {}) 15 | remap = Karabiner::Remap.new(target, options[:to]) 16 | add_child(remap) 17 | end 18 | 19 | def show_message(message) 20 | property = Karabiner::Property.new("autogen", "__ShowStatusMessage__ #{message}") 21 | add_child(property) 22 | end 23 | 24 | def invoke(application) 25 | Karabiner::History.register_application(application) 26 | Karabiner::Vkopenurldef.application_keycode(application) 27 | end 28 | 29 | def execute(script) 30 | Karabiner::History.register_script(script) 31 | Karabiner::Vkopenurldef.script_keycode(script) 32 | end 33 | 34 | private 35 | 36 | def method_missing(property, value = '', options = {}) 37 | if AVAILABLE_PROPERTIES.include?(property) 38 | property = Karabiner::Property.new(property, value, options) 39 | add_child(property) 40 | else 41 | super 42 | end 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /lib/karabiner/dsl/root.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/namespace" 2 | require "karabiner/appdef" 3 | require "karabiner/config" 4 | require "karabiner/item" 5 | require "karabiner/group" 6 | require "karabiner/dsl/group" 7 | 8 | module Karabiner::DSL::Root 9 | include Karabiner::DSL::Group 10 | 11 | def group(name, &block) 12 | group = Karabiner::Group.new(name) 13 | group.instance_exec(&block) 14 | add_child(group) 15 | end 16 | 17 | def config(name, &block) 18 | config = Karabiner::Config.new(name) 19 | config.parent = self 20 | config.instance_exec(&block) 21 | add_config(config) 22 | end 23 | 24 | def appdef(appname = '', options = {}) 25 | appdef = Karabiner::Appdef.new(appname, options) 26 | add_child(appdef) 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /lib/karabiner/group.rb: -------------------------------------------------------------------------------- 1 | require "forwardable" 2 | require "karabiner/namespace" 3 | require "karabiner/dsl/group" 4 | 5 | class Karabiner::Group 6 | extend Forwardable 7 | include Karabiner::XmlTree 8 | include Karabiner::DSL::Group 9 | 10 | def_delegator :@item, :to_xml 11 | def_delegator :@item, :add_child 12 | 13 | def initialize(name) 14 | @item = Karabiner::Item.new(name, skip_identifier: true) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/karabiner/history.rb: -------------------------------------------------------------------------------- 1 | require "set" 2 | 3 | module Karabiner::History 4 | def self.clear_histroy 5 | registered_applications.clear 6 | registered_scripts.clear 7 | end 8 | 9 | def self.register_application(application) 10 | registered_applications.add(application) 11 | end 12 | 13 | def self.register_script(script) 14 | registered_scripts.add(script) 15 | end 16 | 17 | def self.registered_applications 18 | @@registered_applications ||= Set.new 19 | end 20 | 21 | def self.registered_scripts 22 | @@registered_scripts ||= Set.new 23 | end 24 | end 25 | -------------------------------------------------------------------------------- /lib/karabiner/item.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/dsl/item" 2 | require "karabiner/xml_tree" 3 | 4 | class Karabiner::Item 5 | include Karabiner::XmlTree 6 | include Karabiner::DSL::Item 7 | 8 | AVAILABLE_OPTIONS = %i[ 9 | not 10 | only 11 | config_not 12 | config_only 13 | device_not 14 | device_only 15 | ].freeze 16 | 17 | def initialize(name, options = {}) 18 | @skip_identifier = options.delete(:skip_identifier) 19 | 20 | if name 21 | property = Karabiner::Property.new("name", name) 22 | add_child(property) 23 | end 24 | 25 | options.each do |option, value| 26 | raise "Unavailable option: #{option}" unless AVAILABLE_OPTIONS.include?(option) 27 | 28 | property = Karabiner::Property.new(option, value) 29 | add_child(property) 30 | end 31 | end 32 | 33 | def to_xml 34 | validate_name_existence 35 | generate_identifier unless @skip_identifier 36 | 37 | super 38 | end 39 | 40 | private 41 | 42 | def validate_name_existence 43 | properties = search_children(Karabiner::Property) 44 | raise "Name property does not exist" unless properties.map(&:attr).include?("name") 45 | end 46 | 47 | def generate_identifier 48 | properties = search_children(Karabiner::Property) 49 | return if properties.map(&:attr).include?("identifier") 50 | 51 | name = properties.find { |p| p.attr == "name" } 52 | generated_identifier = name.value.gsub(/[^a-zA-Z0-9]/, "_").downcase 53 | identifier = Karabiner::Property.new("identifier", "remap.#{generated_identifier}") 54 | children[1, 0] = identifier 55 | end 56 | end 57 | -------------------------------------------------------------------------------- /lib/karabiner/key.rb: -------------------------------------------------------------------------------- 1 | class Karabiner::Key 2 | def self.normalize_input(raw_input) 3 | if raw_input.match(/^VK_/) 4 | raw_input 5 | else 6 | raw_input = raw_input.tr(' ', '_').tr('+', '-').upcase 7 | end 8 | end 9 | 10 | def self.normalize_and_freeze!(map) 11 | map.tap do |h| 12 | h.keys.each { |k| h[normalize_input(k)] = h.delete(k) } 13 | end 14 | map.freeze 15 | end 16 | 17 | # Karabiner full keycode reference: 18 | # https://pqrs.org/osx/karabiner/xml.html#keycode-list 19 | KEYCODE_MAP = normalize_and_freeze!({ 20 | "nil" => "VK_NONE", 21 | "none" => "VK_NONE", 22 | "0" => "KEY_0", 23 | "1" => "KEY_1", 24 | "2" => "KEY_2", 25 | "3" => "KEY_3", 26 | "4" => "KEY_4", 27 | "5" => "KEY_5", 28 | "6" => "KEY_6", 29 | "7" => "KEY_7", 30 | "8" => "KEY_8", 31 | "9" => "KEY_9", 32 | "Up" => "CURSOR_UP", 33 | "Down" => "CURSOR_DOWN", 34 | "Right" => "CURSOR_RIGHT", 35 | "Left" => "CURSOR_LEFT", 36 | "]" => "BRACKET_RIGHT", 37 | "[" => "BRACKET_LEFT", 38 | ";" => "SEMICOLON", 39 | "-" => "MINUS", 40 | "," => "COMMA", 41 | "." => "DOT", 42 | "\\" => "BACKSLASH", 43 | "/" => "SLASH", 44 | "=" => "EQUAL", 45 | "'" => "QUOTE", 46 | "`" => "BACKQUOTE", 47 | "Ctrl_R" => "CONTROL_R", 48 | "Ctrl_L" => "CONTROL_L", 49 | "Alt_R" => "OPTION_R", 50 | "Alt_L" => "OPTION_L", 51 | "Opt_R" => "OPTION_R", 52 | "Opt_L" => "OPTION_L", 53 | "Cmd_R" => "COMMAND_R", 54 | "Cmd_L" => "COMMAND_L", 55 | "Esc" => "ESCAPE", 56 | }) 57 | CONSUMER_MAP = normalize_and_freeze!({ 58 | "Brightness Down" => "BRIGHTNESS_DOWN", 59 | "Brightness Up" => "BRIGHTNESS_UP", 60 | "Keyboardlight Off" => "KEYBOARDLIGHT_OFF", 61 | "Keyboardlight Low" => "KEYBOARDLIGHT_LOW", 62 | "Keyboardlight High" => "KEYBOARDLIGHT_HIGH", 63 | "Keyboard Light Off" => "KEYBOARDLIGHT_OFF", 64 | "Keyboard Light Low" => "KEYBOARDLIGHT_LOW", 65 | "Keyboard Light High" => "KEYBOARDLIGHT_HIGH", 66 | "Music Prev" => "MUSIC_PREV", 67 | "Music Play" => "MUSIC_PLAY", 68 | "Music Next" => "MUSIC_NEXT", 69 | "Prev" => "MUSIC_PREV", 70 | "Play" => "MUSIC_PLAY", 71 | "Next" => "MUSIC_NEXT", 72 | "Volume Mute" => "VOLUME_MUTE", 73 | "Volume Down" => "VOLUME_DOWN", 74 | "Volume Up" => "VOLUME_UP", 75 | "Mute" => "VOLUME_MUTE", 76 | "Eject" => "EJECT", 77 | "Power" => "POWER", 78 | "Numlock" => "NUMLOCK", 79 | "Num Lock" => "NUMLOCK", 80 | "Video Mirror" => "VIDEO_MIRROR", 81 | }) 82 | PREFIX_MAP = normalize_and_freeze!({ 83 | "C" => "VK_CONTROL", 84 | "Ctrl" => "VK_CONTROL", 85 | "Cmd" => "VK_COMMAND", 86 | "Shift" => "VK_SHIFT", 87 | "M" => "VK_OPTION", 88 | "Opt" => "VK_OPTION", 89 | "Alt" => "VK_OPTION", 90 | }) 91 | PREFIX_EXPRESSION = "(#{PREFIX_MAP.keys.map { |k| k + '-' }.join('|')})" 92 | 93 | def initialize(expression) 94 | @expression = expression 95 | end 96 | 97 | def to_s 98 | key_combination(@expression) 99 | end 100 | 101 | private 102 | 103 | def key_combination(raw_combination) 104 | return "KeyCode::VK_NONE" if raw_combination.nil? 105 | raw_combination = normalize_key_combination(raw_combination) 106 | raw_prefixes, raw_key = split_key_combination(raw_combination) 107 | return key_expression(raw_key) if raw_prefixes.empty? 108 | 109 | prefixes = raw_prefixes.map { |raw_prefix| PREFIX_MAP[raw_prefix] } 110 | "#{key_expression(raw_key)}, #{prefixes.join(' | ')}" 111 | end 112 | 113 | def key_expression(raw_key) 114 | case raw_key 115 | when /^#{Regexp.union(KEYCODE_MAP.keys)}$/ 116 | "KeyCode::#{KEYCODE_MAP[raw_key]}" 117 | when /^#{Regexp.union(CONSUMER_MAP.keys)}$/ 118 | "ConsumerKeyCode::#{CONSUMER_MAP[raw_key]}" 119 | else 120 | "KeyCode::#{raw_key}" 121 | end 122 | end 123 | 124 | def normalize_key_combination(raw_combination) 125 | self.class.normalize_input(raw_combination) 126 | end 127 | 128 | def split_key_combination(raw_combination) 129 | prefixes = [] 130 | key = raw_combination.dup 131 | 132 | while key.match(/^#{PREFIX_EXPRESSION}/) 133 | key.gsub!(/^#{PREFIX_EXPRESSION}/) do 134 | prefixes << $1.gsub(/-$/, "") 135 | "" 136 | end 137 | end 138 | 139 | [prefixes, key] 140 | end 141 | end 142 | -------------------------------------------------------------------------------- /lib/karabiner/namespace.rb: -------------------------------------------------------------------------------- 1 | # Files which require this file can avoid deep nesting for class declaration 2 | class Karabiner 3 | module DSL 4 | end 5 | end 6 | -------------------------------------------------------------------------------- /lib/karabiner/property.rb: -------------------------------------------------------------------------------- 1 | class Karabiner::Property 2 | include Karabiner::XmlTree 3 | 4 | def initialize(attr, value, options = {}) 5 | @attr = attr.to_s 6 | @value = value 7 | @options = options 8 | end 9 | attr_accessor :attr, :value 10 | 11 | def to_xml 12 | open_tag = @options.map { |a, v| "#{a}=\"#{v}\"" }.unshift(attr).join(" ") 13 | "<#{open_tag}>#{value}" 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /lib/karabiner/remap.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/key" 2 | require "karabiner/property" 3 | 4 | class Karabiner::Remap < Karabiner::Property 5 | def initialize(from, to) 6 | tos = [to].flatten 7 | 8 | super( 9 | "autogen", 10 | [ 11 | "__KeyToKey__ #{Karabiner::Key.new(from)}", 12 | *tos.map { |to| Karabiner::Key.new(to) }, 13 | ].join(", "), 14 | ) 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /lib/karabiner/root.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/dsl/root" 2 | require "karabiner/history" 3 | require "karabiner/vkopenurldef" 4 | 5 | class Karabiner::Root 6 | include Karabiner::XmlTree 7 | include Karabiner::DSL::Root 8 | 9 | def initialize 10 | @configs = [] 11 | end 12 | 13 | def to_xml 14 | add_registered_applications 15 | add_registered_scripts 16 | 17 | [ 18 | "", 19 | super(1), 20 | ].join("\n") 21 | end 22 | 23 | private 24 | 25 | def add_registered_applications 26 | Karabiner::History.registered_applications.each do |application| 27 | vkopenurldef = Karabiner::Vkopenurldef.for_application(application) 28 | add_child(vkopenurldef) 29 | end 30 | end 31 | 32 | def add_registered_scripts 33 | Karabiner::History.registered_scripts.each do |script| 34 | vkopenurldef = Karabiner::Vkopenurldef.for_script(script) 35 | add_child(vkopenurldef) 36 | end 37 | end 38 | 39 | def add_config(config) 40 | @configs << config 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /lib/karabiner/version.rb: -------------------------------------------------------------------------------- 1 | class Karabiner 2 | VERSION = "0.4.0" 3 | end 4 | -------------------------------------------------------------------------------- /lib/karabiner/vkopenurldef.rb: -------------------------------------------------------------------------------- 1 | require "karabiner/xml_tree" 2 | 3 | class Karabiner::Vkopenurldef 4 | include Karabiner::XmlTree 5 | 6 | def self.application_keycode(application) 7 | "VK_OPEN_URL_APP_#{File.basename(application, '.app').gsub(/ /, "_")}" 8 | end 9 | 10 | def self.script_keycode(script) 11 | "VK_OPEN_URL_SHELL_#{script.gsub(/[^a-zA-Z]/, "_")}" 12 | end 13 | 14 | def self.for_application(application) 15 | self.new.tap do |definition| 16 | 17 | if application =~ %r|\A/.+app\z| 18 | app_url = application 19 | else 20 | app_url = "/Applications/#{application}.app" 21 | end 22 | 23 | name = Karabiner::Property.new("name", "KeyCode::#{application_keycode(application)}") 24 | url = Karabiner::Property.new("url", app_url, type: "file") 25 | definition.add_child(name, url) 26 | end 27 | end 28 | 29 | def self.for_script(script) 30 | self.new.tap do |definition| 31 | name = Karabiner::Property.new("name", "KeyCode::#{script_keycode(script)}") 32 | url = Karabiner::Property.new("url", "", type: "shell") 33 | definition.add_child(name, url) 34 | end 35 | end 36 | end 37 | -------------------------------------------------------------------------------- /lib/karabiner/xml_tree.rb: -------------------------------------------------------------------------------- 1 | module Karabiner::XmlTree 2 | attr_reader :parent 3 | 4 | def add_child(*objects) 5 | objects.each do |object| 6 | children << object 7 | end 8 | 9 | children.each do |child| 10 | child.parent = self 11 | end 12 | end 13 | 14 | def search_children(klass) 15 | children.select { |c| c.is_a?(klass) } 16 | end 17 | 18 | def to_xml(distance_between_children = 0) 19 | tag_name = self.class.to_s.split("::").last.downcase 20 | newline_count = distance_between_children + 1 21 | 22 | [ 23 | "<#{tag_name}>", 24 | children.map(&:to_xml).join("\n" * newline_count).gsub(/^/, " "), 25 | "", 26 | ].join("\n") 27 | end 28 | 29 | protected 30 | 31 | attr_writer :parent 32 | 33 | private 34 | 35 | def children 36 | @children ||= [] 37 | end 38 | 39 | def parent 40 | @parent 41 | end 42 | end 43 | -------------------------------------------------------------------------------- /spec/lib/karabiner/appdef_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Karabiner::Appdef do 4 | describe "#to_xml" do 5 | it "returns valid xml from appdef with equal" do 6 | appdef = Karabiner::Appdef.new("CHROME", equal: "com.google.Chrome") 7 | expect(appdef.to_xml).to eq(<<-EOS.unindent.strip) 8 | 9 | CHROME 10 | com.google.Chrome 11 | 12 | EOS 13 | end 14 | 15 | it "returns valid xml from appdef with prefix" do 16 | appdef = Karabiner::Appdef.new("CHROME", prefix: "com") 17 | expect(appdef.to_xml).to eq(<<-EOS.unindent.strip) 18 | 19 | CHROME 20 | com 21 | 22 | EOS 23 | end 24 | 25 | it "returns valid xml from appdef with suffix" do 26 | appdef = Karabiner::Appdef.new("CHROME", suffix: "Chrome") 27 | expect(appdef.to_xml).to eq(<<-EOS.unindent.strip) 28 | 29 | CHROME 30 | Chrome 31 | 32 | EOS 33 | end 34 | end 35 | end 36 | -------------------------------------------------------------------------------- /spec/lib/karabiner/item_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Karabiner::Item do 4 | describe "#to_xml" do 5 | it "generates valid identifier from name" do 6 | item = described_class.new("Change MUSIC_PLAY to 9") 7 | expect(item.to_xml).to eq(<<-EOS.unindent.strip) 8 | 9 | Change MUSIC_PLAY to 9 10 | remap.change_music_play_to_9 11 | 12 | EOS 13 | end 14 | end 15 | end 16 | -------------------------------------------------------------------------------- /spec/lib/karabiner/karabiner_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Karabiner::CLI do 4 | describe ".current_config" do 5 | subject { described_class.current_config } 6 | 7 | let(:cli_path) { Karabiner::CLI::CLI_PATH } 8 | 9 | before do 10 | allow_any_instance_of(Kernel).to receive(:'`').with("#{cli_path} changed").and_return(<<-EOS.unindent) 11 | remap.command_k_to_command_l=1 12 | repeat.initial_wait=100 13 | repeat.wait=20 14 | option.terminal_command_option=1 15 | notsave.automatically_enable_keyboard_device=1 16 | EOS 17 | end 18 | 19 | it "returns config hash" do 20 | expect(subject).to eq({ 21 | "option.terminal_command_option" => "1", 22 | "remap.command_k_to_command_l" => "1", 23 | "repeat.initial_wait" => "100", 24 | "repeat.wait" => "20", 25 | "notsave.automatically_enable_keyboard_device" => "1", 26 | }) 27 | end 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/lib/karabiner/key_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Karabiner::Key do 4 | describe ".normalize_input" do 5 | it { expect(described_class.normalize_input('Cmd+Volume Up')).to eq('CMD-VOLUME_UP') } 6 | end 7 | 8 | describe "#to_s" do 9 | EXPECTED_RESULTS = { 10 | "nil" => "KeyCode::VK_NONE", 11 | "none" => "KeyCode::VK_NONE", 12 | "A" => "KeyCode::A", 13 | "B" => "KeyCode::B", 14 | "C" => "KeyCode::C", 15 | "D" => "KeyCode::D", 16 | "E" => "KeyCode::E", 17 | "F" => "KeyCode::F", 18 | "G" => "KeyCode::G", 19 | "H" => "KeyCode::H", 20 | "I" => "KeyCode::I", 21 | "J" => "KeyCode::J", 22 | "K" => "KeyCode::K", 23 | "L" => "KeyCode::L", 24 | "M" => "KeyCode::M", 25 | "N" => "KeyCode::N", 26 | "O" => "KeyCode::O", 27 | "P" => "KeyCode::P", 28 | "Q" => "KeyCode::Q", 29 | "R" => "KeyCode::R", 30 | "S" => "KeyCode::S", 31 | "T" => "KeyCode::T", 32 | "U" => "KeyCode::U", 33 | "V" => "KeyCode::V", 34 | "W" => "KeyCode::W", 35 | "X" => "KeyCode::X", 36 | "Y" => "KeyCode::Y", 37 | "Z" => "KeyCode::Z", 38 | "0" => "KeyCode::KEY_0", 39 | "1" => "KeyCode::KEY_1", 40 | "2" => "KeyCode::KEY_2", 41 | "3" => "KeyCode::KEY_3", 42 | "4" => "KeyCode::KEY_4", 43 | "5" => "KeyCode::KEY_5", 44 | "6" => "KeyCode::KEY_6", 45 | "7" => "KeyCode::KEY_7", 46 | "8" => "KeyCode::KEY_8", 47 | "9" => "KeyCode::KEY_9", 48 | "Up" => "KeyCode::CURSOR_UP", 49 | "Down" => "KeyCode::CURSOR_DOWN", 50 | "Right" => "KeyCode::CURSOR_RIGHT", 51 | "Left" => "KeyCode::CURSOR_LEFT", 52 | "]" => "KeyCode::BRACKET_RIGHT", 53 | "[" => "KeyCode::BRACKET_LEFT", 54 | ";" => "KeyCode::SEMICOLON", 55 | "-" => "KeyCode::MINUS", 56 | "," => "KeyCode::COMMA", 57 | "." => "KeyCode::DOT", 58 | "\\" => "KeyCode::BACKSLASH", 59 | "/" => "KeyCode::SLASH", 60 | "=" => "KeyCode::EQUAL", 61 | "'" => "KeyCode::QUOTE", 62 | "`" => "KeyCode::BACKQUOTE", 63 | "Ctrl_R" => "KeyCode::CONTROL_R", 64 | "Ctrl_L" => "KeyCode::CONTROL_L", 65 | "Alt_R" => "KeyCode::OPTION_R", 66 | "Alt_L" => "KeyCode::OPTION_L", 67 | "Opt_R" => "KeyCode::OPTION_R", 68 | "Opt_L" => "KeyCode::OPTION_L", 69 | "Cmd_R" => "KeyCode::COMMAND_R", 70 | "Cmd_L" => "KeyCode::COMMAND_L", 71 | "Shift_R" => "KeyCode::SHIFT_R", 72 | "Shift_L" => "KeyCode::SHIFT_L", 73 | "Esc" => "KeyCode::ESCAPE", 74 | 75 | "Brightness Down" => "ConsumerKeyCode::BRIGHTNESS_DOWN", 76 | "Brightness Up" => "ConsumerKeyCode::BRIGHTNESS_UP", 77 | "Keyboardlight Off" => "ConsumerKeyCode::KEYBOARDLIGHT_OFF", 78 | "Keyboardlight Low" => "ConsumerKeyCode::KEYBOARDLIGHT_LOW", 79 | "Keyboardlight High" => "ConsumerKeyCode::KEYBOARDLIGHT_HIGH", 80 | "Keyboard Light Off" => "ConsumerKeyCode::KEYBOARDLIGHT_OFF", 81 | "Keyboard Light Low" => "ConsumerKeyCode::KEYBOARDLIGHT_LOW", 82 | "Keyboard Light High" => "ConsumerKeyCode::KEYBOARDLIGHT_HIGH", 83 | "Music Prev" => "ConsumerKeyCode::MUSIC_PREV", 84 | "Music Play" => "ConsumerKeyCode::MUSIC_PLAY", 85 | "Music Next" => "ConsumerKeyCode::MUSIC_NEXT", 86 | "Prev" => "ConsumerKeyCode::MUSIC_PREV", 87 | "Play" => "ConsumerKeyCode::MUSIC_PLAY", 88 | "Next" => "ConsumerKeyCode::MUSIC_NEXT", 89 | "Volume Mute" => "ConsumerKeyCode::VOLUME_MUTE", 90 | "Volume Down" => "ConsumerKeyCode::VOLUME_DOWN", 91 | "Volume Up" => "ConsumerKeyCode::VOLUME_UP", 92 | "Mute" => "ConsumerKeyCode::VOLUME_MUTE", 93 | "Eject" => "ConsumerKeyCode::EJECT", 94 | "Power" => "ConsumerKeyCode::POWER", 95 | "Numlock" => "ConsumerKeyCode::NUMLOCK", 96 | "Num Lock" => "ConsumerKeyCode::NUMLOCK", 97 | "Video Mirror" => "ConsumerKeyCode::VIDEO_MIRROR", 98 | }.freeze 99 | 100 | it "converts single key expression as expected" do 101 | EXPECTED_RESULTS.each do |expression, result| 102 | expect(described_class.new(expression).to_s).to eq(result) 103 | end 104 | end 105 | 106 | it "converts double key combination as expected" do 107 | Karabiner::Key::PREFIX_MAP.each do |prefix, vk| 108 | key, keycode = EXPECTED_RESULTS.to_a.sample 109 | expect(described_class.new("#{prefix}-#{key}").to_s).to eq("#{keycode}, #{vk}") 110 | end 111 | end 112 | 113 | it "converts triple key combination as expected" do 114 | key, keycode = EXPECTED_RESULTS.to_a.sample 115 | unique_maps = Karabiner::Key::PREFIX_MAP.to_a.sort_by { rand }.uniq { |a| a[1] } 116 | unique_maps.combination(2) do |(prefix1, vk1), (prefix2, vk2)| 117 | expect(described_class.new("#{prefix1}-#{prefix2}-#{key}").to_s).to eq("#{keycode}, #{vk1} | #{vk2}") 118 | end 119 | end 120 | 121 | it "converts key expression in case-insensitive manner" do 122 | EXPECTED_RESULTS.each do |expression, result| 123 | expect(described_class.new(expression.swapcase).to_s).to eq(result) 124 | end 125 | end 126 | 127 | it "accepts nil class as key expression" do 128 | expect(described_class.new(nil).to_s).to eq("KeyCode::VK_NONE") 129 | end 130 | end 131 | end 132 | -------------------------------------------------------------------------------- /spec/lib/karabiner/remap_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | 3 | describe Karabiner::Remap do 4 | describe "#to_xml" do 5 | it "converts key remap to autogen tag" do 6 | expect(Karabiner::Remap.new("Cmd-Shift-]", "Opt-Ctrl-Up").to_xml). 7 | to eq("__KeyToKey__ KeyCode::BRACKET_RIGHT, VK_COMMAND | VK_SHIFT, KeyCode::CURSOR_UP, VK_OPTION | VK_CONTROL") 8 | end 9 | end 10 | end 11 | -------------------------------------------------------------------------------- /spec/lib/karabiner_spec.rb: -------------------------------------------------------------------------------- 1 | require "spec_helper" 2 | require "tempfile" 3 | 4 | describe Karabiner do 5 | let!(:config) { Tempfile.new(".karabiner") } 6 | let(:xml_dir) { "/tmp" } 7 | let(:xml_path) { File.join(xml_dir, Karabiner::XML_FILE_NAME) } 8 | let(:result) { File.read(xml_path) } 9 | 10 | before do 11 | stub_const("Karabiner::XML_DIR", xml_dir) 12 | allow(Karabiner::CLI).to receive(:reload_xml) 13 | 14 | # Silence stdout 15 | allow_any_instance_of(Kernel).to receive(:puts) 16 | end 17 | 18 | after do 19 | config.close! 20 | end 21 | 22 | def prepare_karabiner(karabiner) 23 | config.write(karabiner) 24 | config.rewind 25 | end 26 | 27 | def expect_result(expected_result) 28 | karabiner = Karabiner.new(config.path) 29 | karabiner.apply_configuration 30 | expect(result).to eq(expected_result) 31 | end 32 | 33 | it "accepts blank config" do 34 | prepare_karabiner("") 35 | 36 | expect_result(<<-EOS.unindent) 37 | 38 | 39 | 40 | 41 | EOS 42 | end 43 | 44 | it "accepts cmd combination" do 45 | prepare_karabiner(<<-EOS) 46 | item "Command+A to Command+B" do 47 | remap "Cmd-A", to: "Cmd-B" 48 | end 49 | EOS 50 | 51 | expect_result(<<-EOS.unindent) 52 | 53 | 54 | 55 | Command+A to Command+B 56 | remap.command_a_to_command_b 57 | __KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND 58 | 59 | 60 | EOS 61 | end 62 | 63 | it "accepts multiple remaps" do 64 | prepare_karabiner(<<-EOS) 65 | item "multiple remaps" do 66 | remap "Cmd-A", to: "Cmd-B" 67 | remap "Shift-A", to: "Shift-B" 68 | end 69 | EOS 70 | 71 | expect_result(<<-EOS.unindent) 72 | 73 | 74 | 75 | multiple remaps 76 | remap.multiple_remaps 77 | __KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND 78 | __KeyToKey__ KeyCode::A, VK_SHIFT, KeyCode::B, VK_SHIFT 79 | 80 | 81 | EOS 82 | end 83 | 84 | it "accepts multiple items" do 85 | prepare_karabiner(<<-EOS) 86 | item "first item" do 87 | remap "Cmd-C-A", to: "Cmd-M-B" 88 | end 89 | 90 | item "second item" do 91 | remap "Shift-Opt-A", to: "Shift-Cmd-B" 92 | end 93 | EOS 94 | 95 | expect_result(<<-EOS.unindent) 96 | 97 | 98 | 99 | first item 100 | remap.first_item 101 | __KeyToKey__ KeyCode::A, VK_COMMAND | VK_CONTROL, KeyCode::B, VK_COMMAND | VK_OPTION 102 | 103 | 104 | 105 | second item 106 | remap.second_item 107 | __KeyToKey__ KeyCode::A, VK_SHIFT | VK_OPTION, KeyCode::B, VK_SHIFT | VK_COMMAND 108 | 109 | 110 | EOS 111 | end 112 | 113 | it "accepts appdef and app option" do 114 | prepare_karabiner(<<-EOS) 115 | appdef "CHROME", equal: "com.google.Chrome" 116 | 117 | item "Command+K to Command+L", only: "CHROME" do 118 | remap "Cmd-K", to: "Cmd-L" 119 | end 120 | EOS 121 | 122 | expect_result(<<-EOS.unindent) 123 | 124 | 125 | 126 | CHROME 127 | com.google.Chrome 128 | 129 | 130 | 131 | Command+K to Command+L 132 | remap.command_k_to_command_l 133 | CHROME 134 | __KeyToKey__ KeyCode::K, VK_COMMAND, KeyCode::L, VK_COMMAND 135 | 136 | 137 | EOS 138 | end 139 | 140 | it "accepts config and show_message" do 141 | prepare_karabiner(<<-EOS) 142 | item "CapsLock ON", config_not: "notsave.private_capslock_on" do 143 | remap "Cmd-L", to: ["capslock", "VK_CONFIG_FORCE_ON_notsave_private_capslock_on"] 144 | end 145 | 146 | item "CapsLock OFF", config_only: "notsave.private_capslock_on" do 147 | remap "Cmd-L", to: ["capslock", "VK_CONFIG_FORCE_OFF_notsave_private_capslock_on"] 148 | end 149 | 150 | item "CapsLock Mode" do 151 | identifier "notsave.private_capslock_on", vk_config: "true" 152 | show_message "CapsLock" 153 | end 154 | EOS 155 | 156 | expect_result(<<-EOS.unindent) 157 | 158 | 159 | 160 | CapsLock ON 161 | remap.capslock_on 162 | notsave.private_capslock_on 163 | __KeyToKey__ KeyCode::L, VK_COMMAND, KeyCode::CAPSLOCK, KeyCode::VK_CONFIG_FORCE_ON_notsave_private_capslock_on 164 | 165 | 166 | 167 | CapsLock OFF 168 | remap.capslock_off 169 | notsave.private_capslock_on 170 | __KeyToKey__ KeyCode::L, VK_COMMAND, KeyCode::CAPSLOCK, KeyCode::VK_CONFIG_FORCE_OFF_notsave_private_capslock_on 171 | 172 | 173 | 174 | CapsLock Mode 175 | notsave.private_capslock_on 176 | __ShowStatusMessage__ CapsLock 177 | 178 | 179 | EOS 180 | end 181 | 182 | it "accepts implicit autogen selection" do 183 | prepare_karabiner(<<-EOS) 184 | item "Control+LeftClick to Command+LeftClick" do 185 | autogen "__PointingButtonToPointingButton__ PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_CONTROL, PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_COMMAND" 186 | end 187 | EOS 188 | 189 | expect_result(<<-EOS.unindent) 190 | 191 | 192 | 193 | Control+LeftClick to Command+LeftClick 194 | remap.control_leftclick_to_command_leftclick 195 | __PointingButtonToPointingButton__ PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_CONTROL, PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_COMMAND 196 | 197 | 198 | EOS 199 | end 200 | 201 | it "application invoking" do 202 | prepare_karabiner(<<-EOS) 203 | item "Application shortcuts" do 204 | remap "C-o", to: invoke("YoruFukurou") 205 | remap "C-u", to: invoke("Google Chrome") 206 | remap "C-h", to: invoke("/Users/johndoe/Applications/iTerm.app") 207 | end 208 | 209 | item "duplicate app" do 210 | remap "C-a", to: invoke("YoruFukurou") 211 | end 212 | EOS 213 | 214 | expect_result(<<-EOS.unindent) 215 | 216 | 217 | 218 | Application shortcuts 219 | remap.application_shortcuts 220 | __KeyToKey__ KeyCode::O, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_YoruFukurou 221 | __KeyToKey__ KeyCode::U, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_Google_Chrome 222 | __KeyToKey__ KeyCode::H, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_iTerm 223 | 224 | 225 | 226 | duplicate app 227 | remap.duplicate_app 228 | __KeyToKey__ KeyCode::A, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_YoruFukurou 229 | 230 | 231 | 232 | KeyCode::VK_OPEN_URL_APP_YoruFukurou 233 | /Applications/YoruFukurou.app 234 | 235 | 236 | 237 | KeyCode::VK_OPEN_URL_APP_Google_Chrome 238 | /Applications/Google Chrome.app 239 | 240 | 241 | 242 | KeyCode::VK_OPEN_URL_APP_iTerm 243 | /Users/johndoe/Applications/iTerm.app 244 | 245 | 246 | EOS 247 | end 248 | 249 | it "accepts group items" do 250 | prepare_karabiner(<<-EOS) 251 | group "Option" do 252 | item "First" do 253 | identifier "option.option_first" 254 | end 255 | 256 | item "Second" do 257 | identifier "option.option_second" 258 | end 259 | end 260 | EOS 261 | 262 | expect_result(<<-EOS.unindent) 263 | 264 | 265 | 266 | Option 267 | 268 | First 269 | option.option_first 270 | 271 | 272 | Second 273 | option.option_second 274 | 275 | 276 | 277 | EOS 278 | end 279 | 280 | context "when items are surrounded by config" do 281 | it "accepts cmd combination" do 282 | prepare_karabiner(<<-EOS) 283 | config "Default" do 284 | item "Command+A to Command+B" do 285 | remap "Cmd-A", to: "Cmd-B" 286 | end 287 | end 288 | EOS 289 | 290 | expect_result(<<-EOS.unindent) 291 | 292 | 293 | 294 | Command+A to Command+B 295 | remap.command_a_to_command_b 296 | __KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND 297 | 298 | 299 | EOS 300 | end 301 | 302 | it "accepts group items" do 303 | prepare_karabiner(<<-EOS) 304 | config "Original" do 305 | group "Option" do 306 | item "First" do 307 | identifier "option.option_first" 308 | end 309 | 310 | item "Second" do 311 | identifier "option.option_second" 312 | end 313 | end 314 | end 315 | EOS 316 | 317 | expect_result(<<-EOS.unindent) 318 | 319 | 320 | 321 | Option 322 | 323 | First 324 | option.option_first 325 | 326 | 327 | Second 328 | option.option_second 329 | 330 | 331 | 332 | EOS 333 | end 334 | end 335 | end 336 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require "karabiner" 2 | require "pry" 3 | --------------------------------------------------------------------------------