rawline 0.3.0 → 0.3.1

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.
@@ -1,704 +1,705 @@
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
- module RawLine
13
-
14
- #
15
- # The Editor class defines methods to:
16
- #
17
- # * Read characters from STDIN or any type of input
18
- # * Write characters to STDOUT or any type of output
19
- # * Bind keys to specific actions
20
- # * Perform line-related operations like moving, navigating through history, etc.
21
- #
22
- # Note that the following default key bindings are provided:
23
- #
24
- # * TAB: word completion defined via completion_proc
25
- # * LEFT/RIGHT ARROWS: cursor movement (left/right)
26
- # * UP/DOWN ARROWS: history navigation
27
- # * DEL: Delete character under cursor
28
- # * BACKSPACE: Delete character before cursor
29
- # * INSERT: Toggle insert/replace mode (default: insert)
30
- # * CTRL+K: Clear the whole line
31
- # * CTRL+Z: undo (unless already registered by the OS)
32
- # * CTRL+Y: redo (unless already registered by the OS)
33
- #
34
- class Editor
35
-
36
- include HighLine::SystemExtensions
37
-
38
- attr_accessor :char, :history_size, :line_history_size
39
- attr_accessor :terminal, :keys, :mode
40
- attr_accessor :completion_proc, :line, :history, :completion_append_string
41
- attr_accessor :match_hidden_files, :completion_matches
42
- attr_accessor :word_break_characters
43
-
44
- #
45
- # Create an instance of RawLine::Editor which can be used
46
- # to read from input and perform line-editing operations.
47
- # This method takes an optional block used to override the
48
- # following instance attributes:
49
- # * <tt>@history_size</tt> - the size of the editor history buffer (30).
50
- # * <tt>@line_history_size</tt> - the size of the editor line history buffer (50).
51
- # * <tt>@keys</tt> - the keys (arrays of character codes) bound to specific actions.
52
- # * <tt>@word_break_characters</tt> - a string listing all characters which can be used as word separators (" \t\n\"\\'`@$><=;|&{(/").
53
- # * <tt>@mode</tt> - The editor's character insertion mode (:insert).
54
- # * <tt>@completion_proc</tt> - a Proc object used to perform word completion.
55
- # * <tt>@completion_append_string</tt> - a string to append to completed words ('').
56
- # * <tt>@completion_matches</tt> - word completion candidates.
57
- # * <tt>@terminal</tt> - a RawLine::Terminal containing character key codes.
58
- #
59
- def initialize(input=STDIN, output=STDOUT)
60
- @input = input
61
- @output = output
62
- case RUBY_PLATFORM
63
- when /mswin/i then
64
- @terminal = WindowsTerminal.new
65
- if RawLine.win32console? then
66
- @win32_io = Win32::Console::ANSI::IO.new
67
- end
68
- else
69
- @terminal = VT220Terminal.new
70
- end
71
- @history_size = 30
72
- @line_history_size = 50
73
- @keys = {}
74
- @word_break_characters = " \t\n\"\\'`@$><=;|&{(/"
75
- @mode = :insert
76
- @completion_proc = filename_completion_proc
77
- @completion_append_string = ''
78
- @match_hidden_files = false
79
- @completion_matches = HistoryBuffer.new(0) { |h| h.duplicates = false; h.cycle = true }
80
- set_default_keys
81
- yield self if block_given?
82
- update_word_separator
83
- @add_history = false
84
- @history = HistoryBuffer.new(@history_size) do |h|
85
- h.duplicates = false;
86
- h.exclude = lambda { |item| item.strip == "" }
87
- end
88
- @char = nil
89
- end
90
-
91
- #
92
- # Return the current RawLine version
93
- #
94
- def library_version
95
- "RawLine v#{RawLine.version}"
96
- end
97
-
98
- #
99
- # Read characters from <tt>@input</tt> until the user presses ENTER
100
- # (use it in the same way as you'd use IO#gets)
101
- # * An optional prompt can be specified to be printed at the beginning of the line ("").
102
- # * An optional flag can be specified to enable/disable editor history (false)
103
- #
104
- def read(prompt="", add_history=false)
105
- update_word_separator
106
- @output.print prompt if prompt != ""
107
- @add_history = add_history
108
- @line = Line.new(@line_history_size) do |l|
109
- l.prompt = prompt
110
- l.word_separator = @word_separator
111
- end
112
- add_to_line_history
113
- loop do
114
- read_character
115
- process_character
116
- break if @char == @terminal.keys[:enter] || !@char
117
- end
118
- @output.print "\n"
119
- "#{@line.text}\n"
120
- end
121
-
122
- # Readline compatibility aliases
123
- alias readline read
124
- alias completion_append_char completion_append_string
125
- alias completion_append_char= completion_append_string=
126
- alias basic_word_break_characters word_break_characters
127
- alias basic_word_break_characters= word_break_characters=
128
- alias completer_word_break_characters word_break_characters
129
- alias completer_word_break_characters= word_break_characters=
130
-
131
- #
132
- # Read and parse a character from <tt>@input</tt>.
133
- # This method is called automatically by <tt>read</tt>
134
- #
135
- def read_character
136
- @output.flush
137
- c = get_character(@input)
138
- @char = parse_key_code(c) || c
139
- end
140
-
141
- #
142
- # Parse a key or key sequence into the corresponding codes.
143
- # This method is called automatically by <tt>read_character</tt>
144
- #
145
- def parse_key_code(code)
146
- if @terminal.escape_codes.include? code then
147
- sequence = [code]
148
- seqs = []
149
- loop do
150
- c = get_character(@input)
151
- sequence << c
152
- seqs = @terminal.escape_sequences.select { |e| e[0..sequence.length-1] == sequence }
153
- break if seqs.empty?
154
- return sequence if [sequence] == seqs
155
- end
156
- else
157
- return (@terminal.keys.has_value? [code]) ? [code] : nil
158
- end
159
- end
160
-
161
- #
162
- # Write a string to <tt>@output</tt> starting from the cursor position.
163
- # Characters at the right of the cursor are shifted to the right if
164
- # <tt>@mode == :insert</tt>, deleted otherwise.
165
- #
166
- def write(string)
167
- string.each_byte { |c| print_character c, true }
168
- add_to_line_history
169
- end
170
-
171
- #
172
- # Write a new line to <tt>@output</tt>, overwriting any existing text
173
- # and printing an end of line character.
174
- #
175
- def write_line(string)
176
- clear_line
177
- @output.print string
178
- @line.text = string
179
- add_to_line_history
180
- add_to_history
181
- @char = nil
182
- end
183
-
184
- #
185
- # Process a character. If the key corresponding to the inputted character
186
- # is bound to an action, call <tt>press_key</tt>, otherwise call <tt>default_action</tt>.
187
- # This method is called automatically by <tt>read</tt>
188
- #
189
- def process_character
190
- case @char.class.to_s
191
- when 'Fixnum' then
192
- default_action
193
- when 'Array'
194
- press_key if key_bound?
195
- end
196
- end
197
-
198
- #
199
- # Bind a key to an action specified via <tt>block</tt>.
200
- # <tt>key</tt> can be:
201
- #
202
- # * A Symbol identifying a character or character sequence defined for the current terminal
203
- # * A Fixnum identifying a character defined for the current terminal
204
- # * An Array identifying a character or character sequence defined for the current terminal
205
- # * A String identifying a character or character sequence, even if it is not defined for the current terminal
206
- # * An Hash identifying a character or character sequence, even if it is not defined for the current terminal
207
- #
208
- # If <tt>key</tt> is a hash, then:
209
- #
210
- # * It must contain only one key/value pair
211
- # * The key identifies the name of the character or character sequence
212
- # * The value identifies the code(s) corresponding to the character or character sequence
213
- # * The value can be a Fixnum, a String or an Array.
214
- #
215
- def bind(key, &block)
216
- case key.class.to_s
217
- when 'Symbol' then
218
- raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys[key]
219
- @keys[@terminal.keys[key]] = block
220
- when 'Array' then
221
- raise BindingException, "Unknown key or key sequence '#{key.join(", ")}' (#{key.class.to_s})" unless @terminal.keys.has_value? key
222
- @keys[key] = block
223
- when 'Fixnum' then
224
- raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys.has_value? [key]
225
- @keys[[key]] = block
226
- when 'String' then
227
- if key.length == 1 then
228
- @keys[[key.ord]] = block
229
- else
230
- bind_hash({:"#{key}" => key}, block)
231
- end
232
- when 'Hash' then
233
- raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
234
- bind_hash(key, block)
235
- else
236
- raise BindingException, "Unable to bind '#{key.to_s}' (#{key.class.to_s})"
237
- end
238
- @terminal.update
239
- end
240
-
241
- #
242
- # Return true if the last character read via <tt>read</tt> is bound to an action.
243
- #
244
- def key_bound?
245
- @keys[@char] ? true : false
246
- end
247
-
248
- #
249
- # Call the action bound to the last character read via <tt>read</tt>.
250
- # This method is called automatically by <tt>process_character</tt>.
251
- #
252
- def press_key
253
- @keys[@char].call
254
- end
255
-
256
- #
257
- # Execute the default action for the last character read via <tt>read</tt>.
258
- # By default it prints the character to the screen via <tt>print_character</tt>.
259
- # This method is called automatically by <tt>process_character</tt>.
260
- #
261
- def default_action
262
- print_character
263
- end
264
-
265
- #
266
- # Write a character to <tt>@output</tt> at cursor position,
267
- # shifting characters as appropriate.
268
- # If <tt>no_line_history</tt> is set to <tt>true</tt>, the updated
269
- # won't be saved in the history of the current line.
270
- #
271
- def print_character(char=@char, no_line_history = false)
272
- unless @line.length >= @line.max_length-2 then
273
- if @line.position < @line.length then
274
- chars = select_characters_from_cursor if @mode == :insert
275
- @output.putc char
276
- @line.text[@line.position] = (@mode == :insert) ? "#{char.chr}#{@line.text[@line.position].chr}" : "#{char.chr}"
277
- @line.right
278
- if @mode == :insert then
279
- raw_print chars
280
- chars.length.times { @output.putc ?\b.ord } # move cursor back
281
- end
282
- else
283
- @output.putc char
284
- @line.right
285
- @line << char
286
- end
287
- add_to_line_history unless no_line_history
288
- end
289
- end
290
-
291
- #
292
- # Complete the current word according to what returned by
293
- # <tt>@completion_proc</tt>. Characters can be appended to the
294
- # completed word via <tt>@completion_append_character</tt> and word
295
- # separators can be defined via <tt>@word_separator</tt>.
296
- #
297
- # This action is bound to the tab key by default, so the first
298
- # match is displayed the first time the user presses tab, and all
299
- # the possible messages will be displayed (cyclically) when tab is
300
- # pressed again.
301
- #
302
- def complete
303
- completion_char = @char
304
- @completion_matches.empty
305
- word_start = @line.word[:start]
306
- sub_word = @line.text[@line.word[:start]..@line.position-1] || ""
307
- matches = @completion_proc.call(sub_word) unless @completion_proc == []
308
- matches = (matches.is_a?(Array)) ? matches.sort.reverse : []
309
- complete_word = lambda do |match|
310
- unless @line.word[:text].length == 0
311
- # If not in a word, print the match, otherwise continue existing word
312
- move_to_position(@line.word[:end]+@completion_append_string.length+1)
313
- end
314
- (@line.position-word_start).times { delete_left_character(true) }
315
- write match+@completion_append_string
316
- end
317
- unless matches.empty? then
318
- @completion_matches.resize(matches.length)
319
- matches.each { |w| @completion_matches << w }
320
- # Get first match
321
- @completion_matches.back
322
- match = @completion_matches.get
323
- complete_word.call(match)
324
- read_character
325
- while @char == completion_char do
326
- move_to_position(word_start)
327
- @completion_matches.back
328
- match = @completion_matches.get
329
- complete_word.call(match)
330
- read_character
331
- end
332
- process_character
333
- end
334
- end
335
-
336
- #
337
- # Complete file and directory names.
338
- # Hidden files and directories are matched only if <tt>@match_hidden_files</tt> is true.
339
- #
340
- def filename_completion_proc
341
- lambda do |word|
342
- dirs = @line.text.split('/')
343
- path = @line.text.match(/^\/|[a-zA-Z]:\//) ? "/" : Dir.pwd+"/"
344
- if dirs.length == 0 then # starting directory
345
- dir = path
346
- else
347
- dirs.delete(dirs.last) unless File.directory?(path+dirs.join('/'))
348
- dir = path+dirs.join('/')
349
- end
350
- Dir.entries(dir).select { |e| (e =~ /^\./ && @match_hidden_files && word == '') || (e =~ /^#{word}/ && e !~ /^\./) }
351
- end
352
- end
353
-
354
-
355
- #
356
- # Adds <tt>@line.text</tt> to the editor history. This action is
357
- # bound to the enter key by default.
358
- #
359
- def newline
360
- add_to_history
361
- end
362
-
363
- #
364
- # Move the cursor left (if possible) by printing a
365
- # backspace, updating <tt>@line.position</tt> accordingly.
366
- # This action is bound to the left arrow key by default.
367
- #
368
- def move_left
369
- unless @line.bol? then
370
- @output.putc ?\b.ord
371
- @line.left
372
- return true
373
- end
374
- false
375
- end
376
-
377
- #
378
- # Move the cursor right (if possible) by re-printing the
379
- # character at the right of the cursor, if any, and updating
380
- # <tt>@line.position</tt> accordingly.
381
- # This action is bound to the right arrow key by default.
382
- #
383
- def move_right
384
- unless @line.position > @line.eol then
385
- @line.right
386
- @output.putc @line.text[@line.position-1]
387
- return true
388
- end
389
- false
390
- end
391
-
392
- #
393
- # Print debug information about the current line. Note that after
394
- # the message is displayed, the line text and position will be restored.
395
- #
396
- def debug_line
397
- pos = @line.position
398
- text = @line.text
399
- word = @line.word
400
- @output.puts
401
- @output.puts "Text: [#{text}]"
402
- @output.puts "Length: #{@line.length}"
403
- @output.puts "Position: #{pos}"
404
- @output.puts "Character at Position: [#{text[pos].chr}] (#{text[pos]})" unless pos >= @line.length
405
- @output.puts "Current Word: [#{word[:text]}] (#{word[:start]} -- #{word[:end]})"
406
- clear_line
407
- raw_print text
408
- overwrite_line(text, pos)
409
- end
410
-
411
- #
412
- # Print the content of the editor history. Note that after
413
- # the message is displayed, the line text and position will be restored.
414
- #
415
- def show_history
416
- pos = @line.position
417
- text = @line.text
418
- @output.puts
419
- @output.puts "History:"
420
- @history.each {|l| puts "- [#{l}]"}
421
- overwrite_line(text, pos)
422
- end
423
-
424
- #
425
- # Clear the editor history.
426
- #
427
- def clear_history
428
- @history.empty
429
- end
430
-
431
- #
432
- # Delete the character at the left of the cursor.
433
- # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
434
- # recorded in the line history.
435
- # This action is bound to the backspace key by default.
436
- #
437
- def delete_left_character(no_line_history=false)
438
- if move_left then
439
- delete_character(no_line_history)
440
- end
441
- end
442
-
443
- #
444
- # Delete the character under the cursor.
445
- # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
446
- # recorded in the line history.
447
- # This action is bound to the delete key by default.
448
- #
449
- def delete_character(no_line_history=false)
450
- unless @line.position > @line.eol
451
- # save characters to shift
452
- chars = (@line.eol?) ? ' ' : select_characters_from_cursor(1)
453
- # remove character from console and shift characters
454
- raw_print chars
455
- @output.putc ?\s.ord
456
- (chars.length+1).times { @output.putc ?\b.ord }
457
- #remove character from line
458
- @line[@line.position] = ''
459
- add_to_line_history unless no_line_history
460
- end
461
- end
462
-
463
- #
464
- # Clear the current line, i.e.
465
- # <tt>@line.text</tt> and <tt>@line.position</tt>.
466
- # This action is bound to ctrl+k by default.
467
- #
468
- def clear_line
469
- @output.putc ?\r
470
- print @line.prompt
471
- @line.length.times { @output.putc ?\s.ord }
472
- @line.length.times { @output.putc ?\b.ord }
473
- add_to_line_history
474
- @line.text = ""
475
- @line.position = 0
476
- end
477
-
478
- #
479
- # Undo the last modification to the current line (<tt>@line.text</tt>).
480
- # This action is bound to ctrl+z by default.
481
- #
482
- def undo
483
- generic_history_back(@line.history) if @line.history.position == nil
484
- generic_history_back(@line.history)
485
- end
486
-
487
- #
488
- # Redo a previously-undone modification to the
489
- # current line (<tt>@line.text</tt>).
490
- # This action is bound to ctrl+y by default.
491
- #
492
- def redo
493
- generic_history_forward(@line.history)
494
- end
495
-
496
- #
497
- # Load the previous entry of the editor in place of the
498
- # current line (<tt>@line.text</tt>).
499
- # This action is bound to the up arrow key by default.
500
- #
501
- def history_back
502
- unless @history.position
503
- current_line = @line.text.dup
504
- # Temporarily override exclusion rules
505
- exclude = @history.exclude.dup
506
- @history.exclude = lambda{|a|}
507
- # Add current line
508
- @history << current_line
509
- @history.exclude = exclude
510
- @history.back
511
- end
512
- generic_history_back(@history)
513
- add_to_line_history
514
- end
515
-
516
- #
517
- # Load the next entry of the editor history in place of the
518
- # current line (<tt>@line.text</tt>).
519
- # This action is bound to down arrow key by default.
520
- #
521
- def history_forward
522
- generic_history_forward(@history)
523
- add_to_line_history
524
- end
525
-
526
- #
527
- # Add the current line (<tt>@line.text</tt>) to the
528
- # line history, to allow undo/redo
529
- # operations.
530
- #
531
- def add_to_line_history
532
- @line.history << @line.text.dup unless @line.text == ""
533
- end
534
-
535
- #
536
- # Add the current line (<tt>@line.text</tt>) to the editor history.
537
- #
538
- def add_to_history
539
- @history << @line.text.dup if @add_history && @line.text != ""
540
- end
541
-
542
- #
543
- # Toggle the editor <tt>@mode</tt> to :replace or :insert (default).
544
- #
545
- def toggle_mode
546
- case @mode
547
- when :insert then @mode = :replace
548
- when :replace then @mode = :insert
549
- end
550
- end
551
-
552
- #
553
- # Overwrite the current line (<tt>@line.text</tt>)
554
- # with <tt>new_line</tt>, and optionally reset the cursor position to
555
- # <tt>position</tt>.
556
- #
557
- def overwrite_line(new_line, position=nil)
558
- pos = position || new_line.length
559
- text = @line.text
560
- @output.putc ?\r.ord
561
- print @line.prompt
562
- raw_print new_line
563
- n = text.length-new_line.length+1
564
- if n > 0
565
- n.times { @output.putc ?\s.ord }
566
- n.times { @output.putc ?\b.ord }
567
- end
568
- @line.position = new_line.length
569
- move_to_position(pos)
570
- @line.text = new_line
571
- end
572
-
573
- #
574
- # Move the cursor to <tt>pos</tt>.
575
- #
576
- def move_to_position(pos)
577
- n = pos-@line.position
578
- case
579
- when n > 0 then
580
- n.times { move_right }
581
- when n < 0 then
582
- n.abs.times {move_left}
583
- when n == 0 then
584
- end
585
- end
586
-
587
- private
588
-
589
- def update_word_separator
590
- chars = []
591
- @word_break_characters.each_byte do |c|
592
- ch = (c.is_a? Fixnum) ? c : c.ord
593
- value = (ch == ?\s.ord) ? ' ' : Regexp.escape(ch.chr).to_s
594
- chars << value
595
- end
596
- @word_separator = /#{chars.join('|')}/
597
- end
598
-
599
- def bind_hash(key, block)
600
- key.each_pair do |j,k|
601
- 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)
602
- code = []
603
- case k.class.to_s
604
- when 'Fixnum' then
605
- code = [k]
606
- when 'String' then
607
- k.each_byte { |b| code << b }
608
- when 'Array' then
609
- code = k
610
- else
611
- raise BindingException, "Unable to bind '#{k.to_s}' (#{k.class.to_s})"
612
- end
613
- @terminal.keys[j] = code
614
- @keys[code] = block
615
- end
616
- end
617
-
618
- def select_characters_from_cursor(offset=0)
619
- select_characters(:right, @line.length-@line.position, offset)
620
- end
621
-
622
- def raw_print(string)
623
- string.each_byte { |c| @output.putc c }
624
- end
625
-
626
- def generic_history_back(history)
627
- unless history.empty?
628
- history.back
629
- line = history.get
630
- overwrite_line(line)
631
- end
632
- end
633
-
634
- def generic_history_forward(history)
635
- if history.forward then
636
- overwrite_line(history.get)
637
- end
638
- end
639
-
640
- def select_characters(direction, n, offset=0)
641
- if direction == :right then
642
- @line.text[@line.position+offset..@line.position+offset+n]
643
- elsif direction == :left then
644
- @line.text[@line.position-offset-n..@line.position-offset]
645
- end
646
- end
647
-
648
- def set_default_keys
649
- bind(:enter) { newline }
650
- bind(:tab) { complete }
651
- bind(:backspace) { delete_left_character }
652
- bind(:ctrl_k) { clear_line }
653
- bind(:ctrl_u) { undo }
654
- bind(:ctrl_r) { self.redo }
655
- bind(:left_arrow) { move_left }
656
- bind(:right_arrow) { move_right }
657
- bind(:up_arrow) { history_back }
658
- bind(:down_arrow) { history_forward }
659
- bind(:delete) { delete_character }
660
- bind(:insert) { toggle_mode }
661
- end
662
-
663
- end
664
-
665
- if RawLine.ansi? then
666
-
667
- class Editor
668
-
669
- if RUBY_PLATFORM.match(/mswin/) && RawLine.win32console? then
670
- def escape(string)
671
- string.each_byte { |c| @win32_io.putc c }
672
- end
673
- else
674
- def escape(string)
675
- @output.print string
676
- end
677
- end
678
-
679
- undef move_left
680
- def move_left
681
- unless @line.bol? then
682
- @line.left
683
- escape "\e[D"
684
- return true
685
- end
686
- false
687
- end
688
-
689
- undef move_right
690
- def move_right
691
- unless @line.position > @line.eol then
692
- @line.right
693
- escape "\e[C"
694
- return true
695
- end
696
- false
697
- end
698
-
699
- end
700
- end
701
-
702
- end
703
-
704
-
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
+ module RawLine
13
+
14
+ #
15
+ # The Editor class defines methods to:
16
+ #
17
+ # * Read characters from STDIN or any type of input
18
+ # * Write characters to STDOUT or any type of output
19
+ # * Bind keys to specific actions
20
+ # * Perform line-related operations like moving, navigating through history, etc.
21
+ #
22
+ # Note that the following default key bindings are provided:
23
+ #
24
+ # * TAB: word completion defined via completion_proc
25
+ # * LEFT/RIGHT ARROWS: cursor movement (left/right)
26
+ # * UP/DOWN ARROWS: history navigation
27
+ # * DEL: Delete character under cursor
28
+ # * BACKSPACE: Delete character before cursor
29
+ # * INSERT: Toggle insert/replace mode (default: insert)
30
+ # * CTRL+K: Clear the whole line
31
+ # * CTRL+Z: undo (unless already registered by the OS)
32
+ # * CTRL+Y: redo (unless already registered by the OS)
33
+ #
34
+ class Editor
35
+
36
+ include HighLine::SystemExtensions
37
+
38
+ attr_accessor :char, :history_size, :line_history_size
39
+ attr_accessor :terminal, :keys, :mode
40
+ attr_accessor :completion_proc, :line, :history, :completion_append_string
41
+ attr_accessor :match_hidden_files, :completion_matches
42
+ attr_accessor :word_break_characters
43
+
44
+ #
45
+ # Create an instance of RawLine::Editor which can be used
46
+ # to read from input and perform line-editing operations.
47
+ # This method takes an optional block used to override the
48
+ # following instance attributes:
49
+ # * <tt>@history_size</tt> - the size of the editor history buffer (30).
50
+ # * <tt>@line_history_size</tt> - the size of the editor line history buffer (50).
51
+ # * <tt>@keys</tt> - the keys (arrays of character codes) bound to specific actions.
52
+ # * <tt>@word_break_characters</tt> - a string listing all characters which can be used as word separators (" \t\n\"\\'`@$><=;|&{(/").
53
+ # * <tt>@mode</tt> - The editor's character insertion mode (:insert).
54
+ # * <tt>@completion_proc</tt> - a Proc object used to perform word completion.
55
+ # * <tt>@completion_append_string</tt> - a string to append to completed words ('').
56
+ # * <tt>@completion_matches</tt> - word completion candidates.
57
+ # * <tt>@terminal</tt> - a RawLine::Terminal containing character key codes.
58
+ #
59
+ def initialize(input=STDIN, output=STDOUT)
60
+ @input = input
61
+ @output = output
62
+ case RUBY_PLATFORM
63
+ when /mswin/i then
64
+ @terminal = WindowsTerminal.new
65
+ if RawLine.win32console? then
66
+ @win32_io = Win32::Console::ANSI::IO.new
67
+ end
68
+ else
69
+ @terminal = VT220Terminal.new
70
+ end
71
+ @history_size = 30
72
+ @line_history_size = 50
73
+ @keys = {}
74
+ @word_break_characters = " \t\n\"\\'`@$><=;|&{(/"
75
+ @mode = :insert
76
+ @completion_proc = filename_completion_proc
77
+ @completion_append_string = ''
78
+ @match_hidden_files = false
79
+ @completion_matches = HistoryBuffer.new(0) { |h| h.duplicates = false; h.cycle = true }
80
+ set_default_keys
81
+ yield self if block_given?
82
+ update_word_separator
83
+ @add_history = false
84
+ @history = HistoryBuffer.new(@history_size) do |h|
85
+ h.duplicates = false;
86
+ h.exclude = lambda { |item| item.strip == "" }
87
+ end
88
+ @char = nil
89
+ end
90
+
91
+ #
92
+ # Return the current RawLine version
93
+ #
94
+ def library_version
95
+ "RawLine v#{RawLine.rawline_version}"
96
+ end
97
+
98
+ #
99
+ # Read characters from <tt>@input</tt> until the user presses ENTER
100
+ # (use it in the same way as you'd use IO#gets)
101
+ # * An optional prompt can be specified to be printed at the beginning of the line ("").
102
+ # * An optional flag can be specified to enable/disable editor history (false)
103
+ #
104
+ def read(prompt="", add_history=false)
105
+ update_word_separator
106
+ @output.print prompt if prompt != ""
107
+ @add_history = add_history
108
+ @line = Line.new(@line_history_size) do |l|
109
+ l.prompt = prompt
110
+ l.word_separator = @word_separator
111
+ end
112
+ add_to_line_history
113
+ loop do
114
+ read_character
115
+ process_character
116
+ break if @char == @terminal.keys[:enter] || !@char
117
+ end
118
+ @output.print "\n"
119
+ @line.text
120
+ end
121
+
122
+ # Readline compatibility aliases
123
+ alias readline read
124
+ alias completion_append_character completion_append_string
125
+ alias completion_append_character= completion_append_string=
126
+ alias basic_word_break_characters word_break_characters
127
+ alias basic_word_break_characters= word_break_characters=
128
+ alias completer_word_break_characters word_break_characters
129
+ alias completer_word_break_characters= word_break_characters=
130
+
131
+ #
132
+ # Read and parse a character from <tt>@input</tt>.
133
+ # This method is called automatically by <tt>read</tt>
134
+ #
135
+ def read_character
136
+ @output.flush
137
+ c = get_character(@input)
138
+ @char = parse_key_code(c) || c
139
+ end
140
+
141
+ #
142
+ # Parse a key or key sequence into the corresponding codes.
143
+ # This method is called automatically by <tt>read_character</tt>
144
+ #
145
+ def parse_key_code(code)
146
+ if @terminal.escape_codes.include? code then
147
+ sequence = [code]
148
+ seqs = []
149
+ loop do
150
+ c = get_character(@input)
151
+ sequence << c
152
+ seqs = @terminal.escape_sequences.select { |e| e[0..sequence.length-1] == sequence }
153
+ break if seqs.empty?
154
+ return sequence if [sequence] == seqs
155
+ end
156
+ else
157
+ return (@terminal.keys.has_value? [code]) ? [code] : nil
158
+ end
159
+ end
160
+
161
+ #
162
+ # Write a string to <tt>@output</tt> starting from the cursor position.
163
+ # Characters at the right of the cursor are shifted to the right if
164
+ # <tt>@mode == :insert</tt>, deleted otherwise.
165
+ #
166
+ def write(string)
167
+ string.each_byte { |c| print_character c, true }
168
+ add_to_line_history
169
+ end
170
+
171
+ #
172
+ # Write a new line to <tt>@output</tt>, overwriting any existing text
173
+ # and printing an end of line character.
174
+ #
175
+ def write_line(string)
176
+ clear_line
177
+ @output.print string
178
+ @line.text = string
179
+ add_to_line_history
180
+ add_to_history
181
+ @char = nil
182
+ end
183
+
184
+ #
185
+ # Process a character. If the key corresponding to the inputted character
186
+ # is bound to an action, call <tt>press_key</tt>, otherwise call <tt>default_action</tt>.
187
+ # This method is called automatically by <tt>read</tt>
188
+ #
189
+ def process_character
190
+ case @char.class.to_s
191
+ when 'Fixnum' then
192
+ default_action
193
+ when 'Array'
194
+ press_key if key_bound?
195
+ end
196
+ end
197
+
198
+ #
199
+ # Bind a key to an action specified via <tt>block</tt>.
200
+ # <tt>key</tt> can be:
201
+ #
202
+ # * A Symbol identifying a character or character sequence defined for the current terminal
203
+ # * A Fixnum identifying a character defined for the current terminal
204
+ # * An Array identifying a character or character sequence defined for the current terminal
205
+ # * A String identifying a character or character sequence, even if it is not defined for the current terminal
206
+ # * An Hash identifying a character or character sequence, even if it is not defined for the current terminal
207
+ #
208
+ # If <tt>key</tt> is a hash, then:
209
+ #
210
+ # * It must contain only one key/value pair
211
+ # * The key identifies the name of the character or character sequence
212
+ # * The value identifies the code(s) corresponding to the character or character sequence
213
+ # * The value can be a Fixnum, a String or an Array.
214
+ #
215
+ def bind(key, &block)
216
+ case key.class.to_s
217
+ when 'Symbol' then
218
+ raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys[key]
219
+ @keys[@terminal.keys[key]] = block
220
+ when 'Array' then
221
+ raise BindingException, "Unknown key or key sequence '#{key.join(", ")}' (#{key.class.to_s})" unless @terminal.keys.has_value? key
222
+ @keys[key] = block
223
+ when 'Fixnum' then
224
+ raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys.has_value? [key]
225
+ @keys[[key]] = block
226
+ when 'String' then
227
+ if key.length == 1 then
228
+ @keys[[key.ord]] = block
229
+ else
230
+ bind_hash({:"#{key}" => key}, block)
231
+ end
232
+ when 'Hash' then
233
+ raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
234
+ bind_hash(key, block)
235
+ else
236
+ raise BindingException, "Unable to bind '#{key.to_s}' (#{key.class.to_s})"
237
+ end
238
+ @terminal.update
239
+ end
240
+
241
+ #
242
+ # Return true if the last character read via <tt>read</tt> is bound to an action.
243
+ #
244
+ def key_bound?
245
+ @keys[@char] ? true : false
246
+ end
247
+
248
+ #
249
+ # Call the action bound to the last character read via <tt>read</tt>.
250
+ # This method is called automatically by <tt>process_character</tt>.
251
+ #
252
+ def press_key
253
+ @keys[@char].call
254
+ end
255
+
256
+ #
257
+ # Execute the default action for the last character read via <tt>read</tt>.
258
+ # By default it prints the character to the screen via <tt>print_character</tt>.
259
+ # This method is called automatically by <tt>process_character</tt>.
260
+ #
261
+ def default_action
262
+ print_character
263
+ end
264
+
265
+ #
266
+ # Write a character to <tt>@output</tt> at cursor position,
267
+ # shifting characters as appropriate.
268
+ # If <tt>no_line_history</tt> is set to <tt>true</tt>, the updated
269
+ # won't be saved in the history of the current line.
270
+ #
271
+ def print_character(char=@char, no_line_history = false)
272
+ unless @line.length >= @line.max_length-2 then
273
+ if @line.position < @line.length then
274
+ chars = select_characters_from_cursor if @mode == :insert
275
+ @output.putc char
276
+ @line.text[@line.position] = (@mode == :insert) ? "#{char.chr}#{@line.text[@line.position].chr}" : "#{char.chr}"
277
+ @line.right
278
+ if @mode == :insert then
279
+ raw_print chars
280
+ chars.length.times { @output.putc ?\b.ord } # move cursor back
281
+ end
282
+ else
283
+ @output.putc char
284
+ @line.right
285
+ @line << char
286
+ end
287
+ add_to_line_history unless no_line_history
288
+ end
289
+ end
290
+
291
+ #
292
+ # Complete the current word according to what returned by
293
+ # <tt>@completion_proc</tt>. Characters can be appended to the
294
+ # completed word via <tt>@completion_append_character</tt> and word
295
+ # separators can be defined via <tt>@word_separator</tt>.
296
+ #
297
+ # This action is bound to the tab key by default, so the first
298
+ # match is displayed the first time the user presses tab, and all
299
+ # the possible messages will be displayed (cyclically) when tab is
300
+ # pressed again.
301
+ #
302
+ def complete
303
+ completion_char = @char
304
+ @completion_matches.empty
305
+ word_start = @line.word[:start]
306
+ sub_word = @line.text[@line.word[:start]..@line.position-1] || ""
307
+ matches = @completion_proc.call(sub_word) unless !completion_proc || @completion_proc == []
308
+ matches = matches.to_a.compact.sort.reverse
309
+ complete_word = lambda do |match|
310
+ unless @line.word[:text].length == 0
311
+ # If not in a word, print the match, otherwise continue existing word
312
+ move_to_position(@line.word[:end]+@completion_append_string.to_s.length+1)
313
+ end
314
+ (@line.position-word_start).times { delete_left_character(true) }
315
+ write match+@completion_append_string.to_s
316
+ end
317
+ unless matches.empty? then
318
+ @completion_matches.resize(matches.length)
319
+ matches.each { |w| @completion_matches << w }
320
+ # Get first match
321
+ @completion_matches.back
322
+ match = @completion_matches.get
323
+ complete_word.call(match)
324
+ read_character
325
+ while @char == completion_char do
326
+ move_to_position(word_start)
327
+ @completion_matches.back
328
+ match = @completion_matches.get
329
+ complete_word.call(match)
330
+ read_character
331
+ end
332
+ process_character
333
+ end
334
+ end
335
+
336
+ #
337
+ # Complete file and directory names.
338
+ # Hidden files and directories are matched only if <tt>@match_hidden_files</tt> is true.
339
+ #
340
+ def filename_completion_proc
341
+ lambda do |word|
342
+ dirs = @line.text.split('/')
343
+ path = @line.text.match(/^\/|[a-zA-Z]:\//) ? "/" : Dir.pwd+"/"
344
+ if dirs.length == 0 then # starting directory
345
+ dir = path
346
+ else
347
+ dirs.delete(dirs.last) unless File.directory?(path+dirs.join('/'))
348
+ dir = path+dirs.join('/')
349
+ end
350
+ Dir.entries(dir).select { |e| (e =~ /^\./ && @match_hidden_files && word == '') || (e =~ /^#{word}/ && e !~ /^\./) }
351
+ end
352
+ end
353
+
354
+
355
+ #
356
+ # Adds <tt>@line.text</tt> to the editor history. This action is
357
+ # bound to the enter key by default.
358
+ #
359
+ def newline
360
+ add_to_history
361
+ end
362
+
363
+ #
364
+ # Move the cursor left (if possible) by printing a
365
+ # backspace, updating <tt>@line.position</tt> accordingly.
366
+ # This action is bound to the left arrow key by default.
367
+ #
368
+ def move_left
369
+ unless @line.bol? then
370
+ @output.putc ?\b.ord
371
+ @line.left
372
+ return true
373
+ end
374
+ false
375
+ end
376
+
377
+ #
378
+ # Move the cursor right (if possible) by re-printing the
379
+ # character at the right of the cursor, if any, and updating
380
+ # <tt>@line.position</tt> accordingly.
381
+ # This action is bound to the right arrow key by default.
382
+ #
383
+ def move_right
384
+ unless @line.position > @line.eol then
385
+ @line.right
386
+ @output.putc @line.text[@line.position-1]
387
+ return true
388
+ end
389
+ false
390
+ end
391
+
392
+ #
393
+ # Print debug information about the current line. Note that after
394
+ # the message is displayed, the line text and position will be restored.
395
+ #
396
+ def debug_line
397
+ pos = @line.position
398
+ text = @line.text
399
+ word = @line.word
400
+ @output.puts
401
+ @output.puts "Text: [#{text}]"
402
+ @output.puts "Length: #{@line.length}"
403
+ @output.puts "Position: #{pos}"
404
+ @output.puts "Character at Position: [#{text[pos].chr}] (#{text[pos]})" unless pos >= @line.length
405
+ @output.puts "Current Word: [#{word[:text]}] (#{word[:start]} -- #{word[:end]})"
406
+ clear_line
407
+ raw_print text
408
+ overwrite_line(text, pos)
409
+ end
410
+
411
+ #
412
+ # Print the content of the editor history. Note that after
413
+ # the message is displayed, the line text and position will be restored.
414
+ #
415
+ def show_history
416
+ pos = @line.position
417
+ text = @line.text
418
+ @output.puts
419
+ @output.puts "History:"
420
+ @history.each {|l| puts "- [#{l}]"}
421
+ overwrite_line(text, pos)
422
+ end
423
+
424
+ #
425
+ # Clear the editor history.
426
+ #
427
+ def clear_history
428
+ @history.empty
429
+ end
430
+
431
+ #
432
+ # Delete the character at the left of the cursor.
433
+ # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
434
+ # recorded in the line history.
435
+ # This action is bound to the backspace key by default.
436
+ #
437
+ def delete_left_character(no_line_history=false)
438
+ if move_left then
439
+ delete_character(no_line_history)
440
+ end
441
+ end
442
+
443
+ #
444
+ # Delete the character under the cursor.
445
+ # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
446
+ # recorded in the line history.
447
+ # This action is bound to the delete key by default.
448
+ #
449
+ def delete_character(no_line_history=false)
450
+ unless @line.position > @line.eol
451
+ # save characters to shift
452
+ chars = (@line.eol?) ? ' ' : select_characters_from_cursor(1)
453
+ # remove character from console and shift characters
454
+ raw_print chars
455
+ @output.putc ?\s.ord
456
+ (chars.length+1).times { @output.putc ?\b.ord }
457
+ #remove character from line
458
+ @line[@line.position] = ''
459
+ add_to_line_history unless no_line_history
460
+ end
461
+ end
462
+
463
+ #
464
+ # Clear the current line, i.e.
465
+ # <tt>@line.text</tt> and <tt>@line.position</tt>.
466
+ # This action is bound to ctrl+k by default.
467
+ #
468
+ def clear_line
469
+ @output.putc ?\r
470
+ print @line.prompt
471
+ @line.length.times { @output.putc ?\s.ord }
472
+ @line.length.times { @output.putc ?\b.ord }
473
+ add_to_line_history
474
+ @line.text = ""
475
+ @line.position = 0
476
+ end
477
+
478
+ #
479
+ # Undo the last modification to the current line (<tt>@line.text</tt>).
480
+ # This action is bound to ctrl+z by default.
481
+ #
482
+ def undo
483
+ generic_history_back(@line.history) if @line.history.position == nil
484
+ generic_history_back(@line.history)
485
+ end
486
+
487
+ #
488
+ # Redo a previously-undone modification to the
489
+ # current line (<tt>@line.text</tt>).
490
+ # This action is bound to ctrl+y by default.
491
+ #
492
+ def redo
493
+ generic_history_forward(@line.history)
494
+ end
495
+
496
+ #
497
+ # Load the previous entry of the editor in place of the
498
+ # current line (<tt>@line.text</tt>).
499
+ # This action is bound to the up arrow key by default.
500
+ #
501
+ def history_back
502
+ unless @history.position
503
+ current_line = @line.text.dup
504
+ # Temporarily override exclusion rules
505
+ exclude = @history.exclude.dup
506
+ @history.exclude = lambda{|a|}
507
+ # Add current line
508
+ @history << current_line
509
+ @history.exclude = exclude
510
+ @history.back
511
+ end
512
+ generic_history_back(@history)
513
+ add_to_line_history
514
+ end
515
+
516
+ #
517
+ # Load the next entry of the editor history in place of the
518
+ # current line (<tt>@line.text</tt>).
519
+ # This action is bound to down arrow key by default.
520
+ #
521
+ def history_forward
522
+ generic_history_forward(@history)
523
+ add_to_line_history
524
+ end
525
+
526
+ #
527
+ # Add the current line (<tt>@line.text</tt>) to the
528
+ # line history, to allow undo/redo
529
+ # operations.
530
+ #
531
+ def add_to_line_history
532
+ @line.history << @line.text.dup unless @line.text == ""
533
+ end
534
+
535
+ #
536
+ # Add the current line (<tt>@line.text</tt>) to the editor history.
537
+ #
538
+ def add_to_history
539
+ @history << @line.text.dup if @add_history && @line.text != ""
540
+ end
541
+
542
+ #
543
+ # Toggle the editor <tt>@mode</tt> to :replace or :insert (default).
544
+ #
545
+ def toggle_mode
546
+ case @mode
547
+ when :insert then @mode = :replace
548
+ when :replace then @mode = :insert
549
+ end
550
+ end
551
+
552
+ #
553
+ # Overwrite the current line (<tt>@line.text</tt>)
554
+ # with <tt>new_line</tt>, and optionally reset the cursor position to
555
+ # <tt>position</tt>.
556
+ #
557
+ def overwrite_line(new_line, position=nil)
558
+ pos = position || new_line.length
559
+ text = @line.text
560
+ @output.putc ?\r.ord
561
+ print @line.prompt
562
+ raw_print new_line
563
+ n = text.length-new_line.length+1
564
+ if n > 0
565
+ n.times { @output.putc ?\s.ord }
566
+ n.times { @output.putc ?\b.ord }
567
+ end
568
+ @line.position = new_line.length
569
+ move_to_position(pos)
570
+ @line.text = new_line
571
+ end
572
+
573
+ #
574
+ # Move the cursor to <tt>pos</tt>.
575
+ #
576
+ def move_to_position(pos)
577
+ n = pos-@line.position
578
+ case
579
+ when n > 0 then
580
+ n.times { move_right }
581
+ when n < 0 then
582
+ n.abs.times {move_left}
583
+ when n == 0 then
584
+ end
585
+ end
586
+
587
+ private
588
+
589
+ def update_word_separator
590
+ return @word_separator = "" if @word_break_characters.to_s == ""
591
+ chars = []
592
+ @word_break_characters.each_byte do |c|
593
+ ch = (c.is_a? Fixnum) ? c : c.ord
594
+ value = (ch == ?\s.ord) ? ' ' : Regexp.escape(ch.chr).to_s
595
+ chars << value
596
+ end
597
+ @word_separator = /#{chars.join('|')}/
598
+ end
599
+
600
+ def bind_hash(key, block)
601
+ key.each_pair do |j,k|
602
+ 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)
603
+ code = []
604
+ case k.class.to_s
605
+ when 'Fixnum' then
606
+ code = [k]
607
+ when 'String' then
608
+ k.each_byte { |b| code << b }
609
+ when 'Array' then
610
+ code = k
611
+ else
612
+ raise BindingException, "Unable to bind '#{k.to_s}' (#{k.class.to_s})"
613
+ end
614
+ @terminal.keys[j] = code
615
+ @keys[code] = block
616
+ end
617
+ end
618
+
619
+ def select_characters_from_cursor(offset=0)
620
+ select_characters(:right, @line.length-@line.position, offset)
621
+ end
622
+
623
+ def raw_print(string)
624
+ string.each_byte { |c| @output.putc c }
625
+ end
626
+
627
+ def generic_history_back(history)
628
+ unless history.empty?
629
+ history.back
630
+ line = history.get
631
+ overwrite_line(line)
632
+ end
633
+ end
634
+
635
+ def generic_history_forward(history)
636
+ if history.forward then
637
+ overwrite_line(history.get)
638
+ end
639
+ end
640
+
641
+ def select_characters(direction, n, offset=0)
642
+ if direction == :right then
643
+ @line.text[@line.position+offset..@line.position+offset+n]
644
+ elsif direction == :left then
645
+ @line.text[@line.position-offset-n..@line.position-offset]
646
+ end
647
+ end
648
+
649
+ def set_default_keys
650
+ bind(:enter) { newline }
651
+ bind(:tab) { complete }
652
+ bind(:backspace) { delete_left_character }
653
+ bind(:ctrl_k) { clear_line }
654
+ bind(:ctrl_u) { undo }
655
+ bind(:ctrl_r) { self.redo }
656
+ bind(:left_arrow) { move_left }
657
+ bind(:right_arrow) { move_right }
658
+ bind(:up_arrow) { history_back }
659
+ bind(:down_arrow) { history_forward }
660
+ bind(:delete) { delete_character }
661
+ bind(:insert) { toggle_mode }
662
+ end
663
+
664
+ end
665
+
666
+ if RawLine.ansi? then
667
+
668
+ class Editor
669
+
670
+ if RUBY_PLATFORM.match(/mswin/) && RawLine.win32console? then
671
+ def escape(string)
672
+ string.each_byte { |c| @win32_io.putc c }
673
+ end
674
+ else
675
+ def escape(string)
676
+ @output.print string
677
+ end
678
+ end
679
+
680
+ undef move_left
681
+ def move_left
682
+ unless @line.bol? then
683
+ @line.left
684
+ escape "\e[D"
685
+ return true
686
+ end
687
+ false
688
+ end
689
+
690
+ undef move_right
691
+ def move_right
692
+ unless @line.position > @line.eol then
693
+ @line.right
694
+ escape "\e[C"
695
+ return true
696
+ end
697
+ false
698
+ end
699
+
700
+ end
701
+ end
702
+
703
+ end
704
+
705
+