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,44 @@
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 RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for media keys.
10
+ module Media
11
+ # Returns true if this is a media key.
12
+ #
13
+ # Media keys include: play, pause, stop, track controls, volume controls.
14
+ # These are only available in terminals supporting the Kitty keyboard protocol.
15
+ #
16
+ # event.media? # => true for media_play, media_pause, etc.
17
+ def media?
18
+ @kind == :media
19
+ end
20
+
21
+ # Handles media-specific DWIM logic for method_missing.
22
+ private def match_media_dwim?(key_name)
23
+ return false unless @kind == :media
24
+
25
+ # Allow unprefixed predicate
26
+ # e.g., pause? returns true for media_pause
27
+ if @code.start_with?("media_")
28
+ base_code = @code.delete_prefix("media_")
29
+ return true if key_name == base_code
30
+ end
31
+
32
+ # Bidirectional media overlaps
33
+ # e.g., play? and pause? both match media_play_pause
34
+ return true if @code == "media_play_pause" && (key_name == "play" || key_name == "pause")
35
+
36
+ # e.g., play_pause? matches media_play or media_pause
37
+ return true if key_name == "play_pause" && (@code == "media_play" || @code == "media_pause")
38
+
39
+ false
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,95 @@
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 RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for modifier keys.
10
+ module Modifier
11
+ # Returns true if CTRL is held OR if this is a left_control/right_control key event.
12
+ def ctrl?
13
+ @modifiers.include?("ctrl") || @code == "left_control" || @code == "right_control"
14
+ end
15
+
16
+ # Alias for {#ctrl?}.
17
+ alias control? ctrl?
18
+
19
+ # Returns true if ALT is held OR if this is a left_alt/right_alt key event.
20
+ def alt?
21
+ @modifiers.include?("alt") || @code == "left_alt" || @code == "right_alt"
22
+ end
23
+
24
+ # Alias for {#alt?}.
25
+ alias option? alt?
26
+
27
+ # Returns true if SHIFT is held OR if this is a left_shift/right_shift key event.
28
+ def shift?
29
+ @modifiers.include?("shift") || @code == "left_shift" || @code == "right_shift"
30
+ end
31
+
32
+ # Returns true if SUPER is held OR if this is a left_super/right_super key event.
33
+ # Also responds to platform aliases: win?, command?, cmd?, tux?
34
+ def super?
35
+ @modifiers.include?("super") || @code == "left_super" || @code == "right_super"
36
+ end
37
+
38
+ # Alias for {#super?}.
39
+ alias win? super?
40
+ # Alias for {#super?}.
41
+ alias command? super?
42
+ # Alias for {#super?}.
43
+ alias cmd? super?
44
+ # Alias for {#super?}.
45
+ alias tux? super?
46
+
47
+ # Returns true if HYPER is held OR if this is a left_hyper/right_hyper key event.
48
+ def hyper?
49
+ @modifiers.include?("hyper") || @code == "left_hyper" || @code == "right_hyper"
50
+ end
51
+
52
+ # Returns true if META is held OR if this is a left_meta/right_meta key event.
53
+ def meta?
54
+ @modifiers.include?("meta") || @code == "left_meta" || @code == "right_meta"
55
+ end
56
+
57
+ # Returns true if this is a modifier key event.
58
+ #
59
+ # Some applications need to know if an event represents a generic key
60
+ # press or a specific modifier key (like CTRL or ALT) being pressed on
61
+ # its own.
62
+ #
63
+ # This method identifies if the key event itself is a modifier key.
64
+ #
65
+ # === Example
66
+ #
67
+ # if event.modifier?
68
+ # # Handle solo modifier key press
69
+ # end
70
+ def modifier?
71
+ @kind == :modifier
72
+ end
73
+
74
+ # Handles modifier-specific DWIM logic for method_missing.
75
+ private def match_modifier_dwim?(key_name, key_sym)
76
+ # Platform modifier aliases
77
+ modifier_aliases = {
78
+ win: "super",
79
+ command: "super",
80
+ cmd: "super",
81
+ tux: "super",
82
+ }.freeze
83
+
84
+ target_modifier = modifier_aliases[key_sym]
85
+ if target_modifier
86
+ return true if @modifiers.include?(target_modifier)
87
+ return true if @code == "left_#{target_modifier}" || @code == "right_#{target_modifier}"
88
+ end
89
+
90
+ false
91
+ end
92
+ end
93
+ end
94
+ end
95
+ 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
+ module RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for navigation keys.
10
+ module Navigation
11
+ # Returns true if this is a standard key.
12
+ #
13
+ # Standard keys include: characters, Enter, Tab, arrow keys, navigation keys.
14
+ #
15
+ # event.standard? # => true for "a", "enter", "up", etc.
16
+ def standard?
17
+ @kind == :standard
18
+ end
19
+
20
+ # Alias for {#standard?}.
21
+ #
22
+ # Provided for semantic clarity when checking if a key has no special category.
23
+ #
24
+ # event.unmodified? # => true for standard keys like "a", "enter", "up"
25
+ alias unmodified? standard?
26
+
27
+ # Handles navigation-specific DWIM logic for method_missing.
28
+ private def match_navigation_dwim?(key_name, key_sym)
29
+ # DWIM: reverse_tab? matches both BackTab key and Shift+Tab combo
30
+ if key_name == "reverse_tab"
31
+ return true if @code == "back_tab"
32
+ return true if @code == "tab" && @modifiers.include?("shift")
33
+ end
34
+
35
+ # DWIM: Check explicit aliases
36
+ navigation_aliases = {
37
+ return: "enter",
38
+ back: "backspace",
39
+ del: "delete",
40
+ ins: "insert",
41
+ pgup: "page_up",
42
+ pageup: "page_up",
43
+ pgdn: "page_down",
44
+ pagedown: "page_down",
45
+ }.freeze
46
+
47
+ target_code = navigation_aliases[key_sym]
48
+ return true if target_code && @code == target_code && @modifiers.empty?
49
+
50
+ false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
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 RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for system and function keys.
10
+ module System
11
+ # Returns true if this is a system key.
12
+ #
13
+ # System keys include: Esc, CapsLock, ScrollLock, NumLock, PrintScreen, Pause, Menu, KeypadBegin.
14
+ #
15
+ # event.system? # => true for pause, esc, caps_lock, etc.
16
+ def system?
17
+ @kind == :system
18
+ end
19
+
20
+ # Returns true if this is a function key (F1-F24).
21
+ #
22
+ # event.function? # => true for f1, f2, ..., f24
23
+ def function?
24
+ @kind == :function
25
+ end
26
+
27
+ # Handles system-specific DWIM logic for method_missing.
28
+ private def match_system_dwim?(key_name, key_sym)
29
+ system_aliases = {
30
+ scrlk: "scroll_lock",
31
+ scroll: "scroll_lock",
32
+ prtsc: "print_screen",
33
+ print: "print_screen",
34
+ escape: "esc",
35
+ }.freeze
36
+
37
+ target_code = system_aliases[key_sym]
38
+ return true if target_code && @code == target_code && @modifiers.empty?
39
+
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -3,6 +3,12 @@
3
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
+ require_relative "key/character"
7
+ require_relative "key/media"
8
+ require_relative "key/modifier"
9
+ require_relative "key/navigation"
10
+ require_relative "key/system"
11
+
6
12
  module RatatuiRuby
7
13
  class Event
8
14
  # Captures a keyboard interaction.
@@ -33,7 +39,34 @@ module RatatuiRuby
33
39
  # in type: :key, code: "c", modifiers: ["ctrl"]
34
40
  # exit
35
41
  # end
42
+ #
43
+ # === Terminal Compatibility
44
+ #
45
+ # Some key combinations never reach your application. Terminal emulators intercept them for
46
+ # built-in features like tab switching. Common culprits:
47
+ #
48
+ # * Ctrl+PageUp/PageDown (tab switching in Terminal.app, iTerm2)
49
+ # * Ctrl+Tab (tab switching)
50
+ # * Cmd+key combinations (macOS system shortcuts)
51
+ #
52
+ # If modifiers appear missing, test with a different terminal. Kitty, WezTerm, and Alacritty
53
+ # pass more keys through. See <tt>doc/terminal_limitations.md</tt> for details.
54
+ #
55
+ # === Enhanced Keys (Kitty Protocol)
56
+ #
57
+ # Terminals supporting the Kitty keyboard protocol report additional keys:
58
+ #
59
+ # * Media keys: <tt>:play</tt>, <tt>:play_pause</tt>, <tt>:track_next</tt>, <tt>:mute_volume</tt>
60
+ # * Individual modifiers: <tt>:left_shift</tt>, <tt>:right_control</tt>, <tt>:left_super</tt>
61
+ #
62
+ # These keys will not work in Terminal.app, iTerm2, or GNOME Terminal.
36
63
  class Key < Event
64
+ include Character
65
+ include Media
66
+ include Modifier
67
+ include Navigation
68
+ include System
69
+
37
70
  # The key code (e.g., <tt>"a"</tt>, <tt>"enter"</tt>, <tt>"up"</tt>).
38
71
  #
39
72
  # puts event.code # => "enter"
@@ -44,6 +77,15 @@ module RatatuiRuby
44
77
  # puts event.modifiers # => ["ctrl", "shift"]
45
78
  attr_reader :modifiers
46
79
 
80
+ # The category of the key.
81
+ #
82
+ # One of: <tt>:standard</tt>, <tt>:function</tt>, <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>.
83
+ #
84
+ # This allows grouping keys by their logical type without parsing the code string.
85
+ #
86
+ # event.kind # => :media
87
+ attr_reader :kind
88
+
47
89
  # Returns true for Key events.
48
90
  #
49
91
  # event.key? # => true
@@ -59,17 +101,21 @@ module RatatuiRuby
59
101
  # The key code (String).
60
102
  # [modifiers]
61
103
  # List of modifiers (Array<String>).
62
- def initialize(code:, modifiers: [])
63
- @code = code
64
- @modifiers = modifiers.sort
104
+ # [kind]
105
+ # The key category (Symbol). One of: <tt>:standard</tt>, <tt>:function</tt>,
106
+ # <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>. Defaults to <tt>:standard</tt>.
107
+ def initialize(code:, modifiers: [], kind: :standard)
108
+ @code = code.freeze
109
+ @modifiers = modifiers.map(&:freeze).sort.freeze
110
+ @kind = kind
65
111
  end
66
112
 
67
113
  # Compares the event with another object.
68
114
  #
69
115
  # - If +other+ is a +Symbol+, compares against #to_sym.
70
116
  # - If +other+ is a +String+, compares against #to_s.
71
- # - Otherwise, performs standard equality check.
72
- # - Otherwise, compares internal state (code + modifiers).
117
+ # - If +other+ is a +Key+, compares as a value object.
118
+ # - Otherwise, compares using standard equality.
73
119
  def ==(other)
74
120
  case other
75
121
  when Symbol
@@ -91,9 +137,25 @@ module RatatuiRuby
91
137
  # === Supported Keys
92
138
  #
93
139
  # [Standard]
94
- # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:esc</tt>, <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:home</tt>, <tt>:end</tt>, <tt>:delete</tt>, <tt>:insert</tt>, <tt>:f1</tt>..<tt>:f12</tt>
140
+ # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:back_tab</tt>, <tt>:esc</tt>, <tt>:null</tt>
95
141
  # [Navigation]
96
- # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>
142
+ # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>, <tt>:home</tt>, <tt>:end</tt>,
143
+ # <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:insert</tt>, <tt>:delete</tt>
144
+ # [Function Keys]
145
+ # <tt>:f1</tt> through <tt>:f12</tt> (and beyond, e.g. <tt>:f24</tt>)
146
+ # [Lock Keys]
147
+ # <tt>:caps_lock</tt>, <tt>:scroll_lock</tt>, <tt>:num_lock</tt>
148
+ # [System Keys]
149
+ # <tt>:print_screen</tt>, <tt>:pause</tt>, <tt>:menu</tt>, <tt>:keypad_begin</tt>
150
+ # [Media Keys]
151
+ # <tt>:play</tt>, <tt>:media_pause</tt>, <tt>:play_pause</tt>, <tt>:reverse</tt>, <tt>:stop</tt>,
152
+ # <tt>:fast_forward</tt>, <tt>:rewind</tt>, <tt>:track_next</tt>, <tt>:track_previous</tt>,
153
+ # <tt>:record</tt>, <tt>:lower_volume</tt>, <tt>:raise_volume</tt>, <tt>:mute_volume</tt>
154
+ # [Modifier Keys]
155
+ # <tt>:left_shift</tt>, <tt>:left_control</tt>, <tt>:left_alt</tt>, <tt>:left_super</tt>,
156
+ # <tt>:left_hyper</tt>, <tt>:left_meta</tt>, <tt>:right_shift</tt>, <tt>:right_control</tt>,
157
+ # <tt>:right_alt</tt>, <tt>:right_super</tt>, <tt>:right_hyper</tt>, <tt>:right_meta</tt>,
158
+ # <tt>:iso_level3_shift</tt>, <tt>:iso_level5_shift</tt>
97
159
  # [Characters]
98
160
  # <tt>:a</tt>, <tt>:b</tt>, <tt>:1</tt>, <tt>:space</tt>, etc.
99
161
  #
@@ -108,7 +170,7 @@ module RatatuiRuby
108
170
  if mods.empty?
109
171
  @code.to_sym
110
172
  else
111
- "#{mods}_#{@code}".to_sym
173
+ :"#{mods}_#{@code}"
112
174
  end
113
175
  end
114
176
 
@@ -131,46 +193,7 @@ module RatatuiRuby
131
193
 
132
194
  # Returns inspection string.
133
195
  def inspect
134
- "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect}>"
135
- end
136
-
137
- # Returns true if CTRL is held.
138
- def ctrl?
139
- @modifiers.include?("ctrl")
140
- end
141
-
142
- # Returns true if ALT is held.
143
- def alt?
144
- @modifiers.include?("alt")
145
- end
146
-
147
- # Returns true if SHIFT is held.
148
- def shift?
149
- @modifiers.include?("shift")
150
- end
151
-
152
- # Returns true if the key represents a single printable character.
153
- #
154
- # RatatuiRuby::Event::Key.new(code: "a").text? # => true
155
- # RatatuiRuby::Event::Key.new(code: "enter").text? # => false
156
- # RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
157
- def text?
158
- @code.length == 1
159
- end
160
-
161
- # Returns the key as a printable character (if applicable).
162
- #
163
- # [Printable Characters]
164
- # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
165
- # [Special Keys]
166
- # Returns an empty string (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
167
- #
168
- # This is equivalent to +to_s+.
169
- #
170
- # RatatuiRuby::Event::Key.new(code: "a").char # => "a"
171
- # RatatuiRuby::Event::Key.new(code: "enter").char # => ""
172
- def char
173
- to_s
196
+ "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect} kind=#{@kind.inspect}>"
174
197
  end
175
198
 
176
199
  # Supports dynamic key predicate methods via method_missing.
@@ -184,12 +207,47 @@ module RatatuiRuby
184
207
  #
185
208
  # The method name is converted to a symbol and compared against the event.
186
209
  # This works for any key code or modifier+key combination.
210
+ #
211
+ # === Smart Predicates (DWIM)
212
+ #
213
+ # For convenience, generic predicates match both system and media variants:
214
+ #
215
+ # event.pause? # => true for BOTH system "pause" AND "media_pause"
216
+ # event.play? # => true for "media_play"
217
+ # event.stop? # => true for "media_stop"
218
+ #
219
+ # This "Do What I Mean" behavior reduces boilerplate when you just want to
220
+ # respond to a conceptual action (e.g., "pause the playback") regardless of
221
+ # whether the user pressed a keyboard key or a media button.
222
+ #
223
+ # For strict matching, use the full predicate or compare the code directly:
224
+ #
225
+ # event.media_pause? # => true ONLY for media pause
226
+ # event.code == "pause" # => true ONLY for system pause
187
227
  def method_missing(name, *args, &block)
188
228
  if name.to_s.end_with?("?")
189
- key_sym = name.to_s[0...-1].to_sym
190
- return self == key_sym
229
+ key_name = name.to_s[0...-1]
230
+ key_sym = key_name.to_sym
231
+
232
+ # Fast path: Exact match (e.g., media_pause? for media_pause)
233
+ return true if self == key_sym
234
+
235
+ # Delegate category-specific DWIM logic to mixins
236
+ return true if match_media_dwim?(key_name)
237
+ return true if match_modifier_dwim?(key_name, key_sym)
238
+ return true if match_navigation_dwim?(key_name, key_sym)
239
+ return true if match_system_dwim?(key_name, key_sym)
240
+
241
+ # DWIM: Universal underscore-insensitivity
242
+ # Normalize both predicate and code by stripping underscores
243
+ normalized_predicate = key_name.delete("_")
244
+ normalized_code = @code.delete("_")
245
+ return true if normalized_predicate == normalized_code && @modifiers.empty?
246
+
247
+ false
248
+ else
249
+ super
191
250
  end
192
- super
193
251
  end
194
252
 
195
253
  # Declares that this class responds to dynamic predicate methods.
@@ -202,9 +260,11 @@ module RatatuiRuby
202
260
  # case event
203
261
  # in type: :key, code: "c", modifiers: ["ctrl"]
204
262
  # puts "Ctrl+C pressed"
263
+ # in type: :key, kind: :media
264
+ # puts "Media key pressed"
205
265
  # end
206
266
  def deconstruct_keys(keys)
207
- { type: :key, code: @code, modifiers: @modifiers }
267
+ { type: :key, code: @code, modifiers: @modifiers, kind: @kind }
208
268
  end
209
269
  end
210
270
  end
@@ -67,11 +67,11 @@ module RatatuiRuby
67
67
  # [modifiers]
68
68
  # List of modifiers (Array<String>).
69
69
  def initialize(kind:, x:, y:, button:, modifiers: [])
70
- @kind = kind
70
+ @kind = kind.freeze
71
71
  @x = x
72
72
  @y = y
73
- @button = button || "none"
74
- @modifiers = modifiers.sort
73
+ @button = (button || "none").freeze
74
+ @modifiers = modifiers.map(&:freeze).sort.freeze
75
75
  end
76
76
 
77
77
  # Returns true if mouse button was pressed down.
@@ -0,0 +1,43 @@
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 RatatuiRuby
7
+ class Event
8
+ # {Null object}[https://en.wikipedia.org/wiki/Null_object_pattern] for absent events.
9
+ #
10
+ # Event loops poll for input 60 times per second. Usually nothing is happening.
11
+ # If <tt>RatatuiRuby.poll_event</tt> returned <tt>nil</tt>, you would need
12
+ # nil-checks: <tt>event&.key?</tt>, <tt>next unless event</tt>.
13
+ #
14
+ # This class eliminates that friction. It responds to every predicate with
15
+ # <tt>false</tt>. Call <tt>none?</tt> to detect it explicitly. Pattern-match on
16
+ # <tt>type: :none</tt> for exhaustive dispatch.
17
+ #
18
+ # Use it to simplify your event loop. No guards. Optional `else` clauses.
19
+ #
20
+ # See Martin Fowler's {Special Case}[https://martinfowler.com/eaaCatalog/specialCase.html] pattern.
21
+ #
22
+ # === Predicate Example
23
+ #
24
+ # event = RatatuiRuby.poll_event
25
+ # break if event.ctrl_c?
26
+ # redraw if event.none?
27
+ #
28
+ # === Pattern Matching Example
29
+ #
30
+ # redraw if RatatuiRuby.poll_event in type: :none
31
+ class None < Event
32
+ # Returns true for None events.
33
+ def none?
34
+ true
35
+ end
36
+
37
+ # Deconstructs the event for pattern matching.
38
+ def deconstruct_keys(keys)
39
+ { type: :none }
40
+ end
41
+ end
42
+ end
43
+ end
@@ -47,7 +47,7 @@ module RatatuiRuby
47
47
  # [content]
48
48
  # Pasted text (String).
49
49
  def initialize(content:)
50
- @content = content
50
+ @content = content.freeze
51
51
  end
52
52
 
53
53
  # Deconstructs the event for pattern matching.
@@ -6,11 +6,63 @@
6
6
  module RatatuiRuby
7
7
  # Base class for all RatatuiRuby events.
8
8
  #
9
- # Events are returned by RatatuiRuby.poll_event.
10
- # All events support Ruby 3.0+ pattern matching via #deconstruct_keys.
9
+ # Events represent terminal input: keyboard, mouse, resize, paste, focus changes.
10
+ # Returned by RatatuiRuby.poll_event. All events support Ruby 3.0+ pattern matching.
11
11
  #
12
- # See RatatuiRuby.poll_event
12
+ # == Event Types
13
+ #
14
+ # * <tt>Key</tt> — keyboard input
15
+ # * <tt>Mouse</tt> — mouse clicks, movement, wheel
16
+ # * <tt>Resize</tt> — terminal resized
17
+ # * <tt>Paste</tt> — clipboard paste
18
+ # * <tt>FocusGained</tt> — terminal gained focus
19
+ # * <tt>FocusLost</tt> — terminal lost focus
20
+ # * <tt>None</tt> — no event available (Null Object)
21
+ #
22
+ # == Pattern Matching (Exhaustive)
23
+ #
24
+ # Use <tt>case...in</tt> to dispatch on every possible event type. This ensures
25
+ # you handle every case without needing an +else+ clause:
26
+ #
27
+ # case RatatuiRuby.poll_event
28
+ # in { type: :key, code: "q" }
29
+ # break
30
+ # in { type: :key, code: code, modifiers: }
31
+ # handle_key(code, modifiers)
32
+ # in { type: :mouse, kind: "down", x:, y: }
33
+ # handle_click(x, y)
34
+ # in { type: :mouse, kind:, x:, y: }
35
+ # # handle other mouse activities
36
+ # in { type: :resize, width:, height: }
37
+ # handle_resize(width, height)
38
+ # in { type: :paste, content: }
39
+ # handle_paste(content)
40
+ # in { type: :focus_gained }
41
+ # handle_focus_gain
42
+ # in { type: :focus_lost }
43
+ # handle_focus_loss
44
+ # in { type: :none }
45
+ # # Idle
46
+ # end
47
+ #
48
+ # == Predicates
49
+ #
50
+ # Check event types with predicates without pattern matching:
51
+ #
52
+ # event = RatatuiRuby.poll_event
53
+ # if event.key?
54
+ # puts "Key pressed"
55
+ # elsif event.none?
56
+ # # Idle
57
+ # elsif event.mouse?
58
+ # puts "Mouse event"
59
+ # end
13
60
  class Event
61
+ # Returns true if this is a None event.
62
+ def none?
63
+ false
64
+ end
65
+
14
66
  # Returns true if this is a Key event.
15
67
  def key?
16
68
  false
@@ -67,10 +119,10 @@ module RatatuiRuby
67
119
  def deconstruct_keys(keys)
68
120
  {}
69
121
  end
70
-
71
122
  end
72
123
  end
73
124
 
125
+ require_relative "event/none"
74
126
  require_relative "event/key"
75
127
  require_relative "event/mouse"
76
128
  require_relative "event/resize"