yap-rawline 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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