yap-rawline 0.1.0
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 +7 -0
- data/CHANGELOG.rdoc +42 -0
- data/LICENSE +12 -0
- data/README.rdoc +60 -0
- data/examples/key_tester.rb +21 -0
- data/examples/rawline_irb.rb +28 -0
- data/examples/rawline_rush.rb +27 -0
- data/examples/rawline_shell.rb +189 -0
- data/examples/readline_emulation.rb +17 -0
- data/lib/rawline.rb +60 -0
- data/lib/rawline/completer.rb +71 -0
- data/lib/rawline/editor.rb +938 -0
- data/lib/rawline/event_loop.rb +78 -0
- data/lib/rawline/event_registry.rb +17 -0
- data/lib/rawline/history_buffer.rb +177 -0
- data/lib/rawline/keycode_parser.rb +49 -0
- data/lib/rawline/line.rb +169 -0
- data/lib/rawline/prompt.rb +12 -0
- data/lib/rawline/terminal.rb +161 -0
- data/lib/rawline/terminal/vt220_terminal.rb +66 -0
- data/lib/rawline/terminal/windows_terminal.rb +66 -0
- data/spec/editor_spec.rb +260 -0
- data/spec/history_buffer_spec.rb +106 -0
- data/spec/keycode_parser_spec.rb +96 -0
- data/spec/line_spec.rb +58 -0
- data/spec/prompt_spec.rb +8 -0
- data/spec/spec_helper.rb +96 -0
- metadata +119 -0
@@ -0,0 +1,71 @@
|
|
1
|
+
module RawLine
|
2
|
+
|
3
|
+
class Completer
|
4
|
+
def initialize(char:, line:, completion:, completion_found:, completion_not_found:, done:, keys:)
|
5
|
+
@completion_char = char
|
6
|
+
@line = line
|
7
|
+
@completion_proc = completion
|
8
|
+
@completion_found_proc = completion_found
|
9
|
+
@completion_not_found_proc = completion_not_found
|
10
|
+
@done_proc = done
|
11
|
+
@keys = keys
|
12
|
+
|
13
|
+
@completion_matches = HistoryBuffer.new(0) do |h|
|
14
|
+
h.duplicates = false
|
15
|
+
h.cycle = true
|
16
|
+
end
|
17
|
+
@completion_matches.empty
|
18
|
+
|
19
|
+
@first_time = true
|
20
|
+
@word_start = @line.word[:start]
|
21
|
+
end
|
22
|
+
|
23
|
+
def read_bytes(bytes)
|
24
|
+
return unless bytes.any?
|
25
|
+
|
26
|
+
if bytes.map(&:ord) == @keys[:left_arrow]
|
27
|
+
@completion_matches.forward
|
28
|
+
match = @completion_matches.get
|
29
|
+
@completion_found_proc.call(completion: match, possible_completions: @completion_matches.reverse)
|
30
|
+
elsif bytes.map(&:ord) == @keys[:right_arrow]
|
31
|
+
@completion_matches.back
|
32
|
+
match = @completion_matches.get
|
33
|
+
@completion_found_proc.call(completion: match, possible_completions: @completion_matches.reverse)
|
34
|
+
elsif bytes.map(&:ord) != @completion_char
|
35
|
+
@done_proc.call(bytes)
|
36
|
+
elsif @first_time
|
37
|
+
matches = @completion_proc.call(sub_word, @line.text) unless !@completion_proc || @completion_proc == []
|
38
|
+
matches = matches.to_a.compact.sort.reverse
|
39
|
+
|
40
|
+
if matches.any?
|
41
|
+
@completion_matches.resize(matches.length)
|
42
|
+
matches.each { |w| @completion_matches << w }
|
43
|
+
|
44
|
+
# Get first match
|
45
|
+
@completion_matches.back
|
46
|
+
match = @completion_matches.get
|
47
|
+
|
48
|
+
# completion matches is a history implementation and its in reverse order from what
|
49
|
+
# a user would expect
|
50
|
+
@completion_found_proc.call(completion: match, possible_completions: @completion_matches.reverse)
|
51
|
+
else
|
52
|
+
@completion_not_found_proc.call
|
53
|
+
@done_proc.call
|
54
|
+
end
|
55
|
+
@first_time = false
|
56
|
+
else
|
57
|
+
@completion_matches.back
|
58
|
+
match = @completion_matches.get
|
59
|
+
|
60
|
+
@completion_found_proc.call(completion: match, possible_completions: @completion_matches.reverse)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def sub_word
|
67
|
+
@line.text[@line.word[:start]..@line.position-1] || ""
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
@@ -0,0 +1,938 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
#
|
4
|
+
# editor.rb
|
5
|
+
#
|
6
|
+
# Created by Fabio Cevasco on 2008-03-01.
|
7
|
+
# Copyright (c) 2008 Fabio Cevasco. All rights reserved.
|
8
|
+
#
|
9
|
+
# This is Free Software. See LICENSE for details.
|
10
|
+
#
|
11
|
+
|
12
|
+
require 'forwardable'
|
13
|
+
require 'terminal_layout'
|
14
|
+
require 'ansi_string'
|
15
|
+
require 'term/ansicolor'
|
16
|
+
require 'fcntl'
|
17
|
+
|
18
|
+
module RawLine
|
19
|
+
|
20
|
+
#
|
21
|
+
# The Editor class defines methods to:
|
22
|
+
#
|
23
|
+
# * Read characters from STDIN or any type of input
|
24
|
+
# * Write characters to STDOUT or any type of output
|
25
|
+
# * Bind keys to specific actions
|
26
|
+
# * Perform line-related operations like moving, navigating through history, etc.
|
27
|
+
#
|
28
|
+
# Note that the following default key bindings are provided:
|
29
|
+
#
|
30
|
+
# * TAB: word completion defined via completion_proc
|
31
|
+
# * LEFT/RIGHT ARROWS: cursor movement (left/right)
|
32
|
+
# * UP/DOWN ARROWS: history navigation
|
33
|
+
# * DEL: Delete character under cursor
|
34
|
+
# * BACKSPACE: Delete character before cursor
|
35
|
+
# * INSERT: Toggle insert/replace mode (default: insert)
|
36
|
+
# * CTRL+K: Clear the whole line
|
37
|
+
# * CTRL+Z: undo (unless already registered by the OS)
|
38
|
+
# * CTRL+Y: redo (unless already registered by the OS)
|
39
|
+
#
|
40
|
+
class Editor
|
41
|
+
extend Forwardable
|
42
|
+
include HighLine::SystemExtensions
|
43
|
+
|
44
|
+
attr_accessor :char, :history_size, :line_history_size, :highlight_history_matching_text
|
45
|
+
attr_accessor :terminal, :keys, :mode
|
46
|
+
attr_accessor :completion_class, :completion_proc, :line, :history, :completion_append_string
|
47
|
+
attr_accessor :match_hidden_files
|
48
|
+
attr_accessor :word_break_characters
|
49
|
+
attr_reader :output
|
50
|
+
attr_accessor :dom
|
51
|
+
|
52
|
+
# TODO: dom traversal for lookup rather than assignment
|
53
|
+
attr_accessor :prompt_box, :input_box, :content_box
|
54
|
+
|
55
|
+
#
|
56
|
+
# Create an instance of RawLine::Editor which can be used
|
57
|
+
# to read from input and perform line-editing operations.
|
58
|
+
# This method takes an optional block used to override the
|
59
|
+
# following instance attributes:
|
60
|
+
# * <tt>@history_size</tt> - the size of the editor history buffer (30).
|
61
|
+
# * <tt>@line_history_size</tt> - the size of the editor line history buffer (50).
|
62
|
+
# * <tt>@keys</tt> - the keys (arrays of character codes) bound to specific actions.
|
63
|
+
# * <tt>@word_break_characters</tt> - a regex used for word separation, default inclues: " \t\n\"\\'`@$><=;|&{("
|
64
|
+
# * <tt>@mode</tt> - The editor's character insertion mode (:insert).
|
65
|
+
# * <tt>@completion_proc</tt> - a Proc object used to perform word completion.
|
66
|
+
# * <tt>@completion_append_string</tt> - a string to append to completed words ('').
|
67
|
+
# * <tt>@terminal</tt> - a RawLine::Terminal containing character key codes.
|
68
|
+
#
|
69
|
+
def initialize(input=STDIN, output=STDOUT)
|
70
|
+
@input = input
|
71
|
+
# @output = output
|
72
|
+
|
73
|
+
case RUBY_PLATFORM
|
74
|
+
when /mswin/i then
|
75
|
+
@terminal = WindowsTerminal.new
|
76
|
+
if RawLine.win32console? then
|
77
|
+
@win32_io = Win32::Console::ANSI::IO.new
|
78
|
+
end
|
79
|
+
else
|
80
|
+
@terminal = VT220Terminal.new
|
81
|
+
end
|
82
|
+
@history_size = 30
|
83
|
+
@line_history_size = 50
|
84
|
+
@keys = {}
|
85
|
+
@word_break_characters = " \t\n\"'@\$><=;|&{("
|
86
|
+
@mode = :insert
|
87
|
+
@completion_class = Completer
|
88
|
+
@completion_proc = filename_completion_proc
|
89
|
+
@completion_append_string = ''
|
90
|
+
@match_hidden_files = false
|
91
|
+
set_default_keys
|
92
|
+
@add_history = false
|
93
|
+
@highlight_history_matching_text = true
|
94
|
+
@history = HistoryBuffer.new(@history_size) do |h|
|
95
|
+
h.duplicates = false;
|
96
|
+
h.exclude = lambda { |item| item.strip == "" }
|
97
|
+
end
|
98
|
+
@keyboard_input_processors = [self]
|
99
|
+
yield self if block_given?
|
100
|
+
update_word_separator
|
101
|
+
@char = nil
|
102
|
+
|
103
|
+
@event_registry = Rawline::EventRegistry.new do |registry|
|
104
|
+
registry.subscribe :default, -> (_) { self.check_for_keyboard_input }
|
105
|
+
registry.subscribe :dom_tree_change, -> (_) { self.render }
|
106
|
+
end
|
107
|
+
@event_loop = Rawline::EventLoop.new(registry: @event_registry)
|
108
|
+
|
109
|
+
@dom ||= build_dom_tree
|
110
|
+
@renderer ||= build_renderer
|
111
|
+
|
112
|
+
initialize_line
|
113
|
+
end
|
114
|
+
|
115
|
+
attr_reader :dom
|
116
|
+
|
117
|
+
def events
|
118
|
+
@event_loop
|
119
|
+
end
|
120
|
+
|
121
|
+
#
|
122
|
+
# Return the current RawLine version
|
123
|
+
#
|
124
|
+
def library_version
|
125
|
+
"RawLine v#{RawLine.rawline_version}"
|
126
|
+
end
|
127
|
+
|
128
|
+
def prompt
|
129
|
+
@line.prompt if @line
|
130
|
+
end
|
131
|
+
|
132
|
+
def prompt=(text)
|
133
|
+
return if !@allow_prompt_updates || @line.nil? || @line.prompt == text
|
134
|
+
@prompt_box.content = Prompt.new(text)
|
135
|
+
end
|
136
|
+
|
137
|
+
def initialize_line
|
138
|
+
@input_box.content = ""
|
139
|
+
update_word_separator
|
140
|
+
@add_history = true #add_history
|
141
|
+
@line = Line.new(@line_history_size) do |l|
|
142
|
+
l.prompt = @prompt_box.content
|
143
|
+
l.word_separator = @word_separator
|
144
|
+
end
|
145
|
+
add_to_line_history
|
146
|
+
@allow_prompt_updates = true
|
147
|
+
end
|
148
|
+
|
149
|
+
def reset_line
|
150
|
+
initialize_line
|
151
|
+
render(reset: true)
|
152
|
+
end
|
153
|
+
|
154
|
+
def check_for_keyboard_input
|
155
|
+
bytes = []
|
156
|
+
begin
|
157
|
+
file_descriptor_flags = @input.fcntl(Fcntl::F_GETFL, 0)
|
158
|
+
loop do
|
159
|
+
string = @input.read_nonblock(4096)
|
160
|
+
bytes.concat string.bytes
|
161
|
+
end
|
162
|
+
rescue IO::WaitReadable
|
163
|
+
# reset flags so O_NONBLOCK is turned off on the file descriptor
|
164
|
+
# if it was turned on during the read_nonblock above
|
165
|
+
retry if IO.select([@input], [], [], 0.01)
|
166
|
+
|
167
|
+
@input.fcntl(Fcntl::F_SETFL, file_descriptor_flags)
|
168
|
+
@keyboard_input_processors.last.read_bytes(bytes)
|
169
|
+
|
170
|
+
@event_loop.add_event name: 'check_for_keyboard_input', source: self
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def read_bytes(bytes)
|
175
|
+
return unless bytes.any?
|
176
|
+
old_position = @line.position
|
177
|
+
key_codes = parse_key_codes(bytes)
|
178
|
+
key_codes.each do |key_code|
|
179
|
+
@char = key_code
|
180
|
+
process_character
|
181
|
+
|
182
|
+
new_position = @line.position
|
183
|
+
|
184
|
+
if !@ignore_position_change && new_position != old_position
|
185
|
+
@matching_text = @line.text[0...@line.position]
|
186
|
+
end
|
187
|
+
|
188
|
+
@ignore_position_change = false
|
189
|
+
if @char == @terminal.keys[:enter] || !@char
|
190
|
+
@allow_prompt_updates = false
|
191
|
+
move_to_beginning_of_input
|
192
|
+
@event_loop.add_event name: "line_read", source: self, payload: { line: @line.text.without_ansi.dup }
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
def on_read_line(&blk)
|
198
|
+
@event_registry.subscribe :line_read, &blk
|
199
|
+
end
|
200
|
+
|
201
|
+
def start
|
202
|
+
@input.raw!
|
203
|
+
at_exit { @input.cooked! }
|
204
|
+
|
205
|
+
Signal.trap("SIGWINCH") do
|
206
|
+
@event_loop.add_event name: "terminal-resized", source: self
|
207
|
+
end
|
208
|
+
|
209
|
+
@event_registry.subscribe("terminal-resized") do
|
210
|
+
@render_tree.width = terminal_width
|
211
|
+
@render_tree.height = terminal_height
|
212
|
+
@event_loop.add_event name: "render", source: self
|
213
|
+
end
|
214
|
+
|
215
|
+
@event_loop.add_event name: "render", source: self
|
216
|
+
@event_loop.start
|
217
|
+
end
|
218
|
+
|
219
|
+
def subscribe(*args, &blk)
|
220
|
+
@event_registry.subscribe(*args, &blk)
|
221
|
+
end
|
222
|
+
|
223
|
+
#
|
224
|
+
# Parse a key or key sequence into the corresponding codes.
|
225
|
+
#
|
226
|
+
def parse_key_codes(bytes)
|
227
|
+
KeycodeParser.new(@terminal.keys).parse_bytes(bytes)
|
228
|
+
end
|
229
|
+
|
230
|
+
#
|
231
|
+
# Write a string to <tt># @output</tt> starting from the cursor position.
|
232
|
+
# Characters at the right of the cursor are shifted to the right if
|
233
|
+
# <tt>@mode == :insert</tt>, deleted otherwise.
|
234
|
+
#
|
235
|
+
def write(string)
|
236
|
+
string.each_byte { |c| print_character c, true }
|
237
|
+
add_to_line_history
|
238
|
+
end
|
239
|
+
|
240
|
+
#
|
241
|
+
# Write a new line to <tt># @output</tt>, overwriting any existing text
|
242
|
+
# and printing an end of line character.
|
243
|
+
#
|
244
|
+
def write_line(string)
|
245
|
+
clear_line
|
246
|
+
# @output.print string
|
247
|
+
@line.text = string
|
248
|
+
@input_box.position = @line.position
|
249
|
+
add_to_line_history
|
250
|
+
add_to_history
|
251
|
+
@char = nil
|
252
|
+
end
|
253
|
+
|
254
|
+
#
|
255
|
+
# Process a character. If the key corresponding to the inputted character
|
256
|
+
# is bound to an action, call <tt>press_key</tt>, otherwise call <tt>default_action</tt>.
|
257
|
+
# This method is called automatically by <tt>read</tt>
|
258
|
+
#
|
259
|
+
def process_character
|
260
|
+
case @char.class.to_s
|
261
|
+
when 'Fixnum' then
|
262
|
+
default_action
|
263
|
+
when 'Array'
|
264
|
+
press_key if key_bound?
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
#
|
269
|
+
# Bind a key to an action specified via <tt>block</tt>.
|
270
|
+
# <tt>key</tt> can be:
|
271
|
+
#
|
272
|
+
# * A Symbol identifying a character or character sequence defined for the current terminal
|
273
|
+
# * A Fixnum identifying a character defined for the current terminal
|
274
|
+
# * An Array identifying a character or character sequence defined for the current terminal
|
275
|
+
# * A String identifying a character or character sequence, even if it is not defined for the current terminal
|
276
|
+
# * An Hash identifying a character or character sequence, even if it is not defined for the current terminal
|
277
|
+
#
|
278
|
+
# If <tt>key</tt> is a hash, then:
|
279
|
+
#
|
280
|
+
# * It must contain only one key/value pair
|
281
|
+
# * The key identifies the name of the character or character sequence
|
282
|
+
# * The value identifies the code(s) corresponding to the character or character sequence
|
283
|
+
# * The value can be a Fixnum, a String or an Array.
|
284
|
+
#
|
285
|
+
def bind(key, &block)
|
286
|
+
case key.class.to_s
|
287
|
+
when 'Symbol' then
|
288
|
+
raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys[key]
|
289
|
+
@keys[@terminal.keys[key]] = block
|
290
|
+
when 'Array' then
|
291
|
+
raise BindingException, "Unknown key or key sequence '#{key.join(", ")}' (#{key.class.to_s})" unless @terminal.keys.has_value? key
|
292
|
+
@keys[key] = block
|
293
|
+
when 'Fixnum' then
|
294
|
+
raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys.has_value? [key]
|
295
|
+
@keys[[key]] = block
|
296
|
+
when 'String' then
|
297
|
+
if key.length == 1 then
|
298
|
+
@keys[[key.ord]] = block
|
299
|
+
else
|
300
|
+
bind_hash({:"#{key}" => key}, block)
|
301
|
+
end
|
302
|
+
when 'Hash' then
|
303
|
+
raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
|
304
|
+
bind_hash(key, block)
|
305
|
+
else
|
306
|
+
raise BindingException, "Unable to bind '#{key.to_s}' (#{key.class.to_s})"
|
307
|
+
end
|
308
|
+
@terminal.update
|
309
|
+
end
|
310
|
+
|
311
|
+
#
|
312
|
+
# Return true if the last character read via <tt>read</tt> is bound to an action.
|
313
|
+
#
|
314
|
+
def key_bound?
|
315
|
+
@keys[@char] ? true : false
|
316
|
+
end
|
317
|
+
|
318
|
+
#
|
319
|
+
# Call the action bound to the last character read via <tt>read</tt>.
|
320
|
+
# This method is called automatically by <tt>process_character</tt>.
|
321
|
+
#
|
322
|
+
def press_key
|
323
|
+
@keys[@char].call
|
324
|
+
end
|
325
|
+
|
326
|
+
#
|
327
|
+
# Execute the default action for the last character read via <tt>read</tt>.
|
328
|
+
# By default it prints the character to the screen via <tt>print_character</tt>.
|
329
|
+
# This method is called automatically by <tt>process_character</tt>.
|
330
|
+
#
|
331
|
+
def default_action
|
332
|
+
@input_box.content += @char.chr
|
333
|
+
print_character
|
334
|
+
end
|
335
|
+
|
336
|
+
#
|
337
|
+
# Write a character to <tt># @output</tt> at cursor position,
|
338
|
+
# shifting characters as appropriate.
|
339
|
+
# If <tt>no_line_history</tt> is set to <tt>true</tt>, the updated
|
340
|
+
# won't be saved in the history of the current line.
|
341
|
+
#
|
342
|
+
def print_character(char=@char, no_line_history = false)
|
343
|
+
if @line.position < @line.length then
|
344
|
+
chars = select_characters_from_cursor if @mode == :insert
|
345
|
+
@line.text[@line.position] = (@mode == :insert) ? "#{char.chr}#{@line.text[@line.position]}" : "#{char.chr}"
|
346
|
+
@line.right
|
347
|
+
@input_box.position = @line.position
|
348
|
+
# if @mode == :insert then
|
349
|
+
# chars.length.times { @line.left } # move cursor back
|
350
|
+
# end
|
351
|
+
else
|
352
|
+
@line.right
|
353
|
+
@line << char
|
354
|
+
end
|
355
|
+
@input_box.content = @line.text
|
356
|
+
@input_box.position = @line.position
|
357
|
+
add_to_line_history unless no_line_history
|
358
|
+
end
|
359
|
+
|
360
|
+
#
|
361
|
+
# Complete the current word according to what returned by
|
362
|
+
# <tt>@completion_proc</tt>. Characters can be appended to the
|
363
|
+
# completed word via <tt>@completion_append_character</tt> and word
|
364
|
+
# separators can be defined via <tt>@word_separator</tt>.
|
365
|
+
#
|
366
|
+
# This action is bound to the tab key by default, so the first
|
367
|
+
# match is displayed the first time the user presses tab, and all
|
368
|
+
# the possible messages will be displayed (cyclically) when tab is
|
369
|
+
# pressed again.
|
370
|
+
#
|
371
|
+
def complete
|
372
|
+
completer = @completion_class.new(
|
373
|
+
char: @char,
|
374
|
+
line: @line,
|
375
|
+
completion: @completion_proc,
|
376
|
+
completion_found: -> (completion:, possible_completions:) {
|
377
|
+
completion_found(completion: completion, possible_completions: possible_completions)
|
378
|
+
},
|
379
|
+
completion_not_found: -> {
|
380
|
+
completion_not_found
|
381
|
+
},
|
382
|
+
done: -> (*leftover_bytes){
|
383
|
+
completion_done
|
384
|
+
leftover_bytes = leftover_bytes.flatten
|
385
|
+
@keyboard_input_processors.pop
|
386
|
+
if leftover_bytes.any?
|
387
|
+
@keyboard_input_processors.last.read_bytes(leftover_bytes)
|
388
|
+
end
|
389
|
+
},
|
390
|
+
keys: terminal.keys
|
391
|
+
)
|
392
|
+
@keyboard_input_processors.push(completer)
|
393
|
+
completer.read_bytes(@char)
|
394
|
+
end
|
395
|
+
|
396
|
+
def completion_found(completion:, possible_completions:)
|
397
|
+
if @on_word_complete
|
398
|
+
word = @line.word[:text]
|
399
|
+
sub_word = @line.text[@line.word[:start]..@line.position-1] || ""
|
400
|
+
@on_word_complete.call(name: "word-completion", payload: { sub_word: sub_word, word: word, completion: completion, possible_completions: possible_completions })
|
401
|
+
end
|
402
|
+
|
403
|
+
if @line.word[:text].length > 0
|
404
|
+
# If not in a word, print the match, otherwise continue existing word
|
405
|
+
move_to_position(@line.word[:end]+@completion_append_string.to_s.length+1)
|
406
|
+
end
|
407
|
+
(@line.position-@line.word[:start]).times { delete_left_character(true) }
|
408
|
+
write completion.to_s + @completion_append_string.to_s
|
409
|
+
end
|
410
|
+
|
411
|
+
def completion_not_found
|
412
|
+
if @on_word_complete_no_match
|
413
|
+
word = @line.word[:text]
|
414
|
+
sub_word = @line.text[@line.word[:start]..@line.position-1] || ""
|
415
|
+
@on_word_complete_no_match.call(name: "word-completion-no-match", payload: { sub_word: sub_word, word: word })
|
416
|
+
end
|
417
|
+
end
|
418
|
+
|
419
|
+
def completion_done
|
420
|
+
if @on_word_complete_done
|
421
|
+
@on_word_complete_done.call
|
422
|
+
end
|
423
|
+
end
|
424
|
+
|
425
|
+
def on_word_complete(&blk)
|
426
|
+
@on_word_complete = blk
|
427
|
+
end
|
428
|
+
|
429
|
+
def on_word_complete_no_match(&blk)
|
430
|
+
@on_word_complete_no_match = blk
|
431
|
+
end
|
432
|
+
|
433
|
+
def on_word_complete_done(&blk)
|
434
|
+
@on_word_complete_done = blk
|
435
|
+
end
|
436
|
+
|
437
|
+
#
|
438
|
+
# Complete file and directory names.
|
439
|
+
# Hidden files and directories are matched only if <tt>@match_hidden_files</tt> is true.
|
440
|
+
#
|
441
|
+
def filename_completion_proc
|
442
|
+
lambda do |word, _|
|
443
|
+
dirs = @line.text.split('/')
|
444
|
+
path = @line.text.match(/^\/|[a-zA-Z]:\//) ? "/" : Dir.pwd+"/"
|
445
|
+
if dirs.length == 0 then # starting directory
|
446
|
+
dir = path
|
447
|
+
else
|
448
|
+
dirs.delete(dirs.last) unless File.directory?(path+dirs.join('/'))
|
449
|
+
dir = path+dirs.join('/')
|
450
|
+
end
|
451
|
+
Dir.entries(dir).select { |e| (e =~ /^\./ && @match_hidden_files && word == '') || (e =~ /^#{word}/ && e !~ /^\./) }
|
452
|
+
end
|
453
|
+
end
|
454
|
+
|
455
|
+
|
456
|
+
#
|
457
|
+
# Adds <tt>@line.text</tt> to the editor history. This action is
|
458
|
+
# bound to the enter key by default.
|
459
|
+
#
|
460
|
+
def newline
|
461
|
+
add_to_history
|
462
|
+
@history.clear_position
|
463
|
+
end
|
464
|
+
|
465
|
+
#
|
466
|
+
# Move the cursor left (if possible) by printing a
|
467
|
+
# backspace, updating <tt>@line.position</tt> accordingly.
|
468
|
+
# This action is bound to the left arrow key by default.
|
469
|
+
#
|
470
|
+
def move_left
|
471
|
+
unless @line.bol? then
|
472
|
+
@line.left
|
473
|
+
@input_box.position = @line.position
|
474
|
+
return true
|
475
|
+
end
|
476
|
+
false
|
477
|
+
end
|
478
|
+
|
479
|
+
#
|
480
|
+
# Move the cursor right (if possible) by re-printing the
|
481
|
+
# character at the right of the cursor, if any, and updating
|
482
|
+
# <tt>@line.position</tt> accordingly.
|
483
|
+
# This action is bound to the right arrow key by default.
|
484
|
+
#
|
485
|
+
def move_right
|
486
|
+
unless @line.position > @line.eol then
|
487
|
+
@line.right
|
488
|
+
@input_box.position = @line.position
|
489
|
+
return true
|
490
|
+
end
|
491
|
+
false
|
492
|
+
end
|
493
|
+
|
494
|
+
#
|
495
|
+
# Print debug information about the current line. Note that after
|
496
|
+
# the message is displayed, the line text and position will be restored.
|
497
|
+
#
|
498
|
+
def debug_line
|
499
|
+
pos = @line.position
|
500
|
+
text = @line.text
|
501
|
+
word = @line.word
|
502
|
+
# @output.puts
|
503
|
+
# @output.puts "Text: [#{text}]"
|
504
|
+
# @output.puts "Length: #{@line.length}"
|
505
|
+
# @output.puts "Position: #{pos}"
|
506
|
+
# @output.puts "Character at Position: [#{text[pos].chr}] (#{text[pos]})" unless pos >= @line.length
|
507
|
+
# @output.puts "Current Word: [#{word[:text]}] (#{word[:start]} -- #{word[:end]})"
|
508
|
+
clear_line
|
509
|
+
raw_print text
|
510
|
+
overwrite_line(text, pos)
|
511
|
+
end
|
512
|
+
|
513
|
+
#
|
514
|
+
# Print the content of the editor history. Note that after
|
515
|
+
# the message is displayed, the line text and position will be restored.
|
516
|
+
#
|
517
|
+
def show_history
|
518
|
+
pos = @line.position
|
519
|
+
text = @line.text
|
520
|
+
# @output.puts
|
521
|
+
# @output.puts "History:"
|
522
|
+
@history.each {|l| puts "- [#{l}]"}
|
523
|
+
overwrite_line(text, pos)
|
524
|
+
end
|
525
|
+
|
526
|
+
#
|
527
|
+
# Clear the editor history.
|
528
|
+
#
|
529
|
+
def clear_history
|
530
|
+
@history.empty
|
531
|
+
end
|
532
|
+
|
533
|
+
#
|
534
|
+
# Delete the character at the left of the cursor.
|
535
|
+
# If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
|
536
|
+
# recorded in the line history.
|
537
|
+
# This action is bound to the backspace key by default.
|
538
|
+
#
|
539
|
+
def delete_left_character(no_line_history=false)
|
540
|
+
if move_left then
|
541
|
+
delete_character(no_line_history)
|
542
|
+
end
|
543
|
+
end
|
544
|
+
|
545
|
+
#
|
546
|
+
# Delete the character under the cursor.
|
547
|
+
# If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
|
548
|
+
# recorded in the line history.
|
549
|
+
# This action is bound to the delete key by default.
|
550
|
+
#
|
551
|
+
def delete_character(no_line_history=false)
|
552
|
+
unless @line.position > @line.eol
|
553
|
+
# save characters to shift
|
554
|
+
chars = (@line.eol?) ? ' ' : select_characters_from_cursor(1)
|
555
|
+
# remove character from console and shift characters
|
556
|
+
# (chars.length+1).times { # @output.putc ?\b.ord }
|
557
|
+
#remove character from line
|
558
|
+
@line[@line.position] = ''
|
559
|
+
@input_box.content = @line.text
|
560
|
+
@input_box.position = @line.position
|
561
|
+
add_to_line_history unless no_line_history
|
562
|
+
end
|
563
|
+
end
|
564
|
+
|
565
|
+
#
|
566
|
+
# Clear the current line, i.e.
|
567
|
+
# <tt>@line.text</tt> and <tt>@line.position</tt>.
|
568
|
+
# This action is bound to ctrl+k by default.
|
569
|
+
#
|
570
|
+
def clear_line
|
571
|
+
# @output.putc ?\r
|
572
|
+
# @output.print @line.prompt
|
573
|
+
# @line.length.times { @output.putc ?\s.ord }
|
574
|
+
# @line.length.times { @output.putc ?\b.ord }
|
575
|
+
add_to_line_history
|
576
|
+
@line.text = ""
|
577
|
+
@line.position = 0
|
578
|
+
@input_box.position = @line.position
|
579
|
+
@history.clear_position
|
580
|
+
end
|
581
|
+
|
582
|
+
def clear_screen
|
583
|
+
# @output.print @terminal.term_info.control_string("clear")
|
584
|
+
# @terminal.clear_screen
|
585
|
+
# @output.print @line.prompt
|
586
|
+
# @output.print @line.text
|
587
|
+
# (@line.length - @line.position).times { @output.putc ?\b.ord }
|
588
|
+
end
|
589
|
+
|
590
|
+
def clear_screen_down
|
591
|
+
# @output.print @terminal.term_info.control_string("ed")
|
592
|
+
# @terminal.clear_screen_down
|
593
|
+
end
|
594
|
+
|
595
|
+
#
|
596
|
+
# Undo the last modification to the current line (<tt>@line.text</tt>).
|
597
|
+
# This action is bound to ctrl+z by default.
|
598
|
+
#
|
599
|
+
def undo
|
600
|
+
generic_history_back(@line.history) if @line.history.position == nil
|
601
|
+
generic_history_back(@line.history)
|
602
|
+
end
|
603
|
+
|
604
|
+
#
|
605
|
+
# Redo a previously-undone modification to the
|
606
|
+
# current line (<tt>@line.text</tt>).
|
607
|
+
# This action is bound to ctrl+y by default.
|
608
|
+
#
|
609
|
+
def redo
|
610
|
+
generic_history_forward(@line.history)
|
611
|
+
end
|
612
|
+
|
613
|
+
#
|
614
|
+
# Load the previous entry of the editor in place of the
|
615
|
+
# current line (<tt>@line.text</tt>).
|
616
|
+
# This action is bound to the up arrow key by default.
|
617
|
+
#
|
618
|
+
def history_back
|
619
|
+
generic_history_back(@history)
|
620
|
+
add_to_line_history
|
621
|
+
end
|
622
|
+
|
623
|
+
#
|
624
|
+
# Load the next entry of the editor history in place of the
|
625
|
+
# current line (<tt>@line.text</tt>).
|
626
|
+
# This action is bound to down arrow key by default.
|
627
|
+
#
|
628
|
+
def history_forward
|
629
|
+
generic_history_forward(@history)
|
630
|
+
add_to_line_history
|
631
|
+
end
|
632
|
+
|
633
|
+
#
|
634
|
+
# Add the current line (<tt>@line.text</tt>) to the
|
635
|
+
# line history, to allow undo/redo
|
636
|
+
# operations.
|
637
|
+
#
|
638
|
+
def add_to_line_history
|
639
|
+
@line.history << @line.text.dup unless @line.text == ""
|
640
|
+
end
|
641
|
+
|
642
|
+
#
|
643
|
+
# Add the current line (<tt>@line.text</tt>) to the editor history.
|
644
|
+
#
|
645
|
+
def add_to_history
|
646
|
+
@history << @line.text.dup if @add_history && @line.text != ""
|
647
|
+
end
|
648
|
+
|
649
|
+
#
|
650
|
+
# Toggle the editor <tt>@mode</tt> to :replace or :insert (default).
|
651
|
+
#
|
652
|
+
def toggle_mode
|
653
|
+
case @mode
|
654
|
+
when :insert then @mode = :replace
|
655
|
+
when :replace then @mode = :insert
|
656
|
+
end
|
657
|
+
end
|
658
|
+
|
659
|
+
def terminal_row_for_line_position(line_position)
|
660
|
+
((@line.prompt.length + line_position) / terminal_width.to_f).ceil
|
661
|
+
end
|
662
|
+
|
663
|
+
def current_terminal_row
|
664
|
+
((@line.position + @line.prompt.length + 1) / terminal_width.to_f).ceil
|
665
|
+
end
|
666
|
+
|
667
|
+
def number_of_terminal_rows
|
668
|
+
((@line.length + @line.prompt.length) / terminal_width.to_f).ceil
|
669
|
+
end
|
670
|
+
|
671
|
+
def kill_forward
|
672
|
+
@line.text[@line.position..-1].tap do
|
673
|
+
@line[line.position..-1] = ""
|
674
|
+
@input_box.content = line.text
|
675
|
+
@input_box.position = @line.position
|
676
|
+
end
|
677
|
+
end
|
678
|
+
|
679
|
+
def yank_forward(text)
|
680
|
+
@line.text[line.position] = text
|
681
|
+
@line.position = line.position + text.length
|
682
|
+
@input_box.content = line.text
|
683
|
+
@input_box.position = @line.position
|
684
|
+
end
|
685
|
+
|
686
|
+
#
|
687
|
+
# Overwrite the current line (<tt>@line.text</tt>)
|
688
|
+
# with <tt>new_line</tt>, and optionally reset the cursor position to
|
689
|
+
# <tt>position</tt>.
|
690
|
+
#
|
691
|
+
def overwrite_line(new_line, position=nil, options={})
|
692
|
+
text = @line.text
|
693
|
+
@highlighting = false
|
694
|
+
|
695
|
+
if options[:highlight_up_to]
|
696
|
+
@highlighting = true
|
697
|
+
new_line = highlight_text_up_to(new_line, options[:highlight_up_to])
|
698
|
+
end
|
699
|
+
|
700
|
+
@ignore_position_change = true
|
701
|
+
@line.position = new_line.length
|
702
|
+
@line.text = new_line
|
703
|
+
@input_box.content = @line.text
|
704
|
+
@input_box.position = @line.position
|
705
|
+
@event_loop.add_event name: "render", source: @input_box
|
706
|
+
end
|
707
|
+
|
708
|
+
def highlight_text_up_to(text, position)
|
709
|
+
ANSIString.new("\e[1m#{text[0...position]}\e[0m#{text[position..-1]}")
|
710
|
+
end
|
711
|
+
|
712
|
+
def move_to_beginning_of_input
|
713
|
+
@line.position = @line.bol
|
714
|
+
@input_box.position = @line.position
|
715
|
+
end
|
716
|
+
|
717
|
+
def move_to_end_of_input
|
718
|
+
@line.position = @line.length
|
719
|
+
@input_box.position = @line.position
|
720
|
+
end
|
721
|
+
|
722
|
+
#
|
723
|
+
# Move the cursor to <tt>pos</tt>.
|
724
|
+
#
|
725
|
+
def move_to_position(pos)
|
726
|
+
rows_to_move = current_terminal_row - terminal_row_for_line_position(pos)
|
727
|
+
if rows_to_move > 0
|
728
|
+
# rows_to_move.times { @output.print @terminal.term_info.control_string("cuu1") }
|
729
|
+
# @terminal.move_up_n_rows(rows_to_move)
|
730
|
+
else
|
731
|
+
# rows_to_move.abs.times { @output.print @terminal.term_info.control_string("cud1") }
|
732
|
+
# @terminal.move_down_n_rows(rows_to_move.abs)
|
733
|
+
end
|
734
|
+
column = (@line.prompt.length + pos) % terminal_width
|
735
|
+
# @output.print @terminal.term_info.control_string("hpa", column)
|
736
|
+
# @terminal.move_to_column((@line.prompt.length + pos) % terminal_width)
|
737
|
+
@line.position = pos
|
738
|
+
@input_box.position = @line.position
|
739
|
+
end
|
740
|
+
|
741
|
+
def move_to_end_of_line
|
742
|
+
rows_to_move_down = number_of_terminal_rows - current_terminal_row
|
743
|
+
# rows_to_move_down.times { @output.print @terminal.term_info.control_string("cud1") }
|
744
|
+
# @terminal.move_down_n_rows rows_to_move_down
|
745
|
+
@line.position = @line.length
|
746
|
+
@input_box.position = @line.position
|
747
|
+
|
748
|
+
column = (@line.prompt.length + @line.position) % terminal_width
|
749
|
+
# @output.print @terminal.term_info.control_string("hpa", column)
|
750
|
+
# @terminal.move_to_column((@line.prompt.length + @line.position) % terminal_width)
|
751
|
+
end
|
752
|
+
|
753
|
+
def move_up_n_lines(n)
|
754
|
+
# n.times { @output.print @terminal.term_info.control_string("cuu1") }
|
755
|
+
# @terminal.move_up_n_rows(n)
|
756
|
+
end
|
757
|
+
|
758
|
+
def move_down_n_lines(n)
|
759
|
+
# n.times { @output.print @terminal.term_info.control_string("cud1") }
|
760
|
+
# @terminal.move_down_n_rows(n)
|
761
|
+
end
|
762
|
+
|
763
|
+
private
|
764
|
+
|
765
|
+
def build_dom_tree
|
766
|
+
@prompt_box = TerminalLayout::Box.new(content: "default-prompt>", style: {display: :inline})
|
767
|
+
@input_box = TerminalLayout::InputBox.new(content: "", style: {display: :inline})
|
768
|
+
@content_box = TerminalLayout::Box.new(content: "", style: {display: :block})
|
769
|
+
TerminalLayout::Box.new(children:[@prompt_box, @input_box, @content_box])
|
770
|
+
end
|
771
|
+
|
772
|
+
def build_renderer
|
773
|
+
@renderer = TerminalLayout::TerminalRenderer.new(output: $stdout)
|
774
|
+
@render_tree = TerminalLayout::RenderTree.new(
|
775
|
+
@dom,
|
776
|
+
parent: nil,
|
777
|
+
style: { width:terminal_width, height:terminal_height },
|
778
|
+
renderer: @renderer
|
779
|
+
)
|
780
|
+
|
781
|
+
@dom.on(:child_changed) do |*args|
|
782
|
+
@event_loop.add_event name: "render", source: @dom#, target: event[:target]
|
783
|
+
end
|
784
|
+
|
785
|
+
@dom.on :cursor_position_changed do |*args|
|
786
|
+
@renderer.render_cursor(@input_box)
|
787
|
+
end
|
788
|
+
|
789
|
+
@event_registry.subscribe :render, -> (_) { render(reset: false) }
|
790
|
+
|
791
|
+
@renderer
|
792
|
+
end
|
793
|
+
|
794
|
+
def render(reset: false)
|
795
|
+
@render_tree.layout
|
796
|
+
@renderer.reset if reset
|
797
|
+
@renderer.render(@render_tree)
|
798
|
+
@event_loop.add_event name: "check_for_keyboard_input"
|
799
|
+
end
|
800
|
+
|
801
|
+
def update_word_separator
|
802
|
+
return @word_separator = "" if @word_break_characters.to_s == ""
|
803
|
+
chars = []
|
804
|
+
@word_break_characters.each_byte do |c|
|
805
|
+
ch = (c.is_a? Fixnum) ? c : c.ord
|
806
|
+
value = (ch == ?\s.ord) ? ' ' : Regexp.escape(ch.chr).to_s
|
807
|
+
chars << value
|
808
|
+
end
|
809
|
+
@word_separator = /(?<!\\)[#{chars.join}]/
|
810
|
+
end
|
811
|
+
|
812
|
+
def bind_hash(key, block)
|
813
|
+
key.each_pair do |j,k|
|
814
|
+
raise BindingException, "'#{k[0].chr}' is not a legal escape code for '#{@terminal.class.to_s}'." unless k.length > 1 && @terminal.escape_codes.include?(k[0].ord)
|
815
|
+
code = []
|
816
|
+
case k.class.to_s
|
817
|
+
when 'Fixnum' then
|
818
|
+
code = [k]
|
819
|
+
when 'String' then
|
820
|
+
k.each_byte { |b| code << b }
|
821
|
+
when 'Array' then
|
822
|
+
code = k
|
823
|
+
else
|
824
|
+
raise BindingException, "Unable to bind '#{k.to_s}' (#{k.class.to_s})"
|
825
|
+
end
|
826
|
+
@terminal.keys[j] = code
|
827
|
+
@keys[code] = block
|
828
|
+
end
|
829
|
+
end
|
830
|
+
|
831
|
+
def select_characters_from_cursor(offset=0)
|
832
|
+
select_characters(:right, @line.length-@line.position, offset)
|
833
|
+
end
|
834
|
+
|
835
|
+
def raw_print(string)
|
836
|
+
# string.each_byte { |c| @output.putc c }
|
837
|
+
end
|
838
|
+
|
839
|
+
def generic_history_back(history)
|
840
|
+
unless history.empty?
|
841
|
+
history.back(matching_text: matching_text)
|
842
|
+
line = history.get
|
843
|
+
return unless line
|
844
|
+
|
845
|
+
cursor_position = nil
|
846
|
+
if supports_partial_text_matching? && highlight_history_matching_text
|
847
|
+
if line && matching_text
|
848
|
+
cursor_position = [line.length, matching_text.length].min
|
849
|
+
elsif matching_text
|
850
|
+
cursor_position = matching_text.length
|
851
|
+
end
|
852
|
+
end
|
853
|
+
|
854
|
+
overwrite_line(line, cursor_position, highlight_up_to: cursor_position)
|
855
|
+
end
|
856
|
+
end
|
857
|
+
|
858
|
+
def supports_partial_text_matching?
|
859
|
+
history.supports_partial_text_matching?
|
860
|
+
end
|
861
|
+
|
862
|
+
def generic_history_forward(history)
|
863
|
+
if history.forward(matching_text: matching_text)
|
864
|
+
line = history.get
|
865
|
+
return unless line
|
866
|
+
|
867
|
+
cursor_position = if supports_partial_text_matching? && highlight_history_matching_text && matching_text
|
868
|
+
[line.length, matching_text.length].min
|
869
|
+
end
|
870
|
+
|
871
|
+
overwrite_line(line, cursor_position, highlight_up_to: cursor_position)
|
872
|
+
end
|
873
|
+
end
|
874
|
+
|
875
|
+
def select_characters(direction, n, offset=0)
|
876
|
+
if direction == :right then
|
877
|
+
@line.text[@line.position+offset..@line.position+offset+n]
|
878
|
+
elsif direction == :left then
|
879
|
+
@line.text[@line.position-offset-n..@line.position-offset]
|
880
|
+
end
|
881
|
+
end
|
882
|
+
|
883
|
+
def set_default_keys
|
884
|
+
bind(:enter) { newline }
|
885
|
+
bind(:tab) { complete }
|
886
|
+
bind(:backspace) { delete_left_character }
|
887
|
+
bind(:ctrl_c) { raise Interrupt }
|
888
|
+
bind(:ctrl_k) { clear_line }
|
889
|
+
bind(:ctrl_u) { undo }
|
890
|
+
bind(:ctrl_r) { self.redo }
|
891
|
+
bind(:left_arrow) { move_left }
|
892
|
+
bind(:right_arrow) { move_right }
|
893
|
+
bind(:up_arrow) { history_back }
|
894
|
+
bind(:down_arrow) { history_forward }
|
895
|
+
bind(:delete) { delete_character }
|
896
|
+
bind(:insert) { toggle_mode }
|
897
|
+
end
|
898
|
+
|
899
|
+
def matching_text
|
900
|
+
return nil unless @line
|
901
|
+
return nil if @line.text == ""
|
902
|
+
if @history.searching?
|
903
|
+
@matching_text
|
904
|
+
else
|
905
|
+
@matching_text = @line[0...@line.position]
|
906
|
+
end
|
907
|
+
end
|
908
|
+
end
|
909
|
+
|
910
|
+
if RawLine.ansi? then
|
911
|
+
|
912
|
+
class Editor
|
913
|
+
|
914
|
+
if RUBY_PLATFORM.match(/mswin/) && RawLine.win32console? then
|
915
|
+
def escape(string)
|
916
|
+
string.each_byte { |c| @win32_io.putc c }
|
917
|
+
end
|
918
|
+
else
|
919
|
+
def escape(string)
|
920
|
+
# @output.print string
|
921
|
+
end
|
922
|
+
end
|
923
|
+
|
924
|
+
def terminal_width
|
925
|
+
terminal_size[0]
|
926
|
+
end
|
927
|
+
|
928
|
+
def terminal_height
|
929
|
+
terminal_size[1]
|
930
|
+
end
|
931
|
+
|
932
|
+
def cursor_position
|
933
|
+
terminal.cursor_position
|
934
|
+
end
|
935
|
+
end
|
936
|
+
end
|
937
|
+
|
938
|
+
end
|