reline 0.3.5 → 0.6.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,322 @@
1
+ require 'io/console'
2
+ require 'io/wait'
3
+
4
+ class Reline::ANSI < Reline::IO
5
+ ANSI_CURSOR_KEY_BINDINGS = {
6
+ # Up
7
+ 'A' => [:ed_prev_history, {}],
8
+ # Down
9
+ 'B' => [:ed_next_history, {}],
10
+ # Right
11
+ 'C' => [:ed_next_char, { ctrl: :em_next_word, meta: :em_next_word }],
12
+ # Left
13
+ 'D' => [:ed_prev_char, { ctrl: :ed_prev_word, meta: :ed_prev_word }],
14
+ # End
15
+ 'F' => [:ed_move_to_end, {}],
16
+ # Home
17
+ 'H' => [:ed_move_to_beg, {}],
18
+ }
19
+
20
+ attr_writer :input, :output
21
+
22
+ def initialize
23
+ @input = STDIN
24
+ @output = STDOUT
25
+ @buf = []
26
+ @output_buffer = nil
27
+ @old_winch_handler = nil
28
+ end
29
+
30
+ def encoding
31
+ @input.external_encoding || Encoding.default_external
32
+ rescue IOError
33
+ # STDIN.external_encoding raises IOError in Ruby <= 3.0 when STDIN is closed
34
+ Encoding.default_external
35
+ end
36
+
37
+ def set_default_key_bindings(config)
38
+ set_bracketed_paste_key_bindings(config)
39
+ set_default_key_bindings_ansi_cursor(config)
40
+ set_default_key_bindings_comprehensive_list(config)
41
+ {
42
+ [27, 91, 90] => :completion_journey_up, # S-Tab
43
+ }.each_pair do |key, func|
44
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
45
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
46
+ end
47
+ {
48
+ # default bindings
49
+ [27, 32] => :em_set_mark, # M-<space>
50
+ [24, 24] => :em_exchange_mark, # C-x C-x
51
+ }.each_pair do |key, func|
52
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
53
+ end
54
+ end
55
+
56
+ def set_bracketed_paste_key_bindings(config)
57
+ [:emacs, :vi_insert, :vi_command].each do |keymap|
58
+ config.add_default_key_binding_by_keymap(keymap, START_BRACKETED_PASTE.bytes, :bracketed_paste_start)
59
+ end
60
+ end
61
+
62
+ def set_default_key_bindings_ansi_cursor(config)
63
+ ANSI_CURSOR_KEY_BINDINGS.each do |char, (default_func, modifiers)|
64
+ bindings = [
65
+ ["\e[#{char}", default_func], # CSI + char
66
+ ["\eO#{char}", default_func] # SS3 + char, application cursor key mode
67
+ ]
68
+ if modifiers[:ctrl]
69
+ # CSI + ctrl_key_modifier + char
70
+ bindings << ["\e[1;5#{char}", modifiers[:ctrl]]
71
+ end
72
+ if modifiers[:meta]
73
+ # CSI + meta_key_modifier + char
74
+ bindings << ["\e[1;3#{char}", modifiers[:meta]]
75
+ # Meta(ESC) + CSI + char
76
+ bindings << ["\e\e[#{char}", modifiers[:meta]]
77
+ end
78
+ bindings.each do |sequence, func|
79
+ key = sequence.bytes
80
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
81
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
82
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
83
+ end
84
+ end
85
+ end
86
+
87
+ def set_default_key_bindings_comprehensive_list(config)
88
+ {
89
+ # xterm
90
+ [27, 91, 51, 126] => :key_delete, # kdch1
91
+ [27, 91, 53, 126] => :ed_search_prev_history, # kpp
92
+ [27, 91, 54, 126] => :ed_search_next_history, # knp
93
+
94
+ # Console (80x25)
95
+ [27, 91, 49, 126] => :ed_move_to_beg, # Home
96
+ [27, 91, 52, 126] => :ed_move_to_end, # End
97
+
98
+ # urxvt / exoterm
99
+ [27, 91, 55, 126] => :ed_move_to_beg, # Home
100
+ [27, 91, 56, 126] => :ed_move_to_end, # End
101
+ }.each_pair do |key, func|
102
+ config.add_default_key_binding_by_keymap(:emacs, key, func)
103
+ config.add_default_key_binding_by_keymap(:vi_insert, key, func)
104
+ config.add_default_key_binding_by_keymap(:vi_command, key, func)
105
+ end
106
+ end
107
+
108
+ def with_raw_input
109
+ if @input.tty?
110
+ @input.raw(intr: true) { yield }
111
+ else
112
+ yield
113
+ end
114
+ end
115
+
116
+ def inner_getc(timeout_second)
117
+ unless @buf.empty?
118
+ return @buf.shift
119
+ end
120
+ until @input.wait_readable(0.01)
121
+ timeout_second -= 0.01
122
+ return nil if timeout_second <= 0
123
+
124
+ Reline.core.line_editor.handle_signal
125
+ end
126
+ c = @input.getbyte
127
+
128
+ # When "Escape non-ASCII Input with Control-V" is enabled in macOS Terminal.app,
129
+ # all non-ascii bytes are automatically escaped with `C-v`.
130
+ # "\xE3\x81\x82" (U+3042) becomes "\x16\xE3\x16\x81\x16\x82".
131
+ (c == 0x16 && @input.tty? && @input.raw(min: 0, time: 0, &:getbyte)) || c
132
+ rescue Errno::EIO
133
+ # Maybe the I/O has been closed.
134
+ nil
135
+ end
136
+
137
+ START_BRACKETED_PASTE = String.new("\e[200~", encoding: Encoding::ASCII_8BIT)
138
+ END_BRACKETED_PASTE = String.new("\e[201~", encoding: Encoding::ASCII_8BIT)
139
+ def read_bracketed_paste
140
+ buffer = String.new(encoding: Encoding::ASCII_8BIT)
141
+ until buffer.end_with?(END_BRACKETED_PASTE)
142
+ c = inner_getc(Float::INFINITY)
143
+ break unless c
144
+ buffer << c
145
+ end
146
+ string = buffer.delete_suffix(END_BRACKETED_PASTE).force_encoding(encoding)
147
+ string.valid_encoding? ? string : ''
148
+ end
149
+
150
+ # if the usage expects to wait indefinitely, use Float::INFINITY for timeout_second
151
+ def getc(timeout_second)
152
+ inner_getc(timeout_second)
153
+ end
154
+
155
+ def in_pasting?
156
+ not empty_buffer?
157
+ end
158
+
159
+ def empty_buffer?
160
+ unless @buf.empty?
161
+ return false
162
+ end
163
+ !@input.wait_readable(0)
164
+ end
165
+
166
+ def ungetc(c)
167
+ @buf.unshift(c)
168
+ end
169
+
170
+ def retrieve_keybuffer
171
+ begin
172
+ return unless @input.wait_readable(0.001)
173
+ str = @input.read_nonblock(1024)
174
+ str.bytes.each do |c|
175
+ @buf.push(c)
176
+ end
177
+ rescue EOFError
178
+ end
179
+ end
180
+
181
+ def get_screen_size
182
+ s = @input.winsize
183
+ return s if s[0] > 0 && s[1] > 0
184
+ s = [ENV["LINES"].to_i, ENV["COLUMNS"].to_i]
185
+ return s if s[0] > 0 && s[1] > 0
186
+ [24, 80]
187
+ rescue SystemCallError
188
+ [24, 80]
189
+ end
190
+
191
+ def set_screen_size(rows, columns)
192
+ @input.winsize = [rows, columns]
193
+ self
194
+ rescue SystemCallError
195
+ self
196
+ end
197
+
198
+ private def cursor_pos_internal(timeout:)
199
+ match = nil
200
+ @input.raw do |stdin|
201
+ @output << "\e[6n"
202
+ @output.flush
203
+ timeout_at = Time.now + timeout
204
+ buf = +''
205
+ while (wait = timeout_at - Time.now) > 0 && stdin.wait_readable(wait)
206
+ buf << stdin.readpartial(1024)
207
+ if (match = buf.match(/\e\[(?<row>\d+);(?<column>\d+)R/))
208
+ buf = match.pre_match + match.post_match
209
+ break
210
+ end
211
+ end
212
+ @buf.concat buf.bytes
213
+ end
214
+ [match[:column].to_i - 1, match[:row].to_i - 1] if match
215
+ end
216
+
217
+ def cursor_pos
218
+ col, row = cursor_pos_internal(timeout: 0.5) if both_tty?
219
+ Reline::CursorPos.new(col || 0, row || 0)
220
+ end
221
+
222
+ def both_tty?
223
+ @input.tty? && @output.tty?
224
+ end
225
+
226
+ def write(string)
227
+ if @output_buffer
228
+ @output_buffer << string
229
+ else
230
+ @output.write(string)
231
+ end
232
+ end
233
+
234
+ def buffered_output
235
+ @output_buffer = +''
236
+ yield
237
+ @output.write(@output_buffer)
238
+ ensure
239
+ @output_buffer = nil
240
+ end
241
+
242
+ def move_cursor_column(x)
243
+ write "\e[#{x + 1}G"
244
+ end
245
+
246
+ def move_cursor_up(x)
247
+ if x > 0
248
+ write "\e[#{x}A"
249
+ elsif x < 0
250
+ move_cursor_down(-x)
251
+ end
252
+ end
253
+
254
+ def move_cursor_down(x)
255
+ if x > 0
256
+ write "\e[#{x}B"
257
+ elsif x < 0
258
+ move_cursor_up(-x)
259
+ end
260
+ end
261
+
262
+ def hide_cursor
263
+ write "\e[?25l"
264
+ end
265
+
266
+ def show_cursor
267
+ write "\e[?25h"
268
+ end
269
+
270
+ def erase_after_cursor
271
+ write "\e[K"
272
+ end
273
+
274
+ # This only works when the cursor is at the bottom of the scroll range
275
+ # For more details, see https://github.com/ruby/reline/pull/577#issuecomment-1646679623
276
+ def scroll_down(x)
277
+ return if x.zero?
278
+ # We use `\n` instead of CSI + S because CSI + S would cause https://github.com/ruby/reline/issues/576
279
+ write "\n" * x
280
+ end
281
+
282
+ def clear_screen
283
+ write "\e[2J"
284
+ write "\e[1;1H"
285
+ end
286
+
287
+ def set_winch_handler(&handler)
288
+ @old_winch_handler = Signal.trap('WINCH') do |arg|
289
+ handler.call
290
+ @old_winch_handler.call(arg) if @old_winch_handler.respond_to?(:call)
291
+ end
292
+ @old_cont_handler = Signal.trap('CONT') do |arg|
293
+ @input.raw!(intr: true) if @input.tty?
294
+ # Rerender the screen. Note that screen size might be changed while suspended.
295
+ handler.call
296
+ @old_cont_handler.call(arg) if @old_cont_handler.respond_to?(:call)
297
+ end
298
+ rescue ArgumentError
299
+ # Signal.trap may raise an ArgumentError if the platform doesn't support the signal.
300
+ end
301
+
302
+ def read_single_char(timeout_second)
303
+ # Disable intr to read `C-c` `C-z` `C-\` for quoted insert
304
+ @input.raw(intr: false) do
305
+ super
306
+ end
307
+ end
308
+
309
+ def prep
310
+ # Enable bracketed paste
311
+ write "\e[?2004h" if Reline.core.config.enable_bracketed_paste && both_tty?
312
+ retrieve_keybuffer
313
+ nil
314
+ end
315
+
316
+ def deprep(otio)
317
+ # Disable bracketed paste
318
+ write "\e[?2004l" if Reline.core.config.enable_bracketed_paste && both_tty?
319
+ Signal.trap('WINCH', @old_winch_handler) if @old_winch_handler
320
+ Signal.trap('CONT', @old_cont_handler) if @old_cont_handler
321
+ end
322
+ end
@@ -0,0 +1,120 @@
1
+ require 'io/wait'
2
+
3
+ class Reline::Dumb < Reline::IO
4
+ RESET_COLOR = '' # Do not send color reset sequence
5
+
6
+ attr_writer :output
7
+
8
+ def initialize(encoding: nil)
9
+ @input = STDIN
10
+ @output = STDOUT
11
+ @buf = []
12
+ @pasting = false
13
+ @encoding = encoding
14
+ @screen_size = [24, 80]
15
+ end
16
+
17
+ def dumb?
18
+ true
19
+ end
20
+
21
+ def encoding
22
+ if @encoding
23
+ @encoding
24
+ elsif RUBY_PLATFORM =~ /mswin|mingw/
25
+ Encoding::UTF_8
26
+ else
27
+ @input.external_encoding || Encoding.default_external
28
+ end
29
+ rescue IOError
30
+ # STDIN.external_encoding raises IOError in Ruby <= 3.0 when STDIN is closed
31
+ Encoding.default_external
32
+ end
33
+
34
+ def set_default_key_bindings(_)
35
+ end
36
+
37
+ def input=(val)
38
+ @input = val
39
+ end
40
+
41
+ def with_raw_input
42
+ yield
43
+ end
44
+
45
+ def write(string)
46
+ @output.write(string)
47
+ end
48
+
49
+ def buffered_output
50
+ yield
51
+ end
52
+
53
+ def getc(_timeout_second)
54
+ unless @buf.empty?
55
+ return @buf.shift
56
+ end
57
+ c = nil
58
+ loop do
59
+ Reline.core.line_editor.handle_signal
60
+ result = @input.wait_readable(0.1)
61
+ next if result.nil?
62
+ c = @input.read(1)
63
+ break
64
+ end
65
+ c&.ord
66
+ end
67
+
68
+ def ungetc(c)
69
+ @buf.unshift(c)
70
+ end
71
+
72
+ def get_screen_size
73
+ @screen_size
74
+ end
75
+
76
+ def cursor_pos
77
+ Reline::CursorPos.new(0, 0)
78
+ end
79
+
80
+ def hide_cursor
81
+ end
82
+
83
+ def show_cursor
84
+ end
85
+
86
+ def move_cursor_column(val)
87
+ end
88
+
89
+ def move_cursor_up(val)
90
+ end
91
+
92
+ def move_cursor_down(val)
93
+ end
94
+
95
+ def erase_after_cursor
96
+ end
97
+
98
+ def scroll_down(val)
99
+ end
100
+
101
+ def clear_screen
102
+ end
103
+
104
+ def set_screen_size(rows, columns)
105
+ @screen_size = [rows, columns]
106
+ end
107
+
108
+ def set_winch_handler(&handler)
109
+ end
110
+
111
+ def in_pasting?
112
+ @pasting
113
+ end
114
+
115
+ def prep
116
+ end
117
+
118
+ def deprep(otio)
119
+ end
120
+ end