reline 0.2.7 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,5 +1,13 @@
1
- require 'fiddle'
2
- require 'fiddle/import'
1
+ begin
2
+ require 'fiddle'
3
+ require 'fiddle/import'
4
+ rescue LoadError
5
+ module Reline::Terminfo
6
+ def self.curses_dl
7
+ false
8
+ end
9
+ end
10
+ end
3
11
 
4
12
  module Reline::Terminfo
5
13
  extend Fiddle::Importer
@@ -50,7 +58,7 @@ module Reline::Terminfo
50
58
  @curses_dl = nil if @curses_dl == false
51
59
  @curses_dl
52
60
  end
53
- end
61
+ end if not Reline.const_defined?(:Terminfo) or not Reline::Terminfo.respond_to?(:curses_dl)
54
62
 
55
63
  module Reline::Terminfo
56
64
  dlload curses_dl
@@ -66,12 +74,27 @@ module Reline::Terminfo
66
74
  #extern 'char *tparm(const char *str, ...)'
67
75
  @tiparm = Fiddle::Function.new(curses_dl['tparm'], [Fiddle::TYPE_VOIDP, Fiddle::TYPE_VARIADIC], Fiddle::TYPE_VOIDP)
68
76
  end
69
- # TODO: add int tigetflag(char *capname) and int tigetnum(char *capname)
77
+ begin
78
+ #extern 'int tigetflag(char *str)'
79
+ @tigetflag = Fiddle::Function.new(curses_dl['tigetflag'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
80
+ rescue Fiddle::DLError
81
+ # OpenBSD lacks tigetflag
82
+ #extern 'int tgetflag(char *str)'
83
+ @tigetflag = Fiddle::Function.new(curses_dl['tgetflag'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
84
+ end
85
+ begin
86
+ #extern 'int tigetnum(char *str)'
87
+ @tigetnum = Fiddle::Function.new(curses_dl['tigetnum'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
88
+ rescue Fiddle::DLError
89
+ # OpenBSD lacks tigetnum
90
+ #extern 'int tgetnum(char *str)'
91
+ @tigetnum = Fiddle::Function.new(curses_dl['tgetnum'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_INT)
92
+ end
70
93
 
71
94
  def self.setupterm(term, fildes)
72
95
  errret_int = String.new("\x00" * 8, encoding: 'ASCII-8BIT')
73
96
  ret = @setupterm.(term, fildes, errret_int)
74
- errret = errret_int.unpack('i')[0]
97
+ errret = errret_int.unpack1('i')
75
98
  case ret
76
99
  when 0 # OK
77
100
  0
@@ -114,6 +137,28 @@ module Reline::Terminfo
114
137
  @tiparm.(str, *new_args).to_s
115
138
  end
116
139
 
140
+ def self.tigetflag(capname)
141
+ flag = @tigetflag.(capname).to_i
142
+ case flag
143
+ when -1
144
+ raise TerminfoError, "not boolean capability: #{capname}"
145
+ when 0
146
+ raise TerminfoError, "can't find capability: #{capname}"
147
+ end
148
+ flag
149
+ end
150
+
151
+ def self.tigetnum(capname)
152
+ num = @tigetnum.(capname).to_i
153
+ case num
154
+ when -2
155
+ raise TerminfoError, "not numeric capability: #{capname}"
156
+ when -1
157
+ raise TerminfoError, "can't find capability: #{capname}"
158
+ end
159
+ num
160
+ end
161
+
117
162
  def self.enabled?
118
163
  true
119
164
  end
@@ -79,6 +79,8 @@ class Reline::Unicode
79
79
 
80
80
  require 'reline/unicode/east_asian_width'
81
81
 
82
+ HalfwidthDakutenHandakuten = /[\u{FF9E}\u{FF9F}]/
83
+
82
84
  MBCharWidthRE = /
83
85
  (?<width_2_1>
84
86
  [#{ EscapedChars.map {|c| "\\x%02x" % c.ord }.join }] (?# ^ + char, such as ^M, ^H, ^[, ...)
@@ -93,6 +95,12 @@ class Reline::Unicode
93
95
  #{ EastAsianWidth::TYPE_H }
94
96
  | #{ EastAsianWidth::TYPE_NA }
95
97
  | #{ EastAsianWidth::TYPE_N }
98
+ )(?!#{ HalfwidthDakutenHandakuten })
99
+ | (?<width_2_3>
100
+ (?: #{ EastAsianWidth::TYPE_H }
101
+ | #{ EastAsianWidth::TYPE_NA }
102
+ | #{ EastAsianWidth::TYPE_N })
103
+ #{ HalfwidthDakutenHandakuten }
96
104
  )
97
105
  | (?<ambiguous_width>
98
106
  #{EastAsianWidth::TYPE_A}
@@ -101,15 +109,15 @@ class Reline::Unicode
101
109
 
102
110
  def self.get_mbchar_width(mbchar)
103
111
  ord = mbchar.ord
104
- if (0x00 <= ord and ord <= 0x1F)
112
+ if (0x00 <= ord and ord <= 0x1F) # in EscapedPairs
105
113
  return 2
106
- elsif (0x20 <= ord and ord <= 0x7E)
114
+ elsif (0x20 <= ord and ord <= 0x7E) # printable ASCII chars
107
115
  return 1
108
116
  end
109
117
  m = mbchar.encode(Encoding::UTF_8).match(MBCharWidthRE)
110
118
  case
111
119
  when m.nil? then 1 # TODO should be U+FFFD � REPLACEMENT CHARACTER
112
- when m[:width_2_1], m[:width_2_2] then 2
120
+ when m[:width_2_1], m[:width_2_2], m[:width_2_3] then 2
113
121
  when m[:width_3] then 3
114
122
  when m[:width_0] then 0
115
123
  when m[:width_1] then 1
@@ -185,6 +193,37 @@ class Reline::Unicode
185
193
  [lines, height]
186
194
  end
187
195
 
196
+ # Take a chunk of a String cut by width with escape sequences.
197
+ def self.take_range(str, start_col, max_width, encoding = str.encoding)
198
+ chunk = String.new(encoding: encoding)
199
+ total_width = 0
200
+ rest = str.encode(Encoding::UTF_8)
201
+ in_zero_width = false
202
+ rest.scan(WIDTH_SCANNER) do |gc|
203
+ case
204
+ when gc[NON_PRINTING_START_INDEX]
205
+ in_zero_width = true
206
+ when gc[NON_PRINTING_END_INDEX]
207
+ in_zero_width = false
208
+ when gc[CSI_REGEXP_INDEX]
209
+ chunk << gc[CSI_REGEXP_INDEX]
210
+ when gc[OSC_REGEXP_INDEX]
211
+ chunk << gc[OSC_REGEXP_INDEX]
212
+ when gc[GRAPHEME_CLUSTER_INDEX]
213
+ gc = gc[GRAPHEME_CLUSTER_INDEX]
214
+ if in_zero_width
215
+ chunk << gc
216
+ else
217
+ mbchar_width = get_mbchar_width(gc)
218
+ total_width += mbchar_width
219
+ break if (start_col + max_width) < total_width
220
+ chunk << gc if start_col < total_width
221
+ end
222
+ end
223
+ end
224
+ chunk
225
+ end
226
+
188
227
  def self.get_next_mbchar_size(line, byte_pointer)
189
228
  grapheme = line.byteslice(byte_pointer..-1).grapheme_clusters.first
190
229
  grapheme ? grapheme.bytesize : 0
@@ -1,3 +1,3 @@
1
1
  module Reline
2
- VERSION = '0.2.7'
2
+ VERSION = '0.3.1'
3
3
  end
@@ -42,6 +42,14 @@ class Reline::Windows
42
42
  }.each_pair do |key, func|
43
43
  config.add_default_key_binding_by_keymap(:emacs, key, func)
44
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
45
53
  end
46
54
 
47
55
  if defined? JRUBY_VERSION
@@ -106,6 +114,7 @@ class Reline::Windows
106
114
  SCROLLLOCK_ON = 0x0040
107
115
  SHIFT_PRESSED = 0x0010
108
116
 
117
+ VK_TAB = 0x09
109
118
  VK_END = 0x23
110
119
  VK_HOME = 0x24
111
120
  VK_LEFT = 0x25
@@ -133,9 +142,11 @@ class Reline::Windows
133
142
  @@GetFileType = Win32API.new('kernel32', 'GetFileType', ['L'], 'L')
134
143
  @@GetFileInformationByHandleEx = Win32API.new('kernel32', 'GetFileInformationByHandleEx', ['L', 'I', 'P', 'L'], 'I')
135
144
  @@FillConsoleOutputAttribute = Win32API.new('kernel32', 'FillConsoleOutputAttribute', ['L', 'L', 'L', 'L', 'P'], 'L')
145
+ @@SetConsoleCursorInfo = Win32API.new('kernel32', 'SetConsoleCursorInfo', ['L', 'P'], 'L')
136
146
 
137
147
  @@GetConsoleMode = Win32API.new('kernel32', 'GetConsoleMode', ['L', 'P'], 'L')
138
148
  @@SetConsoleMode = Win32API.new('kernel32', 'SetConsoleMode', ['L', 'L'], 'L')
149
+ @@WaitForSingleObject = Win32API.new('kernel32', 'WaitForSingleObject', ['L', 'L'], 'L')
139
150
  ENABLE_VIRTUAL_TERMINAL_PROCESSING = 4
140
151
 
141
152
  private_class_method def self.getconsolemode
@@ -157,7 +168,9 @@ class Reline::Windows
157
168
  @@input_buf = []
158
169
  @@output_buf = []
159
170
 
160
- def self.msys_tty?(io=@@hConsoleInputHandle)
171
+ @@output = STDOUT
172
+
173
+ def self.msys_tty?(io = @@hConsoleInputHandle)
161
174
  # check if fd is a pipe
162
175
  if @@GetFileType.call(io) != FILE_TYPE_PIPE
163
176
  return false
@@ -173,7 +186,7 @@ class Reline::Windows
173
186
  # DWORD FileNameLength;
174
187
  # WCHAR FileName[1];
175
188
  # } FILE_NAME_INFO
176
- len = p_buffer[0, 4].unpack("L")[0]
189
+ len = p_buffer[0, 4].unpack1("L")
177
190
  name = p_buffer[4, len].encode(Encoding::UTF_8, Encoding::UTF_16LE, invalid: :replace)
178
191
 
179
192
  # Check if this could be a MSYS2 pty pipe ('\msys-XXXX-ptyN-XX')
@@ -197,10 +210,34 @@ class Reline::Windows
197
210
  [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ],
198
211
  [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ],
199
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] ],
200
216
  ]
201
217
 
218
+ @@hsg = nil
219
+
202
220
  def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
203
221
 
222
+ # high-surrogate
223
+ if 0xD800 <= char_code and char_code <= 0xDBFF
224
+ @@hsg = char_code
225
+ return
226
+ end
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
235
+ end
236
+ else
237
+ # ignore high-surrogate without low-surrogate if there
238
+ @@hsg = nil
239
+ end
240
+
204
241
  key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
205
242
 
206
243
  match = KEY_MAP.find { |args,| key.matches?(**args) }
@@ -212,30 +249,42 @@ class Reline::Windows
212
249
  # no char, only control keys
213
250
  return if key.char_code == 0 and key.control_keys.any?
214
251
 
252
+ @@output_buf.push("\e".ord) if key.control_keys.include?(:ALT)
253
+
215
254
  @@output_buf.concat(key.char.bytes)
216
255
  end
217
256
 
218
257
  def self.check_input_event
219
258
  num_of_events = 0.chr * 8
220
- while @@output_buf.empty? #or true
221
- next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack('L').first == 0
222
- 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
223
268
  read_event = 0.chr * 4
224
- if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
225
- event = input_record[0, 2].unpack('s*').first
226
- case event
227
- when WINDOW_BUFFER_SIZE_EVENT
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)
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
239
288
  end
240
289
  end
241
290
  end
@@ -256,7 +305,7 @@ class Reline::Windows
256
305
  end
257
306
 
258
307
  def self.empty_buffer?
259
- if not @@input_buf.empty?
308
+ if not @@output_buf.empty?
260
309
  false
261
310
  elsif @@kbhit.call == 0
262
311
  true
@@ -265,17 +314,37 @@ class Reline::Windows
265
314
  end
266
315
  end
267
316
 
268
- 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
269
330
  csbi = 0.chr * 22
270
- @@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
271
339
  csbi[0, 4].unpack('SS').reverse
272
340
  end
273
341
 
274
342
  def self.cursor_pos
275
- csbi = 0.chr * 22
276
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
277
- x = csbi[4, 2].unpack('s*').first
278
- 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')
279
348
  Reline::CursorPos.new(x, y)
280
349
  end
281
350
 
@@ -295,6 +364,7 @@ class Reline::Windows
295
364
 
296
365
  def self.move_cursor_down(val)
297
366
  if val > 0
367
+ return unless csbi = get_console_screen_buffer_info
298
368
  screen_height = get_screen_size.first
299
369
  y = cursor_pos.y + val
300
370
  y = screen_height - 1 if y > (screen_height - 1)
@@ -305,42 +375,74 @@ class Reline::Windows
305
375
  end
306
376
 
307
377
  def self.erase_after_cursor
308
- csbi = 0.chr * 24
309
- @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi)
310
- 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')
311
381
  written = 0.chr * 4
312
382
  @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, get_screen_size.last - cursor_pos.x, cursor, written)
313
- @@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)
314
384
  end
315
385
 
316
386
  def self.scroll_down(val)
317
- return if val.zero?
318
- screen_height = get_screen_size.first
319
- val = screen_height - 1 if val > (screen_height - 1)
320
- scroll_rectangle = [0, val, get_screen_size.last, get_screen_size.first].pack('s4')
321
- destination_origin = 0 # y * 65536 + x
322
- fill = [' '.ord, 0].pack('SS')
323
- @@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
324
411
  end
325
412
 
326
413
  def self.clear_screen
327
- csbi = 0.chr * 22
328
- return if @@GetConsoleScreenBufferInfo.call(@@hConsoleHandle, csbi) == 0
329
- buffer_width = csbi[0, 2].unpack('S').first
330
- attributes = csbi[8, 2].unpack('S').first
331
- _window_left, window_top, _window_right, window_bottom = *csbi[10,8].unpack('S*')
332
- fill_length = buffer_width * (window_bottom - window_top + 1)
333
- screen_topleft = window_top * 65536
334
- written = 0.chr * 4
335
- @@FillConsoleOutputCharacter.call(@@hConsoleHandle, 0x20, fill_length, screen_topleft, written)
336
- @@FillConsoleOutputAttribute.call(@@hConsoleHandle, attributes, fill_length, screen_topleft, written)
337
- @@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
338
426
  end
339
427
 
340
428
  def self.set_screen_size(rows, columns)
341
429
  raise NotImplementedError
342
430
  end
343
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
+
344
446
  def self.set_winch_handler(&handler)
345
447
  @@winch_handler = handler
346
448
  end