yap-rawline 0.3.5 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: d79b164cb6b3ae9d4466e30f1597b238f125d125
4
- data.tar.gz: f3f078c0e1bc30afbd66ab83fe189a8dc7515c05
3
+ metadata.gz: b3cf478bbbaca65f44353988eb4f3a6522dff7db
4
+ data.tar.gz: b6b4edd7affe941e8ea350bcf8aa0d660481de25
5
5
  SHA512:
6
- metadata.gz: 656851a08f73fb8843df4a5641f98f5c509649f5777db197519e3726d282a007f54d655ca92b165794f425d5ff61a4fdaae9e28dbb8d4bbd4c6ec9ab65a22ddf
7
- data.tar.gz: fd867c4e039e5d09bbff714bd92514d01a293f45a052fd4ec1de46a91fcad6fc8bef36666f34cc21a181fedf29067ffa24b65bdca4a6f181303eeabbc4fa34d2
6
+ metadata.gz: 63b4b7ab19f77f8a6c4d57d40aa7bc5e3def8509700d67cb25559655bb2c24f3d1017232ce4a6da11b52e4fff86566108dd21cfca3fdf20dd85978e7f6cf81f7
7
+ data.tar.gz: d73ad94f1951fc5eb7254b0c998b53233ba757bd5a2846d32593b427c05e797c203fd2a7cb29cd851fe76505172aead24ca765f96268e2f2a6511bfa0d57c3be
data/lib/rawline.rb CHANGED
@@ -50,7 +50,9 @@ require "#{dir}/rawline/prompt"
50
50
  require "#{dir}/rawline/completer"
51
51
  require "#{dir}/rawline/event_loop"
52
52
  require "#{dir}/rawline/event_registry"
53
+ require "#{dir}/rawline/key_bindings"
53
54
  require "#{dir}/rawline/keycode_parser"
55
+ require "#{dir}/rawline/line_editor"
54
56
  require "#{dir}/rawline/non_blocking_input"
55
57
  require "#{dir}/rawline/renderer"
56
58
  require "#{dir}/rawline/dom_tree"
@@ -4,6 +4,8 @@ module RawLine
4
4
  class DomTree < TerminalLayout::Box
5
5
  attr_accessor :prompt_box, :input_box, :content_box
6
6
 
7
+ attr_accessor :focused_input_box
8
+
7
9
  def initialize(children: nil)
8
10
  unless children
9
11
  @prompt_box = TerminalLayout::Box.new(content: "default-prompt>", style: {display: :inline})
@@ -12,7 +14,20 @@ module RawLine
12
14
  super(style: {}, children: [@prompt_box, @input_box, @content_box])
13
15
  else
14
16
  super(style: {}, children: children)
17
+ @input_box = find_child_of_type(TerminalLayout::InputBox)
15
18
  end
19
+ focus_input_box(@input_box)
20
+ end
21
+
22
+ def focus_input_box(box)
23
+ @focused_input_box.remove_focus! if @focused_input_box
24
+ @focused_input_box = box
25
+ @focused_input_box.focus! if @focused_input_box
26
+ end
27
+
28
+ def input_box=(box)
29
+ @input_box = box
30
+ focus_input_box(@input_box)
16
31
  end
17
32
  end
18
33
  end
@@ -49,7 +49,7 @@ module RawLine
49
49
  attr_accessor :dom
50
50
 
51
51
  # TODO: dom traversal for lookup rather than assignment
52
- attr_accessor :prompt_box, :input_box, :content_box
52
+ attr_accessor :prompt_box, :input_box, :content_box, :focused_input_box
53
53
 
54
54
  def self.create(dom: nil, &blk)
55
55
  terminal = nil
@@ -76,6 +76,8 @@ module RawLine
76
76
  height: terminal.height
77
77
  )
78
78
 
79
+ KeyBindings.default_terminal = terminal
80
+
79
81
  new(
80
82
  dom: dom,
81
83
  input: NonBlockingInput.new(input),
@@ -87,13 +89,15 @@ module RawLine
87
89
 
88
90
  class Environment
89
91
  attr_accessor :keys, :completion_class, :history, :word_separator
92
+ attr_accessor :keyboard_input_processors
90
93
 
91
94
  # * <tt>@history_size</tt> - the size of the editor history buffer (30).
92
95
  # * <tt>@keys</tt> - the keys (arrays of character codes) bound to specific actions.
93
96
  # * <tt>@line_history_size</tt> - the size of the editor line history buffer (50).
94
97
  def initialize(env: nil)
95
98
  @env = env
96
- @keys = {}
99
+ @keys = KeyBindings.new
100
+ @keyboard_input_processors = []
97
101
 
98
102
  @completion_class = Completer
99
103
 
@@ -139,7 +143,7 @@ module RawLine
139
143
  @match_hidden_files = false
140
144
  set_default_keys
141
145
  @add_history = false
142
- @keyboard_input_processors = [self]
146
+ push_keyboard_input_processor self
143
147
  yield self if block_given?
144
148
  update_word_separator
145
149
  @char = nil
@@ -154,11 +158,14 @@ module RawLine
154
158
  def env ; @env_stack.last ; end
155
159
  def new_env ; Environment.new ; end
156
160
  def push_env(env) ; @env_stack.push env ; end
157
- def pop_env(env) ; @env_stack.pop ; end
161
+ def pop_env ; @env_stack.pop ; end
162
+ def keyboard_input_processor ; env.keyboard_input_processors.last ; end
163
+ def push_keyboard_input_processor(kip) ; env.keyboard_input_processors.push kip ; end
164
+ def pop_keyboard_input_processor ; env.keyboard_input_processors.pop ; end
158
165
 
159
- def completion_class ; env.completion_class ; end
160
- def history ; env.history ; end
161
- def keys ; env.keys ; end
166
+ def completion_class ; @env_stack.first.completion_class ; end
167
+ def history ; @env_stack.first.history ; end
168
+ def keys ; @env_stack.first.keys ; end
162
169
 
163
170
  #
164
171
  # Return the current RawLine version
@@ -174,10 +181,12 @@ module RawLine
174
181
  def prompt=(text)
175
182
  return if @line && @line.prompt == text
176
183
  @prompt = Prompt.new(text)
184
+ Treefell['editor'].puts "prompt=#{prompt.inspect}"
177
185
  @dom.prompt_box.content = @prompt
178
186
  end
179
187
 
180
188
  def redraw_prompt
189
+ Treefell['editor'].puts "redrawing prompt=#{prompt.inspect} reset=true"
181
190
  render(reset: true)
182
191
  end
183
192
 
@@ -185,6 +194,7 @@ module RawLine
185
194
  def terminal_height ; @terminal.height ; end
186
195
 
187
196
  def content_box ; @dom.content_box ; end
197
+ def focused_input_box ; @dom.focused_input_box ; end
188
198
  def input_box ; @dom.input_box ; end
189
199
  def prompt_box ; @dom.prompt_box ; end
190
200
 
@@ -201,11 +211,14 @@ module RawLine
201
211
  at_exit { @terminal.cooked! }
202
212
 
203
213
  Signal.trap("SIGWINCH") do
214
+ Treefell['editor'].puts "terminal-resized trap=SIGWINCH"
204
215
  @event_loop.add_event name: "terminal-resized", source: self
205
216
  end
206
217
 
207
218
  @event_registry.subscribe("terminal-resized") do
208
- @renderer.update_dimensions(width: terminal_width, height: terminal_height)
219
+ width, height = terminal_width, terminal_height
220
+ Treefell['editor'].puts "terminal-resizing width=#{width} height=#{height}"
221
+ @renderer.update_dimensions(width: width, height: height)
209
222
  @event_loop.add_event name: "render", source: self
210
223
  end
211
224
 
@@ -232,7 +245,7 @@ module RawLine
232
245
  def check_for_keyboard_input
233
246
  bytes = @input.read
234
247
  if bytes.any?
235
- @keyboard_input_processors.last.read_bytes(bytes)
248
+ keyboard_input_processor.read_bytes(bytes)
236
249
  end
237
250
  @event_loop.add_event name: 'check_for_keyboard_input', source: self
238
251
  end
@@ -244,28 +257,32 @@ module RawLine
244
257
  key_code_sequences = parse_key_code_sequences(bytes)
245
258
  key_code_sequences.each do |sequence|
246
259
  @char = sequence
247
- process_character
248
-
249
- new_position = @line.position
250
-
251
260
  if @char == @terminal.keys[:enter] || !@char
252
261
  process_line
262
+ else
263
+ process_character
264
+ new_position = @line.position
253
265
  end
254
266
  end
255
267
  end
256
268
 
257
269
  def process_line
258
270
  @event_loop.add_event(name: "process_line", source: self) do
271
+ add_to_history
272
+
259
273
  @terminal.snapshot_tty_attrs
260
274
  @terminal.pseudo_cooked!
261
275
 
262
- move_to_beginning_of_input
276
+ @terminal.move_to_beginning_of_row
263
277
  @terminal.puts
264
278
  end
265
279
 
266
- @event_loop.add_event name: "line_read", source: self, payload: { line: @line.text.without_ansi.dup }
280
+ @event_loop.add_event(name: "line_read", source: self, payload: { line: @line.text.without_ansi.dup })
281
+ @event_loop.add_event(name: "prepare_new_line", source: self) do
282
+ move_to_beginning_of_input
283
+ end
267
284
  @event_loop.add_event(name: "restore_tty_attrs", source: self) { @terminal.restore_tty_attrs }
268
- @event_loop.add_event name: "render", source: self, payload: { reset: true }
285
+ @event_loop.add_event(name: "render", source: self, payload: { reset: true })
269
286
  end
270
287
 
271
288
  #
@@ -299,59 +316,18 @@ module RawLine
299
316
  # * The value can be a Fixnum, a String or an Array.
300
317
  #
301
318
  def bind(key, &block)
302
- case key.class.to_s
303
- when 'Symbol' then
304
- raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys[key]
305
- keys[@terminal.keys[key]] = block
306
- when 'Array' then
307
- raise BindingException, "Unknown key or key sequence '#{key.join(", ")}' (#{key.class.to_s})" unless @terminal.keys.has_value? key
308
- keys[key] = block
309
- when 'Fixnum' then
310
- raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys.has_value? [key]
311
- keys[[key]] = block
312
- when 'String' then
313
- if key.length == 1 then
314
- keys[[key.ord]] = block
315
- else
316
- bind_hash({:"#{key}" => key}, block)
317
- end
318
- when 'Hash' then
319
- raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
320
- bind_hash(key, block)
321
- else
322
- raise BindingException, "Unable to bind '#{key.to_s}' (#{key.class.to_s})"
323
- end
324
- @terminal.update
319
+ keys.bind(key, &block)
325
320
  end
326
321
 
327
322
  def unbind(key)
328
- block = case key.class.to_s
329
- when 'Symbol' then
330
- keys.delete @terminal.keys[key]
331
- when 'Array' then
332
- keys.delete keys[key]
333
- when 'Fixnum' then
334
- keys.delete[[key]]
335
- when 'String' then
336
- if key.length == 1 then
337
- keys.delete([key.ord])
338
- else
339
- raise NotImplementedError, "This is no implemented yet. It needs to return the previously bound block"
340
- bind_hash({:"#{key}" => key}, block)
341
- end
342
- when 'Hash' then
343
- raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
344
- bind_hash(key, -> { })
345
- end
346
- @terminal.update
347
- block
323
+ keys.unbind(key)
348
324
  end
349
325
 
350
326
  #
351
327
  # Return true if the last character read via <tt>read</tt> is bound to an action.
352
328
  #
353
329
  def key_bound?
354
- keys[@char] ? true : false
330
+ keys.bound?(@char)
355
331
  end
356
332
 
357
333
  #
@@ -403,14 +379,14 @@ module RawLine
403
379
  # This action is bound to ctrl+k by default.
404
380
  #
405
381
  def clear_line
382
+ Treefell['editor'].puts "clear_line"
406
383
  add_to_line_history
407
- @line.text = ""
408
- @line.position = 0
409
- @dom.input_box.position = @line.position
384
+ @line_editor.clear_line
410
385
  history.clear_position
411
386
  end
412
387
 
413
388
  def clear_screen
389
+ Treefell['editor'].puts "clear_screen"
414
390
  @terminal.clear_screen
415
391
  render(reset: true)
416
392
  end
@@ -422,21 +398,19 @@ module RawLine
422
398
  # This action is bound to the backspace key by default.
423
399
  #
424
400
  def delete_left_character(no_line_history=false)
425
- if move_left then
426
- delete_character(no_line_history)
401
+ Treefell['editor'].puts "delete_left_character"
402
+ if @line_editor.delete_left_character
403
+ add_to_line_history unless no_line_history
404
+ history.clear_position
427
405
  end
428
406
  end
429
407
 
430
408
  def delete_n_characters(number_of_characters_to_delete, no_line_history=false)
431
- number_of_characters_to_delete.times do |n|
432
- @line[@line.position] = ''
433
- @line.left
409
+ Treefell['editor'].puts "delete_n_characters n=#{number_of_characters_to_delete}"
410
+ if @line_editor.delete_n_characters(number_of_characters_to_delete)
411
+ add_to_line_history unless no_line_history
412
+ history.clear_position
434
413
  end
435
-
436
- @dom.input_box.position = @line.position
437
- @dom.input_box.content = @line.text
438
- add_to_line_history unless no_line_history
439
- history.clear_position
440
414
  end
441
415
 
442
416
  #
@@ -446,37 +420,54 @@ module RawLine
446
420
  # This action is bound to the delete key by default.
447
421
  #
448
422
  def delete_character(no_line_history=false)
449
- unless @line.position > @line.eol
450
- # save characters to shift
451
- chars = (@line.eol?) ? ' ' : select_characters_from_cursor(1)
452
- #remove character from line
453
- @line[@line.position] = ''
454
- @dom.input_box.content = @line.text
455
- @dom.input_box.position = @line.position
423
+ Treefell['editor'].puts "delete_character"
424
+ if @line_editor.delete_character
456
425
  add_to_line_history unless no_line_history
457
426
  history.clear_position
458
427
  end
459
428
  end
460
429
 
461
430
  def highlight_text_up_to(text, position)
462
- ANSIString.new("\e[1m#{text[0...position]}\e[0m#{text[position..-1]}")
431
+ Treefell['editor'].puts "highlight_text_up_to text=#{text} position=#{position}"
432
+ @line_editor.highlight_text_up_to(text, position)
433
+ end
434
+
435
+ #
436
+ # Inserts a string at the current line position, shifting characters
437
+ # to right if necessary.
438
+ #
439
+ def insert(string, add_to_line_history: true)
440
+ Treefell['editor'].puts "insert string=#{string} add_to_line_history=#{add_to_line_history}"
441
+ if @line_editor.insert(string)
442
+ self.add_to_line_history if add_to_line_history
443
+ end
463
444
  end
464
445
 
465
446
  def kill_forward
466
- killed_text = @line.text[@line.position..-1]
467
- @line.text[@line.position..-1] = ANSIString.new("")
468
- @dom.input_box.content = line.text
469
- @dom.input_box.position = @line.position
470
- history.clear_position
471
- killed_text
447
+ Treefell['editor'].puts "kill_forward"
448
+ @line_editor.kill_forward.tap do
449
+ add_to_line_history(allow_empty: true)
450
+ history.clear_position
451
+ end
452
+ end
453
+
454
+ #
455
+ # Write a string starting from the cursor position ovewriting any character
456
+ # at the current position if necessary.
457
+ #
458
+ def write(string, add_to_line_history: true)
459
+ Treefell['editor'].puts "write string=#{string}"
460
+ if @line_editor.write(string)
461
+ self.add_to_line_history if add_to_line_history
462
+ end
472
463
  end
473
464
 
474
465
  def yank_forward(text)
475
- @line.text[line.position] = text
476
- @line.position = line.position + text.length
477
- @dom.input_box.content = line.text
478
- @dom.input_box.position = @line.position
479
- history.clear_position
466
+ Treefell['editor'].puts "yank_forward"
467
+ @line_editor.yank_forward(text).tap do
468
+ add_to_line_history
469
+ history.clear_position
470
+ end
480
471
  end
481
472
 
482
473
  #
@@ -485,12 +476,8 @@ module RawLine
485
476
  # This action is bound to the left arrow key by default.
486
477
  #
487
478
  def move_left
488
- unless @line.bol? then
489
- @line.left
490
- @dom.input_box.position = @line.position
491
- return true
492
- end
493
- false
479
+ Treefell['editor'].puts "move_left"
480
+ @line_editor.move_left
494
481
  end
495
482
 
496
483
  #
@@ -500,28 +487,25 @@ module RawLine
500
487
  # This action is bound to the right arrow key by default.
501
488
  #
502
489
  def move_right
503
- unless @line.position > @line.eol then
504
- @line.right
505
- @dom.input_box.position = @line.position
506
- return true
507
- end
508
- false
490
+ Treefell['editor'].puts "move_right"
491
+ @line_editor.move_right
509
492
  end
510
493
 
511
494
  def move_to_beginning_of_input
512
- @line.position = @line.bol
513
- @dom.input_box.position = @line.position
495
+ Treefell['editor'].puts "move_to_beginning_of_input"
496
+ @line_editor.move_to_beginning_of_input
514
497
  end
515
498
 
516
499
  def move_to_end_of_input
517
- @line.position = @line.length
518
- @dom.input_box.position = @line.position
500
+ Treefell['editor'].puts "move_to_end_of_input"
501
+ @line_editor.move_to_end_of_input
519
502
  end
520
503
 
521
504
  #
522
505
  # Move the cursor to <tt>pos</tt>.
523
506
  #
524
507
  def move_to_position(pos)
508
+ Treefell['editor'].puts "move_to_position position=#{pos}"
525
509
  rows_to_move = current_terminal_row - terminal_row_for_line_position(pos)
526
510
  if rows_to_move > 0
527
511
  # rows_to_move.times { @output.print @terminal.term_info.control_string("cuu1") }
@@ -533,20 +517,7 @@ module RawLine
533
517
  column = (@line.prompt.length + pos) % terminal_width
534
518
  # @output.print @terminal.term_info.control_string("hpa", column)
535
519
  # @terminal.move_to_column((@line.prompt.length + pos) % terminal_width)
536
- @line.position = pos
537
- @dom.input_box.position = @line.position
538
- end
539
-
540
- def move_to_end_of_line
541
- rows_to_move_down = number_of_terminal_rows - current_terminal_row
542
- # rows_to_move_down.times { @output.print @terminal.term_info.control_string("cud1") }
543
- # @terminal.move_down_n_rows rows_to_move_down
544
- @line.position = @line.length
545
- @dom.input_box.position = @line.position
546
-
547
- column = (@line.prompt.length + @line.position) % terminal_width
548
- # @output.print @terminal.term_info.control_string("hpa", column)
549
- # @terminal.move_to_column((@line.prompt.length + @line.position) % terminal_width)
520
+ @line_editor.position = pos
550
521
  end
551
522
 
552
523
  #
@@ -555,22 +526,14 @@ module RawLine
555
526
  # <tt>position</tt>.
556
527
  #
557
528
  def overwrite_line(new_line, position=nil, options={})
558
- text = @line.text
559
- @highlighting = false
560
-
561
- if options[:highlight_up_to]
562
- @highlighting = true
563
- new_line = highlight_text_up_to(new_line, options[:highlight_up_to])
529
+ Treefell['editor'].puts "overwrite_line new_line=#{new_line} position=#{position} options=#{options.inspect}"
530
+ if @line_editor.overwrite_line(new_line, position, options)
531
+ @event_loop.add_event name: "render", source: focused_input_box
564
532
  end
565
-
566
- @line.position = position || new_line.length
567
- @line.text = new_line
568
- @dom.input_box.content = @line.text
569
- @dom.input_box.position = @line.position
570
- @event_loop.add_event name: "render", source: @dom.input_box
571
533
  end
572
534
 
573
535
  def reset_line
536
+ Treefell['editor'].puts "reset_line"
574
537
  initialize_line
575
538
  render(reset: true)
576
539
  end
@@ -599,32 +562,6 @@ module RawLine
599
562
  render(reset: true)
600
563
  end
601
564
 
602
- #
603
- # Inserts a string at the current line position, shifting characters
604
- # to right if necessary.
605
- #
606
- def insert(string, add_to_line_history: true)
607
- @line.text.insert @line.position, string
608
- string.length.times { @line.right }
609
- @dom.input_box.position = @line.position
610
- @dom.input_box.content = @line.text
611
-
612
- self.add_to_line_history if add_to_line_history
613
- end
614
-
615
- #
616
- # Write a string starting from the cursor position ovewriting any character
617
- # at the current position if necessary.
618
- #
619
- def write(string, add_to_line_history: true)
620
- @line.text[@line.position] = string
621
- string.length.times { @line.right }
622
- @dom.input_box.position = @line.position
623
- @dom.input_box.content = @line.text
624
-
625
- self.add_to_line_history if add_to_line_history
626
- end
627
-
628
565
  ############################################################################
629
566
  #
630
567
  # COMPLETION
@@ -643,7 +580,7 @@ module RawLine
643
580
  # pressed again.
644
581
  #
645
582
  def complete
646
- @dom.input_box.cursor_off
583
+ focused_input_box.cursor_off
647
584
  completer = completion_class.new(
648
585
  char: @char,
649
586
  line: @line,
@@ -660,15 +597,15 @@ module RawLine
660
597
  done: -> (*leftover_bytes){
661
598
  completion_done
662
599
  leftover_bytes = leftover_bytes.flatten
663
- @keyboard_input_processors.pop
600
+ pop_keyboard_input_processor
664
601
  if leftover_bytes.any?
665
- @keyboard_input_processors.last.read_bytes(leftover_bytes)
602
+ keyboard_input_processor.read_bytes(leftover_bytes)
666
603
  end
667
- @dom.input_box.cursor_on
604
+ focused_input_box.cursor_on
668
605
  },
669
606
  keys: terminal.keys
670
607
  )
671
- @keyboard_input_processors.push(completer)
608
+ push_keyboard_input_processor(completer)
672
609
  completer.read_bytes(@char)
673
610
  end
674
611
 
@@ -826,15 +763,21 @@ module RawLine
826
763
  # line history, to allow undo/redo
827
764
  # operations.
828
765
  #
829
- def add_to_line_history
830
- @line.history << @line.text.dup unless @line.text == ""
766
+ def add_to_line_history(allow_empty: false)
767
+ if allow_empty || !@line.text.empty?
768
+ Treefell['editor'].puts "add_to_line_history text=#{@line.text}"
769
+ @line.history << @line.text.dup
770
+ end
831
771
  end
832
772
 
833
773
  #
834
774
  # Add the current line (<tt>@line.text</tt>) to the editor history.
835
775
  #
836
- def add_to_history
837
- history << @line.text.dup if @add_history && @line.text != ""
776
+ def add_to_history(allow_empty: false)
777
+ if @add_history && (allow_empty || !@line.text.empty?)
778
+ Treefell['editor'].puts "add_to_history text=#{@line.text}"
779
+ history << @line.text.dup
780
+ end
838
781
  end
839
782
 
840
783
  ############################################################################
@@ -861,6 +804,11 @@ module RawLine
861
804
  overwrite_line(text, pos)
862
805
  end
863
806
 
807
+ def focus_input_box(box)
808
+ @dom.focus_input_box(box)
809
+ @renderer.render_cursor
810
+ end
811
+
864
812
  private
865
813
 
866
814
  def initialize_events
@@ -870,14 +818,24 @@ module RawLine
870
818
  end
871
819
  @event_loop = Rawline::EventLoop.new(registry: @event_registry)
872
820
 
821
+ @dom.on(:content_changed) do |*args|
822
+ Treefell['editor'].puts 'DOM content changed, re-rendering'
823
+ @event_loop.add_event name: "render"
824
+ end
825
+
873
826
  @dom.on(:child_changed) do |*args|
874
827
  Treefell['editor'].puts 'DOM child changed, re-rendering'
875
- @event_loop.add_event name: "render", source: @dom#, target: event[:target]
828
+ @event_loop.add_event name: "render"
876
829
  end
877
830
 
878
831
  @dom.on :position_changed do |*args|
879
832
  Treefell['editor'].puts 'DOM position changed, rendering cursor'
880
- @renderer.render_cursor(@dom.input_box)
833
+ @renderer.render
834
+ end
835
+
836
+ @dom.on :focus_changed do |*args|
837
+ Treefell['editor'].puts 'DOM focused changed, re-rendering'
838
+ @renderer.render
881
839
  end
882
840
 
883
841
  @event_registry.subscribe :render, -> (_) { render(reset: false) }
@@ -889,13 +847,17 @@ module RawLine
889
847
  end
890
848
 
891
849
  def initialize_line
892
- @dom.input_box.content = ""
850
+ focused_input_box.content = ""
893
851
  update_word_separator
894
852
  @add_history = true
895
853
  @line = env.initialize_line do |l|
896
854
  l.prompt = @dom.prompt_box.content
897
855
  l.word_separator = @word_separator
898
856
  end
857
+ @line_editor = LineEditor.new(
858
+ @line,
859
+ sync_with: -> { focused_input_box }
860
+ )
899
861
  add_to_line_history
900
862
  @allow_prompt_updates = true
901
863
  end
@@ -911,32 +873,10 @@ module RawLine
911
873
  @word_separator = /(?<!\\)[#{chars.join}]/
912
874
  end
913
875
 
914
- def bind_hash(key, block)
915
- key.each_pair do |j,k|
916
- 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)
917
- code = []
918
- case k.class.to_s
919
- when 'Fixnum' then
920
- code = [k]
921
- when 'String' then
922
- k.each_byte { |b| code << b }
923
- when 'Array' then
924
- code = k
925
- else
926
- raise BindingException, "Unable to bind '#{k.to_s}' (#{k.class.to_s})"
927
- end
928
- @terminal.keys[j] = code
929
- keys[code] = block
930
- end
931
- end
932
-
933
- def select_characters_from_cursor(offset=0)
934
- select_characters(:right, @line.length-@line.position, offset)
935
- end
936
-
937
876
  def generic_history_back(history)
938
877
  unless history.empty?
939
878
  history.back
879
+ Treefell['editor'].puts "generic_history_back position=#{history.position} history=#{history.to_a.inspect}"
940
880
  line = history.get
941
881
  return unless line
942
882
 
@@ -947,6 +887,7 @@ module RawLine
947
887
 
948
888
  def generic_history_forward(history)
949
889
  if history.forward
890
+ Treefell['editor'].puts "generic_history_back position=#{history.position} history=#{history.to_a.inspect}"
950
891
  line = history.get
951
892
  return unless line
952
893
 
@@ -955,14 +896,6 @@ module RawLine
955
896
  end
956
897
  end
957
898
 
958
- def select_characters(direction, n, offset=0)
959
- if direction == :right then
960
- @line.text[@line.position+offset..@line.position+offset+n]
961
- elsif direction == :left then
962
- @line.text[@line.position-offset-n..@line.position-offset]
963
- end
964
- end
965
-
966
899
  def set_default_keys
967
900
  bind(:space) { insert(' ') }
968
901
  bind(:enter) { newline }
@@ -0,0 +1,109 @@
1
+ module RawLine
2
+ class KeyBindings
3
+ attr_reader :keys
4
+
5
+ class << self
6
+ attr_accessor :default_terminal
7
+ end
8
+
9
+ def initialize(terminal: self.class.default_terminal)
10
+ @terminal = terminal
11
+ @keys = {}
12
+ end
13
+
14
+ def [](char)
15
+ keys[char]
16
+ end
17
+
18
+ #
19
+ # Bind a key to an action specified via <tt>block</tt>.
20
+ # <tt>key</tt> can be:
21
+ #
22
+ # * A Symbol identifying a character or character sequence defined for the current terminal
23
+ # * A Fixnum identifying a character defined for the current terminal
24
+ # * An Array identifying a character or character sequence defined for the current terminal
25
+ # * A String identifying a character or character sequence, even if it is not defined for the current terminal
26
+ # * An Hash identifying a character or character sequence, even if it is not defined for the current terminal
27
+ #
28
+ # If <tt>key</tt> is a hash, then:
29
+ #
30
+ # * It must contain only one key/value pair
31
+ # * The key identifies the name of the character or character sequence
32
+ # * The value identifies the code(s) corresponding to the character or character sequence
33
+ # * The value can be a Fixnum, a String or an Array.
34
+ #
35
+ def bind(key, &block)
36
+ case key.class.to_s
37
+ when 'Symbol' then
38
+ raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys[key]
39
+ keys[@terminal.keys[key]] = block
40
+ when 'Array' then
41
+ raise BindingException, "Unknown key or key sequence '#{key.join(", ")}' (#{key.class.to_s})" unless @terminal.keys.has_value? key
42
+ keys[key] = block
43
+ when 'Fixnum' then
44
+ raise BindingException, "Unknown key or key sequence '#{key.to_s}' (#{key.class.to_s})" unless @terminal.keys.has_value? [key]
45
+ keys[[key]] = block
46
+ when 'String' then
47
+ if key.length == 1 then
48
+ keys[[key.ord]] = block
49
+ else
50
+ bind_hash({:"#{key}" => key}, block)
51
+ end
52
+ when 'Hash' then
53
+ raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
54
+ bind_hash(key, block)
55
+ else
56
+ raise BindingException, "Unable to bind '#{key.to_s}' (#{key.class.to_s})"
57
+ end
58
+ @terminal.update
59
+ end
60
+
61
+ def bound?(char)
62
+ keys[char] ? true : false
63
+ end
64
+
65
+ def unbind(key)
66
+ block = case key.class.to_s
67
+ when 'Symbol' then
68
+ keys.delete @terminal.keys[key]
69
+ when 'Array' then
70
+ keys.delete keys[key]
71
+ when 'Fixnum' then
72
+ keys.delete[[key]]
73
+ when 'String' then
74
+ if key.length == 1 then
75
+ keys.delete([key.ord])
76
+ else
77
+ raise NotImplementedError, "This is no implemented yet. It needs to return the previously bound block"
78
+ bind_hash({:"#{key}" => key}, block)
79
+ end
80
+ when 'Hash' then
81
+ raise BindingException, "Cannot bind more than one key or key sequence at once" unless key.values.length == 1
82
+ bind_hash(key, -> { })
83
+ end
84
+ @terminal.update
85
+ block
86
+ end
87
+
88
+ private
89
+
90
+ def bind_hash(key, block)
91
+ key.each_pair do |j,k|
92
+ 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)
93
+ code = []
94
+ case k.class.to_s
95
+ when 'Fixnum' then
96
+ code = [k]
97
+ when 'String' then
98
+ k.each_byte { |b| code << b }
99
+ when 'Array' then
100
+ code = k
101
+ else
102
+ raise BindingException, "Unable to bind '#{k.to_s}' (#{k.class.to_s})"
103
+ end
104
+ @terminal.keys[j] = code
105
+ keys[code] = block
106
+ end
107
+ end
108
+ end
109
+ end
data/lib/rawline/line.rb CHANGED
@@ -32,7 +32,7 @@ module RawLine
32
32
  # * <tt>@position</tt> - the current cursor position within the line.
33
33
  # * <tt>@prompt</tt> - a prompt to prepend to the line text.
34
34
  #
35
- def initialize(history_size)
35
+ def initialize(history_size=30)
36
36
  @text = ANSIString.new("")
37
37
  @history_size = history_size
38
38
  @position = 0
@@ -0,0 +1,194 @@
1
+ module RawLine
2
+ class LineEditor
3
+ def initialize(line, sync_with: -> {})
4
+ @line = line
5
+ @sync_with_proc = sync_with
6
+ end
7
+
8
+ #
9
+ # Clear the current line, i.e.
10
+ # <tt>@line.text</tt> and <tt>@line.position</tt>.
11
+ # This action is bound to ctrl+k by default.
12
+ #
13
+ def clear_line
14
+ @line.text = ""
15
+ @line.position = 0
16
+ sync!
17
+ true
18
+ end
19
+
20
+ #
21
+ # Delete the character at the left of the cursor.
22
+ # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
23
+ # recorded in the line history.
24
+ # This action is bound to the backspace key by default.
25
+ #
26
+ def delete_left_character
27
+ if move_left then
28
+ delete_character
29
+ sync!
30
+ return true
31
+ end
32
+ false
33
+ end
34
+
35
+ def delete_n_characters(number_of_characters_to_delete)
36
+ number_of_characters_to_delete.times do |n|
37
+ @line[@line.position] = ''
38
+ @line.left
39
+ end
40
+ sync!
41
+ true
42
+ end
43
+
44
+ #
45
+ # Delete the character under the cursor.
46
+ # If <tt>no_line_hisytory</tt> is set to true, the deletion won't be
47
+ # recorded in the line history.
48
+ # This action is bound to the delete key by default.
49
+ #
50
+ def delete_character(no_line_history=false)
51
+ unless @line.position > @line.eol
52
+ # save characters to shift
53
+ chars = (@line.eol?) ? ' ' : select_characters_from_cursor(1)
54
+ #remove character from line
55
+ @line[@line.position] = ''
56
+ sync!
57
+ return true
58
+ end
59
+ false
60
+ end
61
+
62
+ def highlight_text_up_to(text, position)
63
+ ANSIString.new("\e[1m#{text[0...position]}\e[0m#{text[position..-1]}")
64
+ end
65
+
66
+ #
67
+ # Inserts a string at the current line position, shifting characters
68
+ # to right if necessary.
69
+ #
70
+ def insert(string)
71
+ return false if string.empty?
72
+ @line.text.insert @line.position, string
73
+ string.length.times { @line.right }
74
+ sync!
75
+ true
76
+ end
77
+
78
+ def kill_forward
79
+ @line.text[@line.position..-1].tap do
80
+ @line.text[@line.position..-1] = ANSIString.new("")
81
+ sync!
82
+ end
83
+ end
84
+
85
+ #
86
+ # Write a string starting from the cursor position ovewriting any character
87
+ # at the current position if necessary.
88
+ #
89
+ def write(string)
90
+ if @line.eol?
91
+ @line.text[@line.position] = string
92
+ else
93
+ @line.text << string
94
+ end
95
+ string.length.times { @line.right }
96
+ sync!
97
+ true
98
+ end
99
+
100
+ def yank_forward(text)
101
+ @line.text[@line.position...@line.position] = text
102
+ @line.position = @line.position + text.length
103
+ sync!
104
+ @line.text
105
+ end
106
+
107
+ #
108
+ # Move the cursor left (if possible) by printing a
109
+ # backspace, updating <tt>@line.position</tt> accordingly.
110
+ # This action is bound to the left arrow key by default.
111
+ #
112
+ def move_left
113
+ unless @line.bol? then
114
+ @line.left
115
+ sync!
116
+ return true
117
+ end
118
+ false
119
+ end
120
+
121
+ def move_right
122
+ unless @line.position > @line.eol then
123
+ @line.right
124
+ sync!
125
+ return true
126
+ end
127
+ false
128
+ end
129
+
130
+ def move_to_beginning_of_input
131
+ @line.position = @line.bol
132
+ sync!
133
+ true
134
+ end
135
+
136
+ def move_to_end_of_input
137
+ @line.position = @line.length
138
+ sync!
139
+ true
140
+ end
141
+
142
+ #
143
+ # Overwrite the current line (<tt>@line.text</tt>)
144
+ # with <tt>new_line</tt>, and optionally reset the cursor position to
145
+ # <tt>position</tt>.
146
+ #
147
+ def overwrite_line(new_line, position=nil, options={})
148
+ text = @line.text
149
+ @highlighting = false
150
+
151
+ if options[:highlight_up_to]
152
+ @highlighting = true
153
+ new_line = highlight_text_up_to(new_line, options[:highlight_up_to])
154
+ end
155
+
156
+ @line.position = position || new_line.length
157
+ @line.text = new_line
158
+ sync!
159
+ true
160
+ end
161
+
162
+ def position=(position)
163
+ @line.position = position
164
+ sync!
165
+ end
166
+
167
+ def text
168
+ @line.text
169
+ end
170
+
171
+ private
172
+
173
+ def select_characters_from_cursor(offset=0)
174
+ select_characters(:right, @line.length-@line.position, offset)
175
+ end
176
+
177
+ def select_characters(direction, n, offset=0)
178
+ if direction == :right then
179
+ @line.text[@line.position+offset..@line.position+offset+n]
180
+ elsif direction == :left then
181
+ @line.text[@line.position-offset-n..@line.position-offset]
182
+ end
183
+ end
184
+
185
+ def sync!
186
+ positionable_object = @sync_with_proc.call
187
+ if positionable_object
188
+ positionable_object.position = @line.position
189
+ positionable_object.content = @line.text
190
+ end
191
+ end
192
+
193
+ end
194
+ end
@@ -17,8 +17,8 @@ module RawLine
17
17
  @renderer.render(@render_tree, reset: reset)
18
18
  end
19
19
 
20
- def render_cursor(input_box)
21
- @renderer.render_cursor(input_box)
20
+ def render_cursor
21
+ @renderer.render_cursor(@dom.focused_input_box)
22
22
  end
23
23
 
24
24
  def update_dimensions(width:, height:)
@@ -129,6 +129,10 @@ module RawLine
129
129
  term_info.control "el1"
130
130
  end
131
131
 
132
+ def clear_to_end_of_line
133
+ term_info.control "el"
134
+ end
135
+
132
136
  def clear_screen
133
137
  term_info.control "clear"
134
138
  end
@@ -137,6 +141,14 @@ module RawLine
137
141
  term_info.control "ed"
138
142
  end
139
143
 
144
+ def hide_cursor
145
+ term_info.control_string "civis"
146
+ end
147
+
148
+ def show_cursor
149
+ term_info.control_string "cnorm"
150
+ end
151
+
140
152
  def move_to_beginning_of_row
141
153
  move_to_column 0
142
154
  end
@@ -1,3 +1,3 @@
1
1
  module RawLine
2
- VERSION = "0.3.5"
2
+ VERSION = "0.4.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: yap-rawline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Fabio Cevasco
@@ -37,14 +37,14 @@ dependencies:
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: 0.3.0
40
+ version: 0.4.0
41
41
  type: :runtime
42
42
  prerelease: false
43
43
  version_requirements: !ruby/object:Gem::Requirement
44
44
  requirements:
45
45
  - - "~>"
46
46
  - !ruby/object:Gem::Version
47
- version: 0.3.0
47
+ version: 0.4.0
48
48
  - !ruby/object:Gem::Dependency
49
49
  name: term-ansicolor
50
50
  requirement: !ruby/object:Gem::Requirement
@@ -110,8 +110,10 @@ files:
110
110
  - lib/rawline/event_loop.rb
111
111
  - lib/rawline/event_registry.rb
112
112
  - lib/rawline/history_buffer.rb
113
+ - lib/rawline/key_bindings.rb
113
114
  - lib/rawline/keycode_parser.rb
114
115
  - lib/rawline/line.rb
116
+ - lib/rawline/line_editor.rb
115
117
  - lib/rawline/non_blocking_input.rb
116
118
  - lib/rawline/prompt.rb
117
119
  - lib/rawline/renderer.rb