yap-rawline 0.3.5 → 0.4.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.
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