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.
@@ -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