reline 0.2.3 → 0.2.7

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.
@@ -4,4 +4,16 @@ class Reline::KeyActor::Base
4
4
  def get_method(key)
5
5
  self.class::MAPPING[key]
6
6
  end
7
+
8
+ def initialize
9
+ @default_key_bindings = {}
10
+ end
11
+
12
+ def default_key_bindings
13
+ @default_key_bindings
14
+ end
15
+
16
+ def reset_default_key_bindings
17
+ @default_key_bindings.clear
18
+ end
7
19
  end
@@ -124,6 +124,7 @@ class Reline::LineEditor
124
124
  @prompt_cache_time = Time.now.to_f
125
125
  end
126
126
  prompt_list.map!{ prompt } if @vi_arg or @searching_prompt
127
+ prompt_list = [prompt] if prompt_list.empty?
127
128
  mode_string = check_mode_string
128
129
  prompt_list = prompt_list.map{ |pr| mode_string + pr } if mode_string
129
130
  prompt = prompt_list[@line_index]
@@ -149,7 +150,7 @@ class Reline::LineEditor
149
150
  @screen_size = Reline::IOGate.get_screen_size
150
151
  @screen_height = @screen_size.first
151
152
  reset_variables(prompt, encoding: encoding)
152
- @old_trap = Signal.trap('SIGINT') {
153
+ @old_trap = Signal.trap(:INT) {
153
154
  if @scroll_partial_screen
154
155
  move_cursor_down(@screen_height - (@line_index - @scroll_partial_screen) - 1)
155
156
  else
@@ -157,8 +158,16 @@ class Reline::LineEditor
157
158
  end
158
159
  Reline::IOGate.move_cursor_column(0)
159
160
  scroll_down(1)
160
- @old_trap.call if @old_trap.respond_to?(:call) # can also be string, ex: "DEFAULT"
161
- raise Interrupt
161
+ case @old_trap
162
+ when 'DEFAULT', 'SYSTEM_DEFAULT'
163
+ raise Interrupt
164
+ when 'IGNORE'
165
+ # Do nothing
166
+ when 'EXIT'
167
+ exit
168
+ else
169
+ @old_trap.call
170
+ end
162
171
  }
163
172
  Reline::IOGate.set_winch_handler do
164
173
  @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
@@ -343,8 +352,9 @@ class Reline::LineEditor
343
352
  else
344
353
  end_of_line_cursor = new_cursor_max
345
354
  end
346
- line_to_calc.encode(Encoding::UTF_8).grapheme_clusters.each do |gc|
347
- mbchar_width = Reline::Unicode.get_mbchar_width(gc)
355
+ line_to_calc.grapheme_clusters.each do |gc|
356
+ mbchar = gc.encode(Encoding::UTF_8)
357
+ mbchar_width = Reline::Unicode.get_mbchar_width(mbchar)
348
358
  now = new_cursor + mbchar_width
349
359
  if now > end_of_line_cursor or now > cursor
350
360
  break
@@ -407,7 +417,6 @@ class Reline::LineEditor
407
417
  return
408
418
  end
409
419
  new_highest_in_this = calculate_height_by_width(prompt_width + calculate_width(@line.nil? ? '' : @line))
410
- # FIXME: end of logical line sometimes breaks
411
420
  rendered = false
412
421
  if @add_newline_to_end_of_buffer
413
422
  rerender_added_newline(prompt, prompt_width)
@@ -676,7 +685,6 @@ class Reline::LineEditor
676
685
  private def render_partial(prompt, prompt_width, line_to_render, this_started_from, with_control: true)
677
686
  visual_lines, height = split_by_width(line_to_render.nil? ? prompt : prompt + line_to_render, @screen_size.last)
678
687
  cursor_up_from_last_line = 0
679
- # TODO: This logic would be sometimes buggy if this logical line isn't the current @line_index.
680
688
  if @scroll_partial_screen
681
689
  last_visual_line = this_started_from + (height - 1)
682
690
  last_screen_line = @scroll_partial_screen + (@screen_height - 1)
@@ -724,13 +732,17 @@ class Reline::LineEditor
724
732
  Reline::IOGate.move_cursor_column(0)
725
733
  if line.nil?
726
734
  if calculate_width(visual_lines[index - 1], true) == Reline::IOGate.get_screen_size.last
727
- # Reaches the end of line.
728
- #
729
- # When the cursor is at the end of the line and erases characters
730
- # after the cursor, some terminals delete the character at the
731
- # cursor position.
732
- move_cursor_down(1)
733
- Reline::IOGate.move_cursor_column(0)
735
+ # reaches the end of line
736
+ if Reline::IOGate.win? and Reline::IOGate.win_legacy_console?
737
+ # A newline is automatically inserted if a character is rendered at
738
+ # eol on command prompt.
739
+ else
740
+ # When the cursor is at the end of the line and erases characters
741
+ # after the cursor, some terminals delete the character at the
742
+ # cursor position.
743
+ move_cursor_down(1)
744
+ Reline::IOGate.move_cursor_column(0)
745
+ end
734
746
  else
735
747
  Reline::IOGate.erase_after_cursor
736
748
  move_cursor_down(1)
@@ -739,6 +751,10 @@ class Reline::LineEditor
739
751
  next
740
752
  end
741
753
  @output.write line
754
+ if Reline::IOGate.win? and Reline::IOGate.win_legacy_console? and calculate_width(line, true) == Reline::IOGate.get_screen_size.last
755
+ # A newline is automatically inserted if a character is rendered at eol on command prompt.
756
+ @rest_height -= 1 if @rest_height > 0
757
+ end
742
758
  @output.flush
743
759
  if @first_prompt
744
760
  @first_prompt = false
@@ -803,6 +819,7 @@ class Reline::LineEditor
803
819
  end
804
820
  move_cursor_up(back)
805
821
  move_cursor_down(@first_line_started_from + @started_from)
822
+ @rest_height = (Reline::IOGate.get_screen_size.first - 1) - Reline::IOGate.cursor_pos.y
806
823
  Reline::IOGate.move_cursor_column((prompt_width + @cursor) % @screen_size.last)
807
824
  end
808
825
 
@@ -1148,8 +1165,25 @@ class Reline::LineEditor
1148
1165
 
1149
1166
  def call_completion_proc
1150
1167
  result = retrieve_completion_block(true)
1151
- slice = result[1]
1152
- result = @completion_proc.(slice) if @completion_proc and slice
1168
+ preposing, target, postposing = result
1169
+ if @completion_proc and target
1170
+ argnum = @completion_proc.parameters.inject(0) { |result, item|
1171
+ case item.first
1172
+ when :req, :opt
1173
+ result + 1
1174
+ when :rest
1175
+ break 3
1176
+ end
1177
+ }
1178
+ case argnum
1179
+ when 1
1180
+ result = @completion_proc.(target)
1181
+ when 2
1182
+ result = @completion_proc.(target, preposing)
1183
+ when 3..Float::INFINITY
1184
+ result = @completion_proc.(target, preposing, postposing)
1185
+ end
1186
+ end
1153
1187
  Reline.core.instance_variable_set(:@completion_quote_character, nil)
1154
1188
  result
1155
1189
  end
@@ -1197,8 +1231,16 @@ class Reline::LineEditor
1197
1231
  end
1198
1232
 
1199
1233
  def retrieve_completion_block(set_completion_quote_character = false)
1200
- word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1201
- quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1234
+ if Reline.completer_word_break_characters.empty?
1235
+ word_break_regexp = nil
1236
+ else
1237
+ word_break_regexp = /\A[#{Regexp.escape(Reline.completer_word_break_characters)}]/
1238
+ end
1239
+ if Reline.completer_quote_characters.empty?
1240
+ quote_characters_regexp = nil
1241
+ else
1242
+ quote_characters_regexp = /\A[#{Regexp.escape(Reline.completer_quote_characters)}]/
1243
+ end
1202
1244
  before = @line.byteslice(0, @byte_pointer)
1203
1245
  rest = nil
1204
1246
  break_pointer = nil
@@ -1219,14 +1261,14 @@ class Reline::LineEditor
1219
1261
  elsif quote and slice.start_with?(escaped_quote)
1220
1262
  # skip
1221
1263
  i += 2
1222
- elsif slice =~ quote_characters_regexp # find new "
1264
+ elsif quote_characters_regexp and slice =~ quote_characters_regexp # find new "
1223
1265
  rest = $'
1224
1266
  quote = $&
1225
1267
  closing_quote = /(?!\\)#{Regexp.escape(quote)}/
1226
1268
  escaped_quote = /\\#{Regexp.escape(quote)}/
1227
1269
  i += 1
1228
1270
  break_pointer = i - 1
1229
- elsif not quote and slice =~ word_break_regexp
1271
+ elsif word_break_regexp and not quote and slice =~ word_break_regexp
1230
1272
  rest = $'
1231
1273
  i += 1
1232
1274
  before = @line.byteslice(i, @byte_pointer - i)
@@ -1254,6 +1296,19 @@ class Reline::LineEditor
1254
1296
  end
1255
1297
  target = before
1256
1298
  end
1299
+ if @is_multiline
1300
+ if @previous_line_index
1301
+ lines = whole_lines(index: @previous_line_index, line: @line)
1302
+ else
1303
+ lines = whole_lines
1304
+ end
1305
+ if @line_index > 0
1306
+ preposing = lines[0..(@line_index - 1)].join("\n") + "\n" + preposing
1307
+ end
1308
+ if (lines.size - 1) > @line_index
1309
+ postposing = postposing + "\n" + lines[(@line_index + 1)..-1].join("\n")
1310
+ end
1311
+ end
1257
1312
  [preposing.encode(@encoding), target.encode(@encoding), postposing.encode(@encoding)]
1258
1313
  end
1259
1314
 
@@ -1281,10 +1336,32 @@ class Reline::LineEditor
1281
1336
 
1282
1337
  def delete_text(start = nil, length = nil)
1283
1338
  if start.nil? and length.nil?
1284
- @line&.clear
1285
- @byte_pointer = 0
1286
- @cursor = 0
1287
- @cursor_max = 0
1339
+ if @is_multiline
1340
+ if @buffer_of_lines.size == 1
1341
+ @line&.clear
1342
+ @byte_pointer = 0
1343
+ @cursor = 0
1344
+ @cursor_max = 0
1345
+ elsif @line_index == (@buffer_of_lines.size - 1) and @line_index > 0
1346
+ @buffer_of_lines.pop
1347
+ @line_index -= 1
1348
+ @line = @buffer_of_lines[@line_index]
1349
+ @byte_pointer = 0
1350
+ @cursor = 0
1351
+ @cursor_max = calculate_width(@line)
1352
+ elsif @line_index < (@buffer_of_lines.size - 1)
1353
+ @buffer_of_lines.delete_at(@line_index)
1354
+ @line = @buffer_of_lines[@line_index]
1355
+ @byte_pointer = 0
1356
+ @cursor = 0
1357
+ @cursor_max = calculate_width(@line)
1358
+ end
1359
+ else
1360
+ @line&.clear
1361
+ @byte_pointer = 0
1362
+ @cursor = 0
1363
+ @cursor_max = 0
1364
+ end
1288
1365
  elsif not start.nil? and not length.nil?
1289
1366
  if @line
1290
1367
  before = @line.byteslice(0, start)
@@ -0,0 +1,126 @@
1
+ require 'fiddle'
2
+ require 'fiddle/import'
3
+
4
+ module Reline::Terminfo
5
+ extend Fiddle::Importer
6
+
7
+ class TerminfoError < StandardError; end
8
+
9
+ def self.curses_dl_files
10
+ case RUBY_PLATFORM
11
+ when /mingw/, /mswin/
12
+ # aren't supported
13
+ []
14
+ when /cygwin/
15
+ %w[cygncursesw-10.dll cygncurses-10.dll]
16
+ when /darwin/
17
+ %w[libncursesw.dylib libcursesw.dylib libncurses.dylib libcurses.dylib]
18
+ else
19
+ %w[libncursesw.so libcursesw.so libncurses.so libcurses.so]
20
+ end
21
+ end
22
+
23
+ @curses_dl = false
24
+ def self.curses_dl
25
+ return @curses_dl unless @curses_dl == false
26
+ if RUBY_VERSION >= '3.0.0'
27
+ # Gem module isn't defined in test-all of the Ruby repository, and
28
+ # Fiddle in Ruby 3.0.0 or later supports Fiddle::TYPE_VARIADIC.
29
+ fiddle_supports_variadic = true
30
+ elsif Fiddle.const_defined?(:VERSION) and Gem::Version.create(Fiddle::VERSION) >= Gem::Version.create('1.0.1')
31
+ # Fiddle::TYPE_VARIADIC is supported from Fiddle 1.0.1.
32
+ fiddle_supports_variadic = true
33
+ else
34
+ fiddle_supports_variadic = false
35
+ end
36
+ if fiddle_supports_variadic and not Fiddle.const_defined?(:TYPE_VARIADIC)
37
+ # If the libffi version is not 3.0.5 or higher, there isn't TYPE_VARIADIC.
38
+ fiddle_supports_variadic = false
39
+ end
40
+ if fiddle_supports_variadic
41
+ curses_dl_files.each do |curses_name|
42
+ result = Fiddle::Handle.new(curses_name)
43
+ rescue Fiddle::DLError
44
+ next
45
+ else
46
+ @curses_dl = result
47
+ break
48
+ end
49
+ end
50
+ @curses_dl = nil if @curses_dl == false
51
+ @curses_dl
52
+ end
53
+ end
54
+
55
+ module Reline::Terminfo
56
+ dlload curses_dl
57
+ #extern 'int setupterm(char *term, int fildes, int *errret)'
58
+ @setupterm = Fiddle::Function.new(curses_dl['setupterm'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
59
+ #extern 'char *tigetstr(char *capname)'
60
+ @tigetstr = Fiddle::Function.new(curses_dl['tigetstr'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
61
+ begin
62
+ #extern 'char *tiparm(const char *str, ...)'
63
+ @tiparm = Fiddle::Function.new(curses_dl['tiparm'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VARIADIC], Fiddle::TYPE_VOIDP)
64
+ rescue Fiddle::DLError
65
+ # OpenBSD lacks tiparm
66
+ #extern 'char *tparm(const char *str, ...)'
67
+ @tiparm = Fiddle::Function.new(curses_dl['tparm'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VARIADIC], Fiddle::TYPE_VOIDP)
68
+ end
69
+ # TODO: add int tigetflag(char *capname) and int tigetnum(char *capname)
70
+
71
+ def self.setupterm(term, fildes)
72
+ errret_int = String.new("\x00" * 8, encoding: 'ASCII-8BIT')
73
+ ret = @setupterm.(term, fildes, errret_int)
74
+ errret = errret_int.unpack('i')[0]
75
+ case ret
76
+ when 0 # OK
77
+ 0
78
+ when -1 # ERR
79
+ case errret
80
+ when 1
81
+ raise TerminfoError.new('The terminal is hardcopy, cannot be used for curses applications.')
82
+ when 0
83
+ raise TerminfoError.new('The terminal could not be found, or that it is a generic type, having too little information for curses applications to run.')
84
+ when -1
85
+ raise TerminfoError.new('The terminfo database could not be found.')
86
+ else # unknown
87
+ -1
88
+ end
89
+ else # unknown
90
+ -2
91
+ end
92
+ end
93
+
94
+ class StringWithTiparm < String
95
+ def tiparm(*args) # for method chain
96
+ Reline::Terminfo.tiparm(self, *args)
97
+ end
98
+ end
99
+
100
+ def self.tigetstr(capname)
101
+ capability = @tigetstr.(capname)
102
+ case capability.to_i
103
+ when 0, -1
104
+ raise TerminfoError, "can't find capability: #{capname}"
105
+ end
106
+ StringWithTiparm.new(capability.to_s)
107
+ end
108
+
109
+ def self.tiparm(str, *args)
110
+ new_args = []
111
+ args.each do |a|
112
+ new_args << Fiddle::TYPE_INT << a
113
+ end
114
+ @tiparm.(str, *new_args).to_s
115
+ end
116
+
117
+ def self.enabled?
118
+ true
119
+ end
120
+ end if Reline::Terminfo.curses_dl
121
+
122
+ module Reline::Terminfo
123
+ def self.enabled?
124
+ false
125
+ end
126
+ end unless Reline::Terminfo.curses_dl
@@ -108,6 +108,7 @@ class Reline::Unicode
108
108
  end
109
109
  m = mbchar.encode(Encoding::UTF_8).match(MBCharWidthRE)
110
110
  case
111
+ when m.nil? then 1 # TODO should be U+FFFD � REPLACEMENT CHARACTER
111
112
  when m[:width_2_1], m[:width_2_2] then 2
112
113
  when m[:width_3] then 3
113
114
  when m[:width_0] then 0
@@ -1,3 +1,3 @@
1
1
  module Reline
2
- VERSION = '0.2.3'
2
+ VERSION = '0.2.7'
3
3
  end
@@ -9,23 +9,40 @@ 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
+ end
29
46
 
30
47
  if defined? JRUBY_VERSION
31
48
  require 'win32api'
@@ -69,13 +86,36 @@ class Reline::Windows
69
86
  end
70
87
  end
71
88
 
89
+ VK_RETURN = 0x0D
72
90
  VK_MENU = 0x12
73
91
  VK_LMENU = 0xA4
74
92
  VK_CONTROL = 0x11
75
93
  VK_SHIFT = 0x10
94
+ VK_DIVIDE = 0x6F
95
+
96
+ KEY_EVENT = 0x01
97
+ WINDOW_BUFFER_SIZE_EVENT = 0x04
98
+
99
+ CAPSLOCK_ON = 0x0080
100
+ ENHANCED_KEY = 0x0100
101
+ LEFT_ALT_PRESSED = 0x0002
102
+ LEFT_CTRL_PRESSED = 0x0008
103
+ NUMLOCK_ON = 0x0020
104
+ RIGHT_ALT_PRESSED = 0x0001
105
+ RIGHT_CTRL_PRESSED = 0x0004
106
+ SCROLLLOCK_ON = 0x0040
107
+ SHIFT_PRESSED = 0x0010
108
+
109
+ VK_END = 0x23
110
+ VK_HOME = 0x24
111
+ VK_LEFT = 0x25
112
+ VK_UP = 0x26
113
+ VK_RIGHT = 0x27
114
+ VK_DOWN = 0x28
115
+ VK_DELETE = 0x2E
116
+
76
117
  STD_INPUT_HANDLE = -10
77
118
  STD_OUTPUT_HANDLE = -11
78
- WINDOW_BUFFER_SIZE_EVENT = 0x04
79
119
  FILE_TYPE_PIPE = 0x0003
80
120
  FILE_NAME_INFO = 2
81
121
  @@getwch = Win32API.new('msvcrt', '_getwch', [], 'I')
@@ -89,11 +129,31 @@ class Reline::Windows
89
129
  @@hConsoleHandle = @@GetStdHandle.call(STD_OUTPUT_HANDLE)
90
130
  @@hConsoleInputHandle = @@GetStdHandle.call(STD_INPUT_HANDLE)
91
131
  @@GetNumberOfConsoleInputEvents = Win32API.new('kernel32', 'GetNumberOfConsoleInputEvents', ['L', 'P'], 'L')
92
- @@ReadConsoleInput = Win32API.new('kernel32', 'ReadConsoleInput', ['L', 'P', 'L', 'P'], 'L')
132
+ @@ReadConsoleInputW = Win32API.new('kernel32', 'ReadConsoleInputW', ['L', 'P', 'L', 'P'], 'L')
93
133
  @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
94
134
  @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
95
135
  @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
96
136
 
137
+ @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
138
+ @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
139
+ ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
140
+
141
+ private_class_method def self.getconsolemode
142
+ mode = "\000\000\000\000"
143
+ @@GetConsoleMode.call(@@hConsoleHandle, mode)
144
+ mode.unpack1('L')
145
+ end
146
+
147
+ private_class_method def self.setconsolemode(mode)
148
+ @@SetConsoleMode.call(@@hConsoleHandle, mode)
149
+ end
150
+
151
+ @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
152
+ #if @@legacy_console
153
+ # setconsolemode(getconsolemode() | ENABLE_VIRTUAL_TERMINAL_PROCESSING)
154
+ # @@legacy_console = (getconsolemode() & ENABLE_VIRTUAL_TERMINAL_PROCESSING == 0)
155
+ #end
156
+
97
157
  @@input_buf = []
98
158
  @@output_buf = []
99
159
 
@@ -121,78 +181,70 @@ class Reline::Windows
121
181
  name =~ /(msys-|cygwin-).*-pty/ ? true : false
122
182
  end
123
183
 
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)
130
- 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
145
- end
184
+ KEY_MAP = [
185
+ # It's treated as Meta+Enter on Windows.
186
+ [ { control_keys: :CTRL, virtual_key_code: 0x0D }, "\e\r".bytes ],
187
+ [ { control_keys: :SHIFT, virtual_key_code: 0x0D }, "\e\r".bytes ],
188
+
189
+ # It's treated as Meta+Space on Windows.
190
+ [ { control_keys: :CTRL, char_code: 0x20 }, "\e ".bytes ],
191
+
192
+ # Emulate getwch() key sequences.
193
+ [ { control_keys: [], virtual_key_code: VK_UP }, [0, 72] ],
194
+ [ { control_keys: [], virtual_key_code: VK_DOWN }, [0, 80] ],
195
+ [ { control_keys: [], virtual_key_code: VK_RIGHT }, [0, 77] ],
196
+ [ { control_keys: [], virtual_key_code: VK_LEFT }, [0, 75] ],
197
+ [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ],
198
+ [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ],
199
+ [ { control_keys: [], virtual_key_code: VK_END }, [0, 79] ],
200
+ ]
201
+
202
+ def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
203
+
204
+ key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
205
+
206
+ match = KEY_MAP.find { |args,| key.matches?(**args) }
207
+ unless match.nil?
208
+ @@output_buf.concat(match.last)
209
+ return
146
210
  end
147
- @@input_buf.shift
211
+
212
+ # no char, only control keys
213
+ return if key.char_code == 0 and key.control_keys.any?
214
+
215
+ @@output_buf.concat(key.char.bytes)
148
216
  end
149
217
 
150
- def self.getc
218
+ def self.check_input_event
151
219
  num_of_events = 0.chr * 8
152
- while @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) != 0 and num_of_events.unpack('L').first > 0
220
+ while @@output_buf.empty? #or true
221
+ next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack('L').first == 0
153
222
  input_record = 0.chr * 18
154
223
  read_event = 0.chr * 4
155
- if @@ReadConsoleInput.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
224
+ if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
156
225
  event = input_record[0, 2].unpack('s*').first
157
- if event == WINDOW_BUFFER_SIZE_EVENT
226
+ case event
227
+ when WINDOW_BUFFER_SIZE_EVENT
158
228
  @@winch_handler.()
229
+ when KEY_EVENT
230
+ key_down = input_record[4, 4].unpack('l*').first
231
+ repeat_count = input_record[8, 2].unpack('s*').first
232
+ virtual_key_code = input_record[10, 2].unpack('s*').first
233
+ virtual_scan_code = input_record[12, 2].unpack('s*').first
234
+ char_code = input_record[14, 2].unpack('S*').first
235
+ control_key_state = input_record[16, 2].unpack('S*').first
236
+ is_key_down = key_down.zero? ? false : true
237
+ if is_key_down
238
+ process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
239
+ end
159
240
  end
160
241
  end
161
242
  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
243
+ end
244
+
245
+ def self.getc
246
+ check_input_event
247
+ @@output_buf.shift
196
248
  end
197
249
 
198
250
  def self.ungetc(c)
@@ -301,4 +353,43 @@ class Reline::Windows
301
353
  def self.deprep(otio)
302
354
  # do nothing
303
355
  end
356
+
357
+ class KeyEventRecord
358
+
359
+ attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys
360
+
361
+ def initialize(virtual_key_code, char_code, control_key_state)
362
+ @virtual_key_code = virtual_key_code
363
+ @char_code = char_code
364
+ @control_key_state = control_key_state
365
+ @enhanced = control_key_state & ENHANCED_KEY != 0
366
+
367
+ (@control_keys = []).tap do |control_keys|
368
+ # symbols must be sorted to make comparison is easier later on
369
+ control_keys << :ALT if control_key_state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0
370
+ control_keys << :CTRL if control_key_state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0
371
+ control_keys << :SHIFT if control_key_state & SHIFT_PRESSED != 0
372
+ end.freeze
373
+ end
374
+
375
+ def char
376
+ @char_code.chr(Encoding::UTF_8)
377
+ end
378
+
379
+ def enhanced?
380
+ @enhanced
381
+ end
382
+
383
+ # Verifies if the arguments match with this key event.
384
+ # Nil arguments are ignored, but at least one must be passed as non-nil.
385
+ # To verify that no control keys were pressed, pass an empty array: `control_keys: []`.
386
+ def matches?(control_keys: nil, virtual_key_code: nil, char_code: nil)
387
+ raise ArgumentError, 'No argument was passed to match key event' if control_keys.nil? && virtual_key_code.nil? && char_code.nil?
388
+
389
+ (control_keys.nil? || [*control_keys].sort == @control_keys) &&
390
+ (virtual_key_code.nil? || @virtual_key_code == virtual_key_code) &&
391
+ (char_code.nil? || char_code == @char_code)
392
+ end
393
+
394
+ end
304
395
  end