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,321 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Application Architecture
7
-
8
- Architect robust TUI applications using core lifecycle patterns and API best practices.
9
-
10
- ## Core Concepts
11
-
12
- Your app lives inside a terminal. You need to respect its rules.
13
-
14
- ### Lifecycle Management
15
-
16
- Terminals have state. They remember cursor positions, input modes, and screen buffers.
17
-
18
- **The Problem:** If your app crashes or exits without cleaning up, it "breaks" the user's terminal. The cursor vanishes. Input echoes constantly. The alternate screen doesn't clear.
19
-
20
- **The Solution:** The library's lifecycle manager handles this for you. It enters "raw mode" on startup and guarantees restoration on exit.
21
-
22
- #### Use `RatatuiRuby.run`
23
-
24
- This method acts as a safety net. It initializes the terminal, yields control to your block, and restores the terminal afterwards—even if your code raises an exception.
25
-
26
- <!-- SPDX-SnippetBegin -->
27
- <!--
28
- SPDX-FileCopyrightText: 2026 Kerrick Long
29
- SPDX-License-Identifier: MIT-0
30
- -->
31
- ```ruby
32
- RatatuiRuby.run do |tui|
33
- loop do
34
- tui.draw do |frame|
35
- frame.render_widget(tui.paragraph(text: "Hello"), frame.area)
36
- end
37
- break if tui.poll_event == "q"
38
- end
39
- end
40
- # Terminal is restored here
41
- ```
42
- <!-- SPDX-SnippetEnd -->
43
-
44
- #### Manual Management
45
-
46
- Need granular control? You can initialize and restore the terminal yourself. Use `ensure` blocks to guarantee cleanup.
47
-
48
- <!-- SPDX-SnippetBegin -->
49
- <!--
50
- SPDX-FileCopyrightText: 2026 Kerrick Long
51
- SPDX-License-Identifier: MIT-0
52
- -->
53
- ```ruby
54
- RatatuiRuby.init_terminal
55
- begin
56
- RatatuiRuby.draw do |frame|
57
- frame.render_widget(RatatuiRuby::Widgets::Paragraph.new(text: "Hello"), frame.area)
58
- end
59
- ensure
60
- RatatuiRuby.restore_terminal
61
- # Terminal is restored here
62
- end
63
- ```
64
- <!-- SPDX-SnippetEnd -->
65
-
66
- #### Signal Handling
67
-
68
- External processes send signals. Your TUI must handle them gracefully.
69
-
70
- **The Problem:** If a signal terminates your process before `restore_terminal` runs, the terminal stays in raw mode. Your shell becomes unusable until you type `reset` and press Enter (the text won't echo, but it works).
71
-
72
- **The Solution:** Ruby's default signal handlers work correctly with `ensure` blocks. Most signals unwind the stack, which triggers cleanup.
73
-
74
- | Signal | Source | Terminal Restored? |
75
- |--------|--------|--------------------|
76
- | SIGTERM | `kill -15` | ✓ Yes — ensure runs |
77
- | SIGINT | `kill -2` (not Ctrl+C) | ✓ Yes — ensure runs |
78
- | SIGKILL | `kill -9` | ✗ No — cannot be caught |
79
-
80
- > [!IMPORTANT]
81
- > **Ctrl+C in Raw Mode:** When your app is in raw mode, pressing Ctrl+C does *not* send SIGINT. It's captured as a `:ctrl_c` key event. Handle this in your event loop—don't use `trap("INT")`.
82
-
83
- <!-- SPDX-SnippetBegin -->
84
- <!--
85
- SPDX-FileCopyrightText: 2026 Kerrick Long
86
- SPDX-License-Identifier: MIT-0
87
- -->
88
- ```ruby
89
- RatatuiRuby.run do |tui|
90
- loop do
91
- # ...
92
- event = tui.poll_event
93
- break if event == :ctrl_c # Handle Ctrl+C yourself
94
- end
95
- end
96
- ```
97
- <!-- SPDX-SnippetEnd -->
98
-
99
- **Recovery:** If a TUI app leaves your terminal broken, run `reset` in the shell to restore normal behavior.
100
-
101
-
102
- ### Stateful Widgets
103
-
104
- Most widgets are stateless configuration. You create them, render them, and they are gone. However, the **runtime status** of some widgets (like Lists and Tables) must persist across frames (e.g., scroll offsets or selection).
105
-
106
- **The Problem:** If you re-create a List configuration every frame, you lose the context of where it was scrolled or what was selected. If Ratatui auto-scrolls to a selection, you can't read that new offset back from an immutable input widget.
107
-
108
- **The Solution:** Use "Stateful Rendering". You create a mutable State object (Output/Status) once and pass it to `render_stateful_widget`. **The Widget configuration (Input) is still mandatory**, but the State object (passed separately) captures the runtime changes.
109
-
110
- > [!IMPORTANT]
111
- > **Precedence Rule:** When using `render_stateful_widget`, the **State object is the single source of truth** for selection and offset. Widget properties (`selected_index`, `selected_row`, `offset`) are **ignored**.
112
- >
113
- > For example: `list(selected_index: 0)` with `state.select(5)` → Item 5 is highlighted, not Item 0.
114
-
115
- **Use Case:** When you need to read back the scroll offset (e.g., for mouse hit testing) or persist selection without managing indexes manually.
116
-
117
- <!-- SPDX-SnippetBegin -->
118
- <!--
119
- SPDX-FileCopyrightText: 2026 Kerrick Long
120
- SPDX-License-Identifier: MIT-0
121
- -->
122
- ```ruby
123
- # Initialize state once
124
- @list_state = RatatuiRuby::ListState.new
125
-
126
- RatatuiRuby.run do |tui|
127
- loop do
128
- tui.draw do |frame|
129
- # Create immutable widget (selected_index is ignored in stateful mode)
130
- list = tui.list(items: ["A", "B", "C"])
131
-
132
- # Render with state — state takes precedence
133
- frame.render_stateful_widget(list, frame.area, @list_state)
134
- end
135
-
136
- # Read back offset calculated by Ratatui
137
- puts "Current Scroll Offset: #{@list_state.offset}"
138
- end
139
- end
140
- ```
141
- <!-- SPDX-SnippetEnd -->
142
-
143
- ### API Convenience
144
-
145
- Writing UI trees involves nesting many widgets.
146
-
147
- **The Problem:** Explicitly namespacing `RatatuiRuby::` for every widget (e.g., `RatatuiRuby::Widgets::Paragraph.new`) is tedious. It creates visual noise that hides your layout structure.
148
-
149
- **The Solution:** The TUI API (`tui`) provides shorthand factories for every widget. It yields a TUI object to your block.
150
-
151
- <!-- SPDX-SnippetBegin -->
152
- <!--
153
- SPDX-FileCopyrightText: 2026 Kerrick Long
154
- SPDX-License-Identifier: MIT-0
155
- -->
156
- ```ruby
157
- RatatuiRuby.run do |tui|
158
- loop do
159
- tui.draw do |frame|
160
- # Split layout using Session helpes
161
- sidebar_area, content_area = tui.layout_split(
162
- frame.area,
163
- direction: :horizontal,
164
- constraints: [
165
- tui.constraint_length(20),
166
- tui.constraint_min(0)
167
- ]
168
- )
169
-
170
- # Render sidebar
171
- frame.render_widget(
172
- tui.paragraph(
173
- text: tui.text_line(spans: [
174
- tui.text_span(content: "Side", style: tui.style(fg: :blue)),
175
- tui.text_span(content: "bar")
176
- ]),
177
- block: tui.block(borders: [:all], title: "Nav")
178
- ),
179
- sidebar_area
180
- )
181
-
182
- # Render main content
183
- frame.render_widget(
184
- tui.paragraph(
185
- text: "Main Content",
186
- style: tui.style(fg: :green),
187
- block: tui.block(borders: [:all], title: "Content")
188
- ),
189
- content_area
190
- )
191
- end
192
-
193
- event = tui.poll_event
194
- break if event == "q" || event == :ctrl_c
195
- end
196
- end
197
- ```
198
- <!-- SPDX-SnippetEnd -->
199
-
200
- #### Raw API
201
-
202
- Building your own abstractions? You might prefer explicit class instantiation. The raw constants are always available.
203
-
204
- <!-- SPDX-SnippetBegin -->
205
- <!--
206
- SPDX-FileCopyrightText: 2026 Kerrick Long
207
- SPDX-License-Identifier: MIT-0
208
- -->
209
- ```ruby
210
- RatatuiRuby.run do
211
- loop do
212
- RatatuiRuby.draw do |frame|
213
- # Manual split
214
- rects = RatatuiRuby::Layout::Layout.split(
215
- frame.area,
216
- direction: :horizontal,
217
- constraints: [
218
- RatatuiRuby::Layout::Constraint.length(20),
219
- RatatuiRuby::Layout::Constraint.min(0)
220
- ]
221
- )
222
-
223
- frame.render_widget(
224
- RatatuiRuby::Widgets::Paragraph.new(
225
- text: RatatuiRuby::Text::Line.new(spans: [
226
- RatatuiRuby::Text::Span.new(content: "Side", style: RatatuiRuby::Style::Style.new(fg: :blue)),
227
- RatatuiRuby::Text::Span.new(content: "bar")
228
- ]),
229
- block: RatatuiRuby::Widgets::Block.new(borders: [:all], title: "Nav")
230
- ),
231
- rects[0]
232
- )
233
-
234
- frame.render_widget(
235
- RatatuiRuby::Widgets::Paragraph.new(
236
- text: "Main Content",
237
- style: RatatuiRuby::Style::Style.new(fg: :green),
238
- block: RatatuiRuby::Widgets::Block.new(borders: [:all], title: "Content")
239
- ),
240
- rects[1]
241
- )
242
- end
243
-
244
- event = RatatuiRuby.poll_event
245
- break if event == "q" || event == :ctrl_c
246
- end
247
- end
248
- ```
249
- <!-- SPDX-SnippetEnd -->
250
-
251
- ## Thread and Ractor Safety
252
-
253
- Building for Ruby 4.0's parallel future? Know which objects can travel between Ractors.
254
-
255
- ### Data Objects (Shareable)
256
-
257
- These are deeply frozen and `Ractor.shareable?`. Include them in immutable Models/Messages freely:
258
-
259
- | Object | Source |
260
- |--------|--------|
261
- | `Event::*` | `poll_event` |
262
- | `Cell` | `get_cell_at` |
263
- | `Rect` | `Layout.split`, `Frame#area` |
264
-
265
- ### I/O Handles (Not Shareable)
266
-
267
- These have side effects and are intentionally not shareable:
268
-
269
- | Object | Valid Usage |
270
- |--------|-------------|
271
- | `TUI` | Cache in `@tui` during run loop. Don't include in Models. |
272
- | `Frame` | Pass to helpers during draw block. Invalid after block returns. |
273
-
274
- <!-- SPDX-SnippetBegin -->
275
- <!--
276
- SPDX-FileCopyrightText: 2026 Kerrick Long
277
- SPDX-License-Identifier: MIT-0
278
- -->
279
- ```ruby
280
- # Good: Cache session in instance variable
281
- RatatuiRuby.run do |tui|
282
- @tui = tui
283
- loop { render; handle_input }
284
- end
285
-
286
- # Bad: Include in immutable Model (won't work with Ractors)
287
- Model = Data.define(:tui, :count) # Don't do this
288
- ```
289
- <!-- SPDX-SnippetEnd -->
290
-
291
-
292
- ## Reference Architectures
293
-
294
- Simple scripts work well with valid linear code. Complex apps need structure.
295
-
296
- We provide these reference architectures to inspire you:
297
-
298
- ### Model-View-Update
299
-
300
- **Source:** [examples/app_all_events](../../examples/app_all_events/README.md)
301
-
302
- This pattern implements unidirectional data flow inspired by The Elm Architecture:
303
- * **Model:** A single immutable `Data.define` object holding all application state.
304
- * **Msg:** Semantic value objects that decouple raw events from business logic.
305
- * **Update:** A pure function that computes the next state: `Update.call(msg, model) -> Model`.
306
- * **View:** Pure rendering logic that accepts the immutable Model.
307
-
308
- Use this when you want predictable state management and easy-to-test logic.
309
-
310
- ### Component-Based
311
-
312
- **Source:** [examples/app_color_picker](../../examples/app_color_picker/README.md)
313
-
314
- This pattern addresses the difficulty of mouse interaction and complex UI orchestration:
315
- * **Component Contract:** Every UI element implements `render(tui, frame, area)` and `handle_event(event)`.
316
- * **Encapsulated Hit Testing:** Components cache their render area and check `contains?` internally.
317
- * **Symbolic Signals:** `handle_event` returns semantic symbols (`:consumed`, `:submitted`) instead of just booleans.
318
- * **Container (Mediator):** A parent container routes events via Chain of Responsibility and coordinates cross-component effects.
319
-
320
- Use this when you need rich interactivity (mouse clicks, drag-and-drop) or complex dynamic layouts.
321
-
@@ -1,193 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
- # Application Testing Guide
6
-
7
- This guide explains how to test your RatatuiRuby applications using the provided `RatatuiRuby::TestHelper`.
8
-
9
- ## Overview
10
-
11
- You need to verify that your application looks and behaves correctly. Manually checking every character on a terminal screen is tedious. Dealing with race conditions and complex state management in tests creates friction.
12
-
13
- The `TestHelper` module solves this. It provides a headless "test terminal" to capture output and a suite of robust assertions to verify state.
14
-
15
- Use it to write fast, deterministic tests for your TUI applications.
16
-
17
- ## Setup
18
-
19
- First, require the test helper in your test file or `test_helper.rb`:
20
-
21
- <!-- SPDX-SnippetBegin -->
22
- <!--
23
- SPDX-FileCopyrightText: 2025 Kerrick Long
24
- SPDX-License-Identifier: MIT-0
25
- -->
26
- ```ruby
27
- require "ratatui_ruby/test_helper"
28
- require "minitest/autorun" # or your preferred test framework
29
- ```
30
- <!-- SPDX-SnippetEnd -->
31
-
32
- Then, include the module in your test class:
33
-
34
- <!-- SPDX-SnippetBegin -->
35
- <!--
36
- SPDX-FileCopyrightText: 2025 Kerrick Long
37
- SPDX-License-Identifier: MIT-0
38
- -->
39
- ```ruby
40
- class MyApplicationTest < Minitest::Test
41
- include RatatuiRuby::TestHelper
42
- # ...
43
- end
44
- ```
45
- <!-- SPDX-SnippetEnd -->
46
-
47
- ## Writing a View Test
48
-
49
- To test a view or widget, wrap your assertions in `with_test_terminal`. This sets up a temporary, in-memory backend for Ratatui to draw to.
50
-
51
- 1. **Initialize the terminal:** Call `with_test_terminal`.
52
- 2. **Render your code:** Instantiate your widget and draw it to a frame.
53
- 3. **Assert output:** Check the `buffer_content` against your expectations.
54
-
55
- <!-- SPDX-SnippetBegin -->
56
- <!--
57
- SPDX-FileCopyrightText: 2026 Kerrick Long
58
- SPDX-License-Identifier: MIT-0
59
- -->
60
- ```ruby
61
- def test_rendering
62
- # Uses default 80x24 terminal
63
- with_test_terminal do
64
- # 1. Instantiate your widget
65
- widget = RatatuiRuby::Widgets::Paragraph.new(text: "Hello World")
66
-
67
- # 2. Render it using the Frame API
68
- RatatuiRuby.draw do |frame|
69
- frame.render_widget(widget, frame.area)
70
- end
71
-
72
- # 3. Assert on the output
73
- assert_includes buffer_content.first, "Hello World"
74
- end
75
- end
76
- ```
77
- <!-- SPDX-SnippetEnd -->
78
-
79
- For the full API list, including `buffer_content` and `cursor_position`, see [RatatuiRuby::TestHelper::Terminal](../lib/ratatui_ruby/test_helper/terminal.rb).
80
-
81
- ## Verifying Styles
82
-
83
- You often need to check colors and modifiers (bold, italic) to ensure your highlighting logic works.
84
-
85
- Use `assert_fg_color`, `assert_bg_color`, and modifier helpers like `assert_bold`.
86
-
87
- <!-- SPDX-SnippetBegin -->
88
- <!--
89
- SPDX-FileCopyrightText: 2026 Kerrick Long
90
- SPDX-License-Identifier: MIT-0
91
- -->
92
- ```ruby
93
- # Assert specific cell style
94
- assert_fg_color(:red, 0, 0)
95
- assert_bold(0, 0)
96
-
97
- # Or check a whole area
98
- assert_area_style({ x: 0, y: 0, w: 10, h: 1 }, bg: :blue)
99
- ```
100
- <!-- SPDX-SnippetEnd -->
101
-
102
- See [RatatuiRuby::TestHelper::StyleAssertions](../lib/ratatui_ruby/test_helper/style_assertions.rb) for the comprehensive list of style helpers.
103
-
104
- ## Simulating Input
105
-
106
- You need to test user interactions like typing or clicking. Stubbing `poll_event` directly is brittle.
107
-
108
- Use `inject_event` to push mock events into the queue. This ensures safe, deterministic handling of input.
109
-
110
- > [!IMPORTANT]
111
- > Call `inject_event` inside a `with_test_terminal` block to avoid race conditions.
112
-
113
- <!-- SPDX-SnippetBegin -->
114
- <!--
115
- SPDX-FileCopyrightText: 2026 Kerrick Long
116
- SPDX-License-Identifier: MIT-0
117
- -->
118
- ```ruby
119
- with_test_terminal do
120
- # Simulate 'q' key press
121
- inject_event("key", { code: "q" })
122
-
123
- # The application receives the 'q' event
124
- event = RatatuiRuby.poll_event
125
- assert_equal "q", event.code
126
- end
127
- ```
128
- <!-- SPDX-SnippetEnd -->
129
-
130
- See [RatatuiRuby::TestHelper::EventInjection](../lib/ratatui_ruby/test_helper/event_injection.rb) for helper methods like `inject_keys` and `inject_click`.
131
-
132
- ## Snapshot Testing
133
-
134
- Snapshots let you verify complex layouts without manually asserting every line.
135
-
136
- Use `assert_snapshots` to compare the current screen against stored reference files.
137
-
138
- <!-- SPDX-SnippetBegin -->
139
- <!--
140
- SPDX-FileCopyrightText: 2026 Kerrick Long
141
- SPDX-License-Identifier: MIT-0
142
- -->
143
- ```ruby
144
- with_test_terminal do
145
- MyApp.new.run
146
- assert_snapshots("dashboard_view")
147
- end
148
- ```
149
- <!-- SPDX-SnippetEnd -->
150
-
151
- This generates both `.txt` (plain text) and `.ansi` (styled) snapshot files. The `.ansi` files contain ANSI escape codes—`cat` them in a terminal to see exactly what the screen looked like. For a visual tour of your test suite, try `cat **/*.ansi` in any shell that supports globbing.
152
-
153
- ### Handling Non-Determinism
154
-
155
- Snapshots must be deterministic. Random data or current timestamps will cause test failures ("flakes").
156
-
157
- To prevent this:
158
- 1. **Seed Randomness:** Use a fixed seed for any RNG.
159
- 2. **Stub Time:** Force the application to use a static time.
160
-
161
- For detailed strategies and code examples, see [RatatuiRuby::TestHelper::Snapshot](../lib/ratatui_ruby/test_helper/snapshot.rb).
162
-
163
- ## Isolated View Testing
164
-
165
- Sometimes you want to test a single view component without spinning up the full `TestTerminal` engine.
166
-
167
- Use `MockFrame` and `StubRect` to test render logic in isolation.
168
-
169
- <!-- SPDX-SnippetBegin -->
170
- <!--
171
- SPDX-FileCopyrightText: 2026 Kerrick Long
172
- SPDX-License-Identifier: MIT-0
173
- -->
174
- ```ruby
175
- def test_logs_view
176
- frame = RatatuiRuby::TestHelper::TestDoubles::MockFrame.new
177
- area = RatatuiRuby::TestHelper::TestDoubles::StubRect.new(width: 40, height: 10)
178
-
179
- # Call your view directly
180
- MyView.new.render(frame, area)
181
-
182
- # Inspect what was rendered
183
- rendered = frame.rendered_widgets.first
184
- assert_equal "Logs", rendered[:widget].block.title
185
- end
186
- ```
187
- <!-- SPDX-SnippetEnd -->
188
-
189
- See [RatatuiRuby::TestHelper::TestDoubles](../lib/ratatui_ruby/test_helper/test_doubles.rb).
190
-
191
- ## Example
192
-
193
- Check out the [examples directory](../../examples/) for fully tested applications showcasing these patterns.
@@ -1,190 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Async Operations in TUI Applications
7
-
8
- TUI applications fetch data from APIs, run shell commands, and query databases. These operations take time. Blocking the render loop freezes the interface.
9
-
10
- You want responsive UIs. The checklist shows "Loading..." while data arrives. The interface never hangs.
11
-
12
- This guide explains async patterns that work with raw terminal mode.
13
-
14
- ## The Raw Terminal Problem
15
-
16
- `RatatuiRuby.run` enters raw terminal mode:
17
-
18
- - stdin reconfigures for character-by-character input
19
- - stdout carries terminal escape sequences
20
- - External commands expecting normal terminal I/O fail
21
-
22
- ### What Breaks
23
-
24
- <!-- SPDX-SnippetBegin -->
25
- <!--
26
- SPDX-FileCopyrightText: 2026 Kerrick Long
27
- SPDX-License-Identifier: MIT-0
28
- -->
29
- ```ruby
30
- # These fail inside a Thread during raw mode:
31
- `git ls-remote --tags origin` # Returns empty or hangs
32
- IO.popen(["git", "ls-remote", ...]) # Same
33
- Open3.capture2("git", "ls-remote", ...) # Same
34
- ```
35
- <!-- SPDX-SnippetEnd -->
36
-
37
- The commands succeed synchronously. They fail asynchronously. The difference: thread context inherits the parent's raw terminal state.
38
-
39
- ### Why Threads Fail
40
-
41
- Ruby's GIL releases during I/O. But:
42
-
43
- 1. Subprocesses inherit the parent's terminal state.
44
- 2. Git/SSH try to read credentials from raw-mode stdin.
45
- 3. The read blocks forever. Or returns empty.
46
-
47
- `GIT_TERMINAL_PROMPT=0` prevents prompts. Auth fails silently instead of hanging.
48
-
49
- ## Solutions
50
-
51
- ### Pre-Check Before Raw Mode
52
-
53
- Run slow operations before entering the TUI:
54
-
55
- <!-- SPDX-SnippetBegin -->
56
- <!--
57
- SPDX-FileCopyrightText: 2026 Kerrick Long
58
- SPDX-License-Identifier: MIT-0
59
- -->
60
- ```ruby
61
- def initialize
62
- @data = fetch_data # Runs before RatatuiRuby.run
63
- end
64
- ```
65
- <!-- SPDX-SnippetEnd -->
66
-
67
- **Trade-off**: Delays startup.
68
-
69
- ### Process.spawn with File Output
70
-
71
- Spawn a separate process before entering raw mode. Write results to a temp file. Poll for completion:
72
-
73
- <!-- SPDX-SnippetBegin -->
74
- <!--
75
- SPDX-FileCopyrightText: 2026 Kerrick Long
76
- SPDX-License-Identifier: MIT-0
77
- -->
78
- ```ruby
79
- class AsyncChecker
80
- CACHE_FILE = File.join(Dir.tmpdir, "my_check_result.txt")
81
-
82
- def initialize
83
- @loading = true
84
- @result = nil
85
- @pid = Process.spawn("my-command > #{CACHE_FILE}")
86
- end
87
-
88
- def loading?
89
- return false unless @loading
90
-
91
- # Non-blocking poll
92
- _pid, status = Process.waitpid2(@pid, Process::WNOHANG)
93
- if status
94
- @result = File.read(CACHE_FILE).strip
95
- @loading = false
96
- end
97
- @loading
98
- end
99
- end
100
- ```
101
- <!-- SPDX-SnippetEnd -->
102
-
103
- **Key points**:
104
-
105
- - `Process.spawn` returns immediately.
106
- - The command runs in a separate process, not a thread.
107
- - Results pass through a temp file. Avoids pipe/terminal issues.
108
- - `Process::WNOHANG` polls without blocking.
109
-
110
- ### Thread for CPU-Bound Work
111
-
112
- Ruby threads work for pure computation:
113
-
114
- <!-- SPDX-SnippetBegin -->
115
- <!--
116
- SPDX-FileCopyrightText: 2026 Kerrick Long
117
- SPDX-License-Identifier: MIT-0
118
- -->
119
- ```ruby
120
- Thread.new { @result = expensive_calculation }
121
- ```
122
- <!-- SPDX-SnippetEnd -->
123
-
124
- Avoid threads for shell commands.
125
-
126
- ## Ractors
127
-
128
- Ractors provide true parallelism. Trade-offs:
129
-
130
- - No mutable shared state.
131
- - Limited to Ractor-safe values.
132
- - Terminal I/O issues remain.
133
-
134
- For TUI async, `Process.spawn` solves the problem cleanly.
135
-
136
- ## Pattern Summary
137
-
138
- | Approach | Use Case | Raw Mode Safe? |
139
- |----------|----------|----------------|
140
- | Sync before TUI | Fast checks (<100ms) | Yes |
141
- | Process.spawn + file | Shell commands, network | Yes |
142
- | Thread | CPU-bound Ruby code | Yes |
143
- | Thread + shell | Shell commands | **No** |
144
-
145
- ## Real Example: Git Tag Check
146
-
147
- Check if a tag exists on the remote:
148
-
149
- <!-- SPDX-SnippetBegin -->
150
- <!--
151
- SPDX-FileCopyrightText: 2026 Kerrick Long
152
- SPDX-License-Identifier: MIT-0
153
- -->
154
- ```ruby
155
- class GitRepo
156
- CACHE_FILE = File.join(Dir.tmpdir, "git_tag_pushed.txt")
157
-
158
- def initialize
159
- @version = `git describe --tags --abbrev=0`.strip
160
- @tag_pushed = nil
161
- @loading = true
162
- @pid = Process.spawn(
163
- "git ls-remote --tags origin | grep -q '#{@version}' " \
164
- "&& echo true > #{CACHE_FILE} || echo false > #{CACHE_FILE}"
165
- )
166
- end
167
-
168
- def loading?
169
- return false unless @loading
170
-
171
- _pid, status = Process.waitpid2(@pid, Process::WNOHANG)
172
- if status
173
- @tag_pushed = File.read(CACHE_FILE).strip == "true"
174
- @loading = false
175
- end
176
- @loading
177
- end
178
-
179
- def refresh
180
- # Sync version for manual refresh (user presses 'r')
181
- @loading = true
182
- remote_tags = `git ls-remote --tags origin`.strip
183
- @tag_pushed = remote_tags.include?(@version)
184
- @loading = false
185
- end
186
- end
187
- ```
188
- <!-- SPDX-SnippetEnd -->
189
-
190
- The TUI starts instantly. The tag check runs in the background. The checklist updates when the result arrives.