ratatui_ruby 0.9.0 → 0.10.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 (268) 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 +2 -1
  7. data/CHANGELOG.md +122 -0
  8. data/REUSE.toml +5 -0
  9. data/Rakefile +1 -1
  10. data/Steepfile +49 -0
  11. data/doc/concepts/debugging.md +401 -0
  12. data/doc/getting_started/quickstart.md +8 -3
  13. data/doc/images/app_all_events.png +0 -0
  14. data/doc/images/app_color_picker.png +0 -0
  15. data/doc/images/app_debugging_showcase.gif +0 -0
  16. data/doc/images/app_debugging_showcase.png +0 -0
  17. data/doc/images/app_login_form.png +0 -0
  18. data/doc/images/app_stateful_interaction.png +0 -0
  19. data/doc/images/verify_quickstart_dsl.png +0 -0
  20. data/doc/images/verify_quickstart_layout.png +0 -0
  21. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  22. data/doc/images/verify_readme_usage.png +0 -0
  23. data/doc/images/widget_barchart.png +0 -0
  24. data/doc/images/widget_block.png +0 -0
  25. data/doc/images/widget_box.png +0 -0
  26. data/doc/images/widget_calendar.png +0 -0
  27. data/doc/images/widget_canvas.png +0 -0
  28. data/doc/images/widget_cell.png +0 -0
  29. data/doc/images/widget_center.png +0 -0
  30. data/doc/images/widget_chart.png +0 -0
  31. data/doc/images/widget_gauge.png +0 -0
  32. data/doc/images/widget_layout_split.png +0 -0
  33. data/doc/images/widget_line_gauge.png +0 -0
  34. data/doc/images/widget_list.png +0 -0
  35. data/doc/images/widget_map.png +0 -0
  36. data/doc/images/widget_overlay.png +0 -0
  37. data/doc/images/widget_popup.png +0 -0
  38. data/doc/images/widget_ratatui_logo.png +0 -0
  39. data/doc/images/widget_ratatui_mascot.png +0 -0
  40. data/doc/images/widget_rect.png +0 -0
  41. data/doc/images/widget_render.png +0 -0
  42. data/doc/images/widget_rich_text.png +0 -0
  43. data/doc/images/widget_scroll_text.png +0 -0
  44. data/doc/images/widget_scrollbar.png +0 -0
  45. data/doc/images/widget_sparkline.png +0 -0
  46. data/doc/images/widget_style_colors.png +0 -0
  47. data/doc/images/widget_table.png +0 -0
  48. data/doc/images/widget_tabs.png +0 -0
  49. data/doc/images/widget_text_width.png +0 -0
  50. data/doc/troubleshooting/async.md +4 -0
  51. data/examples/app_debugging_showcase/README.md +119 -0
  52. data/examples/app_debugging_showcase/app.rb +318 -0
  53. data/examples/widget_canvas/app.rb +19 -14
  54. data/examples/widget_gauge/app.rb +18 -3
  55. data/examples/widget_layout_split/app.rb +16 -4
  56. data/examples/widget_list/app.rb +22 -6
  57. data/examples/widget_rect/app.rb +7 -6
  58. data/examples/widget_rich_text/app.rb +62 -37
  59. data/examples/widget_style_colors/app.rb +26 -47
  60. data/examples/widget_table/app.rb +28 -5
  61. data/examples/widget_text_width/app.rb +6 -4
  62. data/ext/ratatui_ruby/Cargo.lock +48 -1
  63. data/ext/ratatui_ruby/Cargo.toml +6 -2
  64. data/ext/ratatui_ruby/src/color.rs +82 -0
  65. data/ext/ratatui_ruby/src/errors.rs +28 -0
  66. data/ext/ratatui_ruby/src/events.rs +16 -14
  67. data/ext/ratatui_ruby/src/lib.rs +56 -0
  68. data/ext/ratatui_ruby/src/rendering.rs +3 -1
  69. data/ext/ratatui_ruby/src/style.rs +48 -21
  70. data/ext/ratatui_ruby/src/terminal.rs +40 -9
  71. data/ext/ratatui_ruby/src/text.rs +21 -9
  72. data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
  73. data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
  74. data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
  75. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  76. data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
  77. data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
  78. data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
  79. data/lib/ratatui_ruby/buffer/cell.rb +25 -15
  80. data/lib/ratatui_ruby/buffer.rb +134 -2
  81. data/lib/ratatui_ruby/cell.rb +13 -5
  82. data/lib/ratatui_ruby/debug.rb +215 -0
  83. data/lib/ratatui_ruby/event/key.rb +3 -2
  84. data/lib/ratatui_ruby/event/sync.rb +52 -0
  85. data/lib/ratatui_ruby/event.rb +7 -1
  86. data/lib/ratatui_ruby/layout/constraint.rb +184 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +119 -13
  88. data/lib/ratatui_ruby/layout/position.rb +55 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +188 -0
  90. data/lib/ratatui_ruby/layout/size.rb +55 -0
  91. data/lib/ratatui_ruby/layout.rb +4 -0
  92. data/lib/ratatui_ruby/style/color.rb +149 -0
  93. data/lib/ratatui_ruby/style/style.rb +51 -4
  94. data/lib/ratatui_ruby/style.rb +2 -0
  95. data/lib/ratatui_ruby/symbols.rb +435 -0
  96. data/lib/ratatui_ruby/synthetic_events.rb +86 -0
  97. data/lib/ratatui_ruby/table_state.rb +51 -0
  98. data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
  99. data/lib/ratatui_ruby/test_helper/event_injection.rb +34 -1
  100. data/lib/ratatui_ruby/test_helper.rb +9 -0
  101. data/lib/ratatui_ruby/text/line.rb +245 -0
  102. data/lib/ratatui_ruby/text/span.rb +158 -0
  103. data/lib/ratatui_ruby/text.rb +99 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
  105. data/lib/ratatui_ruby/tui/core.rb +13 -2
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
  107. data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
  108. data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
  109. data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
  110. data/lib/ratatui_ruby/tui.rb +22 -1
  111. data/lib/ratatui_ruby/version.rb +1 -1
  112. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
  115. data/lib/ratatui_ruby/widgets/block.rb +14 -6
  116. data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
  117. data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
  118. data/lib/ratatui_ruby/widgets/cell.rb +2 -0
  119. data/lib/ratatui_ruby/widgets/center.rb +2 -0
  120. data/lib/ratatui_ruby/widgets/chart.rb +6 -0
  121. data/lib/ratatui_ruby/widgets/clear.rb +2 -0
  122. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
  126. data/lib/ratatui_ruby/widgets/list.rb +87 -3
  127. data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +45 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
  136. data/lib/ratatui_ruby/widgets/table.rb +13 -3
  137. data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
  138. data/lib/ratatui_ruby/widgets.rb +1 -0
  139. data/lib/ratatui_ruby.rb +51 -16
  140. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  141. data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
  142. data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
  143. data/sig/examples/app_all_events/view.rbs +1 -1
  144. data/sig/examples/app_stateful_interaction/app.rbs +5 -5
  145. data/sig/examples/widget_block_demo/app.rbs +6 -6
  146. data/sig/manifest.yaml +5 -0
  147. data/sig/patches/data.rbs +26 -0
  148. data/sig/patches/debugger__.rbs +8 -0
  149. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  150. data/sig/ratatui_ruby/buffer.rbs +18 -0
  151. data/sig/ratatui_ruby/cell.rbs +44 -0
  152. data/sig/ratatui_ruby/clear.rbs +18 -0
  153. data/sig/ratatui_ruby/constraint.rbs +26 -0
  154. data/sig/ratatui_ruby/debug.rbs +45 -0
  155. data/sig/ratatui_ruby/draw.rbs +30 -0
  156. data/sig/ratatui_ruby/event.rbs +68 -8
  157. data/sig/ratatui_ruby/frame.rbs +4 -4
  158. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  159. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  160. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  161. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  162. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  163. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  164. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  165. data/sig/ratatui_ruby/ratatui_ruby.rbs +83 -4
  166. data/sig/ratatui_ruby/rect.rbs +17 -0
  167. data/sig/ratatui_ruby/style/color.rbs +22 -0
  168. data/sig/ratatui_ruby/style/style.rbs +29 -0
  169. data/sig/ratatui_ruby/symbols.rbs +141 -0
  170. data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
  171. data/sig/ratatui_ruby/table_state.rbs +6 -0
  172. data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
  173. data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
  174. data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
  175. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
  176. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
  177. data/sig/ratatui_ruby/text/line.rbs +27 -0
  178. data/sig/ratatui_ruby/text/span.rbs +23 -0
  179. data/sig/ratatui_ruby/text.rbs +12 -0
  180. data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
  181. data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
  182. data/sig/ratatui_ruby/tui/core.rbs +2 -2
  183. data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
  184. data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
  185. data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
  186. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
  187. data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
  188. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  189. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  190. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  191. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  192. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  193. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  194. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  195. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  196. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  197. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  198. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  199. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  200. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  201. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  202. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  203. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  204. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  205. data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
  206. data/tasks/steep.rake +11 -0
  207. metadata +82 -63
  208. data/doc/contributors/v1.0.0_blockers.md +0 -876
  209. data/doc/troubleshooting/debugging.md +0 -101
  210. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
  211. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
  212. data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
  213. data/lib/ratatui_ruby/schema/block.rb +0 -198
  214. data/lib/ratatui_ruby/schema/calendar.rb +0 -84
  215. data/lib/ratatui_ruby/schema/canvas.rb +0 -239
  216. data/lib/ratatui_ruby/schema/center.rb +0 -67
  217. data/lib/ratatui_ruby/schema/chart.rb +0 -159
  218. data/lib/ratatui_ruby/schema/clear.rb +0 -62
  219. data/lib/ratatui_ruby/schema/constraint.rb +0 -151
  220. data/lib/ratatui_ruby/schema/cursor.rb +0 -50
  221. data/lib/ratatui_ruby/schema/gauge.rb +0 -72
  222. data/lib/ratatui_ruby/schema/layout.rb +0 -122
  223. data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
  224. data/lib/ratatui_ruby/schema/list.rb +0 -135
  225. data/lib/ratatui_ruby/schema/list_item.rb +0 -51
  226. data/lib/ratatui_ruby/schema/overlay.rb +0 -51
  227. data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
  228. data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
  229. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
  230. data/lib/ratatui_ruby/schema/rect.rb +0 -174
  231. data/lib/ratatui_ruby/schema/row.rb +0 -76
  232. data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
  233. data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
  234. data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
  235. data/lib/ratatui_ruby/schema/style.rb +0 -97
  236. data/lib/ratatui_ruby/schema/table.rb +0 -141
  237. data/lib/ratatui_ruby/schema/tabs.rb +0 -85
  238. data/lib/ratatui_ruby/schema/text.rb +0 -217
  239. data/sig/examples/app_all_events/model/events.rbs +0 -15
  240. data/sig/examples/app_all_events/view_state.rbs +0 -21
  241. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
  242. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
  243. data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
  244. data/sig/ratatui_ruby/schema/block.rbs +0 -18
  245. data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
  246. data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
  247. data/sig/ratatui_ruby/schema/center.rbs +0 -17
  248. data/sig/ratatui_ruby/schema/chart.rbs +0 -39
  249. data/sig/ratatui_ruby/schema/constraint.rbs +0 -22
  250. data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
  251. data/sig/ratatui_ruby/schema/draw.rbs +0 -33
  252. data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
  253. data/sig/ratatui_ruby/schema/layout.rbs +0 -27
  254. data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
  255. data/sig/ratatui_ruby/schema/list.rbs +0 -28
  256. data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
  257. data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
  258. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
  259. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
  260. data/sig/ratatui_ruby/schema/rect.rbs +0 -48
  261. data/sig/ratatui_ruby/schema/row.rbs +0 -28
  262. data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
  263. data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
  264. data/sig/ratatui_ruby/schema/style.rbs +0 -19
  265. data/sig/ratatui_ruby/schema/table.rbs +0 -32
  266. data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
  267. data/sig/ratatui_ruby/schema/text.rbs +0 -31
  268. /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
@@ -1,101 +0,0 @@
1
- <!--
2
- SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
- SPDX-License-Identifier: CC-BY-SA-4.0
4
- -->
5
-
6
- # Debugging TUI Applications
7
-
8
- TUI applications run in raw terminal mode. stderr and stdout carry terminal escape sequences. Using `puts` or `warn` inside the render loop corrupts the display.
9
-
10
- This creates a problem: how do you inspect variables and trace execution?
11
-
12
- Write debug output to files. Tail them in a separate terminal.
13
-
14
- ## File-Based Logging
15
-
16
- Create timestamped log files to avoid overwrites:
17
-
18
- <!-- SPDX-SnippetBegin -->
19
- <!--
20
- SPDX-FileCopyrightText: 2026 Kerrick Long
21
- SPDX-License-Identifier: MIT-0
22
- -->
23
- ```ruby
24
- FileUtils.mkdir_p(File.join(Dir.tmpdir, "my_debug"))
25
- timestamp = Time.now.strftime('%Y%m%d_%H%M%S_%N')
26
- File.write(
27
- File.join(Dir.tmpdir, "my_debug", "#{timestamp}.log"),
28
- "variable=#{value.inspect}\n"
29
- )
30
- ```
31
- <!-- SPDX-SnippetEnd -->
32
-
33
- Or append to a single file:
34
-
35
- <!-- SPDX-SnippetBegin -->
36
- <!--
37
- SPDX-FileCopyrightText: 2026 Kerrick Long
38
- SPDX-License-Identifier: MIT-0
39
- -->
40
- ```ruby
41
- File.write("/tmp/debug.log", "#{Time.now}: #{message}\n", mode: "a")
42
- ```
43
- <!-- SPDX-SnippetEnd -->
44
-
45
- Tail the logs in a separate terminal:
46
-
47
- <!-- SPDX-SnippetBegin -->
48
- <!--
49
- SPDX-FileCopyrightText: 2026 Kerrick Long
50
- SPDX-License-Identifier: MIT-0
51
- -->
52
- ```bash
53
- # Single file
54
- tail -f /tmp/debug.log
55
-
56
- # Directory of timestamped files
57
- watch -n 0.5 'ls -la /tmp/my_debug/ && cat /tmp/my_debug/*.log'
58
- ```
59
- <!-- SPDX-SnippetEnd -->
60
-
61
- ## REPL Debugging with `__FILE__` Guards
62
-
63
- Unit tests verify correctness. But during exploratory debugging, you want to poke at objects interactively. Loading the full TUI just to inspect one object is slow.
64
-
65
- Wrap your main execution in a guard:
66
-
67
- <!-- SPDX-SnippetBegin -->
68
- <!--
69
- SPDX-FileCopyrightText: 2026 Kerrick Long
70
- SPDX-License-Identifier: MIT-0
71
- -->
72
- ```ruby
73
- if __FILE__ == $PROGRAM_NAME
74
- MyApp.new.run
75
- end
76
- ```
77
- <!-- SPDX-SnippetEnd -->
78
-
79
- Now load the file and interact with classes directly:
80
-
81
- <!-- SPDX-SnippetBegin -->
82
- <!--
83
- SPDX-FileCopyrightText: 2026 Kerrick Long
84
- SPDX-License-Identifier: MIT-0
85
- -->
86
- ```bash
87
- ruby -e 'load "./bin/my_tui"; obj = MyClass.new; sleep 1; puts obj.result'
88
- ```
89
- <!-- SPDX-SnippetEnd -->
90
-
91
- This exercises domain logic without entering raw terminal mode. Use it for exploratory debugging. Write tests using the [TestHelper](application_testing.md) for regression coverage.
92
-
93
- ## Isolating Terminal Issues
94
-
95
- Something works in a `ruby -e` script but fails in the TUI. Common causes:
96
-
97
- 1. **Thread context.** Ruby threads share the process's terminal state.
98
- 2. **Raw mode.** External commands fail when stdin/stdout are reconfigured.
99
- 3. **SSH/Git auth.** Commands that prompt for credentials hang or return empty.
100
-
101
- See [Async Operations](async.md) for solutions.
@@ -1,47 +0,0 @@
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 BarChart
10
- # A bar in a grouped bar chart.
11
- #
12
- # === Examples
13
- #
14
- # BarChart::Bar.new(value: 10, style: Style.new(fg: :red), label: "A")
15
- class Bar < Data.define(:value, :label, :style, :value_style, :text_value)
16
- ##
17
- # :attr_reader: value
18
- # The value of the bar (Integer).
19
-
20
- ##
21
- # :attr_reader: label
22
- # The label of the bar (optional String, Text::Span, or Text::Line for rich styling).
23
-
24
- ##
25
- # :attr_reader: style
26
- # The style of the bar (optional Style).
27
-
28
- ##
29
- # :attr_reader: value_style
30
- # The style of the value (optional Style).
31
-
32
- ##
33
- # :attr_reader: text_value
34
- # The text to display as the value (optional String, Text::Span, or Text::Line for rich styling).
35
-
36
- def initialize(value:, label: nil, style: nil, value_style: nil, text_value: nil)
37
- super(
38
- value: Integer(value),
39
- label:,
40
- style:,
41
- value_style:,
42
- text_value:
43
- )
44
- end
45
- end
46
- end
47
- end
@@ -1,25 +0,0 @@
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 BarChart
10
- # A group of bars in a grouped bar chart.
11
- #
12
- # === Examples
13
- #
14
- # BarChart::BarGroup.new(label: "Q1", bars: [BarChart::Bar.new(value: 10), BarChart::Bar.new(value: 20)])
15
- class BarGroup < Data.define(:label, :bars)
16
- ##
17
- # :attr_reader: label
18
- # The label of the group (String).
19
-
20
- ##
21
- # :attr_reader: bars
22
- # The bars in the group (Array of Bar).
23
- end
24
- end
25
- end
@@ -1,287 +0,0 @@
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
- # Displays categorical data as bars.
10
- #
11
- # Raw tables of numbers are hard to scan. Comparing magnitudes requires mental arithmetic, which slows down decision-making.
12
- #
13
- # This widget visualizes the data. It renders vertical bars proportional to their value.
14
- #
15
- # Use it to compare server loads, sales figures, or any discrete datasets.
16
- #
17
- # {rdoc-image:/doc/images/widget_barchart.png}[link:/examples/widget_barchart/app_rb.html]
18
- #
19
- # === Example
20
- #
21
- # Run the interactive demo from the terminal:
22
- #
23
- #--
24
- # SPDX-SnippetBegin
25
- # SPDX-FileCopyrightText: 2026 Kerrick Long
26
- # SPDX-License-Identifier: MIT-0
27
- #++
28
- # ruby examples/widget_barchart/app.rb
29
- #
30
- # # Grouped Bar Chart
31
- # BarChart.new(
32
- # data: [
33
- # BarGroup.new(label: "Q1", bars: [Bar.new(value: 40), Bar.new(value: 45)]),
34
- # BarGroup.new(label: "Q2", bars: [Bar.new(value: 50), Bar.new(value: 55)])
35
- # ],
36
- # bar_width: 5,
37
- # group_gap: 3
38
- # )
39
- #--
40
- # SPDX-SnippetEnd
41
- #++
42
- class BarChart < Data.define(:data, :bar_width, :bar_gap, :group_gap, :max, :style, :block, :direction, :label_style, :value_style, :bar_set)
43
- ##
44
- ##
45
- ##
46
- ##
47
- # :attr_reader: data
48
- # The data to display.
49
- #
50
- # Supports multiple formats:
51
- # [<tt>Hash</tt>]
52
- # Mapping labels (<tt>String</tt> or <tt>Symbol</tt>) to values (<tt>Integer</tt>).
53
- # [<tt>Array</tt> of tuples]
54
- # Ordered list of <tt>["Label", Value]</tt> or <tt>["Label", Value, Style]</tt> pairs.
55
- # [<tt>Array</tt> of <tt>BarChart::BarGroup</tt>]
56
- #--
57
- # SPDX-SnippetBegin
58
- # SPDX-FileCopyrightText: 2025 Kerrick Long
59
- # SPDX-License-Identifier: MIT-0
60
- #++
61
- # List of <tt>BarChart::BarGroup</tt> objects for grouped charts.
62
- #
63
- #--
64
- # SPDX-SnippetEnd
65
- #++
66
- # === Examples
67
- #
68
- # Hash (Simple):
69
- #--
70
- # SPDX-SnippetBegin
71
- # SPDX-FileCopyrightText: 2025 Kerrick Long
72
- # SPDX-License-Identifier: MIT-0
73
- #++
74
- # { "Apples" => 10, :Oranges => 15 }
75
- #
76
- #--
77
- # SPDX-SnippetEnd
78
- #++
79
- # Array of Tuples (Ordered):
80
- #--
81
- # SPDX-SnippetBegin
82
- # SPDX-FileCopyrightText: 2025 Kerrick Long
83
- # SPDX-License-Identifier: MIT-0
84
- #++
85
- # [["Mon", 20], ["Tue", 30], ["Wed", 25]]
86
- #
87
- #--
88
- # SPDX-SnippetEnd
89
- #++
90
- # BarGroup (Grouped):
91
- #--
92
- # SPDX-SnippetBegin
93
- # SPDX-FileCopyrightText: 2025 Kerrick Long
94
- # SPDX-License-Identifier: MIT-0
95
- #++
96
- # [
97
- # RatatuiRuby::BarChart::BarGroup.new(label: "Q1", bars: [
98
- # RatatuiRuby::BarChart::Bar.new(value: 50, label: "Rev"),
99
- # RatatuiRuby::BarChart::Bar.new(value: 30, label: "Cost")
100
- # ])
101
- # ]
102
- #--
103
- # SPDX-SnippetEnd
104
- #++
105
-
106
- ##
107
- # :attr_reader: bar_width
108
- # Width of each bar in characters.
109
-
110
- ##
111
- # :attr_reader: bar_gap
112
- # Spaces between bars.
113
-
114
- ##
115
- # :attr_reader: group_gap
116
- # Spaces between groups (for grouped bar charts).
117
-
118
- ##
119
- # :attr_reader: max
120
- # Maximum value for the Y-axis (optional).
121
- #
122
- # If nil, it is calculated from the data.
123
-
124
- ##
125
- # :attr_reader: style
126
- # Style for the bars.
127
-
128
- ##
129
- # :attr_reader: block
130
- # Optional wrapping block.
131
-
132
- ##
133
- # :attr_reader: label_style
134
- # Style for the bar labels (optional).
135
-
136
- ##
137
- # :attr_reader: value_style
138
- # Style for the bar values (optional).
139
-
140
- ##
141
- # :attr_reader: bar_set
142
- # Custom characters for the bars (optional).
143
- #
144
- # A Hash with keys defining the characters for the bars.
145
- # 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>.
146
- #
147
- # You can also use integers (0-8) as keys, where 0 is empty, 4 is half, and 8 is full.
148
- #
149
- # Alternatively, you can pass an Array of 9 strings, where index 0 is empty and index 8 is full.
150
- #
151
- # === Examples
152
- #
153
- #--
154
- # SPDX-SnippetBegin
155
- # SPDX-FileCopyrightText: 2025 Kerrick Long
156
- # SPDX-License-Identifier: MIT-0
157
- #++
158
- # bar_set: {
159
- # empty: " ",
160
- # one_eighth: " ",
161
- # one_quarter: "▂",
162
- # three_eighths: "▃",
163
- # half: "▄",
164
- # five_eighths: "▅",
165
- # three_quarters: "▆",
166
- # seven_eighths: "▇",
167
- # full: "█"
168
- # }
169
- #
170
- # # Numeric keys (0-8)
171
- # bar_set: {
172
- # 0 => " ", 1 => " ", 2 => "▂", 3 => "▃", 4 => "▄", 5 => "▅", 6 => "▆", 7 => "▇", 8 => "█"
173
- # }
174
- #
175
- # # Array (9 items)
176
- # bar_set: [" ", " ", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
177
- #--
178
- # SPDX-SnippetEnd
179
- #++
180
-
181
- BAR_KEYS = %i[empty one_eighth one_quarter three_eighths half five_eighths three_quarters seven_eighths full].freeze
182
-
183
- # Creates a new BarChart widget.
184
- #
185
- # [data]
186
- # Data to display. Hash, Array of arrays, or Array of BarGroup.
187
- # [bar_width]
188
- # Width of each bar (Integer).
189
- # [bar_gap]
190
- # Gap between bars (Integer).
191
- # [group_gap]
192
- # Gap between groups (Integer).
193
- # [max]
194
- # Maximum value of the bar chart (Integer).
195
- # [style]
196
- # Base style for the widget (Style).
197
- # [block]
198
- # Block to render around the chart (Block).
199
- # [direction]
200
- # Direction of the bars (:vertical or :horizontal).
201
- # [label_style]
202
- # Style object for labels (optional).
203
- # [value_style]
204
- # Style object for values (optional).
205
- # [bar_set]
206
- # Hash or Array: Custom characters for the bars.
207
- 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)
208
- if bar_set
209
- if bar_set.is_a?(Array) && bar_set.size == 9
210
- # Convert Array to Hash using BAR_KEYS order
211
- bar_set = BAR_KEYS.zip(bar_set).to_h
212
- else
213
- bar_set = bar_set.dup
214
- # Normalize numeric keys (0-8) to symbolic keys
215
- BAR_KEYS.each_with_index do |key, i|
216
- if (val = bar_set.delete(i) || bar_set.delete(i.to_s))
217
- bar_set[key] = val
218
- end
219
- end
220
- end
221
- end
222
-
223
- # Normalize data to Array of BarGroup
224
- data = if data.is_a?(Hash)
225
- if direction == :horizontal
226
- bars = data.map do |label, value|
227
- Bar.new(value:, label: label.to_s)
228
- end
229
- [BarGroup.new(label: "", bars:)]
230
- else
231
- data.map do |label, value|
232
- BarGroup.new(label: label.to_s, bars: [Bar.new(value:)])
233
- end
234
- end
235
- elsif data.is_a?(Array)
236
- if data.empty?
237
- []
238
- elsif data.first.is_a?(BarGroup)
239
- data
240
- elsif data.first.is_a?(Array)
241
- # Tuples
242
- if direction == :horizontal
243
- bars = data.map do |item|
244
- label = item[0].to_s
245
- value = item[1]
246
- style = item[2]
247
-
248
- bar = Bar.new(value:, label:)
249
- bar = bar.with(style:) if style
250
- bar
251
- end
252
- [BarGroup.new(label: "", bars:)]
253
- else
254
- data.map do |item|
255
- label = item[0].to_s
256
- value = item[1]
257
- style = item[2]
258
-
259
- bar = Bar.new(value:)
260
- bar = bar.with(style:) if style
261
- BarGroup.new(label:, bars: [bar])
262
- end
263
- end
264
- else
265
- # Fallback
266
- data
267
- end
268
- else
269
- data
270
- end
271
-
272
- super(
273
- data:,
274
- bar_width: Integer(bar_width),
275
- bar_gap: Integer(bar_gap),
276
- group_gap: Integer(group_gap),
277
- max: max.nil? ? nil : Integer(max),
278
- style:,
279
- block:,
280
- direction:,
281
- label_style:,
282
- value_style:,
283
- bar_set:
284
- )
285
- end
286
- end
287
- end
@@ -1,198 +0,0 @@
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
- # Defines the visual container for a widget.
10
- #
11
- # Widgets often float in void. Without boundaries, interfaces become a chaotic mess of text. Users need structure to parse information.
12
- #
13
- # This widget creates that structure. It wraps content in borders. It labels sections with titles. It paints the background.
14
- #
15
- # Use blocks to define distinct areas. Group related information. Create a visual hierarchy that guides the user's eye.
16
- #
17
- # {rdoc-image:/doc/images/widget_box.png}[link:/examples/widget_box/app_rb.html]
18
- #
19
- # === Example
20
- #
21
- # Run the interactive demo from the terminal:
22
- #
23
- # ruby examples/widget_box/app.rb
24
- class Block < Data.define(:title, :titles, :title_alignment, :title_style, :borders, :border_style, :border_type, :border_set, :style, :padding, :children)
25
- ##
26
- # :attr_reader: title
27
- # The main title displayed on the top border.
28
- #
29
- # === Example
30
- #
31
- # Block.new(title: "Main").title # => "Main"
32
-
33
- ##
34
- # :attr_reader: titles
35
- # Additional titles for complex labeling.
36
- #
37
- # Each title can be a <tt>String</tt> or a <tt>Hash</tt> with keys <tt>:content</tt>, <tt>:alignment</tt>, <tt>:position</tt> (<tt>:top</tt> or <tt>:bottom</tt>), and <tt>:style</tt>.
38
- #
39
- # === Example
40
- #
41
- # Block.new(titles: ["Top", { content: "Bottom", position: :bottom }]).titles
42
-
43
- ##
44
- # :attr_reader: title_alignment
45
- # Alignment of the main title.
46
- #
47
- # One of <tt>:left</tt>, <tt>:center</tt>, or <tt>:right</tt>.
48
- #
49
- # === Example
50
- #
51
- # Block.new(title_alignment: :center).title_alignment # => :center
52
-
53
- ##
54
- # :attr_reader: title_style
55
- # Style applied to all titles if not overridden.
56
- #
57
- # === Example
58
- #
59
- # Block.new(title_style: Style.new(fg: :red)).title_style
60
-
61
- ##
62
- # :attr_reader: borders
63
- # Visible borders.
64
- #
65
- # An array containing any of <tt>:top</tt>, <tt>:bottom</tt>, <tt>:left</tt>, <tt>:right</tt>, or <tt>:all</tt>.
66
- #
67
- # === Example
68
- #
69
- # Block.new(borders: [:left, :right]).borders # => [:left, :right]
70
-
71
- ##
72
- # :attr_reader: border_style
73
- # Full style (colors/modifiers) for the border lines.
74
- #
75
- # A Style object or Hash with <tt>:fg</tt>, <tt>:bg</tt>, and <tt>:modifiers</tt>.
76
- # This allows borders to be bold, italic, colored, etc.
77
-
78
- ##
79
- # :attr_reader: border_type
80
- # Visual style of the border lines.
81
- #
82
- # One of <tt>:plain</tt>, <tt>:rounded</tt>, <tt>:double</tt>, <tt>:thick</tt>, etc.
83
-
84
- ##
85
- # :attr_reader: border_set
86
- # Custom characters for the border lines.
87
- #
88
- # A Hash with keys defining the characters for the borders.
89
- # Keys: <tt>:top_left</tt>, <tt>:top_right</tt>, <tt>:bottom_left</tt>, <tt>:bottom_right</tt>,
90
- # <tt>:vertical_left</tt>, <tt>:vertical_right</tt>, <tt>:horizontal_top</tt>, <tt>:horizontal_bottom</tt>.
91
- #
92
- # Providing this overrides <tt>border_type</tt>.
93
- #
94
- #
95
- # === Example
96
- #
97
- # Block.new(border_set: { top_left: "1", top_right: "2", bottom_left: "3", bottom_right: "4", vertical_left: "5", vertical_right: "6", horizontal_top: "7", horizontal_bottom: "8" })
98
-
99
- ##
100
- # :attr_reader: style
101
- # Base style (colors/modifiers) for the block content.
102
-
103
- ##
104
- # :attr_reader: padding
105
- # Inner padding.
106
- #
107
- # Can be a single <tt>Integer</tt> (uniform) or a 4-element <tt>Array</tt> (left, right, top, bottom).
108
- #
109
- # === Example
110
- #
111
- #--
112
- # SPDX-SnippetBegin
113
- # SPDX-FileCopyrightText: 2025 Kerrick Long
114
- # SPDX-License-Identifier: MIT-0
115
- #++
116
- # Block.new(padding: 2).padding # => 2
117
- # Block.new(padding: [1, 1, 0, 0]).padding # => [1, 1, 0, 0]
118
- #--
119
- # SPDX-SnippetEnd
120
- #++
121
-
122
- ##
123
- # :attr_reader: children
124
- # Widgets to render inside the block (optional).
125
- #
126
- # When provided, each child widget is rendered within the block's area.
127
- #
128
- # === Example
129
- #
130
- #--
131
- # SPDX-SnippetBegin
132
- # SPDX-FileCopyrightText: 2025 Kerrick Long
133
- # SPDX-License-Identifier: MIT-0
134
- #++
135
- # Block.new(
136
- # title: "Content",
137
- # borders: [:all],
138
- # children: [Paragraph.new(text: "Hello")]
139
- # )
140
- #--
141
- # SPDX-SnippetEnd
142
- #++
143
-
144
- # Creates a new Block.
145
- #
146
- # [title]
147
- # Main title string (optional).
148
- # [titles]
149
- # Array of additional titles (optional).
150
- # [title_alignment]
151
- # Alignment symbol: <tt>:left</tt> (default), <tt>:center</tt>, <tt>:right</tt>.
152
- # [title_style]
153
- # Base style for all titles (optional).
154
- # [borders]
155
- # Array of borders to show: <tt>:top</tt>, <tt>:bottom</tt>, <tt>:left</tt>, <tt>:right</tt>, or <tt>:all</tt> (default).
156
- # [border_style]
157
- # Style object or Hash for the border lines.
158
- # [border_type]
159
- # Symbol: <tt>:plain</tt> (default), <tt>:rounded</tt>, <tt>:double</tt>, <tt>:thick</tt>, <tt>:hidden</tt>, <tt>:quadrant_inside</tt>, <tt>:quadrant_outside</tt>.
160
- # [border_set]
161
- # Hash: Custom characters for the border lines. Unique characters are interned (leaked) permanently, so avoid infinite dynamic variations.
162
- # [style]
163
- # Style object or Hash for the block's content area.
164
- # [padding]
165
- # Integer (uniform) or Array[4] (left, right, top, bottom).
166
- # [children]
167
- # Array of widgets to render inside the block (optional).
168
- def initialize(title: nil, titles: [], title_alignment: nil, title_style: nil, borders: [:all], border_style: nil, border_type: nil, border_set: nil, style: nil, padding: 0, children: [])
169
- if border_set
170
- border_set = border_set.dup
171
- %i[top_left top_right bottom_left bottom_right vertical_left vertical_right horizontal_top horizontal_bottom].each do |long_key|
172
- short_key = long_key.to_s.split("_").map { |s| s[0] }.join
173
- if (val = border_set.delete(short_key.to_sym) || border_set.delete(short_key))
174
- border_set[long_key] = val
175
- end
176
- end
177
- end
178
- coerced_padding = if padding.is_a?(Array)
179
- padding.map { |v| Integer(v) }
180
- else
181
- Integer(padding)
182
- end
183
- super(
184
- title:,
185
- titles:,
186
- title_alignment:,
187
- title_style:,
188
- borders:,
189
- border_style:,
190
- border_type:,
191
- border_set:,
192
- style:,
193
- padding: coerced_padding,
194
- children:
195
- )
196
- end
197
- end
198
- end