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.
Files changed (46) hide show
  1. checksums.yaml +7 -0
  2. data/.doc/ext/reflex-terminal/native.cpp +19 -0
  3. data/.doc/ext/reflex-terminal/renderer.cpp +110 -0
  4. data/.doc/ext/reflex-terminal/terminal.cpp +530 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +12 -0
  6. data/.github/workflows/release-gem.yml +60 -0
  7. data/.github/workflows/tag.yml +35 -0
  8. data/.github/workflows/test.yml +46 -0
  9. data/.github/workflows/utils.rb +127 -0
  10. data/CONTRIBUTING.md +7 -0
  11. data/ChangeLog.md +6 -0
  12. data/Gemfile +5 -0
  13. data/LICENSE +21 -0
  14. data/README.md +100 -0
  15. data/Rakefile +44 -0
  16. data/VERSION +1 -0
  17. data/examples/terminal.rb +101 -0
  18. data/ext/reflex-terminal/defs.h +17 -0
  19. data/ext/reflex-terminal/extconf.rb +29 -0
  20. data/ext/reflex-terminal/native.cpp +19 -0
  21. data/ext/reflex-terminal/renderer.cpp +118 -0
  22. data/ext/reflex-terminal/terminal.cpp +572 -0
  23. data/include/reflex/terminal.h +233 -0
  24. data/include/reflex-terminal/defs.h +36 -0
  25. data/include/reflex-terminal/renderer.h +57 -0
  26. data/include/reflex-terminal/ruby/renderer.h +41 -0
  27. data/include/reflex-terminal/ruby/terminal.h +43 -0
  28. data/include/reflex-terminal/ruby.h +11 -0
  29. data/include/reflex-terminal.h +13 -0
  30. data/lib/reflex/bell_event.rb +15 -0
  31. data/lib/reflex/terminal.rb +168 -0
  32. data/lib/reflex/terminal_view.rb +268 -0
  33. data/lib/reflex-terminal/ext.rb +2 -0
  34. data/lib/reflex-terminal/extension.rb +41 -0
  35. data/lib/reflex-terminal.rb +7 -0
  36. data/reflex-terminal.gemspec +39 -0
  37. data/src/pty.cpp +236 -0
  38. data/src/renderer.cpp +365 -0
  39. data/src/terminal.cpp +1547 -0
  40. data/src/terminal.h +66 -0
  41. data/src/win32/pty.cpp +387 -0
  42. data/test/helper.rb +11 -0
  43. data/test/test_renderer.rb +113 -0
  44. data/test/test_terminal.rb +683 -0
  45. data/test/test_terminal_view.rb +182 -0
  46. metadata +152 -0
@@ -0,0 +1,182 @@
1
+ require_relative 'helper'
2
+
3
+
4
+ class TestTerminalView < Test::Unit::TestCase
5
+
6
+ def view(*args, **kwargs)
7
+ Reflex::TerminalView.new(*args, **kwargs)
8
+ end
9
+
10
+ # a press on the cell at (x, y). Pointer#down is filled in by the window
11
+ # as it dispatches, so a hand-made event can only carry a press, not the
12
+ # drag that would reach back to one
13
+ def press(view, x, y, click_count: 1, modifiers: 0, button: Reflex::Pointer::MOUSE_LEFT)
14
+ position = Reflex::Point.new(
15
+ x * view.font.width('M'), y * view.font.height.ceil)
16
+ Reflex::PointerEvent.new Reflex::Pointer.new(
17
+ 1, Reflex::Pointer::MOUSE | button,
18
+ Reflex::Pointer::DOWN, position, modifiers, click_count, false, 0)
19
+ end
20
+
21
+ def key_down(chars, code, modifiers = 0)
22
+ Reflex::KeyEvent.new Reflex::KeyEvent::DOWN, chars, code, modifiers, 0
23
+ end
24
+
25
+ def terminal_view(text)
26
+ t = Reflex::Terminal.new 20, 4
27
+ t.feed text
28
+ t.update
29
+ [view(terminal: t), t]
30
+ end
31
+
32
+ def test_initialize()
33
+ v = view
34
+ assert_kind_of Reflex::View, v
35
+ assert_nil v.terminal
36
+ assert_equal Reflex::TerminalView::DEFAULT_FONT_SIZE, v.font_size
37
+ end
38
+
39
+ def test_terminal()
40
+ t = Reflex::Terminal.new 80, 24
41
+ assert_same t, view(terminal: t).terminal
42
+ end
43
+
44
+ def test_font()
45
+ size = Reflex::TerminalView::DEFAULT_FONT_SIZE
46
+ name = Reflex::TerminalView::DEFAULT_FONT_NAME
47
+
48
+ assert_include view(font: name) .font.name, name
49
+ assert_equal size, view(font: name) .font.size
50
+ assert_equal size, view(font: [name]) .font.size
51
+ assert_equal size, view(font: []) .font.size
52
+ assert_equal 20, view(font: [name, 20]).font.size
53
+
54
+ # an unknown name falls back to a proportional font, which would
55
+ # silently break the cell grid
56
+ font = view.font
57
+ assert_equal font.width('i'), font.width('W')
58
+ end
59
+
60
+ def glyph_count(view)
61
+ view.instance_variable_get(:@renderer).glyph_count
62
+ end
63
+
64
+ def test_on_update()
65
+ # a terminal handed over after it had stopped changing still has to be
66
+ # baked, or every glyph on it is drawn the slow way for good
67
+ v, t = terminal_view "hello\r\n"
68
+ assert_false t.update, 'nothing left for the update to report'
69
+
70
+ v.on_update nil
71
+ assert_operator glyph_count(v), :>, 0
72
+
73
+ # the atlas holds glyphs of one size, so a new font throws it away and
74
+ # the next update has to fill it again
75
+ v.font_size = 20
76
+ assert_equal 0, glyph_count(v)
77
+ v.on_update nil
78
+ assert_operator glyph_count(v), :>, 0
79
+ end
80
+
81
+ def test_on_key_down()
82
+ v, t = terminal_view "hello world\r\n"
83
+ v.on_pointer_down press(v, 0, 0, click_count: 2)
84
+
85
+ # a modifier arrives here on its own, and one held with command is a
86
+ # shortcut for the application to answer rather than something typed
87
+ v.on_key_down key_down('', Reflex::KEY_COMMAND, Reflex::MOD_COMMAND)
88
+ assert_true t.selection?
89
+
90
+ v.on_key_down key_down('c', Reflex::KEY_C, Reflex::MOD_COMMAND)
91
+ assert_true t.selection?
92
+
93
+ v.on_key_down key_down('a', Reflex::KEY_A)
94
+ assert_false t.selection?
95
+ end
96
+
97
+ def test_on_pointer_down()
98
+ v, t = terminal_view "hello world\r\n"
99
+
100
+ v.on_pointer_down press(v, 0, 0, click_count: 2)
101
+ assert_equal 'hello', t.selected_text
102
+
103
+ v.on_pointer_down press(v, 6, 0, click_count: 2)# the column decides which word
104
+ assert_equal 'world', t.selected_text
105
+
106
+ v.on_pointer_down press(v, 6, 0, click_count: 3)
107
+ assert_equal 'hello world', t.selected_text
108
+ assert_true t.selection?
109
+
110
+ v.on_pointer_down press(v, 0, 0)
111
+ assert_false t.selection?
112
+ end
113
+
114
+ def test_on_pointer_down_right_button()
115
+ # right-clicking is how an application is asked what to do with what
116
+ # is already picked out, so taking it away first is no use to anyone
117
+ v, t = terminal_view "hello world\r\n"
118
+ v.on_pointer_down press(v, 0, 0, click_count: 2)
119
+ assert_equal 'hello', t.selected_text
120
+
121
+ v.on_pointer_down press(v, 0, 0, button: Reflex::Pointer::MOUSE_RIGHT)
122
+ assert_equal 'hello', t.selected_text
123
+ end
124
+
125
+ def test_on_pointer_down_while_tracking()
126
+ v, t = terminal_view "hello world\r\n"
127
+ t.feed "\e[?1000h\e[?1006h"# normal tracking + SGR format
128
+ t.read_pending_input
129
+
130
+ v.on_pointer_down press(v, 0, 0, click_count: 2)
131
+ assert_false t.selection?, 'the child asked for the mouse'
132
+ assert_not_equal '', t.read_pending_input
133
+
134
+ v.on_pointer_down press(v, 0, 0, click_count: 2, modifiers: Reflex::MOD_SHIFT)
135
+ assert_equal 'hello', t.selected_text
136
+ assert_equal '', t.read_pending_input# taken by the selection
137
+ end
138
+
139
+ def test_on_bell()
140
+ v, t = terminal_view "hello world\r\n"
141
+ bells = []
142
+ v.on(:bell) {|e| bells << e.count}
143
+
144
+ v.on_update nil
145
+ assert_equal [], bells
146
+
147
+ # a bell moves nothing on the screen, so it has to be picked up even
148
+ # when the update leaves nothing to draw
149
+ t.feed "\a\a"
150
+ v.on_update nil
151
+ assert_equal [2], bells
152
+
153
+ # answered once: the same bells do not come round again
154
+ v.on_update nil
155
+ assert_equal [2], bells
156
+ end
157
+
158
+ def test_on_bell_with_a_rung_terminal()
159
+ # the bells a terminal rang before the view was attached to it are
160
+ # not the view's to answer
161
+ t = Reflex::Terminal.new 20, 4
162
+ t.feed "\a\a"
163
+ v = view terminal: t
164
+
165
+ bells = []
166
+ v.on(:bell) {|e| bells << e.count}
167
+ v.on_update nil
168
+ assert_equal [], bells
169
+
170
+ t.feed "\a"
171
+ v.on_update nil
172
+ assert_equal [1], bells
173
+ end
174
+
175
+ def test_to_cell()
176
+ # clamped so that a drag leaving the view keeps selecting up to the edge
177
+ v, = terminal_view "hello world\r\n"
178
+ assert_equal [0, 0], v.send(:to_cell, -50, -50)
179
+ assert_equal [19, 3], v.send(:to_cell, 1e6, 1e6)
180
+ end
181
+
182
+ end unless linux?# rays has no font lookup by name there
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: reflex-terminal
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - xordog
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2026-08-04 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: xot
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.4.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.4.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: rucy
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.4.0
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.4.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: rays
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.4.0
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.4.0
55
+ - !ruby/object:Gem::Dependency
56
+ name: reflexion
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.6.0
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.6.0
69
+ description: Provides Reflex::Terminal, a headless terminal emulator model built on
70
+ libghostty-vt, and Reflex::TerminalView, a View that renders it in the Reflex GUI
71
+ toolkit.
72
+ email: xordog@gmail.com
73
+ executables: []
74
+ extensions:
75
+ - Rakefile
76
+ extra_rdoc_files:
77
+ - ".doc/ext/reflex-terminal/native.cpp"
78
+ - ".doc/ext/reflex-terminal/renderer.cpp"
79
+ - ".doc/ext/reflex-terminal/terminal.cpp"
80
+ files:
81
+ - ".doc/ext/reflex-terminal/native.cpp"
82
+ - ".doc/ext/reflex-terminal/renderer.cpp"
83
+ - ".doc/ext/reflex-terminal/terminal.cpp"
84
+ - ".github/PULL_REQUEST_TEMPLATE.md"
85
+ - ".github/workflows/release-gem.yml"
86
+ - ".github/workflows/tag.yml"
87
+ - ".github/workflows/test.yml"
88
+ - ".github/workflows/utils.rb"
89
+ - CONTRIBUTING.md
90
+ - ChangeLog.md
91
+ - Gemfile
92
+ - LICENSE
93
+ - README.md
94
+ - Rakefile
95
+ - VERSION
96
+ - examples/terminal.rb
97
+ - ext/reflex-terminal/defs.h
98
+ - ext/reflex-terminal/extconf.rb
99
+ - ext/reflex-terminal/native.cpp
100
+ - ext/reflex-terminal/renderer.cpp
101
+ - ext/reflex-terminal/terminal.cpp
102
+ - include/reflex-terminal.h
103
+ - include/reflex-terminal/defs.h
104
+ - include/reflex-terminal/renderer.h
105
+ - include/reflex-terminal/ruby.h
106
+ - include/reflex-terminal/ruby/renderer.h
107
+ - include/reflex-terminal/ruby/terminal.h
108
+ - include/reflex/terminal.h
109
+ - lib/reflex-terminal.rb
110
+ - lib/reflex-terminal/ext.rb
111
+ - lib/reflex-terminal/extension.rb
112
+ - lib/reflex/bell_event.rb
113
+ - lib/reflex/terminal.rb
114
+ - lib/reflex/terminal_view.rb
115
+ - reflex-terminal.gemspec
116
+ - src/pty.cpp
117
+ - src/renderer.cpp
118
+ - src/terminal.cpp
119
+ - src/terminal.h
120
+ - src/win32/pty.cpp
121
+ - test/helper.rb
122
+ - test/test_renderer.rb
123
+ - test/test_terminal.rb
124
+ - test/test_terminal_view.rb
125
+ homepage: https://github.com/xord/reflex-terminal
126
+ licenses:
127
+ - MIT
128
+ metadata: {}
129
+ post_install_message:
130
+ rdoc_options: []
131
+ require_paths:
132
+ - lib
133
+ required_ruby_version: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: 3.0.0
138
+ required_rubygems_version: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ requirements: []
144
+ rubygems_version: 3.4.19
145
+ signing_key:
146
+ specification_version: 4
147
+ summary: A terminal emulator view for Reflex.
148
+ test_files:
149
+ - test/helper.rb
150
+ - test/test_renderer.rb
151
+ - test/test_terminal.rb
152
+ - test/test_terminal_view.rb