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,237 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class Menu
7
+
8
+ def self.import(file)
9
+ name = File.basename(file, File.extname(file))
10
+ set = new(name)
11
+ list = JSON[File.read(file, encoding: 'utf-8')]
12
+ set.items.concat list.map { |h| Item.from_json(h) }
13
+
14
+ set
15
+ end
16
+
17
+ attr_reader :name, :items
18
+
19
+ def initialize(name)
20
+ @name = name
21
+ @items = []
22
+ end
23
+
24
+ def to_dsl
25
+ dsl = "menu #{name.to_source} do\n\n"
26
+ items.each { |i| dsl << "#{i.to_dsl}\n" }
27
+ dsl << "\nend"
28
+ end
29
+
30
+ def write(dir)
31
+ file = "#{dir}/#{name}.menu.rb"
32
+ File.open(file, 'wb:utf-8') do |f|
33
+ f.puts '# encoding: utf-8'
34
+ f.puts "\n#{to_dsl}"
35
+ end
36
+ end
37
+
38
+ def export(dir)
39
+ file = "#{dir}/#{name}.sublime-menu"
40
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
41
+ end
42
+
43
+ def to_json
44
+ "[\n" <<
45
+ items.map { |i| i.to_json(' ') }.join(",\n") <<
46
+ "\n]"
47
+ end
48
+
49
+
50
+ class Item
51
+
52
+ def self.from_json(json_hash)
53
+ h = json_hash.dup
54
+ item = Item.new.tap do |i|
55
+ i.caption = h.delete('caption')
56
+ i.mnemonic = h.delete('mnemonic')
57
+ cmd = h.delete('command')
58
+ args = h.delete('args')
59
+ i.command = Command.new(cmd, args) if cmd
60
+ i.id = h.delete('id')
61
+ i.checkbox = h.delete('checkbox')
62
+ i.platform = h.delete('platform')
63
+ end
64
+ children = h.delete('children') || []
65
+ children.each do |c|
66
+ item.items << Item.from_json(c)
67
+ end
68
+ h.empty? or warn "unkown keys ignored: #{h.inspect}"
69
+
70
+ item
71
+ end
72
+
73
+ attr_accessor :command, :caption, :mnemonic, :id, :checkbox, :platform
74
+ attr_reader :items
75
+
76
+ def initialize()
77
+ @command = nil
78
+ @caption = nil
79
+ @mnemonic = nil
80
+ @id = nil
81
+ @checkbox = nil
82
+ @platform = nil
83
+ @items = []
84
+ end
85
+
86
+ def to_dsl(indent = ' ')
87
+ args = ''
88
+ options = []
89
+
90
+ if caption
91
+ cap = caption.gsub('&', '&&')
92
+ if mnemonic
93
+ if cap =~ /^(.*?)(#{mnemonic})(.*)$/i
94
+ args << "#{$1}&#{$2}#{$3}".to_source
95
+ else
96
+ args << cap.to_source
97
+ options << "mnemonic: #{mnemonic.to_source}"
98
+ end
99
+ else
100
+ args << cap.to_source
101
+ end
102
+ end
103
+
104
+ if command
105
+ args << ', ' unless args.empty?
106
+ args << command.to_dsl
107
+ options << "mnemonic: #{mnemonic.to_source}" if caption.nil? && mnemonic
108
+ end
109
+
110
+ options << "id: #{id.to_source}" if id
111
+ options << "checkbox: true" if checkbox
112
+ options << "platform: #{platform.to_source}" if platform
113
+
114
+ dsl = "#{indent}item #{args}"
115
+ unless options.empty?
116
+ dsl << ', ' unless args.empty?
117
+ dsl << options.join(', ')
118
+ end
119
+
120
+ unless items.empty?
121
+ i = indent + ' '
122
+ dsl << " do\n"
123
+ items.each do |c|
124
+ dsl << c.to_dsl(i) << "\n"
125
+ end
126
+ dsl << "#{indent}end"
127
+ end
128
+
129
+ dsl
130
+ end
131
+
132
+ def to_h(include_items = true)
133
+ h = {}
134
+ h['caption'] = caption if caption
135
+ h['mnemonic'] = mnemonic if mnemonic
136
+ h.merge! command.to_h if command
137
+ h['id'] = id if id
138
+ h['checkbox'] = checkbox if checkbox
139
+ h['platform'] = platform if platform
140
+ h['children'] = items.map(&:to_h) if include_items && !items.empty?
141
+ h
142
+ end
143
+
144
+ def to_json(indent)
145
+ return indent + JSON.generate(to_h) if items.empty?
146
+ json = indent + JSON.pretty_generate(to_h(false))
147
+ json = json[0..-3] # remove trailing "\n}"
148
+ json.gsub!("\n", "\n#{indent}")
149
+ json << %(,\n#{indent} "children": [\n)
150
+ ind = indent + ' '
151
+ json << items.map { |i| i.to_json(ind) }.join(",\n")
152
+ json << "\n#{indent} ]\n#{indent}}"
153
+ end
154
+
155
+ end
156
+
157
+
158
+ class DSLReader < Tools::BlankSlate
159
+
160
+ def initialize(file = nil)
161
+ @menus = []
162
+ @item_stack = []
163
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
164
+ end
165
+
166
+ def _menus
167
+ @menus
168
+ end
169
+
170
+ def menu(name, &block)
171
+ @item_stack.empty? or raise Error, "menu blocks cannot be nested"
172
+ @item_stack.push Menu.new(name)
173
+ instance_eval(&block)
174
+ @menus << @item_stack.pop
175
+ end
176
+
177
+ def item(*args, &block)
178
+ @item_stack.empty? and raise Error, "'item' is invalid outside of a menu block"
179
+ item = new_item(args)
180
+ @item_stack.last.items << item
181
+ return unless block
182
+ @item_stack.push item
183
+ instance_eval(&block)
184
+ @item_stack.pop
185
+ end
186
+
187
+ def method_missing(sym, *args, &block)
188
+ @item_stack.empty? and raise Error, "'#{sym}' is invalid outside of a menu block"
189
+ Command.from_method_missing(sym, args)
190
+ end
191
+
192
+ def new_item(args)
193
+ args.empty? and raise Error, "no argument for 'item'"
194
+ item = Item.new
195
+
196
+ # get the caption if any
197
+ if args.first.is_a?(String)
198
+ caption = args.shift
199
+ caption =~ /&([^& ])/ and item.mnemonic = $1.upcase
200
+ item.caption = caption.gsub(/&(.)/, '\1')
201
+ return item if args.empty?
202
+ end
203
+
204
+ # get the command if any
205
+ cmd = args.first
206
+ if cmd.is_a?(Command)
207
+ cmd.error and raise Error, "item '#{caption}': #{cmd.error}"
208
+ item.command = cmd
209
+ args.shift
210
+ return item if args.empty?
211
+ end
212
+
213
+ # options
214
+ options = args.first
215
+ args.length == 1 && options.is_a?(Hash) or
216
+ raise Error, "invalid arguments for 'item': #{args.inspect}"
217
+
218
+ item.id = options.delete(:id)
219
+ item.checkbox = options.delete(:checkbox)
220
+ item.platform = options.delete(:platform)
221
+ mnemonic = options.delete(:mnemonic)
222
+ if mnemonic
223
+ item.mnemonic && mnemonic != item.mnemonic and
224
+ warn "item '#{caption}': mnemonic #{item.mnemonic} overwritten by #{mnemonic}"
225
+ item.mnemonic = mnemonic
226
+ end
227
+
228
+ item
229
+ end
230
+
231
+ end
232
+
233
+
234
+ end
235
+
236
+ end
237
+ end
@@ -0,0 +1,149 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class Mouse
7
+
8
+ def self.sublime
9
+ @sublime ||= create_sublime_mouse
10
+ end
11
+
12
+ def self.create_sublime_mouse
13
+ mouse = new('Sublime Text')
14
+ (1..9).each { |i| mouse.add_button "button#{i}" }
15
+ mouse.add_button "scroll_up"
16
+ mouse.add_button "scroll_down"
17
+ %w(shift ctrl alt super).each { |m| mouse.add_modifier m }
18
+ mouse.modifiers.concat mouse.buttons
19
+ mouse
20
+ end
21
+
22
+ attr_reader :name, :buttons, :modifiers
23
+
24
+ def initialize(name)
25
+ @name = name
26
+ @buttons = []
27
+ @modifiers = []
28
+ @clicks = []
29
+ end
30
+
31
+ alias to_s name
32
+
33
+ def clone(name)
34
+ clone = Mouse.new(name)
35
+ buttons.each { |b| clone.add_button b.st_name, b.name }
36
+ (modifiers - buttons).each { |m| clone.add_modifier m.st_name, m.name }
37
+ clone.modifiers.concat clone.buttons
38
+ clone
39
+ end
40
+
41
+ def button(name)
42
+ buttons.find { |b| b.name == name }
43
+ end
44
+
45
+ def modifier(name)
46
+ modifiers.find { |b| b.name == name }
47
+ end
48
+
49
+ def add_button(st_name, name = nil)
50
+ @buttons << Button.new(st_name, name)
51
+ end
52
+
53
+ def add_modifier(st_name, name = nil)
54
+ @modifiers << Button.new(st_name, name)
55
+ end
56
+
57
+ def clicks_hash
58
+ @clicks_hash ||= {}
59
+ end
60
+
61
+ def modifiers_index_hash
62
+ @modifiers_index_hash ||= begin
63
+ h = {}
64
+ modifiers.each_with_index { |b,i| h[b.name] = i }
65
+ h
66
+ end
67
+ end
68
+
69
+ def ensure_click(spec)
70
+ *modifier_names, button_name = spec.split('+')
71
+
72
+ button = button(button_name) or
73
+ raise Error, "invalid button #{button_name.inspect}"
74
+
75
+ # check & reorder the modifiers
76
+ unless modifier_names.empty?
77
+ sorted = []
78
+ modifier_names.each do |name|
79
+ i = modifiers_index_hash[name]
80
+ i or raise Error, "invalid modifier #{name.inspect}"
81
+ sorted[i] = name
82
+ end
83
+ modifier_names = sorted.compact
84
+ end
85
+
86
+ # if there is a registered click for this spec, return it
87
+ std_spec = [*modifier_names, button_name].join('+')
88
+ click = clicks_hash[std_spec]
89
+ return click if click
90
+
91
+ # can't have the button in the modifiers
92
+ modifier_names.include?(button_name) and
93
+ raise Error, "can't have the button in the modifiers: #{spec.inspect}"
94
+
95
+ # create and register the click
96
+ modifiers = modifier_names.map { |n| modifier(n) }
97
+ click = Click.new(modifiers, button)
98
+ clicks_hash[click.to_spec] = click
99
+
100
+ click
101
+ end
102
+
103
+
104
+ ##
105
+ # A mouse button or modifier.
106
+
107
+ class Button
108
+
109
+ attr_reader :st_name
110
+ attr_accessor :name
111
+
112
+ def initialize(st_name, name)
113
+ @st_name = st_name
114
+ @name = name || st_name
115
+ end
116
+
117
+ end
118
+
119
+
120
+ ##
121
+ # A mouse click: button + modifiers.
122
+
123
+ class Click
124
+
125
+ attr_reader :button, :modifiers
126
+
127
+ def initialize(modifiers, button)
128
+ @modifiers = modifiers
129
+ @button = button
130
+ end
131
+
132
+ def to_spec
133
+ [*modifiers.map(&:name), button.name].join('+')
134
+ end
135
+
136
+ alias to_s to_spec
137
+
138
+ def to_h
139
+ h = { 'button' => button.st_name }
140
+ h['modifiers'] = modifiers.map(&:st_name) unless modifiers.empty?
141
+ h
142
+ end
143
+
144
+ end
145
+
146
+ end
147
+
148
+ end
149
+ end
@@ -0,0 +1,185 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+
6
+ class MouseMap
7
+
8
+ def self.import(file)
9
+ name = File.basename(file, File.extname(file))
10
+ map = new(name)
11
+ list = JSON[File.read(file, encoding: 'utf-8')]
12
+ begin
13
+ map.bindings.concat list.map { |h| MouseBinding.from_json(h) }
14
+ rescue => ex
15
+ puts "file: #{file}"
16
+ raise ex
17
+ end
18
+
19
+ map
20
+ end
21
+
22
+ attr_reader :name, :bindings
23
+
24
+ def initialize(name)
25
+ @name = name
26
+ @bindings = []
27
+ end
28
+
29
+ def write(dir)
30
+ file = "#{dir}/#{name}.mousemap.rb"
31
+ File.open(file, 'wb') do |f|
32
+ f.puts "\nmousemap #{name.to_source} do"
33
+ f.puts
34
+ bindings.each do |b|
35
+ begin
36
+ f.puts ' ' << b.to_dsl
37
+ rescue => ex
38
+ puts "file: #{file}\nbinding: #{b.click}"
39
+ raise ex
40
+ end
41
+ end
42
+ f.puts "\nend"
43
+ end
44
+ end
45
+
46
+ def export(dir)
47
+ file = "#{dir}/#{name}.sublime-mousemap"
48
+ File.open(file, 'wb:utf-8') { |f| f.write to_json }
49
+ end
50
+
51
+ def to_json
52
+ "[\n" <<
53
+ bindings.map { |b| JSON.generate(b.to_h) }.join(",\n").indent(2) <<
54
+ "\n]"
55
+ end
56
+
57
+
58
+ ##
59
+ # A mouse binding: a click with its command(s).
60
+
61
+ class MouseBinding
62
+
63
+ def self.from_json(json_hash)
64
+ h = json_hash.dup
65
+
66
+ button = h.delete('button') or raise Error, 'no button: ' << json_hash.inspect
67
+ modifiers = h.delete('modifiers')
68
+ spec = [*modifiers, button].join('+')
69
+ click = Mouse.sublime.ensure_click(spec)
70
+
71
+ count = h.delete('count')
72
+ count = count.to_i if count
73
+
74
+ press = h.delete('press_command')
75
+ press_command = press ? Command.new(press, h.delete('press_args')) : nil
76
+
77
+ cmd = h.delete('command')
78
+ command = cmd ? Command.new(cmd, h.delete('args')) : nil
79
+
80
+ h.empty? or raise Error, 'unexpected JSON keys: ' << h.inspect
81
+ new(click, count, press_command, command)
82
+
83
+ rescue => ex
84
+ warn "error with binding #{json_hash.inspect}"
85
+ warn ex.message
86
+ raise
87
+ end
88
+
89
+ attr_reader :click, :count, :press_command, :command
90
+ attr_accessor :source_file
91
+
92
+ def initialize(click, count, press_command, command)
93
+ @click = click
94
+ @count = count
95
+ @press_command = press_command
96
+ @command = command
97
+ end
98
+
99
+ def to_dsl
100
+ spec = click.to_spec
101
+ dsl = "click#{count} #{spec.to_source}"
102
+ dsl << ", down: #{press_command.to_dsl}" if press_command
103
+ dsl << ", up: #{command.to_dsl}" if command
104
+
105
+ dsl
106
+ end
107
+
108
+ def to_h
109
+ h = click.to_h
110
+ h['count'] = count if count
111
+ h.merge! press_command.to_h('press_command', 'press_args') if press_command
112
+ h.merge! command.to_h if command
113
+ h
114
+ end
115
+
116
+ end
117
+
118
+
119
+ class DSLReader < Tools::BlankSlate
120
+
121
+ def initialize(file = nil)
122
+ @mousemaps = []
123
+ @current_map = nil
124
+ @mouse = Mouse.sublime
125
+ instance_eval File.read(file, encoding: 'utf-8'), file if file
126
+ end
127
+
128
+ def _mousemaps
129
+ @mousemaps
130
+ end
131
+
132
+ def mousemap(name, &block)
133
+ @current_map and raise Error, "'mousemap' blocks cannot be nested"
134
+ @current_map = MouseMap.new(name)
135
+ instance_eval(&block)
136
+ @mousemaps << @current_map
137
+ @current_map = nil
138
+ end
139
+
140
+ def button_names(maps={})
141
+ # create a copy of the Sublime mouse
142
+ @mouse = Mouse.sublime.clone('Custom Names')
143
+ maps.each_pair do |name, st_name|
144
+ b = @mouse.modifier(st_name) or
145
+ raise Error, "no button nor modifier named '#{st_name}'"
146
+ b.name = name
147
+ end
148
+ end
149
+
150
+ def click1(spec, options={})
151
+ click spec, options.merge({ count: 1 })
152
+ end
153
+
154
+ def click2(spec, options={})
155
+ click spec, options.merge({ count: 2 })
156
+ end
157
+
158
+ def click3(spec, options={})
159
+ click spec, options.merge({ count: 3 })
160
+ end
161
+
162
+ def click(spec, options={})
163
+ @current_map or raise Error, "'click' is invalid outside of a 'mousemap' block"
164
+ click = @mouse.ensure_click(spec)
165
+ count = options.delete(:count)
166
+ press_cmd = options.delete(:down)
167
+ press_cmd && press_cmd.error and raise Error, "click '#{spec}':#{press_cmd.error}"
168
+ cmd = options.delete(:up)
169
+ cmd && cmd.error and raise Error, "click '#{spec}':#{cmd.error}"
170
+ press_cmd || cmd or
171
+ raise Error, "click '#{spec}': no 'up' nor 'down' command"
172
+ @current_map.bindings << MouseBinding.new(click, count, press_cmd, cmd)
173
+ end
174
+
175
+ def method_missing(sym, *args, &block)
176
+ @current_map or raise Error, "'#{sym}' is invalid outside of a 'mousemap' block"
177
+ Command.from_method_missing(sym, args)
178
+ end
179
+
180
+ end
181
+
182
+ end
183
+
184
+ end
185
+ end
@@ -0,0 +1,91 @@
1
+ # encoding: utf-8
2
+
3
+ module SublimeDSL
4
+ module SublimeText
5
+ class Package
6
+
7
+ ##
8
+ # 'Universal' DSL reader: reads any DSL block and updates its package.
9
+
10
+ class DSLReader
11
+
12
+ def initialize(file, package)
13
+ @file = file
14
+ @package = package
15
+ instance_eval File.read(file, encoding: 'utf-8'), file
16
+ end
17
+
18
+ def method_missing(sym, *args, &block)
19
+ raise DSLError, "'#{sym}' is not a valid Package DSL statement"
20
+ end
21
+
22
+ def theme(*args, &block)
23
+ r = TextMate::Theme::DSLReader.new
24
+ r.theme(*args, &block)
25
+ @package.themes.concat r._themes
26
+ end
27
+
28
+ def language(*args, &block)
29
+ r = TextMate::Grammar::DSLReader.new
30
+ r.language(*args, &block)
31
+ @package.grammars.concat r._grammars
32
+ end
33
+
34
+ def preferences(*args, &block)
35
+ r = TextMate::Preference::DSLReader.new
36
+ r.preferences(*args, &block)
37
+ @package.preferences.concat r._preferences
38
+ end
39
+
40
+ def snippets(*args, &block)
41
+ r = TextMate::Snippet::DSLReader.new
42
+ r.snippets(*args, &block)
43
+ @package.snippets.concat r._snippets
44
+ end
45
+
46
+ def settings(*args, &block)
47
+ r = SettingSet::DSLReader.new
48
+ r.settings(*args, &block)
49
+ @package.setting_sets.concat r._setting_sets
50
+ end
51
+
52
+ def macro(*args, &block)
53
+ r = Macro::DSLReader.new
54
+ r.macro(*args, &block)
55
+ @package.macros.concat r._macros
56
+ end
57
+
58
+ def commands(*args, &block)
59
+ r = CommandSet::DSLReader.new
60
+ r.commands(*args, &block)
61
+ @package.command_sets.concat r._command_sets
62
+ end
63
+
64
+ def menu(*args, &block)
65
+ r = Menu::DSLReader.new
66
+ r.menu(*args, &block)
67
+ @package.menus.concat r._menus
68
+ end
69
+
70
+ def mousemap(*args, &block)
71
+ r = MouseMap::DSLReader.new
72
+ r.mousemap(*args, &block)
73
+ @package.mousemaps.concat r._mousemaps
74
+ end
75
+
76
+ def keymap(*args, &block)
77
+ r = KeyMap::DSLReader.new
78
+ r._file = @file
79
+ r.keymap(*args, &block)
80
+ @package.keymaps.concat r._keymaps
81
+ end
82
+
83
+ def keyboard(*args, &block)
84
+ raise Error, "keyboards must be defined in a separate *.keyboard.rb file"
85
+ end
86
+
87
+ end
88
+
89
+ end
90
+ end
91
+ end