ratatui_ruby 0.3.1 → 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 (350) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +89 -132
  7. data/CHANGELOG.md +223 -1
  8. data/README.md +23 -16
  9. data/REUSE.toml +20 -0
  10. data/doc/application_architecture.md +176 -0
  11. data/doc/application_testing.md +17 -10
  12. data/doc/contributors/design/ruby_frontend.md +11 -7
  13. data/doc/contributors/developing_examples.md +261 -0
  14. data/doc/contributors/documentation_style.md +104 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/index.md +2 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +125 -0
  19. data/doc/images/app_all_events.png +0 -0
  20. data/doc/images/app_analytics.png +0 -0
  21. data/doc/images/app_color_picker.png +0 -0
  22. data/doc/images/app_custom_widget.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_map_demo.png +0 -0
  25. data/doc/images/app_mouse_events.png +0 -0
  26. data/doc/images/app_table_select.png +0 -0
  27. data/doc/images/verify_quickstart_dsl.png +0 -0
  28. data/doc/images/verify_quickstart_layout.png +0 -0
  29. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  30. data/doc/images/verify_readme_usage.png +0 -0
  31. data/doc/images/widget_barchart_demo.png +0 -0
  32. data/doc/images/widget_block_padding.png +0 -0
  33. data/doc/images/widget_block_titles.png +0 -0
  34. data/doc/images/widget_box_demo.png +0 -0
  35. data/doc/images/widget_calendar_demo.png +0 -0
  36. data/doc/images/widget_cell_demo.png +0 -0
  37. data/doc/images/widget_chart_demo.png +0 -0
  38. data/doc/images/widget_gauge_demo.png +0 -0
  39. data/doc/images/widget_layout_split.png +0 -0
  40. data/doc/images/widget_line_gauge_demo.png +0 -0
  41. data/doc/images/widget_list_demo.png +0 -0
  42. data/doc/images/widget_list_styles.png +0 -0
  43. data/doc/images/widget_popup_demo.png +0 -0
  44. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  45. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  46. data/doc/images/widget_rect.png +0 -0
  47. data/doc/images/widget_render.png +0 -0
  48. data/doc/images/widget_rich_text.png +0 -0
  49. data/doc/images/widget_scroll_text.png +0 -0
  50. data/doc/images/widget_scrollbar_demo.png +0 -0
  51. data/doc/images/widget_sparkline_demo.png +0 -0
  52. data/doc/images/widget_style_colors.png +0 -0
  53. data/doc/images/widget_table_flex.png +0 -0
  54. data/doc/images/widget_tabs_demo.png +0 -0
  55. data/doc/index.md +1 -0
  56. data/doc/interactive_design.md +116 -0
  57. data/doc/quickstart.md +186 -84
  58. data/examples/app_all_events/README.md +81 -0
  59. data/examples/app_all_events/app.rb +93 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +75 -0
  62. data/examples/app_all_events/model/events.rb +180 -0
  63. data/examples/app_all_events/model/highlight.rb +57 -0
  64. data/examples/app_all_events/model/timestamp.rb +54 -0
  65. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +24 -0
  66. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +24 -0
  67. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +24 -0
  68. data/examples/app_all_events/test/snapshots/after_key_a.txt +24 -0
  69. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +24 -0
  70. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +24 -0
  71. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +24 -0
  72. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +24 -0
  73. data/examples/app_all_events/test/snapshots/after_paste.txt +24 -0
  74. data/examples/app_all_events/test/snapshots/after_resize.txt +24 -0
  75. data/examples/app_all_events/test/snapshots/after_right_click.txt +24 -0
  76. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +24 -0
  77. data/examples/app_all_events/test/snapshots/initial_state.txt +24 -0
  78. data/examples/app_all_events/view/app_view.rb +78 -0
  79. data/examples/app_all_events/view/controls_view.rb +50 -0
  80. data/examples/app_all_events/view/counts_view.rb +55 -0
  81. data/examples/app_all_events/view/live_view.rb +69 -0
  82. data/examples/app_all_events/view/log_view.rb +60 -0
  83. data/{lib/ratatui_ruby/output.rb → examples/app_all_events/view.rb} +1 -1
  84. data/examples/app_all_events/view_state.rb +42 -0
  85. data/examples/app_color_picker/README.md +94 -0
  86. data/examples/app_color_picker/app.rb +112 -0
  87. data/examples/app_color_picker/clipboard.rb +84 -0
  88. data/examples/app_color_picker/color.rb +191 -0
  89. data/examples/app_color_picker/copy_dialog.rb +170 -0
  90. data/examples/app_color_picker/harmony.rb +56 -0
  91. data/examples/app_color_picker/input.rb +142 -0
  92. data/examples/app_color_picker/palette.rb +80 -0
  93. data/examples/app_color_picker/scene.rb +201 -0
  94. data/examples/app_login_form/app.rb +108 -0
  95. data/examples/app_map_demo/app.rb +93 -0
  96. data/examples/app_table_select/app.rb +201 -0
  97. data/examples/verify_quickstart_dsl/app.rb +45 -0
  98. data/examples/verify_quickstart_layout/app.rb +69 -0
  99. data/examples/verify_quickstart_lifecycle/app.rb +48 -0
  100. data/examples/verify_readme_usage/app.rb +34 -0
  101. data/examples/widget_barchart_demo/app.rb +238 -0
  102. data/examples/widget_block_padding/app.rb +67 -0
  103. data/examples/widget_block_titles/app.rb +69 -0
  104. data/examples/widget_box_demo/app.rb +250 -0
  105. data/examples/widget_calendar_demo/app.rb +109 -0
  106. data/examples/widget_cell_demo/app.rb +104 -0
  107. data/examples/widget_chart_demo/app.rb +213 -0
  108. data/examples/widget_gauge_demo/app.rb +212 -0
  109. data/examples/widget_layout_split/app.rb +246 -0
  110. data/examples/widget_line_gauge_demo/app.rb +217 -0
  111. data/examples/widget_list_demo/app.rb +382 -0
  112. data/examples/widget_list_styles/app.rb +141 -0
  113. data/examples/widget_popup_demo/app.rb +104 -0
  114. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  115. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  116. data/examples/widget_rect/app.rb +205 -0
  117. data/examples/widget_render/app.rb +184 -0
  118. data/examples/widget_rich_text/app.rb +137 -0
  119. data/examples/widget_scroll_text/app.rb +108 -0
  120. data/examples/widget_scrollbar_demo/app.rb +153 -0
  121. data/examples/widget_sparkline_demo/app.rb +274 -0
  122. data/examples/widget_style_colors/app.rb +102 -0
  123. data/examples/widget_table_flex/app.rb +95 -0
  124. data/examples/widget_tabs_demo/app.rb +167 -0
  125. data/ext/ratatui_ruby/Cargo.lock +889 -115
  126. data/ext/ratatui_ruby/Cargo.toml +4 -3
  127. data/ext/ratatui_ruby/clippy.toml +7 -0
  128. data/ext/ratatui_ruby/extconf.rb +7 -0
  129. data/ext/ratatui_ruby/src/events.rs +293 -219
  130. data/ext/ratatui_ruby/src/frame.rs +115 -0
  131. data/ext/ratatui_ruby/src/lib.rs +105 -24
  132. data/ext/ratatui_ruby/src/rendering.rs +94 -10
  133. data/ext/ratatui_ruby/src/style.rs +357 -93
  134. data/ext/ratatui_ruby/src/terminal.rs +121 -31
  135. data/ext/ratatui_ruby/src/text.rs +178 -0
  136. data/ext/ratatui_ruby/src/widgets/barchart.rs +99 -24
  137. data/ext/ratatui_ruby/src/widgets/block.rs +32 -3
  138. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  139. data/ext/ratatui_ruby/src/widgets/canvas.rs +44 -9
  140. data/ext/ratatui_ruby/src/widgets/chart.rs +79 -27
  141. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  142. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  143. data/ext/ratatui_ruby/src/widgets/layout.rs +223 -15
  144. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  145. data/ext/ratatui_ruby/src/widgets/list.rs +114 -11
  146. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  147. data/ext/ratatui_ruby/src/widgets/overlay.rs +4 -2
  148. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  149. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  150. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +51 -0
  151. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +61 -7
  152. data/ext/ratatui_ruby/src/widgets/sparkline.rs +73 -6
  153. data/ext/ratatui_ruby/src/widgets/table.rs +177 -64
  154. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  155. data/lib/ratatui_ruby/cell.rb +166 -0
  156. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  157. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  158. data/lib/ratatui_ruby/event/key.rb +211 -0
  159. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  160. data/lib/ratatui_ruby/event/none.rb +43 -0
  161. data/lib/ratatui_ruby/event/paste.rb +71 -0
  162. data/lib/ratatui_ruby/event/resize.rb +80 -0
  163. data/lib/ratatui_ruby/event.rb +131 -0
  164. data/lib/ratatui_ruby/frame.rb +87 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  166. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +23 -0
  167. data/lib/ratatui_ruby/schema/bar_chart.rb +226 -17
  168. data/lib/ratatui_ruby/schema/block.rb +178 -11
  169. data/lib/ratatui_ruby/schema/calendar.rb +70 -14
  170. data/lib/ratatui_ruby/schema/canvas.rb +213 -46
  171. data/lib/ratatui_ruby/schema/center.rb +46 -8
  172. data/lib/ratatui_ruby/schema/chart.rb +134 -32
  173. data/lib/ratatui_ruby/schema/clear.rb +22 -53
  174. data/lib/ratatui_ruby/schema/constraint.rb +72 -12
  175. data/lib/ratatui_ruby/schema/cursor.rb +23 -5
  176. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  177. data/lib/ratatui_ruby/schema/gauge.rb +56 -12
  178. data/lib/ratatui_ruby/schema/layout.rb +91 -9
  179. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  180. data/lib/ratatui_ruby/schema/list.rb +92 -16
  181. data/lib/ratatui_ruby/schema/overlay.rb +29 -3
  182. data/lib/ratatui_ruby/schema/paragraph.rb +82 -25
  183. data/lib/ratatui_ruby/schema/ratatui_logo.rb +29 -0
  184. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +34 -0
  185. data/lib/ratatui_ruby/schema/rect.rb +59 -10
  186. data/lib/ratatui_ruby/schema/scrollbar.rb +127 -19
  187. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  188. data/lib/ratatui_ruby/schema/sparkline.rb +120 -12
  189. data/lib/ratatui_ruby/schema/style.rb +39 -11
  190. data/lib/ratatui_ruby/schema/table.rb +109 -18
  191. data/lib/ratatui_ruby/schema/tabs.rb +71 -10
  192. data/lib/ratatui_ruby/schema/text.rb +90 -0
  193. data/lib/ratatui_ruby/session/autodoc.rb +417 -0
  194. data/lib/ratatui_ruby/session.rb +163 -0
  195. data/lib/ratatui_ruby/test_helper.rb +322 -13
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby.rb +184 -38
  198. data/sig/examples/app_all_events/app.rbs +11 -0
  199. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  200. data/sig/examples/app_all_events/model/events.rbs +15 -0
  201. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  202. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  203. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  204. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  205. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  206. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  207. data/sig/examples/app_all_events/view.rbs +8 -0
  208. data/sig/examples/app_all_events/view_state.rbs +15 -0
  209. data/sig/examples/app_color_picker/app.rbs +12 -0
  210. data/sig/examples/app_login_form/app.rbs +11 -0
  211. data/sig/examples/app_map_demo/app.rbs +11 -0
  212. data/sig/examples/app_table_select/app.rbs +11 -0
  213. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  214. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  215. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  216. data/sig/examples/widget_block_padding/app.rbs +11 -0
  217. data/sig/examples/widget_block_titles/app.rbs +11 -0
  218. data/sig/examples/widget_box_demo/app.rbs +11 -0
  219. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  220. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  221. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  222. data/sig/examples/widget_gauge_demo/app.rbs +11 -0
  223. data/sig/examples/widget_layout_split/app.rbs +10 -0
  224. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  225. data/sig/examples/widget_list_demo/app.rbs +12 -0
  226. data/sig/examples/widget_list_styles/app.rbs +11 -0
  227. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  228. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  229. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  230. data/sig/examples/widget_rect/app.rbs +12 -0
  231. data/sig/examples/widget_render/app.rbs +10 -0
  232. data/sig/examples/widget_rich_text/app.rbs +11 -0
  233. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  234. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  235. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  236. data/sig/examples/widget_style_colors/app.rbs +14 -0
  237. data/sig/examples/widget_table_flex/app.rbs +11 -0
  238. data/sig/ratatui_ruby/event.rbs +69 -0
  239. data/sig/ratatui_ruby/frame.rbs +9 -0
  240. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -3
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  244. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  245. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  246. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  247. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  248. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  249. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  250. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  251. data/sig/ratatui_ruby/schema/draw.rbs +27 -0
  252. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  253. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  255. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  256. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  257. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +8 -0
  258. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  259. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  260. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  261. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  262. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  263. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  264. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  265. data/sig/ratatui_ruby/session.rbs +94 -0
  266. data/tasks/autodoc/inventory.rb +61 -0
  267. data/tasks/autodoc/member.rb +56 -0
  268. data/tasks/autodoc/name.rb +19 -0
  269. data/tasks/autodoc/notice.rb +26 -0
  270. data/tasks/autodoc/rbs.rb +38 -0
  271. data/tasks/autodoc/rdoc.rb +45 -0
  272. data/tasks/autodoc.rake +47 -0
  273. data/tasks/bump/history.rb +2 -2
  274. data/tasks/doc.rake +600 -6
  275. data/tasks/example_viewer.html.erb +172 -0
  276. data/tasks/lint.rake +8 -4
  277. data/tasks/resources/build.yml.erb +13 -11
  278. data/tasks/resources/index.html.erb +6 -0
  279. data/tasks/sourcehut.rake +4 -4
  280. data/tasks/terminal_preview/app_screenshot.rb +33 -0
  281. data/tasks/terminal_preview/crash_report.rb +52 -0
  282. data/tasks/terminal_preview/example_app.rb +25 -0
  283. data/tasks/terminal_preview/launcher_script.rb +46 -0
  284. data/tasks/terminal_preview/preview_collection.rb +58 -0
  285. data/tasks/terminal_preview/preview_timing.rb +22 -0
  286. data/tasks/terminal_preview/safety_confirmation.rb +56 -0
  287. data/tasks/terminal_preview/saved_screenshot.rb +53 -0
  288. data/tasks/terminal_preview/system_appearance.rb +11 -0
  289. data/tasks/terminal_preview/terminal_window.rb +136 -0
  290. data/tasks/terminal_preview/window_id.rb +14 -0
  291. data/tasks/terminal_preview.rake +28 -0
  292. data/tasks/test.rake +2 -2
  293. data/tasks/website/index_page.rb +3 -3
  294. data/tasks/website/version.rb +10 -10
  295. data/tasks/website/version_menu.rb +10 -12
  296. data/tasks/website/versioned_documentation.rb +49 -17
  297. data/tasks/website/website.rb +6 -8
  298. data/tasks/website.rake +4 -4
  299. metadata +206 -54
  300. data/LICENSES/BSD-2-Clause.txt +0 -9
  301. data/doc/images/examples-analytics.rb.png +0 -0
  302. data/doc/images/examples-box_demo.rb.png +0 -0
  303. data/doc/images/examples-calendar_demo.rb.png +0 -0
  304. data/doc/images/examples-chart_demo.rb.png +0 -0
  305. data/doc/images/examples-custom_widget.rb.png +0 -0
  306. data/doc/images/examples-dashboard.rb.png +0 -0
  307. data/doc/images/examples-list_styles.rb.png +0 -0
  308. data/doc/images/examples-login_form.rb.png +0 -0
  309. data/doc/images/examples-map_demo.rb.png +0 -0
  310. data/doc/images/examples-mouse_events.rb.png +0 -0
  311. data/doc/images/examples-popup_demo.rb.gif +0 -0
  312. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  313. data/doc/images/examples-scroll_text.rb.png +0 -0
  314. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  315. data/doc/images/examples-stock_ticker.rb.png +0 -0
  316. data/doc/images/examples-system_monitor.rb.png +0 -0
  317. data/doc/images/examples-table_select.rb.png +0 -0
  318. data/examples/analytics.rb +0 -88
  319. data/examples/box_demo.rb +0 -71
  320. data/examples/calendar_demo.rb +0 -55
  321. data/examples/chart_demo.rb +0 -84
  322. data/examples/custom_widget.rb +0 -43
  323. data/examples/dashboard.rb +0 -72
  324. data/examples/list_styles.rb +0 -66
  325. data/examples/login_form.rb +0 -115
  326. data/examples/map_demo.rb +0 -58
  327. data/examples/mouse_events.rb +0 -95
  328. data/examples/popup_demo.rb +0 -105
  329. data/examples/quickstart_dsl.rb +0 -30
  330. data/examples/quickstart_lifecycle.rb +0 -40
  331. data/examples/readme_usage.rb +0 -21
  332. data/examples/scroll_text.rb +0 -74
  333. data/examples/scrollbar_demo.rb +0 -75
  334. data/examples/stock_ticker.rb +0 -93
  335. data/examples/system_monitor.rb +0 -94
  336. data/examples/table_select.rb +0 -70
  337. data/examples/test_analytics.rb +0 -65
  338. data/examples/test_box_demo.rb +0 -38
  339. data/examples/test_calendar_demo.rb +0 -66
  340. data/examples/test_dashboard.rb +0 -38
  341. data/examples/test_list_styles.rb +0 -61
  342. data/examples/test_login_form.rb +0 -63
  343. data/examples/test_map_demo.rb +0 -100
  344. data/examples/test_popup_demo.rb +0 -62
  345. data/examples/test_scroll_text.rb +0 -130
  346. data/examples/test_stock_ticker.rb +0 -39
  347. data/examples/test_system_monitor.rb +0 -40
  348. data/examples/test_table_select.rb +0 -37
  349. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  350. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,142 @@
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 "color"
7
+
8
+ # Manages text input and color parsing with error feedback.
9
+ #
10
+ # Users type color values. They make mistakes—typos, invalid formats. The app
11
+ # needs to validate their input and show helpful error messages. Manually
12
+ # tracking input state, validation, and error messages across renders is
13
+ # cumbersome and error-prone.
14
+ #
15
+ # This object holds the current input string. It validates by parsing. It stores
16
+ # errors and clears them when appropriate. It provides methods to manipulate
17
+ # the input (append, delete).
18
+ #
19
+ # Use it to build text input forms where validation feedback matters.
20
+ #
21
+ # === Example
22
+ #
23
+ # input = Input.new
24
+ # input.append_char("#")
25
+ # input.append_char("f")
26
+ # input.append_char("f")
27
+ # color = input.parse # => Color or nil
28
+ # puts input.error # => error message if parse failed
29
+ class Input
30
+ PRINTABLE_PATTERN = /[\w#,().\s%]/
31
+
32
+ # Creates a new Input with an optional initial value.
33
+ #
34
+ # [initial_value] String initial color input (default: <tt>"#F96302"</tt>)
35
+ def initialize(initial_value = "#F96302")
36
+ @value = initial_value
37
+ @error = ""
38
+ end
39
+
40
+ # Current input string.
41
+ #
42
+ # === Example
43
+ #
44
+ # input = Input.new
45
+ # input.value # => "#F96302"
46
+ def value
47
+ @value
48
+ end
49
+
50
+ # Error message from the last failed parse, or empty string.
51
+ #
52
+ # === Example
53
+ #
54
+ # input.parse # => nil (invalid)
55
+ # input.error # => "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
56
+ def error
57
+ @error
58
+ end
59
+
60
+ # Clears the current error message.
61
+ def clear_error
62
+ @error = ""
63
+ end
64
+
65
+ # Appends a character to the input if it matches the printable pattern.
66
+ #
67
+ # Silently ignores non-printable characters. Valid characters include
68
+ # letters, digits, hash, comma, parentheses, dot, space, and percent.
69
+ #
70
+ # [char] String single character
71
+ def append_char(char)
72
+ @value += char if char.length == 1 && char.match?(PRINTABLE_PATTERN)
73
+ end
74
+
75
+ # Removes the last character from the input.
76
+ def delete_char
77
+ @value = @value[0...-1]
78
+ end
79
+
80
+ # Replaces the entire input string.
81
+ #
82
+ # [text] String new input value
83
+ def set(text)
84
+ @value = text
85
+ end
86
+
87
+ # Parses the current input as a Color.
88
+ #
89
+ # Returns a Color if valid; nil otherwise. Sets the error message on failure.
90
+ # Clears the error message on success.
91
+ #
92
+ # === Example
93
+ #
94
+ # input = Input.new("#FF0000")
95
+ # color = input.parse # => Color
96
+ # input.error # => ""
97
+ def parse
98
+ color = Color.parse(@value)
99
+ if color
100
+ clear_error
101
+ color
102
+ else
103
+ @error = "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
104
+ nil
105
+ end
106
+ end
107
+
108
+ # Renders the input widget for display in a TUI frame.
109
+ #
110
+ # Shows the current input value with a cursor. Displays the error message
111
+ # in red if one is set.
112
+ #
113
+ # [tui] Session or TUI factory object
114
+ #
115
+ # === Example
116
+ #
117
+ # input = Input.new
118
+ # widget = input.render(tui)
119
+ # frame.render_widget(widget, area)
120
+ def render(tui)
121
+ input_lines = [
122
+ tui.text_line(spans: [
123
+ tui.text_span(content: @value),
124
+ tui.text_span(content: "_", style: tui.style(modifiers: [:reversed])),
125
+ ]),
126
+ ]
127
+
128
+ unless @error.empty?
129
+ input_lines << tui.text_line(spans: [
130
+ tui.text_span(content: @error, style: tui.style(fg: :red)),
131
+ ])
132
+ end
133
+
134
+ tui.block(
135
+ title: "Color Input",
136
+ borders: [:all],
137
+ children: [
138
+ tui.paragraph(text: input_lines),
139
+ ]
140
+ )
141
+ end
142
+ end
@@ -0,0 +1,80 @@
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 "color"
7
+
8
+ # Holds a primary color and its harmonies.
9
+ #
10
+ # Color pickers need to show related colors: shades, tints, complements. Building
11
+ # these relationships repeatedly is redundant. Passing them individually through
12
+ # rendering pipelines is awkward.
13
+ #
14
+ # This object owns a primary color and generates its harmonies on demand. It
15
+ # provides accessor methods and rendering helpers.
16
+ #
17
+ # Use it to organize color data for palette displays.
18
+ #
19
+ # === Example
20
+ #
21
+ # color = Color.parse("#FF0000")
22
+ # palette = Palette.new(color)
23
+ # palette.main # => Color
24
+ # palette.all # => [Harmony, Harmony, ...]
25
+ # blocks = palette.as_blocks(tui) # => [Block, Block, ...]
26
+ class Palette
27
+ def initialize(primary_color)
28
+ @primary = primary_color
29
+ end
30
+
31
+ # The primary (main) color, or nil if no color is set.
32
+ #
33
+ # === Example
34
+ #
35
+ # palette = Palette.new(color)
36
+ # palette.main.hex # => "#FF0000"
37
+ def main
38
+ @primary
39
+ end
40
+
41
+ # All harmonies: main, shade, tint, complement, split 1, split 2, split-complement.
42
+ #
43
+ # Returns an empty array if no primary color is set.
44
+ #
45
+ # === Example
46
+ #
47
+ # palette = Palette.new(color)
48
+ # palette.all.size # => 7
49
+ def all
50
+ return [] if @primary.nil?
51
+
52
+ @primary.harmonies
53
+ end
54
+
55
+ # Renders all harmonies as TUI Block widgets.
56
+ #
57
+ # Each harmony becomes a titled block showing its color swatch. Returns an empty
58
+ # array if no primary color is set.
59
+ #
60
+ # [tui] Session or TUI factory object
61
+ #
62
+ # === Example
63
+ #
64
+ # palette = Palette.new(color)
65
+ # blocks = palette.as_blocks(tui)
66
+ # # blocks[0] => Block titled "Main" with color swatch
67
+ def as_blocks(tui)
68
+ return [] if @primary.nil?
69
+
70
+ all.map do |harmony|
71
+ tui.block(
72
+ title: harmony.label,
73
+ borders: [:all],
74
+ children: [
75
+ tui.paragraph(text: harmony.color_swatch_lines(tui)),
76
+ ]
77
+ )
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,201 @@
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 "input"
7
+ require_relative "palette"
8
+ require_relative "clipboard"
9
+ require_relative "copy_dialog"
10
+
11
+ # Orchestrates layout and rendering of the color picker UI.
12
+ #
13
+ # Building a complete color picker UI involves layout calculation, widget
14
+ # composition, and coordinate tracking for hit testing. Keeping this logic
15
+ # scattered across the main app makes the app harder to read and test.
16
+ #
17
+ # This object owns the layout logic. It orchestrates all sections. It calculates
18
+ # and caches rects for hit testing.
19
+ #
20
+ # Use it to encapsulate complex UI composition.
21
+ #
22
+ # === Example
23
+ #
24
+ # scene = Scene.new(tui)
25
+ # scene.render(frame, input:, palette:, clipboard:, dialog:)
26
+ #
27
+ # # For hit testing:
28
+ # rect = scene.export_rect
29
+ # if rect.contains?(x, y)
30
+ # # Handle click
31
+ # end
32
+ class Scene
33
+ def initialize(tui)
34
+ @tui = tui
35
+ @export_area_rect = nil
36
+ @hotkey_style = @tui.style(modifiers: [:bold, :underlined])
37
+ end
38
+
39
+ # Renders the complete UI given all model objects.
40
+ #
41
+ # Calculates layout once per frame. Renders input, palette, export, controls,
42
+ # and dialog sections. Caches the export area rect for hit testing.
43
+ #
44
+ # [frame] Frame object from RatatuiRuby.draw block
45
+ # [input] Input object for text input display
46
+ # [palette] Palette object for color display
47
+ # [clipboard] Clipboard object for feedback message
48
+ # [dialog] CopyDialog object for confirmation dialog
49
+ #
50
+ # === Example
51
+ #
52
+ # scene.render(frame, input: @input, palette: @palette, clipboard: @clipboard, dialog: @dialog)
53
+ def render(frame, input:, palette:, clipboard:, dialog:)
54
+ input_area, rest = @tui.layout_split(
55
+ frame.area,
56
+ direction: :vertical,
57
+ constraints: [
58
+ @tui.constraint_length(3),
59
+ @tui.constraint_fill(1),
60
+ ]
61
+ )
62
+
63
+ color_area, control_area = @tui.layout_split(
64
+ rest,
65
+ direction: :vertical,
66
+ constraints: [
67
+ @tui.constraint_length(14),
68
+ @tui.constraint_fill(1),
69
+ ]
70
+ )
71
+
72
+ harmony_area, @export_area_rect = @tui.layout_split(
73
+ color_area,
74
+ direction: :vertical,
75
+ constraints: [
76
+ @tui.constraint_length(7),
77
+ @tui.constraint_fill(1),
78
+ ]
79
+ )
80
+
81
+ frame.render_widget(input.render(@tui), input_area)
82
+ frame.render_widget(build_palette_section(palette, harmony_area), harmony_area)
83
+ frame.render_widget(build_export_section(palette), @export_area_rect)
84
+ frame.render_widget(build_controls_section(clipboard), control_area)
85
+
86
+ if dialog.active?
87
+ dialog_center = calculate_center_area(frame.area, 40, 8)
88
+ frame.render_widget(@tui.clear, frame.area)
89
+ frame.render_widget(dialog.render(@tui, dialog_center), dialog_center)
90
+ end
91
+ end
92
+
93
+ # The cached rectangle of the export formats section, used for hit testing.
94
+ #
95
+ # Populated during #render. Use this to detect clicks on the export section.
96
+ #
97
+ # === Example
98
+ #
99
+ # scene.render(frame, ...)
100
+ # if scene.export_rect.contains?(x, y)
101
+ # # Click on export section
102
+ # end
103
+ def export_rect
104
+ @export_area_rect
105
+ end
106
+
107
+ private def build_palette_section(palette, _harmony_area)
108
+ if palette.main.nil?
109
+ @tui.paragraph(text: "No color selected")
110
+ else
111
+ blocks = palette.as_blocks(@tui)
112
+ @tui.layout(
113
+ direction: :horizontal,
114
+ constraints: Array.new(blocks.size) { @tui.constraint_fill(1) },
115
+ children: blocks
116
+ )
117
+ end
118
+ end
119
+
120
+ private def build_export_section(palette)
121
+ if palette.main.nil?
122
+ @tui.block(
123
+ title: "Export Formats",
124
+ borders: [:all],
125
+ children: [
126
+ @tui.paragraph(
127
+ text: @tui.text_line(spans: [
128
+ @tui.text_span(content: "Enter a color to see formats"),
129
+ ])
130
+ ),
131
+ ]
132
+ )
133
+ else
134
+ color = palette.main
135
+ hex = color.hex
136
+ rgb = color.rgb
137
+ hsl = color.hsl_string
138
+ text_color = color.contrasting_text_color
139
+ bg_style = @tui.style(bg: hex, fg: text_color)
140
+
141
+ @tui.block(
142
+ title: "Export Formats",
143
+ borders: [:all],
144
+ style: bg_style,
145
+ children: [
146
+ @tui.paragraph(
147
+ text: [
148
+ @tui.text_line(spans: [
149
+ @tui.text_span(content: "HEX: ", style: bg_style),
150
+ @tui.text_span(content: hex, style: @tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
151
+ ]),
152
+ @tui.text_line(spans: [
153
+ @tui.text_span(content: "RGB: ", style: bg_style),
154
+ @tui.text_span(content: rgb, style: @tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
155
+ ]),
156
+ @tui.text_line(spans: [
157
+ @tui.text_span(content: "HSL: ", style: bg_style),
158
+ @tui.text_span(content: hsl, style: @tui.style(bg: hex, fg: text_color, modifiers: [:underlined])),
159
+ ]),
160
+ ]
161
+ ),
162
+ ]
163
+ )
164
+ end
165
+ end
166
+
167
+ private def build_controls_section(clipboard)
168
+ control_lines = [
169
+ @tui.text_line(spans: [
170
+ @tui.text_span(content: "a-z/0-9", style: @hotkey_style),
171
+ @tui.text_span(content: ": Type "),
172
+ @tui.text_span(content: "enter", style: @hotkey_style),
173
+ @tui.text_span(content: ": Parse "),
174
+ @tui.text_span(content: "bksp", style: @hotkey_style),
175
+ @tui.text_span(content: ": Erase "),
176
+ @tui.text_span(content: "esc", style: @hotkey_style),
177
+ @tui.text_span(content: ": Quit"),
178
+ ]),
179
+ ]
180
+
181
+ unless clipboard.message.empty?
182
+ control_lines << @tui.text_line(spans: [
183
+ @tui.text_span(content: clipboard.message, style: @tui.style(fg: :green, modifiers: [:bold])),
184
+ ])
185
+ end
186
+
187
+ @tui.block(
188
+ title: "Controls",
189
+ borders: [:all],
190
+ children: [
191
+ @tui.paragraph(text: control_lines),
192
+ ]
193
+ )
194
+ end
195
+
196
+ private def calculate_center_area(parent_area, width, height)
197
+ x = (parent_area.width - width) / 2
198
+ y = (parent_area.height - height) / 2
199
+ @tui.rect(x:, y:, width:, height:)
200
+ end
201
+ end
@@ -0,0 +1,108 @@
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
+ class AppLoginForm
10
+ PREFIX = "Enter Username: [ "
11
+ SUFFIX = " ]"
12
+
13
+ def initialize
14
+ @username = ""
15
+ @show_popup = false
16
+ end
17
+
18
+ def run
19
+ RatatuiRuby.run do |tui|
20
+ @tui = tui
21
+ loop do
22
+ render
23
+ break if handle_input == :quit
24
+ end
25
+ end
26
+ end
27
+
28
+ private def render
29
+ # 1. Base Layer Construction
30
+ # We want a cursor relative to the paragraph.
31
+ # So we wrap Paragraph and Cursor in an Overlay, and put that Overlay in a Center.
32
+
33
+ # Calculate cursor position
34
+ # Border takes 1 cell.
35
+ # Cursor X = 1 (border) + PREFIX.length + username.length
36
+ # Cursor Y = 1 (border + line 0)
37
+ cursor_x = 1 + PREFIX.length + @username.length
38
+ cursor_y = 1
39
+
40
+ # The content of the base form
41
+ form_content = @tui.overlay(layers: [
42
+ @tui.paragraph(
43
+ text: "#{PREFIX}#{@username}#{SUFFIX}",
44
+ block: @tui.block(borders: :all, title: "Login Form"),
45
+ alignment: :left
46
+ ),
47
+ @tui.cursor(x: cursor_x, y: cursor_y),
48
+ ])
49
+
50
+ # Center the form on screen
51
+ base_layer = @tui.center(
52
+ child: form_content,
53
+ width_percent: 50,
54
+ height_percent: 20
55
+ )
56
+
57
+ # 2. Popup Layer Construction
58
+ final_view = if @show_popup
59
+ popup_message = @tui.center(
60
+ child: @tui.paragraph(
61
+ text: "Login Successful!\nPress 'q' to quit.",
62
+ style: @tui.style(fg: :green, bg: :black),
63
+ block: @tui.block(borders: :all),
64
+ alignment: :center,
65
+ wrap: true
66
+ ),
67
+ width_percent: 30,
68
+ height_percent: 20
69
+ )
70
+
71
+ # Render Base Layer (background) THEN Popup Layer
72
+ @tui.overlay(layers: [base_layer, popup_message])
73
+ else
74
+ base_layer
75
+ end
76
+
77
+ # 3. Draw
78
+ @tui.draw do |frame|
79
+ frame.render_widget(final_view, frame.area)
80
+ end
81
+ end
82
+
83
+ private def handle_input
84
+ case @tui.poll_event
85
+ in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
86
+ return :quit if @show_popup
87
+ nil
88
+ in { type: :key, code: "c", modifiers: ["ctrl"] }
89
+ :quit
90
+ in { type: :key, code: "enter" }
91
+ @show_popup ||= true
92
+ nil
93
+ in { type: :key, code: "backspace" }
94
+ @username.chop! unless @show_popup
95
+ nil
96
+ in { type: :key, code: "esc" }
97
+ :quit unless @show_popup
98
+ in { type: :key, code:, modifiers: [] }
99
+ # Simple text input (single character, no modifiers)
100
+ @username += code if !@show_popup && code.length == 1
101
+ nil
102
+ else
103
+ nil
104
+ end
105
+ end
106
+ end
107
+
108
+ AppLoginForm.new.run if __FILE__ == $0
@@ -0,0 +1,93 @@
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
+ # An example of the Canvas widget showing a world map and animated shapes.
10
+ class AppMapDemo
11
+ include RatatuiRuby
12
+
13
+ COLORS = [:black, :blue, :white, nil].freeze
14
+ MARKERS = [:braille, :half_block, :dot, :block, :bar, :quadrant, :sextant, :octant].freeze
15
+
16
+ # Returns a Canvas view for the map demo with the given circle radius.
17
+ #
18
+ # +tui+:: The RatatuiRuby::Session instance.
19
+ # +radius+:: The radius of the animated circle.
20
+ # +marker+:: The marker type.
21
+ # +background_color+:: The background color of the canvas.
22
+ # +show_labels+:: Whether to show city labels.
23
+ def view(tui, radius, marker = :braille, background_color = nil, show_labels: true)
24
+ shapes = [
25
+ tui.shape_map(color: :green, resolution: :high),
26
+ tui.shape_circle(x: 0.0, y: 0.0, radius:, color: :red),
27
+ tui.shape_line(x1: 0.0, y1: 0.0, x2: 50.0, y2: 25.0, color: :yellow),
28
+ ]
29
+
30
+ if show_labels
31
+ shapes += [
32
+ tui.shape_label(x: -0.1, y: 51.5, text: "London", style: tui.style(fg: :cyan)),
33
+ tui.shape_label(x: 139.7, y: 35.7, text: "Tokyo", style: tui.style(fg: :magenta)),
34
+ tui.shape_label(x: -74.0, y: 40.7, text: "New York", style: tui.style(fg: :yellow)),
35
+ tui.shape_label(x: -122.4, y: 37.8, text: "San Francisco", style: tui.style(fg: :blue)),
36
+ tui.shape_label(x: 151.2, y: -33.9, text: "Sydney", style: tui.style(fg: :green)),
37
+ ]
38
+ end
39
+
40
+ tui.canvas(
41
+ shapes:,
42
+ x_bounds: [-180.0, 180.0],
43
+ y_bounds: [-90.0, 90.0],
44
+ marker:,
45
+ block: tui.block(title: "World Map ['b' bg, 'm' marker: #{marker}, 'l' labels: #{show_labels ? 'on' : 'off'}]", borders: :all),
46
+ background_color:
47
+ )
48
+ end
49
+
50
+ # Runs the map demo loop.
51
+ def run
52
+ RatatuiRuby.run do |tui|
53
+ radius = 0.0
54
+ direction = 1
55
+ bg_index = 0
56
+ marker_index = 0
57
+ show_labels = true
58
+
59
+ loop do
60
+ # Animate the circle radius
61
+ radius += 0.5 * direction
62
+ if radius > 10.0 || radius < 0.0
63
+ direction *= -1
64
+ end
65
+
66
+ # Define the view
67
+ canvas = view(tui, radius, MARKERS[marker_index], COLORS[bg_index], show_labels:)
68
+
69
+ tui.draw do |frame|
70
+ frame.render_widget(canvas, frame.area)
71
+ end
72
+
73
+ event = tui.poll_event
74
+ case event
75
+ in { type: :key, code: "q" } | { type: :key, code: :ctrl_c }
76
+ break
77
+ in type: :key, code: "b"
78
+ bg_index = (bg_index + 1) % COLORS.size
79
+ in type: :key, code: "m"
80
+ marker_index = (marker_index + 1) % MARKERS.size
81
+ in type: :key, code: "l"
82
+ show_labels = !show_labels
83
+ else
84
+ # Ignore other events
85
+ end
86
+
87
+ sleep 0.05
88
+ end
89
+ end
90
+ end
91
+ end
92
+
93
+ AppMapDemo.new.run if __FILE__ == $PROGRAM_NAME