ratatui_ruby 1.1.0 → 1.1.1

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 (259) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +1 -1
  3. data/ext/ratatui_ruby/Cargo.toml +1 -1
  4. data/lib/ratatui_ruby/version.rb +1 -1
  5. metadata +1 -255
  6. data/.builds/ruby-3.2.yml +0 -54
  7. data/.builds/ruby-3.3.yml +0 -54
  8. data/.builds/ruby-3.4.yml +0 -54
  9. data/.builds/ruby-4.0.0.yml +0 -54
  10. data/.pre-commit-config.yaml +0 -16
  11. data/.rubocop.yml +0 -10
  12. data/AGENTS.md +0 -147
  13. data/CHANGELOG.md +0 -736
  14. data/README.md +0 -187
  15. data/README.rdoc +0 -302
  16. data/Rakefile +0 -11
  17. data/Steepfile +0 -50
  18. data/doc/concepts/application_architecture.md +0 -321
  19. data/doc/concepts/application_testing.md +0 -193
  20. data/doc/concepts/async.md +0 -190
  21. data/doc/concepts/custom_widgets.md +0 -247
  22. data/doc/concepts/debugging.md +0 -401
  23. data/doc/concepts/event_handling.md +0 -162
  24. data/doc/concepts/interactive_design.md +0 -146
  25. data/doc/contributors/auditing/parity.md +0 -239
  26. data/doc/contributors/design/ruby_frontend.md +0 -448
  27. data/doc/contributors/design/rust_backend.md +0 -434
  28. data/doc/contributors/design.md +0 -11
  29. data/doc/contributors/developing_examples.md +0 -400
  30. data/doc/contributors/documentation_style.md +0 -121
  31. data/doc/contributors/index.md +0 -21
  32. data/doc/contributors/releasing.md +0 -215
  33. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  34. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  35. data/doc/contributors/todo/align/term.md +0 -351
  36. data/doc/contributors/todo/align/terminal.md +0 -647
  37. data/doc/contributors/todo/future_work.md +0 -169
  38. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  39. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  40. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  41. data/doc/custom.css +0 -22
  42. data/doc/getting_started/quickstart.md +0 -291
  43. data/doc/getting_started/why.md +0 -93
  44. data/doc/images/app_all_events.png +0 -0
  45. data/doc/images/app_cli_rich_moments.gif +0 -0
  46. data/doc/images/app_color_picker.png +0 -0
  47. data/doc/images/app_debugging_showcase.gif +0 -0
  48. data/doc/images/app_debugging_showcase.png +0 -0
  49. data/doc/images/app_external_editor.gif +0 -0
  50. data/doc/images/app_login_form.png +0 -0
  51. data/doc/images/app_stateful_interaction.png +0 -0
  52. data/doc/images/verify_quickstart_dsl.png +0 -0
  53. data/doc/images/verify_quickstart_layout.png +0 -0
  54. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  55. data/doc/images/verify_readme_usage.png +0 -0
  56. data/doc/images/widget_barchart.png +0 -0
  57. data/doc/images/widget_block.png +0 -0
  58. data/doc/images/widget_box.png +0 -0
  59. data/doc/images/widget_calendar.png +0 -0
  60. data/doc/images/widget_canvas.png +0 -0
  61. data/doc/images/widget_cell.png +0 -0
  62. data/doc/images/widget_center.png +0 -0
  63. data/doc/images/widget_chart.png +0 -0
  64. data/doc/images/widget_gauge.png +0 -0
  65. data/doc/images/widget_layout_split.png +0 -0
  66. data/doc/images/widget_line_gauge.png +0 -0
  67. data/doc/images/widget_list.png +0 -0
  68. data/doc/images/widget_map.png +0 -0
  69. data/doc/images/widget_overlay.png +0 -0
  70. data/doc/images/widget_popup.png +0 -0
  71. data/doc/images/widget_ratatui_logo.png +0 -0
  72. data/doc/images/widget_ratatui_mascot.png +0 -0
  73. data/doc/images/widget_rect.png +0 -0
  74. data/doc/images/widget_render.png +0 -0
  75. data/doc/images/widget_rich_text.png +0 -0
  76. data/doc/images/widget_scroll_text.png +0 -0
  77. data/doc/images/widget_scrollbar.png +0 -0
  78. data/doc/images/widget_sparkline.png +0 -0
  79. data/doc/images/widget_style_colors.png +0 -0
  80. data/doc/images/widget_table.png +0 -0
  81. data/doc/images/widget_tabs.png +0 -0
  82. data/doc/images/widget_text_width.png +0 -0
  83. data/doc/index.md +0 -34
  84. data/doc/troubleshooting/async.md +0 -4
  85. data/doc/troubleshooting/terminal_limitations.md +0 -131
  86. data/doc/troubleshooting/tui_output.md +0 -197
  87. data/examples/app_all_events/README.md +0 -114
  88. data/examples/app_all_events/app.rb +0 -98
  89. data/examples/app_all_events/model/app_model.rb +0 -159
  90. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  91. data/examples/app_all_events/model/event_entry.rb +0 -94
  92. data/examples/app_all_events/model/msg.rb +0 -39
  93. data/examples/app_all_events/model/timestamp.rb +0 -56
  94. data/examples/app_all_events/update.rb +0 -75
  95. data/examples/app_all_events/view/app_view.rb +0 -80
  96. data/examples/app_all_events/view/controls_view.rb +0 -54
  97. data/examples/app_all_events/view/counts_view.rb +0 -61
  98. data/examples/app_all_events/view/live_view.rb +0 -72
  99. data/examples/app_all_events/view/log_view.rb +0 -57
  100. data/examples/app_all_events/view.rb +0 -9
  101. data/examples/app_cli_rich_moments/README.md +0 -81
  102. data/examples/app_cli_rich_moments/app.rb +0 -189
  103. data/examples/app_color_picker/README.md +0 -156
  104. data/examples/app_color_picker/app.rb +0 -76
  105. data/examples/app_color_picker/clipboard.rb +0 -86
  106. data/examples/app_color_picker/color.rb +0 -193
  107. data/examples/app_color_picker/controls.rb +0 -92
  108. data/examples/app_color_picker/copy_dialog.rb +0 -168
  109. data/examples/app_color_picker/export_pane.rb +0 -128
  110. data/examples/app_color_picker/harmony.rb +0 -58
  111. data/examples/app_color_picker/input.rb +0 -176
  112. data/examples/app_color_picker/main_container.rb +0 -180
  113. data/examples/app_color_picker/palette.rb +0 -111
  114. data/examples/app_debugging_showcase/README.md +0 -119
  115. data/examples/app_debugging_showcase/app.rb +0 -318
  116. data/examples/app_external_editor/README.md +0 -62
  117. data/examples/app_external_editor/app.rb +0 -344
  118. data/examples/app_login_form/README.md +0 -58
  119. data/examples/app_login_form/app.rb +0 -109
  120. data/examples/app_stateful_interaction/README.md +0 -35
  121. data/examples/app_stateful_interaction/app.rb +0 -328
  122. data/examples/timeout_demo.rb +0 -45
  123. data/examples/verify_quickstart_dsl/README.md +0 -55
  124. data/examples/verify_quickstart_dsl/app.rb +0 -49
  125. data/examples/verify_quickstart_layout/README.md +0 -77
  126. data/examples/verify_quickstart_layout/app.rb +0 -73
  127. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  128. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  129. data/examples/verify_readme_usage/README.md +0 -49
  130. data/examples/verify_readme_usage/app.rb +0 -42
  131. data/examples/verify_website_managed/README.md +0 -48
  132. data/examples/verify_website_managed/app.rb +0 -36
  133. data/examples/verify_website_menu/README.md +0 -60
  134. data/examples/verify_website_menu/app.rb +0 -84
  135. data/examples/verify_website_spinner/README.md +0 -44
  136. data/examples/verify_website_spinner/app.rb +0 -34
  137. data/examples/widget_barchart/README.md +0 -58
  138. data/examples/widget_barchart/app.rb +0 -240
  139. data/examples/widget_block/README.md +0 -44
  140. data/examples/widget_block/app.rb +0 -258
  141. data/examples/widget_box/README.md +0 -54
  142. data/examples/widget_box/app.rb +0 -255
  143. data/examples/widget_calendar/README.md +0 -48
  144. data/examples/widget_calendar/app.rb +0 -115
  145. data/examples/widget_canvas/README.md +0 -31
  146. data/examples/widget_canvas/app.rb +0 -130
  147. data/examples/widget_cell/README.md +0 -45
  148. data/examples/widget_cell/app.rb +0 -112
  149. data/examples/widget_center/README.md +0 -33
  150. data/examples/widget_center/app.rb +0 -118
  151. data/examples/widget_chart/README.md +0 -50
  152. data/examples/widget_chart/app.rb +0 -220
  153. data/examples/widget_gauge/README.md +0 -50
  154. data/examples/widget_gauge/app.rb +0 -229
  155. data/examples/widget_layout_split/README.md +0 -53
  156. data/examples/widget_layout_split/app.rb +0 -260
  157. data/examples/widget_line_gauge/README.md +0 -50
  158. data/examples/widget_line_gauge/app.rb +0 -219
  159. data/examples/widget_list/README.md +0 -58
  160. data/examples/widget_list/app.rb +0 -382
  161. data/examples/widget_map/README.md +0 -48
  162. data/examples/widget_map/app.rb +0 -95
  163. data/examples/widget_overlay/README.md +0 -45
  164. data/examples/widget_overlay/app.rb +0 -250
  165. data/examples/widget_popup/README.md +0 -45
  166. data/examples/widget_popup/app.rb +0 -106
  167. data/examples/widget_ratatui_logo/README.md +0 -43
  168. data/examples/widget_ratatui_logo/app.rb +0 -104
  169. data/examples/widget_ratatui_mascot/README.md +0 -43
  170. data/examples/widget_ratatui_mascot/app.rb +0 -95
  171. data/examples/widget_rect/README.md +0 -53
  172. data/examples/widget_rect/app.rb +0 -222
  173. data/examples/widget_render/README.md +0 -46
  174. data/examples/widget_render/app.rb +0 -186
  175. data/examples/widget_render/app.rbs +0 -41
  176. data/examples/widget_rich_text/README.md +0 -44
  177. data/examples/widget_rich_text/app.rb +0 -193
  178. data/examples/widget_scroll_text/README.md +0 -46
  179. data/examples/widget_scroll_text/app.rb +0 -109
  180. data/examples/widget_scrollbar/README.md +0 -46
  181. data/examples/widget_scrollbar/app.rb +0 -155
  182. data/examples/widget_sparkline/README.md +0 -51
  183. data/examples/widget_sparkline/app.rb +0 -277
  184. data/examples/widget_style_colors/README.md +0 -43
  185. data/examples/widget_style_colors/app.rb +0 -83
  186. data/examples/widget_table/README.md +0 -57
  187. data/examples/widget_table/app.rb +0 -285
  188. data/examples/widget_tabs/README.md +0 -50
  189. data/examples/widget_tabs/app.rb +0 -183
  190. data/examples/widget_text_width/README.md +0 -44
  191. data/examples/widget_text_width/app.rb +0 -117
  192. data/migrate_to_buffer.rb +0 -145
  193. data/mise.toml +0 -8
  194. data/tasks/autodoc/examples.rb +0 -87
  195. data/tasks/autodoc/member.rb +0 -58
  196. data/tasks/autodoc/name.rb +0 -21
  197. data/tasks/autodoc.rake +0 -21
  198. data/tasks/bump/bump_workflow.rb +0 -49
  199. data/tasks/bump/cargo_lockfile.rb +0 -21
  200. data/tasks/bump/changelog.rb +0 -104
  201. data/tasks/bump/header.rb +0 -32
  202. data/tasks/bump/history.rb +0 -32
  203. data/tasks/bump/links.rb +0 -69
  204. data/tasks/bump/manifest.rb +0 -33
  205. data/tasks/bump/patch_release.rb +0 -19
  206. data/tasks/bump/release_branch.rb +0 -17
  207. data/tasks/bump/release_from_trunk.rb +0 -49
  208. data/tasks/bump/repository.rb +0 -54
  209. data/tasks/bump/ruby_gem.rb +0 -29
  210. data/tasks/bump/sem_ver.rb +0 -44
  211. data/tasks/bump/unreleased_section.rb +0 -73
  212. data/tasks/bump.rake +0 -61
  213. data/tasks/doc/documentation.rb +0 -59
  214. data/tasks/doc/link/file_url.rb +0 -30
  215. data/tasks/doc/link/relative_path.rb +0 -61
  216. data/tasks/doc/link/web_url.rb +0 -55
  217. data/tasks/doc/link.rb +0 -52
  218. data/tasks/doc/link_audit.rb +0 -116
  219. data/tasks/doc/problem.rb +0 -40
  220. data/tasks/doc/source_file.rb +0 -93
  221. data/tasks/doc.rake +0 -905
  222. data/tasks/example_viewer.html.erb +0 -172
  223. data/tasks/extension.rake +0 -14
  224. data/tasks/license/headers_md.rb +0 -223
  225. data/tasks/license/headers_rb.rb +0 -210
  226. data/tasks/license/license_utils.rb +0 -130
  227. data/tasks/license/snippets_md.rb +0 -315
  228. data/tasks/license/snippets_rdoc.rb +0 -150
  229. data/tasks/license.rake +0 -91
  230. data/tasks/lint.rake +0 -170
  231. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  232. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  233. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  234. data/tasks/rbs_predicates.rake +0 -31
  235. data/tasks/rdoc_config.rb +0 -29
  236. data/tasks/resources/build.yml.erb +0 -60
  237. data/tasks/resources/index.html.erb +0 -141
  238. data/tasks/resources/rubies.yml +0 -7
  239. data/tasks/sourcehut.rake +0 -110
  240. data/tasks/steep.rake +0 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  242. data/tasks/terminal_preview/crash_report.rb +0 -54
  243. data/tasks/terminal_preview/example_app.rb +0 -27
  244. data/tasks/terminal_preview/launcher_script.rb +0 -48
  245. data/tasks/terminal_preview/preview_collection.rb +0 -60
  246. data/tasks/terminal_preview/preview_timing.rb +0 -24
  247. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  248. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  249. data/tasks/terminal_preview/system_appearance.rb +0 -13
  250. data/tasks/terminal_preview/terminal_window.rb +0 -138
  251. data/tasks/terminal_preview/window_id.rb +0 -16
  252. data/tasks/terminal_preview.rake +0 -30
  253. data/tasks/test.rake +0 -36
  254. data/tasks/website/index_page.rb +0 -30
  255. data/tasks/website/version.rb +0 -122
  256. data/tasks/website/version_menu.rb +0 -68
  257. data/tasks/website/versioned_documentation.rb +0 -83
  258. data/tasks/website/website.rb +0 -53
  259. data/tasks/website.rake +0 -28
@@ -1,647 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
-
4
- SPDX-License-Identifier: CC-BY-SA-4.0
5
- -->
6
-
7
- # Terminal Object Design Proposals
8
-
9
- ## Executive Summary
10
-
11
- The current ratatui_ruby architecture abstracts Terminal functionality behind module-level methods (`RatatuiRuby.draw`, `RatatuiRuby.poll_event`) and the `TUI` facade class. This document proposes a design for introducing a proper `Terminal` object that aligns with upstream Ratatui's architecture while maintaining Ruby idioms, the established Mullet Architecture, and full backward compatibility.
12
-
13
- ## Background
14
-
15
- ### Current Architecture
16
-
17
- **Module-Level API** (`RatatuiRuby` module):
18
- - `init_terminal`, `restore_terminal`, `run`
19
- - `draw`, `poll_event`, `get_cell_at`
20
- - `get_viewport_area`, `get_terminal_size`
21
- - `insert_before` (for inline viewports)
22
- - `cursor_position`, `cursor_position=`
23
-
24
- **Rust Implementation**:
25
- - Global `TERMINAL` singleton wrapped in `Mutex<Option<TerminalWrapper>>`
26
- - `TerminalWrapper` enum supporting `Crossterm` and `Test` backends
27
- - Direct FFI methods exposed to Ruby
28
-
29
- **Upstream Ratatui Terminal struct** provides:
30
- - `new(backend)`, `with_options(backend, options)` — construction
31
- - `draw(callback)` — frame rendering
32
- - `get_frame()` — direct frame access
33
- - `hide_cursor()`, `show_cursor()`, `get_cursor_position()`, `set_cursor_position()` — cursor control
34
- - `clear()` — screen clearing
35
- - `resize(area)`, `autoresize()` — size management
36
- - `insert_before(height, draw_fn)` — inline viewport insertion
37
- - `backend()`, `backend_mut()` — backend access
38
- - `current_buffer_mut()` — buffer inspection
39
- - `flush()`, `swap_buffers()` — low-level rendering control
40
-
41
- ### Design Principles (from ruby_frontend.md)
42
-
43
- 1. **Ratatui Alignment**: Ruby namespace mirrors Rust module hierarchy
44
- 2. **Two-Layer Architecture (Mullet)**:
45
- - Layer 1: Explicit schema classes (`RatatuiRuby::Widgets::*`)
46
- - Layer 2: Ergonomic DSL (`TUI` facade)
47
- 3. **Explicit Over Magic**: No runtime metaprogramming
48
- 4. **Data-Driven UI**: Immediate mode, immutable data structures
49
- 5. **Separation of Configuration and Status**: Widgets (input) vs State (output)
50
- 6. **No Render Logic in Ruby**: Ruby defines data, Rust renders
51
-
52
- ---
53
-
54
- ## Proposal 1: Terminal Class with Instance-Based API (Full Upstream Port)
55
-
56
- ### Overview
57
- Create a proper `RatatuiRuby::Terminal` class that mirrors the upstream Ratatui Terminal struct. Maintains full backward compatibility via singleton delegation pattern.
58
-
59
- ### Design Philosophy: Rust-Side Object Construction
60
-
61
- **Proposal 1 design decision**: FFI methods return fully-constructed Ruby objects, not raw primitives or hashes.
62
-
63
- Using Magnus, Rust can instantiate Ruby classes directly:
64
- - `_poll_event_instance(timeout)` returns `Event::Key`, `Event::Mouse`, `Event::None`, etc.
65
- - `_viewport_area_instance()` returns `Layout::Rect`
66
- - `_get_cell_at_instance(x, y)` returns `Buffer::Cell`
67
-
68
- This keeps Terminal methods as **thin delegates** to Rust, with all object construction logic centralized in the FFI layer. Ruby code becomes cleaner and less error-prone.
69
-
70
- **Current pattern** (returns hash):
71
- <!-- SPDX-SnippetBegin -->
72
- <!--
73
- SPDX-FileCopyrightText: 2026 Kerrick Long
74
- SPDX-License-Identifier: MIT-0
75
- -->
76
- ```ruby
77
- def poll_event(timeout: 0.016)
78
- raw = _poll_event_instance(timeout)
79
- return Event::None.new.freeze if raw.nil?
80
- case raw[:type]
81
- when :key then Event::Key.new(code: raw[:code], ...)
82
- # ... 30 lines of parsing
83
- end
84
- end
85
- ```
86
- <!-- SPDX-SnippetEnd -->
87
-
88
- **Proposal 1 pattern** (returns object):
89
- <!-- SPDX-SnippetBegin -->
90
- <!--
91
- SPDX-FileCopyrightText: 2026 Kerrick Long
92
- SPDX-License-Identifier: MIT-0
93
- -->
94
- ```ruby
95
- def poll_event(timeout: 0.016)
96
- _poll_event_instance(timeout) # Rust instantiates Event::Key, etc.
97
- end
98
- ```
99
- <!-- SPDX-SnippetEnd -->
100
-
101
- ### Implementation
102
-
103
- <!-- SPDX-SnippetBegin -->
104
- <!--
105
- SPDX-FileCopyrightText: 2026 Kerrick Long
106
- SPDX-License-Identifier: MIT-0
107
- -->
108
- ```ruby
109
- # lib/ratatui_ruby/terminal.rb
110
- module RatatuiRuby
111
- class Terminal
112
- # Construction
113
- def initialize(viewport: :fullscreen, height: nil)
114
- @viewport = resolve_viewport(viewport, height)
115
- _init_terminal_instance(@viewport.type.to_s, @viewport.height)
116
- end
117
-
118
- # Core rendering (thin delegate)
119
- def draw(&block)
120
- _draw_instance(&block)
121
- end
122
-
123
- # Event polling (Rust returns Event objects)
124
- def poll_event(timeout: 0.016)
125
- _poll_event_instance(timeout)
126
- end
127
-
128
- # Cursor control
129
- def hide_cursor
130
- _hide_cursor_instance
131
- end
132
-
133
- def show_cursor
134
- _show_cursor_instance
135
- end
136
-
137
- # Cursor position (Rust returns Layout::Position)
138
- def cursor_position
139
- _get_cursor_position_instance
140
- end
141
-
142
- def cursor_position=(position)
143
- if position.is_a?(Array)
144
- x, y = position
145
- else
146
- x, y = position.x, position.y
147
- end
148
- _set_cursor_position_instance(x, y)
149
- end
150
-
151
- # Viewport operations
152
- def insert_before(height, widget = nil, &block)
153
- content = widget || block&.call
154
- _insert_before_instance(height, content)
155
- end
156
-
157
- # Viewport queries (Rust returns Layout::Rect)
158
- def viewport_area
159
- _get_viewport_area_instance
160
- end
161
-
162
- def terminal_size
163
- _get_terminal_size_instance
164
- end
165
-
166
- # Screen management
167
- def clear
168
- _clear_instance
169
- end
170
-
171
- def resize(width, height)
172
- _resize_instance(width, height)
173
- end
174
-
175
- def autoresize
176
- _autoresize_instance
177
- end
178
-
179
- # Buffer inspection (Rust returns Buffer::Cell)
180
- def get_cell_at(x, y)
181
- _get_cell_at_instance(x, y)
182
- end
183
-
184
- # Low-level rendering control
185
- def flush
186
- _flush_instance
187
- end
188
-
189
- def swap_buffers
190
- _swap_buffers_instance
191
- end
192
-
193
- # Backend access (advanced)
194
- def backend
195
- _backend_instance
196
- end
197
-
198
- def backend_mut
199
- _backend_mut_instance
200
- end
201
-
202
- # Lifecycle
203
- def restore
204
- _restore_terminal_instance
205
- end
206
-
207
- private
208
-
209
- def resolve_viewport(viewport, height)
210
- case viewport
211
- when nil, :fullscreen then Terminal::Viewport.fullscreen
212
- when :inline then Terminal::Viewport.inline(height || 8)
213
- when Terminal::Viewport then viewport
214
- else raise ArgumentError, "Unknown viewport: #{viewport.inspect}"
215
- end
216
- end
217
- end
218
-
219
- # Module-level singleton for backward compatibility
220
- @default_terminal = nil
221
-
222
- class << self
223
- private
224
-
225
- def default_terminal
226
- @default_terminal ||= Terminal.new
227
- end
228
- end
229
-
230
- # Existing module methods delegate to singleton (BACKWARD COMPATIBLE)
231
- def self.draw(&block)
232
- default_terminal.draw(&block)
233
- end
234
-
235
- def self.poll_event(timeout: 0.016)
236
- default_terminal.poll_event(timeout:)
237
- end
238
-
239
- def self.get_cell_at(x, y)
240
- default_terminal.get_cell_at(x, y)
241
- end
242
-
243
- def self.get_viewport_area
244
- default_terminal.viewport_area
245
- end
246
-
247
- def self.get_terminal_size
248
- default_terminal.terminal_size
249
- end
250
-
251
- def self.insert_before(height, widget = nil, &block)
252
- default_terminal.insert_before(height, widget, &block)
253
- end
254
-
255
- # ... (all other module methods delegate similarly)
256
- end
257
-
258
- # TUI facade delegates to the Terminal instance passed to it
259
- class TUI
260
- def initialize(terminal)
261
- @terminal = terminal
262
- end
263
-
264
- # Expose the terminal instance
265
- attr_reader :terminal
266
-
267
- # Delegate core operations to terminal
268
- def draw(&block)
269
- @terminal.draw(&block)
270
- end
271
-
272
- def poll_event(timeout: 0.016)
273
- @terminal.poll_event(timeout:)
274
- end
275
-
276
- # ... (all existing TUI factory methods remain unchanged)
277
- end
278
-
279
- # run creates a Terminal and yields a TUI wrapping it
280
- def self.run(viewport: :fullscreen, height: nil, &block)
281
- terminal = Terminal.new(viewport:, height:)
282
- tui = TUI.new(terminal)
283
- yield tui
284
- ensure
285
- terminal.restore
286
- end
287
- ```
288
- <!-- SPDX-SnippetEnd -->
289
-
290
- ### Usage: Three APIs (Deprecation Plan)
291
-
292
- <!-- SPDX-SnippetBegin -->
293
- <!--
294
- SPDX-FileCopyrightText: 2026 Kerrick Long
295
- SPDX-License-Identifier: MIT-0
296
- -->
297
- ```ruby
298
- # API 1: Beginner-friendly TUI facade (PERMANENT)
299
- # This is the recommended API for most users
300
- RatatuiRuby.run do |tui|
301
- tui.draw { |frame| ... }
302
- event = tui.poll_event
303
-
304
- # Access the underlying Terminal instance
305
- puts tui.terminal.viewport_type # :inline or :fullscreen
306
- puts tui.terminal.width # terminal width
307
- tui.terminal.clear if some_condition
308
- end
309
-
310
- # API 2: Direct module methods (DEPRECATED, remove before v1.0.0)
311
- # This API should never have existed and will be removed
312
- RatatuiRuby.init_terminal
313
- RatatuiRuby.draw { |frame| ... }
314
- event = RatatuiRuby.poll_event
315
- RatatuiRuby.restore_terminal
316
-
317
- # API 3: Explicit Terminal instance (PERMANENT)
318
- # This is aligned with upstream Ratatui and provides explicit resource management
319
- terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 10)
320
- terminal.draw { |frame| ... }
321
- event = terminal.poll_event
322
- terminal.restore
323
- ```
324
- <!-- SPDX-SnippetEnd -->
325
-
326
- **Migration strategy**:
327
- - **API 1** (`RatatuiRuby.run { |tui| }`) remains the primary, beginner-friendly interface
328
- - **API 3** (`Terminal.new`) is the advanced, upstream-aligned interface for explicit control
329
- - **API 2** (module methods like `RatatuiRuby.draw`) exists only for backward compatibility during transition
330
- - Add deprecation warnings immediately after implementing Terminal
331
- - Remove completely before v1.0.0 release
332
-
333
- ### Rust Changes
334
-
335
- The Rust implementation needs significant refactoring to support instance-based terminals:
336
-
337
- **Current Architecture**:
338
- <!-- SPDX-SnippetBegin -->
339
- <!--
340
- SPDX-FileCopyrightText: 2026 Kerrick Long
341
- SPDX-License-Identifier: MIT-0
342
- -->
343
- ```rust
344
- pub static TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
345
- ```
346
- <!-- SPDX-SnippetEnd -->
347
-
348
- **New Architecture**:
349
- <!-- SPDX-SnippetBegin -->
350
- <!--
351
- SPDX-FileCopyrightText: 2026 Kerrick Long
352
- SPDX-License-Identifier: MIT-0
353
- -->
354
- ```rust
355
- // Instance tracking
356
- static TERMINAL_INSTANCES: Mutex<HashMap<u64, TerminalWrapper>> = Mutex::new(HashMap::new());
357
- static NEXT_TERMINAL_ID: AtomicU64 = AtomicU64::new(0);
358
-
359
- pub fn init_terminal_instance(viewport_type: String, height: Option<u16>) -> Result<u64, Error> {
360
- let id = NEXT_TERMINAL_ID.fetch_add(1, Ordering::SeqCst);
361
- let terminal = create_terminal(viewport_type, height)?;
362
-
363
- let mut instances = TERMINAL_INSTANCES.lock().unwrap();
364
- instances.insert(id, terminal);
365
- Ok(id)
366
- }
367
-
368
- pub fn draw_instance(terminal_id: u64, callback: Value) -> Result<(), Error> {
369
- let mut instances = TERMINAL_INSTANCES.lock().unwrap();
370
- let terminal = instances.get_mut(&terminal_id)
371
- .ok_or_else(|| Error::new(error_class, "Terminal instance not found"))?;
372
-
373
- match terminal {
374
- TerminalWrapper::Crossterm(term) => term.draw(|frame| { ... }),
375
- TerminalWrapper::Test(term) => term.draw(|frame| { ... }),
376
- }
377
- }
378
- ```
379
- <!-- SPDX-SnippetEnd -->
380
-
381
- **Module-level methods maintain singleton**:
382
- <!-- SPDX-SnippetBegin -->
383
- <!--
384
- SPDX-FileCopyrightText: 2026 Kerrick Long
385
- SPDX-License-Identifier: MIT-0
386
- -->
387
- ```rust
388
- static DEFAULT_TERMINAL_ID: Mutex<Option<u64>> = Mutex::new(None);
389
-
390
- pub fn init_terminal(viewport_type: String, height: Option<u16>) -> Result<(), Error> {
391
- let id = init_terminal_instance(viewport_type, height)?;
392
- *DEFAULT_TERMINAL_ID.lock().unwrap() = Some(id);
393
- Ok(())
394
- }
395
-
396
- pub fn draw(callback: Value) -> Result<(), Error> {
397
- let default_id = DEFAULT_TERMINAL_ID.lock().unwrap()
398
- .ok_or_else(|| Error::new(error_class, "No default terminal initialized"))?;
399
- draw_instance(default_id, callback)
400
- }
401
- ```
402
- <!-- SPDX-SnippetEnd -->
403
-
404
- **Rust-side object instantiation** (Magnus):
405
- <!-- SPDX-SnippetBegin -->
406
- <!--
407
- SPDX-FileCopyrightText: 2026 Kerrick Long
408
- SPDX-License-Identifier: MIT-0
409
- -->
410
- ```rust
411
- pub fn poll_event_instance(terminal_id: u64, timeout: Option<f64>) -> Result<Value, Error> {
412
- let ruby = magnus::Ruby::get().unwrap();
413
- let module = ruby.define_module("RatatuiRuby")?;
414
- let event_module = module.const_get::<_, RModule>("Event")?;
415
-
416
- match crossterm::event::poll(timeout_duration)? {
417
- true => match crossterm::event::read()? {
418
- CrosstermEvent::Key(key_event) => {
419
- let key_class = event_module.const_get::<_, RClass>("Key")?;
420
- key_class.new_instance((
421
- ("code", extract_key_code(key_event.code)),
422
- ("modifiers", extract_modifiers(key_event.modifiers)),
423
- ("kind", extract_kind(key_event.kind)),
424
- ))?.freeze()
425
- },
426
- CrosstermEvent::Mouse(mouse_event) => {
427
- let mouse_class = event_module.const_get::<_, RClass>("Mouse")?;
428
- mouse_class.new_instance((
429
- ("kind", extract_mouse_kind(mouse_event.kind)),
430
- ("x", mouse_event.column),
431
- ("y", mouse_event.row),
432
- ("button", extract_mouse_button(mouse_event.kind)),
433
- ("modifiers", extract_modifiers(mouse_event.modifiers)),
434
- ))?.freeze()
435
- },
436
- // ... other event types
437
- },
438
- false => {
439
- let none_class = event_module.const_get::<_, RClass>("None")?;
440
- none_class.new_instance(())?.freeze()
441
- }
442
- }
443
- }
444
-
445
- // Similar pattern for Layout::Rect, Buffer::Cell, etc.
446
- pub fn get_viewport_area_instance(terminal_id: u64) -> Result<Value, Error> {
447
- let ruby = magnus::Ruby::get().unwrap();
448
- let module = ruby.define_module("RatatuiRuby")?;
449
- let layout_module = module.const_get::<_, RModule>("Layout")?;
450
- let rect_class = layout_module.const_get::<_, RClass>("Rect")?;
451
-
452
- let area = get_terminal_viewport_area(terminal_id)?;
453
- rect_class.new_instance((
454
- ("x", area.x),
455
- ("y", area.y),
456
- ("width", area.width),
457
- ("height", area.height),
458
- ))
459
- }
460
- ```
461
- <!-- SPDX-SnippetEnd -->
462
-
463
- ### Pros
464
-
465
- ✅ **Full alignment with upstream Ratatui** — Terminal is a first-class object
466
- ✅ **Explicit ownership model** — terminal is a managed resource
467
- ✅ **Supports multiple terminals** (future: testing, headless)
468
- ✅ **Clear lifecycle** — `new`, `draw`, `restore`
469
- ✅ **Non-breaking** — all existing APIs continue to work via singleton delegation
470
- ✅ **Three API tiers** — beginner (TUI), intermediate (module), advanced (instance)
471
- ✅ **MVU-compatible** — `Command.tui(->(tui) { tui.terminal.clear })` provides escape hatch for terminal operations while maintaining Update purity
472
-
473
- ### Cons
474
-
475
- ❌ **Significant Rust refactoring** — requires instance tracking, ID management, and both instance-based FFI methods (`_draw_instance`) and singleton-delegating methods (`_draw`). The global `TERMINAL` mutex must be replaced with an instance registry (`HashMap<u64, TerminalWrapper>`), adding complexity to every terminal operation.
476
-
477
- ---
478
-
479
- ## Verification Plan
480
-
481
- ### Unit Tests
482
-
483
- <!-- SPDX-SnippetBegin -->
484
- <!--
485
- SPDX-FileCopyrightText: 2026 Kerrick Long
486
- SPDX-License-Identifier: MIT-0
487
- -->
488
- ```ruby
489
- # test/test_terminal.rb
490
- class TestTerminal < Minitest::Test
491
- def setup
492
- RatatuiRuby.init_test_terminal(80, 24, "fullscreen")
493
- end
494
-
495
- def teardown
496
- RatatuiRuby.restore_terminal
497
- end
498
-
499
- def test_terminal_instance_creation
500
- terminal = RatatuiRuby::Terminal.new
501
- assert_instance_of RatatuiRuby::Terminal, terminal
502
- end
503
-
504
- def test_terminal_size
505
- terminal = RatatuiRuby::Terminal.new
506
- size = terminal.terminal_size
507
- assert_equal 80, size.width
508
- assert_equal 24, size.height
509
- end
510
-
511
- def test_viewport_type_fullscreen
512
- terminal = RatatuiRuby::Terminal.new
513
- assert_equal :fullscreen, terminal.viewport_type
514
- end
515
-
516
- def test_tui_terminal_access
517
- RatatuiRuby.run do |tui|
518
- assert_instance_of RatatuiRuby::Terminal, tui.terminal
519
- end
520
- end
521
-
522
- def test_backward_compatible_module_methods
523
- # Module methods still work via singleton delegation
524
- RatatuiRuby.draw { |frame| frame.render_widget(...) }
525
- event = RatatuiRuby.poll_event
526
- assert_instance_of RatatuiRuby::Event::None, event
527
- end
528
- end
529
-
530
- # test/test_terminal_inline.rb
531
- class TestTerminalInline < Minitest::Test
532
- def setup
533
- RatatuiRuby.init_test_terminal(80, 24, "inline", 8)
534
- end
535
-
536
- def teardown
537
- RatatuiRuby.restore_terminal
538
- end
539
-
540
- def test_inline_viewport
541
- terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 8)
542
- assert_equal :inline, terminal.viewport_type
543
- end
544
-
545
- def test_insert_before_works_inline
546
- terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 8)
547
- terminal.insert_before(1, RatatuiRuby::Widgets::Paragraph.new(text: "test"))
548
- # Should not raise
549
- end
550
- end
551
- ```
552
- <!-- SPDX-SnippetEnd -->
553
-
554
- ### Integration Tests
555
-
556
- <!-- SPDX-SnippetBegin -->
557
- <!--
558
- SPDX-FileCopyrightText: 2026 Kerrick Long
559
- SPDX-License-Identifier: MIT-0
560
- -->
561
- ```ruby
562
- # examples/terminal_instance_demo.rb
563
- require "ratatui_ruby"
564
-
565
- # API 3: Direct Terminal instance
566
- terminal = RatatuiRuby::Terminal.new(viewport: :inline, height: 10)
567
-
568
- terminal.draw do |frame|
569
- info = <<~INFO
570
- Terminal Information:
571
- - Type: #{terminal.viewport_type}
572
- - Size: #{terminal.width}x#{terminal.height}
573
-
574
- Press 'q' to quit
575
- INFO
576
-
577
- paragraph = RatatuiRuby::Widgets::Paragraph.new(text: info)
578
- frame.render_widget(paragraph, frame.area)
579
- end
580
-
581
- # Insert log above viewport
582
- terminal.insert_before(1, RatatuiRuby::Widgets::Paragraph.new(text: "[LOG] Started"))
583
-
584
- loop do
585
- event = terminal.poll_event
586
- break if event.key? && event.code == "q"
587
- end
588
-
589
- terminal.restore
590
- ```
591
- <!-- SPDX-SnippetEnd -->
592
-
593
- ### Manual Testing Checklist
594
-
595
- - [ ] `Terminal.new` creates new instance
596
- - [ ] `terminal.width` and `terminal.height` match expected dimensions
597
- - [ ] `terminal.viewport_type` returns correct mode
598
- - [ ] `tui.terminal` provides access from TUI facade
599
- - [ ] `terminal.insert_before` works in inline mode
600
- - [ ] `terminal.poll_event` returns Event objects
601
- - [ ] `terminal.cursor_position` returns Position object
602
- - [ ] Module methods (`RatatuiRuby.draw`) still work (backward compat)
603
- - [ ] Deprecation warnings appear for module methods
604
-
605
- ---
606
-
607
- ## Future Enhancements
608
-
609
- ### Command.tui Integration
610
-
611
- Implement the MVU escape hatch:
612
-
613
- <!-- SPDX-SnippetBegin -->
614
- <!--
615
- SPDX-FileCopyrightText: 2026 Kerrick Long
616
- SPDX-License-Identifier: MIT-0
617
- -->
618
- ```ruby
619
- module RatatuiRuby
620
- module Tea
621
- module Command
622
- def self.tui(callable)
623
- TuiCommand.new(callable)
624
- end
625
-
626
- class TuiCommand < Data.define(:callable)
627
- def execute(tui)
628
- callable.call(tui)
629
- nil
630
- end
631
- end
632
- end
633
- end
634
- end
635
-
636
- # Usage in Update
637
- def update(model, message)
638
- case message
639
- when clear_requested
640
- [model, Command.tui(->(tui) { tui.terminal.clear })]
641
- end
642
- end
643
- ```
644
- <!-- SPDX-SnippetEnd -->
645
-
646
-
647
-