ratatui_ruby 1.4.0-x86_64-linux

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 (292) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +15 -0
  3. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  4. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  5. data/LICENSES/CC0-1.0.txt +121 -0
  6. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  7. data/LICENSES/MIT-0.txt +16 -0
  8. data/LICENSES/MIT.txt +21 -0
  9. data/REUSE.toml +42 -0
  10. data/exe/.gitkeep +0 -0
  11. data/ext/ratatui_ruby/.cargo/config.toml +13 -0
  12. data/ext/ratatui_ruby/.gitignore +4 -0
  13. data/ext/ratatui_ruby/Cargo.lock +1737 -0
  14. data/ext/ratatui_ruby/Cargo.toml +24 -0
  15. data/ext/ratatui_ruby/clippy.toml +7 -0
  16. data/ext/ratatui_ruby/extconf.rb +21 -0
  17. data/ext/ratatui_ruby/src/color.rs +82 -0
  18. data/ext/ratatui_ruby/src/errors.rs +28 -0
  19. data/ext/ratatui_ruby/src/events.rs +700 -0
  20. data/ext/ratatui_ruby/src/frame.rs +241 -0
  21. data/ext/ratatui_ruby/src/lib.rs +343 -0
  22. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  23. data/ext/ratatui_ruby/src/rendering.rs +158 -0
  24. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  25. data/ext/ratatui_ruby/src/style.rs +469 -0
  26. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  27. data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
  28. data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
  29. data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
  30. data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
  31. data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
  32. data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
  33. data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
  34. data/ext/ratatui_ruby/src/text.rs +225 -0
  35. data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
  36. data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
  37. data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
  38. data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
  39. data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
  40. data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
  41. data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
  42. data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
  43. data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
  44. data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
  45. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
  46. data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
  47. data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
  48. data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
  49. data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
  50. data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
  51. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  52. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
  53. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
  54. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  55. data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
  56. data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
  57. data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
  58. data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
  59. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  60. data/lib/ratatui_ruby/backend.rb +59 -0
  61. data/lib/ratatui_ruby/buffer/cell.rb +212 -0
  62. data/lib/ratatui_ruby/buffer.rb +149 -0
  63. data/lib/ratatui_ruby/cell.rb +208 -0
  64. data/lib/ratatui_ruby/debug.rb +215 -0
  65. data/lib/ratatui_ruby/draw.rb +63 -0
  66. data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
  67. data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
  68. data/lib/ratatui_ruby/event/key/character.rb +53 -0
  69. data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
  70. data/lib/ratatui_ruby/event/key/media.rb +46 -0
  71. data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
  72. data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
  73. data/lib/ratatui_ruby/event/key/system.rb +47 -0
  74. data/lib/ratatui_ruby/event/key.rb +479 -0
  75. data/lib/ratatui_ruby/event/mouse.rb +291 -0
  76. data/lib/ratatui_ruby/event/none.rb +53 -0
  77. data/lib/ratatui_ruby/event/paste.rb +130 -0
  78. data/lib/ratatui_ruby/event/resize.rb +221 -0
  79. data/lib/ratatui_ruby/event/sync.rb +52 -0
  80. data/lib/ratatui_ruby/event.rb +163 -0
  81. data/lib/ratatui_ruby/frame.rb +257 -0
  82. data/lib/ratatui_ruby/labs/a11y.rb +182 -0
  83. data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
  84. data/lib/ratatui_ruby/labs.rb +47 -0
  85. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  86. data/lib/ratatui_ruby/layout/constraint.rb +337 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +258 -0
  88. data/lib/ratatui_ruby/layout/position.rb +81 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +733 -0
  90. data/lib/ratatui_ruby/layout/size.rb +62 -0
  91. data/lib/ratatui_ruby/layout.rb +29 -0
  92. data/lib/ratatui_ruby/list_state.rb +201 -0
  93. data/lib/ratatui_ruby/output_guard.rb +171 -0
  94. data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
  95. data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
  96. data/lib/ratatui_ruby/style/color.rb +149 -0
  97. data/lib/ratatui_ruby/style/style.rb +147 -0
  98. data/lib/ratatui_ruby/style.rb +19 -0
  99. data/lib/ratatui_ruby/symbols.rb +435 -0
  100. data/lib/ratatui_ruby/synthetic_events.rb +106 -0
  101. data/lib/ratatui_ruby/table_state.rb +251 -0
  102. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  103. data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
  104. data/lib/ratatui_ruby/terminal.rb +66 -0
  105. data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
  106. data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
  107. data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
  108. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  109. data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
  110. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  111. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  112. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  113. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  114. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  115. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  116. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  117. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  118. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  119. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  120. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  121. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  122. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  123. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  124. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  125. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  126. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  127. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  128. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  129. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  130. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  131. data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
  132. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  133. data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
  134. data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
  135. data/lib/ratatui_ruby/test_helper.rb +115 -0
  136. data/lib/ratatui_ruby/text/line.rb +245 -0
  137. data/lib/ratatui_ruby/text/span.rb +158 -0
  138. data/lib/ratatui_ruby/text.rb +99 -0
  139. data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
  140. data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
  141. data/lib/ratatui_ruby/tui/core.rb +67 -0
  142. data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
  143. data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
  144. data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
  145. data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
  146. data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
  147. data/lib/ratatui_ruby/tui.rb +106 -0
  148. data/lib/ratatui_ruby/version.rb +12 -0
  149. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
  150. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
  151. data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
  152. data/lib/ratatui_ruby/widgets/block.rb +266 -0
  153. data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
  154. data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
  155. data/lib/ratatui_ruby/widgets/cell.rb +59 -0
  156. data/lib/ratatui_ruby/widgets/center.rb +71 -0
  157. data/lib/ratatui_ruby/widgets/chart.rb +172 -0
  158. data/lib/ratatui_ruby/widgets/clear.rb +66 -0
  159. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  160. data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
  161. data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
  162. data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
  163. data/lib/ratatui_ruby/widgets/list.rb +252 -0
  164. data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
  165. data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
  166. data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
  167. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
  168. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
  169. data/lib/ratatui_ruby/widgets/row.rb +123 -0
  170. data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
  171. data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
  172. data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
  173. data/lib/ratatui_ruby/widgets/table.rb +213 -0
  174. data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
  175. data/lib/ratatui_ruby/widgets.rb +43 -0
  176. data/lib/ratatui_ruby.rb +555 -0
  177. data/sig/examples/app_all_events/app.rbs +11 -0
  178. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  179. data/sig/examples/app_all_events/model/event_entry.rbs +23 -0
  180. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  181. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  182. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  183. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  184. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  185. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  186. data/sig/examples/app_all_events/view.rbs +14 -0
  187. data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
  188. data/sig/examples/app_color_picker/app.rbs +17 -0
  189. data/sig/examples/app_external_editor/app.rbs +12 -0
  190. data/sig/examples/app_login_form/app.rbs +11 -0
  191. data/sig/examples/app_stateful_interaction/app.rbs +39 -0
  192. data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
  193. data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
  194. data/sig/examples/verify_readme_usage/app.rbs +17 -0
  195. data/sig/examples/widget_block_demo/app.rbs +38 -0
  196. data/sig/examples/widget_box_demo/app.rbs +17 -0
  197. data/sig/examples/widget_calendar_demo/app.rbs +17 -0
  198. data/sig/examples/widget_cell_demo/app.rbs +17 -0
  199. data/sig/examples/widget_chart_demo/app.rbs +17 -0
  200. data/sig/examples/widget_gauge_demo/app.rbs +17 -0
  201. data/sig/examples/widget_layout_split/app.rbs +16 -0
  202. data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
  203. data/sig/examples/widget_list_demo/app.rbs +17 -0
  204. data/sig/examples/widget_map_demo/app.rbs +17 -0
  205. data/sig/examples/widget_popup_demo/app.rbs +17 -0
  206. data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
  207. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
  208. data/sig/examples/widget_rect/app.rbs +18 -0
  209. data/sig/examples/widget_render/app.rbs +16 -0
  210. data/sig/examples/widget_rich_text/app.rbs +17 -0
  211. data/sig/examples/widget_scroll_text/app.rbs +17 -0
  212. data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
  213. data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
  214. data/sig/examples/widget_style_colors/app.rbs +20 -0
  215. data/sig/examples/widget_table_demo/app.rbs +17 -0
  216. data/sig/examples/widget_text_width/app.rbs +16 -0
  217. data/sig/generated/event_key_predicates.rbs +1348 -0
  218. data/sig/manifest.yaml +5 -0
  219. data/sig/patches/data.rbs +26 -0
  220. data/sig/patches/debugger__.rbs +8 -0
  221. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  222. data/sig/ratatui_ruby/backend.rbs +12 -0
  223. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  224. data/sig/ratatui_ruby/buffer.rbs +18 -0
  225. data/sig/ratatui_ruby/cell.rbs +44 -0
  226. data/sig/ratatui_ruby/clear.rbs +18 -0
  227. data/sig/ratatui_ruby/constraint.rbs +26 -0
  228. data/sig/ratatui_ruby/debug.rbs +45 -0
  229. data/sig/ratatui_ruby/draw.rbs +30 -0
  230. data/sig/ratatui_ruby/event.rbs +249 -0
  231. data/sig/ratatui_ruby/frame.rbs +23 -0
  232. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  233. data/sig/ratatui_ruby/labs.rbs +90 -0
  234. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  235. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  236. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  237. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  238. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  239. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  240. data/sig/ratatui_ruby/list_state.rbs +23 -0
  241. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  242. data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
  243. data/sig/ratatui_ruby/rect.rbs +17 -0
  244. data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
  245. data/sig/ratatui_ruby/session.rbs +93 -0
  246. data/sig/ratatui_ruby/style/color.rbs +22 -0
  247. data/sig/ratatui_ruby/style/style.rbs +29 -0
  248. data/sig/ratatui_ruby/symbols.rbs +141 -0
  249. data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
  250. data/sig/ratatui_ruby/table_state.rbs +27 -0
  251. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  252. data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
  253. data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
  254. data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
  255. data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
  256. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
  257. data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
  258. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
  259. data/sig/ratatui_ruby/test_helper.rbs +18 -0
  260. data/sig/ratatui_ruby/text/line.rbs +27 -0
  261. data/sig/ratatui_ruby/text/span.rbs +23 -0
  262. data/sig/ratatui_ruby/text.rbs +12 -0
  263. data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
  264. data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
  265. data/sig/ratatui_ruby/tui/core.rbs +23 -0
  266. data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
  267. data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
  268. data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
  269. data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
  270. data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
  271. data/sig/ratatui_ruby/tui.rbs +25 -0
  272. data/sig/ratatui_ruby/version.rbs +12 -0
  273. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  274. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  275. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  276. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  277. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  278. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  279. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  280. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  281. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  282. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  283. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  284. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  285. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  286. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  287. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  288. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  289. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  290. data/sig/ratatui_ruby/widgets.rbs +16 -0
  291. data/vendor/goodcop/base.yml +1047 -0
  292. metadata +729 -0
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ class Event
10
+ class Key < Event
11
+ # Methods and logic for system and function keys.
12
+ module System
13
+ # Returns true if this is a system key.
14
+ #
15
+ # System keys include: Esc, CapsLock, ScrollLock, NumLock, PrintScreen, Pause, Menu, KeypadBegin.
16
+ #
17
+ # event.system? # => true for pause, esc, caps_lock, etc.
18
+ def system?
19
+ @kind == :system
20
+ end
21
+
22
+ # Returns true if this is a function key (F1-F24).
23
+ #
24
+ # event.function? # => true for f1, f2, ..., f24
25
+ def function?
26
+ @kind == :function
27
+ end
28
+
29
+ # Handles system-specific DWIM logic for method_missing.
30
+ private def match_system_dwim?(key_name, key_sym)
31
+ system_aliases = {
32
+ scrlk: "scroll_lock",
33
+ scroll: "scroll_lock",
34
+ prtsc: "print_screen",
35
+ print: "print_screen",
36
+ escape: "esc",
37
+ }.freeze
38
+
39
+ target_code = system_aliases[key_sym]
40
+ return true if target_code && @code == target_code && @modifiers.empty?
41
+
42
+ false
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,479 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ require_relative "key/character"
9
+ require_relative "key/dwim"
10
+ require_relative "key/media"
11
+ require_relative "key/modifier"
12
+ require_relative "key/navigation"
13
+ require_relative "key/system"
14
+
15
+ module RatatuiRuby
16
+ class Event
17
+ # Captures a keyboard interaction.
18
+ #
19
+ # The keyboard is the primary interface for your terminal application. Raw key codes are often cryptic,
20
+ # and handling modifiers manually is error-prone.
21
+ #
22
+ # This event creates clarity. It encapsulates the interaction, providing a normalized +code+ and
23
+ # a list of active +modifiers+.
24
+ #
25
+ # Compare it directly to strings or symbols for rapid development, or use pattern matching for
26
+ # complex control schemes.
27
+ #
28
+ # === Examples
29
+ #
30
+ # Using predicates:
31
+ #--
32
+ # SPDX-SnippetBegin
33
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
34
+ # SPDX-License-Identifier: MIT-0
35
+ #++
36
+ # if event.key? && event.ctrl? && event.code == "c"
37
+ # exit
38
+ # end
39
+ #
40
+ #--
41
+ # SPDX-SnippetEnd
42
+ #++
43
+ # Using symbol comparison:
44
+ #--
45
+ # SPDX-SnippetBegin
46
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
47
+ # SPDX-License-Identifier: MIT-0
48
+ #++
49
+ # if event == :ctrl_c
50
+ # exit
51
+ # end
52
+ #
53
+ #--
54
+ # SPDX-SnippetEnd
55
+ #++
56
+ # Using pattern matching:
57
+ #--
58
+ # SPDX-SnippetBegin
59
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
60
+ # SPDX-License-Identifier: MIT-0
61
+ #++
62
+ # case event
63
+ # in type: :key, code: "c", modifiers: ["ctrl"]
64
+ # exit
65
+ # end
66
+ #
67
+ #--
68
+ # SPDX-SnippetEnd
69
+ #++
70
+ # === Terminal Compatibility
71
+ #
72
+ # Some key combinations never reach your application. Terminal emulators intercept them for
73
+ # built-in features like tab switching. Common culprits:
74
+ #
75
+ # * Ctrl+PageUp/PageDown (tab switching in Terminal.app, iTerm2)
76
+ # * Ctrl+Tab (tab switching)
77
+ # * Cmd+key combinations (macOS system shortcuts)
78
+ #
79
+ # If modifiers appear missing, test with a different terminal. Kitty, WezTerm, and Alacritty
80
+ # pass more keys through. See <tt>doc/terminal_limitations.md</tt> for details.
81
+ #
82
+ # === Enhanced Keys (Kitty Protocol)
83
+ #
84
+ # Terminals supporting the Kitty keyboard protocol report additional keys:
85
+ #
86
+ # * Media keys: <tt>:play</tt>, <tt>:play_pause</tt>, <tt>:track_next</tt>, <tt>:mute_volume</tt>
87
+ # * Individual modifiers: <tt>:left_shift</tt>, <tt>:right_control</tt>, <tt>:left_super</tt>
88
+ #
89
+ # These keys will not work in Terminal.app, iTerm2, or GNOME Terminal.
90
+ class Key < Event
91
+ include Character
92
+ include Dwim
93
+ include Media
94
+ include Modifier
95
+ include Navigation
96
+ include System
97
+
98
+ # The key code (e.g., <tt>"a"</tt>, <tt>"enter"</tt>, <tt>"up"</tt>).
99
+ #
100
+ # puts event.code # => "enter"
101
+ attr_reader :code
102
+
103
+ # List of active modifiers (<tt>"ctrl"</tt>, <tt>"alt"</tt>, <tt>"shift"</tt>).
104
+ #
105
+ # puts event.modifiers # => ["ctrl", "shift"]
106
+ attr_reader :modifiers
107
+
108
+ # The category of the key.
109
+ #
110
+ # One of: <tt>:standard</tt>, <tt>:function</tt>, <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>.
111
+ #
112
+ # This allows grouping keys by their logical type without parsing the code string.
113
+ #
114
+ # event.kind # => :media
115
+ attr_reader :kind
116
+
117
+ # Returns true for Key events.
118
+ #
119
+ #--
120
+ # SPDX-SnippetBegin
121
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
122
+ # SPDX-License-Identifier: MIT-0
123
+ #++
124
+ # event.key? # => true
125
+ # event.mouse? # => false
126
+ # event.resize? # => false
127
+ #--
128
+ # SPDX-SnippetEnd
129
+ #++
130
+ def key?
131
+ true
132
+ end
133
+
134
+ # Creates a new Key event.
135
+ #
136
+ # [code]
137
+ # The key code (String).
138
+ # [modifiers]
139
+ # List of modifiers (Array<String>).
140
+ # [kind]
141
+ #--
142
+ # SPDX-SnippetBegin
143
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
144
+ # SPDX-License-Identifier: MIT-0
145
+ #++
146
+ # The key category (Symbol). One of: <tt>:standard</tt>, <tt>:function</tt>,
147
+ # <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>. Defaults to <tt>:standard</tt>.
148
+ #--
149
+ # SPDX-SnippetEnd
150
+ #++
151
+ def initialize(code:, modifiers: [], kind: :standard)
152
+ @code = code.freeze
153
+ @modifiers = modifiers.map(&:freeze).sort.freeze
154
+ @kind = kind
155
+ end
156
+
157
+ # Compares the event with another object.
158
+ #
159
+ # - If +other+ is a +Symbol+, compares against #to_sym.
160
+ # - If +other+ is a +String+, compares against #to_s.
161
+ # - If +other+ is a +Key+, compares as a value object.
162
+ # - Otherwise, compares using standard equality.
163
+ def ==(other)
164
+ case other
165
+ when Symbol
166
+ to_sym == other
167
+ when String
168
+ to_s == other
169
+ when Key
170
+ code == other.code && modifiers == other.modifiers
171
+ else
172
+ super
173
+ end
174
+ end
175
+
176
+ # Converts the event to a Symbol representation.
177
+ #
178
+ # The format is <tt>[modifiers_]code</tt>. Modifiers are sorted alphabetically (alt, ctrl, shift)
179
+ # and joined by underscores.
180
+ #
181
+ # === Supported Keys
182
+ #
183
+ # [Standard]
184
+ # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:back_tab</tt>, <tt>:esc</tt>, <tt>:null</tt>
185
+ # [Navigation]
186
+ #--
187
+ # SPDX-SnippetBegin
188
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
189
+ # SPDX-License-Identifier: MIT-0
190
+ #++
191
+ # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>, <tt>:home</tt>, <tt>:end</tt>,
192
+ # <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:insert</tt>, <tt>:delete</tt>
193
+ #--
194
+ # SPDX-SnippetEnd
195
+ #++
196
+ # [Function Keys]
197
+ # <tt>:f1</tt> through <tt>:f12</tt> (and beyond, e.g. <tt>:f24</tt>)
198
+ # [Lock Keys]
199
+ # <tt>:caps_lock</tt>, <tt>:scroll_lock</tt>, <tt>:num_lock</tt>
200
+ # [System Keys]
201
+ # <tt>:print_screen</tt>, <tt>:pause</tt>, <tt>:menu</tt>, <tt>:keypad_begin</tt>
202
+ # [Media Keys]
203
+ #--
204
+ # SPDX-SnippetBegin
205
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
206
+ # SPDX-License-Identifier: MIT-0
207
+ #++
208
+ # <tt>:play</tt>, <tt>:media_pause</tt>, <tt>:play_pause</tt>, <tt>:reverse</tt>, <tt>:stop</tt>,
209
+ # <tt>:fast_forward</tt>, <tt>:rewind</tt>, <tt>:track_next</tt>, <tt>:track_previous</tt>,
210
+ # <tt>:record</tt>, <tt>:lower_volume</tt>, <tt>:raise_volume</tt>, <tt>:mute_volume</tt>
211
+ #--
212
+ # SPDX-SnippetEnd
213
+ #++
214
+ # [Modifier Keys]
215
+ #--
216
+ # SPDX-SnippetBegin
217
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
218
+ # SPDX-License-Identifier: MIT-0
219
+ #++
220
+ # <tt>:left_shift</tt>, <tt>:left_control</tt>, <tt>:left_alt</tt>, <tt>:left_super</tt>,
221
+ # <tt>:left_hyper</tt>, <tt>:left_meta</tt>, <tt>:right_shift</tt>, <tt>:right_control</tt>,
222
+ # <tt>:right_alt</tt>, <tt>:right_super</tt>, <tt>:right_hyper</tt>, <tt>:right_meta</tt>,
223
+ # <tt>:iso_level3_shift</tt>, <tt>:iso_level5_shift</tt>
224
+ #--
225
+ # SPDX-SnippetEnd
226
+ #++
227
+ # [Characters]
228
+ #--
229
+ # SPDX-SnippetBegin
230
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
231
+ # SPDX-License-Identifier: MIT-0
232
+ #++
233
+ # <tt>:a</tt>, <tt>:b</tt>, <tt>:1</tt>, <tt>:space</tt>, etc.
234
+ #
235
+ #--
236
+ # SPDX-SnippetEnd
237
+ #++
238
+ # === Modifier Examples
239
+ #
240
+ # * <tt>:ctrl_c</tt>
241
+ # * <tt>:alt_enter</tt>
242
+ # * <tt>:shift_left</tt>
243
+ # * <tt>:ctrl_alt_delete</tt>
244
+ def to_sym
245
+ mods = @modifiers.join("_")
246
+ if mods.empty?
247
+ @code.to_sym
248
+ else
249
+ :"#{mods}_#{@code}"
250
+ end
251
+ end
252
+
253
+ # Converts the event to its String representation.
254
+ #
255
+ # [Printable Characters]
256
+ # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
257
+ # [Special Keys]
258
+ # Returns an empty string (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt> all return <tt>""</tt>).
259
+ # [Modifiers]
260
+ #--
261
+ # SPDX-SnippetBegin
262
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
263
+ # SPDX-License-Identifier: MIT-0
264
+ #++
265
+ # Returns the character if printable, ignoring modifiers unless they alter the character code itself.
266
+ # Note that <tt>ctrl+c</tt> typically returns <tt>"c"</tt> as the code, so +to_s+ will return <tt>"c"</tt>.
267
+ #--
268
+ # SPDX-SnippetEnd
269
+ #++
270
+ def to_s
271
+ if text?
272
+ @code
273
+ else
274
+ ""
275
+ end
276
+ end
277
+
278
+ # Returns inspection string.
279
+ def inspect
280
+ "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect} kind=#{@kind.inspect}>"
281
+ end
282
+
283
+ # Supports dynamic key predicate methods via method_missing.
284
+ #
285
+ # Allows convenient checking for specific keys or key combinations:
286
+ #
287
+ #--
288
+ # SPDX-SnippetBegin
289
+ # SPDX-FileCopyrightText: 2025 Kerrick Long
290
+ # SPDX-License-Identifier: MIT-0
291
+ #++
292
+ # event.ctrl_c? # => true if Ctrl+C
293
+ # event.enter? # => true if Enter
294
+ # event.shift_up? # => true if Shift+Up
295
+ # event.q? # => true if "q"
296
+ #
297
+ #--
298
+ # SPDX-SnippetEnd
299
+ #++
300
+ # The method name is converted to a symbol and compared against the event.
301
+ # This works for any key code or modifier+key combination.
302
+ #
303
+ # === Smart Predicates (DWIM)
304
+ #
305
+ # For convenience, generic predicates match both system and media variants:
306
+ #
307
+ #--
308
+ # SPDX-SnippetBegin
309
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
310
+ # SPDX-License-Identifier: MIT-0
311
+ #++
312
+ # event.pause? # => true for BOTH system "pause" AND "media_pause"
313
+ # event.play? # => true for "media_play"
314
+ # event.stop? # => true for "media_stop"
315
+ #
316
+ #--
317
+ # SPDX-SnippetEnd
318
+ #++
319
+ # This "Do What I Mean" behavior reduces boilerplate when you just want to
320
+ # respond to a conceptual action (e.g., "pause the playback") regardless of
321
+ # whether the user pressed a keyboard key or a media button.
322
+ #
323
+ # For strict matching, use the full predicate or compare the code directly:
324
+ #
325
+ #--
326
+ # SPDX-SnippetBegin
327
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
328
+ # SPDX-License-Identifier: MIT-0
329
+ #++
330
+ # event.media_pause? # => true ONLY for media pause
331
+ # event.code == "pause" # => true ONLY for system pause
332
+ #
333
+ #--
334
+ # SPDX-SnippetEnd
335
+ #++
336
+ # === Arrow Key Aliases
337
+ #
338
+ # Arrow keys respond to <tt>arrow_up?</tt> and <tt>up_arrow?</tt> variants.
339
+ # This disambiguates from Mouse events, which also respond to <tt>up?</tt>
340
+ # and <tt>down?</tt>:
341
+ #
342
+ #--
343
+ # SPDX-SnippetBegin
344
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
345
+ # SPDX-License-Identifier: MIT-0
346
+ #++
347
+ # event.arrow_up? # => true for up arrow key
348
+ # event.up_arrow? # => true for up arrow key
349
+ # event.arrow_down? # => true for down arrow key
350
+ #
351
+ #--
352
+ # SPDX-SnippetEnd
353
+ #++
354
+ # === Key Prefix and Suffix
355
+ #
356
+ # Predicates accept <tt>key_</tt> prefix or <tt>_key</tt> suffix for explicit
357
+ # key event matching in mixed event contexts:
358
+ #
359
+ #--
360
+ # SPDX-SnippetBegin
361
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
362
+ # SPDX-License-Identifier: MIT-0
363
+ #++
364
+ # event.key_up? # => true for up arrow key
365
+ # event.key_q? # => true for "q" key
366
+ # event.q_key? # => true for "q" key
367
+ # event.enter_key? # => true for enter key
368
+ #
369
+ #--
370
+ # SPDX-SnippetEnd
371
+ #++
372
+ # === Capital Letters and Shift
373
+ #
374
+ # Capital letter predicates match shifted keys naturally. The terminal reports
375
+ # the produced character with shift in the modifiers:
376
+ #
377
+ #--
378
+ # SPDX-SnippetBegin
379
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
380
+ # SPDX-License-Identifier: MIT-0
381
+ #++
382
+ # event.G? # => true for code="G" modifiers=["shift"]
383
+ # event.shift_g? # => true for code="G" modifiers=["shift"]
384
+ # event.alt_B? # => true for code="B" modifiers=["alt", "shift"]
385
+ #
386
+ #--
387
+ # SPDX-SnippetEnd
388
+ #++
389
+ def method_missing(name, *args, **kwargs, &block)
390
+ if name.to_s.end_with?("?")
391
+ name_str = name.to_s
392
+ key_name = name_str.chop # Returns String, never nil for non-empty string
393
+ key_sym = key_name.to_sym
394
+
395
+ # Fast path: Exact match (e.g., media_pause? for media_pause)
396
+ return true if self == key_sym
397
+
398
+ # Delegate category-specific DWIM logic to mixins
399
+ return true if match_media_dwim?(key_name)
400
+ return true if match_modifier_dwim?(key_name, key_sym)
401
+ return true if match_navigation_dwim?(key_name, key_sym)
402
+ return true if match_system_dwim?(key_name, key_sym)
403
+
404
+ # DWIM: key_ prefix and _key suffix (disambiguate from mouse events)
405
+ # key_up? → up?, q_key? → q?, etc.
406
+ key_name = key_name.delete_prefix("key_").delete_suffix("_key")
407
+
408
+ # Fast path after prefix/suffix stripping
409
+ return true if self == key_name.to_sym
410
+
411
+ # DWIM: Single character codes match even with shift modifier present
412
+ # G? matches code="G" modifiers=["shift"], B? matches code="B" modifiers=["alt","shift"]
413
+ # @? matches code="@" modifiers=["shift"]
414
+ # The terminal reports the produced character, shift is implicit for these
415
+ if key_name.length == 1 && @code == key_name && @modifiers.include?("shift")
416
+ return true
417
+ end
418
+
419
+ # DWIM: Uppercase in predicate implies shift, so alt_B? matches alt_shift_B
420
+ # Parse predicate to extract modifiers and final letter
421
+ if key_name.match?(/\A([a-z_]+_)?([A-Z])\z/)
422
+ pred_letter = key_name[-1]
423
+ pred_mods = key_name.chop.delete_suffix("_").split("_").reject(&:empty?)
424
+ expected_mods = (pred_mods + ["shift"]).sort
425
+ return true if @code == pred_letter && @modifiers == expected_mods
426
+ end
427
+
428
+ # DWIM: Case-insensitive letter matching with modifiers
429
+ # shift_g? matches code="G" modifiers=["shift"]
430
+ if @code.length == 1 && @code.match?(/[A-Za-z]/) && (to_sym.to_s.downcase == key_name.downcase)
431
+ return true
432
+ end
433
+
434
+ # DWIM: Universal underscore-insensitivity
435
+ # Normalize both predicate and code by stripping underscores
436
+ normalized_predicate = key_name.delete("_")
437
+ normalized_code = @code.delete("_")
438
+ return true if normalized_predicate == normalized_code && @modifiers.empty?
439
+
440
+ # DWIM: Underscore variants delegate to existing methods
441
+ # space_bar? → spacebar? → space?, sig_int? → sigint?
442
+ normalized_method = :"#{normalized_predicate}?"
443
+ if normalized_method != name && respond_to?(normalized_method)
444
+ return public_send(normalized_method)
445
+ end
446
+
447
+ false
448
+ else
449
+ super
450
+ end
451
+ end
452
+
453
+ # Declares that this class responds to dynamic predicate methods.
454
+ def respond_to_missing?(name, *args)
455
+ name.to_s.end_with?("?") || super
456
+ end
457
+
458
+ # Deconstructs the event for pattern matching.
459
+ #
460
+ #--
461
+ # SPDX-SnippetBegin
462
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
463
+ # SPDX-License-Identifier: MIT-0
464
+ #++
465
+ # case event
466
+ # in type: :key, code: "c", modifiers: ["ctrl"]
467
+ # puts "Ctrl+C pressed"
468
+ # in type: :key, kind: :media
469
+ # puts "Media key pressed"
470
+ # end
471
+ #--
472
+ # SPDX-SnippetEnd
473
+ #++
474
+ def deconstruct_keys(keys)
475
+ { type: :key, code: @code, modifiers: @modifiers, kind: @kind }
476
+ end
477
+ end
478
+ end
479
+ end