reline 0.1.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.
@@ -9,23 +9,48 @@ class Reline::Windows
9
9
  true
10
10
  end
11
11
 
12
- RAW_KEYSTROKE_CONFIG = {
13
- [224, 72] => :ed_prev_history, # ↑
14
- [224, 80] => :ed_next_history, # ↓
15
- [224, 77] => :ed_next_char, # →
16
- [224, 75] => :ed_prev_char, # ←
17
- [224, 83] => :key_delete, # Del
18
- [224, 71] => :ed_move_to_beg, # Home
19
- [224, 79] => :ed_move_to_end, # End
20
- [ 0, 41] => :ed_unassigned, # input method on/off
21
- [ 0, 72] => :ed_prev_history, #
22
- [ 0, 80] => :ed_next_history, #
23
- [ 0, 77] => :ed_next_char, #
24
- [ 0, 75] => :ed_prev_char, #
25
- [ 0, 83] => :key_delete, # Del
26
- [ 0, 71] => :ed_move_to_beg, # Home
27
- [ 0, 79] => :ed_move_to_end # End
28
- }
12
+ def self.win_legacy_console?
13
+ @@legacy_console
14
+ end
15
+
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
29
54
 
30
55
  if defined? JRUBY_VERSION
31
56
  require 'win32api'
@@ -69,13 +94,37 @@ class Reline::Windows
69
94
  end
70
95
  end
71
96
 
97
+ VK_RETURN = 0x0D
72
98
  VK_MENU = 0x12
73
99
  VK_LMENU = 0xA4
74
100
  VK_CONTROL = 0x11
75
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
+
76
126
  STD_INPUT_HANDLE = -10
77
127
  STD_OUTPUT_HANDLE = -11
78
- WINDOW_BUFFER_SIZE_EVENT = 0x04
79
128
  FILE_TYPE_PIPE = 0x0003
80
129
  FILE_NAME_INFO = 2
81
130
  @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
@@ -89,15 +138,39 @@ class Reline::Windows
89
138
  @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
90
139
  @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
91
140
  @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
92
- @@ReadConsoleInput = Win32API.new('kernel32', 'ReadConsoleInput', ['L', 'P', 'L', 'P'], 'L')
141
+ @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
93
142
  @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
94
143
  @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
95
144
  @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
145
+ @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
146
+
147
+ @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
148
+ @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
149
+ @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
150
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
151
+
152
+ private_class_method def self.getconsolemode
153
+ mode = "\000\000\000\000"
154
+ @@GetConsoleMode.call(@@hConsoleHandle, mode)
155
+ mode.unpack1('L')
156
+ end
157
+
158
+ private_class_method def self.setconsolemode(mode)
159
+ @@SetConsoleMode.call(@@hConsoleHandle, mode)
160
+ end
161
+
162
+ @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
163
+ #if @@legacy_console
164
+ # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
165
+ # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
166
+ #end
96
167
 
97
168
  @@input_buf = []
98
169
  @@output_buf = []
99
170
 
100
- def self.msys_tty?(io=@@hConsoleInputHandle)
171
+ @@output = STDOUT
172
+
173
+ def self.msys_tty?(io = @@hConsoleInputHandle)
101
174
  # check if fd is a pipe
102
175
  if @@GetFileType.call(io) != FILE_TYPE_PIPE
103
176
  return false
@@ -113,7 +186,7 @@ class Reline::Windows
113
186
  # DWORD FileNameLength;
114
187
  # WCHAR FileName[1];
115
188
  # } FILE_NAME_INFO
116
- len = p_buffer[0, 4].unpack("L")[0]
189
+ len = p_buffer[0, 4].unpack1("L")
117
190
  name = p_buffer[4, len].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace)
118
191
 
119
192
  # Check if this could be a MSYS2 pty pipe ('\msys-XXXX-ptyN-XX')
@@ -121,95 +194,157 @@ class Reline::Windows
121
194
  name =~ /(msys-|cygwin-).*-pty/ ? true : false
122
195
  end
123
196
 
124
- def self.getwch
125
- unless @@input_buf.empty?
126
- return @@input_buf.shift
127
- end
128
- while @@kbhit.call == 0
129
- 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
130
226
  end
131
- until @@kbhit.call == 0
132
- ret = @@getwch.call
133
- if ret == 0 or ret == 0xE0
134
- @@input_buf << ret
135
- ret = @@getwch.call
136
- @@input_buf << ret
137
- return @@input_buf.shift
138
- end
139
- begin
140
- bytes = ret.chr(Encoding::UTF_8).bytes
141
- @@input_buf.push(*bytes)
142
- rescue Encoding::UndefinedConversionError
143
- @@input_buf << ret
144
- @@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
145
235
  end
236
+ else
237
+ # ignore high-surrogate without low-surrogate if there
238
+ @@hsg = nil
239
+ end
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
146
247
  end
147
- @@input_buf.shift
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)
148
255
  end
149
256
 
150
- def self.getc
257
+ def self.check_input_event
151
258
  num_of_events = 0.chr * 8
152
- while @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) != 0 and num_of_events.unpack('L').first > 0
153
- 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
154
268
  read_event = 0.chr * 4
155
- if @@ReadConsoleInput.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
156
- event = input_record[0, 2].unpack('s*').first
157
- if event == WINDOW_BUFFER_SIZE_EVENT
158
- @@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
159
289
  end
160
290
  end
161
291
  end
162
- unless @@output_buf.empty?
163
- return @@output_buf.shift
164
- end
165
- input = getwch
166
- meta = (@@GetKeyState.call(VK_LMENU) & 0x80) != 0
167
- control = (@@GetKeyState.call(VK_CONTROL) & 0x80) != 0
168
- shift = (@@GetKeyState.call(VK_SHIFT) & 0x80) != 0
169
- force_enter = !input.instance_of?(Array) && (control or shift) && input == 0x0D
170
- if force_enter
171
- # It's treated as Meta+Enter on Windows
172
- @@output_buf.push("\e".ord)
173
- @@output_buf.push(input)
174
- else
175
- case input
176
- when 0x00
177
- meta = false
178
- @@output_buf.push(input)
179
- input = getwch
180
- @@output_buf.push(*input)
181
- when 0xE0
182
- @@output_buf.push(input)
183
- input = getwch
184
- @@output_buf.push(*input)
185
- when 0x03
186
- @@output_buf.push(input)
187
- else
188
- @@output_buf.push(input)
189
- end
190
- end
191
- if meta
192
- "\e".ord
193
- else
194
- @@output_buf.shift
195
- end
292
+ end
293
+
294
+ def self.getc
295
+ check_input_event
296
+ @@output_buf.shift
196
297
  end
197
298
 
198
299
  def self.ungetc(c)
199
300
  @@output_buf.unshift(c)
200
301
  end
201
302
 
202
- def self.get_screen_size
303
+ def self.in_pasting?
304
+ not self.empty_buffer?
305
+ end
306
+
307
+ def self.empty_buffer?
308
+ if not @@output_buf.empty?
309
+ false
310
+ elsif @@kbhit.call == 0
311
+ true
312
+ else
313
+ false
314
+ end
315
+ end
316
+
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
203
330
  csbi = 0.chr * 22
204
- @@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
205
339
  csbi[0, 4].unpack('SS').reverse
206
340
  end
207
341
 
208
342
  def self.cursor_pos
209
- csbi = 0.chr * 22
210
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
211
- x = csbi[4, 2].unpack('s*').first
212
- 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')
213
348
  Reline::CursorPos.new(x, y)
214
349
  end
215
350
 
@@ -219,7 +354,9 @@ class Reline::Windows
219
354
 
220
355
  def self.move_cursor_up(val)
221
356
  if val > 0
222
- @@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y - val) * 65536 + cursor_pos.x)
357
+ y = cursor_pos.y - val
358
+ y = 0 if y < 0
359
+ @@SetConsoleCursorPosition.call(@@hConsoleHandle, y * 65536 + cursor_pos.x)
223
360
  elsif val < 0
224
361
  move_cursor_down(-val)
225
362
  end
@@ -227,6 +364,10 @@ class Reline::Windows
227
364
 
228
365
  def self.move_cursor_down(val)
229
366
  if val > 0
367
+ return unless csbi = get_console_screen_buffer_info
368
+ screen_height = get_screen_size.first
369
+ y = cursor_pos.y + val
370
+ y = screen_height - 1 if y > (screen_height - 1)
230
371
  @@SetConsoleCursorPosition.call(@@hConsoleHandle, (cursor_pos.y + val) * 65536 + cursor_pos.x)
231
372
  elsif val < 0
232
373
  move_cursor_up(-val)
@@ -234,39 +375,74 @@ class Reline::Windows
234
375
  end
235
376
 
236
377
  def self.erase_after_cursor
237
- csbi = 0.chr * 24
238
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
239
- 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')
240
381
  written = 0.chr * 4
241
382
  @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
383
+ @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, get_screen_size.last - cursor_pos.x, cursor, written)
242
384
  end
243
385
 
244
386
  def self.scroll_down(val)
245
- return if val.zero?
246
- scroll_rectangle = [0, val, get_screen_size.last, get_screen_size.first].pack('s4')
247
- destination_origin = 0 # y * 65536 + x
248
- fill = [' '.ord, 0].pack('SS')
249
- @@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
250
411
  end
251
412
 
252
413
  def self.clear_screen
253
- csbi = 0.chr * 22
254
- return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
255
- buffer_width = csbi[0, 2].unpack('S').first
256
- attributes = csbi[8, 2].unpack('S').first
257
- _window_left, window_top, _window_right, window_bottom = *csbi[10,8].unpack('S*')
258
- fill_length = buffer_width * (window_bottom - window_top + 1)
259
- screen_topleft = window_top * 65536
260
- written = 0.chr * 4
261
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
262
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
263
- @@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
264
426
  end
265
427
 
266
428
  def self.set_screen_size(rows, columns)
267
429
  raise NotImplementedError
268
430
  end
269
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
+
270
446
  def self.set_winch_handler(&handler)
271
447
  @@winch_handler = handler
272
448
  end
@@ -279,4 +455,43 @@ class Reline::Windows
279
455
  def self.deprep(otio)
280
456
  # do nothing
281
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
282
497
  end