ratatui_ruby 0.3.1 → 0.4.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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,203 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Developing Examples
7
+
8
+ Guidelines for creating and testing examples in the `examples/` directory.
9
+
10
+ ## Example Structure
11
+
12
+ Every interactive example should follow this pattern, living in its own directory:
13
+
14
+ `examples/my_example/app.rb`:
15
+ ```ruby
16
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
17
+ require "ratatui_ruby"
18
+
19
+ class MyExampleApp
20
+ def initialize
21
+ # Initialize state
22
+ end
23
+
24
+ def run
25
+ RatatuiRuby.run do
26
+ loop do
27
+ render
28
+ break if handle_input == :quit
29
+ end
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ def render
36
+ # Build and draw UI
37
+ RatatuiRuby.draw(layout)
38
+ end
39
+
40
+ def handle_input
41
+ event = RatatuiRuby.poll_event
42
+ # Process event, return :quit to exit
43
+ end
44
+ end
45
+
46
+ MyExampleApp.new.run if __FILE__ == $PROGRAM_NAME
47
+ ```
48
+
49
+ ### Naming Convention (Required)
50
+
51
+ Example classes **must** follow the naming convention:
52
+ - **Directory:** `examples/my_example/` (snake_case)
53
+ - **Class:** `MyExampleApp` (PascalCase with `App` suffix)
54
+
55
+ The class name is derived from the directory name: `my_example` → `MyExampleApp`.
56
+
57
+ This convention enables the `terminal_preview:update` rake task to automatically capture terminal output for all examples without maintaining a manual registry.
58
+
59
+ ### Terminal Size Constraint
60
+
61
+ All interactive examples must fit within an **80×24 terminal** (standard VT100 dimensions). This ensures:
62
+ - Examples work on minimal terminal configurations
63
+ - Tests can use the default `with_test_terminal` size (80x24)
64
+ - Examples remain discoverable and self-documenting through visible hotkey help
65
+
66
+ **Layout pattern for 80×24:**
67
+ - **Bottom control panel:** Allocate ~5-7 lines at bottom for a full-width control block with hotkey documentation. Style hotkeys with **bold and underline** to make them discoverable. Use double-space or pipe separators to compress multiple controls per line. This keeps all UI text at full readability while maximizing space for the main content area.
68
+
69
+ **Best practices:**
70
+ - Use descriptive names (e.g., "Yellow on Black" not "Yellow") so controls are self-documenting and discoverable.
71
+ - **Style hotkeys visually:** Use `modifiers: [:bold, :underlined]` on hotkey letters to make them stand out from descriptions. Example: `i` (bold, underlined) followed by `Items`.
72
+ - Test early by running the example at 80×24 and verifying all content is visible without wrapping, scrolling, or clipping.
73
+
74
+ Every example must also have an RBS file documenting its public methods:
75
+
76
+ `examples/my_example/app.rbs`:
77
+ ```rbs
78
+ class MyExampleApp
79
+ # @public
80
+ def self.new: () -> MyExampleApp
81
+
82
+ # @public
83
+ def run: () -> void
84
+ end
85
+ ```
86
+
87
+ ### Key Requirements
88
+
89
+ 1. **Only `run` should be public.** All other methods (`render`, `handle_input`, helper methods) must be private. This prevents tests from calling internal methods directly.
90
+
91
+ 2. **Use `RatatuiRuby.run` for terminal management.** Never call `init_terminal` or `restore_terminal` directly. The `run` block handles terminal setup/teardown automatically and safely, even if an exception occurs.
92
+
93
+ 3. **Use keyboard keys to cycle through widget attributes.** Users should be able to interactively explore all widget options. Common patterns:
94
+ - Arrow keys: Navigate or adjust values
95
+ - Letter keys: Cycle through styles, modes, or variants. Prefer all lowercase keys to avoid confusion and simplify the UI description.
96
+ - Space: Toggle or select
97
+ - `q` or Ctrl+C: Quit
98
+
99
+ ### Naming Conventions for Controls
100
+
101
+ When documenting hotkeys and cycling options in the UI, use consistent naming:
102
+
103
+ - **Parameter names:** Always match the actual Ruby parameter name. For example:
104
+ - Use "Scroll Padding" (not "Scroll Pad") for the `scroll_padding:` parameter
105
+ - Use "Highlight Style" (not "Highlight") for the `highlight_style:` parameter
106
+ - Use "Repeat Symbol" (not "Repeat") for the `repeat_highlight_symbol:` parameter
107
+
108
+ - **Display names for cycled values:** Create a `name` field in your options hash to keep display names paired with values:
109
+ ```ruby
110
+ @styles = [
111
+ { name: "Yellow Bold", style: RatatuiRuby::Style.new(fg: :yellow, modifiers: [:bold]) },
112
+ { name: "Blue on White", style: RatatuiRuby::Style.new(fg: :blue, bg: :white) }
113
+ ]
114
+
115
+ # In controls: "h: Highlight Style (#{@styles[@style_index][:name]})"
116
+ # Outputs: "h: Highlight Style (Yellow Bold)"
117
+ ```
118
+
119
+ This keeps the UI self-documenting and users can see exact parameter names when they read the hotkey help.
120
+
121
+ ### Hit Testing with the Cached Layout Pattern
122
+
123
+ Examples with mouse interaction should implement the **Cached Layout Pattern** documented in `doc/interactive_design.md`. The `hit_test` example demonstrates this pattern.
124
+
125
+ ## Testing Examples
126
+
127
+ Example tests live alongside examples as `test_app.rb` files in the same directory.
128
+
129
+ ### Testing Pattern
130
+
131
+ `examples/my_example/test_app.rb`:
132
+ ```ruby
133
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
134
+ require "ratatui_ruby"
135
+ require "ratatui_ruby/test_helper"
136
+ require "minitest/autorun"
137
+ require_relative "app"
138
+
139
+ class TestMyExampleApp < Minitest::Test
140
+ include RatatuiRuby::TestHelper
141
+
142
+ def setup
143
+ @app = MyExampleApp.new
144
+ end
145
+
146
+ def test_initial_render
147
+ with_test_terminal do
148
+ inject_key(:q) # Queue quit event
149
+ @app.run # Run the app loop
150
+
151
+ content = buffer_content.join("\n")
152
+ assert_includes content, "Expected Text"
153
+ end
154
+ end
155
+
156
+ def test_keyboard_interaction
157
+ with_test_terminal do
158
+ inject_key("s") # Press 's' to cycle something
159
+ inject_key(:q) # Then quit
160
+ @app.run
161
+
162
+ content = buffer_content.join("\n")
163
+ assert_includes content, "Changed State"
164
+ end
165
+ end
166
+ end
167
+ ```
168
+
169
+ ### Testing Guidelines
170
+
171
+ 1. **Inject events, observe buffer.** Tests should only interact through:
172
+ - `inject_key` / `inject_keys` / `inject_event` for input
173
+ - `buffer_content` for output verification
174
+
175
+ 2. **Never call internal methods.** Don't call `render`, `handle_input`, `__send__`, or access instance variables with `instance_variable_get`. Tests verify behavior through the public `run` method.
176
+
177
+ 3. **Use `inject_key(:q)` to exit.** All examples should support quitting with `q`, so inject this as the final event to terminate the loop.
178
+
179
+ 4. **Assert and refute.** When testing which item was clicked/selected, also verify the opposite didn't happen:
180
+ ```ruby
181
+ assert_includes content, "Left Panel clicked"
182
+ refute_includes content, "Right Panel clicked"
183
+ ```
184
+
185
+ 5. **Test state cycling.** If an example cycles through options (styles, modes, etc.), test that pressing the key actually changes the rendered output.
186
+
187
+ ## Widget Attribute Cycling
188
+
189
+ Examples should demonstrate widget configurability by allowing interactive cycling:
190
+
191
+ | Widget | Attribute | Key Suggestion |
192
+ |--------|-----------|----------------|
193
+ | Tabs | highlight_style | Space |
194
+ | Tabs | divider | d |
195
+ | Tabs | style | s |
196
+ | Block | border_type | Space |
197
+ | Block | border_color | c |
198
+ | List | highlight_style | Space |
199
+ | Sparkline | direction | d |
200
+ | Scrollbar | orientation | o |
201
+ | Scrollbar | theme | s |
202
+
203
+ Display the current state in the UI (e.g., in a title or status bar or paragraph) so users can see what changed. Display the hotkey in the UI as well, so users can see how to change it; the hotkey should not disappear as app state changes.
@@ -0,0 +1,97 @@
1
+ # Documentation Style Guide
2
+
3
+ This project follows a strict and specific documentation style designed to be helpful, readable, and consistent. It combines the structural clarity of Christopher Alexander's Pattern Language with the prose style of William Zinsser's *On Writing Well* and the usability of the U.S. Federal Plain Language Guidelines.
4
+
5
+ **All agents and contributors must adhere to these standards.**
6
+
7
+ ## 1. Core Philosophy
8
+
9
+ * **Context, Problem, Solution (Alexandrian Form):** Do not just say *what* a class does. Explain *why* it exists. Start with the context, state the problem (the pain point without this tool), and then present the class as the solution.
10
+ * **Prose Style (Zinsser/Klinkenborg):** Use short, punchy sentences. Use active voice. Cut unnecessary words. Avoid "allow," "enable," "provide," "support," "functionality," and "capability" where possible. Weak verbs hide the action. Strong verbs drive the sentence.
11
+ * **User-Centric (Plain Language):** Speak directly to the user ("You"). Don't abstract them away ("The developer"). Focus on their goals and how this tool helps them achieve those goals.
12
+
13
+ ## 2. Class Documentation
14
+
15
+ Every public class must begin with a **Context-Problem-Solution** narrative.
16
+
17
+ ### Structure
18
+
19
+ 1. **Summary Line:** A single line explaining the class's role.
20
+ 2. **Context (Narrative):** A short paragraph establishing the domain or situation.
21
+ 3. **Problem (Narrative):** A sentence or two identifying the specific difficulty, complexity, or "pain" the user faces in this context without the widget.
22
+ 4. **Solution (Narrative):** A sentence explaining how this widget solves that problem, often starting with "This widget..." or "Use it to...".
23
+ 5. **Usage (Narrative):** A concrete "Use it to..." sentence listing common applications.
24
+ 6. **Example:** A comprehensive, copy-pasteable code example using `=== Examples`. Provide **multiple** examples to cover different use cases (e.g., basic usage vs. advanced configuration).
25
+
26
+ ### Example
27
+
28
+ **Bad (Generic/Descriptive):**
29
+ ```ruby
30
+ # A widget for displaying list items.
31
+ # It allows the user to select an item from an array of strings.
32
+ # Supports scrolling and custom styling.
33
+ ```
34
+
35
+ **Good (Alexandrian/Zinsser/Plain Language):**
36
+ ```ruby
37
+ # Displays a selectable list of items.
38
+ #
39
+ # Users need to choose from options. Menus, file explorers, and selectors are everywhere.
40
+ # Implementing navigation, highlighting, and scrolling state from scratch is tedious.
41
+ #
42
+ # This widget manages the list. It renders the items. It highlights the selection. It handles the scrolling window.
43
+ #
44
+ # Use it to build main menus, navigation sidebars, or logs.
45
+ ```
46
+
47
+ ## 3. Method and Attribute Documentation
48
+
49
+ ### Prose Style
50
+
51
+ * **Attributes:** Use concise noun phrases. Avoid "This attribute returns..." or "Getter for...".
52
+ * *Bad:* "This is the width of the widget."
53
+ * *Good:* "Width of the widget in cells."
54
+ * **Methods:** Use active, third-person present tense verbs.
55
+ * *Bad:* "Will calculate the total."
56
+ * *Good:* "Calculates the total."
57
+ * **Context:** For complex methods, you may use a condensed version of the Context-Problem-Solution pattern, but keep it brief.
58
+
59
+ ### Syntax Standards
60
+
61
+ * **Examples:** All public methods must include at least one usage example. Use `=== Example`.
62
+ * **Attributes:** Use `attr_reader` with documentation comments immediately preceding them.
63
+ * **Parameters:** Use strict RDoc definition lists `[name] description` for parameters in the `initialize` method.
64
+ * **Formatting:** Use `<tt>` tags for code literals, symbols, and values (e.g., `<tt>:vertical</tt>`).
65
+ * Do **not** use backticks (\`) or markdown-style links `[text](url)`. RDoc does not render them correctly in all contexts.
66
+ * Do **not** use smart quotes.
67
+
68
+ ### Example
69
+
70
+ ```ruby
71
+ # The styling to apply to the content.
72
+ attr_reader :style
73
+
74
+ # Creates a new List.
75
+ #
76
+ # [items] Array of Strings.
77
+ # [selected_index] Integer (nullable).
78
+ def initialize(items: [], selected_index: nil)
79
+ super
80
+ end
81
+ ```
82
+
83
+ ## 4. RDoc Specifics
84
+
85
+ * **No Endless Methods:** Do **not** use Ruby 3.0+ endless method definitions (`def foo = bar`). RDoc currently has a bug where it fails to correctly parse the end of the method, causing subsequent methods to be nested incorrectly in the documentation tree. Always use standard `def ... end` blocks.
86
+ * **No YARD:** Do not use `@param`, `@return`, or other YARD tags. Use standard RDoc formats.
87
+ * **Directives:** Use `:nodoc:` for private or internal methods that should not appear in the API docs.
88
+ * **Headings:** Use `===` for section headers like `=== Examples`.
89
+
90
+ ## 5. Checklist for Agents
91
+
92
+ Before finalizing documentation, ask:
93
+ 1. Did I explain the *problem* this code solves?
94
+ 2. Are my sentences short and active? (Did I remove "allows the user to"?)
95
+ 3. Is the code example valid and copy-pasteable?
96
+ 4. Did I use `<tt>` for symbols and code values?
97
+ 5. Did I document every attribute and parameter?
@@ -0,0 +1,366 @@
1
+ <!-- SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com> -->
2
+ <!-- SPDX-License-Identifier: AGPL-3.0-or-later -->
3
+
4
+ # DWIM / DX Improvements for Application Developers
5
+
6
+ ## Problem Statement
7
+
8
+ Ruby's philosophy of "Do What I Mean" (DWIM) and human-centric design should extend to ratatui_ruby's API. Currently, app developers encounter friction points that force them to remember non-obvious conventions, use overly verbose code, or pattern-match when simple predicates would suffice.
9
+
10
+ This proposal identifies DX issues across the widget API and suggests improvements that maintain backward compatibility while providing ergonomic alternatives.
11
+
12
+ ## DX Issues Identified
13
+
14
+ ### 1. Confusing Event Method Names
15
+
16
+ **Current problem**: `event.char` doesn't exist, but `event.code` returns things like `"enter"`, `"ctrl"`, not just characters.
17
+
18
+ **What users expect**:
19
+ - `event.char` should return the printable character (matching the name)
20
+ - `event.ctrl_c?`, `event.enter?`, etc. should work for all key combinations
21
+ - `event.key?`, `event.mouse?` predicates exist but only for broad categories
22
+
23
+ **Solution implemented**: Added `char` method and dynamic predicates via `method_missing`. See `lib/ratatui_ruby/event/key.rb`.
24
+
25
+ ### 2. Dual Parameter APIs Without Predicates
26
+
27
+ **Current problem**: Widgets accept both forms but no convenience methods to query the state:
28
+
29
+ ```ruby
30
+ # Both work, but which one does the widget store?
31
+ gauge1 = Gauge.new(ratio: 0.75)
32
+ gauge2 = Gauge.new(percent: 75)
33
+ gauge1.ratio # Works
34
+ gauge1.percent # Does NOT exist
35
+ ```
36
+
37
+ Similarly with List and Table:
38
+ ```ruby
39
+ list.selected_index = 2 # Works
40
+ list.selected? # Does NOT exist
41
+ list.is_selected? # Does NOT exist
42
+ ```
43
+
44
+ **Affected widgets**:
45
+ - `Gauge` (ratio vs percent)
46
+ - `LineGauge` (ratio vs percent)
47
+ - `List` (selected_index with no query methods)
48
+ - `Table` (selected_row and selected_column with no query methods)
49
+
50
+ **Suggested solutions**:
51
+
52
+ For `Gauge` and `LineGauge`:
53
+ ```ruby
54
+ # Add convenience predicates
55
+ gauge.percent # => 75 (coerced from ratio internally)
56
+ gauge.percent = 50 # => Updates ratio to 0.5
57
+
58
+ # Or provide explicit accessors
59
+ gauge.as_percent # => 75
60
+ gauge.as_ratio # => 0.75
61
+ ```
62
+
63
+ For `List` and `Table`:
64
+ ```ruby
65
+ list.selected? # => true if selected_index is not nil
66
+ list.selection # => 2 (alias for selected_index)
67
+ list.selected_item # => "Item 3"
68
+
69
+ table.selected_row? # => true if selected_row is not nil
70
+ table.selected_cell? # => true if both row and column selected
71
+ ```
72
+
73
+ ### 3. Symbol Constants for Enum Values
74
+
75
+ **Current problem**: Magic symbol values scattered across code:
76
+
77
+ ```ruby
78
+ list = List.new(
79
+ highlight_spacing: :when_selected, # What are the other options?
80
+ direction: :top_to_bottom, # Is :bottom_to_top valid?
81
+ )
82
+
83
+ layout = Layout.new(
84
+ flex: :legacy # What does "legacy" mean?
85
+ )
86
+
87
+ gauge = Gauge.new(
88
+ use_unicode: true # Unclear what ASCII fallback looks like
89
+ )
90
+ ```
91
+
92
+ Users must consult docs or source code to discover valid options.
93
+
94
+ **Suggested solution**: Add constants to widget classes:
95
+
96
+ ```ruby
97
+ class List < Data
98
+ # Highlight spacing modes
99
+ HIGHLIGHT_ALWAYS = :always
100
+ HIGHLIGHT_WHEN_SELECTED = :when_selected
101
+ HIGHLIGHT_NEVER = :never
102
+
103
+ # Direction modes
104
+ DIRECTION_TOP_TO_BOTTOM = :top_to_bottom
105
+ DIRECTION_BOTTOM_TO_TOP = :bottom_to_top
106
+ end
107
+
108
+ list = List.new(
109
+ highlight_spacing: List::HIGHLIGHT_WHEN_SELECTED,
110
+ direction: List::DIRECTION_TOP_TO_BOTTOM,
111
+ )
112
+ ```
113
+
114
+ Benefits:
115
+ - IDE autocomplete shows valid options
116
+ - Self-documenting code
117
+ - Typos caught at runtime (symbol vs constant)
118
+ - Easy to grep for where these modes are used
119
+
120
+ Affected widgets and their enum values:
121
+ - `List`: `highlight_spacing` (:always, :when_selected, :never), `direction` (:top_to_bottom, :bottom_to_top)
122
+ - `Table`: `highlight_spacing` (same as List), `flex` (:legacy, :default, :fill)
123
+ - `Layout`: `direction` (:vertical, :horizontal), `flex` (:legacy, :default, :fill)
124
+ - `Gauge`/`LineGauge`: `use_unicode` (boolean, but could have MODE_UNICODE, MODE_ASCII)
125
+ - `Paragraph`: `alignment` (:left, :center, :right)
126
+ - `Block`: `border_type` (:plain, :rounded, :double, :thick)
127
+ - `Canvas`: `marker` (:braille, :dots, :half_block, :sextant, :octant)
128
+
129
+ ### 4. Inconsistent Style APIs
130
+
131
+ **Current problem**: Different widgets accept styles differently:
132
+
133
+ ```ruby
134
+ # Table accepts both
135
+ table = Table.new(style: Style.new(fg: :blue))
136
+ table = Table.new(style: { fg: :blue }) # Hash shorthand
137
+
138
+ # But Paragraph doesn't
139
+ paragraph = Paragraph.new(text: "hi", style: Style.new(fg: :blue))
140
+ paragraph = Paragraph.new(text: "hi", style: { fg: :blue }) # Works but undocumented
141
+
142
+ # And Gauge has separate properties
143
+ gauge = Gauge.new(style: Style.new(fg: :blue), gauge_style: Style.new(fg: :green))
144
+ ```
145
+
146
+ **Suggested solution**: Standardize style handling across all widgets:
147
+
148
+ 1. All widgets should accept `Style` objects and `Hash` shorthand
149
+ 2. Document this clearly in each widget
150
+ 3. Add a convenience constructor:
151
+
152
+ ```ruby
153
+ class Style
154
+ def self.with(fg: nil, bg: nil, modifiers: [])
155
+ Style.new(fg: fg, bg: bg, modifiers: modifiers)
156
+ end
157
+ end
158
+
159
+ # Cleaner than always spelling out keyword args
160
+ paragraph = Paragraph.new(text: "hi", style: Style.with(fg: :blue))
161
+ ```
162
+
163
+ ### 5. Missing State Query Predicates
164
+
165
+ **Current problem**: Widgets store state but provide no query methods:
166
+
167
+ ```ruby
168
+ list.selected_index = 0
169
+
170
+ # To check if something is selected, must do:
171
+ if list.selected_index&.nonzero? # Awkward
172
+ if list.selected_index.nil? == false # Confusing
173
+
174
+ # Should be:
175
+ list.selected? # => true
176
+ list.empty? # => false (for items array)
177
+ ```
178
+
179
+ **Suggested solution**: Add predicates to state-holding widgets:
180
+
181
+ ```ruby
182
+ # List
183
+ list.selected? # => !selected_index.nil?
184
+ list.empty? # => items.empty?
185
+ list.selection # => selected_index (alias)
186
+ list.selected_item # => items[selected_index] (convenience)
187
+
188
+ # Table
189
+ table.selected_row? # => !selected_row.nil?
190
+ table.selected_cell? # => !selected_row.nil? && !selected_column.nil?
191
+ table.empty? # => rows.empty?
192
+
193
+ # Gauge
194
+ gauge.filled? # => ratio > 0
195
+ gauge.complete? # => ratio >= 1.0
196
+ ```
197
+
198
+ ### 6. Magic Numeric Coercions
199
+
200
+ **Current problem**: Widgets accept `Numeric` but silently coerce:
201
+
202
+ ```ruby
203
+ # These all work, but behavior is undocumented
204
+ list = List.new(selected_index: "2") # Coerced to 2
205
+ list = List.new(selected_index: 2.7) # Coerced to 2
206
+ list = List.new(selected_index: 2.0) # Coerced to 2
207
+
208
+ gauge = Gauge.new(percent: 150) # Should clamp?
209
+ gauge = Gauge.new(ratio: 1.5) # Should clamp?
210
+ ```
211
+
212
+ **Suggested solution**:
213
+
214
+ 1. Document coercion rules explicitly in RDoc
215
+ 2. Add validation and raise on invalid inputs:
216
+
217
+ ```ruby
218
+ def initialize(percent: nil, ...)
219
+ if percent
220
+ raise ArgumentError, "percent must be 0..100, got #{percent}" unless percent.between?(0, 100)
221
+ ratio = Float(percent) / 100.0
222
+ end
223
+ end
224
+ ```
225
+
226
+ 3. Provide clear error messages:
227
+ ```ruby
228
+ gauge = Gauge.new(percent: 150)
229
+ # => ArgumentError: percent must be between 0 and 100 (got 150)
230
+ ```
231
+
232
+ ## Implementation Strategy
233
+
234
+ ### Phase 1: Event Improvements (DONE)
235
+ - [x] Add `char` method to Key event
236
+ - [x] Implement dynamic predicates via `method_missing`
237
+ - [x] Update examples to use new API
238
+
239
+ ### Phase 2: State Query Predicates
240
+ - [ ] Add predicates to `List` (selected?, empty?, selected_item)
241
+ - [ ] Add predicates to `Table` (selected_row?, selected_cell?, empty?)
242
+ - [ ] Add predicates to `Gauge` (filled?, complete?)
243
+ - [ ] Tests for all new predicates
244
+
245
+ ### Phase 3: Symbol Constants
246
+ - [ ] Add enum constants to `List`, `Table`, `Layout`
247
+ - [ ] Add enum constants to `Gauge`, `LineGauge`, `Paragraph`, `Block`
248
+ - [ ] Update all examples to use constants
249
+ - [ ] Document constants in RDoc
250
+
251
+ ### Phase 4: Style Consistency
252
+ - [ ] Standardize `Hash` shorthand support across all widgets
253
+ - [ ] Add `Style.with(fg:, bg:, modifiers:)` convenience constructor
254
+ - [ ] Update `.rbs` files to reflect HashStyle support
255
+ - [ ] Document in style guide
256
+
257
+ ### Phase 5: Numeric Coercion Validation
258
+ - [ ] Add validation to `Gauge`, `LineGauge`, `List`, `Table`
259
+ - [ ] Raise `ArgumentError` on out-of-range values
260
+ - [ ] Provide clear error messages
261
+ - [ ] Update tests
262
+
263
+ ### Phase 6: Convenience Accessors
264
+ - [ ] Add `percent` to `Gauge` and `LineGauge`
265
+ - [ ] Add `selection` alias to `List` and `Table`
266
+ - [ ] Add `selected_item` to `List`
267
+ - [ ] Tests and documentation
268
+
269
+ ## Example: Before and After
270
+
271
+ ### Before (Confusing)
272
+ ```ruby
273
+ class GameApp
274
+ def initialize
275
+ @menu = List.new(
276
+ items: ["Start Game", "Load Game", "Options", "Quit"],
277
+ selected_index: 0,
278
+ highlight_spacing: :when_selected, # What's valid here?
279
+ direction: :top_to_bottom
280
+ )
281
+ end
282
+
283
+ def handle_input(event)
284
+ case event
285
+ when :ctrl_c
286
+ exit
287
+ when :up
288
+ if @menu.selected_index && @menu.selected_index > 0
289
+ @menu = @menu.with(selected_index: @menu.selected_index - 1)
290
+ end
291
+ end
292
+ end
293
+
294
+ def render(tui)
295
+ tui.draw(@menu)
296
+ end
297
+ end
298
+ ```
299
+
300
+ ### After (DWIM)
301
+ ```ruby
302
+ class GameApp
303
+ def initialize
304
+ @menu = List.new(
305
+ items: ["Start Game", "Load Game", "Options", "Quit"],
306
+ selected_index: 0,
307
+ highlight_spacing: List::HIGHLIGHT_WHEN_SELECTED, # IDE autocomplete!
308
+ direction: List::DIRECTION_TOP_TO_BOTTOM
309
+ )
310
+ end
311
+
312
+ def handle_input(event)
313
+ return if event.ctrl_c? # Dynamic predicate!
314
+
315
+ if event.up?
316
+ move_menu_up if @menu.selected? # State predicate!
317
+ end
318
+ end
319
+
320
+ def move_menu_up
321
+ index = @menu.selected_index
322
+ return if index == 0
323
+ @menu = @menu.with(selected_index: index - 1)
324
+ end
325
+
326
+ def render(tui)
327
+ tui.draw(@menu)
328
+ end
329
+ end
330
+ ```
331
+
332
+ ## Migration Path
333
+
334
+ All changes are backward compatible (additive):
335
+ - Existing code using symbols continues to work
336
+ - New constants coexist with symbols
337
+ - New predicates don't change existing behavior
338
+ - New methods are additions, not replacements
339
+
340
+ Apps can migrate at their own pace:
341
+ ```ruby
342
+ # Old style still works
343
+ list = List.new(highlight_spacing: :when_selected)
344
+
345
+ # New style also works
346
+ list = List.new(highlight_spacing: List::HIGHLIGHT_WHEN_SELECTED)
347
+
348
+ # Mix and match
349
+ if list.selected? # New predicate
350
+ puts list.selected_index # Old accessor
351
+ end
352
+ ```
353
+
354
+ ## Metrics for Success
355
+
356
+ 1. **Discoverability**: New developers can find valid options via IDE autocomplete
357
+ 2. **Clarity**: Code self-documents valid states and modes
358
+ 3. **Type safety**: Constants and predicates provide type checking
359
+ 4. **Error feedback**: Invalid inputs raise with helpful messages
360
+ 5. **Backward compatibility**: Zero breaking changes, all existing code works
361
+
362
+ ## Related Issues
363
+
364
+ - AGENTS.md requirement: All examples must have tests verifying behavior
365
+ - Example improvements: Apply constants and predicates to all example code
366
+ - Documentation: Update style guide with DWIM principles