yap-rawline 0.1.1 → 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.
@@ -5,6 +5,7 @@ module RawLine
5
5
  def initialize(registry:)
6
6
  @registry = registry
7
7
  @events = []
8
+ @counter = 0
8
9
  end
9
10
 
10
11
  # event looks like:
@@ -12,46 +13,79 @@ module RawLine
12
13
  # * source
13
14
  # * target
14
15
  # * payload
15
- def add_event(**event)
16
+ def add_event(**event, &blk)
17
+ unless event.has_key?(:_event_callback)
18
+ event[:_event_callback] = blk if blk
19
+ end
20
+
21
+ unless event.has_key?(:_event_id)
22
+ @counter += 1
23
+ event[:_event_id] = @counter
24
+ end
25
+
16
26
  # if the last event is the same as the incoming then do there is no
17
27
  # need to add it again. For example, rendering events that already
18
28
  # back can be squashed into a single event.
19
29
  if @events.last != event
20
30
  @events << event
31
+ event[:_event_id]
32
+ else
33
+ @events.last[:_event_id]
21
34
  end
22
35
  end
23
36
 
24
- def recur(event:nil, interval_in_ms:, &blk)
25
- if block_given?
26
- # TODO: implement
27
- elsif event
28
- add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) })
29
- else
30
- raise "Must pass in a block or an event."
31
- end
37
+ def clear(event_id)
38
+ @events = @events.reject { |event| event[:_event_id] == event_id }
32
39
  end
33
40
 
34
- def start
35
- loop do
36
- event = @events.shift
37
- if event
38
- recur = event[:recur]
39
- if recur
40
- if current_time_in_ms >= recur[:recur_at]
41
- dispatch_event(event)
42
- interval_in_ms = recur[:interval_in_ms]
43
- add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) } )
44
- else
45
- # put it back on the queue
46
- add_event event
47
- dispatch_event(default_event)
48
- end
41
+ def reset
42
+ @events.clear
43
+ @counter = 0
44
+ end
45
+
46
+ def once(interval_in_ms:, **event, &blk)
47
+ add_event event.merge(once: { run_at: recur_at(interval_in_ms) }), &blk
48
+ end
49
+
50
+ def recur(interval_in_ms:, **event, &blk)
51
+ add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) }), &blk
52
+ end
53
+
54
+ def tick
55
+ event = @events.shift
56
+ if event
57
+ recur = event[:recur]
58
+ once = event[:once]
59
+ if recur
60
+ if current_time_in_ms >= recur[:recur_at]
61
+ dispatch_event(event)
62
+ interval_in_ms = recur[:interval_in_ms]
63
+ add_event event.merge(recur: { interval_in_ms: interval_in_ms, recur_at: recur_at(interval_in_ms) } )
49
64
  else
65
+ # put it back on the queue
66
+ @events << event
67
+ dispatch_event(default_event)
68
+ end
69
+ elsif once
70
+ if current_time_in_ms >= once[:run_at]
50
71
  dispatch_event(event)
72
+ else
73
+ # put it back on the queue
74
+ @events << event
75
+ # add_event event
76
+ dispatch_event(default_event)
51
77
  end
52
78
  else
53
- dispatch_event(default_event)
79
+ dispatch_event(event)
54
80
  end
81
+ else
82
+ dispatch_event(default_event)
83
+ end
84
+ end
85
+
86
+ def start
87
+ loop do
88
+ tick
55
89
  end
56
90
  end
57
91
 
@@ -62,7 +96,7 @@ module RawLine
62
96
  end
63
97
 
64
98
  def default_event
65
- { name: 'default', source: self }
99
+ { name: 'default', source: self, _event_id: -1 }
66
100
  end
67
101
 
68
102
  def recur_at(interval_in_ms)
@@ -73,6 +107,9 @@ module RawLine
73
107
  @registry.subscribers_for_event(event[:name]).each do |subscriber|
74
108
  subscriber.call(event)
75
109
  end
110
+
111
+ callback = event[:_event_callback]
112
+ callback.call(event) if callback
76
113
  end
77
114
  end
78
115
  end
@@ -0,0 +1,34 @@
1
+ module RawLine
2
+ class NonBlockingInput
3
+ DEFAULT_WAIT_TIMEOUT_IN_SECONDS = 0.01
4
+
5
+ attr_accessor :wait_timeout_in_seconds
6
+
7
+ def initialize(input)
8
+ @input = input
9
+ restore_default_timeout
10
+ end
11
+
12
+ def restore_default_timeout
13
+ @wait_timeout_in_seconds = DEFAULT_WAIT_TIMEOUT_IN_SECONDS
14
+ end
15
+
16
+ def read
17
+ bytes = []
18
+ begin
19
+ file_descriptor_flags = @input.fcntl(Fcntl::F_GETFL, 0)
20
+ loop do
21
+ string = @input.read_nonblock(4096)
22
+ bytes.concat string.bytes
23
+ end
24
+ rescue IO::WaitReadable
25
+ # reset flags so O_NONBLOCK is turned off on the file descriptor
26
+ # if it was turned on during the read_nonblock above
27
+ retry if IO.select([@input], [], [], @wait_timeout_in_seconds)
28
+
29
+ @input.fcntl(Fcntl::F_SETFL, file_descriptor_flags)
30
+ end
31
+ bytes
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,29 @@
1
+ module RawLine
2
+ class Renderer
3
+ def initialize(dom:, output:, width:, height:)
4
+ @dom = dom
5
+ @output = output
6
+ @renderer = TerminalLayout::TerminalRenderer.new(output: output)
7
+ @render_tree = TerminalLayout::RenderTree.new(
8
+ dom,
9
+ parent: nil,
10
+ style: { width: width, height: height },
11
+ renderer: @renderer
12
+ )
13
+ end
14
+
15
+ def render(reset: false)
16
+ @render_tree.layout
17
+ @renderer.render(@render_tree, reset: reset)
18
+ end
19
+
20
+ def render_cursor(input_box)
21
+ @renderer.render_cursor(input_box)
22
+ end
23
+
24
+ def update_dimensions(width:, height:)
25
+ @render_tree.width = width
26
+ @render_tree.height = height
27
+ end
28
+ end
29
+ end
@@ -3,6 +3,7 @@
3
3
  require 'terminfo'
4
4
  require 'io/console'
5
5
  require 'ostruct'
6
+ require 'termios'
6
7
 
7
8
  #
8
9
  # terminal.rb
@@ -23,22 +24,25 @@ module RawLine
23
24
  # RawLine::Editor.
24
25
  #
25
26
  class Terminal
26
-
27
27
  include HighLine::SystemExtensions
28
28
 
29
- attr_accessor :escape_codes
29
+ attr_accessor :escape_codes, :input, :output
30
30
  attr_reader :keys, :escape_sequences
31
31
 
32
32
  #
33
33
  # Create an instance of RawLine::Terminal.
34
34
  #
35
- def initialize
35
+ def initialize(input, output)
36
+ @input = input
37
+ @output = output
38
+ @snapshotted_tty_attrs = []
36
39
  @keys =
37
40
  {
38
41
  :tab => [?\t.ord],
39
42
  :return => [?\r.ord],
40
43
  :newline => [?\n.ord],
41
44
  :escape => [?\e.ord],
45
+ :space => [32],
42
46
 
43
47
  :ctrl_a => [?\C-a.ord],
44
48
  :ctrl_b => [?\C-b.ord],
@@ -74,6 +78,39 @@ module RawLine
74
78
 
75
79
  CursorPosition = Struct.new(:column, :row)
76
80
 
81
+ def raw!
82
+ @input.raw!
83
+ end
84
+
85
+ def cooked!
86
+ @input.cooked!
87
+ end
88
+
89
+ def pseudo_cooked!
90
+ old_tty_attrs = Termios.tcgetattr(@input)
91
+ new_tty_attrs = old_tty_attrs.dup
92
+
93
+
94
+ new_tty_attrs.cflag |= Termios::BRKINT | Termios::ISTRIP | Termios::ICRNL | Termios::IXON
95
+
96
+ new_tty_attrs.iflag |= Termios::ICRNL | Termios::IGNBRK
97
+
98
+ new_tty_attrs.oflag |= Termios::OPOST
99
+
100
+ new_tty_attrs.lflag &= ~Termios::ECHONL
101
+ new_tty_attrs.lflag |= Termios::ECHO | Termios::ECHOE | Termios::ECHOK | Termios::ICANON | Termios::ISIG | Termios::IEXTEN
102
+
103
+ Termios::tcsetattr(@input, Termios::TCSANOW, new_tty_attrs)
104
+ end
105
+
106
+ def snapshot_tty_attrs
107
+ @snapshotted_tty_attrs << Termios.tcgetattr(@input)
108
+ end
109
+
110
+ def restore_tty_attrs
111
+ Termios::tcsetattr(@input, Termios::TCSANOW, @snapshotted_tty_attrs.pop)
112
+ end
113
+
77
114
  def cursor_position
78
115
  res = ''
79
116
  $stdin.raw do |stdin|
@@ -131,6 +168,12 @@ module RawLine
131
168
  n.times { term_info.control "cud1" }
132
169
  end
133
170
 
171
+ def puts(*args)
172
+ @output.cooked do
173
+ @output.puts(*args)
174
+ end
175
+ end
176
+
134
177
  def preserve_cursor(&blk)
135
178
  term_info.control "sc" # store cursor position
136
179
  blk.call
@@ -138,6 +181,18 @@ module RawLine
138
181
  term_info.control "rc" # restore cursor position
139
182
  end
140
183
 
184
+ def width
185
+ terminal_size[0]
186
+ end
187
+
188
+ def height
189
+ terminal_size[1]
190
+ end
191
+
192
+ def cursor_position
193
+ cursor_position
194
+ end
195
+
141
196
  #
142
197
  # Update the terminal escape sequences. This method is called automatically
143
198
  # by RawLine::Editor#bind().
@@ -17,8 +17,8 @@ module RawLine
17
17
  #
18
18
  class VT220Terminal < Terminal
19
19
 
20
- def initialize
21
- super
20
+ def initialize(*args)
21
+ super(*args)
22
22
  @escape_codes = [?\e.ord]
23
23
  @keys.merge!(
24
24
  {
data/spec/editor_spec.rb CHANGED
@@ -12,20 +12,62 @@ end
12
12
  require 'stringio'
13
13
  require_relative "../lib/rawline.rb"
14
14
 
15
- describe RawLine::Editor do
15
+ class DummyInput < RawLine::NonBlockingInput
16
+ def initialize
17
+ @input = StringIO.new
18
+ end
16
19
 
17
- before :each do
18
- @output = StringIO.new
20
+ def read
21
+ @input.read.bytes
22
+ end
23
+
24
+ def clear
19
25
  @input = StringIO.new
20
- @editor = RawLine::Editor.new(@input, @output)
21
26
  end
22
27
 
23
- it "reads raw characters from @input" do
24
- @input << "test #1"
28
+ def <<(bytes)
29
+ @input << bytes
30
+ end
31
+
32
+ def rewind
25
33
  @input.rewind
26
- @editor.read
34
+ end
35
+ end
36
+
37
+ describe RawLine::Editor do
38
+ let(:dom) { RawLine::DomTree.new }
39
+ let(:renderer) do
40
+ instance_double(RawLine::Renderer,
41
+ render_cursor: nil,
42
+ render: nil
43
+ )
44
+ end
45
+ let(:input) { DummyInput.new }
46
+ let(:terminal) do
47
+ output = double("IO", cooked: nil)
48
+ RawLine::VT220Terminal.new(input, output)
49
+ end
50
+
51
+ before do
52
+ @editor = RawLine::Editor.new(
53
+ dom: dom,
54
+ input: input,
55
+ renderer: renderer,
56
+ terminal: terminal
57
+ ) do |editor|
58
+ editor.prompt = ">"
59
+ end
60
+ @editor.on_read_line do |event|
61
+ line = event[:payload][:line]
62
+ end
63
+ end
64
+
65
+ it "reads raw characters from @input" do
66
+ input << "test #1"
67
+ input.rewind
68
+ @editor.event_loop.tick
27
69
  expect(@editor.line.text).to eq("test #1")
28
- expect(@output.string).to eq("test #1\n")
70
+ expect(@editor.dom.input_box.content).to eq("test #1")
29
71
  end
30
72
 
31
73
  it "can bind keys to code blocks" do
@@ -38,9 +80,9 @@ describe RawLine::Editor do
38
80
  @editor.terminal.escape_codes << ?\e.ord
39
81
  expect {@editor.bind({:test => "\etest"}) { "test #2e" }}.to_not raise_error
40
82
  expect {@editor.bind("\etest2") { "test #2f" }}.to_not raise_error
41
- @input << ?\C-w.chr
42
- @input.rewind
43
- @editor.read
83
+ input << ?\C-w
84
+ input.rewind
85
+ @editor.event_loop.tick
44
86
  expect(@editor.line.text).to eq("test #2a")
45
87
  @editor.char = [?\C-q.ord]
46
88
  expect(@editor.press_key).to eq("test #2b")
@@ -55,9 +97,9 @@ describe RawLine::Editor do
55
97
  end
56
98
 
57
99
  it "keeps track of the cursor position" do
58
- @input << "test #4"
59
- @input.rewind
60
- @editor.read
100
+ input << "test #4"
101
+ input.rewind
102
+ @editor.event_loop.tick
61
103
  expect(@editor.line.position).to eq(7)
62
104
  3.times { @editor.move_left }
63
105
  expect(@editor.line.position).to eq(4)
@@ -68,103 +110,110 @@ describe RawLine::Editor do
68
110
  describe "keeping track of the cursor position across terminal lines (e.g. multi-line editing)" do
69
111
  let(:terminal_width){ 3 }
70
112
  let(:terminal_height){ 7 }
71
- let(:output){ @output.rewind ; @output.read }
72
113
  let(:arrow_key_left_ansi){ "\e[D" }
73
114
  let(:arrow_key_right_ansi){ "\e[C" }
74
115
 
75
116
  before do
76
- allow(@editor).to receive(:terminal_size).and_return [terminal_width, terminal_height]
117
+ allow(@editor.terminal).to receive(:terminal_size).and_return [terminal_width, terminal_height]
77
118
  end
78
119
 
79
120
  context "and the cursor position is at the first character of the second line" do
80
121
  before do
81
- @input << "123"
122
+ input << "123"
123
+ input.rewind
82
124
  end
83
125
 
84
126
  it "is at the first character of a second line" do
85
- @input.rewind
86
- @editor.read
127
+ @editor.event_loop.tick
87
128
  expect(@editor.line.position).to eq(3)
88
129
  end
89
130
 
90
131
  describe "moving left from the first position of the second line" do
91
- before do
92
- @input << arrow_key_left_ansi
93
- @input.rewind
94
- @editor.read
95
- end
132
+ it "moves the line and cursor position to left by 1 character" do
133
+ @editor.event_loop.tick
134
+ expect(@editor.line.position).to eq(3)
135
+ expect(@editor.input_box.cursor_position.x).to eq(3)
96
136
 
97
- it "sends the escape sequences moving the cursor to the end of the previous line" do
98
- expected_ansi_sequence = "\e[A\e[#{terminal_width}C"
99
- expect(output).to eq("123#{expected_ansi_sequence}\n")
100
- end
137
+ input.clear
138
+ input << arrow_key_left_ansi
139
+ input.rewind
140
+
141
+ @editor.event_loop.reset
142
+ @editor.event_loop.tick
101
143
 
102
- it "correctly sets the line's position" do
103
144
  expect(@editor.line.position).to eq(2)
145
+ expect(@editor.input_box.cursor_position.x).to eq(2)
104
146
  end
105
147
  end
106
148
 
107
149
  describe "moving right from the first position of the second line" do
108
- before do
109
- @input << arrow_key_right_ansi
110
- @input.rewind
111
- @editor.read
112
- end
150
+ it "doesnt move the line and cursor position" do
151
+ @editor.event_loop.tick
152
+ expect(@editor.line.position).to eq(3)
153
+ expect(@editor.input_box.cursor_position.x).to eq(3)
113
154
 
114
- it "doesn't send any escape sequences" do
115
- expected_ansi_sequence = "\e[A\e[#{terminal_width}C"
116
- expect(output).to_not include("\e")
117
- end
155
+ input.clear
156
+ input << arrow_key_right_ansi
157
+ input.rewind
158
+
159
+ @editor.event_loop.reset
160
+ @editor.event_loop.tick
118
161
 
119
- it "doesn't move the cursor when it's at the end of the input" do
120
162
  expect(@editor.line.position).to eq(3)
163
+ expect(@editor.input_box.cursor_position.x).to eq(3)
121
164
  end
122
165
  end
123
166
 
124
167
  describe "moving left to the previous line then right to the next line" do
125
168
  before do
169
+ @editor.event_loop.tick
170
+
126
171
  # this is the one that moves us to the previous line
127
- @input << arrow_key_left_ansi
172
+ input.clear
173
+ input << arrow_key_left_ansi
174
+ input.rewind
175
+ @editor.event_loop.reset
176
+ @editor.event_loop.tick
128
177
 
129
178
  # these are for fun to show that we don't generate unnecessary
130
179
  # escape sequences
131
- @input << arrow_key_left_ansi
132
- @input << arrow_key_left_ansi
133
- @input << arrow_key_left_ansi
180
+ input.clear
181
+ input << arrow_key_left_ansi
182
+ input << arrow_key_left_ansi
183
+ input << arrow_key_left_ansi
184
+ input.rewind
185
+ @editor.event_loop.reset
186
+ @editor.event_loop.tick
134
187
 
135
188
  # now let's move right again
136
- @input << arrow_key_right_ansi
137
- @input << arrow_key_right_ansi
138
- @input << arrow_key_right_ansi
189
+ input.clear
190
+ input << arrow_key_right_ansi
191
+ input << arrow_key_right_ansi
192
+ input << arrow_key_right_ansi
193
+ input.rewind
194
+ @editor.event_loop.reset
195
+ @editor.event_loop.tick
139
196
 
140
197
  # this is the one that puts us on the next line
141
- @input << arrow_key_right_ansi
142
-
143
- @input.rewind
144
- @editor.read
145
- end
146
-
147
- it "sends the the escape sequence for moving to the previous line just once" do
148
- expected_ansi_sequence = "\e[A\e[#{terminal_width}C"
149
- expect(output.scan(expected_ansi_sequence).flatten.length).to eq(1)
150
- end
151
-
152
- it "sends the the escape sequence for moving to the next line just once" do
153
- expected_ansi_sequence = "\e[B\e[#{terminal_width}D"
154
- expect(output.scan(expected_ansi_sequence).flatten.length).to eq(1)
198
+ input.clear
199
+ input << arrow_key_right_ansi
200
+ input.rewind
201
+ @editor.event_loop.reset
202
+ @editor.event_loop.tick
155
203
  end
156
204
 
157
- it "correctly sets the line's position" do
205
+ it "correctly sets the line and cursor position" do
158
206
  expect(@editor.line.position).to eq(3)
207
+ expect(@editor.input_box.cursor_position.x).to eq(3)
159
208
  end
160
209
  end
161
210
  end
162
211
  end
163
212
 
164
213
  it "can delete characters" do
165
- @input << "test #5"
166
- @input.rewind
167
- @editor.read
214
+ input << "test #5"
215
+ input.rewind
216
+ @editor.event_loop.tick
168
217
  3.times { @editor.move_left }
169
218
  4.times { @editor.delete_left_character }
170
219
  3.times { @editor.delete_character }
@@ -173,18 +222,18 @@ describe RawLine::Editor do
173
222
  end
174
223
 
175
224
  it "can clear the whole line" do
176
- @input << "test #5"
177
- @input.rewind
178
- @editor.read
225
+ input << "test #5"
226
+ input.rewind
227
+ @editor.event_loop.tick
179
228
  @editor.clear_line
180
229
  expect(@editor.line.text).to eq("")
181
230
  expect(@editor.line.position).to eq(0)
182
231
  end
183
232
 
184
233
  it "supports undo and redo" do
185
- @input << "test #6"
186
- @input.rewind
187
- @editor.read
234
+ input << "test #6"
235
+ input.rewind
236
+ @editor.event_loop.tick
188
237
  3.times { @editor.delete_left_character }
189
238
  2.times { @editor.undo }
190
239
  expect(@editor.line.text).to eq("test #")
@@ -192,9 +241,9 @@ describe RawLine::Editor do
192
241
  expect(@editor.line.text).to eq("test")
193
242
  end
194
243
 
195
- it "supports history" do
196
- @input << "test #7a"
197
- @input.rewind
244
+ xit "supports history" do
245
+ input << "test #7a"
246
+ input.rewind
198
247
  @editor.read "", true
199
248
  @editor.newline
200
249
  @input << "test #7b"
@@ -218,15 +267,15 @@ describe RawLine::Editor do
218
267
  end
219
268
 
220
269
  it "can overwrite lines" do
221
- @input << "test #8a"
222
- @input.rewind
223
- @editor.read
270
+ input << "test #8a"
271
+ input.rewind
272
+ @editor.event_loop.tick
224
273
  @editor.overwrite_line("test #8b", 2)
225
274
  expect(@editor.line.text).to eq("test #8b")
226
275
  expect(@editor.line.position).to eq(2)
227
276
  end
228
277
 
229
- it "can complete words" do
278
+ xit "can complete words" do
230
279
  @editor.completion_append_string = "\t"
231
280
  @editor.bind(:tab) { @editor.complete }
232
281
  @editor.completion_proc = lambda do |word|
@@ -234,27 +283,33 @@ describe RawLine::Editor do
234
283
  ['select', 'update', 'delete', 'debug', 'destroy'].find_all { |e| e.match(/^#{Regexp.escape(word)}/) }
235
284
  end
236
285
  end
237
- @input << "test #9 de" << ?\t.chr << ?\t.chr
238
- @input.rewind
239
- @editor.read
286
+ input << "test #9 de" << ?\t.chr << ?\t.chr
287
+ input.rewind
288
+ @editor.event_loop.tick
240
289
  expect(@editor.line.text).to eq("test #9 delete\t")
241
290
  end
242
291
 
243
- it "supports INSERT and REPLACE modes" do
244
- @input << "test 0"
245
- @editor.terminal.keys[:left_arrow].each { |k| @input << k.chr }
246
- @input << "#1"
247
- @input.rewind
248
- @editor.read
292
+ xit "supports INSERT and REPLACE modes" do
293
+ input << "test 0"
294
+ @editor.terminal.keys[:left_arrow].each { |k| input << k.chr }
295
+ input << "#1"
296
+ input.rewind
297
+ @editor.event_loop.tick
249
298
  expect(@editor.line.text).to eq("test #10")
250
299
  @editor.toggle_mode
251
- @input << "test 0"
252
- @editor.terminal.keys[:left_arrow].each { |k| @input << k.chr }
253
- @input << "#1"
254
- @input.rewind
255
- @editor.read
300
+ input << "test 0"
301
+ @editor.terminal.keys[:left_arrow].each { |k| input << k.chr }
302
+ input << "#1"
303
+ input.rewind
304
+ @editor.event_loop.tick
256
305
  expect(@editor.line.text).to eq("test #1test #1")
257
306
  end
258
307
 
259
-
308
+ describe '#puts' do
309
+ it 'puts to the terminal, then re-renders' do
310
+ expect(terminal).to receive(:puts).with("A", "B", "C").ordered
311
+ expect(renderer).to receive(:render).with(reset: true)
312
+ @editor.puts("A", "B", "C")
313
+ end
314
+ end
260
315
  end