reline 0.2.5 → 0.2.8.pre.2

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