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,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(state, 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
+ # [state] ViewState 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(state, tui, frame, area)
44
+ def call(state, 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(state, tui, frame, counts_area)
74
+ @live_view.call(state, tui, frame, live_area)
75
+ @log_view.call(state, tui, frame, log_area)
76
+ @controls_view.call(state, tui, frame, control_area)
77
+ end
78
+ end
@@ -0,0 +1,50 @@
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(state, tui, frame, area)
21
+ class View::Controls
22
+ # Renders the controls widget to the given area.
23
+ #
24
+ # [state] ViewState containing style information.
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(state, tui, frame, area)
32
+ def call(state, tui, frame, area)
33
+ widget = tui.paragraph(
34
+ text: [
35
+ tui.text_line(spans: [
36
+ tui.text_span(content: "q", style: state.hotkey_style),
37
+ tui.text_span(content: ": Quit "),
38
+ tui.text_span(content: "Ctrl+C", style: state.hotkey_style),
39
+ tui.text_span(content: ": Quit"),
40
+ ]),
41
+ ],
42
+ block: tui.block(
43
+ title: "Controls",
44
+ borders: [:all],
45
+ border_style: tui.style(fg: :white)
46
+ )
47
+ )
48
+ frame.render_widget(widget, area)
49
+ end
50
+ 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 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
+ # [state] ViewState containing event data and styles.
19
+ # [tui] RatatuiRuby instance.
20
+ # [frame] RatatuiRuby::Frame being rendered.
21
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
22
+ def call(state, tui, frame, area)
23
+ count_lines = []
24
+
25
+ AppAllEvents::EVENT_TYPES.each do |type|
26
+ count = state.events.count(type)
27
+ label = type.to_s.capitalize
28
+ style = state.events.lit?(type) ? state.lit_style : nil
29
+
30
+ count_lines << tui.text_line(spans: [
31
+ tui.text_span(content: "#{label}: ", style:),
32
+ tui.text_span(content: count.to_s, style: style || tui.style(fg: :yellow)),
33
+ ])
34
+
35
+ state.events.sub_counts(type).each do |sub_type, sub_count|
36
+ sub_label = sub_type.to_s.capitalize
37
+ count_lines << tui.text_line(spans: [
38
+ tui.text_span(content: " #{sub_label}: ", style: state.dimmed_style),
39
+ tui.text_span(content: sub_count.to_s, style: state.dimmed_style),
40
+ ])
41
+ end
42
+ end
43
+
44
+ widget = tui.paragraph(
45
+ text: count_lines,
46
+ scroll: [0, 0],
47
+ block: tui.block(
48
+ title: "Event Counts",
49
+ borders: [:all],
50
+ border_style: tui.style(fg: state.border_color)
51
+ )
52
+ )
53
+ frame.render_widget(widget, area)
54
+ end
55
+ end
@@ -0,0 +1,69 @@
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(state, tui, frame, area)
21
+ class View::Live
22
+ # Renders the live event table to the given area.
23
+ #
24
+ # [state] ViewState containing event data and styles.
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(state, tui, frame, area)
32
+ def call(state, tui, frame, area)
33
+ rows = []
34
+
35
+ rows << tui.text_line(spans: [
36
+ tui.text_span(content: "Type".ljust(9), style: tui.style(fg: :gray, modifiers: [:bold])),
37
+ tui.text_span(content: "Time".ljust(10), style: tui.style(fg: :gray, modifiers: [:bold])),
38
+ tui.text_span(content: "Description", style: tui.style(fg: :gray, modifiers: [:bold])),
39
+ ])
40
+
41
+ (AppAllEvents::EVENT_TYPES - [:none]).each do |type|
42
+ event_data = state.events.live_event(type)
43
+
44
+ class_str = type.to_s.capitalize
45
+ time_str = event_data ? event_data[:time].strftime("%H:%M:%S") : "—"
46
+ desc_str = event_data ? event_data[:description] : "—"
47
+
48
+ is_lit = state.events.lit?(type)
49
+ row_style = is_lit ? tui.style(fg: :black, bg: :green) : nil
50
+
51
+ rows << tui.text_line(spans: [
52
+ tui.text_span(content: class_str.ljust(9), style: row_style || tui.style(fg: :cyan)),
53
+ tui.text_span(content: time_str.ljust(10), style: row_style || tui.style(fg: :white)),
54
+ tui.text_span(content: desc_str, style: row_style),
55
+ ])
56
+ end
57
+
58
+ widget = tui.paragraph(
59
+ text: rows,
60
+ scroll: [0, 0],
61
+ block: tui.block(
62
+ title: "Live Display",
63
+ borders: [:all],
64
+ border_style: tui.style(fg: state.border_color)
65
+ )
66
+ )
67
+ frame.render_widget(widget, area)
68
+ end
69
+ end
@@ -0,0 +1,60 @@
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
+ # [state] ViewState containing event data and styles.
20
+ # [tui] RatatuiRuby instance.
21
+ # [frame] RatatuiRuby::Frame being rendered.
22
+ # [area] RatatuiRuby::Rect defining the widget's bounds.
23
+ def call(state, tui, frame, area)
24
+ visible_entries_count = (area.height - 2) / 2
25
+ display_entries = state.events.visible(visible_entries_count)
26
+
27
+ log_lines = []
28
+ if state.events.empty?
29
+ log_lines << tui.text_line(spans: [tui.text_span(content: "No events yet...", style: state.dimmed_style)])
30
+ else
31
+ display_entries.each do |entry|
32
+ entry_style = tui.style(fg: entry.color)
33
+
34
+ # Split description into lines if it's too long, or just let it wrap conceptually (though paragraph wraps by character by default)
35
+ # Using simple inspect output as requested.
36
+ description = entry.description
37
+
38
+ # We want to display it over potentially multiple lines if needed, but the original code did manual 2-line formatting.
39
+ # Let's try to just dump the inspect string. If it's very long, it might be cut off.
40
+ # But the User asked specifically to use inspect.
41
+
42
+ log_lines << tui.text_line(spans: [tui.text_span(content: description, style: entry_style)])
43
+ log_lines << tui.text_line(spans: [tui.text_span(content: "", style: entry_style)]) # Spacer line to match previous 2-line rhythm? Or just compact?
44
+ # Previous view had 2 lines per entry. Let's keep a spacer to make it readable.
45
+ end
46
+ end
47
+
48
+ widget = tui.paragraph(
49
+ text: log_lines,
50
+ scroll: [0, 0],
51
+ wrap: { trim: true },
52
+ block: tui.block(
53
+ title: "Event Log",
54
+ borders: [:all],
55
+ border_style: tui.style(fg: state.border_color)
56
+ )
57
+ )
58
+ frame.render_widget(widget, area)
59
+ end
60
+ 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,42 @@
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
+ # Encapsulates all data required to render the application view.
7
+ #
8
+ # Views need access to models, global settings, and calculated styles.
9
+ # Passing dozens of individual parameters to view components is messy and unmaintainable.
10
+ #
11
+ # This class provides a single, structured object containing all state necessary for rendering.
12
+ #
13
+ # Use it to shuttle data from the main application to the various view components.
14
+ #
15
+ # === Examples
16
+ #
17
+ # state = ViewState.build(events, true, tui, nil)
18
+ # puts state.focused #=> true
19
+ # app_view.call(state, tui, frame, area)
20
+ class ViewState < Data.define(:events, :focused, :hotkey_style, :dimmed_style, :lit_style, :border_color, :area)
21
+ # Builds a new ViewState with calculated styles.
22
+ #
23
+ # [events] Events model instance.
24
+ # [focused] Boolean indicating if the app has focus.
25
+ # [tui] RatatuiRuby instance for style creation.
26
+ # [_resize_sub_counter] Unused parameter (reserved for future use).
27
+ #
28
+ # === Example
29
+ #
30
+ # ViewState.build(events, true, tui, nil) #=> #<ViewState ...>
31
+ def self.build(events, focused, tui, _resize_sub_counter)
32
+ new(
33
+ events:,
34
+ focused:,
35
+ hotkey_style: tui.style(modifiers: [:bold, :underlined]),
36
+ dimmed_style: tui.style(fg: :dark_gray),
37
+ lit_style: tui.style(fg: :green, modifiers: [:bold]),
38
+ border_color: focused ? :green : :gray,
39
+ area: nil
40
+ )
41
+ end
42
+ end
@@ -0,0 +1,94 @@
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, undo/redo prep, clipboard interaction)
12
+ - **Mouse Interaction & Hit Testing**
13
+ - **Dynamic Layouts**
14
+ - **Modal Dialogs**
15
+
16
+ ## Architecture: The "Scene-Orchestrated" Pattern
17
+
18
+ This app uses a pattern we call **"Scene-Orchestrated MVC"**.
19
+
20
+ ### 1. The App (Controller)
21
+ The main `App` class (`app.rb`) acts as the Controller. It:
22
+ - Holds the source of truth (the State).
23
+ - Runs the Event Loop.
24
+ - Routes input events to the appropriate handler.
25
+ - Initializes the `Scene`.
26
+
27
+ ### 2. The Scene (View / Layout Engine)
28
+ The `Scene` class (`scene.rb`) acts as the primary View. Unlike simple examples where the render logic is in the `App` class, here the **Scene owns the Layout**.
29
+ - **Composition**: It takes purely logical objects (`Palette`, `Input`) and decides how to present them.
30
+ - **Hit Testing**: Crucially, the Scene **caches layout rectangles** (like `@export_area_rect`) during the render pass so the Controller knows *where* things are to handle clicks later.
31
+
32
+ ### 3. The Logical Models
33
+ The application logic is broken down into small, testable Plain Old Ruby Objects (POROs) that know nothing about the TUI:
34
+ - **`Color`**: Handles hex parsing, contrast calculation, and transformations.
35
+ - **`Palette`**: Generates color harmonies.
36
+ - **`Input`**: Manages the text buffer and validation state.
37
+ - **`Clipboard`**: Wraps system commands.
38
+
39
+ This separation means your **business logic remains pure Ruby**, while the TUI layer focuses solely on presentation.
40
+
41
+ ## Key Features Showcased
42
+
43
+ ### 🖱️ Mouse Support & Hit Testing
44
+ See `Scene#export_rect` and `App#handle_main_input`.
45
+ The app detects clicks on specific UI elements. This handles the problem: *"How do I know which button the user clicked?"*
46
+ - **Solution**: The rendering layer (Scene) exposes the `Rect` of interactive areas. The event loop checks `rect.contains?(mouse_x, mouse_y)`.
47
+
48
+ ### 🔲 Modal Dialogs
49
+ See `CopyDialog`.
50
+ The app implements a modal overlay that intercepts input.
51
+ - **Pattern**: The `App` checks `if dialog.active?`. If true, it routes events *only* to the dialog, effectively "blocking" the main UI.
52
+
53
+ ### 🎨 Advanced Styling & Layout
54
+ - **Dynamic Constraints**: Layouts that adapt to content.
55
+ - **Visual Feedback**:
56
+ - Input fields turn red on error.
57
+ - Clipboard messages fade out over time (`Clipboard#tick`).
58
+ - Text colors automatically adjust for contrast (Black text on light backgrounds, White on dark).
59
+
60
+ ## Problem Solving: What you can learn
61
+
62
+ Read this example if you are trying to solve:
63
+ 1. **"How do I structure a larger app?"** -> Move render logic out of `App` and into a `Scene` or `View` class.
64
+ 2. **"How do I handle mouse clicks?"** -> Cache the `Rect` during render.
65
+ 3. **"How do I make a popup?"** -> Use a state flag (`active?`) to conditional render on top of everything else (z-ordering) and hijack the input loop.
66
+ 4. **"How do I validate input?"** -> Wrap strings in an `Input` object that tracks both keypresses and validation errors.
67
+
68
+ ## Usage
69
+
70
+ ```bash
71
+ ruby examples/app_color_picker/app.rb
72
+ ```
73
+
74
+ - Type a hex code (e.g., `#FF0055`) or color name (`cyan`).
75
+ - Press `Enter` to generate the palette.
76
+ - Click on the **Export Formats** box to copy the hex code.
77
+
78
+ ## Comparison: Choosing an Architecture
79
+
80
+ Complex applications require structured state habits. This Color Picker and the [App All Events](../app_all_events/README.md) example demonstrate two different approaches.
81
+
82
+ ### The Tool Approach (Color Picker)
83
+
84
+ Tools require interaction. Users click buttons and drag sliders. The Controller needs to know where components exist on screen. MVVM hides this layout data.
85
+
86
+ This example uses a "Scene" pattern. The View exposes layout rectangles. The Controller uses these rectangles to handle mouse clicks.
87
+
88
+ Use this pattern for forms, editors, and mouse-driven tools.
89
+
90
+ ### The Dashboard Approach (AppAllEvents)
91
+
92
+ Dashboards display data. They rarely require complex mouse interaction. Strict MVVM works best there. The View is a pure function. It accepts a `ViewState` and draws it. It ignores input. This simplifies testing.
93
+
94
+ Use that pattern for logs, monitors, and data viewers.
@@ -0,0 +1,112 @@
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 "input"
11
+ require_relative "palette"
12
+ require_relative "clipboard"
13
+ require_relative "copy_dialog"
14
+ require_relative "scene"
15
+
16
+ # A terminal-based color picker application.
17
+ #
18
+ # Terminal users often need to select colors for themes or UI components.
19
+ # Manually typing hex codes and guessing how they will look is slow and error-prone.
20
+ #
21
+ # This application solves the problem by providing an interactive interface. It parses hex strings,
22
+ # generates palettes, and displays them visually in the terminal.
23
+ #
24
+ # Use it to experiment with color combinations and quickly find the right hex codes.
25
+ #
26
+ # === Examples
27
+ #
28
+ # AppColorPicker.new.run
29
+ #
30
+ class AppColorPicker
31
+ # Creates a new <tt>AppColorPicker</tt> instance with a default palette and clipboard.
32
+ def initialize
33
+ @input = Input.new
34
+ @palette = Palette.new(@input.parse)
35
+ @clipboard = Clipboard.new
36
+ @dialog = CopyDialog.new(@clipboard)
37
+ @scene = nil
38
+ end
39
+
40
+ # Starts the terminal session and enters the main event loop.
41
+ #
42
+ # This method initializes the terminal, renders the initial scene, and polls for
43
+ # input until the user quits.
44
+ #
45
+ # === Example
46
+ #
47
+ # app = AppColorPicker.new
48
+ # app.run
49
+ #
50
+ def run
51
+ RatatuiRuby.run do |tui|
52
+ @scene = Scene.new(tui)
53
+ loop do
54
+ render(tui)
55
+ result = handle_input(tui)
56
+ break if result == :quit
57
+ end
58
+ end
59
+ end
60
+
61
+ private def render(tui)
62
+ @clipboard.tick
63
+ tui.draw do |frame|
64
+ @scene.render(frame, input: @input, palette: @palette, clipboard: @clipboard, dialog: @dialog)
65
+ end
66
+ end
67
+
68
+ private def handle_input(tui)
69
+ event = tui.poll_event
70
+ @input.clear_error unless @dialog.active?
71
+
72
+ if @dialog.active?
73
+ handle_dialog_input(event)
74
+ else
75
+ handle_main_input(event)
76
+ end
77
+ end
78
+
79
+ private def handle_dialog_input(event)
80
+ result = @dialog.handle_input(event)
81
+ case event
82
+ in { type: :key, code: "q" } | { type: :key, code: "esc" } | { type: :key, code: "c", modifiers: ["ctrl"] }
83
+ :quit
84
+ else
85
+ result
86
+ end
87
+ end
88
+
89
+ private def handle_main_input(event)
90
+ case event
91
+ in { type: :key, code: "q" } | { type: :key, code: "esc" } | { type: :key, code: "c", modifiers: ["ctrl"] }
92
+ :quit
93
+ in { type: :key, code: "enter" }
94
+ @palette = Palette.new(@input.parse)
95
+ in { type: :key, code: "backspace" }
96
+ @input.delete_char
97
+ in { type: :paste, content: }
98
+ @input.set(content)
99
+ @palette = Palette.new(@input.parse)
100
+ in { type: :key, code: code }
101
+ @input.append_char(code)
102
+ in { type: :mouse, kind: "down", button: "left", x:, y: }
103
+ if @scene && @scene.export_rect&.contains?(x, y) && @palette.main
104
+ @dialog.open(@palette.main.hex)
105
+ end
106
+ else
107
+ nil
108
+ end
109
+ end
110
+ end
111
+
112
+ 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