reline 0.2.3 → 0.2.7

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