sublime_dsl 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
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