ratatui_ruby 1.0.0 → 1.0.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 (236) 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 -232
  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 -146
  13. data/CHANGELOG.md +0 -710
  14. data/README.md +0 -187
  15. data/README.rdoc +0 -302
  16. data/Rakefile +0 -11
  17. data/Steepfile +0 -49
  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 -420
  27. data/doc/contributors/design/rust_backend.md +0 -422
  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/todo/align/api_completeness_audit-finished.md +0 -375
  33. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -206
  34. data/doc/contributors/todo/align/terminal.md +0 -647
  35. data/doc/contributors/todo/future_work.md +0 -169
  36. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  37. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  38. data/doc/custom.css +0 -22
  39. data/doc/getting_started/quickstart.md +0 -291
  40. data/doc/getting_started/why.md +0 -93
  41. data/doc/images/app_all_events.png +0 -0
  42. data/doc/images/app_cli_rich_moments.gif +0 -0
  43. data/doc/images/app_color_picker.png +0 -0
  44. data/doc/images/app_debugging_showcase.gif +0 -0
  45. data/doc/images/app_debugging_showcase.png +0 -0
  46. data/doc/images/app_login_form.png +0 -0
  47. data/doc/images/app_stateful_interaction.png +0 -0
  48. data/doc/images/verify_quickstart_dsl.png +0 -0
  49. data/doc/images/verify_quickstart_layout.png +0 -0
  50. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  51. data/doc/images/verify_readme_usage.png +0 -0
  52. data/doc/images/widget_barchart.png +0 -0
  53. data/doc/images/widget_block.png +0 -0
  54. data/doc/images/widget_box.png +0 -0
  55. data/doc/images/widget_calendar.png +0 -0
  56. data/doc/images/widget_canvas.png +0 -0
  57. data/doc/images/widget_cell.png +0 -0
  58. data/doc/images/widget_center.png +0 -0
  59. data/doc/images/widget_chart.png +0 -0
  60. data/doc/images/widget_gauge.png +0 -0
  61. data/doc/images/widget_layout_split.png +0 -0
  62. data/doc/images/widget_line_gauge.png +0 -0
  63. data/doc/images/widget_list.png +0 -0
  64. data/doc/images/widget_map.png +0 -0
  65. data/doc/images/widget_overlay.png +0 -0
  66. data/doc/images/widget_popup.png +0 -0
  67. data/doc/images/widget_ratatui_logo.png +0 -0
  68. data/doc/images/widget_ratatui_mascot.png +0 -0
  69. data/doc/images/widget_rect.png +0 -0
  70. data/doc/images/widget_render.png +0 -0
  71. data/doc/images/widget_rich_text.png +0 -0
  72. data/doc/images/widget_scroll_text.png +0 -0
  73. data/doc/images/widget_scrollbar.png +0 -0
  74. data/doc/images/widget_sparkline.png +0 -0
  75. data/doc/images/widget_style_colors.png +0 -0
  76. data/doc/images/widget_table.png +0 -0
  77. data/doc/images/widget_tabs.png +0 -0
  78. data/doc/images/widget_text_width.png +0 -0
  79. data/doc/index.md +0 -39
  80. data/doc/troubleshooting/async.md +0 -4
  81. data/doc/troubleshooting/terminal_limitations.md +0 -131
  82. data/doc/troubleshooting/tui_output.md +0 -197
  83. data/examples/app_all_events/README.md +0 -114
  84. data/examples/app_all_events/app.rb +0 -98
  85. data/examples/app_all_events/model/app_model.rb +0 -159
  86. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  87. data/examples/app_all_events/model/event_entry.rb +0 -94
  88. data/examples/app_all_events/model/msg.rb +0 -39
  89. data/examples/app_all_events/model/timestamp.rb +0 -56
  90. data/examples/app_all_events/update.rb +0 -75
  91. data/examples/app_all_events/view/app_view.rb +0 -80
  92. data/examples/app_all_events/view/controls_view.rb +0 -54
  93. data/examples/app_all_events/view/counts_view.rb +0 -61
  94. data/examples/app_all_events/view/live_view.rb +0 -72
  95. data/examples/app_all_events/view/log_view.rb +0 -57
  96. data/examples/app_all_events/view.rb +0 -9
  97. data/examples/app_cli_rich_moments/README.md +0 -81
  98. data/examples/app_cli_rich_moments/app.rb +0 -189
  99. data/examples/app_color_picker/README.md +0 -156
  100. data/examples/app_color_picker/app.rb +0 -76
  101. data/examples/app_color_picker/clipboard.rb +0 -86
  102. data/examples/app_color_picker/color.rb +0 -193
  103. data/examples/app_color_picker/controls.rb +0 -92
  104. data/examples/app_color_picker/copy_dialog.rb +0 -168
  105. data/examples/app_color_picker/export_pane.rb +0 -128
  106. data/examples/app_color_picker/harmony.rb +0 -58
  107. data/examples/app_color_picker/input.rb +0 -176
  108. data/examples/app_color_picker/main_container.rb +0 -180
  109. data/examples/app_color_picker/palette.rb +0 -111
  110. data/examples/app_debugging_showcase/README.md +0 -119
  111. data/examples/app_debugging_showcase/app.rb +0 -318
  112. data/examples/app_login_form/README.md +0 -58
  113. data/examples/app_login_form/app.rb +0 -109
  114. data/examples/app_stateful_interaction/README.md +0 -35
  115. data/examples/app_stateful_interaction/app.rb +0 -328
  116. data/examples/timeout_demo.rb +0 -45
  117. data/examples/verify_quickstart_dsl/README.md +0 -55
  118. data/examples/verify_quickstart_dsl/app.rb +0 -49
  119. data/examples/verify_quickstart_layout/README.md +0 -77
  120. data/examples/verify_quickstart_layout/app.rb +0 -73
  121. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  122. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  123. data/examples/verify_readme_usage/README.md +0 -49
  124. data/examples/verify_readme_usage/app.rb +0 -42
  125. data/examples/verify_website_managed/README.md +0 -48
  126. data/examples/verify_website_managed/app.rb +0 -36
  127. data/examples/verify_website_menu/README.md +0 -60
  128. data/examples/verify_website_menu/app.rb +0 -84
  129. data/examples/verify_website_spinner/README.md +0 -44
  130. data/examples/verify_website_spinner/app.rb +0 -34
  131. data/examples/widget_barchart/README.md +0 -58
  132. data/examples/widget_barchart/app.rb +0 -240
  133. data/examples/widget_block/README.md +0 -44
  134. data/examples/widget_block/app.rb +0 -258
  135. data/examples/widget_box/README.md +0 -54
  136. data/examples/widget_box/app.rb +0 -255
  137. data/examples/widget_calendar/README.md +0 -48
  138. data/examples/widget_calendar/app.rb +0 -115
  139. data/examples/widget_canvas/README.md +0 -31
  140. data/examples/widget_canvas/app.rb +0 -130
  141. data/examples/widget_cell/README.md +0 -45
  142. data/examples/widget_cell/app.rb +0 -112
  143. data/examples/widget_center/README.md +0 -33
  144. data/examples/widget_center/app.rb +0 -118
  145. data/examples/widget_chart/README.md +0 -50
  146. data/examples/widget_chart/app.rb +0 -220
  147. data/examples/widget_gauge/README.md +0 -50
  148. data/examples/widget_gauge/app.rb +0 -229
  149. data/examples/widget_layout_split/README.md +0 -53
  150. data/examples/widget_layout_split/app.rb +0 -260
  151. data/examples/widget_line_gauge/README.md +0 -50
  152. data/examples/widget_line_gauge/app.rb +0 -219
  153. data/examples/widget_list/README.md +0 -58
  154. data/examples/widget_list/app.rb +0 -384
  155. data/examples/widget_map/README.md +0 -48
  156. data/examples/widget_map/app.rb +0 -95
  157. data/examples/widget_overlay/README.md +0 -45
  158. data/examples/widget_overlay/app.rb +0 -250
  159. data/examples/widget_popup/README.md +0 -45
  160. data/examples/widget_popup/app.rb +0 -106
  161. data/examples/widget_ratatui_logo/README.md +0 -43
  162. data/examples/widget_ratatui_logo/app.rb +0 -104
  163. data/examples/widget_ratatui_mascot/README.md +0 -43
  164. data/examples/widget_ratatui_mascot/app.rb +0 -95
  165. data/examples/widget_rect/README.md +0 -53
  166. data/examples/widget_rect/app.rb +0 -222
  167. data/examples/widget_render/README.md +0 -46
  168. data/examples/widget_render/app.rb +0 -186
  169. data/examples/widget_render/app.rbs +0 -41
  170. data/examples/widget_rich_text/README.md +0 -44
  171. data/examples/widget_rich_text/app.rb +0 -193
  172. data/examples/widget_scroll_text/README.md +0 -46
  173. data/examples/widget_scroll_text/app.rb +0 -109
  174. data/examples/widget_scrollbar/README.md +0 -46
  175. data/examples/widget_scrollbar/app.rb +0 -155
  176. data/examples/widget_sparkline/README.md +0 -51
  177. data/examples/widget_sparkline/app.rb +0 -277
  178. data/examples/widget_style_colors/README.md +0 -43
  179. data/examples/widget_style_colors/app.rb +0 -83
  180. data/examples/widget_table/README.md +0 -57
  181. data/examples/widget_table/app.rb +0 -279
  182. data/examples/widget_tabs/README.md +0 -50
  183. data/examples/widget_tabs/app.rb +0 -183
  184. data/examples/widget_text_width/README.md +0 -44
  185. data/examples/widget_text_width/app.rb +0 -117
  186. data/migrate_to_buffer.rb +0 -145
  187. data/mise.toml +0 -8
  188. data/tasks/autodoc/examples.rb +0 -87
  189. data/tasks/autodoc/member.rb +0 -58
  190. data/tasks/autodoc/name.rb +0 -21
  191. data/tasks/autodoc.rake +0 -21
  192. data/tasks/bump/cargo_lockfile.rb +0 -21
  193. data/tasks/bump/changelog.rb +0 -47
  194. data/tasks/bump/header.rb +0 -32
  195. data/tasks/bump/history.rb +0 -32
  196. data/tasks/bump/links.rb +0 -69
  197. data/tasks/bump/manifest.rb +0 -33
  198. data/tasks/bump/ruby_gem.rb +0 -49
  199. data/tasks/bump/sem_ver.rb +0 -40
  200. data/tasks/bump/unreleased_section.rb +0 -56
  201. data/tasks/bump.rake +0 -51
  202. data/tasks/doc.rake +0 -887
  203. data/tasks/example_viewer.html.erb +0 -172
  204. data/tasks/extension.rake +0 -14
  205. data/tasks/license/headers_md.rb +0 -223
  206. data/tasks/license/headers_rb.rb +0 -210
  207. data/tasks/license/license_utils.rb +0 -130
  208. data/tasks/license/snippets_md.rb +0 -315
  209. data/tasks/license/snippets_rdoc.rb +0 -150
  210. data/tasks/license.rake +0 -91
  211. data/tasks/lint.rake +0 -170
  212. data/tasks/rdoc_config.rb +0 -29
  213. data/tasks/resources/build.yml.erb +0 -60
  214. data/tasks/resources/index.html.erb +0 -141
  215. data/tasks/resources/rubies.yml +0 -7
  216. data/tasks/sourcehut.rake +0 -110
  217. data/tasks/steep.rake +0 -11
  218. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  219. data/tasks/terminal_preview/crash_report.rb +0 -54
  220. data/tasks/terminal_preview/example_app.rb +0 -27
  221. data/tasks/terminal_preview/launcher_script.rb +0 -48
  222. data/tasks/terminal_preview/preview_collection.rb +0 -60
  223. data/tasks/terminal_preview/preview_timing.rb +0 -24
  224. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  225. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  226. data/tasks/terminal_preview/system_appearance.rb +0 -13
  227. data/tasks/terminal_preview/terminal_window.rb +0 -138
  228. data/tasks/terminal_preview/window_id.rb +0 -16
  229. data/tasks/terminal_preview.rake +0 -30
  230. data/tasks/test.rake +0 -33
  231. data/tasks/website/index_page.rb +0 -30
  232. data/tasks/website/version.rb +0 -127
  233. data/tasks/website/version_menu.rb +0 -68
  234. data/tasks/website/versioned_documentation.rb +0 -83
  235. data/tasks/website/website.rb +0 -53
  236. 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
-