ratatui_ruby 0.5.0 → 0.7.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 (311) 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 +10 -4
  7. data/CHANGELOG.md +79 -7
  8. data/README.md +37 -5
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +96 -22
  11. data/doc/application_testing.md +76 -30
  12. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  13. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  14. data/doc/contributors/architectural_overhaul/task.md +37 -0
  15. data/doc/contributors/design/ruby_frontend.md +288 -56
  16. data/doc/contributors/design/rust_backend.md +349 -54
  17. data/doc/contributors/developing_examples.md +134 -49
  18. data/doc/contributors/index.md +7 -5
  19. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  20. data/doc/event_handling.md +11 -3
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_canvas_demo.png +0 -0
  32. data/doc/images/widget_cell_demo.png +0 -0
  33. data/doc/images/widget_center_demo.png +0 -0
  34. data/doc/images/widget_chart_demo.png +0 -0
  35. data/doc/images/widget_list_demo.png +0 -0
  36. data/doc/images/widget_overlay_demo.png +0 -0
  37. data/doc/images/widget_render.png +0 -0
  38. data/doc/images/widget_rich_text.png +0 -0
  39. data/doc/images/widget_scroll_text.png +0 -0
  40. data/doc/images/widget_sparkline_demo.png +0 -0
  41. data/doc/images/widget_table_demo.png +0 -0
  42. data/doc/images/widget_tabs_demo.png +0 -0
  43. data/doc/images/widget_text_width.png +0 -0
  44. data/doc/index.md +11 -6
  45. data/doc/interactive_design.md +2 -2
  46. data/doc/quickstart.md +127 -165
  47. data/doc/terminal_limitations.md +92 -0
  48. data/doc/v0.7.0_migration.md +236 -0
  49. data/doc/why.md +93 -0
  50. data/examples/app_all_events/README.md +47 -27
  51. data/examples/app_all_events/app.rb +38 -35
  52. data/examples/app_all_events/model/app_model.rb +157 -0
  53. data/examples/app_all_events/model/event_entry.rb +17 -0
  54. data/examples/app_all_events/model/msg.rb +37 -0
  55. data/examples/app_all_events/update.rb +73 -0
  56. data/examples/app_all_events/view/app_view.rb +9 -9
  57. data/examples/app_all_events/view/controls_view.rb +9 -7
  58. data/examples/app_all_events/view/counts_view.rb +13 -9
  59. data/examples/app_all_events/view/live_view.rb +9 -8
  60. data/examples/app_all_events/view/log_view.rb +11 -16
  61. data/examples/app_color_picker/README.md +84 -42
  62. data/examples/app_color_picker/app.rb +24 -62
  63. data/examples/app_color_picker/controls.rb +90 -0
  64. data/examples/app_color_picker/copy_dialog.rb +45 -49
  65. data/examples/app_color_picker/export_pane.rb +126 -0
  66. data/examples/app_color_picker/input.rb +99 -67
  67. data/examples/app_color_picker/main_container.rb +178 -0
  68. data/examples/app_color_picker/palette.rb +55 -26
  69. data/examples/app_login_form/README.md +49 -0
  70. data/examples/app_login_form/app.rb +2 -3
  71. data/examples/app_stateful_interaction/README.md +33 -0
  72. data/examples/app_stateful_interaction/app.rb +272 -0
  73. data/examples/timeout_demo.rb +43 -0
  74. data/examples/verify_quickstart_dsl/README.md +49 -0
  75. data/examples/verify_quickstart_dsl/app.rb +2 -0
  76. data/examples/verify_quickstart_layout/README.md +71 -0
  77. data/examples/verify_quickstart_layout/app.rb +2 -0
  78. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  79. data/examples/verify_quickstart_lifecycle/app.rb +10 -4
  80. data/examples/verify_readme_usage/README.md +43 -0
  81. data/examples/verify_readme_usage/app.rb +8 -2
  82. data/examples/widget_barchart_demo/README.md +50 -0
  83. data/examples/widget_barchart_demo/app.rb +5 -5
  84. data/examples/widget_block_demo/README.md +36 -0
  85. data/examples/widget_block_demo/app.rb +256 -0
  86. data/examples/widget_box_demo/README.md +45 -0
  87. data/examples/widget_calendar_demo/README.md +39 -0
  88. data/examples/widget_calendar_demo/app.rb +5 -1
  89. data/examples/widget_canvas_demo/README.md +27 -0
  90. data/examples/widget_canvas_demo/app.rb +123 -0
  91. data/examples/widget_cell_demo/README.md +36 -0
  92. data/examples/widget_cell_demo/app.rb +31 -24
  93. data/examples/widget_center_demo/README.md +29 -0
  94. data/examples/widget_center_demo/app.rb +116 -0
  95. data/examples/widget_chart_demo/README.md +41 -0
  96. data/examples/widget_chart_demo/app.rb +7 -2
  97. data/examples/widget_gauge_demo/README.md +41 -0
  98. data/examples/widget_layout_split/README.md +44 -0
  99. data/examples/widget_line_gauge_demo/README.md +41 -0
  100. data/examples/widget_list_demo/README.md +49 -0
  101. data/examples/widget_list_demo/app.rb +91 -107
  102. data/examples/widget_map_demo/README.md +39 -0
  103. data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
  104. data/examples/widget_overlay_demo/README.md +36 -0
  105. data/examples/widget_overlay_demo/app.rb +248 -0
  106. data/examples/widget_popup_demo/README.md +36 -0
  107. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  108. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  109. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  110. data/examples/widget_rect/README.md +38 -0
  111. data/examples/widget_render/README.md +37 -0
  112. data/examples/widget_render/app.rb +3 -3
  113. data/examples/widget_rich_text/README.md +35 -0
  114. data/examples/widget_rich_text/app.rb +62 -33
  115. data/examples/widget_scroll_text/README.md +37 -0
  116. data/examples/widget_scroll_text/app.rb +0 -1
  117. data/examples/widget_scrollbar_demo/README.md +37 -0
  118. data/examples/widget_sparkline_demo/README.md +42 -0
  119. data/examples/widget_sparkline_demo/app.rb +4 -3
  120. data/examples/widget_style_colors/README.md +34 -0
  121. data/examples/widget_table_demo/README.md +48 -0
  122. data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
  123. data/examples/widget_tabs_demo/README.md +41 -0
  124. data/examples/widget_tabs_demo/app.rb +15 -1
  125. data/examples/widget_text_width/README.md +35 -0
  126. data/examples/widget_text_width/app.rb +113 -0
  127. data/exe/.gitkeep +0 -0
  128. data/ext/ratatui_ruby/Cargo.lock +11 -4
  129. data/ext/ratatui_ruby/Cargo.toml +2 -1
  130. data/ext/ratatui_ruby/src/events.rs +238 -26
  131. data/ext/ratatui_ruby/src/frame.rs +116 -3
  132. data/ext/ratatui_ruby/src/lib.rs +37 -6
  133. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  134. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  135. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  136. data/ext/ratatui_ruby/src/text.rs +13 -4
  137. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  138. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  139. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  140. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  141. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  142. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  143. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  144. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  145. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  146. data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
  147. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  148. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  149. data/lib/ratatui_ruby/buffer.rb +15 -0
  150. data/lib/ratatui_ruby/cell.rb +4 -4
  151. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  152. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  153. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  154. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  155. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  156. data/lib/ratatui_ruby/event/key.rb +111 -51
  157. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  158. data/lib/ratatui_ruby/event/paste.rb +1 -1
  159. data/lib/ratatui_ruby/frame.rb +100 -4
  160. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  161. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  162. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  163. data/lib/ratatui_ruby/layout.rb +19 -0
  164. data/lib/ratatui_ruby/list_state.rb +88 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  167. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  168. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  169. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  170. data/lib/ratatui_ruby/schema/list.rb +25 -4
  171. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  172. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  173. data/lib/ratatui_ruby/schema/row.rb +66 -0
  174. data/lib/ratatui_ruby/schema/style.rb +24 -4
  175. data/lib/ratatui_ruby/schema/table.rb +29 -11
  176. data/lib/ratatui_ruby/schema/text.rb +96 -3
  177. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  178. data/lib/ratatui_ruby/style/style.rb +81 -0
  179. data/lib/ratatui_ruby/style.rb +15 -0
  180. data/lib/ratatui_ruby/table_state.rb +90 -0
  181. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  182. data/lib/ratatui_ruby/test_helper/snapshot.rb +414 -0
  183. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  184. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  185. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  186. data/lib/ratatui_ruby/test_helper.rb +65 -358
  187. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  188. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  189. data/lib/ratatui_ruby/tui/core.rb +38 -0
  190. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  191. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  192. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  193. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  194. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  195. data/lib/ratatui_ruby/tui.rb +75 -0
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  198. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  199. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  200. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  201. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  202. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  203. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  204. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  205. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  206. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  207. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  208. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  209. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  210. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  211. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  212. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  213. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  214. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  215. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  216. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  217. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  218. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  219. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  220. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  221. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  222. data/lib/ratatui_ruby/widgets.rb +40 -0
  223. data/lib/ratatui_ruby.rb +64 -57
  224. data/sig/examples/app_all_events/view.rbs +1 -1
  225. data/sig/examples/app_all_events/view_state.rbs +1 -1
  226. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  227. data/sig/examples/widget_block_demo/app.rbs +32 -0
  228. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  229. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  230. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  231. data/sig/ratatui_ruby/event.rbs +11 -1
  232. data/sig/ratatui_ruby/frame.rbs +2 -0
  233. data/sig/ratatui_ruby/list_state.rbs +13 -0
  234. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  235. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  236. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  237. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  238. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  239. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  240. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  241. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  242. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  243. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  244. data/sig/ratatui_ruby/schema/text.rbs +9 -6
  245. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  246. data/sig/ratatui_ruby/session.rbs +41 -48
  247. data/sig/ratatui_ruby/table_state.rbs +15 -0
  248. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  249. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  250. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  251. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  252. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  253. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  254. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  255. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  256. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  257. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  258. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  259. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  260. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  261. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  262. data/sig/ratatui_ruby/tui.rbs +19 -0
  263. data/tasks/autodoc/examples.rb +79 -0
  264. data/tasks/autodoc.rake +7 -35
  265. data/tasks/bump/changelog.rb +3 -3
  266. data/tasks/bump/links.rb +67 -0
  267. data/tasks/sourcehut.rake +64 -21
  268. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  269. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  270. metadata +169 -48
  271. data/doc/contributors/dwim_dx.md +0 -366
  272. data/doc/images/app_analytics.png +0 -0
  273. data/doc/images/app_custom_widget.png +0 -0
  274. data/doc/images/app_mouse_events.png +0 -0
  275. data/doc/images/app_table_select.png +0 -0
  276. data/doc/images/widget_block_padding.png +0 -0
  277. data/doc/images/widget_block_titles.png +0 -0
  278. data/doc/images/widget_list_styles.png +0 -0
  279. data/doc/images/widget_table_flex.png +0 -0
  280. data/examples/app_all_events/model/events.rb +0 -180
  281. data/examples/app_all_events/model/highlight.rb +0 -57
  282. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  283. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  284. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  285. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  286. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  287. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  288. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  289. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  290. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  291. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  292. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  293. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  294. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  295. data/examples/app_all_events/view_state.rb +0 -42
  296. data/examples/app_color_picker/scene.rb +0 -201
  297. data/examples/widget_block_padding/app.rb +0 -67
  298. data/examples/widget_block_titles/app.rb +0 -69
  299. data/examples/widget_list_styles/app.rb +0 -141
  300. data/examples/widget_table_flex/app.rb +0 -95
  301. data/lib/ratatui_ruby/session/autodoc.rb +0 -417
  302. data/lib/ratatui_ruby/session.rb +0 -163
  303. data/sig/examples/widget_block_padding/app.rbs +0 -11
  304. data/sig/examples/widget_block_titles/app.rbs +0 -11
  305. data/sig/examples/widget_list_styles/app.rbs +0 -11
  306. data/tasks/autodoc/inventory.rb +0 -61
  307. data/tasks/autodoc/notice.rb +0 -26
  308. data/tasks/autodoc/rbs.rb +0 -38
  309. data/tasks/autodoc/rdoc.rb +0 -45
  310. data/tasks/bump/comparison_links.rb +0 -41
  311. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -1,366 +0,0 @@
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
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
Binary file
@@ -1,180 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
- # SPDX-License-Identifier: AGPL-3.0-or-later
5
-
6
- require_relative "timestamp"
7
- require_relative "event_color_cycle"
8
- require_relative "event_entry"
9
-
10
- # Manages the history and state of all application events.
11
- #
12
- # Applications need to track, count, and display event history for debugging and feedback.
13
- # Direct management of event arrays and counters across the app leads to scattered state.
14
- #
15
- # This class centralizes event storage. It records new events, maintains counts, and manages temporary highlights.
16
- #
17
- # Use it to store key presses, mouse clicks, and window resizes for display in a log or counter.
18
- #
19
- # === Examples
20
- #
21
- # events = Events.new
22
- # events.record(key_event)
23
- # puts events.count(:key) #=> 1
24
- #
25
- # if events.lit?(:key)
26
- # puts "Key event just happened!"
27
- # end
28
- class Events
29
- # Duration in milliseconds for an event to remain highlighted in the UI.
30
- HIGHLIGHT_DURATION_MS = 300
31
-
32
- # Creates a new Events manager.
33
- def initialize
34
- @entries = []
35
- @color_cycle = EventColorCycle.new
36
- @none_count = 0
37
- @lit_type = nil
38
- @lit_until = Timestamp.now
39
- @live = {}
40
- end
41
-
42
- # Records a new event.
43
- #
44
- # [event] RatatuiRuby::Event object.
45
- # [context] Hash of additional context (e.g., last_dimensions).
46
- #
47
- # === Example
48
- #
49
- # events.record(mouse_event)
50
- def record(event, context: {})
51
- if event.is_a?(RatatuiRuby::Event::None) || event == :none
52
- @none_count += 1
53
- return
54
- end
55
-
56
- color = @color_cycle.next_color
57
- timestamp = Timestamp.now
58
- entry = EventEntry.create(event, color, timestamp)
59
- @entries << entry
60
- update_lit_type(entry)
61
-
62
- display_type = live_type_for(entry.type)
63
- @live[display_type] = { time: Time.now, description: entry.description }
64
- end
65
-
66
- private def live_type_for(type)
67
- case type
68
- when :focus_gained, :focus_lost
69
- :focus
70
- else
71
- type
72
- end
73
- end
74
-
75
- # Returns the most recent live event data for a type.
76
- #
77
- # [type] Symbol event type to look up.
78
- #
79
- # === Example
80
- #
81
- # events.live_event(:key) #=> { time: ..., description: "..." }
82
- def live_event(type)
83
- @live[type]
84
- end
85
-
86
- # Returns all recorded live event data.
87
- #
88
- # === Example
89
- #
90
- # events.live_events #=> { key: { ... }, mouse: { ... } }
91
- def live_events
92
- @live
93
- end
94
-
95
- # Returns the most recent entries up to the given limit.
96
- #
97
- # [max_entries] Integer maximum number of entries to return.
98
- #
99
- # === Example
100
- #
101
- # events.visible(10) #=> [#<EventEntry ...>, ...]
102
- def visible(max_entries)
103
- @entries.last(max_entries)
104
- end
105
-
106
- # Checks if any events have been recorded.
107
- #
108
- # === Example
109
- #
110
- # events.empty? #=> true
111
- def empty?
112
- @entries.empty?
113
- end
114
-
115
- # Returns the count of events for a type.
116
- #
117
- # [type] Symbol event type.
118
- #
119
- # === Example
120
- #
121
- # events.count(:key) #=> 5
122
- def count(type)
123
- return @none_count if type == :none
124
-
125
- @entries.count { |e| e.matches_type?(type) }
126
- end
127
-
128
- # Returns counts grouped by subtype (kind or modifier status).
129
- #
130
- # [type] Symbol event type.
131
- #
132
- # === Example
133
- #
134
- # events.sub_counts(:mouse) #=> { "down" => 1, "up" => 2 }
135
- def sub_counts(type)
136
- return {} if type == :none
137
-
138
- entries = @entries.select { |e| e.matches_type?(type) }
139
-
140
- defaults = { key: %w[unmodified modified], focus: %w[gained lost], mouse: %w[down up drag moved scroll_up scroll_down] }
141
- entries.each_with_object(defaults.fetch(type, []).to_h { |k| [k, 0] }) do |entry, counts|
142
- group = if entry.event.respond_to?(:kind)
143
- entry.event.kind.to_s
144
- elsif entry.event.respond_to?(:modifiers)
145
- entry.event.modifiers.empty? ? "unmodified" : "modified"
146
- elsif type == :focus
147
- entry.type.to_s.sub("focus_", "")
148
- end
149
-
150
- counts[group] += 1 if group
151
- end
152
- end
153
-
154
- # Checks if a type should be highlighted.
155
- #
156
- # [type] Symbol event type.
157
- #
158
- # === Example
159
- #
160
- # events.lit?(:key) #=> true
161
- def lit?(type)
162
- return false if Timestamp.now.milliseconds >= @lit_until.milliseconds
163
-
164
- @lit_type == type
165
- end
166
-
167
- # Returns all event entries.
168
- #
169
- # === Example
170
- #
171
- # events.entries #=> [#<EventEntry ...>, ...]
172
- def entries
173
- @entries
174
- end
175
-
176
- private def update_lit_type(entry)
177
- @lit_type = live_type_for(entry.type)
178
- @lit_until = Timestamp.new(milliseconds: Timestamp.now.milliseconds + HIGHLIGHT_DURATION_MS)
179
- end
180
- end
@@ -1,57 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
- # SPDX-License-Identifier: AGPL-3.0-or-later
5
-
6
- require_relative "timestamp"
7
-
8
- # Manages temporary visual highlights for different event types.
9
- #
10
- # Users need visual feedback when an event occurs, but highlights should fade.
11
- # Manually tracking timers for every highlightable element is complex.
12
- #
13
- # This class manages the "lit" state of multiple keys with a consistent duration.
14
- #
15
- # Use it to trigger and check for temporary UI highlights.
16
- #
17
- # === Examples
18
- #
19
- # highlight = Highlight.new
20
- # highlight.light_up(:key)
21
- # highlight.lit?(:key) #=> true
22
- # sleep(0.4)
23
- # highlight.lit?(:key) #=> false
24
- class Highlight
25
- # Duration in milliseconds that a highlight remains active.
26
- DURATION_MS = 300
27
-
28
- # Creates a new Highlight manager.
29
- def initialize
30
- @lit_types = {}
31
- end
32
-
33
- # Triggers a highlight for the given type.
34
- #
35
- # [type] Symbol to highlight.
36
- #
37
- # === Example
38
- #
39
- # highlight.light_up(:mouse)
40
- def light_up(type)
41
- @lit_types[type] = Timestamp.now
42
- end
43
-
44
- # Checks if a highlight is currently active for the given type.
45
- #
46
- # [type] Symbol to check.
47
- #
48
- # === Example
49
- #
50
- # highlight.lit?(:mouse) #=> true
51
- def lit?(type)
52
- timestamp = @lit_types[type]
53
- return false unless timestamp
54
-
55
- !timestamp.elapsed?(DURATION_MS)
56
- end
57
- end
@@ -1,24 +0,0 @@
1
- ┌Event Counts──────┐ ┌Live Display─────────────────────────────────────────────┐
2
- │Key: 0 │ │Type Time Description │
3
- │ Unmodified: 0 │ │Key — — │
4
- │ Modified: 0 │ │Mouse — — │
5
- │Mouse: 0 │ │Resize — — │
6
- │ Down: 0 │ │Paste — — │
7
- │ Up: 0 │ │Focus XX:XX:XX #<RatatuiRuby::Event::FocusLost:0xXXXXXX│
8
- │ Drag: 0 │ │ │
9
- │ Moved: 0 │ └─────────────────────────────────────────────────────────┘
10
- │ Scroll_up: 0 │ ┌Event Log────────────────────────────────────────────────┐
11
- │ Scroll_down: 0 │ │#<RatatuiRuby::Event::FocusLost:0xXXXXXX> │
12
- │Resize: 0 │ │ │
13
- │Paste: 0 │ │ │
14
- │Focus: 1 │ │ │
15
- │ Gained: 0 │ │ │
16
- │ Lost: 1 │ │ │
17
- │None: 0 │ │ │
18
- │ │ │ │
19
- │ │ │ │
20
- │ │ │ │
21
- └──────────────────┘ └─────────────────────────────────────────────────────────┘
22
- ┌Controls──────────────────────────────────────────────────────────────────────┐
23
- │q: Quit Ctrl+C: Quit │
24
- └──────────────────────────────────────────────────────────────────────────────┘
@@ -1,24 +0,0 @@
1
- ┌Event Counts──────┐ ┌Live Display─────────────────────────────────────────────┐
2
- │Key: 0 │ │Type Time Description │
3
- │ Unmodified: 0 │ │Key — — │
4
- │ Modified: 0 │ │Mouse — — │
5
- │Mouse: 0 │ │Resize — — │
6
- │ Down: 0 │ │Paste — — │
7
- │ Up: 0 │ │Focus XX:XX:XX #<RatatuiRuby::Event::FocusGained:0xXXXXXX│
8
- │ Drag: 0 │ │ │
9
- │ Moved: 0 │ └─────────────────────────────────────────────────────────┘
10
- │ Scroll_up: 0 │ ┌Event Log────────────────────────────────────────────────┐
11
- │ Scroll_down: 0 │ │#<RatatuiRuby::Event::FocusLost:0xXXXXXX> │
12
- │Resize: 0 │ │ │
13
- │Paste: 0 │ │#<RatatuiRuby::Event::FocusGained:0xXXXXXX> │
14
- │Focus: 2 │ │ │
15
- │ Gained: 1 │ │ │
16
- │ Lost: 1 │ │ │
17
- │None: 0 │ │ │
18
- │ │ │ │
19
- │ │ │ │
20
- │ │ │ │
21
- └──────────────────┘ └─────────────────────────────────────────────────────────┘
22
- ┌Controls──────────────────────────────────────────────────────────────────────┐
23
- │q: Quit Ctrl+C: Quit │
24
- └──────────────────────────────────────────────────────────────────────────────┘