rawline 0.2.0

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