reline 0.2.5 → 0.2.8.pre.2

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.
@@ -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
@@ -185,6 +185,36 @@ class Reline::Unicode
185
185
  [lines, height]
186
186
  end
187
187
 
188
+ # Take a chunk of a String with escape sequences.
189
+ def self.take_range(str, col, length, encoding = str.encoding)
190
+ chunk = String.new(encoding: encoding)
191
+ width = 0
192
+ rest = str.encode(Encoding::UTF_8)
193
+ in_zero_width = false
194
+ rest.scan(WIDTH_SCANNER) do |gc|
195
+ case
196
+ when gc[NON_PRINTING_START_INDEX]
197
+ in_zero_width = true
198
+ when gc[NON_PRINTING_END_INDEX]
199
+ in_zero_width = false
200
+ when gc[CSI_REGEXP_INDEX]
201
+ chunk << gc[CSI_REGEXP_INDEX]
202
+ when gc[OSC_REGEXP_INDEX]
203
+ chunk << gc[OSC_REGEXP_INDEX]
204
+ when gc[GRAPHEME_CLUSTER_INDEX]
205
+ gc = gc[GRAPHEME_CLUSTER_INDEX]
206
+ if in_zero_width
207
+ chunk << gc
208
+ else
209
+ width = get_mbchar_width(gc)
210
+ break if (width + length) <= col
211
+ chunk << gc if col <= width
212
+ end
213
+ end
214
+ end
215
+ chunk
216
+ end
217
+
188
218
  def self.get_next_mbchar_size(line, byte_pointer)
189
219
  grapheme = line.byteslice(byte_pointer..-1).grapheme_clusters.first
190
220
  grapheme ? grapheme.bytesize : 0
@@ -1,3 +1,3 @@
1
1
  module Reline
2
- VERSION = '0.2.5'
2
+ VERSION = '0.2.8.pre.2'
3
3
  end
@@ -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
@@ -145,78 +192,74 @@ class Reline::Windows
145
192
  name =~ /(msys-|cygwin-).*-pty/ ? true : false
146
193
  end
147
194
 
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)
154
- 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
169
- end
195
+ KEY_MAP = [
196
+ # It's treated as Meta+Enter on Windows.
197
+ [ { control_keys: :CTRL, virtual_key_code: 0x0D }, "\e\r".bytes ],
198
+ [ { control_keys: :SHIFT, virtual_key_code: 0x0D }, "\e\r".bytes ],
199
+
200
+ # It's treated as Meta+Space on Windows.
201
+ [ { control_keys: :CTRL, char_code: 0x20 }, "\e ".bytes ],
202
+
203
+ # Emulate getwch() key sequences.
204
+ [ { control_keys: [], virtual_key_code: VK_UP }, [0, 72] ],
205
+ [ { control_keys: [], virtual_key_code: VK_DOWN }, [0, 80] ],
206
+ [ { control_keys: [], virtual_key_code: VK_RIGHT }, [0, 77] ],
207
+ [ { control_keys: [], virtual_key_code: VK_LEFT }, [0, 75] ],
208
+ [ { control_keys: [], virtual_key_code: VK_DELETE }, [0, 83] ],
209
+ [ { control_keys: [], virtual_key_code: VK_HOME }, [0, 71] ],
210
+ [ { control_keys: [], virtual_key_code: VK_END }, [0, 79] ],
211
+
212
+ # Emulate ANSI key sequence.
213
+ [ { control_keys: :SHIFT, virtual_key_code: VK_TAB }, [27, 91, 90] ],
214
+ ]
215
+
216
+ def self.process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
217
+
218
+ key = KeyEventRecord.new(virtual_key_code, char_code, control_key_state)
219
+
220
+ match = KEY_MAP.find { |args,| key.matches?(**args) }
221
+ unless match.nil?
222
+ @@output_buf.concat(match.last)
223
+ return
170
224
  end
171
- @@input_buf.shift
225
+
226
+ # no char, only control keys
227
+ return if key.char_code == 0 and key.control_keys.any?
228
+
229
+ @@output_buf.concat(key.char.bytes)
172
230
  end
173
231
 
174
- def self.getc
232
+ def self.check_input_event
175
233
  num_of_events = 0.chr * 8
176
- while @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) != 0 and num_of_events.unpack('L').first > 0
234
+ while @@output_buf.empty? #or true
235
+ next if @@WaitForSingleObject.(@@hConsoleInputHandle, 100) != 0 # max 0.1 sec
236
+ next if @@GetNumberOfConsoleInputEvents.(@@hConsoleInputHandle, num_of_events) == 0 or num_of_events.unpack('L').first == 0
177
237
  input_record = 0.chr * 18
178
238
  read_event = 0.chr * 4
179
- if @@ReadConsoleInput.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
239
+ if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
180
240
  event = input_record[0, 2].unpack('s*').first
181
- if event == WINDOW_BUFFER_SIZE_EVENT
241
+ case event
242
+ when WINDOW_BUFFER_SIZE_EVENT
182
243
  @@winch_handler.()
244
+ when KEY_EVENT
245
+ key_down = input_record[4, 4].unpack('l*').first
246
+ repeat_count = input_record[8, 2].unpack('s*').first
247
+ virtual_key_code = input_record[10, 2].unpack('s*').first
248
+ virtual_scan_code = input_record[12, 2].unpack('s*').first
249
+ char_code = input_record[14, 2].unpack('S*').first
250
+ control_key_state = input_record[16, 2].unpack('S*').first
251
+ is_key_down = key_down.zero? ? false : true
252
+ if is_key_down
253
+ process_key_event(repeat_count, virtual_key_code, virtual_scan_code, char_code, control_key_state)
254
+ end
183
255
  end
184
256
  end
185
257
  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
258
+ end
259
+
260
+ def self.getc
261
+ check_input_event
262
+ @@output_buf.shift
220
263
  end
221
264
 
222
265
  def self.ungetc(c)
@@ -313,6 +356,20 @@ class Reline::Windows
313
356
  raise NotImplementedError
314
357
  end
315
358
 
359
+ def self.hide_cursor
360
+ size = 100
361
+ visible = 0 # 0 means false
362
+ cursor_info = [size, visible].pack('Li')
363
+ @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
364
+ end
365
+
366
+ def self.show_cursor
367
+ size = 100
368
+ visible = 1 # 1 means true
369
+ cursor_info = [size, visible].pack('Li')
370
+ @@SetConsoleCursorInfo.call(@@hConsoleHandle, cursor_info)
371
+ end
372
+
316
373
  def self.set_winch_handler(&handler)
317
374
  @@winch_handler = handler
318
375
  end
@@ -325,4 +382,43 @@ class Reline::Windows
325
382
  def self.deprep(otio)
326
383
  # do nothing
327
384
  end
385
+
386
+ class KeyEventRecord
387
+
388
+ attr_reader :virtual_key_code, :char_code, :control_key_state, :control_keys
389
+
390
+ def initialize(virtual_key_code, char_code, control_key_state)
391
+ @virtual_key_code = virtual_key_code
392
+ @char_code = char_code
393
+ @control_key_state = control_key_state
394
+ @enhanced = control_key_state & ENHANCED_KEY != 0
395
+
396
+ (@control_keys = []).tap do |control_keys|
397
+ # symbols must be sorted to make comparison is easier later on
398
+ control_keys << :ALT if control_key_state & (LEFT_ALT_PRESSED | RIGHT_ALT_PRESSED) != 0
399
+ control_keys << :CTRL if control_key_state & (LEFT_CTRL_PRESSED | RIGHT_CTRL_PRESSED) != 0
400
+ control_keys << :SHIFT if control_key_state & SHIFT_PRESSED != 0
401
+ end.freeze
402
+ end
403
+
404
+ def char
405
+ @char_code.chr(Encoding::UTF_8)
406
+ end
407
+
408
+ def enhanced?
409
+ @enhanced
410
+ end
411
+
412
+ # Verifies if the arguments match with this key event.
413
+ # Nil arguments are ignored, but at least one must be passed as non-nil.
414
+ # To verify that no control keys were pressed, pass an empty array: `control_keys: []`.
415
+ def matches?(control_keys: nil, virtual_key_code: nil, char_code: nil)
416
+ raise ArgumentError, 'No argument was passed to match key event' if control_keys.nil? && virtual_key_code.nil? && char_code.nil?
417
+
418
+ (control_keys.nil? || [*control_keys].sort == @control_keys) &&
419
+ (virtual_key_code.nil? || @virtual_key_code == virtual_key_code) &&
420
+ (char_code.nil? || char_code == @char_code)
421
+ end
422
+
423
+ end
328
424
  end