sublime_dsl 0.1.1

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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/README.md +136 -0
  3. data/Rakefile +248 -0
  4. data/SYNTAX.md +927 -0
  5. data/bin/subdsl +4 -0
  6. data/lib/sublime_dsl/cli/export.rb +134 -0
  7. data/lib/sublime_dsl/cli/import.rb +143 -0
  8. data/lib/sublime_dsl/cli.rb +125 -0
  9. data/lib/sublime_dsl/core_ext/enumerable.rb +24 -0
  10. data/lib/sublime_dsl/core_ext/string.rb +129 -0
  11. data/lib/sublime_dsl/core_ext.rb +4 -0
  12. data/lib/sublime_dsl/sublime_text/command.rb +157 -0
  13. data/lib/sublime_dsl/sublime_text/command_set.rb +112 -0
  14. data/lib/sublime_dsl/sublime_text/keyboard.rb +659 -0
  15. data/lib/sublime_dsl/sublime_text/keymap/dsl_reader.rb +194 -0
  16. data/lib/sublime_dsl/sublime_text/keymap.rb +385 -0
  17. data/lib/sublime_dsl/sublime_text/macro.rb +91 -0
  18. data/lib/sublime_dsl/sublime_text/menu.rb +237 -0
  19. data/lib/sublime_dsl/sublime_text/mouse.rb +149 -0
  20. data/lib/sublime_dsl/sublime_text/mousemap.rb +185 -0
  21. data/lib/sublime_dsl/sublime_text/package/dsl_reader.rb +91 -0
  22. data/lib/sublime_dsl/sublime_text/package/exporter.rb +138 -0
  23. data/lib/sublime_dsl/sublime_text/package/importer.rb +127 -0
  24. data/lib/sublime_dsl/sublime_text/package/reader.rb +102 -0
  25. data/lib/sublime_dsl/sublime_text/package/writer.rb +112 -0
  26. data/lib/sublime_dsl/sublime_text/package.rb +96 -0
  27. data/lib/sublime_dsl/sublime_text/setting_set.rb +123 -0
  28. data/lib/sublime_dsl/sublime_text.rb +48 -0
  29. data/lib/sublime_dsl/textmate/custom_base_name.rb +45 -0
  30. data/lib/sublime_dsl/textmate/grammar/dsl_reader.rb +383 -0
  31. data/lib/sublime_dsl/textmate/grammar/dsl_writer.rb +178 -0
  32. data/lib/sublime_dsl/textmate/grammar/plist_reader.rb +163 -0
  33. data/lib/sublime_dsl/textmate/grammar/plist_writer.rb +153 -0
  34. data/lib/sublime_dsl/textmate/grammar.rb +252 -0
  35. data/lib/sublime_dsl/textmate/plist.rb +141 -0
  36. data/lib/sublime_dsl/textmate/preference.rb +301 -0
  37. data/lib/sublime_dsl/textmate/snippet.rb +437 -0
  38. data/lib/sublime_dsl/textmate/theme/dsl_reader.rb +87 -0
  39. data/lib/sublime_dsl/textmate/theme/item.rb +74 -0
  40. data/lib/sublime_dsl/textmate/theme/plist_writer.rb +53 -0
  41. data/lib/sublime_dsl/textmate/theme.rb +364 -0
  42. data/lib/sublime_dsl/textmate.rb +9 -0
  43. data/lib/sublime_dsl/tools/blank_slate.rb +49 -0
  44. data/lib/sublime_dsl/tools/console.rb +74 -0
  45. data/lib/sublime_dsl/tools/helpers.rb +152 -0
  46. data/lib/sublime_dsl/tools/regexp_wannabe.rb +154 -0
  47. data/lib/sublime_dsl/tools/stable_inspect.rb +20 -0
  48. data/lib/sublime_dsl/tools/value_equality.rb +37 -0
  49. data/lib/sublime_dsl/tools/xml.rb +66 -0
  50. data/lib/sublime_dsl/tools.rb +66 -0
  51. data/lib/sublime_dsl.rb +23 -0
  52. metadata +145 -0
@@ -0,0 +1,659 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ ##
7
+ # A keyboard.
8
+
9
+ class Keyboard
10
+
11
+ SUBLIME_MODIFIERS = %w(shift ctrl alt super)
12
+
13
+ SUBLIME_KEYS = %w(
14
+ escape f1 f2 f3 f4 f5 f6 f7 f8 f9 f10 f11 f12 f13 f14 f15 f16 f17 f18 f19 f20 sysreq pause
15
+ up down right left
16
+ insert home end pageup pagedown backspace delete
17
+ tab enter context_menu
18
+ keypad0 keypad1 keypad2 keypad3 keypad4 keypad5 keypad6 keypad7 keypad8 keypad9
19
+ keypad_period keypad_divide keypad_multiply keypad_minus keypad_plus keypad_enter
20
+ clear break
21
+ browser_back browser_forward browser_refresh browser_stop
22
+ browser_search browser_favorites browser_home
23
+
24
+ ` 0 1 2 3 4 5 6 7 8 9 - = +
25
+ a b c d e f g h i j k l m n o p q r s t u v w x y z
26
+ [ ] \\ ; ' , . /
27
+ space
28
+ )
29
+
30
+ SUBLIME_ALIAS_MAP = {
31
+ 'forward_slash' => '/',
32
+ 'backquote' => '`',
33
+ 'equals' => '=',
34
+ 'plus' => '+',
35
+ 'minus' => '-'
36
+ }
37
+
38
+ SUBLIME_ALIAS_RE = /^(#{SUBLIME_ALIAS_MAP.keys.join('|')})$/
39
+
40
+ @defining_sublime = false
41
+
42
+ # The standard Sublime Text keyboard.
43
+ def self.sublime
44
+ @sublime ||= begin
45
+ kb = Keyboard.new('Sublime Text')
46
+ SUBLIME_MODIFIERS.each { |name| kb.add_modifier name }
47
+ SUBLIME_KEYS.each { |name| kb.add_key name }
48
+ # map_char and map_key call this method while @sublime is not yet set
49
+ unless @defining_sublime
50
+ @defining_sublime = true
51
+ # FIXME: space => key_event nil, but generates a key_event when modified
52
+ kb.map_char 'space' => ' ', key_event: nil
53
+ @defining_sublime = false
54
+ end
55
+ kb
56
+ end
57
+ end
58
+
59
+ def self.get(name, root)
60
+ file = nil
61
+ Dir.chdir(root) do
62
+ files = Dir['**/*.keyboard.rb']
63
+ file = SublimeText.order_config(files).last
64
+ file or raise Error, "file '#{name}.keyboard.rb' not found"
65
+ file = File.expand_path(file)
66
+ end
67
+ DSLReader.new(file)._keyboard
68
+ end
69
+
70
+ attr_reader :name, :os
71
+ attr_reader :modifiers, :keys # Key instances
72
+ attr_reader :keystrokes_hash # normalized spec => KeyStroke or CharStroke instance
73
+
74
+ def initialize(name)
75
+ @name = name
76
+ @os = nil
77
+ @modifiers = []
78
+ @keys = []
79
+ @keystrokes_hash = {}
80
+ # Vintage generic character
81
+ add_keystroke CharStroke.new('<character>')
82
+ end
83
+
84
+ def os=(value)
85
+ case value.to_s.downcase
86
+ when 'windows' then @os = 'Windows'
87
+ when 'osx' then @os = 'OSX'
88
+ when 'linux' then @os = 'Linux'
89
+ else raise Error, "invalid OS value: #{value}"
90
+ end
91
+ end
92
+
93
+ def modifier(name)
94
+ modifiers.find { |k| k.name == name }
95
+ end
96
+
97
+ def key(name)
98
+ keys.find { |k| k.name == name }
99
+ end
100
+
101
+ def add_modifier(name)
102
+ m = Key.new(name)
103
+ m.st_name = name if SUBLIME_MODIFIERS.include?(name)
104
+ @modifiers << m
105
+ end
106
+
107
+ def map_modifier(name, st_name)
108
+ m = modifier(name) or raise Error, "unknown modifier: '#{name}'"
109
+ SUBLIME_MODIFIERS.include?(st_name) or raise Error, "invalid ST modifier: #{st_name}"
110
+ m.st_name = st_name
111
+ end
112
+
113
+ # Create a Key +name+ and adds the corresponding KeyStroke to this keyboard.
114
+ #
115
+ # * If +name+ is one character long, the KeyStroke generates a chr_event +name+
116
+ # and no key_event.
117
+ #
118
+ # * Otherwise, the KeyStroke generates no chr_event, and a key_event +name+
119
+ # if +name+ is a Sublime Text key name.
120
+
121
+ def add_key(name)
122
+ k = Key.new(name)
123
+ k.st_name = name if SUBLIME_KEYS.include?(name)
124
+ @keys << k
125
+ ks = KeyStroke.new([], k)
126
+ if name.length == 1
127
+ ks.chr_event = name
128
+ else
129
+ ks.key_event = k.st_name
130
+ end
131
+ add_keystroke ks
132
+
133
+ k
134
+ end
135
+
136
+ # Assigns the key_event for the keystroke +spec+.
137
+ # +st_spec+ must be a valid ST keystroke, or nil if
138
+ # the keystroke is not seen by ST.
139
+
140
+ def map_key(spec, st_spec)
141
+ ks = ensure_keystroke(spec)
142
+ if st_spec.nil?
143
+ ks.key_event = nil
144
+ else
145
+ st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
146
+ st_spec = st_ks.to_spec
147
+ if ks.modifiers.empty?
148
+ ks.key.st_name = st_spec
149
+ ks.key_event = st_spec if st_spec.length > 1
150
+ else
151
+ ks.key_event = st_spec
152
+ end
153
+ end
154
+ end
155
+
156
+ # Map a keystroke to a chr_event.
157
+ # Optionally sets the key_event.
158
+ def map_char(options = {})
159
+ ks_name = options.keys.first
160
+ ks = ensure_keystroke(ks_name)
161
+ ks.chr_event = options[ks_name]
162
+ if options.has_key?(:key_event)
163
+ st_spec = options[:key_event]
164
+ if st_spec
165
+ st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
166
+ st_spec = st_ks.to_spec
167
+ end
168
+ ks.key_event = st_spec
169
+ end
170
+ end
171
+
172
+ def add_keystroke(ks)
173
+ @keystrokes_hash[ks.to_spec] = ks
174
+ end
175
+
176
+ def keystrokes
177
+ keystrokes_hash.values
178
+ end
179
+
180
+ def keystroke_for_sublime_spec(st_spec)
181
+ st_ks = Keyboard.sublime.ensure_keystroke(st_spec)
182
+ st_spec = st_ks.to_spec # standardize
183
+
184
+ # return the first one with a key event = the passed spec
185
+ this_ks = keystrokes.find { |ks| ks.key_event == st_spec }
186
+ return this_ks if this_ks
187
+
188
+ # if one char, no problem
189
+ return ensure_keystroke(st_spec) if st_spec.length == 1 || st_spec == '<character>'
190
+
191
+ # not (yet?) registered: find a keystroke with the same key
192
+ base_ks = keystrokes.find { |ks| ks.key && ks.key.st_name == st_ks.key.name }
193
+ if base_ks
194
+ this_spec = st_ks.modifiers.map(&:name).join('+') << '+' << base_ks.key.name
195
+ this_ks = ensure_keystroke(this_spec)
196
+ return this_ks
197
+ end
198
+
199
+ NullStroke.new(st_spec)
200
+ end
201
+
202
+ # Returns a KeyStroke or CharStroke for +spec+, and adds it to
203
+ # the registered keystrokes if not already there. Raises an
204
+ # exception if +spec+ is not valid.
205
+ #
206
+ # * If +spec+ is one character long, returns a KeyStroke or CharStroke
207
+ # if found, otherwise creates a new CharStroke.
208
+ #
209
+ # * If +spec+ is more than one character long, the key has to exist,
210
+ # as well as the modifiers if any, otherwise an exception is raised.
211
+ # If the corresponding KeyStroke is not found, it will be created
212
+ # and registered.
213
+
214
+ def ensure_keystroke(spec)
215
+
216
+ # split the specification
217
+ # ctrl++ -> ['ctrl', '+']
218
+ # ctrl+num+ -> ['ctrl', 'num+']
219
+ *modifier_names, key_name = spec.split(/\+(?!$)/)
220
+
221
+ # normalize the key name
222
+ self == Keyboard.sublime and
223
+ key_name.sub! SUBLIME_ALIAS_RE, SUBLIME_ALIAS_MAP
224
+
225
+ # check & reorder the modifiers
226
+ unless modifier_names.empty?
227
+ sorted = []
228
+ modifier_names.each do |name|
229
+ i = modifiers_index_hash[name] or
230
+ raise Error, "invalid modifier #{name.inspect} for keyboard #{self.name}"
231
+ sorted[i] = name
232
+ end
233
+ modifier_names = sorted.compact
234
+ end
235
+
236
+ # if there is a registered keystroke for this spec, return it
237
+ std_spec = [*modifier_names, key_name].join('+')
238
+ ks = keystrokes_hash[std_spec]
239
+ return ks if ks
240
+
241
+ # shift + character is not ok
242
+ modifiers = modifier_names.map { |n| modifier(n) }
243
+ modifiers.map(&:st_name) == ['shift'] && key_name.length == 1 and
244
+ raise Error, "#{spec.to_source(true)} is invalid: specify the corresponding character"
245
+
246
+ key = key(key_name)
247
+
248
+ # The ST keyboard accepts any character as a valid key
249
+ if self == Keyboard.sublime && key.nil?
250
+ key_name.length == 1 or raise Error, "invalid key name in #{spec.to_source(true)}"
251
+ key = add_key(key_name)
252
+ return keystrokes_hash[key_name] if modifiers.empty?
253
+ end
254
+
255
+ if key
256
+ # registered key
257
+ ks = KeyStroke.new(modifiers, key)
258
+ assign_default_key_event ks
259
+ else
260
+ # unregistered: has to be a character
261
+ key_name.length == 1 or
262
+ raise Error, "#{spec.inspect}: key #{key_name.inspect} is undefined"
263
+ modifier_names.empty? or
264
+ raise Error, "#{spec.inspect}: #{key_name.inspect} is not a key"
265
+ ks = CharStroke.new(key_name)
266
+ end
267
+
268
+ add_keystroke ks
269
+
270
+ ks
271
+ end
272
+
273
+ # Assign the default key_event of a new keystroke (before its registration).
274
+ # It will be the modified key_event of a less specific keystroke.
275
+ #
276
+ # For instance, if we register 'shift+ctrl+keypad5', and 'shift+keypad5' has
277
+ # key event 'clear', this assigns 'ctrl+clear'. If there are several
278
+ # possibilities, the one(s) with the most modifiers are selected.
279
+ # If there are ex-aequo, the order of precedence is the order of registration
280
+ # of the modifiers.
281
+
282
+ def assign_default_key_event(keystroke)
283
+
284
+ # the ST keyboard: just register
285
+ if self == Keyboard.sublime
286
+ spec = keystroke.to_spec
287
+ keystroke.key_event = spec if spec.length > 1
288
+ return
289
+ end
290
+
291
+ # we always have modifiers, as all non-modified keys are already registered
292
+ keystroke.modifiers.empty? and raise Error, "bug: #{keystroke} is not registered"
293
+
294
+ # if the key has a ST name, assume the ST equivalent
295
+ if keystroke.key.st_name
296
+ spec = keystroke.modifiers.map(&:st_name).join('+') << '+' << keystroke.key.st_name
297
+ keystroke.key_event = spec
298
+ return
299
+ end
300
+
301
+ # the candidates are the registered keystrokes for that key with all
302
+ # modifiers included in the passed modifiers (so at least the keystroke
303
+ # for the key itself)
304
+ candidates = keystrokes_hash.values.select do |ks|
305
+ ks.key == keystroke.key &&
306
+ ks.modifiers - keystroke.modifiers == []
307
+ end
308
+ candidates.empty? and raise Error, "bug: nothing registered for #{keystroke}"
309
+
310
+ if candidates.length > 1
311
+
312
+ # select the one(s) with the most modifiers
313
+ max = 0
314
+ candidates.each do |ks|
315
+ max = ks.modifiers.length if ks.modifiers.length > max
316
+ end
317
+ candidates.reject! { |ks| ks.modifiers.length < max }
318
+
319
+ # apply modifier priority:
320
+ # create the bit mask for each keystroke,
321
+ # and then select the lowest one
322
+ if candidates.length > 1
323
+ sort_array =
324
+ candidates.map do |ks|
325
+ mask = 0
326
+ ks.modifiers.each do |m|
327
+ mask += (1 << modifiers_index_hash[m.name])
328
+ end
329
+ [ks, mask]
330
+ end
331
+ candidates = sort_array.sort_by(&:last).map(&:first)
332
+ end
333
+
334
+ end
335
+
336
+ # select the reference keystroke
337
+ ref = candidates.first
338
+ if ref.key_event.nil?
339
+ keystroke.key_event = nil
340
+ return
341
+ end
342
+
343
+ # apply the modifier delta versus the reference
344
+ delta = keystroke.modifiers - ref.modifiers
345
+ spec = delta.map(&:st_name).join('+') << '+' << ref.key_event
346
+ keystroke.key_event = Keyboard.sublime.ensure_keystroke(spec).to_spec
347
+
348
+ end
349
+
350
+ def modifiers_index_hash
351
+ @modifiers_index_hash ||= begin
352
+ h = {}
353
+ modifiers.each_with_index { |m, i| h[m.name] = i }
354
+ h
355
+ end
356
+ end
357
+
358
+
359
+ ##
360
+ # A physical key or modifier on the keyboard.
361
+
362
+ class Key
363
+
364
+ attr_reader :name
365
+ attr_accessor :st_name
366
+
367
+ def initialize(name)
368
+ @name = name
369
+ @st_name = nil
370
+ end
371
+
372
+ def to_s
373
+ name
374
+ end
375
+
376
+ def eql?(other)
377
+ other.is_a?(Key) && other.name == self.name
378
+ end
379
+
380
+ alias == eql?
381
+
382
+ def hash
383
+ name.hash
384
+ end
385
+
386
+ end
387
+
388
+
389
+ ##
390
+ # A keystroke: modifiers + key.
391
+
392
+ class KeyStroke
393
+
394
+ attr_reader :key, :modifiers
395
+ attr_accessor :key_event
396
+ attr_accessor :chr_event
397
+ attr_accessor :chr_dead
398
+ attr_accessor :os_action
399
+
400
+ def initialize(modifiers, key)
401
+ @modifiers = modifiers
402
+ @key = key
403
+ @key_event = nil
404
+ @chr_event = nil
405
+ @chr_dead = false
406
+ @os_action = nil
407
+ end
408
+
409
+ def type
410
+ :key
411
+ end
412
+
413
+ def to_spec
414
+ (modifiers.dup << key).map(&:to_s).join('+')
415
+ end
416
+
417
+ alias to_s to_spec
418
+
419
+ def inspect
420
+ s = "<#KeyStroke #{to_spec}"
421
+ s << " key_event=#{key_event.inspect}"
422
+ s << " chr_event=#{chr_event.inspect}"
423
+ s << " dead=true" if chr_dead
424
+ s << " os_action=#{os_action.inspect}" if os_action
425
+ s
426
+ end
427
+
428
+ include Tools::ValueEquality
429
+
430
+ def <=>(other)
431
+ c = self.key <=> other.key
432
+ c = self.modifiers <=> other.modifiers if c == 0
433
+ c
434
+ end
435
+
436
+ end
437
+
438
+ ##
439
+ # A character: characters are supposed to be available on any keyboard.
440
+
441
+ class CharStroke
442
+
443
+ def initialize(char)
444
+ @char = char
445
+ end
446
+
447
+ def type
448
+ :char
449
+ end
450
+
451
+ def key
452
+ nil
453
+ end
454
+
455
+ def modifiers
456
+ []
457
+ end
458
+
459
+ def key_event
460
+ nil
461
+ end
462
+
463
+ def chr_event
464
+ @char
465
+ end
466
+
467
+ def chr_dead
468
+ nil
469
+ end
470
+
471
+ def os_action
472
+ nil
473
+ end
474
+
475
+ def to_spec
476
+ @char
477
+ end
478
+
479
+ alias to_s to_spec
480
+
481
+ def inspect
482
+ "<#CharStroke char=#{@char}>"
483
+ end
484
+
485
+ end
486
+
487
+
488
+ ##
489
+ # A ST keystroke that has no equivalent on this keyboard.
490
+
491
+ class NullStroke
492
+
493
+ def initialize(st_spec)
494
+ @key_event = st_spec
495
+ end
496
+
497
+ def type
498
+ :null
499
+ end
500
+
501
+ def key
502
+ nil
503
+ end
504
+
505
+ def modifiers
506
+ []
507
+ end
508
+
509
+ def key_event
510
+ @key_event
511
+ end
512
+
513
+ def chr_event
514
+ nil
515
+ end
516
+
517
+ def chr_dead
518
+ nil
519
+ end
520
+
521
+ def os_action
522
+ nil
523
+ end
524
+
525
+ def to_spec
526
+ nil
527
+ end
528
+
529
+ def inspect
530
+ "<#NullStroke key_event=#{@key_event}>"
531
+ end
532
+
533
+ end
534
+
535
+
536
+ class DSLReader
537
+
538
+ attr_reader :_keyboard
539
+
540
+ def initialize(file)
541
+ @_keyboard = nil
542
+ @in_definition = false
543
+ instance_eval ::File.read(file, encoding: 'utf-8'), file
544
+ end
545
+
546
+ def method_missing(sym, *args, &block)
547
+ raise Error, "'#{sym}' is not a keyboard DSL statement"
548
+ end
549
+
550
+ def keyboard(name)
551
+ @_keyboard and raise Error, 'only one keyboard definition per file'
552
+ @in_definition and raise Error, "'keyboard' blocks cannot be nested"
553
+ @_keyboard = Keyboard.new(name)
554
+ @in_definition = true
555
+ yield self
556
+ @in_definition = false
557
+ end
558
+
559
+ def os(value)
560
+ ensure_context __method__
561
+ _keyboard.os = value
562
+ end
563
+
564
+ def add_modifiers(spec)
565
+ ensure_context __method__
566
+ spec.split(/\s/).each { |name| _keyboard.add_modifier name }
567
+ end
568
+
569
+ def map_modifier(options={})
570
+ ensure_context __method__
571
+ name = options.keys.first
572
+ name or raise Error, 'missing argument'
573
+ st_name = options.delete(name)
574
+ options.empty? or warn "extraneous arguments ignored: #{options.inspect}"
575
+ _keyboard.map_modifier name, st_name
576
+ end
577
+
578
+ def add_keys(spec, options={})
579
+ ensure_context __method__
580
+ keys = parse_key_list(spec)
581
+
582
+ st_spec = options.delete(:st_keys)
583
+ if st_spec
584
+ st_keys = parse_key_list(st_spec)
585
+ keys.length == st_keys.length or
586
+ raise Error, "st_keys: got #{st_keys.length} keys, expected #{keys.length}"
587
+ end
588
+ options.empty? or warn "extraneous arguments ignored: #{options.inspect}"
589
+
590
+ keys.each_with_index do |name, i|
591
+ _keyboard.add_key name
592
+ if st_spec
593
+ _keyboard.map_key name, st_keys[i]
594
+ # done automatically when registering key events of new keystrokes:
595
+ # elsif Keyboard.sublime.key(name)
596
+ # _keyboard.map_key name, name
597
+ end
598
+ end
599
+
600
+ end
601
+
602
+ def map_key(options={})
603
+ ensure_context __method__
604
+ name = options.keys.first
605
+ name or raise Error, 'missing argument'
606
+ st_name = options.delete(name)
607
+ options.empty? or warn "extraneous arguments ignored: #{options.inspect}"
608
+ _keyboard.map_key name, st_name
609
+ end
610
+
611
+ def map_char(options={})
612
+ spec = options.keys.first
613
+ spec or raise Error, 'missing argument'
614
+ char = options.delete(spec)
615
+ dead = options.delete(:dead)
616
+ options.empty? or warn "extraneous arguments ignored: #{options.inspect}"
617
+ char.length == 1 or raise Error, "map_dead: expected a character, got #{char.inspect}"
618
+ ks = _keyboard.ensure_keystroke(spec)
619
+ ks.chr_event = char
620
+ ks.chr_dead = true if dead
621
+ end
622
+
623
+ def os_action(options={})
624
+ spec = options.keys.first
625
+ spec or raise Error, 'missing argument'
626
+ action = options.delete(spec)
627
+ key_event = options.delete(:key_event)
628
+ options.empty? or warn "extraneous arguments ignored: #{options.inspect}"
629
+ ks = _keyboard.ensure_keystroke(spec)
630
+ ks.os_action = action
631
+ ks.key_event = nil unless key_event
632
+ end
633
+
634
+ private
635
+
636
+ def ensure_context(method)
637
+ _keyboard or raise Error, "'#{method}' is invalid outside of a 'keyboard' block"
638
+ end
639
+
640
+ def parse_key_list(string)
641
+ specs = string.split(/\s/)
642
+ specs.flat_map do |spec|
643
+ if spec =~ /^([a-z]*)(\d+)-\1(\d+)$/
644
+ stem = $1
645
+ start = $2.to_i
646
+ stop = $3.to_i
647
+ (start..stop).map { |i| "#{stem}#{i}" }
648
+ else
649
+ spec
650
+ end
651
+ end
652
+ end
653
+
654
+ end
655
+
656
+ end
657
+
658
+ end
659
+ end