reline 0.2.3 → 0.2.7
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.
- 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
|