rawline 0.3.0 → 0.3.1

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