ratatui_ruby 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (234) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +6 -0
  7. data/CHANGELOG.md +44 -7
  8. data/README.md +11 -4
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +84 -10
  11. data/doc/application_testing.md +75 -29
  12. data/doc/contributors/design/ruby_frontend.md +39 -3
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +129 -44
  15. data/doc/contributors/examples_audit/p1_high.md +21 -0
  16. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  17. data/doc/contributors/examples_audit.md +41 -0
  18. data/doc/event_handling.md +11 -3
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_color_picker.png +0 -0
  21. data/doc/images/app_login_form.png +0 -0
  22. data/doc/images/app_stateful_interaction.png +0 -0
  23. data/doc/images/verify_quickstart_dsl.png +0 -0
  24. data/doc/images/verify_quickstart_layout.png +0 -0
  25. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  26. data/doc/images/verify_readme_usage.png +0 -0
  27. data/doc/images/widget_barchart_demo.png +0 -0
  28. data/doc/images/widget_block_demo.png +0 -0
  29. data/doc/images/widget_canvas_demo.png +0 -0
  30. data/doc/images/widget_cell_demo.png +0 -0
  31. data/doc/images/widget_center_demo.png +0 -0
  32. data/doc/images/widget_chart_demo.png +0 -0
  33. data/doc/images/widget_list_demo.png +0 -0
  34. data/doc/images/widget_overlay_demo.png +0 -0
  35. data/doc/images/widget_render.png +0 -0
  36. data/doc/images/widget_rich_text.png +0 -0
  37. data/doc/images/widget_scroll_text.png +0 -0
  38. data/doc/images/widget_sparkline_demo.png +0 -0
  39. data/doc/images/widget_table_demo.png +0 -0
  40. data/doc/images/widget_tabs_demo.png +0 -0
  41. data/doc/images/widget_text_width.png +0 -0
  42. data/doc/quickstart.md +69 -76
  43. data/doc/terminal_limitations.md +92 -0
  44. data/examples/app_all_events/README.md +45 -27
  45. data/examples/app_all_events/app.rb +38 -35
  46. data/examples/app_all_events/model/app_model.rb +157 -0
  47. data/examples/app_all_events/model/event_entry.rb +17 -0
  48. data/examples/app_all_events/model/msg.rb +37 -0
  49. data/examples/app_all_events/update.rb +73 -0
  50. data/examples/app_all_events/view/app_view.rb +8 -8
  51. data/examples/app_all_events/view/controls_view.rb +8 -6
  52. data/examples/app_all_events/view/counts_view.rb +12 -8
  53. data/examples/app_all_events/view/live_view.rb +8 -7
  54. data/examples/app_all_events/view/log_view.rb +10 -15
  55. data/examples/app_color_picker/README.md +84 -44
  56. data/examples/app_color_picker/app.rb +24 -62
  57. data/examples/app_color_picker/controls.rb +90 -0
  58. data/examples/app_color_picker/copy_dialog.rb +45 -49
  59. data/examples/app_color_picker/export_pane.rb +126 -0
  60. data/examples/app_color_picker/input.rb +99 -67
  61. data/examples/app_color_picker/main_container.rb +178 -0
  62. data/examples/app_color_picker/palette.rb +55 -26
  63. data/examples/app_login_form/README.md +47 -0
  64. data/examples/app_login_form/app.rb +2 -3
  65. data/examples/app_stateful_interaction/README.md +31 -0
  66. data/examples/app_stateful_interaction/app.rb +272 -0
  67. data/examples/timeout_demo.rb +43 -0
  68. data/examples/verify_quickstart_dsl/README.md +48 -0
  69. data/examples/verify_quickstart_dsl/app.rb +2 -0
  70. data/examples/verify_quickstart_layout/README.md +71 -0
  71. data/examples/verify_quickstart_layout/app.rb +2 -0
  72. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  73. data/examples/verify_quickstart_lifecycle/app.rb +8 -2
  74. data/examples/verify_readme_usage/README.md +43 -0
  75. data/examples/verify_readme_usage/app.rb +8 -2
  76. data/examples/widget_barchart_demo/README.md +49 -0
  77. data/examples/widget_barchart_demo/app.rb +5 -5
  78. data/examples/widget_block_demo/README.md +34 -0
  79. data/examples/widget_block_demo/app.rb +256 -0
  80. data/examples/widget_box_demo/README.md +45 -0
  81. data/examples/widget_calendar_demo/README.md +39 -0
  82. data/examples/widget_canvas_demo/README.md +27 -0
  83. data/examples/widget_canvas_demo/app.rb +123 -0
  84. data/examples/widget_cell_demo/README.md +36 -0
  85. data/examples/widget_cell_demo/app.rb +31 -24
  86. data/examples/widget_center_demo/README.md +29 -0
  87. data/examples/widget_center_demo/app.rb +116 -0
  88. data/examples/widget_chart_demo/README.md +41 -0
  89. data/examples/widget_chart_demo/app.rb +7 -2
  90. data/examples/widget_gauge_demo/README.md +41 -0
  91. data/examples/widget_layout_split/README.md +44 -0
  92. data/examples/widget_line_gauge_demo/README.md +41 -0
  93. data/examples/widget_list_demo/README.md +49 -0
  94. data/examples/widget_list_demo/app.rb +91 -107
  95. data/examples/widget_map_demo/README.md +39 -0
  96. data/examples/{app_map_demo → widget_map_demo}/app.rb +2 -2
  97. data/examples/widget_overlay_demo/app.rb +248 -0
  98. data/examples/widget_popup_demo/README.md +36 -0
  99. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  100. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  101. data/examples/widget_rect/README.md +38 -0
  102. data/examples/widget_render/README.md +37 -0
  103. data/examples/widget_rich_text/README.md +35 -0
  104. data/examples/widget_rich_text/app.rb +62 -33
  105. data/examples/widget_scroll_text/README.md +37 -0
  106. data/examples/widget_scroll_text/app.rb +0 -1
  107. data/examples/widget_scrollbar_demo/README.md +37 -0
  108. data/examples/widget_sparkline_demo/README.md +42 -0
  109. data/examples/widget_sparkline_demo/app.rb +4 -3
  110. data/examples/widget_style_colors/README.md +34 -0
  111. data/examples/widget_table_demo/README.md +48 -0
  112. data/examples/{app_table_select → widget_table_demo}/app.rb +46 -8
  113. data/examples/widget_tabs_demo/README.md +41 -0
  114. data/examples/widget_tabs_demo/app.rb +15 -1
  115. data/examples/widget_text_width/README.md +35 -0
  116. data/examples/widget_text_width/app.rb +106 -0
  117. data/exe/.gitkeep +0 -0
  118. data/ext/ratatui_ruby/Cargo.lock +11 -4
  119. data/ext/ratatui_ruby/Cargo.toml +2 -1
  120. data/ext/ratatui_ruby/src/events.rs +238 -26
  121. data/ext/ratatui_ruby/src/frame.rs +113 -1
  122. data/ext/ratatui_ruby/src/lib.rs +34 -4
  123. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  124. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  125. data/ext/ratatui_ruby/src/text.rs +1 -1
  126. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  127. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  128. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  129. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  130. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  131. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  132. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  133. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  134. data/ext/ratatui_ruby/src/widgets/table.rs +113 -1
  135. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  136. data/lib/ratatui_ruby/cell.rb +4 -4
  137. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  138. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  139. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  140. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  141. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  142. data/lib/ratatui_ruby/event/key.rb +111 -51
  143. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  144. data/lib/ratatui_ruby/event/paste.rb +1 -1
  145. data/lib/ratatui_ruby/frame.rb +96 -0
  146. data/lib/ratatui_ruby/list_state.rb +88 -0
  147. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  148. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  149. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  150. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  151. data/lib/ratatui_ruby/schema/list.rb +25 -4
  152. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  153. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  154. data/lib/ratatui_ruby/schema/style.rb +24 -4
  155. data/lib/ratatui_ruby/schema/table.rb +21 -3
  156. data/lib/ratatui_ruby/schema/text.rb +69 -1
  157. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  158. data/lib/ratatui_ruby/session/autodoc.rb +65 -0
  159. data/lib/ratatui_ruby/session.rb +22 -7
  160. data/lib/ratatui_ruby/table_state.rb +90 -0
  161. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  162. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  163. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  164. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  165. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  166. data/lib/ratatui_ruby/test_helper.rb +65 -358
  167. data/lib/ratatui_ruby/version.rb +1 -1
  168. data/lib/ratatui_ruby.rb +42 -19
  169. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  170. data/sig/examples/widget_block_demo/app.rbs +32 -0
  171. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  172. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  173. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  174. data/sig/ratatui_ruby/event.rbs +11 -1
  175. data/sig/ratatui_ruby/frame.rbs +2 -0
  176. data/sig/ratatui_ruby/list_state.rbs +13 -0
  177. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  178. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  179. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  180. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  181. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  182. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  183. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  184. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  185. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  186. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  187. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  188. data/sig/ratatui_ruby/session.rbs +13 -0
  189. data/sig/ratatui_ruby/table_state.rbs +15 -0
  190. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  191. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  192. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  193. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  194. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  195. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  196. data/tasks/autodoc/examples.rb +79 -0
  197. data/tasks/autodoc/inventory.rb +9 -7
  198. data/tasks/autodoc.rake +11 -5
  199. data/tasks/bump/changelog.rb +3 -3
  200. data/tasks/bump/links.rb +67 -0
  201. data/tasks/sourcehut.rake +61 -21
  202. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  203. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  204. metadata +111 -37
  205. data/doc/images/app_table_select.png +0 -0
  206. data/doc/images/widget_block_padding.png +0 -0
  207. data/doc/images/widget_block_titles.png +0 -0
  208. data/doc/images/widget_list_styles.png +0 -0
  209. data/examples/app_all_events/model/events.rb +0 -180
  210. data/examples/app_all_events/model/highlight.rb +0 -57
  211. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  212. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  213. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  214. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  215. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  216. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  217. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  218. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  219. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  220. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  221. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  222. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  223. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  224. data/examples/app_all_events/view_state.rb +0 -42
  225. data/examples/app_color_picker/scene.rb +0 -201
  226. data/examples/widget_block_padding/app.rb +0 -67
  227. data/examples/widget_block_titles/app.rb +0 -69
  228. data/examples/widget_list_styles/app.rb +0 -141
  229. data/examples/widget_table_flex/app.rb +0 -95
  230. data/sig/examples/widget_block_padding/app.rbs +0 -11
  231. data/sig/examples/widget_block_titles/app.rbs +0 -11
  232. data/sig/examples/widget_list_styles/app.rbs +0 -11
  233. data/tasks/bump/comparison_links.rb +0 -41
  234. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
data/doc/quickstart.md CHANGED
@@ -35,12 +35,11 @@ gem install ratatui_ruby
35
35
 
36
36
  Here is a "Hello World" application that demonstrates the core lifecycle of a **ratatui_ruby** app.
37
37
 
38
+ <!-- SYNC:START:../examples/verify_quickstart_lifecycle/app.rb:main -->
38
39
  ```ruby
39
- require "ratatui_ruby"
40
-
41
40
  # 1. Initialize the terminal
42
41
  RatatuiRuby.init_terminal
43
-
42
+
44
43
  begin
45
44
  # The Main Loop
46
45
  loop do
@@ -57,21 +56,26 @@ begin
57
56
  style: { fg: "white" }
58
57
  )
59
58
  )
60
-
59
+
61
60
  # 3. Draw the UI
62
61
  RatatuiRuby.draw do |frame|
63
62
  frame.render_widget(view, frame.area)
64
63
  end
65
-
64
+
66
65
  # 4. Poll for events
67
- event = RatatuiRuby.poll_event
68
- break if event.key? && event.code == "q"
66
+ case RatatuiRuby.poll_event
67
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
68
+ break
69
+ else
70
+ nil
71
+ end
69
72
  end
70
73
  ensure
71
74
  # 5. Restore the terminal to its original state
72
75
  RatatuiRuby.restore_terminal
73
76
  end
74
77
  ```
78
+ <!-- SYNC:END -->
75
79
 
76
80
  ![quickstart_lifecycle](./images/verify_quickstart_lifecycle.png)
77
81
 
@@ -87,10 +91,8 @@ end
87
91
 
88
92
  You can simplify your code by using `RatatuiRuby.run`. This method handles the terminal lifecycle for you, yielding a `Session` object with factory methods for widgets.
89
93
 
90
- ```rb
91
- require "ratatui_ruby"
92
-
93
- # 1. Initialize the terminal and ensure it is restored.
94
+ <!-- SYNC:START:../examples/verify_quickstart_dsl/app.rb:main -->
95
+ ```ruby
94
96
  RatatuiRuby.run do |tui|
95
97
  loop do
96
98
  # 2. Create your UI with methods instead of classes.
@@ -121,6 +123,7 @@ RatatuiRuby.run do |tui|
121
123
  end
122
124
  end
123
125
  ```
126
+ <!-- SYNC:END -->
124
127
 
125
128
  #### How it works
126
129
 
@@ -135,64 +138,62 @@ For a deeper dive into the available application architectures (Manual vs Manage
135
138
 
136
139
  Real-world applications often need to split the screen into multiple areas. `RatatuiRuby::Layout` lets you do this easily.
137
140
 
141
+ <!-- SYNC:START:../examples/verify_quickstart_layout/app.rb:main -->
138
142
  ```ruby
139
- require "ratatui_ruby"
143
+ loop do
144
+ tui.draw do |frame|
145
+ # 1. Split the screen
146
+ top, bottom = tui.layout_split(
147
+ frame.area,
148
+ direction: :vertical,
149
+ constraints: [
150
+ tui.constraint_percentage(75),
151
+ tui.constraint_percentage(25),
152
+ ]
153
+ )
140
154
 
141
- RatatuiRuby.run do |tui|
142
- loop do
143
- tui.draw do |frame|
144
- # 1. Split the screen
145
- top, bottom = tui.layout_split(
146
- frame.area,
147
- direction: :vertical,
148
- constraints: [
149
- tui.constraint_percentage(75),
150
- tui.constraint_percentage(25),
151
- ]
152
- )
155
+ # 2. Render Top Widget
156
+ frame.render_widget(
157
+ tui.paragraph(
158
+ text: "Hello, Ratatui!",
159
+ alignment: :center,
160
+ block: tui.block(title: "Content", borders: [:all], border_color: "cyan")
161
+ ),
162
+ top
163
+ )
153
164
 
154
- # 2. Render Top Widget
155
- frame.render_widget(
156
- tui.paragraph(
157
- text: "Hello, Ratatui!",
158
- alignment: :center,
159
- block: tui.block(title: "Content", borders: [:all], border_color: "cyan")
165
+ # 3. Render Bottom Widget with Styled Text
166
+ # We use a Line of Spans to style specific characters
167
+ text_line = tui.text_line(
168
+ spans: [
169
+ tui.text_span(content: "Press '"),
170
+ tui.text_span(
171
+ content: "q",
172
+ style: tui.style(modifiers: [:bold, :underlined])
160
173
  ),
161
- top
162
- )
163
-
164
- # 3. Render Bottom Widget with Styled Text
165
- # We use a Line of Spans to style specific characters
166
- text_line = tui.text_line(
167
- spans: [
168
- tui.text_span(content: "Press '"),
169
- tui.text_span(
170
- content: "q",
171
- style: tui.style(modifiers: [:bold, :underlined])
172
- ),
173
- tui.text_span(content: "' to quit."),
174
- ],
175
- alignment: :center
176
- )
174
+ tui.text_span(content: "' to quit."),
175
+ ],
176
+ alignment: :center
177
+ )
177
178
 
178
- frame.render_widget(
179
- tui.paragraph(
180
- text: text_line,
181
- block: tui.block(title: "Controls", borders: [:all])
182
- ),
183
- bottom
184
- )
185
- end
179
+ frame.render_widget(
180
+ tui.paragraph(
181
+ text: text_line,
182
+ block: tui.block(title: "Controls", borders: [:all])
183
+ ),
184
+ bottom
185
+ )
186
+ end
186
187
 
187
- case tui.poll_event
188
- in { type: :key, code: "q" }
189
- break
190
- else
191
- # Ignore other events
192
- end
188
+ case tui.poll_event
189
+ in { type: :key, code: "q" }
190
+ break
191
+ else
192
+ # Ignore other events
193
193
  end
194
194
  end
195
195
  ```
196
+ <!-- SYNC:END -->
196
197
 
197
198
  #### How it works
198
199
 
@@ -221,7 +222,7 @@ Use it to debug your input handling or verify terminal behavior.
221
222
 
222
223
  **What you'll learn:**
223
224
 
224
- * **MVVM Architecture**: Separates logic (Model), state (ViewModel), and rendering (View) for clean, testable code.
225
+ * **Proto-TEA Architecture**: Implements unidirectional data flow (Model-View-Update) with immutable state and pure functions.
225
226
  * **Event Handling**: Captures and distinguishes all input types, including modifiers (`Ctrl+C`) and focus changes.
226
227
  * **Scalable Structure**: Organizes a non-trivial application into small, focused classes instead of a monolithic script.
227
228
 
@@ -231,15 +232,15 @@ Use it to debug your input handling or verify terminal behavior.
231
232
 
232
233
  Interactive tools require complex state. Mapping mouse clicks to widgets and handling modal dialogs creates messy code if handled in the main loop.
233
234
 
234
- This app implements a full Color Picker using a "Scene-Orchestrated" pattern. The Scene calculates layout and exposes cached rectangles for hit testing.
235
+ This app implements a full Color Picker using a "Proto-Kit (Component-Based)" pattern. Each component encapsulates its own rendering, state, and event handling.
235
236
 
236
237
  Use it to build forms, editors, and mouse-driven tools.
237
238
 
238
239
  **What you'll learn:**
239
240
 
240
- * **Scene-Orchestrated MVC**: Separates the View (layout/rendering) from the Controller (event loop) and Model (business logic).
241
- * **Hit Testing**: Caches layout rectangles during the render pass to handle mouse clicks on specific elements.
242
- * **Modal Dialogs**: Implements overlay patterns that intercept input.
241
+ * **Proto-Kit Architecture**: Self-contained components with `render(tui, frame, area)` and `handle_event(event)`.
242
+ * **Encapsulated Hit Testing**: Components cache their render area and check `contains?` internally.
243
+ * **Modal Dialogs**: Implements overlay patterns that intercept input via Chain of Responsibility.
243
244
 
244
245
  #### [Custom Widget (Escape Hatch)](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_custom_widget/app.rb)
245
246
 
@@ -259,17 +260,9 @@ Shows how to use `Overlay`, `Center`, and `Cursor` to build a modal login form w
259
260
 
260
261
  ![login_form](./images/app_login_form.png)
261
262
 
262
- #### [Map Demo](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_map_demo/app.rb)
263
-
264
- Exhibits the `Canvas` widget's power, rendering a world map with city labels, animated circles, and lines.
265
-
266
- ![map_demo](./images/app_map_demo.png)
267
263
 
268
- #### [Table Select](https://git.sr.ht/~kerrick/ratatui_ruby/tree/main/item/examples/app_table_select/app.rb)
269
264
 
270
- Demonstrates interactive row selection in the `Table` widget with keyboard navigation, highlighting selected rows with custom styles and symbols, applying a base style, and dynamically adjusting `column_spacing`. Also demonstrates `column_highlight_style` and the new `cell_highlight_style` for precise selection visualization.
271
265
 
272
- ![table_select](./images/app_table_select.png)
273
266
 
274
267
 
275
268
  ### Widget Demos
@@ -277,14 +270,14 @@ Demonstrates interactive row selection in the `Table` widget with keyboard navig
277
270
  These smaller, focused examples demonstrate specific widgets and their configuration options.
278
271
 
279
272
  * [Bar Chart](../examples/widget_barchart_demo/app.rb)
280
- * [Block Padding](../examples/widget_block_padding/app.rb)
281
- * [Block Titles](../examples/widget_block_titles/app.rb)
273
+ * [Block (Interactive Demo)](../examples/widget_block_demo/app.rb)
282
274
  * [Box (Block/Paragraph)](../examples/widget_box_demo/app.rb)
283
275
  * [Calendar](../examples/widget_calendar_demo/app.rb)
284
276
  * [Chart](../examples/widget_chart_demo/app.rb)
285
277
  * [Gauge](../examples/widget_gauge_demo/app.rb)
286
278
  * [Line Gauge](../examples/widget_line_gauge_demo/app.rb)
287
279
  * [List](../examples/widget_list_demo/app.rb)
280
+ * [Map (Canvas)](../examples/widget_map_demo/app.rb)
288
281
  * [Popup (Clear)](../examples/widget_popup_demo/app.rb)
289
282
  * [Rect](../examples/widget_rect/app.rb)
290
283
  * [Ratatui Logo](../examples/widget_ratatui_logo_demo/app.rb)
@@ -293,6 +286,6 @@ These smaller, focused examples demonstrate specific widgets and their configura
293
286
  * [Scrollbar](../examples/widget_scrollbar_demo/app.rb)
294
287
  * [Scroll Text](../examples/widget_scroll_text/app.rb)
295
288
  * [Sparkline](../examples/widget_sparkline_demo/app.rb)
296
- * [Table Flex](../examples/widget_table_flex/app.rb)
289
+ * [Table (Selection)](../examples/widget_table_demo/app.rb)
297
290
  * [Tabs](../examples/widget_tabs_demo/app.rb)
298
291
  * [Widget Style Colors](../examples/widget_style_colors/app.rb)
@@ -0,0 +1,92 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Terminal Limitations
7
+
8
+ Some behaviors are outside the control of `ratatui_ruby`. This document explains common pitfalls that affect your application or your users, but cannot be fixed in the library.
9
+
10
+ ## Keyboard Event Interception
11
+
12
+ ### The Problem
13
+
14
+ Your application receives a key event, but the modifier flags are missing. You pressed Ctrl+PageUp, but the event shows `code="page_up"` with `modifiers=[]`.
15
+
16
+ ### The Cause
17
+
18
+ Terminal emulators intercept certain key combinations for their own features. The key press never reaches your application—the terminal consumes it first.
19
+
20
+ Common culprits on macOS:
21
+
22
+ | Key Combination | Terminal Behavior |
23
+ |---------------------|--------------------------------------|
24
+ | Ctrl+PageUp/Down | Switch tabs (Terminal.app, iTerm2) |
25
+ | Ctrl+Tab | Switch tabs |
26
+ | Cmd+T / Cmd+N | New tab / New window |
27
+ | Cmd+C / Cmd+V | Copy / Paste (not Ctrl) |
28
+
29
+ Linux terminals vary widely. Windows Terminal and ConEmu have their own defaults.
30
+
31
+ ### The Solution
32
+
33
+ 1. **Test with different terminals.** Kitty, WezTerm, and Alacritty pass more key combinations through to applications by default. If a key works in Kitty but not Terminal.app, the terminal is the issue.
34
+
35
+ 2. **Reconfigure your terminal.** Most terminal emulators let you unbind or remap default shortcuts in their settings.
36
+
37
+ 3. **Use alternative key bindings.** If your users will run your application in various terminals, design your keybindings to avoid commonly intercepted combinations:
38
+ - Use Alt+PageUp instead of Ctrl+PageUp
39
+ - Use Ctrl+J/K instead of Ctrl+Up/Down
40
+ - Avoid Ctrl+Tab entirely
41
+
42
+ 4. **Document requirements.** If your application depends on specific key combinations, document the terminal requirements for your users.
43
+
44
+ ### Enhanced Keyboard Protocol
45
+
46
+ Some terminals support the [Kitty keyboard protocol](https://sw.kovidgoyal.net/kitty/keyboard-protocol/), which provides unambiguous key event reporting including:
47
+
48
+ - Individual modifier key events (LeftShift vs RightShift)
49
+ - Media keys (Play, Pause, Volume controls)
50
+ - Repeat and release events
51
+
52
+ Terminals with full protocol support:
53
+ - Kitty
54
+ - WezTerm
55
+ - Foot
56
+ - Alacritty (partial)
57
+
58
+ Standard terminals (Terminal.app, iTerm2, GNOME Terminal) do not support the enhanced protocol.
59
+
60
+ **RatatuiRuby Status:** The underlying library (crossterm) supports this protocol, but RatatuiRuby does not yet expose a way to enable it. The key code mappings for media keys and individual modifier keys exist, but they will only be received from terminals that enable the protocol by default. This is planned for a future release.
61
+
62
+ ## Mouse Event Limitations
63
+
64
+ ### The Problem
65
+
66
+ Mouse events work in some terminals but not others. Or they work, but only up to certain coordinates.
67
+
68
+ ### The Cause
69
+
70
+ Mouse reporting requires terminal escape sequence support. Older terminals may not support:
71
+
72
+ - SGR mouse mode (coordinates > 223)
73
+ - Mouse motion tracking
74
+ - Button-event tracking
75
+
76
+ ### The Solution
77
+
78
+ Ensure your terminal supports modern mouse modes. Most actively maintained terminals do. If running in a legacy environment, test mouse functionality and provide keyboard alternatives.
79
+
80
+ ## Focus Events
81
+
82
+ ### The Problem
83
+
84
+ `Event::FocusGained` and `Event::FocusLost` are never received.
85
+
86
+ ### The Cause
87
+
88
+ Focus event reporting requires explicit terminal support and configuration. Some terminals don't support it at all.
89
+
90
+ ### The Solution
91
+
92
+ Don't rely on focus events for critical functionality. Treat them as nice-to-have enhancements. If your application shows stale data when the user returns, periodically refresh instead of waiting for focus events.
@@ -5,37 +5,54 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # App All Events Example
7
7
 
8
- This example application captures and visualizes every event supported by `ratatui_ruby`. It serves as a comprehensive reference for event handling and a demonstration of a clean, scalable architectural pattern.
8
+ This example application captures and visualizes every event supported by `ratatui_ruby`. It serves as a comprehensive reference for event handling and a demonstration of the Proto-TEA architectural pattern.
9
9
 
10
- ## Architecture: MVVM (Model-View-ViewModel)
10
+ ## Architecture: Proto-TEA (Model-View-Update)
11
11
 
12
- This application demonstrates the **Model-View-ViewModel (MVVM)** pattern, modified for the immediate-mode nature of terminal UIs. This separation of concerns ensures that the UI logic is completely decoupled from the business logic, making the application easier to test and maintain.
12
+ This application demonstrates **unidirectional data flow** inspired by The Elm Architecture. This separation ensures that state management is predictable and easy to test.
13
13
 
14
- ### 1. Model (`model/`)
15
- The **Model** manages the application's domain data and logic. It knows nothing about the UI.
14
+ ### 1. Model (`model/app_model.rb`)
15
+ A single immutable `Data.define` object holding **all** application state:
16
+ * Event log entries
17
+ * Focus state
18
+ * Window size
19
+ * Highlight timestamps
20
+ * Color cycle index
16
21
 
17
- * **`Events` (`model/events.rb`)**: The core store. It records incoming events, maintains statistics (counts), and handles business logic like "highlight this event type for 300ms."
18
- * **`EventEntry` (`model/event_entry.rb`)**: A value object representing a single recorded event.
22
+ State changes use `.with(...)` to return a new Model instance.
19
23
 
20
- ### 2. View State (ViewModel) (`view_state.rb`)
21
- The **View State** (comparable to a ViewModel or Presenter) is an immutable data structure built specifically for the View.
24
+ ### 2. Msg (`model/msg.rb`)
25
+ Semantic value objects that decouple raw terminal events from business logic:
26
+ * `Msg::Input` — keyboard, mouse, or paste events
27
+ * `Msg::Resize` — terminal size changes
28
+ * `Msg::Focus` — focus gained/lost
29
+ * `Msg::Quit` — exit signal
22
30
 
23
- * **`ViewState`**: It acts as a bridge. In every render loop, the application builds a fresh `ViewState` object, calculating derived data (like styles, active flags, and formatted strings) from the raw Model data.
24
- * **Why?**: This prevents the View from having to contain logic. The View doesn't ask "is the app focused so I should use green?"; it just asks `state.border_color`.
31
+ ### 3. Update (`update.rb`)
32
+ A **pure function** that computes the next state:
25
33
 
26
- ### 3. View (`view/`)
27
- The **View** is responsible **only** for rendering. It receives the `ViewState` and draws to the screen.
34
+ ```ruby
35
+ Update.call(msg, model) -> Model
36
+ ```
28
37
 
29
- * **`View::App` (`view/app_view.rb`)**: The root view. It handles the high-level layout (splitting the screen into areas).
30
- * **Sub-views**: `Counts`, `Live`, `Log`, `Controls`. Each is a small, focused component that renders a specific part of the screen based on the data in `ViewState`.
38
+ All logic previously in `Events.record` now lives here. The function never mutates, never draws, never performs IO.
31
39
 
32
- ### 4. Controller/App (`app.rb`)
33
- The **`AppAllEvents`** class ties it all together. It owns the main loop:
40
+ ### 4. View (`view/`)
41
+ Pure rendering logic. Views accept the immutable `AppModel` and draw to the screen.
42
+ * **`View::App`**: Root view handling high-level layout
43
+ * **Sub-views**: `Counts`, `Live`, `Log`, `Controls`
34
44
 
35
- 1. **Poll**: Waits for an event from the terminal.
36
- 2. **Update**: Passes the event to the **Model** (`@events.record`).
37
- 3. **Build State**: Creates a new **ViewState** from the current Model and global state.
38
- 4. **Render**: Passes the **ViewState** to the **View** to draw the frame.
45
+ ### 5. Runtime (`app.rb`)
46
+ The MVU loop:
47
+
48
+ ```ruby
49
+ loop do
50
+ tui.draw { |f| view.call(model, tui, f, f.area) }
51
+ msg = map_event_to_msg(tui.poll_event, model)
52
+ break if msg.is_a?(Msg::Quit)
53
+ model = Update.call(msg, model)
54
+ end
55
+ ```
39
56
 
40
57
  ## Library Features Showcased
41
58
 
@@ -57,10 +74,10 @@ Reading this code will teach you how to:
57
74
  If you are building an app and your logic isn't catching `Ctrl+Left`, run this app and press the keys. You will see exactly how `ratatui_ruby` parses that input (e.g., is it a `Key` event? What are the modifiers?).
58
75
 
59
76
  ### "How do I structure a real app?"
60
- Hello World examples are great, but they don't scale. This example shows how to structure an application that can grow. By simulating a "dashboard" with multiple independent widgets updating in real-time, it solves the problem of "how do I pass data around without global variables?"
77
+ Hello World examples are great, but they don't scale. This example shows how to structure an application that can grow. By using immutable state and pure functions, it solves the problem of "where does my state live and how does it change?"
61
78
 
62
- ### "How do I implement an event loop?"
63
- It provides a robust reference implementation of the standard `loop { draw; handle_input }` cycle, including the correct way to handle quit signals.
79
+ ### "How do I test my business logic?"
80
+ The `Update` function is pure. You can test it by constructing a `Msg`, calling `Update.call(msg, model)`, and asserting on the returned `Model`. No mocking required.
64
81
 
65
82
  ## Comparison: Choosing an Architecture
66
83
 
@@ -68,14 +85,15 @@ Complex applications require structured state habits. `AppAllEvents` and the [Co
68
85
 
69
86
  ### The Dashboard Approach (AppAllEvents)
70
87
 
71
- Dashboards display data. They rarely require complex mouse interaction. Strict MVVM works best here. The View is a pure function. It accepts a `ViewState` and draws it. It ignores input. This simplifies testing.
88
+ Dashboards display data. They rarely require complex mouse interaction. Proto-TEA works best here. State is immutable. Logic is pure. Updates are predictable. This simplifies testing.
72
89
 
73
90
  Use this pattern for logs, monitors, and data viewers.
74
91
 
75
92
  ### The Tool Approach (Color Picker)
76
93
 
77
- Tools require interaction. Users click buttons and drag sliders. The Controller needs to know where components exist on screen. MVVM hides this layout data.
94
+ Tools require interaction. Users click buttons and drag sliders. Each UI component needs to know where it exists on screen for hit testing.
78
95
 
79
- The Color Picker uses a "Scene" pattern. The View exposes layout rectangles. The Controller uses these rectangles to handle mouse clicks.
96
+ The Color Picker uses a "Proto-Kit (Component-Based)" pattern. Each component encapsulates its own rendering, state, and event handling. The Container routes events and coordinates cross-component effects.
80
97
 
81
98
  Use this pattern for forms, editors, and mouse-driven tools.
99
+
@@ -7,8 +7,9 @@ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
7
  $LOAD_PATH.unshift File.expand_path(__dir__)
8
8
 
9
9
  require "ratatui_ruby"
10
- require_relative "model/events"
11
- require_relative "view_state"
10
+ require_relative "model/app_model"
11
+ require_relative "model/msg"
12
+ require_relative "update"
12
13
  require_relative "view/app_view"
13
14
 
14
15
  # Demonstrates the full range of terminal events supported by RatatuiRuby.
@@ -20,6 +21,14 @@ require_relative "view/app_view"
20
21
  #
21
22
  # Use it to verify your terminal's capabilities or as a reference for complex event handling.
22
23
  #
24
+ # === Architecture
25
+ #
26
+ # This example uses the Proto-TEA (Model-View-Update) pattern:
27
+ # - **Model**: Immutable AppModel holds all state
28
+ # - **Msg**: Semantic message types decouple events from logic
29
+ # - **Update**: Pure function computes next state
30
+ # - **View**: Renders Model to screen
31
+ #
23
32
  # === Examples
24
33
  #
25
34
  # # Run from the command line:
@@ -31,62 +40,56 @@ class AppAllEvents
31
40
  # List of all event types tracked by this application.
32
41
  EVENT_TYPES = %i[key mouse resize paste focus none].freeze
33
42
 
34
- # Creates a new AppAllEvents instance and initializes its state.
43
+ # Creates a new AppAllEvents instance and initializes its view.
35
44
  def initialize
36
45
  @view = View::App.new
37
- @events = Events.new
38
- @focused = true
39
- @last_dimensions = [80, 24]
40
46
  end
41
47
 
42
48
  # Starts the application event loop.
43
49
  #
50
+ # Implements the MVU (Model-View-Update) runtime:
51
+ # 1. **View**: Render current model
52
+ # 2. **Poll**: Get next event
53
+ # 3. **Map**: Convert raw event to semantic Msg
54
+ # 4. **Update**: Compute next model
55
+ #
44
56
  # === Example
45
57
  #
46
58
  # app.run
47
59
  def run
48
60
  RatatuiRuby.run do |tui|
49
- @tui = tui
61
+ model = AppModel.initial
62
+
50
63
  loop do
51
- render
52
- break if handle_input == :quit
53
- end
54
- end
55
- end
64
+ tui.draw { |frame| @view.call(model, tui, frame, frame.area) }
56
65
 
57
- private def render
58
- view_state = ViewState.build(
59
- @events,
60
- @focused,
61
- @tui,
62
- nil
63
- )
66
+ event = tui.poll_event
67
+ msg = map_event_to_msg(event, model)
68
+ break if msg.is_a?(Msg::Quit)
64
69
 
65
- @tui.draw { |frame| @view.call(view_state, @tui, frame, frame.area) }
70
+ model = Update.call(msg, model)
71
+ end
72
+ end
66
73
  end
67
74
 
68
- private def handle_input
69
- event = @tui.poll_event
70
-
75
+ private def map_event_to_msg(event, model)
71
76
  case event
72
77
  when RatatuiRuby::Event::Key
73
- return :quit if event.code == "q"
74
- return :quit if event.code == "c" && event.modifiers.include?("ctrl")
75
- @events.record(event)
78
+ return Msg::Quit.new if event.code == "q"
79
+ return Msg::Quit.new if event.code == "c" && event.modifiers.include?("ctrl")
80
+
81
+ Msg::Input.new(event:)
76
82
  when RatatuiRuby::Event::Resize
77
- @events.record(event, context: { last_dimensions: @last_dimensions })
78
- @last_dimensions = [event.width, event.height]
83
+ Msg::Resize.new(width: event.width, height: event.height, previous_size: model.window_size)
79
84
  when RatatuiRuby::Event::FocusGained
80
- @focused = true
81
- @events.record(event)
85
+ Msg::Focus.new(gained: true)
82
86
  when RatatuiRuby::Event::FocusLost
83
- @focused = false
84
- @events.record(event)
87
+ Msg::Focus.new(gained: false)
88
+ when RatatuiRuby::Event::None
89
+ Msg::NoneEvent.new
85
90
  else
86
- @events.record(event)
91
+ Msg::Input.new(event:)
87
92
  end
88
-
89
- nil
90
93
  end
91
94
  end
92
95