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