yap-rawline 0.1.1 → 0.2.0

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