ratatui_ruby 1.3.0 → 1.3.3

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 (301) hide show
  1. checksums.yaml +4 -4
  2. data/ext/ratatui_ruby/Cargo.lock +2 -1
  3. data/ext/ratatui_ruby/Cargo.toml +2 -1
  4. data/ext/ratatui_ruby/src/color.rs +1 -1
  5. data/ext/ratatui_ruby/src/errors.rs +1 -1
  6. data/ext/ratatui_ruby/src/events.rs +158 -19
  7. data/ext/ratatui_ruby/src/frame.rs +1 -1
  8. data/ext/ratatui_ruby/src/lib.rs +1 -1
  9. data/ext/ratatui_ruby/src/lib_header.rs +1 -1
  10. data/ext/ratatui_ruby/src/rendering.rs +1 -1
  11. data/ext/ratatui_ruby/src/string_width.rs +1 -1
  12. data/ext/ratatui_ruby/src/style.rs +1 -1
  13. data/ext/ratatui_ruby/src/terminal/capabilities.rs +1 -1
  14. data/ext/ratatui_ruby/src/terminal/init.rs +1 -1
  15. data/ext/ratatui_ruby/src/terminal/mod.rs +1 -1
  16. data/ext/ratatui_ruby/src/terminal/mutations.rs +1 -1
  17. data/ext/ratatui_ruby/src/terminal/queries.rs +1 -1
  18. data/ext/ratatui_ruby/src/terminal/query.rs +1 -1
  19. data/ext/ratatui_ruby/src/terminal/storage.rs +1 -1
  20. data/ext/ratatui_ruby/src/terminal/wrapper.rs +1 -1
  21. data/ext/ratatui_ruby/src/text.rs +1 -1
  22. data/ext/ratatui_ruby/src/widgets/barchart.rs +1 -1
  23. data/ext/ratatui_ruby/src/widgets/block.rs +1 -1
  24. data/ext/ratatui_ruby/src/widgets/calendar.rs +1 -1
  25. data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -1
  26. data/ext/ratatui_ruby/src/widgets/center.rs +1 -1
  27. data/ext/ratatui_ruby/src/widgets/chart.rs +1 -1
  28. data/ext/ratatui_ruby/src/widgets/clear.rs +1 -1
  29. data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
  30. data/ext/ratatui_ruby/src/widgets/gauge.rs +1 -1
  31. data/ext/ratatui_ruby/src/widgets/layout.rs +1 -1
  32. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +1 -1
  33. data/ext/ratatui_ruby/src/widgets/list.rs +1 -1
  34. data/ext/ratatui_ruby/src/widgets/list_state.rs +1 -1
  35. data/ext/ratatui_ruby/src/widgets/mod.rs +1 -1
  36. data/ext/ratatui_ruby/src/widgets/overlay.rs +1 -1
  37. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  38. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +1 -1
  39. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +1 -1
  40. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +1 -1
  41. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +1 -1
  42. data/ext/ratatui_ruby/src/widgets/sparkline.rs +1 -1
  43. data/ext/ratatui_ruby/src/widgets/table.rs +1 -1
  44. data/ext/ratatui_ruby/src/widgets/table_state.rs +1 -1
  45. data/ext/ratatui_ruby/src/widgets/tabs.rs +1 -1
  46. data/lib/ratatui_ruby/version.rb +1 -1
  47. metadata +1 -255
  48. data/.builds/ruby-3.2.yml +0 -54
  49. data/.builds/ruby-3.3.yml +0 -54
  50. data/.builds/ruby-3.4.yml +0 -54
  51. data/.builds/ruby-4.0.0.yml +0 -54
  52. data/.pre-commit-config.yaml +0 -16
  53. data/.rubocop.yml +0 -10
  54. data/AGENTS.md +0 -147
  55. data/CHANGELOG.md +0 -771
  56. data/README.md +0 -187
  57. data/README.rdoc +0 -302
  58. data/Rakefile +0 -11
  59. data/Steepfile +0 -50
  60. data/doc/concepts/application_architecture.md +0 -321
  61. data/doc/concepts/application_testing.md +0 -193
  62. data/doc/concepts/async.md +0 -190
  63. data/doc/concepts/custom_widgets.md +0 -247
  64. data/doc/concepts/debugging.md +0 -401
  65. data/doc/concepts/event_handling.md +0 -162
  66. data/doc/concepts/interactive_design.md +0 -146
  67. data/doc/contributors/auditing/parity.md +0 -239
  68. data/doc/contributors/design/ruby_frontend.md +0 -448
  69. data/doc/contributors/design/rust_backend.md +0 -434
  70. data/doc/contributors/design.md +0 -11
  71. data/doc/contributors/developing_examples.md +0 -400
  72. data/doc/contributors/documentation_style.md +0 -121
  73. data/doc/contributors/index.md +0 -21
  74. data/doc/contributors/releasing.md +0 -215
  75. data/doc/contributors/todo/align/api_completeness_audit-finished.md +0 -381
  76. data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +0 -200
  77. data/doc/contributors/todo/align/term.md +0 -351
  78. data/doc/contributors/todo/align/terminal.md +0 -647
  79. data/doc/contributors/todo/future_work.md +0 -169
  80. data/doc/contributors/upstream_requests/paragraph_span_rects.md +0 -259
  81. data/doc/contributors/upstream_requests/tab_rects.md +0 -173
  82. data/doc/contributors/upstream_requests/title_rects.md +0 -132
  83. data/doc/custom.css +0 -22
  84. data/doc/getting_started/quickstart.md +0 -291
  85. data/doc/getting_started/why.md +0 -93
  86. data/doc/images/app_all_events.png +0 -0
  87. data/doc/images/app_cli_rich_moments.gif +0 -0
  88. data/doc/images/app_color_picker.png +0 -0
  89. data/doc/images/app_debugging_showcase.gif +0 -0
  90. data/doc/images/app_debugging_showcase.png +0 -0
  91. data/doc/images/app_external_editor.gif +0 -0
  92. data/doc/images/app_login_form.png +0 -0
  93. data/doc/images/app_stateful_interaction.png +0 -0
  94. data/doc/images/verify_quickstart_dsl.png +0 -0
  95. data/doc/images/verify_quickstart_layout.png +0 -0
  96. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  97. data/doc/images/verify_readme_usage.png +0 -0
  98. data/doc/images/widget_barchart.png +0 -0
  99. data/doc/images/widget_block.png +0 -0
  100. data/doc/images/widget_box.png +0 -0
  101. data/doc/images/widget_calendar.png +0 -0
  102. data/doc/images/widget_canvas.png +0 -0
  103. data/doc/images/widget_cell.png +0 -0
  104. data/doc/images/widget_center.png +0 -0
  105. data/doc/images/widget_chart.png +0 -0
  106. data/doc/images/widget_gauge.png +0 -0
  107. data/doc/images/widget_layout_split.png +0 -0
  108. data/doc/images/widget_line_gauge.png +0 -0
  109. data/doc/images/widget_list.png +0 -0
  110. data/doc/images/widget_map.png +0 -0
  111. data/doc/images/widget_overlay.png +0 -0
  112. data/doc/images/widget_popup.png +0 -0
  113. data/doc/images/widget_ratatui_logo.png +0 -0
  114. data/doc/images/widget_ratatui_mascot.png +0 -0
  115. data/doc/images/widget_rect.png +0 -0
  116. data/doc/images/widget_render.png +0 -0
  117. data/doc/images/widget_rich_text.png +0 -0
  118. data/doc/images/widget_scroll_text.png +0 -0
  119. data/doc/images/widget_scrollbar.png +0 -0
  120. data/doc/images/widget_sparkline.png +0 -0
  121. data/doc/images/widget_style_colors.png +0 -0
  122. data/doc/images/widget_table.png +0 -0
  123. data/doc/images/widget_tabs.png +0 -0
  124. data/doc/images/widget_text_width.png +0 -0
  125. data/doc/index.md +0 -34
  126. data/doc/troubleshooting/async.md +0 -4
  127. data/doc/troubleshooting/terminal_limitations.md +0 -131
  128. data/doc/troubleshooting/tui_output.md +0 -197
  129. data/examples/app_all_events/README.md +0 -114
  130. data/examples/app_all_events/app.rb +0 -98
  131. data/examples/app_all_events/model/app_model.rb +0 -159
  132. data/examples/app_all_events/model/event_color_cycle.rb +0 -43
  133. data/examples/app_all_events/model/event_entry.rb +0 -94
  134. data/examples/app_all_events/model/msg.rb +0 -39
  135. data/examples/app_all_events/model/timestamp.rb +0 -56
  136. data/examples/app_all_events/update.rb +0 -75
  137. data/examples/app_all_events/view/app_view.rb +0 -80
  138. data/examples/app_all_events/view/controls_view.rb +0 -54
  139. data/examples/app_all_events/view/counts_view.rb +0 -61
  140. data/examples/app_all_events/view/live_view.rb +0 -72
  141. data/examples/app_all_events/view/log_view.rb +0 -57
  142. data/examples/app_all_events/view.rb +0 -9
  143. data/examples/app_cli_rich_moments/README.md +0 -81
  144. data/examples/app_cli_rich_moments/app.rb +0 -189
  145. data/examples/app_color_picker/README.md +0 -156
  146. data/examples/app_color_picker/app.rb +0 -76
  147. data/examples/app_color_picker/clipboard.rb +0 -86
  148. data/examples/app_color_picker/color.rb +0 -193
  149. data/examples/app_color_picker/controls.rb +0 -92
  150. data/examples/app_color_picker/copy_dialog.rb +0 -168
  151. data/examples/app_color_picker/export_pane.rb +0 -128
  152. data/examples/app_color_picker/harmony.rb +0 -58
  153. data/examples/app_color_picker/input.rb +0 -176
  154. data/examples/app_color_picker/main_container.rb +0 -180
  155. data/examples/app_color_picker/palette.rb +0 -111
  156. data/examples/app_debugging_showcase/README.md +0 -119
  157. data/examples/app_debugging_showcase/app.rb +0 -318
  158. data/examples/app_external_editor/README.md +0 -62
  159. data/examples/app_external_editor/app.rb +0 -344
  160. data/examples/app_login_form/README.md +0 -58
  161. data/examples/app_login_form/app.rb +0 -109
  162. data/examples/app_stateful_interaction/README.md +0 -35
  163. data/examples/app_stateful_interaction/app.rb +0 -328
  164. data/examples/timeout_demo.rb +0 -45
  165. data/examples/verify_quickstart_dsl/README.md +0 -55
  166. data/examples/verify_quickstart_dsl/app.rb +0 -49
  167. data/examples/verify_quickstart_layout/README.md +0 -77
  168. data/examples/verify_quickstart_layout/app.rb +0 -73
  169. data/examples/verify_quickstart_lifecycle/README.md +0 -68
  170. data/examples/verify_quickstart_lifecycle/app.rb +0 -62
  171. data/examples/verify_readme_usage/README.md +0 -49
  172. data/examples/verify_readme_usage/app.rb +0 -42
  173. data/examples/verify_website_managed/README.md +0 -48
  174. data/examples/verify_website_managed/app.rb +0 -36
  175. data/examples/verify_website_menu/README.md +0 -60
  176. data/examples/verify_website_menu/app.rb +0 -84
  177. data/examples/verify_website_spinner/README.md +0 -44
  178. data/examples/verify_website_spinner/app.rb +0 -34
  179. data/examples/widget_barchart/README.md +0 -58
  180. data/examples/widget_barchart/app.rb +0 -240
  181. data/examples/widget_block/README.md +0 -44
  182. data/examples/widget_block/app.rb +0 -258
  183. data/examples/widget_box/README.md +0 -54
  184. data/examples/widget_box/app.rb +0 -255
  185. data/examples/widget_calendar/README.md +0 -48
  186. data/examples/widget_calendar/app.rb +0 -115
  187. data/examples/widget_canvas/README.md +0 -31
  188. data/examples/widget_canvas/app.rb +0 -130
  189. data/examples/widget_cell/README.md +0 -45
  190. data/examples/widget_cell/app.rb +0 -112
  191. data/examples/widget_center/README.md +0 -33
  192. data/examples/widget_center/app.rb +0 -118
  193. data/examples/widget_chart/README.md +0 -50
  194. data/examples/widget_chart/app.rb +0 -220
  195. data/examples/widget_gauge/README.md +0 -50
  196. data/examples/widget_gauge/app.rb +0 -229
  197. data/examples/widget_layout_split/README.md +0 -53
  198. data/examples/widget_layout_split/app.rb +0 -260
  199. data/examples/widget_line_gauge/README.md +0 -50
  200. data/examples/widget_line_gauge/app.rb +0 -219
  201. data/examples/widget_list/README.md +0 -58
  202. data/examples/widget_list/app.rb +0 -382
  203. data/examples/widget_map/README.md +0 -48
  204. data/examples/widget_map/app.rb +0 -95
  205. data/examples/widget_overlay/README.md +0 -45
  206. data/examples/widget_overlay/app.rb +0 -250
  207. data/examples/widget_popup/README.md +0 -45
  208. data/examples/widget_popup/app.rb +0 -106
  209. data/examples/widget_ratatui_logo/README.md +0 -43
  210. data/examples/widget_ratatui_logo/app.rb +0 -104
  211. data/examples/widget_ratatui_mascot/README.md +0 -43
  212. data/examples/widget_ratatui_mascot/app.rb +0 -95
  213. data/examples/widget_rect/README.md +0 -53
  214. data/examples/widget_rect/app.rb +0 -222
  215. data/examples/widget_render/README.md +0 -46
  216. data/examples/widget_render/app.rb +0 -186
  217. data/examples/widget_render/app.rbs +0 -41
  218. data/examples/widget_rich_text/README.md +0 -44
  219. data/examples/widget_rich_text/app.rb +0 -193
  220. data/examples/widget_scroll_text/README.md +0 -46
  221. data/examples/widget_scroll_text/app.rb +0 -109
  222. data/examples/widget_scrollbar/README.md +0 -46
  223. data/examples/widget_scrollbar/app.rb +0 -155
  224. data/examples/widget_sparkline/README.md +0 -51
  225. data/examples/widget_sparkline/app.rb +0 -277
  226. data/examples/widget_style_colors/README.md +0 -43
  227. data/examples/widget_style_colors/app.rb +0 -83
  228. data/examples/widget_table/README.md +0 -57
  229. data/examples/widget_table/app.rb +0 -285
  230. data/examples/widget_tabs/README.md +0 -50
  231. data/examples/widget_tabs/app.rb +0 -183
  232. data/examples/widget_text_width/README.md +0 -44
  233. data/examples/widget_text_width/app.rb +0 -117
  234. data/migrate_to_buffer.rb +0 -145
  235. data/mise.toml +0 -8
  236. data/tasks/autodoc/examples.rb +0 -87
  237. data/tasks/autodoc/member.rb +0 -58
  238. data/tasks/autodoc/name.rb +0 -21
  239. data/tasks/autodoc.rake +0 -21
  240. data/tasks/bump/bump_workflow.rb +0 -49
  241. data/tasks/bump/cargo_lockfile.rb +0 -21
  242. data/tasks/bump/changelog.rb +0 -104
  243. data/tasks/bump/header.rb +0 -32
  244. data/tasks/bump/history.rb +0 -32
  245. data/tasks/bump/links.rb +0 -69
  246. data/tasks/bump/manifest.rb +0 -33
  247. data/tasks/bump/patch_release.rb +0 -19
  248. data/tasks/bump/release_branch.rb +0 -17
  249. data/tasks/bump/release_from_trunk.rb +0 -49
  250. data/tasks/bump/repository.rb +0 -54
  251. data/tasks/bump/ruby_gem.rb +0 -29
  252. data/tasks/bump/sem_ver.rb +0 -44
  253. data/tasks/bump/unreleased_section.rb +0 -73
  254. data/tasks/bump.rake +0 -61
  255. data/tasks/doc/documentation.rb +0 -59
  256. data/tasks/doc/link/file_url.rb +0 -30
  257. data/tasks/doc/link/relative_path.rb +0 -61
  258. data/tasks/doc/link/web_url.rb +0 -55
  259. data/tasks/doc/link.rb +0 -52
  260. data/tasks/doc/link_audit.rb +0 -116
  261. data/tasks/doc/problem.rb +0 -40
  262. data/tasks/doc/source_file.rb +0 -93
  263. data/tasks/doc.rake +0 -905
  264. data/tasks/example_viewer.html.erb +0 -172
  265. data/tasks/extension.rake +0 -14
  266. data/tasks/license/headers_md.rb +0 -223
  267. data/tasks/license/headers_rb.rb +0 -210
  268. data/tasks/license/license_utils.rb +0 -130
  269. data/tasks/license/snippets_md.rb +0 -315
  270. data/tasks/license/snippets_rdoc.rb +0 -150
  271. data/tasks/license.rake +0 -91
  272. data/tasks/lint.rake +0 -170
  273. data/tasks/rbs_predicates/predicate_catalog.rb +0 -52
  274. data/tasks/rbs_predicates/predicate_tests.rb +0 -124
  275. data/tasks/rbs_predicates/rbs_signature.rb +0 -63
  276. data/tasks/rbs_predicates.rake +0 -31
  277. data/tasks/rdoc_config.rb +0 -29
  278. data/tasks/resources/build.yml.erb +0 -60
  279. data/tasks/resources/index.html.erb +0 -141
  280. data/tasks/resources/rubies.yml +0 -7
  281. data/tasks/sourcehut.rake +0 -122
  282. data/tasks/steep.rake +0 -11
  283. data/tasks/terminal_preview/app_screenshot.rb +0 -45
  284. data/tasks/terminal_preview/crash_report.rb +0 -54
  285. data/tasks/terminal_preview/example_app.rb +0 -27
  286. data/tasks/terminal_preview/launcher_script.rb +0 -48
  287. data/tasks/terminal_preview/preview_collection.rb +0 -60
  288. data/tasks/terminal_preview/preview_timing.rb +0 -24
  289. data/tasks/terminal_preview/safety_confirmation.rb +0 -58
  290. data/tasks/terminal_preview/saved_screenshot.rb +0 -56
  291. data/tasks/terminal_preview/system_appearance.rb +0 -13
  292. data/tasks/terminal_preview/terminal_window.rb +0 -138
  293. data/tasks/terminal_preview/window_id.rb +0 -16
  294. data/tasks/terminal_preview.rake +0 -30
  295. data/tasks/test.rake +0 -36
  296. data/tasks/website/index_page.rb +0 -30
  297. data/tasks/website/version.rb +0 -122
  298. data/tasks/website/version_menu.rb +0 -68
  299. data/tasks/website/versioned_documentation.rb +0 -83
  300. data/tasks/website/website.rb +0 -53
  301. 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
-