reflex-terminal 0.1.0 → 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.
- checksums.yaml +4 -4
- data/.doc/ext/reflex-terminal/renderer.cpp +33 -1
- data/.doc/ext/reflex-terminal/terminal.cpp +26 -10
- data/.github/workflows/release-gem.yml +4 -49
- data/.github/workflows/tag.yml +5 -28
- data/.github/workflows/test.yml +3 -37
- data/ChangeLog.md +23 -0
- data/Gemfile.lock +26 -0
- data/README.md +2 -2
- data/Rakefile +1 -1
- data/VERSION +1 -1
- data/examples/terminal.rb +24 -13
- data/ext/reflex-terminal/renderer.cpp +37 -1
- data/ext/reflex-terminal/terminal.cpp +27 -10
- data/include/reflex/terminal.h +30 -5
- data/include/reflex-terminal/renderer.h +8 -0
- data/lib/reflex/terminal.rb +3 -2
- data/lib/reflex/terminal_view.rb +76 -10
- data/reflex-terminal.gemspec +5 -5
- data/src/pty.cpp +33 -4
- data/src/renderer.cpp +350 -80
- data/src/terminal.cpp +237 -105
- data/src/win32/pty.cpp +2 -2
- data/test/test_renderer.rb +122 -4
- data/test/test_terminal.rb +163 -8
- data/test/test_terminal_view.rb +1 -1
- metadata +12 -12
- data/.github/workflows/utils.rb +0 -127
data/test/test_renderer.rb
CHANGED
|
@@ -31,10 +31,10 @@ class TestRenderer < Test::Unit::TestCase
|
|
|
31
31
|
r = renderer f
|
|
32
32
|
# the font crosses back as a copy, so it is the same one by its own
|
|
33
33
|
# measure rather than by object
|
|
34
|
-
assert_equal f.name,
|
|
35
|
-
assert_equal f.size,
|
|
36
|
-
assert_equal f.width('M'),
|
|
37
|
-
assert_equal f.height.ceil,
|
|
34
|
+
assert_equal f.name, r.font.name
|
|
35
|
+
assert_equal f.size, r.font.size
|
|
36
|
+
assert_equal f.width('M').ceil, r.cell_width
|
|
37
|
+
assert_equal f.height.ceil, r.cell_height
|
|
38
38
|
|
|
39
39
|
# the atlas holds glyphs of one size, so it goes with the old font
|
|
40
40
|
r.bake_glyphs terminal('hello')
|
|
@@ -66,6 +66,124 @@ class TestRenderer < Test::Unit::TestCase
|
|
|
66
66
|
assert_equal ascii + 3, r.glyph_count
|
|
67
67
|
end
|
|
68
68
|
|
|
69
|
+
def test_bake_glyphs_by_style()
|
|
70
|
+
r = renderer
|
|
71
|
+
ascii = (0x20..0x7e).size
|
|
72
|
+
|
|
73
|
+
# a styled cell is another glyph even for a character the seed holds:
|
|
74
|
+
# each face keeps glyphs of its own, over the one shared atlas
|
|
75
|
+
r.bake_glyphs terminal("a \e[1ma\e[0m \e[3ma\e[0m \e[1;3ma\e[0m")
|
|
76
|
+
assert_equal ascii + 3, r.glyph_count
|
|
77
|
+
|
|
78
|
+
# and only the regular face is seeded with ascii
|
|
79
|
+
r.bake_glyphs terminal("\e[1mb")
|
|
80
|
+
assert_equal ascii + 4, r.glyph_count
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def test_draw_styles()
|
|
84
|
+
r = renderer
|
|
85
|
+
t = terminal "aaaa \e[1maaaa\e[0m \e[3maaaa\e[0m"
|
|
86
|
+
r.bake_glyphs t
|
|
87
|
+
|
|
88
|
+
# the same character in another style cannot draw the same
|
|
89
|
+
plain = pixels(r, terminal('aaaa'))
|
|
90
|
+
bold = pixels(r, terminal("\e[1maaaa"))
|
|
91
|
+
italic = pixels(r, terminal("\e[3maaaa"))
|
|
92
|
+
assert_not_equal plain, bold
|
|
93
|
+
assert_not_equal plain, italic
|
|
94
|
+
assert_not_equal bold, italic
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def test_draw_decorations()
|
|
98
|
+
r = renderer
|
|
99
|
+
styles = {
|
|
100
|
+
single: "\e[4m",
|
|
101
|
+
double: "\e[21m",
|
|
102
|
+
curly: "\e[4:3m",
|
|
103
|
+
dotted: "\e[4:4m",
|
|
104
|
+
dashed: "\e[4:5m",
|
|
105
|
+
strikethrough: "\e[9m",
|
|
106
|
+
overline: "\e[53m",
|
|
107
|
+
}
|
|
108
|
+
r.bake_glyphs terminal('aaaa')
|
|
109
|
+
|
|
110
|
+
# every decoration adds its own marks, and no two draw the same
|
|
111
|
+
plain = pixels(r, terminal('aaaa'))
|
|
112
|
+
drawn = styles.map {|name, sgr| [name, pixels(r, terminal("#{sgr}aaaa"))]}
|
|
113
|
+
drawn.each {|name, px| assert_not_equal plain, px, name}
|
|
114
|
+
drawn.combination(2).each do |(n1, p1), (n2, p2)|
|
|
115
|
+
assert_not_equal p1, p2, "#{n1} vs #{n2}"
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def test_draw_decoration_colors()
|
|
120
|
+
r = renderer
|
|
121
|
+
r.bake_glyphs terminal('aaaa')
|
|
122
|
+
|
|
123
|
+
# SGR 58 colors the line apart from the text, and SGR 59 hands it back
|
|
124
|
+
default = pixels(r, terminal("\e[4maaaa"))
|
|
125
|
+
red = pixels(r, terminal("\e[4m\e[58:2::255:0:0maaaa"))
|
|
126
|
+
reset = pixels(r, terminal("\e[4m\e[58:2::255:0:0m\e[59maaaa"))
|
|
127
|
+
assert_not_equal default, red
|
|
128
|
+
assert_equal default, reset
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def test_background_alpha()
|
|
132
|
+
r = renderer
|
|
133
|
+
assert_equal 1, r.background_alpha
|
|
134
|
+
assert_raise(ArgumentError) {r.background_alpha = 1.5}
|
|
135
|
+
|
|
136
|
+
# the theme background thins out, while a cell the child colored
|
|
137
|
+
# itself stays opaque over it
|
|
138
|
+
r.background_alpha = 0.5
|
|
139
|
+
assert_in_delta 0.5, pixels(r, terminal('')).first.last, 1.0 / 255
|
|
140
|
+
|
|
141
|
+
r.background_alpha = 0
|
|
142
|
+
assert_equal [0, 0, 0, 0], pixels(r, terminal('')).first
|
|
143
|
+
assert_equal 1, pixels(r, terminal("\e[41m ")).first.last
|
|
144
|
+
|
|
145
|
+
# a cell colored like the theme was still colored by the child, so it
|
|
146
|
+
# stays opaque rather than joining the background it happens to match
|
|
147
|
+
assert_equal 1, pixels(r, terminal("\e[48;2;0;0;0m ")).first.last
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def test_draw_blink()
|
|
151
|
+
r = renderer
|
|
152
|
+
r.bake_glyphs terminal("\e[5;4mhello")
|
|
153
|
+
lit = pixels(r, terminal("\e[5;4mhello"))
|
|
154
|
+
plain = pixels(r, terminal('hello'))
|
|
155
|
+
|
|
156
|
+
# the off phase hides the glyphs and their decorations, but not the
|
|
157
|
+
# background, and leaves text without the attribute alone
|
|
158
|
+
r.blink_visible = false
|
|
159
|
+
assert_equal pixels(r, terminal('')), pixels(r, terminal("\e[5;4mhello"))
|
|
160
|
+
assert_not_equal pixels(r, terminal('')), pixels(r, terminal("\e[5;41mhello"))
|
|
161
|
+
assert_equal plain, pixels(r, terminal('hello'))
|
|
162
|
+
|
|
163
|
+
r.blink_visible = true
|
|
164
|
+
assert_equal lit, pixels(r, terminal("\e[5;4mhello"))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def test_draw_invisible()
|
|
168
|
+
r = renderer
|
|
169
|
+
r.bake_glyphs terminal('hello')
|
|
170
|
+
|
|
171
|
+
# conceal hides the glyphs, not the cell or its decorations
|
|
172
|
+
assert_equal pixels(r, terminal('')), pixels(r, terminal("\e[8mhello"))
|
|
173
|
+
assert_not_equal pixels(r, terminal('hello')), pixels(r, terminal("\e[8mhello"))
|
|
174
|
+
assert_not_equal pixels(r, terminal('')), pixels(r, terminal("\e[8;4mhello"))
|
|
175
|
+
assert_not_equal pixels(r, terminal('')), pixels(r, terminal("\e[8;41mhello"))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def test_bake_glyphs_skips_concealed()
|
|
179
|
+
r = renderer
|
|
180
|
+
ascii = (0x20..0x7e).size
|
|
181
|
+
|
|
182
|
+
# a concealed span draws no glyphs, so it bakes none either
|
|
183
|
+
r.bake_glyphs terminal("\e[8mあ")
|
|
184
|
+
assert_equal ascii, r.glyph_count
|
|
185
|
+
end
|
|
186
|
+
|
|
69
187
|
def test_draw()
|
|
70
188
|
r = renderer
|
|
71
189
|
t = terminal "hello \e[31mworld\e[0m"
|
data/test/test_terminal.rb
CHANGED
|
@@ -36,6 +36,22 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
36
36
|
R::KeyEvent.new R::KeyEvent::UP, chars, code, modifiers, 0
|
|
37
37
|
end
|
|
38
38
|
|
|
39
|
+
# A spawned child is forked from this process, so its parent id and the
|
|
40
|
+
# script it was handed are enough to pick it out from everything running.
|
|
41
|
+
def child_pids(script)
|
|
42
|
+
`ps -eo pid,ppid,command`.lines.drop(1).filter_map do |line|
|
|
43
|
+
pid, ppid, command = line.strip.split ' ', 3
|
|
44
|
+
pid.to_i if ppid.to_i == Process.pid && command.include?(script)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def process_alive?(pid)
|
|
49
|
+
Process.kill 0, pid
|
|
50
|
+
true
|
|
51
|
+
rescue Errno::ESRCH
|
|
52
|
+
false
|
|
53
|
+
end
|
|
54
|
+
|
|
39
55
|
def wait_for(t, timeout = 5)
|
|
40
56
|
deadline = Time.now + timeout
|
|
41
57
|
until yield
|
|
@@ -66,10 +82,10 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
66
82
|
spans = t.each_span.to_a
|
|
67
83
|
assert_equal 2, spans.size
|
|
68
84
|
|
|
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]
|
|
85
|
+
x, y, width, text, fg, bg, ul, flags = spans[0]
|
|
86
|
+
assert_equal [0, 0, 3, %q[hi ], nil, nil, nil, 0], [x, y, width, text, fg, bg, ul, flags]
|
|
71
87
|
|
|
72
|
-
x, y, width, text, fg, bg, flags = spans[1]
|
|
88
|
+
x, y, width, text, fg, bg, ul, flags = spans[1]
|
|
73
89
|
assert_equal [3, 0, 3, %q[red]], [x, y, width, text]
|
|
74
90
|
assert_kind_of Integer, fg
|
|
75
91
|
assert_nil bg
|
|
@@ -94,17 +110,56 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
94
110
|
assert_false t.update
|
|
95
111
|
end
|
|
96
112
|
|
|
113
|
+
def test_update_sees_a_bare_cursor_motion()
|
|
114
|
+
t = terminal 80, 4
|
|
115
|
+
t.update
|
|
116
|
+
|
|
117
|
+
# moving the cursor dirties no cell, but whoever draws the screen
|
|
118
|
+
# draws the cursor too, so it has to count as damage
|
|
119
|
+
t.feed "\e[C"
|
|
120
|
+
assert_true t.update
|
|
121
|
+
assert_false t.update
|
|
122
|
+
end
|
|
123
|
+
|
|
97
124
|
def test_each_span_attributes()
|
|
98
125
|
t = terminal 80, 4
|
|
99
126
|
t.feed "\e[1mB\e[0m \e[4mU\e[0m \e[7mR"
|
|
100
127
|
t.update
|
|
101
128
|
|
|
102
|
-
flags = t.each_span.map {|x, y, w, str, fg, bg, flags| flags}
|
|
129
|
+
flags = t.each_span.map {|x, y, w, str, fg, bg, ul, flags| flags}
|
|
103
130
|
assert_equal T::BOLD, flags[0] & T::BOLD
|
|
104
131
|
assert_equal 1, (flags[2] & T::UNDERLINE_MASK) >> T::UNDERLINE_SHIFT
|
|
105
132
|
assert_equal T::INVERSE, flags[4] & T::INVERSE
|
|
106
133
|
end
|
|
107
134
|
|
|
135
|
+
def test_each_span_underline_color()
|
|
136
|
+
t = terminal 80, 4
|
|
137
|
+
t.feed "\e]4;12;#123456\a"# pin palette 12, whose default is the theme's
|
|
138
|
+
t.feed "\e[4mA\e[58:2::255:0:0mB\e[58:5:12mC\e[59mD"
|
|
139
|
+
t.update
|
|
140
|
+
|
|
141
|
+
uls = t.each_span.map {|x, y, w, str, fg, bg, ul, flags| ul}
|
|
142
|
+
assert_equal 4, uls.size# one span per color
|
|
143
|
+
assert_nil uls[0]
|
|
144
|
+
assert_equal 0xff0000, uls[1]
|
|
145
|
+
assert_equal 0x123456, uls[2]
|
|
146
|
+
assert_nil uls[3]
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def test_blinking()
|
|
150
|
+
t = terminal 20, 4
|
|
151
|
+
t.update
|
|
152
|
+
assert_false t.blinking?
|
|
153
|
+
|
|
154
|
+
t.feed "\e[5mB"
|
|
155
|
+
t.update
|
|
156
|
+
assert_true t.blinking?
|
|
157
|
+
|
|
158
|
+
t.feed "\e[H\e[2J"
|
|
159
|
+
t.update
|
|
160
|
+
assert_false t.blinking?
|
|
161
|
+
end
|
|
162
|
+
|
|
108
163
|
def test_wide_chars()
|
|
109
164
|
t = terminal 20, 4
|
|
110
165
|
t.feed %q[あい]
|
|
@@ -281,6 +336,17 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
281
336
|
assert_equal 'A', t.read_pending_input
|
|
282
337
|
end
|
|
283
338
|
|
|
339
|
+
def test_write_key_kitty_arrows()
|
|
340
|
+
t = terminal
|
|
341
|
+
# macos delivers an arrow with its function-key character (U+F702) as
|
|
342
|
+
# chars. legacy encoding takes the key over the text, but the kitty
|
|
343
|
+
# protocol sends the text when there is one, so the character has to
|
|
344
|
+
# be recognized as a key rather than text
|
|
345
|
+
t.feed "\e[>1u"
|
|
346
|
+
t.write_key key_down("\uF702", R::KEY_LEFT)
|
|
347
|
+
assert_equal "\e[D", t.read_pending_input
|
|
348
|
+
end
|
|
349
|
+
|
|
284
350
|
def test_write_key_ctrl()
|
|
285
351
|
t = terminal
|
|
286
352
|
# ghostty leaves these to the kitty protocol (fixterms), so they would
|
|
@@ -322,6 +388,17 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
322
388
|
assert_equal "\e[13;1:3u", t.read_pending_input
|
|
323
389
|
end
|
|
324
390
|
|
|
391
|
+
def test_write_key_release_of_committed_text()
|
|
392
|
+
t = terminal
|
|
393
|
+
# a key event synthesized for a committed text carries the text as chars
|
|
394
|
+
# and no key at all; its release must not write the text a second time
|
|
395
|
+
t.feed "\e[>3u"# report event types
|
|
396
|
+
t.write_key key_down('あ', -1)# KEY_NONE
|
|
397
|
+
assert_equal 'あ'.b, t.read_pending_input
|
|
398
|
+
t.write_key key_up('あ', -1)
|
|
399
|
+
assert_equal '', t.read_pending_input
|
|
400
|
+
end
|
|
401
|
+
|
|
325
402
|
def test_read_pending_input()
|
|
326
403
|
t = terminal
|
|
327
404
|
t.write_key key_down("\r", R::KEY_ENTER)
|
|
@@ -388,6 +465,24 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
388
465
|
assert_include text(t), 'after close'
|
|
389
466
|
end
|
|
390
467
|
|
|
468
|
+
def test_close_ends_the_child()
|
|
469
|
+
# a child that never reads its end of the terminal, so nothing short of
|
|
470
|
+
# a signal ends it. alive? says nothing about that: close() forgets the
|
|
471
|
+
# process, and a forgotten one is reported gone whether it is or not.
|
|
472
|
+
# The comment is a name, so that no other test's child answers to it
|
|
473
|
+
script = 'sleep# close-test'
|
|
474
|
+
|
|
475
|
+
t = terminal 40, 6
|
|
476
|
+
t.spawn(*ruby(script))
|
|
477
|
+
wait_for(t) {t.alive?}
|
|
478
|
+
|
|
479
|
+
pids = child_pids script
|
|
480
|
+
assert_equal 1, pids.size
|
|
481
|
+
|
|
482
|
+
t.close
|
|
483
|
+
assert_false process_alive?(pids.first)
|
|
484
|
+
end unless win32?# child_pids reads ps
|
|
485
|
+
|
|
391
486
|
def test_spawn_with_args()
|
|
392
487
|
t = terminal 40, 6
|
|
393
488
|
t.spawn(*ruby('print "spawned"'))
|
|
@@ -459,16 +554,76 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
459
554
|
assert_equal "\e[<0;2;2M", t.read_pending_input# cell (2, 2), left press
|
|
460
555
|
end
|
|
461
556
|
|
|
557
|
+
def test_write_pointer_cancel()
|
|
558
|
+
t = terminal 40, 10
|
|
559
|
+
t.resize 40, 10, cell_width: 8, cell_height: 16
|
|
560
|
+
t.feed "\e[?1000h\e[?1006h"# normal tracking + SGR format
|
|
561
|
+
|
|
562
|
+
mouse = R::Pointer::MOUSE
|
|
563
|
+
left = R::Pointer::MOUSE_LEFT
|
|
564
|
+
right = R::Pointer::MOUSE_RIGHT
|
|
565
|
+
press = -> types {R::PointerEvent.new(
|
|
566
|
+
R::Pointer.new(0, mouse | types, R::Pointer::DOWN, [12, 20], 0, 1, false, 0))}
|
|
567
|
+
|
|
568
|
+
t.read_pending_input
|
|
569
|
+
t.write_pointer press[left]
|
|
570
|
+
assert_equal "\e[<0;2;2M", t.read_pending_input
|
|
571
|
+
t.write_pointer press[right]
|
|
572
|
+
assert_equal "\e[<2;2;2M", t.read_pending_input
|
|
573
|
+
|
|
574
|
+
# a cancel carries every button being pressed, and the child hears each
|
|
575
|
+
# of them released
|
|
576
|
+
t.write_pointer R::PointerEvent.new(
|
|
577
|
+
R::Pointer.new(
|
|
578
|
+
0, mouse | left | right, R::Pointer::CANCEL, [12, 20], 0, 1, false, 0))
|
|
579
|
+
assert_equal "\e[<0;2;2m\e[<2;2;2m", t.read_pending_input
|
|
580
|
+
end
|
|
581
|
+
|
|
582
|
+
def test_mouse_encoding_with_a_fractional_cell()
|
|
583
|
+
# rounding the cell width would land this press cells off
|
|
584
|
+
t = terminal 100, 10
|
|
585
|
+
t.resize 100, 10, cell_width: 8.43, cell_height: 18.62
|
|
586
|
+
t.feed "\e[?1000h\e[?1006h"
|
|
587
|
+
|
|
588
|
+
types = R::Pointer::MOUSE | R::Pointer::MOUSE_LEFT
|
|
589
|
+
t.read_pending_input
|
|
590
|
+
t.write_pointer R::PointerEvent.new(
|
|
591
|
+
R::Pointer.new(0, types, R::Pointer::DOWN, [79 * 8.43 + 4, 5], 0, 1, false, 0))
|
|
592
|
+
assert_equal "\e[<0;80;1M", t.read_pending_input
|
|
593
|
+
end
|
|
594
|
+
|
|
595
|
+
def test_mouse_encoding_in_sgr_pixels_mode()
|
|
596
|
+
# the offset inside the cell survives, scaled onto the integral grid
|
|
597
|
+
t = terminal 100, 10
|
|
598
|
+
t.resize 100, 10, cell_width: 8.43, cell_height: 18.62
|
|
599
|
+
t.feed "\e[?1000h\e[?1016h"
|
|
600
|
+
|
|
601
|
+
types = R::Pointer::MOUSE | R::Pointer::MOUSE_LEFT
|
|
602
|
+
t.read_pending_input
|
|
603
|
+
t.write_pointer R::PointerEvent.new(
|
|
604
|
+
R::Pointer.new(0, types, R::Pointer::DOWN, [79 * 8.43 + 2, 5], 0, 1, false, 0))
|
|
605
|
+
assert_equal "\e[<0;634;5M", t.read_pending_input
|
|
606
|
+
end
|
|
607
|
+
|
|
462
608
|
def test_write_wheel()
|
|
463
609
|
t = terminal 40, 10
|
|
464
610
|
t.resize 40, 10, cell_width: 8, cell_height: 16
|
|
465
611
|
t.feed "\e[?1000h\e[?1006h"
|
|
466
612
|
|
|
467
|
-
#
|
|
468
|
-
|
|
613
|
+
# a wheel delta is in pixels and reflex counts it downwards, so a cell
|
|
614
|
+
# height upwards is one row, and one row up is button 4
|
|
615
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, -16, 0, 0)
|
|
469
616
|
assert_equal "\e[<64;1;1M\e[<64;1;1m", t.read_pending_input
|
|
470
617
|
|
|
471
|
-
t.write_wheel R::WheelEvent.new(0, 0, 0, 0,
|
|
618
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, 16, 0, 0)
|
|
619
|
+
assert_equal "\e[<65;1;1M\e[<65;1;1m", t.read_pending_input
|
|
620
|
+
|
|
621
|
+
# what does not add up to a whole row waits for the next delta rather
|
|
622
|
+
# than being dropped
|
|
623
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, 8, 0, 0)
|
|
624
|
+
assert_equal '', t.read_pending_input
|
|
625
|
+
|
|
626
|
+
t.write_wheel R::WheelEvent.new(0, 0, 0, 0, 8, 0, 0)
|
|
472
627
|
assert_equal "\e[<65;1;1M\e[<65;1;1m", t.read_pending_input
|
|
473
628
|
end
|
|
474
629
|
|
|
@@ -516,7 +671,7 @@ class TestTerminal < Test::Unit::TestCase
|
|
|
516
671
|
assert_equal 'あ', t.selected_text
|
|
517
672
|
assert_equal(
|
|
518
673
|
[['あ', true], ['いうえお', false]],
|
|
519
|
-
t.each_span.map {|x, y, w, str, fg, bg, flags|
|
|
674
|
+
t.each_span.map {|x, y, w, str, fg, bg, ul, flags|
|
|
520
675
|
[str, (flags & T::SELECTED) != 0]})
|
|
521
676
|
end
|
|
522
677
|
|
data/test/test_terminal_view.rb
CHANGED
|
@@ -12,7 +12,7 @@ class TestTerminalView < Test::Unit::TestCase
|
|
|
12
12
|
# drag that would reach back to one
|
|
13
13
|
def press(view, x, y, click_count: 1, modifiers: 0, button: Reflex::Pointer::MOUSE_LEFT)
|
|
14
14
|
position = Reflex::Point.new(
|
|
15
|
-
x * view.font.width('M'), y * view.font.height.ceil)
|
|
15
|
+
x * view.font.width('M').ceil, y * view.font.height.ceil)
|
|
16
16
|
Reflex::PointerEvent.new Reflex::Pointer.new(
|
|
17
17
|
1, Reflex::Pointer::MOUSE | button,
|
|
18
18
|
Reflex::Pointer::DOWN, position, modifiers, click_count, false, 0)
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: reflex-terminal
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- xordog
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-11-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: xot
|
|
@@ -16,56 +16,56 @@ dependencies:
|
|
|
16
16
|
requirements:
|
|
17
17
|
- - "~>"
|
|
18
18
|
- !ruby/object:Gem::Version
|
|
19
|
-
version: 0.
|
|
19
|
+
version: 0.5.0
|
|
20
20
|
type: :runtime
|
|
21
21
|
prerelease: false
|
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
|
23
23
|
requirements:
|
|
24
24
|
- - "~>"
|
|
25
25
|
- !ruby/object:Gem::Version
|
|
26
|
-
version: 0.
|
|
26
|
+
version: 0.5.0
|
|
27
27
|
- !ruby/object:Gem::Dependency
|
|
28
28
|
name: rucy
|
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
|
30
30
|
requirements:
|
|
31
31
|
- - "~>"
|
|
32
32
|
- !ruby/object:Gem::Version
|
|
33
|
-
version: 0.
|
|
33
|
+
version: 0.5.0
|
|
34
34
|
type: :runtime
|
|
35
35
|
prerelease: false
|
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
|
37
37
|
requirements:
|
|
38
38
|
- - "~>"
|
|
39
39
|
- !ruby/object:Gem::Version
|
|
40
|
-
version: 0.
|
|
40
|
+
version: 0.5.0
|
|
41
41
|
- !ruby/object:Gem::Dependency
|
|
42
42
|
name: rays
|
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
|
44
44
|
requirements:
|
|
45
45
|
- - "~>"
|
|
46
46
|
- !ruby/object:Gem::Version
|
|
47
|
-
version: 0.
|
|
47
|
+
version: 0.5.0
|
|
48
48
|
type: :runtime
|
|
49
49
|
prerelease: false
|
|
50
50
|
version_requirements: !ruby/object:Gem::Requirement
|
|
51
51
|
requirements:
|
|
52
52
|
- - "~>"
|
|
53
53
|
- !ruby/object:Gem::Version
|
|
54
|
-
version: 0.
|
|
54
|
+
version: 0.5.0
|
|
55
55
|
- !ruby/object:Gem::Dependency
|
|
56
56
|
name: reflexion
|
|
57
57
|
requirement: !ruby/object:Gem::Requirement
|
|
58
58
|
requirements:
|
|
59
59
|
- - "~>"
|
|
60
60
|
- !ruby/object:Gem::Version
|
|
61
|
-
version: 0.
|
|
61
|
+
version: 0.7.0
|
|
62
62
|
type: :runtime
|
|
63
63
|
prerelease: false
|
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
|
65
65
|
requirements:
|
|
66
66
|
- - "~>"
|
|
67
67
|
- !ruby/object:Gem::Version
|
|
68
|
-
version: 0.
|
|
68
|
+
version: 0.7.0
|
|
69
69
|
description: Provides Reflex::Terminal, a headless terminal emulator model built on
|
|
70
70
|
libghostty-vt, and Reflex::TerminalView, a View that renders it in the Reflex GUI
|
|
71
71
|
toolkit.
|
|
@@ -85,10 +85,10 @@ files:
|
|
|
85
85
|
- ".github/workflows/release-gem.yml"
|
|
86
86
|
- ".github/workflows/tag.yml"
|
|
87
87
|
- ".github/workflows/test.yml"
|
|
88
|
-
- ".github/workflows/utils.rb"
|
|
89
88
|
- CONTRIBUTING.md
|
|
90
89
|
- ChangeLog.md
|
|
91
90
|
- Gemfile
|
|
91
|
+
- Gemfile.lock
|
|
92
92
|
- LICENSE
|
|
93
93
|
- README.md
|
|
94
94
|
- Rakefile
|
|
@@ -141,7 +141,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
141
141
|
- !ruby/object:Gem::Version
|
|
142
142
|
version: '0'
|
|
143
143
|
requirements: []
|
|
144
|
-
rubygems_version: 3.
|
|
144
|
+
rubygems_version: 3.5.22
|
|
145
145
|
signing_key:
|
|
146
146
|
specification_version: 4
|
|
147
147
|
summary: A terminal emulator view for Reflex.
|
data/.github/workflows/utils.rb
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
require 'shellwords'
|
|
2
|
-
|
|
3
|
-
ALL_REPO = 'xord/all'
|
|
4
|
-
ALL_DIR = '../all'
|
|
5
|
-
ALL_FETCH_DEPTH = 100
|
|
6
|
-
|
|
7
|
-
RENAMES = {reflex: 'reflexion'}
|
|
8
|
-
|
|
9
|
-
def sh(cmd)
|
|
10
|
-
puts cmd
|
|
11
|
-
system cmd
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def setup_dependencies(only: nil)
|
|
15
|
-
gemspec_path = `git ls-files`.lines(chomp: true).find {|l| l =~ /\.gemspec$/}
|
|
16
|
-
return unless gemspec_path
|
|
17
|
-
|
|
18
|
-
gemspec = File.read gemspec_path
|
|
19
|
-
name = File.basename gemspec_path, '.gemspec'
|
|
20
|
-
|
|
21
|
-
exts = File.readlines('Rakefile')
|
|
22
|
-
.map {|l| l[%r|^\s*require\W+([\w\-\_]+)/extension\W+$|, 1]}
|
|
23
|
-
.compact
|
|
24
|
-
.reject {|ext| ext == name}
|
|
25
|
-
exts = exts & [only].flatten.map(&:to_s) if only
|
|
26
|
-
return if exts.empty?
|
|
27
|
-
|
|
28
|
-
unless setup_dependencies_via_monorepo(exts)
|
|
29
|
-
setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
exts.each {|ext| sh %( cd ../#{ext} && rake ext )}
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def setup_dependencies_via_monorepo(exts)
|
|
36
|
-
return false unless checkout_monorepo
|
|
37
|
-
exts.each {|ext| sh %( ln -snf all/#{ext} ../#{ext} )}
|
|
38
|
-
true
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def checkout_monorepo()
|
|
42
|
-
uuid = `git log -1 --format=%B`[/^\[\[([0-9a-fA-F-]+)\]\]$/, 1]
|
|
43
|
-
return false unless uuid
|
|
44
|
-
|
|
45
|
-
commit = setup_monorepo uuid
|
|
46
|
-
return false unless commit
|
|
47
|
-
|
|
48
|
-
Dir.chdir(ALL_DIR) {sh %( git checkout -q #{commit} )}
|
|
49
|
-
true
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
def setup_monorepo(uuid)
|
|
53
|
-
unless File.directory? ALL_DIR
|
|
54
|
-
url = "https://github.com/#{ALL_REPO}.git"
|
|
55
|
-
sh %( git clone --no-tags --depth #{ALL_FETCH_DEPTH} #{url} #{ALL_DIR} )
|
|
56
|
-
end
|
|
57
|
-
loop do
|
|
58
|
-
commit = find_monorepo_commit uuid
|
|
59
|
-
return commit if commit
|
|
60
|
-
|
|
61
|
-
deepened = Dir.chdir ALL_DIR do
|
|
62
|
-
before = `git rev-list --count HEAD`.to_i
|
|
63
|
-
sh %( git fetch --deepen #{ALL_FETCH_DEPTH} )
|
|
64
|
-
`git rev-list --count HEAD`.to_i > before
|
|
65
|
-
end
|
|
66
|
-
return nil unless deepened
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
def find_monorepo_commit(uuid)
|
|
71
|
-
Dir.chdir ALL_DIR do
|
|
72
|
-
out = `git log origin/HEAD -F --grep="[[#{uuid}]]" --format=%H -1`.strip
|
|
73
|
-
out.empty? ? nil : out
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
|
-
|
|
77
|
-
def setup_dependencies_via_each_repo_by_version(gemspec, exts)
|
|
78
|
-
exts.each do |ext|
|
|
79
|
-
gem = RENAMES[ext.to_sym].then {|s| s || ext}
|
|
80
|
-
ver = gemspec[/add_dependency.*['"]#{gem}['"].*['"]\s*~>\s*([\d\.]+)\s*['"]/, 1]
|
|
81
|
-
opts = '-c advice.detachedHead=false --depth 1'
|
|
82
|
-
clone = "git clone #{opts} https://github.com/xord/#{ext}.git ../#{ext}"
|
|
83
|
-
|
|
84
|
-
# 'rake subtree:push' pushes all subrepos, so cloning by new tag
|
|
85
|
-
# often fails before tagging each new tag
|
|
86
|
-
sh %( #{clone} --branch v#{ver} || #{clone} )
|
|
87
|
-
end
|
|
88
|
-
end
|
|
89
|
-
|
|
90
|
-
def tag_versions()
|
|
91
|
-
changes = changelogs
|
|
92
|
-
tags = `git tag`.lines chomp: true
|
|
93
|
-
vers = `git log --oneline ./VERSION`
|
|
94
|
-
.lines(chomp: true)
|
|
95
|
-
.map {|line| line.split.first[/^\w+$/]}
|
|
96
|
-
.map {|sha| [`git cat-file -p #{sha}:./VERSION 2>/dev/null`[/[\d\.]+/], sha]}
|
|
97
|
-
.select {|ver, sha| ver && sha}
|
|
98
|
-
.reverse
|
|
99
|
-
.to_h
|
|
100
|
-
|
|
101
|
-
vers.to_a.reverse.each do |ver, sha|
|
|
102
|
-
tag = "v#{ver}"
|
|
103
|
-
break if tags.include?(tag)
|
|
104
|
-
sh %( git tag -a -m \"#{changes[tag]&.gsub '"', '\\"'}\" #{tag} #{sha} )
|
|
105
|
-
end
|
|
106
|
-
end
|
|
107
|
-
|
|
108
|
-
def release(*paths)
|
|
109
|
-
tag = ENV['GITHUB_REF']&.sub(%r|^refs/tags/|, '') || raise('GITHUB_REF tag not set')
|
|
110
|
-
notes = (changelogs[tag] || '').shellescape
|
|
111
|
-
paths = paths.flatten.join ' '
|
|
112
|
-
|
|
113
|
-
sh(%( gh release create #{tag} #{paths} --notes #{notes} )) ||
|
|
114
|
-
sh(%( gh release upload #{tag} #{paths} --clobber )) ||
|
|
115
|
-
raise('failed to upload to releases')
|
|
116
|
-
end
|
|
117
|
-
|
|
118
|
-
def changelogs()
|
|
119
|
-
File.read('ChangeLog.md')
|
|
120
|
-
.split(/^\s*##\s*\[\s*(v[\d\.]+)\s*\].*$/)
|
|
121
|
-
.slice(1..)
|
|
122
|
-
.each_slice(2)
|
|
123
|
-
.to_h
|
|
124
|
-
.transform_values(&:strip!)
|
|
125
|
-
rescue Errno::ENOENT
|
|
126
|
-
raise 'failed to get changelogs'
|
|
127
|
-
end
|