reline 0.3.2 → 0.5.9
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 +33 -2
- data/lib/reline/config.rb +58 -77
- data/lib/reline/face.rb +199 -0
- data/lib/reline/history.rb +1 -1
- data/lib/reline/io/ansi.rb +364 -0
- data/lib/reline/io/dumb.rb +106 -0
- data/lib/reline/{windows.rb → io/windows.rb} +108 -102
- data/lib/reline/io.rb +41 -0
- data/lib/reline/key_actor/base.rb +20 -8
- data/lib/reline/key_actor/composite.rb +17 -0
- data/lib/reline/key_actor/emacs.rb +15 -15
- data/lib/reline/key_actor/vi_command.rb +25 -25
- data/lib/reline/key_actor/vi_insert.rb +7 -7
- data/lib/reline/key_actor.rb +1 -0
- data/lib/reline/key_stroke.rb +88 -84
- data/lib/reline/kill_ring.rb +2 -2
- data/lib/reline/line_editor.rb +1095 -1895
- data/lib/reline/terminfo.rb +13 -29
- data/lib/reline/unicode/east_asian_width.rb +91 -59
- data/lib/reline/unicode.rb +95 -64
- data/lib/reline/version.rb +1 -1
- data/lib/reline.rb +156 -240
- metadata +13 -7
- data/lib/reline/ansi.rb +0 -350
- data/lib/reline/general_io.rb +0 -109
@@ -0,0 +1,364 @@
|
|
1
|
+
require 'io/console'
|
2
|
+
require 'io/wait'
|
3
|
+
|
4
|
+
class Reline::ANSI < Reline::IO
|
5
|
+
CAPNAME_KEY_BINDINGS = {
|
6
|
+
'khome' => :ed_move_to_beg,
|
7
|
+
'kend' => :ed_move_to_end,
|
8
|
+
'kdch1' => :key_delete,
|
9
|
+
'kpp' => :ed_search_prev_history,
|
10
|
+
'knp' => :ed_search_next_history,
|
11
|
+
'kcuu1' => :ed_prev_history,
|
12
|
+
'kcud1' => :ed_next_history,
|
13
|
+
'kcuf1' => :ed_next_char,
|
14
|
+
'kcub1' => :ed_prev_char,
|
15
|
+
}
|
16
|
+
|
17
|
+
ANSI_CURSOR_KEY_BINDINGS = {
|
18
|
+
# Up
|
19
|
+
'A' => [:ed_prev_history, {}],
|
20
|
+
# Down
|
21
|
+
'B' => [:ed_next_history, {}],
|
22
|
+
# Right
|
23
|
+
'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }],
|
24
|
+
# Left
|
25
|
+
'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }],
|
26
|
+
# End
|
27
|
+
'F' => [:ed_move_to_end, {}],
|
28
|
+
# Home
|
29
|
+
'H' => [:ed_move_to_beg, {}],
|
30
|
+
}
|
31
|
+
|
32
|
+
if Reline::Terminfo.enabled?
|
33
|
+
Reline::Terminfo.setupterm(0, 2)
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize
|
37
|
+
@input = STDIN
|
38
|
+
@output = STDOUT
|
39
|
+
@buf = []
|
40
|
+
@old_winch_handler = nil
|
41
|
+
end
|
42
|
+
|
43
|
+
def encoding
|
44
|
+
Encoding.default_external
|
45
|
+
end
|
46
|
+
|
47
|
+
def set_default_key_bindings(config, allow_terminfo: true)
|
48
|
+
set_bracketed_paste_key_bindings(config)
|
49
|
+
set_default_key_bindings_ansi_cursor(config)
|
50
|
+
if allow_terminfo && Reline::Terminfo.enabled?
|
51
|
+
set_default_key_bindings_terminfo(config)
|
52
|
+
else
|
53
|
+
set_default_key_bindings_comprehensive_list(config)
|
54
|
+
end
|
55
|
+
{
|
56
|
+
[27, 91, 90] => :completion_journey_up, # S-Tab
|
57
|
+
}.each_pair do |key, func|
|
58
|
+
config.add_default_key_binding_by_keymap(:emacs, key, func)
|
59
|
+
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
|
60
|
+
end
|
61
|
+
{
|
62
|
+
# default bindings
|
63
|
+
[27, 32] => :em_set_mark, # M-<space>
|
64
|
+
[24, 24] => :em_exchange_mark, # C-x C-x
|
65
|
+
}.each_pair do |key, func|
|
66
|
+
config.add_default_key_binding_by_keymap(:emacs, key, func)
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def set_bracketed_paste_key_bindings(config)
|
71
|
+
[:emacs, :vi_insert, :vi_command].each do |keymap|
|
72
|
+
config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def set_default_key_bindings_ansi_cursor(config)
|
77
|
+
ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
|
78
|
+
bindings = [["\e[#{char}", default_func]] # CSI + char
|
79
|
+
if modifiers[:ctrl]
|
80
|
+
# CSI + ctrl_key_modifier + char
|
81
|
+
bindings << ["\e[1;5#{char}", modifiers[:ctrl]]
|
82
|
+
end
|
83
|
+
if modifiers[:meta]
|
84
|
+
# CSI + meta_key_modifier + char
|
85
|
+
bindings << ["\e[1;3#{char}", modifiers[:meta]]
|
86
|
+
# Meta(ESC) + CSI + char
|
87
|
+
bindings << ["\e\e[#{char}", modifiers[:meta]]
|
88
|
+
end
|
89
|
+
bindings.each do |sequence, func|
|
90
|
+
key = sequence.bytes
|
91
|
+
config.add_default_key_binding_by_keymap(:emacs, key, func)
|
92
|
+
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
|
93
|
+
config.add_default_key_binding_by_keymap(:vi_command, key, func)
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def set_default_key_bindings_terminfo(config)
|
99
|
+
key_bindings = CAPNAME_KEY_BINDINGS.map do |capname, key_binding|
|
100
|
+
begin
|
101
|
+
key_code = Reline::Terminfo.tigetstr(capname)
|
102
|
+
[ key_code.bytes, key_binding ]
|
103
|
+
rescue Reline::Terminfo::TerminfoError
|
104
|
+
# capname is undefined
|
105
|
+
end
|
106
|
+
end.compact.to_h
|
107
|
+
|
108
|
+
key_bindings.each_pair do |key, func|
|
109
|
+
config.add_default_key_binding_by_keymap(:emacs, key, func)
|
110
|
+
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
|
111
|
+
config.add_default_key_binding_by_keymap(:vi_command, key, func)
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
def set_default_key_bindings_comprehensive_list(config)
|
116
|
+
{
|
117
|
+
# xterm
|
118
|
+
[27, 91, 51, 126] => :key_delete, # kdch1
|
119
|
+
[27, 91, 53, 126] => :ed_search_prev_history, # kpp
|
120
|
+
[27, 91, 54, 126] => :ed_search_next_history, # knp
|
121
|
+
|
122
|
+
# Console (80x25)
|
123
|
+
[27, 91, 49, 126] => :ed_move_to_beg, # Home
|
124
|
+
[27, 91, 52, 126] => :ed_move_to_end, # End
|
125
|
+
|
126
|
+
# KDE
|
127
|
+
# Del is 0x08
|
128
|
+
[27, 71, 65] => :ed_prev_history, # ↑
|
129
|
+
[27, 71, 66] => :ed_next_history, # ↓
|
130
|
+
[27, 71, 67] => :ed_next_char, # →
|
131
|
+
[27, 71, 68] => :ed_prev_char, # ←
|
132
|
+
|
133
|
+
# urxvt / exoterm
|
134
|
+
[27, 91, 55, 126] => :ed_move_to_beg, # Home
|
135
|
+
[27, 91, 56, 126] => :ed_move_to_end, # End
|
136
|
+
|
137
|
+
# GNOME
|
138
|
+
[27, 79, 72] => :ed_move_to_beg, # Home
|
139
|
+
[27, 79, 70] => :ed_move_to_end, # End
|
140
|
+
# Del is 0x08
|
141
|
+
# Arrow keys are the same of KDE
|
142
|
+
|
143
|
+
[27, 79, 65] => :ed_prev_history, # ↑
|
144
|
+
[27, 79, 66] => :ed_next_history, # ↓
|
145
|
+
[27, 79, 67] => :ed_next_char, # →
|
146
|
+
[27, 79, 68] => :ed_prev_char, # ←
|
147
|
+
}.each_pair do |key, func|
|
148
|
+
config.add_default_key_binding_by_keymap(:emacs, key, func)
|
149
|
+
config.add_default_key_binding_by_keymap(:vi_insert, key, func)
|
150
|
+
config.add_default_key_binding_by_keymap(:vi_command, key, func)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def input=(val)
|
155
|
+
@input = val
|
156
|
+
end
|
157
|
+
|
158
|
+
def output=(val)
|
159
|
+
@output = val
|
160
|
+
end
|
161
|
+
|
162
|
+
def with_raw_input
|
163
|
+
if @input.tty?
|
164
|
+
@input.raw(intr: true) { yield }
|
165
|
+
else
|
166
|
+
yield
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
def inner_getc(timeout_second)
|
171
|
+
unless @buf.empty?
|
172
|
+
return @buf.shift
|
173
|
+
end
|
174
|
+
until @input.wait_readable(0.01)
|
175
|
+
timeout_second -= 0.01
|
176
|
+
return nil if timeout_second <= 0
|
177
|
+
|
178
|
+
Reline.core.line_editor.handle_signal
|
179
|
+
end
|
180
|
+
c = @input.getbyte
|
181
|
+
(c == 0x16 && @input.tty? && @input.raw(min: 0, time: 0, &:getbyte)) || c
|
182
|
+
rescue Errno::EIO
|
183
|
+
# Maybe the I/O has been closed.
|
184
|
+
nil
|
185
|
+
end
|
186
|
+
|
187
|
+
START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
|
188
|
+
END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
|
189
|
+
def read_bracketed_paste
|
190
|
+
buffer = String.new(encoding: Encoding::ASCII_8BIT)
|
191
|
+
until buffer.end_with?(END_BRACKETED_PASTE)
|
192
|
+
c = inner_getc(Float::INFINITY)
|
193
|
+
break unless c
|
194
|
+
buffer << c
|
195
|
+
end
|
196
|
+
string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding)
|
197
|
+
string.valid_encoding? ? string : ''
|
198
|
+
end
|
199
|
+
|
200
|
+
# if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
|
201
|
+
def getc(timeout_second)
|
202
|
+
inner_getc(timeout_second)
|
203
|
+
end
|
204
|
+
|
205
|
+
def in_pasting?
|
206
|
+
not empty_buffer?
|
207
|
+
end
|
208
|
+
|
209
|
+
def empty_buffer?
|
210
|
+
unless @buf.empty?
|
211
|
+
return false
|
212
|
+
end
|
213
|
+
!@input.wait_readable(0)
|
214
|
+
end
|
215
|
+
|
216
|
+
def ungetc(c)
|
217
|
+
@buf.unshift(c)
|
218
|
+
end
|
219
|
+
|
220
|
+
def retrieve_keybuffer
|
221
|
+
begin
|
222
|
+
return unless @input.wait_readable(0.001)
|
223
|
+
str = @input.read_nonblock(1024)
|
224
|
+
str.bytes.each do |c|
|
225
|
+
@buf.push(c)
|
226
|
+
end
|
227
|
+
rescue EOFError
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
def get_screen_size
|
232
|
+
s = @input.winsize
|
233
|
+
return s if s[0] > 0 && s[1] > 0
|
234
|
+
s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
|
235
|
+
return s if s[0] > 0 && s[1] > 0
|
236
|
+
[24, 80]
|
237
|
+
rescue Errno::ENOTTY, Errno::ENODEV
|
238
|
+
[24, 80]
|
239
|
+
end
|
240
|
+
|
241
|
+
def set_screen_size(rows, columns)
|
242
|
+
@input.winsize = [rows, columns]
|
243
|
+
self
|
244
|
+
rescue Errno::ENOTTY, Errno::ENODEV
|
245
|
+
self
|
246
|
+
end
|
247
|
+
|
248
|
+
def cursor_pos
|
249
|
+
if both_tty?
|
250
|
+
res = +''
|
251
|
+
m = nil
|
252
|
+
@input.raw do |stdin|
|
253
|
+
@output << "\e[6n"
|
254
|
+
@output.flush
|
255
|
+
loop do
|
256
|
+
c = stdin.getc
|
257
|
+
next if c.nil?
|
258
|
+
res << c
|
259
|
+
m = res.match(/\e\[(?<row>\d+);(?<column>\d+)R/)
|
260
|
+
break if m
|
261
|
+
end
|
262
|
+
(m.pre_match + m.post_match).chars.reverse_each do |ch|
|
263
|
+
stdin.ungetc ch
|
264
|
+
end
|
265
|
+
end
|
266
|
+
column = m[:column].to_i - 1
|
267
|
+
row = m[:row].to_i - 1
|
268
|
+
else
|
269
|
+
begin
|
270
|
+
buf = @output.pread(@output.pos, 0)
|
271
|
+
row = buf.count("\n")
|
272
|
+
column = buf.rindex("\n") ? (buf.size - buf.rindex("\n")) - 1 : 0
|
273
|
+
rescue Errno::ESPIPE, IOError
|
274
|
+
# Just returns column 1 for ambiguous width because this I/O is not
|
275
|
+
# tty and can't seek.
|
276
|
+
row = 0
|
277
|
+
column = 1
|
278
|
+
end
|
279
|
+
end
|
280
|
+
Reline::CursorPos.new(column, row)
|
281
|
+
end
|
282
|
+
|
283
|
+
def both_tty?
|
284
|
+
@input.tty? && @output.tty?
|
285
|
+
end
|
286
|
+
|
287
|
+
def move_cursor_column(x)
|
288
|
+
@output.write "\e[#{x + 1}G"
|
289
|
+
end
|
290
|
+
|
291
|
+
def move_cursor_up(x)
|
292
|
+
if x > 0
|
293
|
+
@output.write "\e[#{x}A"
|
294
|
+
elsif x < 0
|
295
|
+
move_cursor_down(-x)
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
def move_cursor_down(x)
|
300
|
+
if x > 0
|
301
|
+
@output.write "\e[#{x}B"
|
302
|
+
elsif x < 0
|
303
|
+
move_cursor_up(-x)
|
304
|
+
end
|
305
|
+
end
|
306
|
+
|
307
|
+
def hide_cursor
|
308
|
+
seq = "\e[?25l"
|
309
|
+
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
|
310
|
+
begin
|
311
|
+
seq = Reline::Terminfo.tigetstr('civis')
|
312
|
+
rescue Reline::Terminfo::TerminfoError
|
313
|
+
# civis is undefined
|
314
|
+
end
|
315
|
+
end
|
316
|
+
@output.write seq
|
317
|
+
end
|
318
|
+
|
319
|
+
def show_cursor
|
320
|
+
seq = "\e[?25h"
|
321
|
+
if Reline::Terminfo.enabled? && Reline::Terminfo.term_supported?
|
322
|
+
begin
|
323
|
+
seq = Reline::Terminfo.tigetstr('cnorm')
|
324
|
+
rescue Reline::Terminfo::TerminfoError
|
325
|
+
# cnorm is undefined
|
326
|
+
end
|
327
|
+
end
|
328
|
+
@output.write seq
|
329
|
+
end
|
330
|
+
|
331
|
+
def erase_after_cursor
|
332
|
+
@output.write "\e[K"
|
333
|
+
end
|
334
|
+
|
335
|
+
# This only works when the cursor is at the bottom of the scroll range
|
336
|
+
# For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
|
337
|
+
def scroll_down(x)
|
338
|
+
return if x.zero?
|
339
|
+
# We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
|
340
|
+
@output.write "\n" * x
|
341
|
+
end
|
342
|
+
|
343
|
+
def clear_screen
|
344
|
+
@output.write "\e[2J"
|
345
|
+
@output.write "\e[1;1H"
|
346
|
+
end
|
347
|
+
|
348
|
+
def set_winch_handler(&handler)
|
349
|
+
@old_winch_handler = Signal.trap('WINCH', &handler)
|
350
|
+
end
|
351
|
+
|
352
|
+
def prep
|
353
|
+
# Enable bracketed paste
|
354
|
+
@output.write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty?
|
355
|
+
retrieve_keybuffer
|
356
|
+
nil
|
357
|
+
end
|
358
|
+
|
359
|
+
def deprep(otio)
|
360
|
+
# Disable bracketed paste
|
361
|
+
@output.write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty?
|
362
|
+
Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
|
363
|
+
end
|
364
|
+
end
|
@@ -0,0 +1,106 @@
|
|
1
|
+
require 'io/wait'
|
2
|
+
|
3
|
+
class Reline::Dumb < Reline::IO
|
4
|
+
RESET_COLOR = '' # Do not send color reset sequence
|
5
|
+
|
6
|
+
def initialize(encoding: nil)
|
7
|
+
@input = STDIN
|
8
|
+
@buf = []
|
9
|
+
@pasting = false
|
10
|
+
@encoding = encoding
|
11
|
+
@screen_size = [24, 80]
|
12
|
+
end
|
13
|
+
|
14
|
+
def dumb?
|
15
|
+
true
|
16
|
+
end
|
17
|
+
|
18
|
+
def encoding
|
19
|
+
if @encoding
|
20
|
+
@encoding
|
21
|
+
elsif RUBY_PLATFORM =~ /mswin|mingw/
|
22
|
+
Encoding::UTF_8
|
23
|
+
else
|
24
|
+
Encoding::default_external
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def set_default_key_bindings(_)
|
29
|
+
end
|
30
|
+
|
31
|
+
def input=(val)
|
32
|
+
@input = val
|
33
|
+
end
|
34
|
+
|
35
|
+
def with_raw_input
|
36
|
+
yield
|
37
|
+
end
|
38
|
+
|
39
|
+
def getc(_timeout_second)
|
40
|
+
unless @buf.empty?
|
41
|
+
return @buf.shift
|
42
|
+
end
|
43
|
+
c = nil
|
44
|
+
loop do
|
45
|
+
Reline.core.line_editor.handle_signal
|
46
|
+
result = @input.wait_readable(0.1)
|
47
|
+
next if result.nil?
|
48
|
+
c = @input.read(1)
|
49
|
+
break
|
50
|
+
end
|
51
|
+
c&.ord
|
52
|
+
end
|
53
|
+
|
54
|
+
def ungetc(c)
|
55
|
+
@buf.unshift(c)
|
56
|
+
end
|
57
|
+
|
58
|
+
def get_screen_size
|
59
|
+
@screen_size
|
60
|
+
end
|
61
|
+
|
62
|
+
def cursor_pos
|
63
|
+
Reline::CursorPos.new(1, 1)
|
64
|
+
end
|
65
|
+
|
66
|
+
def hide_cursor
|
67
|
+
end
|
68
|
+
|
69
|
+
def show_cursor
|
70
|
+
end
|
71
|
+
|
72
|
+
def move_cursor_column(val)
|
73
|
+
end
|
74
|
+
|
75
|
+
def move_cursor_up(val)
|
76
|
+
end
|
77
|
+
|
78
|
+
def move_cursor_down(val)
|
79
|
+
end
|
80
|
+
|
81
|
+
def erase_after_cursor
|
82
|
+
end
|
83
|
+
|
84
|
+
def scroll_down(val)
|
85
|
+
end
|
86
|
+
|
87
|
+
def clear_screen
|
88
|
+
end
|
89
|
+
|
90
|
+
def set_screen_size(rows, columns)
|
91
|
+
@screen_size = [rows, columns]
|
92
|
+
end
|
93
|
+
|
94
|
+
def set_winch_handler(&handler)
|
95
|
+
end
|
96
|
+
|
97
|
+
def in_pasting?
|
98
|
+
@pasting
|
99
|
+
end
|
100
|
+
|
101
|
+
def prep
|
102
|
+
end
|
103
|
+
|
104
|
+
def deprep(otio)
|
105
|
+
end
|
106
|
+
end
|