reline 0.2.5 → 0.3.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.
@@ -13,23 +13,44 @@ class Reline::Windows
13
13
  @@legacy_console
14
14
  end
15
15
 
16
- RAW_KEYSTROKE_CONFIG = {
17
- [224, 72] => :ed_prev_history, # ↑
18
- [224, 80] => :ed_next_history, #
19
- [224, 77] => :ed_next_char, #
20
- [224, 75] => :ed_prev_char, #
21
- [224, 83] => :key_delete, # Del
22
- [224, 71] => :ed_move_to_beg, # Home
23
- [224, 79] => :ed_move_to_end, # End
24
- [ 0, 41] => :ed_unassigned, # input method on/off
25
- [ 0, 72] => :ed_prev_history, #
26
- [ 0, 80] => :ed_next_history, #
27
- [ 0, 77] => :ed_next_char, #
28
- [ 0, 75] => :ed_prev_char, #
29
- [ 0, 83] => :key_delete, # Del
30
- [ 0, 71] => :ed_move_to_beg, # Home
31
- [ 0, 79] => :ed_move_to_end # End
32
- }
16
+ def self.set_default_key_bindings(config)
17
+ {
18
+ [224, 72] => :ed_prev_history, #
19
+ [224, 80] => :ed_next_history, #
20
+ [224, 77] => :ed_next_char, #
21
+ [224, 75] => :ed_prev_char, #
22
+ [224, 83] => :key_delete, # Del
23
+ [224, 71] => :ed_move_to_beg, # Home
24
+ [224, 79] => :ed_move_to_end, # End
25
+ [ 0, 41] => :ed_unassigned, # input method on/off
26
+ [ 0, 72] => :ed_prev_history, #
27
+ [ 0, 80] => :ed_next_history, #
28
+ [ 0, 77] => :ed_next_char, #
29
+ [ 0, 75] => :ed_prev_char, #
30
+ [ 0, 83] => :key_delete, # Del
31
+ [ 0, 71] => :ed_move_to_beg, # Home
32
+ [ 0, 79] => :ed_move_to_end # End
33
+ }.each_pair do |key, func|
34
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
35
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
36
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
37
+ end
38
+
39
+ {
40
+ [27, 32] => :em_set_mark, # M-<space>
41
+ [24, 24] => :em_exchange_mark, # C-x C-x
42
+ }.each_pair do |key, func|
43
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
44
+ end
45
+
46
+ # Emulate ANSI key sequence.
47
+ {
48
+ [27, 91, 90] => :completion_journey_up, # S-Tab
49
+ }.each_pair do |key, func|
50
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
51
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
52
+ end
53
+ end
33
54
 
34
55
  if defined? JRUBY_VERSION
35
56
  require 'win32api'
@@ -73,13 +94,37 @@ class Reline::Windows
73
94
  end
74
95
  end
75
96
 
97
+ VK_RETURN = 0x0D
76
98
  VK_MENU = 0x12
77
99
  VK_LMENU = 0xA4
78
100
  VK_CONTROL = 0x11
79
101
  VK_SHIFT = 0x10
102
+ VK_DIVIDE = 0x6F
103
+
104
+ KEY_EVENT = 0x01
105
+ WINDOW_BUFFER_SIZE_EVENT = 0x04
106
+
107
+ CAPSLOCK_ON = 0x0080
108
+ ENHANCED_KEY = 0x0100
109
+ LEFT_ALT_PRESSED = 0x0002
110
+ LEFT_CTRL_PRESSED = 0x0008
111
+ NUMLOCK_ON = 0x0020
112
+ RIGHT_ALT_PRESSED = 0x0001
113
+ RIGHT_CTRL_PRESSED = 0x0004
114
+ SCROLLLOCK_ON = 0x0040
115
+ SHIFT_PRESSED = 0x0010
116
+
117
+ VK_TAB = 0x09
118
+ VK_END = 0x23
119
+ VK_HOME = 0x24
120
+ VK_LEFT = 0x25
121
+ VK_UP = 0x26
122
+ VK_RIGHT = 0x27
123
+ VK_DOWN = 0x28
124
+ VK_DELETE = 0x2E
125
+
80
126
  STD_INPUT_HANDLE = -10
81
127
  STD_OUTPUT_HANDLE = -11
82
- WINDOW_BUFFER_SIZE_EVENT = 0x04
83
128
  FILE_TYPE_PIPE = 0x0003
84
129
  FILE_NAME_INFO = 2
85
130
  @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
@@ -93,13 +138,15 @@ class Reline::Windows
93
138
  @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
94
139
  @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
95
140
  @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
96
- @@ReadConsoleInput = Win32API.new('kernel32', 'ReadConsoleInput', ['L', 'P', 'L', 'P'], 'L')
141
+ @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
97
142
  @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
98
143
  @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
99
144
  @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
145
+ @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
100
146
 
101
147
  @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
102
148
  @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
149
+ @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
103
150
  ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
104
151
 
105
152
  private_class_method def self.getconsolemode
@@ -121,7 +168,9 @@ class Reline::Windows
121
168
  @@input_buf = []
122
169
  @@output_buf = []
123
170
 
124
- def self.msys_tty?(io=@@hConsoleInputHandle)
171
+ @@output = STDOUT
172
+
173
+ def self.msys_tty?(io = @@hConsoleInputHandle)
125
174
  # check if fd is a pipe
126
175
  if @@GetFileType.call(io) != FILE_TYPE_PIPE
127
176
  return false
@@ -137,7 +186,7 @@ class Reline::Windows
137
186
  # DWORD FileNameLength;
138
187
  # WCHAR FileName[1];
139
188
  # } FILE_NAME_INFO
140
- len = p_buffer[0, 4].unpack("L")[0]
189
+ len = p_buffer[0, 4].unpack1("L")
141
190
  name = p_buffer[4, len].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace)
142
191
 
143
192
  # Check if this could be a MSYS2 pty pipe ('\msys-XXXX-ptyN-XX')
@@ -145,78 +194,106 @@ class Reline::Windows
145
194
  name =~ /(msys-|cygwin-).*-pty/ ? true : false
146
195
  end
147
196
 
148
- def self.getwch
149
- unless @@input_buf.empty?
150
- return @@input_buf.shift
151
- end
152
- while @@kbhit.call == 0
153
- sleep(0.001)
197
+ KEY_MAP = [
198
+ # It's treated as Meta+Enter on Windows.
199
+ [ { control_keys: :CTRL, virtual_key_code: 0x0D }, "\e\r".bytes ],
200
+ [ { control_keys: :SHIFT, virtual_key_code: 0x0D }, "\e\r".bytes ],
201
+
202
+ # It's treated as Meta+Space on Windows.
203
+ [ { control_keys: :CTRL, char_code: 0x20 }, "\e ".bytes ],
204
+
205
+ # Emulate getwch() key sequences.
206
+ [ { control_keys: [], virtual_key_code: VK_UP }, [0, 72] ],
207
+ [ { control_keys: [], virtual_key_code: VK_DOWN }, [0, 80] ],
208
+ [ { control_keys: [], virtual_key_code: VK_RIGHT }, [0, 77] ],
209
+ [ { control_keys: [], virtual_key_code: VK_LEFT }, [0, 75] ],
210
+ [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ],
211
+ [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ],
212
+ [ { control_keys: [], virtual_key_code: VK_END }, [0, 79] ],
213
+
214
+ # Emulate ANSI key sequence.
215
+ [ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ],
216
+ ]
217
+
218
+ @@hsg = nil
219
+
220
+ def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
221
+
222
+ # high-surrogate
223
+ if 0xD800 <= char_code and char_code <= 0xDBFF
224
+ @@hsg = char_code
225
+ return
154
226
  end
155
- until @@kbhit.call == 0
156
- ret = @@getwch.call
157
- if ret == 0 or ret == 0xE0
158
- @@input_buf << ret
159
- ret = @@getwch.call
160
- @@input_buf << ret
161
- return @@input_buf.shift
162
- end
163
- begin
164
- bytes = ret.chr(Encoding::UTF_8).bytes
165
- @@input_buf.push(*bytes)
166
- rescue Encoding::UndefinedConversionError
167
- @@input_buf << ret
168
- @@input_buf << @@getwch.call if ret == 224
227
+ # low-surrogate
228
+ if 0xDC00 <= char_code and char_code <= 0xDFFF
229
+ if @@hsg
230
+ char_code = 0x10000 + (@@hsg - 0xD800) * 0x400 + char_code - 0xDC00
231
+ @@hsg = nil
232
+ else
233
+ # no high-surrogate. ignored.
234
+ return
169
235
  end
236
+ else
237
+ # ignore high-surrogate without low-surrogate if there
238
+ @@hsg = nil
170
239
  end
171
- @@input_buf.shift
240
+
241
+ key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
242
+
243
+ match = KEY_MAP.find { |args,| key.matches?(**args) }
244
+ unless match.nil?
245
+ @@output_buf.concat(match.last)
246
+ return
247
+ end
248
+
249
+ # no char, only control keys
250
+ return if key.char_code == 0 and key.control_keys.any?
251
+
252
+ @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT)
253
+
254
+ @@output_buf.concat(key.char.bytes)
172
255
  end
173
256
 
174
- def self.getc
257
+ def self.check_input_event
175
258
  num_of_events = 0.chr * 8
176
- while @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) != 0 and num_of_events.unpack('L').first > 0
177
- input_record = 0.chr * 18
259
+ while @@output_buf.empty?
260
+ Reline.core.line_editor.resize
261
+ if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec
262
+ # prevent for background consolemode change
263
+ @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
264
+ next
265
+ end
266
+ next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack1('L') == 0
267
+ input_records = 0.chr * 20 * 80
178
268
  read_event = 0.chr * 4
179
- if @@ReadConsoleInput.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
180
- event = input_record[0, 2].unpack('s*').first
181
- if event == WINDOW_BUFFER_SIZE_EVENT
182
- @@winch_handler.()
269
+ if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_records, 80, read_event) != 0
270
+ read_events = read_event.unpack1('L')
271
+ 0.upto(read_events) do |idx|
272
+ input_record = input_records[idx * 20, 20]
273
+ event = input_record[0, 2].unpack1('s*')
274
+ case event
275
+ when WINDOW_BUFFER_SIZE_EVENT
276
+ @@winch_handler.()
277
+ when KEY_EVENT
278
+ key_down = input_record[4, 4].unpack1('l*')
279
+ repeat_count = input_record[8, 2].unpack1('s*')
280
+ virtual_key_code = input_record[10, 2].unpack1('s*')
281
+ virtual_scan_code = input_record[12, 2].unpack1('s*')
282
+ char_code = input_record[14, 2].unpack1('S*')
283
+ control_key_state = input_record[16, 2].unpack1('S*')
284
+ is_key_down = key_down.zero? ? false : true
285
+ if is_key_down
286
+ process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
287
+ end
288
+ end
183
289
  end
184
290
  end
185
291
  end
186
- unless @@output_buf.empty?
187
- return @@output_buf.shift
188
- end
189
- input = getwch
190
- meta = (@@GetKeyState.call(VK_LMENU) & 0x80) != 0
191
- control = (@@GetKeyState.call(VK_CONTROL) & 0x80) != 0
192
- shift = (@@GetKeyState.call(VK_SHIFT) & 0x80) != 0
193
- force_enter = !input.instance_of?(Array) && (control or shift) && input == 0x0D
194
- if force_enter
195
- # It's treated as Meta+Enter on Windows
196
- @@output_buf.push("\e".ord)
197
- @@output_buf.push(input)
198
- else
199
- case input
200
- when 0x00
201
- meta = false
202
- @@output_buf.push(input)
203
- input = getwch
204
- @@output_buf.push(*input)
205
- when 0xE0
206
- @@output_buf.push(input)
207
- input = getwch
208
- @@output_buf.push(*input)
209
- when 0x03
210
- @@output_buf.push(input)
211
- else
212
- @@output_buf.push(input)
213
- end
214
- end
215
- if meta
216
- "\e".ord
217
- else
218
- @@output_buf.shift
219
- end
292
+ end
293
+
294
+ def self.getc
295
+ check_input_event
296
+ @@output_buf.shift
220
297
  end
221
298
 
222
299
  def self.ungetc(c)
@@ -228,7 +305,7 @@ class Reline::Windows
228
305
  end
229
306
 
230
307
  def self.empty_buffer?
231
- if not @@input_buf.empty?
308
+ if not @@output_buf.empty?
232
309
  false
233
310
  elsif @@kbhit.call == 0
234
311
  true
@@ -237,17 +314,37 @@ class Reline::Windows
237
314
  end
238
315
  end
239
316
 
240
- def self.get_screen_size
317
+ def self.get_console_screen_buffer_info
318
+ # CONSOLE_SCREEN_BUFFER_INFO
319
+ # [ 0,2] dwSize.X
320
+ # [ 2,2] dwSize.Y
321
+ # [ 4,2] dwCursorPositions.X
322
+ # [ 6,2] dwCursorPositions.Y
323
+ # [ 8,2] wAttributes
324
+ # [10,2] srWindow.Left
325
+ # [12,2] srWindow.Top
326
+ # [14,2] srWindow.Right
327
+ # [16,2] srWindow.Bottom
328
+ # [18,2] dwMaximumWindowSize.X
329
+ # [20,2] dwMaximumWindowSize.Y
241
330
  csbi = 0.chr * 22
242
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
331
+ return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
332
+ csbi
333
+ end
334
+
335
+ def self.get_screen_size
336
+ unless csbi = get_console_screen_buffer_info
337
+ return [1, 1]
338
+ end
243
339
  csbi[0, 4].unpack('SS').reverse
244
340
  end
245
341
 
246
342
  def self.cursor_pos
247
- csbi = 0.chr * 22
248
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
249
- x = csbi[4, 2].unpack('s*').first
250
- y = csbi[6, 2].unpack('s*').first
343
+ unless csbi = get_console_screen_buffer_info
344
+ return Reline::CursorPos.new(0, 0)
345
+ end
346
+ x = csbi[4, 2].unpack1('s')
347
+ y = csbi[6, 2].unpack1('s')
251
348
  Reline::CursorPos.new(x, y)
252
349
  end
253
350
 
@@ -267,6 +364,7 @@ class Reline::Windows
267
364
 
268
365
  def self.move_cursor_down(val)
269
366
  if val > 0
367
+ return unless csbi = get_console_screen_buffer_info
270
368
  screen_height = get_screen_size.first
271
369
  y = cursor_pos.y + val
272
370
  y = screen_height - 1 if y > (screen_height - 1)
@@ -277,42 +375,74 @@ class Reline::Windows
277
375
  end
278
376
 
279
377
  def self.erase_after_cursor
280
- csbi = 0.chr * 24
281
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
282
- cursor = csbi[4, 4].unpack('L').first
378
+ return unless csbi = get_console_screen_buffer_info
379
+ attributes = csbi[8, 2].unpack1('S')
380
+ cursor = csbi[4, 4].unpack1('L')
283
381
  written = 0.chr * 4
284
382
  @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
285
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, 0, get_screen_size.last - cursor_pos.x, cursor, written)
383
+ @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
286
384
  end
287
385
 
288
386
  def self.scroll_down(val)
289
- return if val.zero?
290
- screen_height = get_screen_size.first
291
- val = screen_height - 1 if val > (screen_height - 1)
292
- scroll_rectangle = [0, val, get_screen_size.last, get_screen_size.first].pack('s4')
293
- destination_origin = 0 # y * 65536 + x
294
- fill = [' '.ord, 0].pack('SS')
295
- @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
387
+ return if val < 0
388
+ return unless csbi = get_console_screen_buffer_info
389
+ buffer_width, buffer_lines, x, y, attributes, window_left, window_top, window_bottom = csbi.unpack('ssssSssx2s')
390
+ screen_height = window_bottom - window_top + 1
391
+ val = screen_height if val > screen_height
392
+
393
+ if @@legacy_console || window_left != 0
394
+ # unless ENABLE_VIRTUAL_TERMINAL,
395
+ # if srWindow.Left != 0 then it's conhost.exe hosted console
396
+ # and puts "\n" causes horizontal scroll. its glitch.
397
+ # FYI irb write from culumn 1, so this gives no gain.
398
+ scroll_rectangle = [0, val, buffer_width, buffer_lines - val].pack('s4')
399
+ destination_origin = 0 # y * 65536 + x
400
+ fill = [' '.ord, attributes].pack('SS')
401
+ @@ScrollConsoleScreenBuffer.call(@@hConsoleHandle, scroll_rectangle, nil, destination_origin, fill)
402
+ else
403
+ origin_x = x + 1
404
+ origin_y = y - window_top + 1
405
+ @@output.write [
406
+ (origin_y != screen_height) ? "\e[#{screen_height};H" : nil,
407
+ "\n" * val,
408
+ (origin_y != screen_height or !x.zero?) ? "\e[#{origin_y};#{origin_x}H" : nil
409
+ ].join
410
+ end
296
411
  end
297
412
 
298
413
  def self.clear_screen
299
- csbi = 0.chr * 22
300
- return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
301
- buffer_width = csbi[0, 2].unpack('S').first
302
- attributes = csbi[8, 2].unpack('S').first
303
- _window_left, window_top, _window_right, window_bottom = *csbi[10,8].unpack('S*')
304
- fill_length = buffer_width * (window_bottom - window_top + 1)
305
- screen_topleft = window_top * 65536
306
- written = 0.chr * 4
307
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
308
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
309
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft)
414
+ if @@legacy_console
415
+ return unless csbi = get_console_screen_buffer_info
416
+ buffer_width, _buffer_lines, attributes, window_top, window_bottom = csbi.unpack('ss@8S@12sx2s')
417
+ fill_length = buffer_width * (window_bottom - window_top + 1)
418
+ screen_topleft = window_top * 65536
419
+ written = 0.chr * 4
420
+ @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
421
+ @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
422
+ @@SetConsoleCursorPosition.call(@@hConsoleHandle, screen_topleft)
423
+ else
424
+ @@output.write "\e[2J" "\e[H"
425
+ end
310
426
  end
311
427
 
312
428
  def self.set_screen_size(rows, columns)
313
429
  raise NotImplementedError
314
430
  end
315
431
 
432
+ def self.hide_cursor
433
+ size = 100
434
+ visible = 0 # 0 means false
435
+ cursor_info = [size, visible].pack('Li')
436
+ @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
437
+ end
438
+
439
+ def self.show_cursor
440
+ size = 100
441
+ visible = 1 # 1 means true
442
+ cursor_info = [size, visible].pack('Li')
443
+ @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
444
+ end
445
+
316
446
  def self.set_winch_handler(&handler)
317
447
  @@winch_handler = handler
318
448
  end
@@ -325,4 +455,43 @@ class Reline::Windows
325
455
  def self.deprep(otio)
326
456
  # do nothing
327
457
  end
458
+
459
+ class KeyEventRecord
460
+
461
+ attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys
462
+
463
+ def initialize(virtual_key_code, char_code, control_key_state)
464
+ @virtual_key_code = virtual_key_code
465
+ @char_code = char_code
466
+ @control_key_state = control_key_state
467
+ @enhanced = control_key_state & ENHANCED_KEY != 0
468
+
469
+ (@control_keys = []).tap do |control_keys|
470
+ # symbols must be sorted to make comparison is easier later on
471
+ control_keys << :ALT if control_key_state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0
472
+ control_keys << :CTRL if control_key_state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0
473
+ control_keys << :SHIFT if control_key_state & SHIFT_PRESSED != 0
474
+ end.freeze
475
+ end
476
+
477
+ def char
478
+ @char_code.chr(Encoding::UTF_8)
479
+ end
480
+
481
+ def enhanced?
482
+ @enhanced
483
+ end
484
+
485
+ # Verifies if the arguments match with this key event.
486
+ # Nil arguments are ignored, but at least one must be passed as non-nil.
487
+ # To verify that no control keys were pressed, pass an empty array: `control_keys: []`.
488
+ def matches?(control_keys: nil, virtual_key_code: nil, char_code: nil)
489
+ raise ArgumentError, 'No argument was passed to match key event' if control_keys.nil? && virtual_key_code.nil? && char_code.nil?
490
+
491
+ (control_keys.nil? || [*control_keys].sort == @control_keys) &&
492
+ (virtual_key_code.nil? || @virtual_key_code == virtual_key_code) &&
493
+ (char_code.nil? || char_code == @char_code)
494
+ end
495
+
496
+ end
328
497
  end