reflex-terminal 0.1.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 +7 -0
- data/.doc/ext/reflex-terminal/native.cpp +19 -0
- data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
- data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
- data/.github/workflows/release-gem.yml +60 -0
- data/.github/workflows/tag.yml +35 -0
- data/.github/workflows/test.yml +46 -0
- data/.github/workflows/utils.rb +127 -0
- data/CONTRIBUTING.md +7 -0
- data/ChangeLog.md +6 -0
- data/Gemfile +5 -0
- data/LICENSE +21 -0
- data/README.md +100 -0
- data/Rakefile +44 -0
- data/VERSION +1 -0
- data/examples/terminal.rb +101 -0
- data/ext/reflex-terminal/defs.h +17 -0
- data/ext/reflex-terminal/extconf.rb +29 -0
- data/ext/reflex-terminal/native.cpp +19 -0
- data/ext/reflex-terminal/renderer.cpp +118 -0
- data/ext/reflex-terminal/terminal.cpp +572 -0
- data/include/reflex/terminal.h +233 -0
- data/include/reflex-terminal/defs.h +36 -0
- data/include/reflex-terminal/renderer.h +57 -0
- data/include/reflex-terminal/ruby/renderer.h +41 -0
- data/include/reflex-terminal/ruby/terminal.h +43 -0
- data/include/reflex-terminal/ruby.h +11 -0
- data/include/reflex-terminal.h +13 -0
- data/lib/reflex/bell_event.rb +15 -0
- data/lib/reflex/terminal.rb +168 -0
- data/lib/reflex/terminal_view.rb +268 -0
- data/lib/reflex-terminal/ext.rb +2 -0
- data/lib/reflex-terminal/extension.rb +41 -0
- data/lib/reflex-terminal.rb +7 -0
- data/reflex-terminal.gemspec +39 -0
- data/src/pty.cpp +236 -0
- data/src/renderer.cpp +365 -0
- data/src/terminal.cpp +1547 -0
- data/src/terminal.h +66 -0
- data/src/win32/pty.cpp +387 -0
- data/test/helper.rb +11 -0
- data/test/test_renderer.rb +113 -0
- data/test/test_terminal.rb +683 -0
- data/test/test_terminal_view.rb +182 -0
- metadata +152 -0
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
require_relative 'helper'
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class TestTerminal < Test::Unit::TestCase
|
|
6
|
+
|
|
7
|
+
R = Reflex
|
|
8
|
+
T = R::Terminal
|
|
9
|
+
|
|
10
|
+
CTRL = R::MOD_CONTROL
|
|
11
|
+
SHIFT = R::MOD_SHIFT
|
|
12
|
+
OPTION = R::MOD_OPTION
|
|
13
|
+
|
|
14
|
+
# a console ends the input on ctrl+z, where a tty ends it on ctrl+d
|
|
15
|
+
EOF_KEY = win32? ? "\x1a\r" : "\x04"
|
|
16
|
+
|
|
17
|
+
def text(t)
|
|
18
|
+
t.lines.join "\n"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def ruby(script)
|
|
22
|
+
# a child that exists everywhere, unlike /bin/sh. Its argument also puts
|
|
23
|
+
# the quoting a windows command line needs through its paces
|
|
24
|
+
[RbConfig.ruby, '-e', script]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def terminal(*args, **kwargs)
|
|
28
|
+
T.new(*args, **kwargs)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def key_down(chars, code, modifiers = 0)
|
|
32
|
+
R::KeyEvent.new R::KeyEvent::DOWN, chars, code, modifiers, 0
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def key_up(chars, code, modifiers = 0)
|
|
36
|
+
R::KeyEvent.new R::KeyEvent::UP, chars, code, modifiers, 0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def wait_for(t, timeout = 5)
|
|
40
|
+
deadline = Time.now + timeout
|
|
41
|
+
until yield
|
|
42
|
+
break if Time.now > deadline
|
|
43
|
+
t.update
|
|
44
|
+
sleep 0.01
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def test_initialize()
|
|
49
|
+
t = terminal 80, 24
|
|
50
|
+
assert_equal 80, t.columns
|
|
51
|
+
assert_equal 24, t.rows
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def test_initialize_with_invalid_size()
|
|
55
|
+
assert_raise(ArgumentError) {terminal 0, 24}
|
|
56
|
+
assert_raise(ArgumentError) {terminal 80, -1}
|
|
57
|
+
# to<int> coerces non-numeric values to 0, so the size check catches it
|
|
58
|
+
assert_raise(ArgumentError) {terminal 'x', 24}
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def test_feed()
|
|
62
|
+
t = terminal 80, 4
|
|
63
|
+
t.feed "hi \e[31mred"
|
|
64
|
+
t.update
|
|
65
|
+
|
|
66
|
+
spans = t.each_span.to_a
|
|
67
|
+
assert_equal 2, spans.size
|
|
68
|
+
|
|
69
|
+
x, y, width, text, fg, bg, flags = spans[0]
|
|
70
|
+
assert_equal [0, 0, 3, %q[hi ], nil, nil, 0], [x, y, width, text, fg, bg, flags]
|
|
71
|
+
|
|
72
|
+
x, y, width, text, fg, bg, flags = spans[1]
|
|
73
|
+
assert_equal [3, 0, 3, %q[red]], [x, y, width, text]
|
|
74
|
+
assert_kind_of Integer, fg
|
|
75
|
+
assert_nil bg
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def test_feed_with_invalid_bytes()
|
|
79
|
+
assert_raise(TypeError) {terminal.feed 42}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_each_span()
|
|
83
|
+
t = terminal 80, 4
|
|
84
|
+
t.feed "x"
|
|
85
|
+
t.update
|
|
86
|
+
assert_equal [0], t.each_span.map {|x, y,| y}
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def test_update()
|
|
90
|
+
t = terminal 80, 4
|
|
91
|
+
t.update# flush initial state
|
|
92
|
+
t.feed "x"
|
|
93
|
+
assert_true t.update
|
|
94
|
+
assert_false t.update
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_each_span_attributes()
|
|
98
|
+
t = terminal 80, 4
|
|
99
|
+
t.feed "\e[1mB\e[0m \e[4mU\e[0m \e[7mR"
|
|
100
|
+
t.update
|
|
101
|
+
|
|
102
|
+
flags = t.each_span.map {|x, y, w, str, fg, bg, flags| flags}
|
|
103
|
+
assert_equal T::BOLD, flags[0] & T::BOLD
|
|
104
|
+
assert_equal 1, (flags[2] & T::UNDERLINE_MASK) >> T::UNDERLINE_SHIFT
|
|
105
|
+
assert_equal T::INVERSE, flags[4] & T::INVERSE
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def test_wide_chars()
|
|
109
|
+
t = terminal 20, 4
|
|
110
|
+
t.feed %q[あい]
|
|
111
|
+
t.update
|
|
112
|
+
|
|
113
|
+
x, y, width, text = t.each_span.first
|
|
114
|
+
assert_equal [0, 0, 4, %q[あい]], [x, y, width, text]
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def test_grapheme_clusters()
|
|
118
|
+
# a cell holds a whole cluster, so a flag is one cell two columns wide
|
|
119
|
+
# rather than the two regional indicators it is written with, each
|
|
120
|
+
# taking two of its own
|
|
121
|
+
t = terminal 20, 4
|
|
122
|
+
t.feed "\u{1F1EF}\u{1F1F5}"
|
|
123
|
+
t.update
|
|
124
|
+
assert_equal [0, 0, 2, "\u{1F1EF}\u{1F1F5}"], t.each_span.first.first(4)
|
|
125
|
+
|
|
126
|
+
# a reset clears the modes, so the one that says this has to be set
|
|
127
|
+
# again behind it
|
|
128
|
+
t.reset
|
|
129
|
+
t.feed "\u{1F1EF}\u{1F1F5}"
|
|
130
|
+
t.update
|
|
131
|
+
assert_equal [0, 0, 2, "\u{1F1EF}\u{1F1F5}"], t.each_span.first.first(4)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def test_encodings()
|
|
135
|
+
t = terminal 20, 4
|
|
136
|
+
t.feed "\e]0;タイトル\a"
|
|
137
|
+
t.feed "あ"
|
|
138
|
+
t.update
|
|
139
|
+
|
|
140
|
+
assert_equal Encoding::UTF_8, t.each_span.first[3].encoding
|
|
141
|
+
assert_equal 'タイトル', t.title
|
|
142
|
+
assert_equal Encoding::UTF_8, t.lines.first.encoding
|
|
143
|
+
|
|
144
|
+
t.feed "\e[c"
|
|
145
|
+
assert_equal Encoding::ASCII_8BIT, t.read_pending_input.encoding
|
|
146
|
+
# read_pending_input is a raw byte stream for the PTY
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_cursor()
|
|
150
|
+
t = terminal 80, 24
|
|
151
|
+
t.feed "abc"
|
|
152
|
+
t.update
|
|
153
|
+
|
|
154
|
+
x, y, style, visible = t.cursor
|
|
155
|
+
assert_equal [3, 0], [x, y]
|
|
156
|
+
assert_true visible
|
|
157
|
+
assert_include [T::CURSOR_BAR, T::CURSOR_BLOCK, T::CURSOR_UNDERLINE], style
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def color(r, g, b) = Reflex::Color.new(r, g, b)
|
|
161
|
+
|
|
162
|
+
def test_default_colors()
|
|
163
|
+
# nil until someone says otherwise, so that a renderer can tell the child
|
|
164
|
+
# asking for a color apart from nobody having asked and use its own theme
|
|
165
|
+
t = terminal
|
|
166
|
+
assert_nil t.default_background_color
|
|
167
|
+
assert_nil t.background_color
|
|
168
|
+
|
|
169
|
+
t.default_background_color = color(1, 0, 0)
|
|
170
|
+
assert_equal color(1, 0, 0), t.default_background_color
|
|
171
|
+
assert_equal color(1, 0, 0), t.background_color
|
|
172
|
+
|
|
173
|
+
# the child changes what is in use without touching what it falls back to
|
|
174
|
+
t.feed "\e]11;#00ff00\a"
|
|
175
|
+
t.update
|
|
176
|
+
assert_equal color(1, 0, 0), t.default_background_color
|
|
177
|
+
assert_equal color(0, 1, 0), t.background_color
|
|
178
|
+
|
|
179
|
+
# and the foreground is set on its own, not as a pair with it
|
|
180
|
+
t.feed "\e]10;#0000ff\a"
|
|
181
|
+
t.update
|
|
182
|
+
assert_equal color(0, 0, 1), t.foreground_color
|
|
183
|
+
|
|
184
|
+
t.default_background_color = nil
|
|
185
|
+
assert_nil t.default_background_color
|
|
186
|
+
|
|
187
|
+
# a terminal color is 8 bits a channel, so a value between two of them
|
|
188
|
+
# comes back as the nearest one rather than as it was given
|
|
189
|
+
t.default_background_color = color(0.5, 0, 0)
|
|
190
|
+
assert_in_delta 0.5, t.default_background_color.red, 1.0 / 255
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def test_palette()
|
|
194
|
+
t = terminal
|
|
195
|
+
t.feed "\e[31mR"
|
|
196
|
+
t.update
|
|
197
|
+
assert_equal 0..255, T::PALETTE_RANGE
|
|
198
|
+
|
|
199
|
+
builtin = t.palette 1
|
|
200
|
+
assert_equal builtin, t.default_palette(1)
|
|
201
|
+
|
|
202
|
+
t.set_default_palette 1, color(1, 1, 1)
|
|
203
|
+
assert_equal color(1, 1, 1), t.palette(1)
|
|
204
|
+
|
|
205
|
+
# a span carries the color the palette resolved to, not the index, so
|
|
206
|
+
# changing the palette changes what the cell reports
|
|
207
|
+
t.update
|
|
208
|
+
assert_equal 0xffffff, t.each_span.first[4]
|
|
209
|
+
|
|
210
|
+
t.set_default_palette 0..3, color(0, 0, 1)
|
|
211
|
+
assert_equal [color(0, 0, 1)] * 4, (0..3).map {|i| t.palette i}
|
|
212
|
+
|
|
213
|
+
# a nil color puts the built-in one back
|
|
214
|
+
t.set_default_palette 1, nil
|
|
215
|
+
assert_equal builtin, t.palette(1)
|
|
216
|
+
|
|
217
|
+
assert_raise(IndexError) {t.palette 256}
|
|
218
|
+
assert_raise(IndexError) {t.palette(-1)}
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def test_feed_device_attributes()
|
|
222
|
+
t = terminal
|
|
223
|
+
t.feed "\e[c"
|
|
224
|
+
assert_match(/\e\[\?\d+/, t.read_pending_input)
|
|
225
|
+
assert_equal '', t.read_pending_input
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
def test_lines()
|
|
229
|
+
t = terminal 10, 4
|
|
230
|
+
t.feed "aaaaabbbbbccccc"
|
|
231
|
+
t.update
|
|
232
|
+
assert_equal ['aaaaabbbbb', 'ccccc'], t.lines[0, 2]
|
|
233
|
+
|
|
234
|
+
t.resize 20, 4
|
|
235
|
+
t.update
|
|
236
|
+
assert_equal 'aaaaabbbbbccccc', t.lines.first
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def test_resize_with_invalid_size()
|
|
240
|
+
assert_raise(ArgumentError) {terminal.resize 0, 24}
|
|
241
|
+
assert_raise(ArgumentError) {terminal.resize 80, 24, cell_width: 0}
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def test_title()
|
|
245
|
+
t = terminal
|
|
246
|
+
assert_equal '', t.title
|
|
247
|
+
t.feed "\e]0;hello\a"
|
|
248
|
+
assert_equal 'hello', t.title
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
def test_bells()
|
|
252
|
+
t = terminal
|
|
253
|
+
assert_equal 0, t.bells
|
|
254
|
+
|
|
255
|
+
t.feed "hi\a"
|
|
256
|
+
assert_equal 1, t.bells
|
|
257
|
+
|
|
258
|
+
# the count only grows, so no bell is dropped between the update that
|
|
259
|
+
# takes it in and the frame that answers it
|
|
260
|
+
t.update
|
|
261
|
+
t.feed "\a\a"
|
|
262
|
+
assert_equal 3, t.bells
|
|
263
|
+
|
|
264
|
+
# the BEL ending an OSC string is a terminator, not a bell
|
|
265
|
+
t.feed "\e]0;hello\a"
|
|
266
|
+
assert_equal 3, t.bells
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def test_write_key()
|
|
270
|
+
t = terminal
|
|
271
|
+
t.write_key key_down("\r", R::KEY_ENTER)
|
|
272
|
+
assert_equal "\r", t.read_pending_input
|
|
273
|
+
|
|
274
|
+
t.write_key key_down('', R::KEY_UP)
|
|
275
|
+
assert_equal "\e[A", t.read_pending_input
|
|
276
|
+
|
|
277
|
+
t.write_key key_down("\x03", R::KEY_C, CTRL)
|
|
278
|
+
assert_equal "\x03", t.read_pending_input
|
|
279
|
+
|
|
280
|
+
t.write_key key_down('A', R::KEY_A, SHIFT)
|
|
281
|
+
assert_equal 'A', t.read_pending_input
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def test_write_key_ctrl()
|
|
285
|
+
t = terminal
|
|
286
|
+
# ghostty leaves these to the kitty protocol (fixterms), so they would
|
|
287
|
+
# otherwise send nothing at all while an app has not asked for it
|
|
288
|
+
{
|
|
289
|
+
R::KEY_I => "\t",
|
|
290
|
+
R::KEY_M => "\r",
|
|
291
|
+
R::KEY_LBRACKET => "\e"
|
|
292
|
+
}.each do |code, expected|
|
|
293
|
+
t.write_key key_down('', code, CTRL)
|
|
294
|
+
assert_equal expected, t.read_pending_input
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
t.feed "\e[>1u"# the app asks for the kitty keyboard protocol
|
|
298
|
+
t.write_key key_down('', R::KEY_I, CTRL)
|
|
299
|
+
assert_equal "\e[105;5u", t.read_pending_input# ctrl+i stays distinct from tab
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def test_write_key_ctrl_platform()
|
|
303
|
+
t = terminal
|
|
304
|
+
# macOS hands over the control character itself for ctrl+-, which the
|
|
305
|
+
# encoder has no legacy encoding for (C-_ is undo in emacs)
|
|
306
|
+
t.write_key key_down("\x1f", R::KEY_MINUS, CTRL)
|
|
307
|
+
assert_equal "\x1f", t.read_pending_input
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def test_write_key_release()
|
|
311
|
+
t = terminal
|
|
312
|
+
t.write_key key_down("\r", R::KEY_ENTER)
|
|
313
|
+
assert_equal "\r", t.read_pending_input
|
|
314
|
+
t.write_key key_up("\r", R::KEY_ENTER)
|
|
315
|
+
assert_equal '', t.read_pending_input# a release says nothing in legacy mode
|
|
316
|
+
|
|
317
|
+
# every key as an escape code, releases included
|
|
318
|
+
t.feed "\e[>10u"
|
|
319
|
+
t.write_key key_down("\r", R::KEY_ENTER)
|
|
320
|
+
assert_equal "\e[13u", t.read_pending_input
|
|
321
|
+
t.write_key key_up("\r", R::KEY_ENTER)
|
|
322
|
+
assert_equal "\e[13;1:3u", t.read_pending_input
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def test_read_pending_input()
|
|
326
|
+
t = terminal
|
|
327
|
+
t.write_key key_down("\r", R::KEY_ENTER)
|
|
328
|
+
assert_equal Encoding::ASCII_8BIT, t.read_pending_input.encoding
|
|
329
|
+
assert_equal '', t.read_pending_input
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
def test_option_as_alt()
|
|
333
|
+
t = terminal
|
|
334
|
+
assert_equal :on, t.option_as_alt
|
|
335
|
+
|
|
336
|
+
t.write_key key_down('∫', R::KEY_B, OPTION)
|
|
337
|
+
assert_equal "\eb", t.read_pending_input
|
|
338
|
+
|
|
339
|
+
assert_raise(ArgumentError) {t.option_as_alt = :invalid}
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def test_option_as_alt_off()
|
|
343
|
+
t = terminal
|
|
344
|
+
t.option_as_alt = :off
|
|
345
|
+
t.write_key key_down('∫', R::KEY_B, OPTION)
|
|
346
|
+
assert_equal '∫'.b, t.read_pending_input
|
|
347
|
+
end if osx?# ghostty applies this setting on macos only
|
|
348
|
+
|
|
349
|
+
def test_spawn()
|
|
350
|
+
t = terminal 40, 6
|
|
351
|
+
assert_false t.alive?
|
|
352
|
+
|
|
353
|
+
t.spawn(*ruby('$stdout.sync = true; $stdin.each_line {|s| print s}'))
|
|
354
|
+
assert_true t.alive?
|
|
355
|
+
assert_raise(Rucy::NativeError) {t.spawn(*ruby('sleep'))}
|
|
356
|
+
|
|
357
|
+
t.write "hello\r"
|
|
358
|
+
wait_for(t) {text(t).include? 'hello'}
|
|
359
|
+
assert_include text(t), 'hello'
|
|
360
|
+
|
|
361
|
+
t.write EOF_KEY
|
|
362
|
+
wait_for(t) {not t.alive?}
|
|
363
|
+
assert_false t.alive?
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def test_spawn_again()
|
|
367
|
+
t = terminal 40, 6
|
|
368
|
+
2.times do |i|
|
|
369
|
+
t.reset
|
|
370
|
+
t.spawn(*ruby("print 'run#{i}'"))
|
|
371
|
+
wait_for(t) {text(t).include? "run#{i}"}
|
|
372
|
+
wait_for(t) {not t.alive?}
|
|
373
|
+
assert_include text(t), "run#{i}"
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def test_close()
|
|
378
|
+
t = terminal 40, 6
|
|
379
|
+
t.spawn(*ruby('sleep'))
|
|
380
|
+
wait_for(t) {t.alive?}
|
|
381
|
+
assert_true t.alive?
|
|
382
|
+
|
|
383
|
+
t.close
|
|
384
|
+
assert_false t.alive?
|
|
385
|
+
|
|
386
|
+
t.spawn(*ruby("print 'after close'"))
|
|
387
|
+
wait_for(t) {text(t).include? 'after close'}
|
|
388
|
+
assert_include text(t), 'after close'
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def test_spawn_with_args()
|
|
392
|
+
t = terminal 40, 6
|
|
393
|
+
t.spawn(*ruby('print "spawned"'))
|
|
394
|
+
wait_for(t) {text(t).include? 'spawned'}
|
|
395
|
+
assert_include text(t), 'spawned'
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
def test_spawn_sets_default_env()
|
|
399
|
+
t = terminal 60, 6
|
|
400
|
+
t.spawn(*ruby('print "[#{ENV["TERM"]}|#{ENV["TERM_PROGRAM"]}]"'))
|
|
401
|
+
wait_for(t) {text(t).include? ']'}
|
|
402
|
+
assert_include text(t), '[xterm-256color|reflex-terminal]'
|
|
403
|
+
end
|
|
404
|
+
|
|
405
|
+
def test_spawn_with_env_overrides_defaults()
|
|
406
|
+
t = terminal 60, 6
|
|
407
|
+
t.spawn(
|
|
408
|
+
{'TERM_PROGRAM' => 'my-app', MY_APP: 'yes'},
|
|
409
|
+
*ruby('print "[#{ENV["TERM_PROGRAM"]}|#{ENV["MY_APP"]}]"'))
|
|
410
|
+
wait_for(t) {text(t).include? ']'}
|
|
411
|
+
assert_include text(t), '[my-app|yes]'
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def test_spawn_with_nil_env_removes_variable()
|
|
415
|
+
t = terminal 60, 6
|
|
416
|
+
# ENV#key? tells an unset variable from one set to an empty string
|
|
417
|
+
t.spawn(
|
|
418
|
+
{'TERM_PROGRAM' => nil, 'EMPTY' => ''},
|
|
419
|
+
*ruby('print "[#{ENV.key?("TERM_PROGRAM")}|#{ENV.key?("EMPTY")}]"'))
|
|
420
|
+
wait_for(t) {text(t).include? ']'}
|
|
421
|
+
assert_include text(t), '[false|true]'
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
def test_spawn_with_env_only()
|
|
425
|
+
t = terminal 60, 6
|
|
426
|
+
t.spawn MY_APP: 'yes'
|
|
427
|
+
wait_for(t) {t.alive?}
|
|
428
|
+
assert_true t.alive?
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def test_spawn_with_string()
|
|
432
|
+
t = terminal 40, 6
|
|
433
|
+
# a construct only a shell expands, to show that one was involved
|
|
434
|
+
command, expected = win32? ?
|
|
435
|
+
['echo %TERM_PROGRAM%', 'reflex-terminal'] :
|
|
436
|
+
['printf "hello world" | tr a-z A-Z', 'HELLO WORLD']
|
|
437
|
+
|
|
438
|
+
t.spawn command
|
|
439
|
+
wait_for(t) {text(t).include? expected}
|
|
440
|
+
assert_include text(t), expected
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def test_write_pointer()
|
|
444
|
+
t = terminal 40, 10
|
|
445
|
+
t.resize 40, 10, cell_width: 8, cell_height: 16
|
|
446
|
+
|
|
447
|
+
types = R::Pointer::MOUSE | R::Pointer::MOUSE_LEFT
|
|
448
|
+
down = R::PointerEvent.new(
|
|
449
|
+
R::Pointer.new(
|
|
450
|
+
0, types, R::Pointer::DOWN, [12, 20], 0, 1, false, 0))
|
|
451
|
+
|
|
452
|
+
assert_false t.mouse_tracking?
|
|
453
|
+
t.write_pointer down
|
|
454
|
+
assert_equal '', t.read_pending_input# tracking off: nothing is sent
|
|
455
|
+
|
|
456
|
+
t.feed "\e[?1000h\e[?1006h"# normal tracking + SGR format
|
|
457
|
+
assert_true t.mouse_tracking?
|
|
458
|
+
t.write_pointer down
|
|
459
|
+
assert_equal "\e[<0;2;2M", t.read_pending_input# cell (2, 2), left press
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def test_write_wheel()
|
|
463
|
+
t = terminal 40, 10
|
|
464
|
+
t.resize 40, 10, cell_width: 8, cell_height: 16
|
|
465
|
+
t.feed "\e[?1000h\e[?1006h"
|
|
466
|
+
|
|
467
|
+
# reflex counts a wheel delta downwards, so button 4 is a negative one
|
|
468
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, -1, 0, 0)
|
|
469
|
+
assert_equal "\e[<64;1;1M\e[<64;1;1m", t.read_pending_input
|
|
470
|
+
|
|
471
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, 1, 0, 0)
|
|
472
|
+
assert_equal "\e[<65;1;1M\e[<65;1;1m", t.read_pending_input
|
|
473
|
+
end
|
|
474
|
+
|
|
475
|
+
def test_select()
|
|
476
|
+
t = terminal 20, 4
|
|
477
|
+
t.feed "hello world\r\nfoo bar baz\r\n"
|
|
478
|
+
t.update
|
|
479
|
+
|
|
480
|
+
assert_false t.selection?
|
|
481
|
+
assert_equal '', t.selected_text
|
|
482
|
+
|
|
483
|
+
t.select 0, 0, 4, 1
|
|
484
|
+
assert_true t.selection?
|
|
485
|
+
assert_equal "hello world\nfoo b", t.selected_text
|
|
486
|
+
|
|
487
|
+
t.select 4, 1, 0, 0# dragging upward gives the later cell first
|
|
488
|
+
assert_equal "hello world\nfoo b", t.selected_text
|
|
489
|
+
end
|
|
490
|
+
|
|
491
|
+
def test_select_off_the_screen()
|
|
492
|
+
t = terminal 20, 4
|
|
493
|
+
t.feed "hello world\r\n"
|
|
494
|
+
t.update
|
|
495
|
+
|
|
496
|
+
# 65536 would name column 0 again if the column were narrowed to the
|
|
497
|
+
# cell index type without a range check of its own
|
|
498
|
+
[[-1, 0], [100, 0], [65536, 0], [0, -100]].each do |x, y|
|
|
499
|
+
t.select x, y, 4, 0
|
|
500
|
+
assert_false t.selection?, "#{x}, #{y} as the first cell"
|
|
501
|
+
|
|
502
|
+
t.select 0, 0, x, y
|
|
503
|
+
assert_false t.selection?, "#{x}, #{y} as the second cell"
|
|
504
|
+
end
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
def test_select_marks_spans()
|
|
508
|
+
t = terminal 20, 4
|
|
509
|
+
t.feed 'あいうえお'
|
|
510
|
+
t.update
|
|
511
|
+
|
|
512
|
+
# the second column holds the spacer of a wide cell, which the spans
|
|
513
|
+
# leave out: the character standing there still has to be marked
|
|
514
|
+
t.select 1, 0, 1, 0
|
|
515
|
+
t.update
|
|
516
|
+
assert_equal 'あ', t.selected_text
|
|
517
|
+
assert_equal(
|
|
518
|
+
[['あ', true], ['いうえお', false]],
|
|
519
|
+
t.each_span.map {|x, y, w, str, fg, bg, flags|
|
|
520
|
+
[str, (flags & T::SELECTED) != 0]})
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
def test_select_rect()
|
|
524
|
+
t = terminal 20, 4
|
|
525
|
+
t.feed "hello world\r\nfoo bar baz\r\n"
|
|
526
|
+
t.update
|
|
527
|
+
|
|
528
|
+
t.select_rect 0, 0, 4, 1
|
|
529
|
+
assert_equal "hello\nfoo b", t.selected_text
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def test_select_word()
|
|
533
|
+
t = terminal 40, 4
|
|
534
|
+
t.feed "hello world\r\n"
|
|
535
|
+
t.update
|
|
536
|
+
|
|
537
|
+
t.select_word 0, 0
|
|
538
|
+
assert_equal 'hello', t.selected_text
|
|
539
|
+
|
|
540
|
+
t.select_word 6, 0# the column decides which word
|
|
541
|
+
assert_equal 'world', t.selected_text
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
def test_select_line()
|
|
545
|
+
t = terminal 10, 4
|
|
546
|
+
t.feed 'aaaaabbbbbccccc'# wraps onto a second row
|
|
547
|
+
t.update
|
|
548
|
+
assert_equal ['aaaaabbbbb', 'ccccc'], t.lines[0, 2]
|
|
549
|
+
|
|
550
|
+
t.select_line 0
|
|
551
|
+
assert_equal 'aaaaabbbbbccccc', t.selected_text
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def test_select_in_the_history()
|
|
555
|
+
t = terminal 20, 3, scrollback_bytes: 64 * 1024
|
|
556
|
+
30.times {|i| t.feed "line#{i}\r\n"}
|
|
557
|
+
t.update
|
|
558
|
+
|
|
559
|
+
# the viewport starts at line28, as test_scrollback works out, so two
|
|
560
|
+
# rows back is a line the history alone still holds
|
|
561
|
+
t.select_line(-2)
|
|
562
|
+
assert_equal 'line26', t.selected_text
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def test_deselect()
|
|
566
|
+
t = terminal 20, 4
|
|
567
|
+
t.feed "hello\r\n"
|
|
568
|
+
t.update
|
|
569
|
+
|
|
570
|
+
t.select_word 0, 0
|
|
571
|
+
assert_true t.selection?
|
|
572
|
+
|
|
573
|
+
t.deselect
|
|
574
|
+
assert_false t.selection?
|
|
575
|
+
assert_equal '', t.selected_text
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def test_scrollback()
|
|
579
|
+
t = terminal 20, 3, scrollback_bytes: 64 * 1024
|
|
580
|
+
30.times {|i| t.feed "line#{i}\r\n"}
|
|
581
|
+
t.update
|
|
582
|
+
assert_equal 0, t.scroll
|
|
583
|
+
|
|
584
|
+
t.scroll_by(-10)
|
|
585
|
+
assert_equal(-10, t.scroll)
|
|
586
|
+
|
|
587
|
+
t.scroll_by 4
|
|
588
|
+
assert_equal(-6, t.scroll)
|
|
589
|
+
|
|
590
|
+
# the last row is the empty line the cursor sits on, so the
|
|
591
|
+
# viewport starts at line28 and two rows back is line26
|
|
592
|
+
t.scroll_to(-2)
|
|
593
|
+
t.update
|
|
594
|
+
assert_equal 'line26', t.lines.first
|
|
595
|
+
|
|
596
|
+
t.scroll_to(-10000)# further back than the history goes
|
|
597
|
+
t.update
|
|
598
|
+
assert_equal 'line0', t.lines.first
|
|
599
|
+
top = t.scroll
|
|
600
|
+
assert_operator top, :<, 0
|
|
601
|
+
|
|
602
|
+
t.scroll_by(-1)# already at the top, so it stays
|
|
603
|
+
assert_equal top, t.scroll
|
|
604
|
+
|
|
605
|
+
t.scroll_to 0
|
|
606
|
+
assert_equal 0, t.scroll
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def test_scrollback_disabled()
|
|
610
|
+
t = terminal 20, 3, scrollback_bytes: 0
|
|
611
|
+
30.times {|i| t.feed "line#{i}\r\n"}
|
|
612
|
+
t.update
|
|
613
|
+
|
|
614
|
+
t.scroll_by(-10)
|
|
615
|
+
assert_equal 0, t.scroll
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def test_history()
|
|
619
|
+
t = terminal 20, 3, scrollback_bytes: 64 * 1024
|
|
620
|
+
30.times {|i| t.feed "line#{i}\r\n"}
|
|
621
|
+
t.update
|
|
622
|
+
|
|
623
|
+
# the 3 rows still on screen are line28, line29 and the empty
|
|
624
|
+
# line the cursor sits on
|
|
625
|
+
assert_equal 28, t.history_rows
|
|
626
|
+
|
|
627
|
+
lines = t.each_history_line.to_a
|
|
628
|
+
assert_equal 28, lines.size
|
|
629
|
+
assert_equal %w[line0 line1 line2], lines[0, 3]
|
|
630
|
+
assert_equal 'line27', lines.last
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
def test_history_longer_than_a_chunk()
|
|
634
|
+
rows = T::HISTORY_CHUNK_SIZE * 2 + 1
|
|
635
|
+
t = terminal 20, 3, scrollback_bytes: 4 * 1024 * 1024
|
|
636
|
+
rows.times {|i| t.feed "line#{i}\r\n"}
|
|
637
|
+
t.update
|
|
638
|
+
|
|
639
|
+
lines = t.each_history_line.to_a
|
|
640
|
+
assert_equal t.history_rows, lines.size
|
|
641
|
+
assert_equal 'line0', lines.first
|
|
642
|
+
assert_equal "line#{rows - 3}", lines.last# the last 2 rows are still on screen
|
|
643
|
+
end
|
|
644
|
+
|
|
645
|
+
def test_history_without_scrollback()
|
|
646
|
+
t = terminal 20, 3, scrollback_bytes: 0
|
|
647
|
+
30.times {|i| t.feed "line#{i}\r\n"}
|
|
648
|
+
t.update
|
|
649
|
+
|
|
650
|
+
assert_equal 0, t.history_rows
|
|
651
|
+
assert_equal [], t.each_history_line.to_a
|
|
652
|
+
end
|
|
653
|
+
|
|
654
|
+
def test_paste()
|
|
655
|
+
t = terminal
|
|
656
|
+
t.paste 'hello'
|
|
657
|
+
assert_equal 'hello', t.read_pending_input
|
|
658
|
+
|
|
659
|
+
t.feed "\e[?2004h"# bracketed paste mode
|
|
660
|
+
t.paste 'hello'
|
|
661
|
+
assert_equal "\e[200~hello\e[201~", t.read_pending_input
|
|
662
|
+
end
|
|
663
|
+
|
|
664
|
+
def test_paste_sanitize()
|
|
665
|
+
t = terminal
|
|
666
|
+
# newlines would run each line as its own command, and an escape
|
|
667
|
+
# sequence could drive the terminal, so both are defused
|
|
668
|
+
str = "a\nb\e[31m"
|
|
669
|
+
t.paste str
|
|
670
|
+
assert_equal "a\rb [31m", t.read_pending_input
|
|
671
|
+
assert_equal "a\nb\e[31m", str
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def test_reset()
|
|
675
|
+
t = terminal 80, 4
|
|
676
|
+
t.feed "\e[31mred"
|
|
677
|
+
t.update
|
|
678
|
+
t.reset
|
|
679
|
+
t.update
|
|
680
|
+
assert_equal [], t.each_span.to_a
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
end# TestTerminal
|