ratatui_ruby 0.3.1 → 0.4.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 (300) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +14 -12
  3. data/.builds/ruby-3.3.yml +14 -12
  4. data/.builds/ruby-3.4.yml +14 -12
  5. data/.builds/ruby-4.0.0.yml +14 -12
  6. data/AGENTS.md +54 -13
  7. data/CHANGELOG.md +186 -1
  8. data/README.md +17 -15
  9. data/doc/application_architecture.md +116 -0
  10. data/doc/application_testing.md +12 -7
  11. data/doc/contributors/better_dx.md +543 -0
  12. data/doc/contributors/design/ruby_frontend.md +1 -1
  13. data/doc/contributors/developing_examples.md +203 -0
  14. data/doc/contributors/documentation_style.md +97 -0
  15. data/doc/contributors/dwim_dx.md +366 -0
  16. data/doc/contributors/example_analysis.md +82 -0
  17. data/doc/custom.css +14 -0
  18. data/doc/event_handling.md +119 -0
  19. data/doc/images/all_events.png +0 -0
  20. data/doc/images/analytics.png +0 -0
  21. data/doc/images/block_padding.png +0 -0
  22. data/doc/images/block_titles.png +0 -0
  23. data/doc/images/box_demo.png +0 -0
  24. data/doc/images/calendar_demo.png +0 -0
  25. data/doc/images/cell_demo.png +0 -0
  26. data/doc/images/chart_demo.png +0 -0
  27. data/doc/images/custom_widget.png +0 -0
  28. data/doc/images/flex_layout.png +0 -0
  29. data/doc/images/gauge_demo.png +0 -0
  30. data/doc/images/hit_test.png +0 -0
  31. data/doc/images/line_gauge_demo.png +0 -0
  32. data/doc/images/list_demo.png +0 -0
  33. data/doc/images/list_styles.png +0 -0
  34. data/doc/images/login_form.png +0 -0
  35. data/doc/images/map_demo.png +0 -0
  36. data/doc/images/mouse_events.png +0 -0
  37. data/doc/images/popup_demo.png +0 -0
  38. data/doc/images/quickstart_dsl.png +0 -0
  39. data/doc/images/quickstart_lifecycle.png +0 -0
  40. data/doc/images/ratatui_logo_demo.png +0 -0
  41. data/doc/images/readme_usage.png +0 -0
  42. data/doc/images/rich_text.png +0 -0
  43. data/doc/images/scroll_text.png +0 -0
  44. data/doc/images/scrollbar_demo.png +0 -0
  45. data/doc/images/sparkline_demo.png +0 -0
  46. data/doc/images/table_flex.png +0 -0
  47. data/doc/images/table_select.png +0 -0
  48. data/doc/images/widget_style_colors.png +0 -0
  49. data/doc/index.md +1 -0
  50. data/doc/interactive_design.md +121 -0
  51. data/doc/quickstart.md +147 -72
  52. data/examples/all_events/app.rb +169 -0
  53. data/examples/all_events/app.rbs +7 -0
  54. data/examples/all_events/test_app.rb +139 -0
  55. data/examples/analytics/app.rb +258 -0
  56. data/examples/analytics/app.rbs +7 -0
  57. data/examples/analytics/test_app.rb +132 -0
  58. data/examples/block_padding/app.rb +63 -0
  59. data/examples/block_padding/app.rbs +7 -0
  60. data/examples/block_padding/test_app.rb +31 -0
  61. data/examples/block_titles/app.rb +61 -0
  62. data/examples/block_titles/app.rbs +7 -0
  63. data/examples/block_titles/test_app.rb +34 -0
  64. data/examples/box_demo/app.rb +216 -0
  65. data/examples/box_demo/app.rbs +7 -0
  66. data/examples/box_demo/test_app.rb +88 -0
  67. data/examples/calendar_demo/app.rb +101 -0
  68. data/examples/calendar_demo/app.rbs +7 -0
  69. data/examples/calendar_demo/test_app.rb +108 -0
  70. data/examples/cell_demo/app.rb +108 -0
  71. data/examples/cell_demo/app.rbs +7 -0
  72. data/examples/cell_demo/test_app.rb +36 -0
  73. data/examples/chart_demo/app.rb +203 -0
  74. data/examples/chart_demo/app.rbs +7 -0
  75. data/examples/chart_demo/test_app.rb +102 -0
  76. data/examples/custom_widget/app.rb +51 -0
  77. data/examples/custom_widget/app.rbs +7 -0
  78. data/examples/custom_widget/test_app.rb +30 -0
  79. data/examples/flex_layout/app.rb +156 -0
  80. data/examples/flex_layout/app.rbs +7 -0
  81. data/examples/flex_layout/test_app.rb +65 -0
  82. data/examples/gauge_demo/app.rb +182 -0
  83. data/examples/gauge_demo/app.rbs +7 -0
  84. data/examples/gauge_demo/test_app.rb +120 -0
  85. data/examples/hit_test/app.rb +175 -0
  86. data/examples/hit_test/app.rbs +7 -0
  87. data/examples/hit_test/test_app.rb +102 -0
  88. data/examples/line_gauge_demo/app.rb +190 -0
  89. data/examples/line_gauge_demo/app.rbs +7 -0
  90. data/examples/line_gauge_demo/test_app.rb +129 -0
  91. data/examples/list_demo/app.rb +253 -0
  92. data/examples/list_demo/app.rbs +12 -0
  93. data/examples/list_demo/test_app.rb +237 -0
  94. data/examples/list_styles/app.rb +140 -0
  95. data/examples/list_styles/app.rbs +7 -0
  96. data/examples/list_styles/test_app.rb +157 -0
  97. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  98. data/examples/login_form/app.rbs +7 -0
  99. data/examples/login_form/test_app.rb +51 -0
  100. data/examples/map_demo/app.rb +90 -0
  101. data/examples/map_demo/app.rbs +7 -0
  102. data/examples/map_demo/test_app.rb +149 -0
  103. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  104. data/examples/mouse_events/app.rbs +7 -0
  105. data/examples/mouse_events/test_app.rb +53 -0
  106. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  107. data/examples/popup_demo/app.rbs +7 -0
  108. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  109. data/examples/quickstart_dsl/app.rb +36 -0
  110. data/examples/quickstart_dsl/app.rbs +7 -0
  111. data/examples/quickstart_dsl/test_app.rb +29 -0
  112. data/examples/quickstart_lifecycle/app.rb +39 -0
  113. data/examples/quickstart_lifecycle/app.rbs +7 -0
  114. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  115. data/examples/ratatui_logo_demo/app.rb +79 -0
  116. data/examples/ratatui_logo_demo/app.rbs +7 -0
  117. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  118. data/examples/ratatui_mascot_demo/app.rb +84 -0
  119. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  120. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  121. data/examples/readme_usage/app.rb +29 -0
  122. data/examples/readme_usage/app.rbs +7 -0
  123. data/examples/readme_usage/test_app.rb +29 -0
  124. data/examples/rich_text/app.rb +141 -0
  125. data/examples/rich_text/app.rbs +7 -0
  126. data/examples/rich_text/test_app.rb +166 -0
  127. data/examples/scroll_text/app.rb +103 -0
  128. data/examples/scroll_text/app.rbs +7 -0
  129. data/examples/scroll_text/test_app.rb +110 -0
  130. data/examples/scrollbar_demo/app.rb +143 -0
  131. data/examples/scrollbar_demo/app.rbs +7 -0
  132. data/examples/scrollbar_demo/test_app.rb +77 -0
  133. data/examples/sparkline_demo/app.rb +240 -0
  134. data/examples/sparkline_demo/app.rbs +10 -0
  135. data/examples/sparkline_demo/test_app.rb +107 -0
  136. data/examples/table_flex/app.rb +65 -0
  137. data/examples/table_flex/app.rbs +7 -0
  138. data/examples/table_flex/test_app.rb +36 -0
  139. data/examples/table_select/app.rb +198 -0
  140. data/examples/table_select/app.rbs +7 -0
  141. data/examples/table_select/test_app.rb +180 -0
  142. data/examples/widget_style_colors/app.rb +104 -0
  143. data/examples/widget_style_colors/app.rbs +14 -0
  144. data/examples/widget_style_colors/test_app.rb +48 -0
  145. data/ext/ratatui_ruby/Cargo.lock +889 -115
  146. data/ext/ratatui_ruby/Cargo.toml +4 -3
  147. data/ext/ratatui_ruby/clippy.toml +7 -0
  148. data/ext/ratatui_ruby/extconf.rb +7 -0
  149. data/ext/ratatui_ruby/src/events.rs +218 -229
  150. data/ext/ratatui_ruby/src/lib.rs +38 -10
  151. data/ext/ratatui_ruby/src/rendering.rs +90 -10
  152. data/ext/ratatui_ruby/src/style.rs +281 -98
  153. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  154. data/ext/ratatui_ruby/src/text.rs +171 -0
  155. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  156. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  157. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  158. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  159. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  160. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  161. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  162. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  163. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  164. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  165. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  166. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  167. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  168. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  169. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  170. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  171. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  172. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  173. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  174. data/lib/ratatui_ruby/cell.rb +166 -0
  175. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  176. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  177. data/lib/ratatui_ruby/event/key.rb +211 -0
  178. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  179. data/lib/ratatui_ruby/event/paste.rb +71 -0
  180. data/lib/ratatui_ruby/event/resize.rb +80 -0
  181. data/lib/ratatui_ruby/event.rb +79 -0
  182. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  184. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  185. data/lib/ratatui_ruby/schema/block.rb +186 -14
  186. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  187. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  188. data/lib/ratatui_ruby/schema/center.rb +49 -11
  189. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  190. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  191. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  192. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  193. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  194. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  195. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  196. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  197. data/lib/ratatui_ruby/schema/list.rb +93 -19
  198. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  199. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  200. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  201. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  202. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  203. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  204. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  205. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  206. data/lib/ratatui_ruby/schema/style.rb +49 -21
  207. data/lib/ratatui_ruby/schema/table.rb +119 -21
  208. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  209. data/lib/ratatui_ruby/schema/text.rb +90 -0
  210. data/lib/ratatui_ruby/session.rb +146 -0
  211. data/lib/ratatui_ruby/test_helper.rb +156 -13
  212. data/lib/ratatui_ruby/version.rb +1 -1
  213. data/lib/ratatui_ruby.rb +143 -23
  214. data/sig/ratatui_ruby/event.rbs +69 -0
  215. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  216. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  217. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  218. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  219. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  220. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  221. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  222. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  223. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  224. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  225. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  226. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  227. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  228. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  229. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  230. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  231. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  232. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  233. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  234. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  235. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  236. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  237. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  238. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  239. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  240. data/tasks/resources/build.yml.erb +13 -11
  241. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  242. data/tasks/terminal_preview/crash_report.rb +54 -0
  243. data/tasks/terminal_preview/example_app.rb +25 -0
  244. data/tasks/terminal_preview/launcher_script.rb +48 -0
  245. data/tasks/terminal_preview/preview_collection.rb +60 -0
  246. data/tasks/terminal_preview/preview_timing.rb +22 -0
  247. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  248. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  249. data/tasks/terminal_preview/system_appearance.rb +11 -0
  250. data/tasks/terminal_preview/terminal_window.rb +138 -0
  251. data/tasks/terminal_preview/window_id.rb +14 -0
  252. data/tasks/terminal_preview.rake +28 -0
  253. data/tasks/test.rake +1 -1
  254. metadata +174 -53
  255. data/doc/images/examples-analytics.rb.png +0 -0
  256. data/doc/images/examples-box_demo.rb.png +0 -0
  257. data/doc/images/examples-calendar_demo.rb.png +0 -0
  258. data/doc/images/examples-chart_demo.rb.png +0 -0
  259. data/doc/images/examples-custom_widget.rb.png +0 -0
  260. data/doc/images/examples-dashboard.rb.png +0 -0
  261. data/doc/images/examples-list_styles.rb.png +0 -0
  262. data/doc/images/examples-login_form.rb.png +0 -0
  263. data/doc/images/examples-map_demo.rb.png +0 -0
  264. data/doc/images/examples-mouse_events.rb.png +0 -0
  265. data/doc/images/examples-popup_demo.rb.gif +0 -0
  266. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  267. data/doc/images/examples-scroll_text.rb.png +0 -0
  268. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  269. data/doc/images/examples-stock_ticker.rb.png +0 -0
  270. data/doc/images/examples-system_monitor.rb.png +0 -0
  271. data/doc/images/examples-table_select.rb.png +0 -0
  272. data/examples/analytics.rb +0 -88
  273. data/examples/box_demo.rb +0 -71
  274. data/examples/calendar_demo.rb +0 -55
  275. data/examples/chart_demo.rb +0 -84
  276. data/examples/custom_widget.rb +0 -43
  277. data/examples/dashboard.rb +0 -72
  278. data/examples/list_styles.rb +0 -66
  279. data/examples/map_demo.rb +0 -58
  280. data/examples/quickstart_dsl.rb +0 -30
  281. data/examples/quickstart_lifecycle.rb +0 -40
  282. data/examples/readme_usage.rb +0 -21
  283. data/examples/scroll_text.rb +0 -74
  284. data/examples/scrollbar_demo.rb +0 -75
  285. data/examples/stock_ticker.rb +0 -93
  286. data/examples/system_monitor.rb +0 -94
  287. data/examples/table_select.rb +0 -70
  288. data/examples/test_analytics.rb +0 -65
  289. data/examples/test_box_demo.rb +0 -38
  290. data/examples/test_calendar_demo.rb +0 -66
  291. data/examples/test_dashboard.rb +0 -38
  292. data/examples/test_list_styles.rb +0 -61
  293. data/examples/test_login_form.rb +0 -63
  294. data/examples/test_map_demo.rb +0 -100
  295. data/examples/test_scroll_text.rb +0 -130
  296. data/examples/test_stock_ticker.rb +0 -39
  297. data/examples/test_system_monitor.rb +0 -40
  298. data/examples/test_table_select.rb +0 -37
  299. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  300. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -0,0 +1,124 @@
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
+ # Reports a mouse interaction.
9
+ #
10
+ # Modern terminals support rich pointer input, but the protocols are complex and varied.
11
+ # Handling clicks, drags, and scrolls requires robust parsing.
12
+ #
13
+ # This event simplifies the complexity. It tells you exactly *what* happened (+kind+),
14
+ # *where* it happened (+x+, +y+), and *which* button was involved.
15
+ #
16
+ # Use this to build interactive UIs. Implement click handlers, draggable sliders, or
17
+ # scrollable viewports with confidence.
18
+ #
19
+ # === Example
20
+ #
21
+ # if event.mouse? && event.down? && event.button == "left"
22
+ # puts "Left click at #{event.x}, #{event.y}"
23
+ # end
24
+ class Mouse < Event
25
+ # The kind of event (<tt>"down"</tt>, <tt>"up"</tt>, <tt>"drag"</tt>, <tt>"moved"</tt>, <tt>"scroll_up"</tt>, <tt>"scroll_down"</tt>).
26
+ #
27
+ # puts event.kind # => "down"
28
+ attr_reader :kind
29
+ # X coordinate (column).
30
+ #
31
+ # puts event.x # => 10
32
+ attr_reader :x
33
+ # Y coordinate (row).
34
+ #
35
+ # puts event.y # => 5
36
+ attr_reader :y
37
+ # The button pressed (<tt>"left"</tt>, <tt>"right"</tt>, <tt>"middle"</tt>, <tt>"none"</tt>).
38
+ #
39
+ # puts event.button # => "left"
40
+ #
41
+ # Can be <tt>nil</tt>, which is treated as <tt>"none"</tt>.
42
+ attr_reader :button
43
+ # List of active modifiers.
44
+ #
45
+ # puts event.modifiers # => ["ctrl"]
46
+ attr_reader :modifiers
47
+
48
+ # Returns true for Mouse events.
49
+ #
50
+ # event.mouse? # => true
51
+ # event.key? # => false
52
+ # event.resize? # => false
53
+ def mouse?
54
+ true
55
+ end
56
+
57
+ # Creates a new Mouse event.
58
+ #
59
+ # [kind]
60
+ # Event kind (String).
61
+ # [x]
62
+ # X coordinate (Integer).
63
+ # [y]
64
+ # Y coordinate (Integer).
65
+ # [button]
66
+ # Button name (String or <tt>nil</tt>).
67
+ # [modifiers]
68
+ # List of modifiers (Array<String>).
69
+ def initialize(kind:, x:, y:, button:, modifiers: [])
70
+ @kind = kind
71
+ @x = x
72
+ @y = y
73
+ @button = button || "none"
74
+ @modifiers = modifiers.sort
75
+ end
76
+
77
+ # Returns true if mouse button was pressed down.
78
+ def down?
79
+ @kind == "down"
80
+ end
81
+
82
+ # Returns true if mouse button was released.
83
+ def up?
84
+ @kind == "up"
85
+ end
86
+
87
+ # Returns true if mouse is being dragged.
88
+ def drag?
89
+ @kind == "drag"
90
+ end
91
+
92
+ # Returns true if scroll wheel moved up.
93
+ def scroll_up?
94
+ @kind == "scroll_up"
95
+ end
96
+
97
+ # Returns true if scroll wheel moved down.
98
+ #
99
+ # if event.scroll_down?
100
+ # scroll_offset += 1
101
+ # end
102
+ def scroll_down?
103
+ @kind == "scroll_down"
104
+ end
105
+
106
+ # Deconstructs the event for pattern matching.
107
+ #
108
+ # case event
109
+ # in type: :mouse, kind: "down", x:, y:
110
+ # puts "Click at #{x}, #{y}"
111
+ # end
112
+ def deconstruct_keys(keys)
113
+ { type: :mouse, kind: @kind, x: @x, y: @y, button: @button, modifiers: @modifiers }
114
+ end
115
+
116
+ ##
117
+ # Compares this event with another for equality.
118
+ def ==(other)
119
+ return false unless other.is_a?(Mouse)
120
+ kind == other.kind && x == other.x && y == other.y && button == other.button && modifiers == other.modifiers
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,71 @@
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
+ # Encapsulates pasted text.
9
+ #
10
+ # Users frequently paste text into terminals. Without specific handling, a paste appears as
11
+ # a flood of rapid keystrokes, often triggering accidental commands or confusing the input state.
12
+ #
13
+ # This event makes pasting safe. It groups the entire inserted block into a single atomic action.
14
+ #
15
+ # Handle this event to support bulk text insertion cleanly. Insert the +content+ directly into
16
+ # your field or buffer without triggering per-character logic.
17
+ #
18
+ # === Examples
19
+ #
20
+ # Using predicates:
21
+ # if event.paste?
22
+ # puts "Pasted: #{event.content}"
23
+ # end
24
+ #
25
+ # Using pattern matching:
26
+ # case event
27
+ # in type: :paste, content:
28
+ # puts "Pasted: #{content}"
29
+ # end
30
+ class Paste < Event
31
+ # The pasted content.
32
+ #
33
+ # puts event.content # => "https://example.com"
34
+ attr_reader :content
35
+
36
+ # Returns true for Paste events.
37
+ #
38
+ # event.paste? # => true
39
+ # event.key? # => false
40
+ # event.resize? # => false
41
+ def paste?
42
+ true
43
+ end
44
+
45
+ # Creates a new Paste event.
46
+ #
47
+ # [content]
48
+ # Pasted text (String).
49
+ def initialize(content:)
50
+ @content = content
51
+ end
52
+
53
+ # Deconstructs the event for pattern matching.
54
+ #
55
+ # case event
56
+ # in type: :paste, content:
57
+ # puts "User pasted: #{content}"
58
+ # end
59
+ def deconstruct_keys(keys)
60
+ { type: :paste, content: @content }
61
+ end
62
+
63
+ ##
64
+ # Compares this event with another for equality.
65
+ def ==(other)
66
+ return false unless other.is_a?(Paste)
67
+ content == other.content
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ module RatatuiRuby
7
+ class Event
8
+ # Signals a change in terminal dimensions.
9
+ #
10
+ # The terminal window is dynamic, not static. The user changes its dimensions at will,
11
+ # usually breaking a fixed layout.
12
+ #
13
+ # This event captures the new state. It delivers the updated +width+ and +height+
14
+ # immediately after the change.
15
+ #
16
+ # Use these dimensions to drive your layout logic. Recalculate constraints. Reallocate space.
17
+ # Fill the new canvas completely to maintain a responsive design.
18
+ #
19
+ # === Examples
20
+ #
21
+ # Using predicates:
22
+ # if event.resize?
23
+ # puts "Resized to #{event.width}x#{event.height}"
24
+ # end
25
+ #
26
+ # Using pattern matching:
27
+ # case event
28
+ # in type: :resize, width:, height:
29
+ # puts "Resized to #{width}x#{height}"
30
+ # end
31
+ class Resize < Event
32
+ # New terminal width in columns.
33
+ #
34
+ # puts event.width # => 80
35
+ attr_reader :width
36
+
37
+ # New terminal height in rows.
38
+ #
39
+ # puts event.height # => 24
40
+ attr_reader :height
41
+
42
+ # Returns true for Resize events.
43
+ #
44
+ # event.resize? # => true
45
+ # event.key? # => false
46
+ # event.mouse? # => false
47
+ def resize?
48
+ true
49
+ end
50
+
51
+ # Creates a new Resize event.
52
+ #
53
+ # [width]
54
+ # New width (Integer).
55
+ # [height]
56
+ # New height (Integer).
57
+ def initialize(width:, height:)
58
+ @width = width
59
+ @height = height
60
+ end
61
+
62
+ # Deconstructs the event for pattern matching.
63
+ #
64
+ # case event
65
+ # in type: :resize, width:, height:
66
+ # puts "Resized to #{width}x#{height}"
67
+ # end
68
+ def deconstruct_keys(keys)
69
+ { type: :resize, width: @width, height: @height }
70
+ end
71
+
72
+ ##
73
+ # Compares this event with another for equality.
74
+ def ==(other)
75
+ return false unless other.is_a?(Resize)
76
+ width == other.width && height == other.height
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,79 @@
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
+ # Base class for all RatatuiRuby events.
8
+ #
9
+ # Events are returned by RatatuiRuby.poll_event.
10
+ # All events support Ruby 3.0+ pattern matching via #deconstruct_keys.
11
+ #
12
+ # See RatatuiRuby.poll_event
13
+ class Event
14
+ # Returns true if this is a Key event.
15
+ def key?
16
+ false
17
+ end
18
+
19
+ # Returns true if this is a Mouse event.
20
+ def mouse?
21
+ false
22
+ end
23
+
24
+ # Returns true if this is a Resize event.
25
+ def resize?
26
+ false
27
+ end
28
+
29
+ # Returns true if this is a Paste event.
30
+ def paste?
31
+ false
32
+ end
33
+
34
+ # Returns true if this is a FocusGained event.
35
+ def focus_gained?
36
+ false
37
+ end
38
+
39
+ # Returns true if this is a FocusLost event.
40
+ def focus_lost?
41
+ false
42
+ end
43
+
44
+ # Responds to dynamic predicate methods for key checks.
45
+ # All non-Key events return false for any key predicate.
46
+ def method_missing(name, *args, &block)
47
+ if name.to_s.end_with?("?")
48
+ false
49
+ else
50
+ super
51
+ end
52
+ end
53
+
54
+ # Declares that this class responds to dynamic predicate methods.
55
+ def respond_to_missing?(name, *args)
56
+ name.to_s.end_with?("?") || super
57
+ end
58
+
59
+ # Deconstructs the event for pattern matching.
60
+ #
61
+ # Keys argument is unused but required by the protocol.
62
+ #
63
+ # case event
64
+ # in type: :key, code:
65
+ # puts "Key: #{code}"
66
+ # end
67
+ def deconstruct_keys(keys)
68
+ {}
69
+ end
70
+
71
+ end
72
+ end
73
+
74
+ require_relative "event/key"
75
+ require_relative "event/mouse"
76
+ require_relative "event/resize"
77
+ require_relative "event/paste"
78
+ require_relative "event/focus_gained"
79
+ require_relative "event/focus_lost"
@@ -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 BarChart
8
+ # A bar in a grouped bar chart.
9
+ #
10
+ # === Examples
11
+ #
12
+ # BarChart::Bar.new(value: 10, style: Style.new(fg: :red), label: "A")
13
+ class Bar < Data.define(:value, :label, :style, :value_style, :text_value)
14
+ ##
15
+ # :attr_reader: value
16
+ # The value of the bar (Integer).
17
+
18
+ ##
19
+ # :attr_reader: label
20
+ # The label of the bar (optional String).
21
+
22
+ ##
23
+ # :attr_reader: style
24
+ # The style of the bar (optional Style).
25
+
26
+ ##
27
+ # :attr_reader: value_style
28
+ # The style of the value (optional Style).
29
+
30
+ ##
31
+ # :attr_reader: text_value
32
+ # The text to display as the value (optional String).
33
+
34
+ def initialize(value:, label: nil, style: nil, value_style: nil, text_value: nil)
35
+ super(
36
+ value: Integer(value),
37
+ label: label,
38
+ style: style,
39
+ value_style: value_style,
40
+ text_value: text_value
41
+ )
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,27 @@
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 BarChart
8
+ # A group of bars in a grouped bar chart.
9
+ #
10
+ # === Examples
11
+ #
12
+ # BarChart::BarGroup.new(label: "Q1", bars: [BarChart::Bar.new(value: 10), BarChart::Bar.new(value: 20)])
13
+ class BarGroup < Data.define(:label, :bars)
14
+ ##
15
+ # :attr_reader: label
16
+ # The label of the group (String).
17
+
18
+ ##
19
+ # :attr_reader: bars
20
+ # The bars in the group (Array of Bar).
21
+
22
+ def initialize(label:, bars:)
23
+ super
24
+ end
25
+ end
26
+ end
27
+ end
@@ -4,25 +4,234 @@
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
6
  module RatatuiRuby
7
- # A widget that displays numeric data as a bar chart.
8
- #
9
- # [data] A hash of { "Label" => value (Integer) }.
10
- # [bar_width] The width of each bar in the chart.
11
- # [bar_gap] The gap between bars.
12
- # [max] Optional maximum value for the Y-axis.
13
- # [style] Optional style for the bars.
14
- # [block] Optional block widget to wrap the chart.
15
- class BarChart < Data.define(:data, :bar_width, :bar_gap, :max, :style, :block)
16
- # Creates a new BarChart widget.
7
+ # Displays categorical data as bars.
17
8
  #
18
- # [data] A hash of { "Label" => value (Integer) }.
19
- # [bar_width] The width of each bar in the chart.
20
- # [bar_gap] The gap between bars.
21
- # [max] Optional maximum value for the Y-axis.
22
- # [style] Optional style for the bars.
23
- # [block] Optional block widget to wrap the chart.
24
- def initialize(data:, bar_width: 3, bar_gap: 1, max: nil, style: nil, block: nil)
25
- super
9
+ # Raw tables of numbers are hard to scan. Comparing magnitudes requires mental arithmetic, which slows down decision-making.
10
+ #
11
+ # This widget visualizes the data. It renders vertical bars proportional to their value.
12
+ #
13
+ # Use it to compare server loads, sales figures, or any discrete datasets.
14
+ #
15
+ # === Examples
16
+ #
17
+ # BarChart.new(
18
+ # data: { "US" => 40, "EU" => 35, "AP" => 25 },
19
+ # bar_width: 5,
20
+ # style: Style.new(fg: :green)
21
+ # )
22
+ #
23
+ # # Grouped Bar Chart
24
+ # BarChart.new(
25
+ # data: [
26
+ # BarGroup.new(label: "Q1", bars: [Bar.new(value: 40), Bar.new(value: 45)]),
27
+ # BarGroup.new(label: "Q2", bars: [Bar.new(value: 50), Bar.new(value: 55)])
28
+ # ],
29
+ # bar_width: 5,
30
+ # group_gap: 3
31
+ # )
32
+ class BarChart < Data.define(:data, :bar_width, :bar_gap, :group_gap, :max, :style, :block, :direction, :label_style, :value_style, :bar_set)
33
+ ##
34
+ ##
35
+ ##
36
+ ##
37
+ # :attr_reader: data
38
+ # The data to display.
39
+ #
40
+ # Supports multiple formats:
41
+ # [<tt>Hash</tt>]
42
+ # Mapping labels (<tt>String</tt> or <tt>Symbol</tt>) to values (<tt>Integer</tt>).
43
+ # [<tt>Array</tt> of tuples]
44
+ # Ordered list of <tt>["Label", Value]</tt> or <tt>["Label", Value, Style]</tt> pairs.
45
+ # [<tt>Array</tt> of <tt>BarChart::BarGroup</tt>]
46
+ # List of <tt>BarChart::BarGroup</tt> objects for grouped charts.
47
+ #
48
+ # === Examples
49
+ #
50
+ # Hash (Simple):
51
+ # { "Apples" => 10, :Oranges => 15 }
52
+ #
53
+ # Array of Tuples (Ordered):
54
+ # [["Mon", 20], ["Tue", 30], ["Wed", 25]]
55
+ #
56
+ # BarGroup (Grouped):
57
+ # [
58
+ # RatatuiRuby::BarChart::BarGroup.new(label: "Q1", bars: [
59
+ # RatatuiRuby::BarChart::Bar.new(value: 50, label: "Rev"),
60
+ # RatatuiRuby::BarChart::Bar.new(value: 30, label: "Cost")
61
+ # ])
62
+ # ]
63
+
64
+ ##
65
+ # :attr_reader: bar_width
66
+ # Width of each bar in characters.
67
+
68
+ ##
69
+ # :attr_reader: bar_gap
70
+ # Spaces between bars.
71
+
72
+ ##
73
+ # :attr_reader: group_gap
74
+ # Spaces between groups (for grouped bar charts).
75
+
76
+ ##
77
+ # :attr_reader: max
78
+ # Maximum value for the Y-axis (optional).
79
+ #
80
+ # If nil, it is calculated from the data.
81
+
82
+ ##
83
+ # :attr_reader: style
84
+ # Style for the bars.
85
+
86
+ ##
87
+ # :attr_reader: block
88
+ # Optional wrapping block.
89
+
90
+ ##
91
+ # :attr_reader: label_style
92
+ # Style for the bar labels (optional).
93
+
94
+ ##
95
+ # :attr_reader: value_style
96
+ # Style for the bar values (optional).
97
+
98
+ ##
99
+ # :attr_reader: bar_set
100
+ # Custom characters for the bars (optional).
101
+ #
102
+ # A Hash with keys defining the characters for the bars.
103
+ # Keys: <tt>:empty</tt>, <tt>:one_eighth</tt>, <tt>:one_quarter</tt>, <tt>:three_eighths</tt>, <tt>:half</tt>, <tt>:five_eighths</tt>, <tt>:three_quarters</tt>, <tt>:seven_eighths</tt>, <tt>:full</tt>.
104
+ #
105
+ # You can also use integers (0-8) as keys, where 0 is empty, 4 is half, and 8 is full.
106
+ #
107
+ # Alternatively, you can pass an Array of 9 strings, where index 0 is empty and index 8 is full.
108
+ #
109
+ # === Examples
110
+ #
111
+ # bar_set: {
112
+ # empty: " ",
113
+ # one_eighth: " ",
114
+ # one_quarter: "▂",
115
+ # three_eighths: "▃",
116
+ # half: "▄",
117
+ # five_eighths: "▅",
118
+ # three_quarters: "▆",
119
+ # seven_eighths: "▇",
120
+ # full: "█"
121
+ # }
122
+ #
123
+ # # Numeric keys (0-8)
124
+ # bar_set: {
125
+ # 0 => " ", 1 => " ", 2 => "▂", 3 => "▃", 4 => "▄", 5 => "▅", 6 => "▆", 7 => "▇", 8 => "█"
126
+ # }
127
+ #
128
+ # # Array (9 items)
129
+ # bar_set: [" ", " ", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
130
+
131
+ BAR_KEYS = %i[empty one_eighth one_quarter three_eighths half five_eighths three_quarters seven_eighths full].freeze
132
+
133
+ # Creates a new BarChart widget.
134
+ #
135
+ # [data]
136
+ # Data to display. Hash, Array of arrays, or Array of BarGroup.
137
+ # [bar_width]
138
+ # Width of each bar (Integer).
139
+ # [bar_gap]
140
+ # Gap between bars (Integer).
141
+ # [group_gap]
142
+ # Gap between groups (Integer).
143
+ # [max]
144
+ # Maximum value of the bar chart (Integer).
145
+ # [style]
146
+ # Base style for the widget (Style).
147
+ # [block]
148
+ # Block to render around the chart (Block).
149
+ # [direction]
150
+ # Direction of the bars (:vertical or :horizontal).
151
+ # [label_style]
152
+ # Style object for labels (optional).
153
+ # [value_style]
154
+ # Style object for values (optional).
155
+ # [bar_set]
156
+ # Hash or Array: Custom characters for the bars.
157
+ def initialize(data:, bar_width: 3, bar_gap: 1, group_gap: 0, max: nil, style: nil, block: nil, direction: :vertical, label_style: nil, value_style: nil, bar_set: nil)
158
+ if bar_set
159
+ if bar_set.is_a?(Array) && bar_set.size == 9
160
+ # Convert Array to Hash using BAR_KEYS order
161
+ bar_set = BAR_KEYS.zip(bar_set).to_h
162
+ else
163
+ bar_set = bar_set.dup
164
+ # Normalize numeric keys (0-8) to symbolic keys
165
+ BAR_KEYS.each_with_index do |key, i|
166
+ if val = bar_set.delete(i) || bar_set.delete(i.to_s)
167
+ bar_set[key] = val
168
+ end
169
+ end
170
+ end
171
+ end
172
+
173
+ # Normalize data to Array of BarGroup
174
+ data = if data.is_a?(Hash)
175
+ if direction == :horizontal
176
+ bars = data.map do |label, value|
177
+ Bar.new(value: value, label: label.to_s)
178
+ end
179
+ [BarGroup.new(label: "", bars: bars)]
180
+ else
181
+ data.map do |label, value|
182
+ BarGroup.new(label: label.to_s, bars: [Bar.new(value: value)])
183
+ end
184
+ end
185
+ elsif data.is_a?(Array)
186
+ if data.empty?
187
+ []
188
+ elsif data.first.is_a?(BarGroup)
189
+ data
190
+ elsif data.first.is_a?(Array)
191
+ # Tuples
192
+ if direction == :horizontal
193
+ bars = data.map do |item|
194
+ label = item[0].to_s
195
+ value = item[1]
196
+ style = item[2]
197
+
198
+ bar = Bar.new(value: value, label: label)
199
+ bar = bar.with(style: style) if style
200
+ bar
201
+ end
202
+ [BarGroup.new(label: "", bars: bars)]
203
+ else
204
+ data.map do |item|
205
+ label = item[0].to_s
206
+ value = item[1]
207
+ style = item[2]
208
+
209
+ bar = Bar.new(value: value)
210
+ bar = bar.with(style: style) if style
211
+ BarGroup.new(label: label, bars: [bar])
212
+ end
213
+ end
214
+ else
215
+ # Fallback
216
+ data
217
+ end
218
+ else
219
+ data
220
+ end
221
+
222
+ super(
223
+ data: data,
224
+ bar_width: Integer(bar_width),
225
+ bar_gap: Integer(bar_gap),
226
+ group_gap: Integer(group_gap),
227
+ max: max.nil? ? nil : Integer(max),
228
+ style: style,
229
+ block: block,
230
+ direction: direction,
231
+ label_style: label_style,
232
+ value_style: value_style,
233
+ bar_set: bar_set
234
+ )
235
+ end
26
236
  end
27
- end
28
237
  end