reline 0.2.3 → 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +46 -0
- data/lib/reline.rb +27 -13
- data/lib/reline/ansi.rb +103 -54
- data/lib/reline/config.rb +30 -13
- data/lib/reline/general_io.rb +11 -3
- data/lib/reline/key_actor/base.rb +12 -0
- data/lib/reline/line_editor.rb +101 -24
- data/lib/reline/terminfo.rb +126 -0
- data/lib/reline/unicode.rb +1 -0
- data/lib/reline/version.rb +1 -1
- data/lib/reline/windows.rb +171 -80
- metadata +4 -60
- data/lib/reline/line_editor.rb.orig +0 -2696
@@ -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
|
data/lib/reline/line_editor.rb
CHANGED
@@ -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(
|
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
|
-
|
161
|
-
|
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.
|
347
|
-
|
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
|
-
#
|
728
|
-
|
729
|
-
|
730
|
-
|
731
|
-
|
732
|
-
|
733
|
-
|
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
|
-
|
1152
|
-
|
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
|
-
|
1201
|
-
|
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
|
-
@
|
1285
|
-
|
1286
|
-
|
1287
|
-
|
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
|
data/lib/reline/unicode.rb
CHANGED
@@ -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
|
data/lib/reline/version.rb
CHANGED
data/lib/reline/windows.rb
CHANGED
@@ -9,23 +9,40 @@ class Reline::Windows
|
|
9
9
|
true
|
10
10
|
end
|
11
11
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
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
|
-
@@
|
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
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
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
|
-
|
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.
|
218
|
+
def self.check_input_event
|
151
219
|
num_of_events = 0.chr * 8
|
152
|
-
while @@
|
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 @@
|
224
|
+
if @@ReadConsoleInputW.(@@hConsoleInputHandle, input_record, 1, read_event) != 0
|
156
225
|
event = input_record[0, 2].unpack('s*').first
|
157
|
-
|
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
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
|
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
|