karabiner 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,42 @@
1
+ module Karabiner::XmlTree
2
+ attr_reader :parent
3
+
4
+ def add_child(*objects)
5
+ objects.each do |object|
6
+ childs << object
7
+ end
8
+
9
+ childs.each do |child|
10
+ child.parent = self
11
+ end
12
+ end
13
+
14
+ def search_childs(klass)
15
+ childs.select { |c| c.is_a?(klass) }
16
+ end
17
+
18
+ def to_xml(distance_between_childs = 0)
19
+ tag_name = self.class.to_s.split("::").last.downcase
20
+ newline_count = distance_between_childs + 1
21
+
22
+ [
23
+ "<#{tag_name}>",
24
+ childs.map(&:to_xml).join("\n" * newline_count).gsub(/^/, " "),
25
+ "</#{tag_name}>",
26
+ ].join("\n")
27
+ end
28
+
29
+ protected
30
+
31
+ attr_writer :parent
32
+
33
+ private
34
+
35
+ def childs
36
+ @childs ||= []
37
+ end
38
+
39
+ def parent
40
+ @parent
41
+ end
42
+ end
data/lib/karabiner.rb ADDED
@@ -0,0 +1,56 @@
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::InvokeHistory.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
@@ -0,0 +1,35 @@
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
+ <appdef>
9
+ <appname>CHROME</appname>
10
+ <equal>com.google.Chrome</equal>
11
+ </appdef>
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
+ <appdef>
19
+ <appname>CHROME</appname>
20
+ <prefix>com</prefix>
21
+ </appdef>
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
+ <appdef>
29
+ <appname>CHROME</appname>
30
+ <suffix>Chrome</suffix>
31
+ </appdef>
32
+ EOS
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,29 @@
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
@@ -0,0 +1,113 @@
1
+ require "spec_helper"
2
+
3
+ describe Karabiner::Key do
4
+ describe "#to_s" do
5
+ EXPECTED_RESULTS = {
6
+ "a" => "KeyCode::A",
7
+ "b" => "KeyCode::B",
8
+ "c" => "KeyCode::C",
9
+ "d" => "KeyCode::D",
10
+ "e" => "KeyCode::E",
11
+ "f" => "KeyCode::F",
12
+ "g" => "KeyCode::G",
13
+ "h" => "KeyCode::H",
14
+ "i" => "KeyCode::I",
15
+ "j" => "KeyCode::J",
16
+ "k" => "KeyCode::K",
17
+ "l" => "KeyCode::L",
18
+ "m" => "KeyCode::M",
19
+ "n" => "KeyCode::N",
20
+ "o" => "KeyCode::O",
21
+ "p" => "KeyCode::P",
22
+ "q" => "KeyCode::Q",
23
+ "r" => "KeyCode::R",
24
+ "s" => "KeyCode::S",
25
+ "t" => "KeyCode::T",
26
+ "u" => "KeyCode::U",
27
+ "v" => "KeyCode::V",
28
+ "w" => "KeyCode::W",
29
+ "x" => "KeyCode::X",
30
+ "y" => "KeyCode::Y",
31
+ "z" => "KeyCode::Z",
32
+ "A" => "KeyCode::A",
33
+ "B" => "KeyCode::B",
34
+ "C" => "KeyCode::C",
35
+ "D" => "KeyCode::D",
36
+ "E" => "KeyCode::E",
37
+ "F" => "KeyCode::F",
38
+ "G" => "KeyCode::G",
39
+ "H" => "KeyCode::H",
40
+ "I" => "KeyCode::I",
41
+ "J" => "KeyCode::J",
42
+ "K" => "KeyCode::K",
43
+ "L" => "KeyCode::L",
44
+ "M" => "KeyCode::M",
45
+ "N" => "KeyCode::N",
46
+ "O" => "KeyCode::O",
47
+ "P" => "KeyCode::P",
48
+ "Q" => "KeyCode::Q",
49
+ "R" => "KeyCode::R",
50
+ "S" => "KeyCode::S",
51
+ "T" => "KeyCode::T",
52
+ "U" => "KeyCode::U",
53
+ "V" => "KeyCode::V",
54
+ "W" => "KeyCode::W",
55
+ "X" => "KeyCode::X",
56
+ "Y" => "KeyCode::Y",
57
+ "Z" => "KeyCode::Z",
58
+ "0" => "KeyCode::KEY_0",
59
+ "1" => "KeyCode::KEY_1",
60
+ "2" => "KeyCode::KEY_2",
61
+ "3" => "KeyCode::KEY_3",
62
+ "4" => "KeyCode::KEY_4",
63
+ "5" => "KeyCode::KEY_5",
64
+ "6" => "KeyCode::KEY_6",
65
+ "7" => "KeyCode::KEY_7",
66
+ "8" => "KeyCode::KEY_8",
67
+ "9" => "KeyCode::KEY_9",
68
+ "Up" => "KeyCode::CURSOR_UP",
69
+ "Down" => "KeyCode::CURSOR_DOWN",
70
+ "Right" => "KeyCode::CURSOR_RIGHT",
71
+ "Left" => "KeyCode::CURSOR_LEFT",
72
+ "]" => "KeyCode::BRACKET_RIGHT",
73
+ "[" => "KeyCode::BRACKET_LEFT",
74
+ ";" => "KeyCode::SEMICOLON",
75
+ "-" => "KeyCode::MINUS",
76
+ "," => "KeyCode::COMMA",
77
+ "." => "KeyCode::DOT",
78
+ "\\" => "KeyCode::BACKSLASH",
79
+ "/" => "KeyCode::SLASH",
80
+ "=" => "KeyCode::EQUAL",
81
+ "'" => "KeyCode::QUOTE",
82
+ "Ctrl_R" => "KeyCode::CONTROL_R",
83
+ "Ctrl_L" => "KeyCode::CONTROL_L",
84
+ "Opt_R" => "KeyCode::OPTION_R",
85
+ "Opt_L" => "KeyCode::OPTION_L",
86
+ "Cmd_R" => "KeyCode::COMMAND_R",
87
+ "Cmd_L" => "KeyCode::COMMAND_L",
88
+ "Shift_R" => "KeyCode::SHIFT_R",
89
+ "Shift_L" => "KeyCode::SHIFT_L",
90
+ }.freeze
91
+
92
+ it "converts single key expression as expected" do
93
+ EXPECTED_RESULTS.each do |expression, result|
94
+ expect(described_class.new(expression).to_s).to eq(result)
95
+ end
96
+ end
97
+
98
+ it "converts double key combination as expected" do
99
+ Karabiner::Key::PREFIX_MAP.each do |prefix, vk|
100
+ key, keycode = EXPECTED_RESULTS.to_a.sample
101
+ expect(described_class.new("#{prefix}-#{key}").to_s).to eq("#{keycode}, #{vk}")
102
+ end
103
+ end
104
+
105
+ it "converts triple key combination as expected" do
106
+ key, keycode = EXPECTED_RESULTS.to_a.sample
107
+ unique_maps = Karabiner::Key::PREFIX_MAP.to_a.sort_by { rand }.uniq { |a| a[1] }
108
+ unique_maps.combination(2) do |(prefix1, vk1), (prefix2, vk2)|
109
+ expect(described_class.new("#{prefix1}-#{prefix2}-#{key}").to_s).to eq("#{keycode}, #{vk1} | #{vk2}")
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,10 @@
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("<autogen>__KeyToKey__ KeyCode::BRACKET_RIGHT, VK_COMMAND | VK_SHIFT, KeyCode::CURSOR_UP, VK_OPTION | VK_CONTROL</autogen>")
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,335 @@
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
+ <?xml version="1.0"?>
38
+ <root>
39
+
40
+ </root>
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
+ <?xml version="1.0"?>
53
+ <root>
54
+ <item>
55
+ <name>Command+A to Command+B</name>
56
+ <identifier>remap.command_a_to_command_b</identifier>
57
+ <autogen>__KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND</autogen>
58
+ </item>
59
+ </root>
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
+ <?xml version="1.0"?>
73
+ <root>
74
+ <item>
75
+ <name>multiple remaps</name>
76
+ <identifier>remap.multiple_remaps</identifier>
77
+ <autogen>__KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND</autogen>
78
+ <autogen>__KeyToKey__ KeyCode::A, VK_SHIFT, KeyCode::B, VK_SHIFT</autogen>
79
+ </item>
80
+ </root>
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
+ <?xml version="1.0"?>
97
+ <root>
98
+ <item>
99
+ <name>first item</name>
100
+ <identifier>remap.first_item</identifier>
101
+ <autogen>__KeyToKey__ KeyCode::A, VK_COMMAND | VK_CONTROL, KeyCode::B, VK_COMMAND | VK_OPTION</autogen>
102
+ </item>
103
+
104
+ <item>
105
+ <name>second item</name>
106
+ <identifier>remap.second_item</identifier>
107
+ <autogen>__KeyToKey__ KeyCode::A, VK_SHIFT | VK_OPTION, KeyCode::B, VK_SHIFT | VK_COMMAND</autogen>
108
+ </item>
109
+ </root>
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
+ <?xml version="1.0"?>
124
+ <root>
125
+ <appdef>
126
+ <appname>CHROME</appname>
127
+ <equal>com.google.Chrome</equal>
128
+ </appdef>
129
+
130
+ <item>
131
+ <name>Command+K to Command+L</name>
132
+ <identifier>remap.command_k_to_command_l</identifier>
133
+ <only>CHROME</only>
134
+ <autogen>__KeyToKey__ KeyCode::K, VK_COMMAND, KeyCode::L, VK_COMMAND</autogen>
135
+ </item>
136
+ </root>
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
+ <?xml version="1.0"?>
158
+ <root>
159
+ <item>
160
+ <name>CapsLock ON</name>
161
+ <identifier>remap.capslock_on</identifier>
162
+ <config_not>notsave.private_capslock_on</config_not>
163
+ <autogen>__KeyToKey__ KeyCode::L, VK_COMMAND, KeyCode::CAPSLOCK, KeyCode::VK_CONFIG_FORCE_ON_notsave_private_capslock_on</autogen>
164
+ </item>
165
+
166
+ <item>
167
+ <name>CapsLock OFF</name>
168
+ <identifier>remap.capslock_off</identifier>
169
+ <config_only>notsave.private_capslock_on</config_only>
170
+ <autogen>__KeyToKey__ KeyCode::L, VK_COMMAND, KeyCode::CAPSLOCK, KeyCode::VK_CONFIG_FORCE_OFF_notsave_private_capslock_on</autogen>
171
+ </item>
172
+
173
+ <item>
174
+ <name>CapsLock Mode</name>
175
+ <identifier vk_config="true">notsave.private_capslock_on</identifier>
176
+ <autogen>__ShowStatusMessage__ CapsLock</autogen>
177
+ </item>
178
+ </root>
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
+ <?xml version="1.0"?>
191
+ <root>
192
+ <item>
193
+ <name>Control+LeftClick to Command+LeftClick</name>
194
+ <identifier>remap.control_leftclick_to_command_leftclick</identifier>
195
+ <autogen>__PointingButtonToPointingButton__ PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_CONTROL, PointingButton::LEFT, MODIFIERFLAG_EITHER_LEFT_OR_RIGHT_COMMAND</autogen>
196
+ </item>
197
+ </root>
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("iTerm")
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
+ <?xml version="1.0"?>
216
+ <root>
217
+ <item>
218
+ <name>Application shortcuts</name>
219
+ <identifier>remap.application_shortcuts</identifier>
220
+ <autogen>__KeyToKey__ KeyCode::O, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_YoruFukurou</autogen>
221
+ <autogen>__KeyToKey__ KeyCode::U, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_Google_Chrome</autogen>
222
+ <autogen>__KeyToKey__ KeyCode::H, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_iTerm</autogen>
223
+ </item>
224
+
225
+ <item>
226
+ <name>duplicate app</name>
227
+ <identifier>remap.duplicate_app</identifier>
228
+ <autogen>__KeyToKey__ KeyCode::A, VK_CONTROL, KeyCode::VK_OPEN_URL_APP_YoruFukurou</autogen>
229
+ </item>
230
+
231
+ <vkopenurldef>
232
+ <name>KeyCode::VK_OPEN_URL_APP_YoruFukurou</name>
233
+ <url type="file">/Applications/YoruFukurou.app</url>
234
+ </vkopenurldef>
235
+
236
+ <vkopenurldef>
237
+ <name>KeyCode::VK_OPEN_URL_APP_Google_Chrome</name>
238
+ <url type="file">/Applications/Google Chrome.app</url>
239
+ </vkopenurldef>
240
+
241
+ <vkopenurldef>
242
+ <name>KeyCode::VK_OPEN_URL_APP_iTerm</name>
243
+ <url type="file">/Applications/iTerm.app</url>
244
+ </vkopenurldef>
245
+ </root>
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
+ <?xml version="1.0"?>
264
+ <root>
265
+ <item>
266
+ <name>Option</name>
267
+ <item>
268
+ <name>First</name>
269
+ <identifier>option.option_first</identifier>
270
+ </item>
271
+ <item>
272
+ <name>Second</name>
273
+ <identifier>option.option_second</identifier>
274
+ </item>
275
+ </item>
276
+ </root>
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
+ <?xml version="1.0"?>
292
+ <root>
293
+ <item>
294
+ <name>Command+A to Command+B</name>
295
+ <identifier>remap.command_a_to_command_b</identifier>
296
+ <autogen>__KeyToKey__ KeyCode::A, VK_COMMAND, KeyCode::B, VK_COMMAND</autogen>
297
+ </item>
298
+ </root>
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
+ <?xml version="1.0"?>
319
+ <root>
320
+ <item>
321
+ <name>Option</name>
322
+ <item>
323
+ <name>First</name>
324
+ <identifier>option.option_first</identifier>
325
+ </item>
326
+ <item>
327
+ <name>Second</name>
328
+ <identifier>option.option_second</identifier>
329
+ </item>
330
+ </item>
331
+ </root>
332
+ EOS
333
+ end
334
+ end
335
+ end
@@ -0,0 +1,2 @@
1
+ require "karabiner"
2
+ require "pry"