ratatui_ruby 0.4.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (441) 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 +98 -176
  7. data/CHANGELOG.md +80 -6
  8. data/README.md +19 -7
  9. data/REUSE.toml +15 -0
  10. data/doc/application_architecture.md +179 -45
  11. data/doc/application_testing.md +80 -32
  12. data/doc/contributors/design/ruby_frontend.md +48 -8
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +191 -48
  15. data/doc/contributors/documentation_style.md +7 -0
  16. data/doc/contributors/examples_audit/p1_high.md +21 -0
  17. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  18. data/doc/contributors/examples_audit.md +41 -0
  19. data/doc/contributors/index.md +2 -0
  20. data/doc/event_handling.md +21 -7
  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_box_demo.png +0 -0
  32. data/doc/images/widget_calendar_demo.png +0 -0
  33. data/doc/images/widget_canvas_demo.png +0 -0
  34. data/doc/images/widget_cell_demo.png +0 -0
  35. data/doc/images/widget_center_demo.png +0 -0
  36. data/doc/images/widget_chart_demo.png +0 -0
  37. data/doc/images/widget_gauge_demo.png +0 -0
  38. data/doc/images/widget_layout_split.png +0 -0
  39. data/doc/images/widget_line_gauge_demo.png +0 -0
  40. data/doc/images/widget_list_demo.png +0 -0
  41. data/doc/images/widget_overlay_demo.png +0 -0
  42. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  43. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  44. data/doc/images/widget_render.png +0 -0
  45. data/doc/images/widget_rich_text.png +0 -0
  46. data/doc/images/widget_scroll_text.png +0 -0
  47. data/doc/images/widget_scrollbar_demo.png +0 -0
  48. data/doc/images/widget_sparkline_demo.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/images/widget_table_demo.png +0 -0
  51. data/doc/images/widget_table_flex.png +0 -0
  52. data/doc/images/widget_tabs_demo.png +0 -0
  53. data/doc/images/widget_text_width.png +0 -0
  54. data/doc/interactive_design.md +25 -30
  55. data/doc/quickstart.md +150 -130
  56. data/doc/terminal_limitations.md +92 -0
  57. data/examples/app_all_events/README.md +99 -0
  58. data/examples/app_all_events/app.rb +96 -0
  59. data/examples/app_all_events/model/app_model.rb +157 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +92 -0
  62. data/examples/app_all_events/model/msg.rb +37 -0
  63. data/examples/app_all_events/model/timestamp.rb +54 -0
  64. data/examples/app_all_events/update.rb +73 -0
  65. data/examples/app_all_events/view/app_view.rb +78 -0
  66. data/examples/app_all_events/view/controls_view.rb +52 -0
  67. data/examples/app_all_events/view/counts_view.rb +59 -0
  68. data/examples/app_all_events/view/live_view.rb +70 -0
  69. data/examples/app_all_events/view/log_view.rb +55 -0
  70. data/examples/app_all_events/view.rb +7 -0
  71. data/examples/app_color_picker/README.md +134 -0
  72. data/examples/app_color_picker/app.rb +74 -0
  73. data/examples/app_color_picker/clipboard.rb +84 -0
  74. data/examples/app_color_picker/color.rb +191 -0
  75. data/examples/app_color_picker/controls.rb +90 -0
  76. data/examples/app_color_picker/copy_dialog.rb +166 -0
  77. data/examples/app_color_picker/export_pane.rb +126 -0
  78. data/examples/app_color_picker/harmony.rb +56 -0
  79. data/examples/app_color_picker/input.rb +174 -0
  80. data/examples/app_color_picker/main_container.rb +178 -0
  81. data/examples/app_color_picker/palette.rb +109 -0
  82. data/examples/app_login_form/README.md +47 -0
  83. data/examples/{login_form → app_login_form}/app.rb +38 -42
  84. data/examples/app_stateful_interaction/README.md +31 -0
  85. data/examples/app_stateful_interaction/app.rb +272 -0
  86. data/examples/timeout_demo.rb +43 -0
  87. data/examples/verify_quickstart_dsl/README.md +48 -0
  88. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
  89. data/examples/verify_quickstart_layout/README.md +71 -0
  90. data/examples/verify_quickstart_layout/app.rb +71 -0
  91. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  92. data/examples/verify_quickstart_lifecycle/app.rb +54 -0
  93. data/examples/verify_readme_usage/README.md +43 -0
  94. data/examples/verify_readme_usage/app.rb +40 -0
  95. data/examples/widget_barchart_demo/README.md +49 -0
  96. data/examples/widget_barchart_demo/app.rb +238 -0
  97. data/examples/widget_block_demo/README.md +34 -0
  98. data/examples/widget_block_demo/app.rb +256 -0
  99. data/examples/widget_box_demo/README.md +45 -0
  100. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  101. data/examples/widget_calendar_demo/README.md +39 -0
  102. data/examples/widget_calendar_demo/app.rb +109 -0
  103. data/examples/widget_canvas_demo/README.md +27 -0
  104. data/examples/widget_canvas_demo/app.rb +123 -0
  105. data/examples/widget_cell_demo/README.md +36 -0
  106. data/examples/widget_cell_demo/app.rb +111 -0
  107. data/examples/widget_center_demo/README.md +29 -0
  108. data/examples/widget_center_demo/app.rb +116 -0
  109. data/examples/widget_chart_demo/README.md +41 -0
  110. data/examples/widget_chart_demo/app.rb +218 -0
  111. data/examples/widget_gauge_demo/README.md +41 -0
  112. data/examples/widget_gauge_demo/app.rb +212 -0
  113. data/examples/widget_layout_split/README.md +44 -0
  114. data/examples/widget_layout_split/app.rb +246 -0
  115. data/examples/widget_line_gauge_demo/README.md +41 -0
  116. data/examples/widget_line_gauge_demo/app.rb +217 -0
  117. data/examples/widget_list_demo/README.md +49 -0
  118. data/examples/widget_list_demo/app.rb +366 -0
  119. data/examples/widget_map_demo/README.md +39 -0
  120. data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
  121. data/examples/widget_overlay_demo/app.rb +248 -0
  122. data/examples/widget_popup_demo/README.md +36 -0
  123. data/examples/widget_popup_demo/app.rb +104 -0
  124. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  125. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  126. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  127. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  128. data/examples/widget_rect/README.md +38 -0
  129. data/examples/widget_rect/app.rb +205 -0
  130. data/examples/widget_render/README.md +37 -0
  131. data/examples/widget_render/app.rb +184 -0
  132. data/examples/widget_rich_text/README.md +35 -0
  133. data/examples/widget_rich_text/app.rb +166 -0
  134. data/examples/widget_scroll_text/README.md +37 -0
  135. data/examples/widget_scroll_text/app.rb +107 -0
  136. data/examples/widget_scrollbar_demo/README.md +37 -0
  137. data/examples/widget_scrollbar_demo/app.rb +153 -0
  138. data/examples/widget_sparkline_demo/README.md +42 -0
  139. data/examples/widget_sparkline_demo/app.rb +275 -0
  140. data/examples/widget_style_colors/README.md +34 -0
  141. data/examples/widget_style_colors/app.rb +19 -21
  142. data/examples/widget_table_demo/README.md +48 -0
  143. data/examples/widget_table_demo/app.rb +239 -0
  144. data/examples/widget_tabs_demo/README.md +41 -0
  145. data/examples/widget_tabs_demo/app.rb +181 -0
  146. data/examples/widget_text_width/README.md +35 -0
  147. data/examples/widget_text_width/app.rb +106 -0
  148. data/ext/ratatui_ruby/Cargo.lock +11 -4
  149. data/ext/ratatui_ruby/Cargo.toml +2 -1
  150. data/ext/ratatui_ruby/src/events.rs +359 -62
  151. data/ext/ratatui_ruby/src/frame.rs +227 -0
  152. data/ext/ratatui_ruby/src/lib.rs +110 -27
  153. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  154. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  155. data/ext/ratatui_ruby/src/style.rs +138 -57
  156. data/ext/ratatui_ruby/src/terminal.rs +42 -22
  157. data/ext/ratatui_ruby/src/text.rs +14 -7
  158. data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
  159. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  160. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  161. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  165. data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
  166. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  167. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  168. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  169. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  170. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  171. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  172. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
  173. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  174. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  175. data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
  176. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  177. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  178. data/lib/ratatui_ruby/cell.rb +7 -7
  179. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  180. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  181. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  182. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  183. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  184. data/lib/ratatui_ruby/event/key.rb +112 -52
  185. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  186. data/lib/ratatui_ruby/event/none.rb +43 -0
  187. data/lib/ratatui_ruby/event/paste.rb +1 -1
  188. data/lib/ratatui_ruby/event.rb +56 -4
  189. data/lib/ratatui_ruby/frame.rb +183 -0
  190. data/lib/ratatui_ruby/list_state.rb +88 -0
  191. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
  192. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  193. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  194. data/lib/ratatui_ruby/schema/block.rb +163 -168
  195. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  196. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  197. data/lib/ratatui_ruby/schema/center.rb +46 -46
  198. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  199. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  200. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  201. data/lib/ratatui_ruby/schema/cursor.rb +30 -25
  202. data/lib/ratatui_ruby/schema/gauge.rb +54 -52
  203. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  204. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  205. data/lib/ratatui_ruby/schema/list.rb +103 -80
  206. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  207. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  208. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  209. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  210. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  211. data/lib/ratatui_ruby/schema/rect.rb +99 -56
  212. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  213. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  214. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  215. data/lib/ratatui_ruby/schema/style.rb +66 -46
  216. data/lib/ratatui_ruby/schema/table.rb +126 -115
  217. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  218. data/lib/ratatui_ruby/schema/text.rb +69 -1
  219. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  220. data/lib/ratatui_ruby/session/autodoc.rb +482 -0
  221. data/lib/ratatui_ruby/session.rb +55 -23
  222. data/lib/ratatui_ruby/table_state.rb +90 -0
  223. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  224. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  225. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  226. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  227. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  228. data/lib/ratatui_ruby/test_helper.rb +66 -193
  229. data/lib/ratatui_ruby/version.rb +1 -1
  230. data/lib/ratatui_ruby.rb +100 -51
  231. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  232. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  233. data/sig/examples/app_all_events/model/events.rbs +15 -0
  234. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  235. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  236. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  237. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  238. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  239. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  240. data/sig/examples/app_all_events/view.rbs +8 -0
  241. data/sig/examples/app_all_events/view_state.rbs +15 -0
  242. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  243. data/sig/examples/app_login_form/app.rbs +11 -0
  244. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  245. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  246. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  247. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  248. data/sig/examples/widget_block_demo/app.rbs +32 -0
  249. data/sig/examples/widget_box_demo/app.rbs +11 -0
  250. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  251. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  252. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  253. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  254. data/sig/examples/widget_layout_split/app.rbs +10 -0
  255. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  256. data/sig/examples/widget_list_demo/app.rbs +12 -0
  257. data/sig/examples/widget_map_demo/app.rbs +11 -0
  258. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  259. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  260. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  261. data/sig/examples/widget_rect/app.rbs +12 -0
  262. data/sig/examples/widget_render/app.rbs +10 -0
  263. data/sig/examples/widget_rich_text/app.rbs +11 -0
  264. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  265. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  266. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  267. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  268. data/sig/examples/widget_table_demo/app.rbs +11 -0
  269. data/sig/examples/widget_text_width/app.rbs +10 -0
  270. data/sig/ratatui_ruby/event.rbs +11 -1
  271. data/sig/ratatui_ruby/frame.rbs +11 -0
  272. data/sig/ratatui_ruby/list_state.rbs +13 -0
  273. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
  274. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  275. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  276. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  277. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  278. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  279. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  280. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  281. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  282. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  283. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  284. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  285. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  286. data/sig/ratatui_ruby/session.rbs +107 -0
  287. data/sig/ratatui_ruby/table_state.rbs +15 -0
  288. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  289. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  290. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  291. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  292. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  293. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  294. data/tasks/autodoc/examples.rb +79 -0
  295. data/tasks/autodoc/inventory.rb +63 -0
  296. data/tasks/autodoc/member.rb +56 -0
  297. data/tasks/autodoc/name.rb +19 -0
  298. data/tasks/autodoc/notice.rb +26 -0
  299. data/tasks/autodoc/rbs.rb +38 -0
  300. data/tasks/autodoc/rdoc.rb +45 -0
  301. data/tasks/autodoc.rake +53 -0
  302. data/tasks/bump/changelog.rb +3 -3
  303. data/tasks/bump/history.rb +2 -2
  304. data/tasks/bump/links.rb +67 -0
  305. data/tasks/doc.rake +600 -6
  306. data/tasks/example_viewer.html.erb +172 -0
  307. data/tasks/lint.rake +8 -4
  308. data/tasks/resources/index.html.erb +6 -0
  309. data/tasks/sourcehut.rake +70 -30
  310. data/tasks/terminal_preview/app_screenshot.rb +14 -6
  311. data/tasks/terminal_preview/crash_report.rb +7 -9
  312. data/tasks/terminal_preview/launcher_script.rb +4 -6
  313. data/tasks/terminal_preview/preview_collection.rb +4 -6
  314. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  315. data/tasks/terminal_preview/saved_screenshot.rb +10 -11
  316. data/tasks/terminal_preview/terminal_window.rb +7 -9
  317. data/tasks/test.rake +1 -1
  318. data/tasks/website/index_page.rb +3 -3
  319. data/tasks/website/version.rb +10 -10
  320. data/tasks/website/version_menu.rb +10 -12
  321. data/tasks/website/versioned_documentation.rb +49 -17
  322. data/tasks/website/website.rb +6 -8
  323. data/tasks/website.rake +4 -4
  324. metadata +232 -127
  325. data/LICENSES/BSD-2-Clause.txt +0 -9
  326. data/doc/contributors/better_dx.md +0 -543
  327. data/doc/contributors/example_analysis.md +0 -82
  328. data/doc/images/all_events.png +0 -0
  329. data/doc/images/block_padding.png +0 -0
  330. data/doc/images/block_titles.png +0 -0
  331. data/doc/images/box_demo.png +0 -0
  332. data/doc/images/calendar_demo.png +0 -0
  333. data/doc/images/cell_demo.png +0 -0
  334. data/doc/images/chart_demo.png +0 -0
  335. data/doc/images/flex_layout.png +0 -0
  336. data/doc/images/gauge_demo.png +0 -0
  337. data/doc/images/line_gauge_demo.png +0 -0
  338. data/doc/images/list_demo.png +0 -0
  339. data/doc/images/list_styles.png +0 -0
  340. data/doc/images/login_form.png +0 -0
  341. data/doc/images/quickstart_dsl.png +0 -0
  342. data/doc/images/quickstart_lifecycle.png +0 -0
  343. data/doc/images/readme_usage.png +0 -0
  344. data/doc/images/rich_text.png +0 -0
  345. data/doc/images/scroll_text.png +0 -0
  346. data/doc/images/scrollbar_demo.png +0 -0
  347. data/doc/images/sparkline_demo.png +0 -0
  348. data/doc/images/table_flex.png +0 -0
  349. data/doc/images/table_select.png +0 -0
  350. data/examples/all_events/app.rb +0 -169
  351. data/examples/all_events/app.rbs +0 -7
  352. data/examples/all_events/test_app.rb +0 -139
  353. data/examples/analytics/app.rb +0 -258
  354. data/examples/analytics/app.rbs +0 -7
  355. data/examples/analytics/test_app.rb +0 -132
  356. data/examples/block_padding/app.rb +0 -63
  357. data/examples/block_padding/app.rbs +0 -7
  358. data/examples/block_padding/test_app.rb +0 -31
  359. data/examples/block_titles/app.rb +0 -61
  360. data/examples/block_titles/app.rbs +0 -7
  361. data/examples/block_titles/test_app.rb +0 -34
  362. data/examples/box_demo/app.rbs +0 -7
  363. data/examples/box_demo/test_app.rb +0 -88
  364. data/examples/calendar_demo/app.rb +0 -101
  365. data/examples/calendar_demo/app.rbs +0 -7
  366. data/examples/calendar_demo/test_app.rb +0 -108
  367. data/examples/cell_demo/app.rb +0 -108
  368. data/examples/cell_demo/app.rbs +0 -7
  369. data/examples/cell_demo/test_app.rb +0 -36
  370. data/examples/chart_demo/app.rb +0 -203
  371. data/examples/chart_demo/app.rbs +0 -7
  372. data/examples/chart_demo/test_app.rb +0 -102
  373. data/examples/custom_widget/app.rb +0 -51
  374. data/examples/custom_widget/app.rbs +0 -7
  375. data/examples/custom_widget/test_app.rb +0 -30
  376. data/examples/flex_layout/app.rb +0 -156
  377. data/examples/flex_layout/app.rbs +0 -7
  378. data/examples/flex_layout/test_app.rb +0 -65
  379. data/examples/gauge_demo/app.rb +0 -182
  380. data/examples/gauge_demo/test_app.rb +0 -120
  381. data/examples/hit_test/app.rb +0 -175
  382. data/examples/hit_test/app.rbs +0 -7
  383. data/examples/hit_test/test_app.rb +0 -102
  384. data/examples/line_gauge_demo/app.rb +0 -190
  385. data/examples/line_gauge_demo/app.rbs +0 -7
  386. data/examples/line_gauge_demo/test_app.rb +0 -129
  387. data/examples/list_demo/app.rb +0 -253
  388. data/examples/list_demo/test_app.rb +0 -237
  389. data/examples/list_styles/app.rb +0 -140
  390. data/examples/list_styles/app.rbs +0 -7
  391. data/examples/list_styles/test_app.rb +0 -157
  392. data/examples/login_form/app.rbs +0 -7
  393. data/examples/login_form/test_app.rb +0 -51
  394. data/examples/map_demo/app.rbs +0 -7
  395. data/examples/map_demo/test_app.rb +0 -149
  396. data/examples/mouse_events/app.rb +0 -97
  397. data/examples/mouse_events/app.rbs +0 -7
  398. data/examples/mouse_events/test_app.rb +0 -53
  399. data/examples/popup_demo/app.rb +0 -103
  400. data/examples/popup_demo/app.rbs +0 -7
  401. data/examples/popup_demo/test_app.rb +0 -54
  402. data/examples/quickstart_dsl/app.rbs +0 -7
  403. data/examples/quickstart_dsl/test_app.rb +0 -29
  404. data/examples/quickstart_lifecycle/app.rb +0 -39
  405. data/examples/quickstart_lifecycle/app.rbs +0 -7
  406. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  407. data/examples/ratatui_logo_demo/app.rb +0 -79
  408. data/examples/ratatui_logo_demo/app.rbs +0 -7
  409. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  410. data/examples/ratatui_mascot_demo/app.rb +0 -84
  411. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  412. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  413. data/examples/readme_usage/app.rb +0 -29
  414. data/examples/readme_usage/app.rbs +0 -7
  415. data/examples/readme_usage/test_app.rb +0 -29
  416. data/examples/rich_text/app.rb +0 -141
  417. data/examples/rich_text/app.rbs +0 -7
  418. data/examples/rich_text/test_app.rb +0 -166
  419. data/examples/scroll_text/app.rb +0 -103
  420. data/examples/scroll_text/app.rbs +0 -7
  421. data/examples/scroll_text/test_app.rb +0 -110
  422. data/examples/scrollbar_demo/app.rb +0 -143
  423. data/examples/scrollbar_demo/app.rbs +0 -7
  424. data/examples/scrollbar_demo/test_app.rb +0 -77
  425. data/examples/sparkline_demo/app.rb +0 -240
  426. data/examples/sparkline_demo/test_app.rb +0 -107
  427. data/examples/table_flex/app.rb +0 -65
  428. data/examples/table_flex/app.rbs +0 -7
  429. data/examples/table_flex/test_app.rb +0 -36
  430. data/examples/table_select/app.rb +0 -198
  431. data/examples/table_select/app.rbs +0 -7
  432. data/examples/table_select/test_app.rb +0 -180
  433. data/examples/widget_style_colors/test_app.rb +0 -48
  434. data/tasks/bump/comparison_links.rb +0 -41
  435. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  436. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  437. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  438. /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
  439. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  440. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  441. /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
@@ -0,0 +1,78 @@
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 "../view"
7
+ require_relative "counts_view"
8
+ require_relative "live_view"
9
+ require_relative "log_view"
10
+ require_relative "controls_view"
11
+
12
+ # Orchestrates the complete UI layout and sub-view composition.
13
+ #
14
+ # Complex applications need a structured way to divide the screen and delegate rendering.
15
+ # Placing all layout logic in one monolithic method makes the code difficult to maintain.
16
+ #
17
+ # This class defines the screen layout using a series of split constraints and delegates to sub-views.
18
+ #
19
+ # Use it as the root view for the All Events example application.
20
+ #
21
+ # === Examples
22
+ #
23
+ # app_view = View::App.new
24
+ # app_view.call(model, tui, frame, area)
25
+ class View::App
26
+ # Creates a new View::App and initializes sub-views.
27
+ def initialize
28
+ @counts_view = View::Counts.new
29
+ @live_view = View::Live.new
30
+ @log_view = View::Log.new
31
+ @controls_view = View::Controls.new
32
+ end
33
+
34
+ # Renders the entire application UI to the given area.
35
+ #
36
+ # [model] AppModel containing all application data.
37
+ # [tui] RatatuiRuby instance.
38
+ # [frame] RatatuiRuby::Frame being rendered.
39
+ # [area] RatatuiRuby::Rect defining the total available space.
40
+ #
41
+ # === Example
42
+ #
43
+ # app_view.call(model, tui, frame, area)
44
+ def call(model, tui, frame, area)
45
+ main_area, control_area = tui.layout_split(
46
+ area,
47
+ direction: :vertical,
48
+ constraints: [
49
+ tui.constraint_fill(1),
50
+ tui.constraint_length(3),
51
+ ]
52
+ )
53
+
54
+ counts_area, _margin_area, right_area = tui.layout_split(
55
+ main_area,
56
+ direction: :horizontal,
57
+ constraints: [
58
+ tui.constraint_length(20),
59
+ tui.constraint_length(1),
60
+ tui.constraint_fill(1),
61
+ ]
62
+ )
63
+
64
+ live_area, log_area = tui.layout_split(
65
+ right_area,
66
+ direction: :vertical,
67
+ constraints: [
68
+ tui.constraint_length(9),
69
+ tui.constraint_fill(1),
70
+ ]
71
+ )
72
+
73
+ @counts_view.call(model, tui, frame, counts_area)
74
+ @live_view.call(model, tui, frame, live_area)
75
+ @log_view.call(model, tui, frame, log_area)
76
+ @controls_view.call(model, tui, frame, control_area)
77
+ end
78
+ end
@@ -0,0 +1,52 @@
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 "../view"
7
+
8
+ # Renders the keyboard controls and shortcuts panel.
9
+ #
10
+ # Users need to know how to interact with the application and exit.
11
+ # Hardcoding control descriptions into the main layout makes the code hard to read.
12
+ #
13
+ # This component renders a formatted paragraph listing available global shortcuts.
14
+ #
15
+ # Use it to display help information in a sidebar or dedicated panel.
16
+ #
17
+ # === Examples
18
+ #
19
+ # controls = View::Controls.new
20
+ # controls.call(model, tui, frame, area)
21
+ class View::Controls
22
+ # Renders the controls widget to the given area.
23
+ #
24
+ # [model] AppModel (unused, included for consistent interface).
25
+ # [tui] RatatuiRuby instance.
26
+ # [frame] RatatuiRuby::Frame being rendered.
27
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
28
+ #
29
+ # === Example
30
+ #
31
+ # controls.call(model, tui, frame, area)
32
+ def call(_model, tui, frame, area)
33
+ hotkey_style = tui.style(modifiers: [:bold, :underlined])
34
+
35
+ widget = tui.paragraph(
36
+ text: [
37
+ tui.text_line(spans: [
38
+ tui.text_span(content: "q", style: hotkey_style),
39
+ tui.text_span(content: ": Quit "),
40
+ tui.text_span(content: "Ctrl+C", style: hotkey_style),
41
+ tui.text_span(content: ": Quit"),
42
+ ]),
43
+ ],
44
+ block: tui.block(
45
+ title: "Controls",
46
+ borders: [:all],
47
+ border_style: tui.style(fg: :white)
48
+ )
49
+ )
50
+ frame.render_widget(widget, area)
51
+ end
52
+ end
@@ -0,0 +1,59 @@
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 "../view"
7
+
8
+ # Renders the event statistics dashboard.
9
+ #
10
+ # Developers auditing input need to see real-time counts of various event types.
11
+ #
12
+ # This component displays a list of event types with their total counts.
13
+ #
14
+ # Use it to build an interactive dashboard of application activity.
15
+ class View::Counts
16
+ # Renders the event counts widget to the given area.
17
+ #
18
+ # [model] AppModel containing event data.
19
+ # [tui] RatatuiRuby instance.
20
+ # [frame] RatatuiRuby::Frame being rendered.
21
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
22
+ def call(model, tui, frame, area)
23
+ dimmed_style = tui.style(fg: :dark_gray)
24
+ lit_style = tui.style(fg: :green, modifiers: [:bold])
25
+ border_color = model.focused ? :green : :gray
26
+
27
+ count_lines = []
28
+
29
+ AppAllEvents::EVENT_TYPES.each do |type|
30
+ count = model.count(type)
31
+ label = type.to_s.capitalize
32
+ style = model.lit?(type) ? lit_style : nil
33
+
34
+ count_lines << tui.text_line(spans: [
35
+ tui.text_span(content: "#{label}: ", style:),
36
+ tui.text_span(content: count.to_s, style: style || tui.style(fg: :yellow)),
37
+ ])
38
+
39
+ model.sub_counts(type).each do |sub_type, sub_count|
40
+ sub_label = sub_type.to_s.capitalize
41
+ count_lines << tui.text_line(spans: [
42
+ tui.text_span(content: " #{sub_label}: ", style: dimmed_style),
43
+ tui.text_span(content: sub_count.to_s, style: dimmed_style),
44
+ ])
45
+ end
46
+ end
47
+
48
+ widget = tui.paragraph(
49
+ text: count_lines,
50
+ scroll: [0, 0],
51
+ block: tui.block(
52
+ title: "Event Counts",
53
+ borders: [:all],
54
+ border_style: tui.style(fg: border_color)
55
+ )
56
+ )
57
+ frame.render_widget(widget, area)
58
+ end
59
+ end
@@ -0,0 +1,70 @@
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 "../view"
7
+
8
+ # Renders a real-time summary of the most recent events.
9
+ #
10
+ # Users need to see the immediate result of their actions without digging through a log.
11
+ # Identifying the specific details of the last key press or mouse move at a glance is difficult.
12
+ #
13
+ # This component displays a table showing the latest event of each type with its timestamp and description.
14
+ #
15
+ # Use it to provide instant feedback for user interactions.
16
+ #
17
+ # === Examples
18
+ #
19
+ # live_view = View::Live.new
20
+ # live_view.call(model, tui, frame, area)
21
+ class View::Live
22
+ # Renders the live event table to the given area.
23
+ #
24
+ # [model] AppModel containing event data.
25
+ # [tui] RatatuiRuby instance.
26
+ # [frame] RatatuiRuby::Frame being rendered.
27
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
28
+ #
29
+ # === Example
30
+ #
31
+ # live_view.call(model, tui, frame, area)
32
+ def call(model, tui, frame, area)
33
+ border_color = model.focused ? :green : :gray
34
+ rows = []
35
+
36
+ rows << tui.text_line(spans: [
37
+ tui.text_span(content: "Type".ljust(9), style: tui.style(fg: :gray, modifiers: [:bold])),
38
+ tui.text_span(content: "Time".ljust(10), style: tui.style(fg: :gray, modifiers: [:bold])),
39
+ tui.text_span(content: "Description", style: tui.style(fg: :gray, modifiers: [:bold])),
40
+ ])
41
+
42
+ (AppAllEvents::EVENT_TYPES - [:none]).each do |type|
43
+ event_data = model.live_event(type)
44
+
45
+ class_str = type.to_s.capitalize
46
+ time_str = event_data ? event_data[:time].strftime("%H:%M:%S") : "—"
47
+ desc_str = event_data ? event_data[:description] : "—"
48
+
49
+ is_lit = model.lit?(type)
50
+ row_style = is_lit ? tui.style(fg: :black, bg: :green) : nil
51
+
52
+ rows << tui.text_line(spans: [
53
+ tui.text_span(content: class_str.ljust(9), style: row_style || tui.style(fg: :cyan)),
54
+ tui.text_span(content: time_str.ljust(10), style: row_style || tui.style(fg: :white)),
55
+ tui.text_span(content: desc_str, style: row_style),
56
+ ])
57
+ end
58
+
59
+ widget = tui.paragraph(
60
+ text: rows,
61
+ scroll: [0, 0],
62
+ block: tui.block(
63
+ title: "Live Display",
64
+ borders: [:all],
65
+ border_style: tui.style(fg: border_color)
66
+ )
67
+ )
68
+ frame.render_widget(widget, area)
69
+ end
70
+ end
@@ -0,0 +1,55 @@
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 "../view"
7
+
8
+ # Renders a detailed, scrollable history of application events.
9
+ #
10
+ # Debugging complex event flows requires a chronological record of raw data.
11
+ # Interpreting raw event objects without formatting is difficult and slow.
12
+ #
13
+ # This component renders event history as a series of formatted, color-coded entries showing raw data.
14
+ #
15
+ # Use it to provide a detailed audit trail of all terminal interactions.
16
+ class View::Log
17
+ # Renders the event log widget to the given area.
18
+ #
19
+ # [model] AppModel containing event data.
20
+ # [tui] RatatuiRuby instance.
21
+ # [frame] RatatuiRuby::Frame being rendered.
22
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
23
+ def call(model, tui, frame, area)
24
+ dimmed_style = tui.style(fg: :dark_gray)
25
+ border_color = model.focused ? :green : :gray
26
+
27
+ visible_entries_count = (area.height - 2) / 2
28
+ display_entries = model.visible(visible_entries_count)
29
+
30
+ log_lines = []
31
+ if model.empty?
32
+ log_lines << tui.text_line(spans: [tui.text_span(content: "No events yet...", style: dimmed_style)])
33
+ else
34
+ display_entries.each do |entry|
35
+ entry_style = tui.style(fg: entry.color)
36
+ description = entry.description
37
+
38
+ log_lines << tui.text_line(spans: [tui.text_span(content: description, style: entry_style)])
39
+ log_lines << tui.text_line(spans: [tui.text_span(content: "", style: entry_style)])
40
+ end
41
+ end
42
+
43
+ widget = tui.paragraph(
44
+ text: log_lines,
45
+ scroll: [0, 0],
46
+ wrap: { trim: true },
47
+ block: tui.block(
48
+ title: "Event Log",
49
+ borders: [:all],
50
+ border_style: tui.style(fg: border_color)
51
+ )
52
+ )
53
+ frame.render_widget(widget, area)
54
+ end
55
+ end
@@ -0,0 +1,7 @@
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
+ module View
7
+ end
@@ -0,0 +1,134 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Color Picker Example
7
+
8
+ This example demonstrates how to build a **Feature-Rich Interactive Application** using `ratatui_ruby`.
9
+
10
+ It goes beyond simple widgets to show a complete, real-world architecture for handling:
11
+ - **Complex State Management** (Input validation, clipboard interaction)
12
+ - **Mouse Interaction & Hit Testing**
13
+ - **Dynamic Layouts**
14
+ - **Modal Dialogs**
15
+
16
+ ## Architecture: The "Proto-Kit" Pattern (Component-Based)
17
+
18
+ This app uses a **Strict Component-Based Architecture** where every UI element encapsulates its own **Rendering**, **State**, and **Event Handling**.
19
+
20
+ ### The Component Contract
21
+
22
+ Every component implements this duck-type interface:
23
+
24
+ ```ruby
25
+ # Renders the component into the given area
26
+ # Caches `area` for hit testing
27
+ def render(tui, frame, area)
28
+ @area = area
29
+ # ... render using frame.render_widget
30
+ end
31
+
32
+ # Processes events; returns a symbolic signal or nil
33
+ def handle_event(event) -> Symbol | nil
34
+ # Returns :consumed, :submitted, :copy_requested, etc.
35
+ end
36
+
37
+ # Optional: time-based updates
38
+ def tick
39
+ end
40
+ ```
41
+
42
+ ### 1. The MainContainer (Orchestrator)
43
+
44
+ The `MainContainer` class (`main_container.rb`) owns all child components and orchestrates the UI:
45
+
46
+ - **Layout Phase:** Calculates `Rect`s using `tui.layout_split`.
47
+ - **Delegation Phase:** Calls `child.render(tui, frame, child_area)` for each component.
48
+ - **Event Routing (Chain of Responsibility):** Delegates events front-to-back. The modal dialog gets priority when active.
49
+ - **Mediator Pattern:** Interprets symbolic signals (`:submitted`, `:copy_requested`) to coordinate cross-component effects.
50
+
51
+ ### 2. Self-Contained Components
52
+
53
+ Each UI element is a self-contained component:
54
+
55
+ - **`Input`**: Text entry with validation. Returns `:submitted` when Enter is pressed.
56
+ - **`Palette`**: Displays color harmonies. Accepts `update_color` from the container.
57
+ - **`ExportPane`**: Shows HEX/RGB/HSL formats. Returns `:copy_requested` when clicked.
58
+ - **`Controls`**: Displays keyboard shortcuts. Has a `tick` lifecycle for clipboard feedback.
59
+ - **`CopyDialog`**: Modal confirmation dialog. Returns `:consumed` when handling events.
60
+
61
+ ### 3. The App (Minimal Runner)
62
+
63
+ The `App` class (`app.rb`) is a thin runner:
64
+ - Creates the `MainContainer`.
65
+ - Runs the main loop: `tick` → `render` → `poll` → `handle_event`.
66
+ - Checks for quit events.
67
+
68
+ ## Key Features Showcased
69
+
70
+ ### 🖱️ Encapsulated Hit Testing
71
+
72
+ Components cache their render area (`@area`) during `render`. In `handle_event`, they check `@area&.contains?(x, y)` to detect clicks. The container never calculates coordinates—hit testing is fully encapsulated.
73
+
74
+ ### 🔲 Modal Dialogs via Chain of Responsibility
75
+
76
+ When `CopyDialog` is active, the `MainContainer` offers it events first. If it returns `:consumed`, event propagation stops. This creates modal behavior without explicit flags in the app.
77
+
78
+ ### 📡 Symbolic Signals (Mediator Pattern)
79
+
80
+ Components return semantic symbols instead of just `:consumed`:
81
+ - `Input` returns `:submitted` when the user presses Enter.
82
+ - `ExportPane` returns `:copy_requested` when clicked.
83
+
84
+ The `MainContainer` interprets these signals to coordinate cross-component communication:
85
+
86
+ ```ruby
87
+ result = @input.handle_event(event)
88
+ case result
89
+ when :submitted
90
+ @palette.update_color(@input.parsed_color)
91
+ return :consumed
92
+ end
93
+ ```
94
+
95
+ ### ⏱️ Lifecycle Hooks (`tick`)
96
+
97
+ Components can have time-based updates. `Controls#tick` delegates to `Clipboard#tick` to decrement the feedback timer.
98
+
99
+ ## Problem Solving: What You Can Learn
100
+
101
+ Read this example if you are trying to solve:
102
+ 1. **"How do I structure a larger app?"** → Use the Component Contract and a Container for orchestration.
103
+ 2. **"How do I handle mouse clicks?"** → Cache `@area` during render; check `contains?` in `handle_event`.
104
+ 3. **"How do I make a popup?"** → Use Chain of Responsibility: the active modal gets events first.
105
+ 4. **"How do I coordinate between components?"** → Use symbolic signals and the Mediator pattern.
106
+ 5. **"How do I validate input?"** → Encapsulate validation inside the `Input` component.
107
+
108
+ ## Usage
109
+
110
+ ```bash
111
+ ruby examples/app_color_picker/app.rb
112
+ ```
113
+
114
+ - Type a hex code (e.g., `#FF0055`) or color name (`cyan`).
115
+ - Press `Enter` to generate the palette.
116
+ - Click on the **Export Formats** box to copy the hex code.
117
+
118
+ ## Comparison: Choosing an Architecture
119
+
120
+ Complex applications require structured state habits. This Color Picker and the [App All Events](../app_all_events/README.md) example demonstrate two different approaches.
121
+
122
+ ### The Tool Approach (Color Picker - Proto-Kit)
123
+
124
+ Tools require interaction. Users click buttons and drag sliders. Components need to know where they exist on screen for hit testing. The Container orchestrates cross-component effects.
125
+
126
+ This example uses the **Proto-Kit (Component-Based)** pattern. Each component owns its own state, rendering, and event handling. The Container routes events and mediates communication.
127
+
128
+ Use this pattern for forms, editors, and mouse-driven tools.
129
+
130
+ ### The Dashboard Approach (AppAllEvents - Proto-TEA)
131
+
132
+ Dashboards display data. They rarely require complex mouse interaction. Proto-TEA (Model-View-Update) works best there. State is immutable. Logic is pure. Updates are predictable. This simplifies testing.
133
+
134
+ Use that pattern for logs, monitors, and data viewers.
@@ -0,0 +1,74 @@
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
+ $LOAD_PATH.unshift File.expand_path(__dir__)
8
+
9
+ require "ratatui_ruby"
10
+ require_relative "main_container"
11
+
12
+ # A terminal-based color picker application.
13
+ #
14
+ # Terminal users often need to select colors for themes or UI components.
15
+ # Manually typing hex codes and guessing how they will look is slow and error-prone.
16
+ #
17
+ # This application solves the problem by providing an interactive interface. It parses hex strings,
18
+ # generates palettes, and displays them visually in the terminal.
19
+ #
20
+ # === Architecture
21
+ #
22
+ # This example uses the Proto-Kit (Component-Based) pattern:
23
+ # - **Components**: Self-contained UI elements with `render`, `handle_event`, and optional `tick`
24
+ # - **Container**: Owns layout, delegates to children, routes events via Chain of Responsibility
25
+ # - **Mediator**: Container interprets symbolic signals (`:consumed`, `:submitted`) for cross-component effects
26
+ #
27
+ # === Examples
28
+ #
29
+ # AppColorPicker.new.run
30
+ #
31
+ class AppColorPicker
32
+ # Creates a new <tt>AppColorPicker</tt> instance.
33
+ def initialize
34
+ @container = nil
35
+ end
36
+
37
+ # Starts the terminal session and enters the main event loop.
38
+ #
39
+ # This method initializes the terminal, creates the MainContainer, and runs
40
+ # the event loop until the user quits.
41
+ #
42
+ # === Example
43
+ #
44
+ # app = AppColorPicker.new
45
+ # app.run
46
+ #
47
+ def run
48
+ RatatuiRuby.run do |tui|
49
+ @container = MainContainer.new(tui)
50
+
51
+ loop do
52
+ @container.tick
53
+ tui.draw { |frame| @container.render(tui, frame, frame.area) }
54
+
55
+ event = tui.poll_event
56
+ break if quit_event?(event)
57
+
58
+ @container.handle_event(event)
59
+ end
60
+ end
61
+ end
62
+
63
+ private def quit_event?(event)
64
+ case event
65
+ in { type: :key, code: "q" } | { type: :key, code: "esc" } |
66
+ { type: :key, code: "c", modifiers: [/ctrl/] }
67
+ true
68
+ else
69
+ false
70
+ end
71
+ end
72
+ end
73
+
74
+ AppColorPicker.new.run if __FILE__ == $PROGRAM_NAME
@@ -0,0 +1,84 @@
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
+ # Manages system clipboard interaction with transient feedback.
7
+ #
8
+ # Apps need to copy data to the clipboard. Users need feedback: "Did it work?"
9
+ # Manual clipboard handling and feedback timers scattered through app logic is
10
+ # messy.
11
+ #
12
+ # This object handles clipboard writes to all platforms (pbcopy, xclip, xsel).
13
+ # It manages a feedback message and countdown timer.
14
+ #
15
+ # Use it to provide copy-to-clipboard functionality with user feedback.
16
+ #
17
+ # === Example
18
+ #
19
+ # clipboard = Clipboard.new
20
+ # clipboard.copy("#FF0000")
21
+ # puts clipboard.message # => "Copied!"
22
+ #
23
+ # # In render loop:
24
+ # clipboard.tick # Decrement timer
25
+ # puts clipboard.message # => "" (after 60 frames)
26
+ class Clipboard
27
+ def initialize
28
+ @message = ""
29
+ @timer = 0
30
+ end
31
+
32
+ # Writes text to the system clipboard.
33
+ #
34
+ # Tries pbcopy (macOS), xclip (Linux), then xsel (Linux fallback). Sets the
35
+ # feedback message to <tt>"Copied!"</tt> and starts a 60-frame timer.
36
+ #
37
+ # [text] String to copy
38
+ #
39
+ # === Example
40
+ #
41
+ # clipboard = Clipboard.new
42
+ # clipboard.copy("#FF0000")
43
+ # clipboard.message # => "Copied!"
44
+ def copy(text)
45
+ if `which pbcopy 2>/dev/null`.strip.length > 0
46
+ IO.popen("pbcopy", "w") { |io| io.write(text) }
47
+ elsif `which xclip 2>/dev/null`.strip.length > 0
48
+ IO.popen("xclip -selection clipboard", "w") { |io| io.write(text) }
49
+ elsif `which xsel 2>/dev/null`.strip.length > 0
50
+ IO.popen("xsel --clipboard --input", "w") { |io| io.write(text) }
51
+ end
52
+ @message = "Copied!"
53
+ @timer = 60
54
+ end
55
+
56
+ # Decrements the feedback timer by one frame.
57
+ #
58
+ # Call this once per render cycle. The message disappears when the timer
59
+ # reaches zero.
60
+ #
61
+ # === Example
62
+ #
63
+ # clipboard.copy("text") # timer = 60
64
+ # clipboard.tick # timer = 59
65
+ # 60.times { clipboard.tick } # message becomes ""
66
+ def tick
67
+ @timer -= 1 if @timer > 0
68
+ @message = "" if @timer <= 0
69
+ end
70
+
71
+ # Current feedback message.
72
+ #
73
+ # Empty string when no active message. <tt>"Copied!"</tt> after a successful
74
+ # copy, fading after 60 frames.
75
+ #
76
+ # === Example
77
+ #
78
+ # clipboard.message # => ""
79
+ # clipboard.copy("x")
80
+ # clipboard.message # => "Copied!"
81
+ def message
82
+ @message
83
+ end
84
+ end