ratatui_ruby 0.4.0 → 0.5.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 (351) 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 +87 -171
  7. data/CHANGELOG.md +38 -1
  8. data/README.md +8 -3
  9. data/REUSE.toml +20 -0
  10. data/doc/application_architecture.md +105 -45
  11. data/doc/application_testing.md +5 -3
  12. data/doc/contributors/design/ruby_frontend.md +9 -5
  13. data/doc/contributors/developing_examples.md +76 -18
  14. data/doc/contributors/documentation_style.md +7 -0
  15. data/doc/contributors/index.md +2 -0
  16. data/doc/event_handling.md +10 -4
  17. data/doc/images/app_all_events.png +0 -0
  18. data/doc/images/app_color_picker.png +0 -0
  19. data/doc/images/verify_readme_usage.png +0 -0
  20. data/doc/images/widget_barchart_demo.png +0 -0
  21. data/doc/images/widget_block_padding.png +0 -0
  22. data/doc/images/widget_block_titles.png +0 -0
  23. data/doc/images/widget_box_demo.png +0 -0
  24. data/doc/images/widget_calendar_demo.png +0 -0
  25. data/doc/images/widget_cell_demo.png +0 -0
  26. data/doc/images/widget_chart_demo.png +0 -0
  27. data/doc/images/widget_gauge_demo.png +0 -0
  28. data/doc/images/widget_layout_split.png +0 -0
  29. data/doc/images/widget_line_gauge_demo.png +0 -0
  30. data/doc/images/widget_list_demo.png +0 -0
  31. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  32. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  33. data/doc/images/widget_render.png +0 -0
  34. data/doc/images/widget_scrollbar_demo.png +0 -0
  35. data/doc/images/widget_sparkline_demo.png +0 -0
  36. data/doc/images/widget_style_colors.png +0 -0
  37. data/doc/images/widget_table_flex.png +0 -0
  38. data/doc/images/widget_tabs_demo.png +0 -0
  39. data/doc/interactive_design.md +25 -30
  40. data/doc/quickstart.md +147 -120
  41. data/examples/app_all_events/README.md +81 -0
  42. data/examples/app_all_events/app.rb +93 -0
  43. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  44. data/examples/app_all_events/model/event_entry.rb +75 -0
  45. data/examples/app_all_events/model/events.rb +180 -0
  46. data/examples/app_all_events/model/highlight.rb +57 -0
  47. data/examples/app_all_events/model/timestamp.rb +54 -0
  48. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +24 -0
  49. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +24 -0
  50. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +24 -0
  51. data/examples/app_all_events/test/snapshots/after_key_a.txt +24 -0
  52. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +24 -0
  53. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +24 -0
  54. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +24 -0
  55. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +24 -0
  56. data/examples/app_all_events/test/snapshots/after_paste.txt +24 -0
  57. data/examples/app_all_events/test/snapshots/after_resize.txt +24 -0
  58. data/examples/app_all_events/test/snapshots/after_right_click.txt +24 -0
  59. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +24 -0
  60. data/examples/app_all_events/test/snapshots/initial_state.txt +24 -0
  61. data/examples/app_all_events/view/app_view.rb +78 -0
  62. data/examples/app_all_events/view/controls_view.rb +50 -0
  63. data/examples/app_all_events/view/counts_view.rb +55 -0
  64. data/examples/app_all_events/view/live_view.rb +69 -0
  65. data/examples/app_all_events/view/log_view.rb +60 -0
  66. data/examples/app_all_events/view.rb +7 -0
  67. data/examples/app_all_events/view_state.rb +42 -0
  68. data/examples/app_color_picker/README.md +94 -0
  69. data/examples/app_color_picker/app.rb +112 -0
  70. data/examples/app_color_picker/clipboard.rb +84 -0
  71. data/examples/app_color_picker/color.rb +191 -0
  72. data/examples/app_color_picker/copy_dialog.rb +170 -0
  73. data/examples/app_color_picker/harmony.rb +56 -0
  74. data/examples/app_color_picker/input.rb +142 -0
  75. data/examples/app_color_picker/palette.rb +80 -0
  76. data/examples/app_color_picker/scene.rb +201 -0
  77. data/examples/{login_form → app_login_form}/app.rb +39 -42
  78. data/examples/{map_demo → app_map_demo}/app.rb +24 -21
  79. data/examples/{table_select → app_table_select}/app.rb +68 -65
  80. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +15 -6
  81. data/examples/verify_quickstart_layout/app.rb +69 -0
  82. data/examples/{quickstart_lifecycle → verify_quickstart_lifecycle}/app.rb +19 -10
  83. data/examples/verify_readme_usage/app.rb +34 -0
  84. data/examples/widget_barchart_demo/app.rb +238 -0
  85. data/examples/{block_padding → widget_block_padding}/app.rb +17 -13
  86. data/examples/{block_titles → widget_block_titles}/app.rb +25 -17
  87. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  88. data/examples/widget_calendar_demo/app.rb +109 -0
  89. data/examples/widget_cell_demo/app.rb +104 -0
  90. data/examples/widget_chart_demo/app.rb +213 -0
  91. data/examples/widget_gauge_demo/app.rb +212 -0
  92. data/examples/widget_layout_split/app.rb +246 -0
  93. data/examples/widget_line_gauge_demo/app.rb +217 -0
  94. data/examples/widget_list_demo/app.rb +382 -0
  95. data/examples/widget_list_styles/app.rb +141 -0
  96. data/examples/widget_popup_demo/app.rb +104 -0
  97. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  98. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  99. data/examples/widget_rect/app.rb +205 -0
  100. data/examples/widget_render/app.rb +184 -0
  101. data/examples/widget_rich_text/app.rb +137 -0
  102. data/examples/widget_scroll_text/app.rb +108 -0
  103. data/examples/widget_scrollbar_demo/app.rb +153 -0
  104. data/examples/widget_sparkline_demo/app.rb +274 -0
  105. data/examples/widget_style_colors/app.rb +19 -21
  106. data/examples/widget_table_flex/app.rb +95 -0
  107. data/examples/widget_tabs_demo/app.rb +167 -0
  108. data/ext/ratatui_ruby/Cargo.lock +1 -1
  109. data/ext/ratatui_ruby/Cargo.toml +1 -1
  110. data/ext/ratatui_ruby/src/events.rs +121 -36
  111. data/ext/ratatui_ruby/src/frame.rs +115 -0
  112. data/ext/ratatui_ruby/src/lib.rs +79 -26
  113. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  114. data/ext/ratatui_ruby/src/style.rs +138 -57
  115. data/ext/ratatui_ruby/src/terminal.rs +5 -9
  116. data/ext/ratatui_ruby/src/text.rs +13 -6
  117. data/ext/ratatui_ruby/src/widgets/barchart.rs +56 -54
  118. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  119. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  120. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  121. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  122. data/ext/ratatui_ruby/src/widgets/list.rs +32 -9
  123. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  124. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  125. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  126. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  127. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +4 -2
  128. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  129. data/ext/ratatui_ruby/src/widgets/table.rs +8 -4
  130. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  131. data/lib/ratatui_ruby/cell.rb +3 -3
  132. data/lib/ratatui_ruby/event/key.rb +1 -1
  133. data/lib/ratatui_ruby/event/none.rb +43 -0
  134. data/lib/ratatui_ruby/event.rb +56 -4
  135. data/lib/ratatui_ruby/frame.rb +87 -0
  136. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +11 -11
  137. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  138. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  139. data/lib/ratatui_ruby/schema/block.rb +163 -168
  140. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  141. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  142. data/lib/ratatui_ruby/schema/center.rb +46 -46
  143. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  144. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  145. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  146. data/lib/ratatui_ruby/schema/cursor.rb +25 -25
  147. data/lib/ratatui_ruby/schema/gauge.rb +53 -53
  148. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  149. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  150. data/lib/ratatui_ruby/schema/list.rb +86 -84
  151. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  152. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  153. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  154. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  155. data/lib/ratatui_ruby/schema/rect.rb +60 -60
  156. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  157. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  158. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  159. data/lib/ratatui_ruby/schema/style.rb +46 -46
  160. data/lib/ratatui_ruby/schema/table.rb +112 -119
  161. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  162. data/lib/ratatui_ruby/session/autodoc.rb +417 -0
  163. data/lib/ratatui_ruby/session.rb +40 -23
  164. data/lib/ratatui_ruby/test_helper.rb +185 -19
  165. data/lib/ratatui_ruby/version.rb +1 -1
  166. data/lib/ratatui_ruby.rb +65 -39
  167. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  168. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  169. data/sig/examples/app_all_events/model/events.rbs +15 -0
  170. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  171. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  172. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  173. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  174. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  175. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  176. data/sig/examples/app_all_events/view.rbs +8 -0
  177. data/sig/examples/app_all_events/view_state.rbs +15 -0
  178. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  179. data/sig/examples/app_login_form/app.rbs +11 -0
  180. data/sig/examples/app_map_demo/app.rbs +11 -0
  181. data/sig/examples/app_table_select/app.rbs +11 -0
  182. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  183. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  184. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  185. data/sig/examples/widget_block_padding/app.rbs +11 -0
  186. data/sig/examples/widget_block_titles/app.rbs +11 -0
  187. data/sig/examples/widget_box_demo/app.rbs +11 -0
  188. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  189. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  190. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  191. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  192. data/sig/examples/widget_layout_split/app.rbs +10 -0
  193. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  194. data/sig/examples/widget_list_demo/app.rbs +12 -0
  195. data/sig/examples/widget_list_styles/app.rbs +11 -0
  196. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  197. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  198. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  199. data/sig/examples/widget_rect/app.rbs +12 -0
  200. data/sig/examples/widget_render/app.rbs +10 -0
  201. data/sig/examples/widget_rich_text/app.rbs +11 -0
  202. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  203. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  204. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  205. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  206. data/sig/examples/widget_table_flex/app.rbs +11 -0
  207. data/sig/ratatui_ruby/frame.rbs +9 -0
  208. data/sig/ratatui_ruby/ratatui_ruby.rbs +3 -2
  209. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  210. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  211. data/sig/ratatui_ruby/session.rbs +94 -0
  212. data/tasks/autodoc/inventory.rb +61 -0
  213. data/tasks/autodoc/member.rb +56 -0
  214. data/tasks/autodoc/name.rb +19 -0
  215. data/tasks/autodoc/notice.rb +26 -0
  216. data/tasks/autodoc/rbs.rb +38 -0
  217. data/tasks/autodoc/rdoc.rb +45 -0
  218. data/tasks/autodoc.rake +47 -0
  219. data/tasks/bump/history.rb +2 -2
  220. data/tasks/doc.rake +600 -6
  221. data/tasks/example_viewer.html.erb +172 -0
  222. data/tasks/lint.rake +8 -4
  223. data/tasks/resources/index.html.erb +6 -0
  224. data/tasks/sourcehut.rake +4 -4
  225. data/tasks/terminal_preview/app_screenshot.rb +1 -3
  226. data/tasks/terminal_preview/crash_report.rb +7 -9
  227. data/tasks/terminal_preview/launcher_script.rb +4 -6
  228. data/tasks/terminal_preview/preview_collection.rb +4 -6
  229. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  230. data/tasks/terminal_preview/saved_screenshot.rb +7 -9
  231. data/tasks/terminal_preview/terminal_window.rb +7 -9
  232. data/tasks/test.rake +1 -1
  233. data/tasks/website/index_page.rb +3 -3
  234. data/tasks/website/version.rb +10 -10
  235. data/tasks/website/version_menu.rb +10 -12
  236. data/tasks/website/versioned_documentation.rb +49 -17
  237. data/tasks/website/website.rb +6 -8
  238. data/tasks/website.rake +4 -4
  239. metadata +156 -125
  240. data/LICENSES/BSD-2-Clause.txt +0 -9
  241. data/doc/contributors/better_dx.md +0 -543
  242. data/doc/contributors/example_analysis.md +0 -82
  243. data/doc/images/all_events.png +0 -0
  244. data/doc/images/block_padding.png +0 -0
  245. data/doc/images/block_titles.png +0 -0
  246. data/doc/images/box_demo.png +0 -0
  247. data/doc/images/calendar_demo.png +0 -0
  248. data/doc/images/cell_demo.png +0 -0
  249. data/doc/images/chart_demo.png +0 -0
  250. data/doc/images/flex_layout.png +0 -0
  251. data/doc/images/gauge_demo.png +0 -0
  252. data/doc/images/line_gauge_demo.png +0 -0
  253. data/doc/images/list_demo.png +0 -0
  254. data/doc/images/readme_usage.png +0 -0
  255. data/doc/images/scrollbar_demo.png +0 -0
  256. data/doc/images/sparkline_demo.png +0 -0
  257. data/doc/images/table_flex.png +0 -0
  258. data/examples/all_events/app.rb +0 -169
  259. data/examples/all_events/app.rbs +0 -7
  260. data/examples/all_events/test_app.rb +0 -139
  261. data/examples/analytics/app.rb +0 -258
  262. data/examples/analytics/app.rbs +0 -7
  263. data/examples/analytics/test_app.rb +0 -132
  264. data/examples/block_padding/app.rbs +0 -7
  265. data/examples/block_padding/test_app.rb +0 -31
  266. data/examples/block_titles/app.rbs +0 -7
  267. data/examples/block_titles/test_app.rb +0 -34
  268. data/examples/box_demo/app.rbs +0 -7
  269. data/examples/box_demo/test_app.rb +0 -88
  270. data/examples/calendar_demo/app.rb +0 -101
  271. data/examples/calendar_demo/app.rbs +0 -7
  272. data/examples/calendar_demo/test_app.rb +0 -108
  273. data/examples/cell_demo/app.rb +0 -108
  274. data/examples/cell_demo/app.rbs +0 -7
  275. data/examples/cell_demo/test_app.rb +0 -36
  276. data/examples/chart_demo/app.rb +0 -203
  277. data/examples/chart_demo/app.rbs +0 -7
  278. data/examples/chart_demo/test_app.rb +0 -102
  279. data/examples/custom_widget/app.rb +0 -51
  280. data/examples/custom_widget/app.rbs +0 -7
  281. data/examples/custom_widget/test_app.rb +0 -30
  282. data/examples/flex_layout/app.rb +0 -156
  283. data/examples/flex_layout/app.rbs +0 -7
  284. data/examples/flex_layout/test_app.rb +0 -65
  285. data/examples/gauge_demo/app.rb +0 -182
  286. data/examples/gauge_demo/test_app.rb +0 -120
  287. data/examples/hit_test/app.rb +0 -175
  288. data/examples/hit_test/app.rbs +0 -7
  289. data/examples/hit_test/test_app.rb +0 -102
  290. data/examples/line_gauge_demo/app.rb +0 -190
  291. data/examples/line_gauge_demo/app.rbs +0 -7
  292. data/examples/line_gauge_demo/test_app.rb +0 -129
  293. data/examples/list_demo/app.rb +0 -253
  294. data/examples/list_demo/test_app.rb +0 -237
  295. data/examples/list_styles/app.rb +0 -140
  296. data/examples/list_styles/app.rbs +0 -7
  297. data/examples/list_styles/test_app.rb +0 -157
  298. data/examples/login_form/app.rbs +0 -7
  299. data/examples/login_form/test_app.rb +0 -51
  300. data/examples/map_demo/app.rbs +0 -7
  301. data/examples/map_demo/test_app.rb +0 -149
  302. data/examples/mouse_events/app.rb +0 -97
  303. data/examples/mouse_events/app.rbs +0 -7
  304. data/examples/mouse_events/test_app.rb +0 -53
  305. data/examples/popup_demo/app.rb +0 -103
  306. data/examples/popup_demo/app.rbs +0 -7
  307. data/examples/popup_demo/test_app.rb +0 -54
  308. data/examples/quickstart_dsl/app.rbs +0 -7
  309. data/examples/quickstart_dsl/test_app.rb +0 -29
  310. data/examples/quickstart_lifecycle/app.rbs +0 -7
  311. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  312. data/examples/ratatui_logo_demo/app.rb +0 -79
  313. data/examples/ratatui_logo_demo/app.rbs +0 -7
  314. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  315. data/examples/ratatui_mascot_demo/app.rb +0 -84
  316. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  317. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  318. data/examples/readme_usage/app.rb +0 -29
  319. data/examples/readme_usage/app.rbs +0 -7
  320. data/examples/readme_usage/test_app.rb +0 -29
  321. data/examples/rich_text/app.rb +0 -141
  322. data/examples/rich_text/app.rbs +0 -7
  323. data/examples/rich_text/test_app.rb +0 -166
  324. data/examples/scroll_text/app.rb +0 -103
  325. data/examples/scroll_text/app.rbs +0 -7
  326. data/examples/scroll_text/test_app.rb +0 -110
  327. data/examples/scrollbar_demo/app.rb +0 -143
  328. data/examples/scrollbar_demo/app.rbs +0 -7
  329. data/examples/scrollbar_demo/test_app.rb +0 -77
  330. data/examples/sparkline_demo/app.rb +0 -240
  331. data/examples/sparkline_demo/test_app.rb +0 -107
  332. data/examples/table_flex/app.rb +0 -65
  333. data/examples/table_flex/app.rbs +0 -7
  334. data/examples/table_flex/test_app.rb +0 -36
  335. data/examples/table_select/app.rbs +0 -7
  336. data/examples/table_select/test_app.rb +0 -180
  337. data/examples/widget_style_colors/test_app.rb +0 -48
  338. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  339. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  340. /data/doc/images/{login_form.png → app_login_form.png} +0 -0
  341. /data/doc/images/{map_demo.png → app_map_demo.png} +0 -0
  342. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  343. /data/doc/images/{table_select.png → app_table_select.png} +0 -0
  344. /data/doc/images/{quickstart_dsl.png → verify_quickstart_dsl.png} +0 -0
  345. /data/doc/images/{ratatui_logo_demo.png → verify_quickstart_layout.png} +0 -0
  346. /data/doc/images/{quickstart_lifecycle.png → verify_quickstart_lifecycle.png} +0 -0
  347. /data/doc/images/{list_styles.png → widget_list_styles.png} +0 -0
  348. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  349. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  350. /data/doc/images/{rich_text.png → widget_rich_text.png} +0 -0
  351. /data/doc/images/{scroll_text.png → widget_scroll_text.png} +0 -0
@@ -0,0 +1,213 @@
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
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ # Demonstrates Cartesian plotting attributes with interactive cycling.
10
+ #
11
+ # Trends and patterns are invisible in raw logs. You need to see the shape of the data to understand the story it tells.
12
+ #
13
+ # This demo showcases the <tt>Chart</tt> widget. It provides an interactive playground where you can toggle marker types, axis alignments, and legend positions in real-time.
14
+ #
15
+ # Use it to understand how to visualize complex X/Y datasets and trends efficiently.
16
+ #
17
+ # === Example
18
+ #
19
+ # Run the demo from the terminal:
20
+ #
21
+ # ruby examples/widget_chart_demo/app.rb
22
+ #
23
+ # rdoc-image:/doc/images/widget_chart_demo.png
24
+ class WidgetChartDemo
25
+ MARKERS = [
26
+ { name: "Dot (·)", marker: :dot },
27
+ { name: "Braille", marker: :braille },
28
+ { name: "Block (█)", marker: :block },
29
+ { name: "Bar", marker: :bar },
30
+ ].freeze
31
+
32
+ X_ALIGNMENTS = [
33
+ { name: "Left", alignment: :left },
34
+ { name: "Center", alignment: :center },
35
+ { name: "Right", alignment: :right },
36
+ ].freeze
37
+
38
+ Y_ALIGNMENTS = [
39
+ { name: "Left", alignment: :left },
40
+ { name: "Center", alignment: :center },
41
+ { name: "Right", alignment: :right },
42
+ ].freeze
43
+
44
+ LEGEND_POSITIONS = [
45
+ { name: "Top Right", position: :top_right },
46
+ { name: "Top Left", position: :top_left },
47
+ { name: "Bottom Right", position: :bottom_right },
48
+ { name: "Bottom Left", position: :bottom_left },
49
+ ].freeze
50
+
51
+ def run
52
+ RatatuiRuby.run do |tui|
53
+ @tui = tui
54
+ init_styles
55
+
56
+ @marker_index = 0
57
+ @dataset_style_index = 0
58
+ @x_alignment_index = 1
59
+ @y_alignment_index = 2
60
+ @legend_position_index = 0
61
+
62
+ loop do
63
+ render
64
+ break if handle_input == :quit
65
+ sleep 0.05
66
+ end
67
+ end
68
+ end
69
+
70
+ private def init_styles
71
+ @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
72
+ @dataset_styles = [
73
+ { name: "Yellow", style: @tui.style(fg: :yellow) },
74
+ { name: "Green", style: @tui.style(fg: :green) },
75
+ { name: "Cyan", style: @tui.style(fg: :cyan) },
76
+ { name: "Red", style: @tui.style(fg: :red) },
77
+ { name: "Magenta", style: @tui.style(fg: :magenta) },
78
+ { name: "Bold Blue", style: @tui.style(fg: :blue, modifiers: [:bold]) },
79
+ { name: "Dim White", style: @tui.style(fg: :white, modifiers: [:dim]) },
80
+ { name: "Italic Green", style: @tui.style(fg: :green, modifiers: [:italic]) },
81
+ { name: "Alert (Red/White/Bar)", style: @tui.style(fg: :white, bg: :red, modifiers: [:bold]) },
82
+ ]
83
+ end
84
+
85
+ private def render
86
+ # Static sample data: sine wave with wider range for better visibility
87
+ line_data = (0..50).map do |i|
88
+ x = i / 5.0
89
+ [x, Math.sin(x)]
90
+ end
91
+
92
+ # Scatter: Random points
93
+ scatter_data = (0..20).map do |_|
94
+ [rand(0.0..10.0), rand(-1.0..1.0)]
95
+ end
96
+
97
+ style = @dataset_styles[@dataset_style_index][:style]
98
+ # Ensure the second dataset has a different style
99
+ scatter_style = @dataset_styles[(@dataset_style_index + 2) % @dataset_styles.length][:style]
100
+
101
+ datasets = [
102
+ @tui.dataset(
103
+ name: "Line",
104
+ data: line_data,
105
+ style:,
106
+ marker: (style.modifiers.include?(:bold) && style.bg) ? :bar : MARKERS[@marker_index][:marker],
107
+ graph_type: :line
108
+ ),
109
+ @tui.dataset(
110
+ name: "Scatter",
111
+ data: scatter_data,
112
+ style: scatter_style,
113
+ marker: (scatter_style.modifiers.include?(:bold) && scatter_style.bg) ? :bar : MARKERS[@marker_index][:marker],
114
+ graph_type: :scatter
115
+ ),
116
+ ]
117
+
118
+ chart = @tui.chart(
119
+ datasets:,
120
+ x_axis: @tui.axis(
121
+ title: "Time",
122
+ bounds: [0.0, 10.0],
123
+ labels: %w[0 5 10],
124
+ style: @tui.style(fg: :yellow),
125
+ labels_alignment: X_ALIGNMENTS[@x_alignment_index][:alignment]
126
+ ),
127
+ y_axis: @tui.axis(
128
+ title: "Amplitude",
129
+ bounds: [-1.0, 1.0],
130
+ labels: %w[-1 0 1],
131
+ style: @tui.style(fg: :cyan),
132
+ labels_alignment: Y_ALIGNMENTS[@y_alignment_index][:alignment]
133
+ ),
134
+ block: @tui.block(
135
+ title: "Chart Widget Demo",
136
+ borders: [:all]
137
+ ),
138
+ legend_position: LEGEND_POSITIONS[@legend_position_index][:position],
139
+ hidden_legend_constraints: [
140
+ @tui.constraint_min(20),
141
+ @tui.constraint_min(10),
142
+ ]
143
+ )
144
+
145
+ controls = @tui.block(
146
+ title: "Controls",
147
+ borders: [:all],
148
+ children: [
149
+ @tui.paragraph(
150
+ text: [
151
+ # Line 1: Markers & Colors
152
+ @tui.text_line(spans: [
153
+ @tui.text_span(content: "m", style: @hotkey_style),
154
+ @tui.text_span(content: ": Marker (#{MARKERS[@marker_index][:name]}) "),
155
+ @tui.text_span(content: "s", style: @hotkey_style),
156
+ @tui.text_span(content: ": Style (#{@dataset_styles[@dataset_style_index][:name]})"),
157
+ ]),
158
+ # Line 2: Axis alignments
159
+ @tui.text_line(spans: [
160
+ @tui.text_span(content: "x", style: @hotkey_style),
161
+ @tui.text_span(content: ": X Align (#{X_ALIGNMENTS[@x_alignment_index][:name]}) "),
162
+ @tui.text_span(content: "y", style: @hotkey_style),
163
+ @tui.text_span(content: ": Y Align (#{Y_ALIGNMENTS[@y_alignment_index][:name]}) "),
164
+ @tui.text_span(content: "l", style: @hotkey_style),
165
+ @tui.text_span(content: ": Legend (#{LEGEND_POSITIONS[@legend_position_index][:name]})"),
166
+ ]),
167
+ # Line 3: Quit
168
+ @tui.text_line(spans: [
169
+ @tui.text_span(content: "q", style: @hotkey_style),
170
+ @tui.text_span(content: ": Quit"),
171
+ ]),
172
+ ]
173
+ ),
174
+ ]
175
+ )
176
+
177
+ @tui.draw do |frame|
178
+ chart_area, controls_area = @tui.layout_split(
179
+ frame.area,
180
+ direction: :vertical,
181
+ constraints: [
182
+ @tui.constraint_fill(1),
183
+ @tui.constraint_length(5),
184
+ ]
185
+ )
186
+ frame.render_widget(chart, chart_area)
187
+ frame.render_widget(controls, controls_area)
188
+ end
189
+ end
190
+
191
+ private def handle_input
192
+ event = @tui.poll_event
193
+
194
+ case event
195
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
196
+ :quit
197
+ in type: :key, code: "m"
198
+ @marker_index = (@marker_index + 1) % MARKERS.length
199
+ in type: :key, code: "s"
200
+ @dataset_style_index = (@dataset_style_index + 1) % @dataset_styles.length
201
+ in type: :key, code: "x"
202
+ @x_alignment_index = (@x_alignment_index + 1) % X_ALIGNMENTS.length
203
+ in type: :key, code: "y"
204
+ @y_alignment_index = (@y_alignment_index + 1) % Y_ALIGNMENTS.length
205
+ in type: :key, code: "l"
206
+ @legend_position_index = (@legend_position_index + 1) % LEGEND_POSITIONS.length
207
+ else
208
+ nil
209
+ end
210
+ end
211
+ end
212
+
213
+ WidgetChartDemo.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,212 @@
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
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ # Demonstrates completion visualization with interactive attribute cycling.
10
+ #
11
+ # Long-running tasks create anxiety. Users need to know that the system is working and how much is left to do.
12
+ #
13
+ # This demo showcases the <tt>Gauge</tt> widget. It provides an interactive playground where you can cycle through different ratios, colors, and label templates in real-time.
14
+ #
15
+ # Use it to understand how to communicate progress and task status in your terminal interface.
16
+ #
17
+ # === Example
18
+ #
19
+ # Run the demo from the terminal:
20
+ #
21
+ # ruby examples/widget_gauge_demo/app.rb
22
+ #
23
+ # rdoc-image:/doc/images/widget_gauge_demo.png
24
+ class WidgetGaugeDemo
25
+ def initialize
26
+ @ratio = 0.65
27
+ @ratios = [0.0, 0.25, 0.5, 0.65, 0.8, 0.95, 1.0]
28
+ @ratio_index = 3
29
+
30
+ @gauge_colors = [
31
+ { name: "Green", color: :green },
32
+ { name: "Yellow", color: :yellow },
33
+ { name: "Red", color: :red },
34
+ { name: "Cyan", color: :cyan },
35
+ { name: "Blue", color: :blue },
36
+ ]
37
+ @gauge_color_index = 0
38
+
39
+ @bg_styles = nil # Initialized in run when @tui is available
40
+ @bg_style_index = 1
41
+
42
+ @use_unicode_options = [true, false]
43
+ @use_unicode_index = 0
44
+
45
+ @label_modes = [
46
+ { name: "Percentage", template: -> (ratio) { "#{(ratio * 100).to_i}%" } },
47
+ { name: "Ratio (decimal)", template: -> (ratio) { format("%.2f", ratio) } },
48
+ { name: "Progress", template: -> (ratio) { "Progress: #{(ratio * 100).to_i}%" } },
49
+ { name: "None", template: -> (_ratio) { nil } },
50
+ ]
51
+ @label_mode_index = 0
52
+ @hotkey_style = nil # Initialized in run when @tui is available
53
+ end
54
+
55
+ def run
56
+ RatatuiRuby.run do |tui|
57
+ @tui = tui
58
+
59
+ # Initialize styles using tui helpers
60
+ @bg_styles = [
61
+ { name: "None", style: nil },
62
+ { name: "Dark Gray BG", style: tui.style(fg: :dark_gray) },
63
+ { name: "White on Black", style: tui.style(fg: :white, bg: :black) },
64
+ { name: "Bold White", style: tui.style(fg: :white, modifiers: [:bold]) },
65
+ ]
66
+ @hotkey_style = tui.style(modifiers: [:bold, :underlined])
67
+
68
+ loop do
69
+ render
70
+ break if handle_input == :quit
71
+ end
72
+ end
73
+ end
74
+
75
+ private def render
76
+ @ratio = @ratios[@ratio_index]
77
+ gauge_color = @gauge_colors[@gauge_color_index][:color]
78
+ bg_style = @bg_styles[@bg_style_index][:style]
79
+ use_unicode = @use_unicode_options[@use_unicode_index]
80
+ label_template = @label_modes[@label_mode_index][:template]
81
+
82
+ gauge_style = @tui.style(fg: gauge_color)
83
+ label = label_template.call(@ratio)
84
+
85
+ @tui.draw do |frame|
86
+ # Split into main content and control panel
87
+ main_area, controls_area = @tui.layout_split(
88
+ frame.area,
89
+ direction: :vertical,
90
+ constraints: [
91
+ @tui.constraint_fill(1),
92
+ @tui.constraint_length(6),
93
+ ]
94
+ )
95
+
96
+ # Split main area into title, gauges, and spacer
97
+ title_area, gauge1_area, gauge2_area, gauge3_area, spacer_area = @tui.layout_split(
98
+ main_area,
99
+ direction: :vertical,
100
+ constraints: [
101
+ @tui.constraint_length(1),
102
+ @tui.constraint_fill(1),
103
+ @tui.constraint_fill(1),
104
+ @tui.constraint_fill(1),
105
+ @tui.constraint_length(1),
106
+ ]
107
+ )
108
+
109
+ # Render title
110
+ title = @tui.paragraph(
111
+ text: "Gauge Widget Demo",
112
+ style: @tui.style(modifiers: [:bold])
113
+ )
114
+ frame.render_widget(title, title_area)
115
+
116
+ # Gauge 1: Main interactive gauge
117
+ gauge1 = @tui.gauge(
118
+ ratio: @ratio,
119
+ label:,
120
+ style: bg_style,
121
+ gauge_style:,
122
+ use_unicode:,
123
+ block: @tui.block(title: "Interactive Gauge")
124
+ )
125
+ frame.render_widget(gauge1, gauge1_area)
126
+
127
+ # Gauge 2: Inverse ratio for comparison
128
+ gauge2 = @tui.gauge(
129
+ ratio: 1.0 - @ratio,
130
+ label: label_template.call(1.0 - @ratio),
131
+ style: bg_style,
132
+ gauge_style:,
133
+ use_unicode:,
134
+ block: @tui.block(title: "Inverse (1.0 - ratio)")
135
+ )
136
+ frame.render_widget(gauge2, gauge2_area)
137
+
138
+ # Gauge 3: Fixed at different stages
139
+ gauge3 = @tui.gauge(
140
+ ratio: [@ratio, 0.5].max,
141
+ label: "Min 50%",
142
+ style: @tui.style(fg: :dark_gray),
143
+ gauge_style: @tui.style(fg: :magenta),
144
+ use_unicode:,
145
+ block: @tui.block(title: "Min Threshold (Magenta)")
146
+ )
147
+ frame.render_widget(gauge3, gauge3_area)
148
+
149
+ # Render empty spacer
150
+ spacer = @tui.paragraph(text: "")
151
+ frame.render_widget(spacer, spacer_area)
152
+
153
+ # Bottom controls panel
154
+ controls = @tui.block(
155
+ title: "Controls",
156
+ borders: [:all],
157
+ children: [
158
+ @tui.paragraph(
159
+ text: [
160
+ # Navigation & General
161
+ @tui.text_line(spans: [
162
+ @tui.text_span(content: "←/→", style: @hotkey_style),
163
+ @tui.text_span(content: ": Adjust Ratio (#{format('%.2f', @ratio)}) "),
164
+ @tui.text_span(content: "q", style: @hotkey_style),
165
+ @tui.text_span(content: ": Quit"),
166
+ ]),
167
+ # Styling
168
+ @tui.text_line(spans: [
169
+ @tui.text_span(content: "g", style: @hotkey_style),
170
+ @tui.text_span(content: ": Color (#{@gauge_colors[@gauge_color_index][:name]}) "),
171
+ @tui.text_span(content: "b", style: @hotkey_style),
172
+ @tui.text_span(content: ": Background (#{@bg_styles[@bg_style_index][:name]})"),
173
+ ]),
174
+ # Options
175
+ @tui.text_line(spans: [
176
+ @tui.text_span(content: "u", style: @hotkey_style),
177
+ @tui.text_span(content: ": Unicode (#{use_unicode ? 'On' : 'Off'}) "),
178
+ @tui.text_span(content: "l", style: @hotkey_style),
179
+ @tui.text_span(content: ": Label (#{@label_modes[@label_mode_index][:name]})"),
180
+ ]),
181
+ ]
182
+ ),
183
+ ]
184
+ )
185
+ frame.render_widget(controls, controls_area)
186
+ end
187
+ end
188
+
189
+ private def handle_input
190
+ case @tui.poll_event
191
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
192
+ :quit
193
+ in type: :key, code: "right"
194
+ @ratio_index = (@ratio_index + 1) % @ratios.length
195
+ in type: :key, code: "left"
196
+ @ratio_index = (@ratio_index - 1) % @ratios.length
197
+ in type: :key, code: "g"
198
+ @gauge_color_index = (@gauge_color_index + 1) % @gauge_colors.length
199
+ in type: :key, code: "b"
200
+ @bg_style_index = (@bg_style_index + 1) % @bg_styles.length
201
+ in type: :key, code: "u"
202
+ @use_unicode_index = (@use_unicode_index + 1) % @use_unicode_options.length
203
+ in type: :key, code: "l"
204
+ @label_mode_index = (@label_mode_index + 1) % @label_modes.length
205
+ else
206
+ # Ignore other events
207
+ nil
208
+ end
209
+ end
210
+ end
211
+
212
+ WidgetGaugeDemo.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,246 @@
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
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
+ require "ratatui_ruby"
8
+
9
+ # Demonstrates dynamic geometry management with interactive cycling.
10
+ #
11
+ # Terminal screens vary in size. Hardcoded positions break when the window resizes. You need a way to organize space dynamically.
12
+ #
13
+ # This demo showcases the <tt>Layout.split</tt> mechanism. It provides an interactive playground where you can toggle directions, all seven flex modes, and various constraint types (Fill, Length, Percentage, Min, Ratio) in real-time.
14
+ #
15
+ # Use it to understand how to build responsive, fluid terminal interfaces that adapt to any window dimensions.
16
+ #
17
+ # === Example
18
+ #
19
+ # Run the demo from the terminal:
20
+ #
21
+ # ruby examples/widget_layout_split/app.rb
22
+ #
23
+ # rdoc-image:/doc/images/widget_layout_split.png
24
+ class WidgetLayoutSplit
25
+ DIRECTIONS = [
26
+ { name: "Vertical", value: :vertical },
27
+ { name: "Horizontal", value: :horizontal },
28
+ ].freeze
29
+
30
+ FLEX_MODES = [
31
+ { name: "Legacy", value: :legacy },
32
+ { name: "Start", value: :start },
33
+ { name: "Center", value: :center },
34
+ { name: "End", value: :end },
35
+ { name: "Space Between", value: :space_between },
36
+ { name: "Space Around", value: :space_around },
37
+ { name: "Space Evenly", value: :space_evenly },
38
+ ].freeze
39
+
40
+ BLOCK_COLORS = %i[red green blue].freeze
41
+
42
+ def initialize
43
+ @direction_index = 0
44
+ @flex_index = 0
45
+ @constraint_index = 0
46
+ @hotkey_style = nil
47
+ end
48
+
49
+ def run
50
+ RatatuiRuby.run do |tui|
51
+ @tui = tui
52
+ @hotkey_style = tui.style(modifiers: [:bold, :underlined])
53
+ @constraint_demos = build_constraint_demos
54
+
55
+ loop do
56
+ render
57
+ break if handle_input == :quit
58
+ end
59
+ end
60
+ end
61
+
62
+ private def build_constraint_demos
63
+ [
64
+ {
65
+ name: "Fill (1:2:1)",
66
+ constraints: -> (dir) {
67
+ [
68
+ @tui.constraint_fill(1),
69
+ @tui.constraint_fill(2),
70
+ @tui.constraint_fill(1),
71
+ ]
72
+ },
73
+ },
74
+ {
75
+ name: "Length (10/15/10)",
76
+ constraints: -> (dir) {
77
+ [
78
+ @tui.constraint_length(10),
79
+ @tui.constraint_length(15),
80
+ @tui.constraint_length(10),
81
+ ]
82
+ },
83
+ },
84
+ {
85
+ name: "Percentage (25/50/25)",
86
+ constraints: -> (dir) {
87
+ [
88
+ @tui.constraint_percentage(25),
89
+ @tui.constraint_percentage(50),
90
+ @tui.constraint_percentage(25),
91
+ ]
92
+ },
93
+ },
94
+ {
95
+ name: "Min (5/10/5)",
96
+ constraints: -> (dir) {
97
+ [
98
+ @tui.constraint_min(5),
99
+ @tui.constraint_min(10),
100
+ @tui.constraint_min(5),
101
+ ]
102
+ },
103
+ },
104
+ {
105
+ name: "Ratio (1:4, 2:4, 1:4)",
106
+ constraints: -> (dir) {
107
+ [
108
+ @tui.constraint_ratio(1, 4),
109
+ @tui.constraint_ratio(2, 4),
110
+ @tui.constraint_ratio(1, 4),
111
+ ]
112
+ },
113
+ },
114
+ {
115
+ name: -> (dir) { (dir == :vertical) ? "Mixed (Len 3, Fill, Pct 25)" : "Mixed (Len 20, Fill, Pct 25)" },
116
+ constraints: -> (dir) {
117
+ fixed = (dir == :vertical) ? 3 : 20
118
+ [
119
+ @tui.constraint_length(fixed),
120
+ @tui.constraint_fill(1),
121
+ @tui.constraint_percentage(25),
122
+ ]
123
+ },
124
+ },
125
+ ]
126
+ end
127
+
128
+ private def current_direction
129
+ DIRECTIONS[@direction_index][:value]
130
+ end
131
+
132
+ private def current_flex
133
+ FLEX_MODES[@flex_index][:value]
134
+ end
135
+
136
+ private def current_constraints
137
+ demo = @constraint_demos[@constraint_index]
138
+ demo[:constraints].call(current_direction)
139
+ end
140
+
141
+ private def current_constraint_name
142
+ demo = @constraint_demos[@constraint_index]
143
+ name = demo[:name]
144
+ name.respond_to?(:call) ? name.call(current_direction) : name
145
+ end
146
+
147
+ private def render
148
+ @tui.draw do |frame|
149
+ # Split into main content and control panel
150
+ main_area, controls_area = @tui.layout_split(
151
+ frame.area,
152
+ direction: :vertical,
153
+ constraints: [
154
+ @tui.constraint_fill(1),
155
+ @tui.constraint_length(6),
156
+ ]
157
+ )
158
+
159
+ render_demo_area(frame, main_area)
160
+ render_controls(frame, controls_area)
161
+ end
162
+ end
163
+
164
+ private def render_demo_area(frame, area)
165
+ # Split demo area into title and content
166
+ title_area, content_area = @tui.layout_split(
167
+ area,
168
+ direction: :vertical,
169
+ constraints: [
170
+ @tui.constraint_length(1),
171
+ @tui.constraint_fill(1),
172
+ ]
173
+ )
174
+
175
+ # Render title
176
+ title = @tui.paragraph(
177
+ text: "Layout.split Demo",
178
+ style: @tui.style(modifiers: [:bold])
179
+ )
180
+ frame.render_widget(title, title_area)
181
+
182
+ # Apply current layout settings to 3 colored blocks
183
+ block_areas = @tui.layout_split(
184
+ content_area,
185
+ direction: current_direction,
186
+ flex: current_flex,
187
+ constraints: current_constraints
188
+ )
189
+
190
+ block_areas.each_with_index do |block_area, i|
191
+ block = @tui.block(
192
+ title: "Block #{i + 1}",
193
+ borders: [:all],
194
+ border_color: BLOCK_COLORS[i % BLOCK_COLORS.length]
195
+ )
196
+ frame.render_widget(block, block_area)
197
+ end
198
+ end
199
+
200
+ private def render_controls(frame, area)
201
+ controls = @tui.block(
202
+ title: "Controls",
203
+ borders: [:all],
204
+ children: [
205
+ @tui.paragraph(
206
+ text: [
207
+ # Row 1: Direction and Flex
208
+ @tui.text_line(spans: [
209
+ @tui.text_span(content: "d", style: @hotkey_style),
210
+ @tui.text_span(content: ": Direction (#{DIRECTIONS[@direction_index][:name]}) "),
211
+ @tui.text_span(content: "f", style: @hotkey_style),
212
+ @tui.text_span(content: ": Flex (#{FLEX_MODES[@flex_index][:name]})"),
213
+ ]),
214
+ @tui.text_line(spans: [
215
+ @tui.text_span(content: "c", style: @hotkey_style),
216
+ @tui.text_span(content: ": Constraints (#{current_constraint_name})"),
217
+ ]),
218
+ # Row 3: Quit
219
+ @tui.text_line(spans: [
220
+ @tui.text_span(content: "q", style: @hotkey_style),
221
+ @tui.text_span(content: ": Quit"),
222
+ ]),
223
+ ]
224
+ ),
225
+ ]
226
+ )
227
+ frame.render_widget(controls, area)
228
+ end
229
+
230
+ private def handle_input
231
+ case @tui.poll_event
232
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
233
+ :quit
234
+ in type: :key, code: "d"
235
+ @direction_index = (@direction_index + 1) % DIRECTIONS.length
236
+ in type: :key, code: "f"
237
+ @flex_index = (@flex_index + 1) % FLEX_MODES.length
238
+ in type: :key, code: "c"
239
+ @constraint_index = (@constraint_index + 1) % @constraint_demos.length
240
+ else
241
+ nil
242
+ end
243
+ end
244
+ end
245
+
246
+ WidgetLayoutSplit.new.run if __FILE__ == $PROGRAM_NAME