ratatui_ruby 0.6.0 → 0.7.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 (171) 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 +4 -4
  7. data/CHANGELOG.md +35 -0
  8. data/README.md +26 -1
  9. data/doc/application_architecture.md +16 -16
  10. data/doc/application_testing.md +1 -1
  11. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  12. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  13. data/doc/contributors/architectural_overhaul/task.md +37 -0
  14. data/doc/contributors/design/ruby_frontend.md +277 -81
  15. data/doc/contributors/design/rust_backend.md +349 -55
  16. data/doc/contributors/developing_examples.md +5 -5
  17. data/doc/contributors/index.md +7 -5
  18. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  19. data/doc/index.md +11 -6
  20. data/doc/interactive_design.md +2 -2
  21. data/doc/quickstart.md +66 -97
  22. data/doc/v0.7.0_migration.md +236 -0
  23. data/doc/why.md +93 -0
  24. data/examples/app_all_events/README.md +6 -4
  25. data/examples/app_all_events/app.rb +1 -1
  26. data/examples/app_all_events/model/app_model.rb +1 -1
  27. data/examples/app_all_events/model/msg.rb +1 -1
  28. data/examples/app_all_events/update.rb +1 -1
  29. data/examples/app_all_events/view/app_view.rb +1 -1
  30. data/examples/app_all_events/view/controls_view.rb +1 -1
  31. data/examples/app_all_events/view/counts_view.rb +1 -1
  32. data/examples/app_all_events/view/live_view.rb +1 -1
  33. data/examples/app_all_events/view/log_view.rb +1 -1
  34. data/examples/app_color_picker/README.md +7 -5
  35. data/examples/app_color_picker/app.rb +1 -1
  36. data/examples/app_login_form/README.md +2 -0
  37. data/examples/app_stateful_interaction/README.md +2 -0
  38. data/examples/app_stateful_interaction/app.rb +1 -1
  39. data/examples/verify_quickstart_dsl/README.md +4 -3
  40. data/examples/verify_quickstart_dsl/app.rb +1 -1
  41. data/examples/verify_quickstart_layout/README.md +1 -1
  42. data/examples/verify_quickstart_lifecycle/README.md +3 -3
  43. data/examples/verify_quickstart_lifecycle/app.rb +2 -2
  44. data/examples/verify_readme_usage/README.md +1 -1
  45. data/examples/widget_barchart_demo/README.md +2 -1
  46. data/examples/widget_block_demo/README.md +2 -0
  47. data/examples/widget_box_demo/README.md +3 -3
  48. data/examples/widget_calendar_demo/README.md +3 -3
  49. data/examples/widget_calendar_demo/app.rb +5 -1
  50. data/examples/widget_canvas_demo/README.md +3 -3
  51. data/examples/widget_cell_demo/README.md +3 -3
  52. data/examples/widget_center_demo/README.md +3 -3
  53. data/examples/widget_chart_demo/README.md +3 -3
  54. data/examples/widget_gauge_demo/README.md +3 -3
  55. data/examples/widget_layout_split/README.md +3 -3
  56. data/examples/widget_line_gauge_demo/README.md +3 -3
  57. data/examples/widget_list_demo/README.md +3 -3
  58. data/examples/widget_map_demo/README.md +3 -3
  59. data/examples/widget_map_demo/app.rb +2 -2
  60. data/examples/widget_overlay_demo/README.md +36 -0
  61. data/examples/widget_popup_demo/README.md +3 -3
  62. data/examples/widget_ratatui_logo_demo/README.md +3 -3
  63. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  64. data/examples/widget_ratatui_mascot_demo/README.md +3 -3
  65. data/examples/widget_rect/README.md +3 -3
  66. data/examples/widget_render/README.md +3 -3
  67. data/examples/widget_render/app.rb +3 -3
  68. data/examples/widget_rich_text/README.md +3 -3
  69. data/examples/widget_scroll_text/README.md +3 -3
  70. data/examples/widget_scrollbar_demo/README.md +3 -3
  71. data/examples/widget_sparkline_demo/README.md +3 -3
  72. data/examples/widget_style_colors/README.md +3 -3
  73. data/examples/widget_table_demo/README.md +3 -3
  74. data/examples/widget_table_demo/app.rb +19 -4
  75. data/examples/widget_tabs_demo/README.md +3 -3
  76. data/examples/widget_text_width/README.md +3 -3
  77. data/examples/widget_text_width/app.rb +8 -1
  78. data/ext/ratatui_ruby/Cargo.lock +1 -1
  79. data/ext/ratatui_ruby/Cargo.toml +1 -1
  80. data/ext/ratatui_ruby/src/frame.rs +6 -5
  81. data/ext/ratatui_ruby/src/lib.rs +3 -2
  82. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  83. data/ext/ratatui_ruby/src/text.rs +12 -3
  84. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  85. data/ext/ratatui_ruby/src/widgets/table.rs +81 -36
  86. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  87. data/lib/ratatui_ruby/buffer.rb +15 -0
  88. data/lib/ratatui_ruby/frame.rb +8 -8
  89. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  90. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  91. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  92. data/lib/ratatui_ruby/layout.rb +19 -0
  93. data/lib/ratatui_ruby/list_state.rb +2 -2
  94. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  95. data/lib/ratatui_ruby/schema/row.rb +66 -0
  96. data/lib/ratatui_ruby/schema/table.rb +10 -10
  97. data/lib/ratatui_ruby/schema/text.rb +27 -2
  98. data/lib/ratatui_ruby/style/style.rb +81 -0
  99. data/lib/ratatui_ruby/style.rb +15 -0
  100. data/lib/ratatui_ruby/table_state.rb +1 -1
  101. data/lib/ratatui_ruby/test_helper/snapshot.rb +24 -0
  102. data/lib/ratatui_ruby/test_helper/style_assertions.rb +1 -1
  103. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  105. data/lib/ratatui_ruby/tui/core.rb +38 -0
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  107. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  108. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  109. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  110. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  111. data/lib/ratatui_ruby/tui.rb +75 -0
  112. data/lib/ratatui_ruby/version.rb +1 -1
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  115. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  116. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  117. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  118. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  119. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  120. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  121. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  122. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  126. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  127. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  136. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  137. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  138. data/lib/ratatui_ruby/widgets.rb +40 -0
  139. data/lib/ratatui_ruby.rb +23 -39
  140. data/sig/examples/app_all_events/view.rbs +1 -1
  141. data/sig/examples/app_all_events/view_state.rbs +1 -1
  142. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  143. data/sig/ratatui_ruby/schema/table.rbs +1 -1
  144. data/sig/ratatui_ruby/schema/text.rbs +1 -0
  145. data/sig/ratatui_ruby/session.rbs +29 -49
  146. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  147. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  148. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  149. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  150. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  151. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  152. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  153. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  154. data/sig/ratatui_ruby/tui.rbs +19 -0
  155. data/tasks/autodoc.rake +1 -35
  156. data/tasks/sourcehut.rake +4 -1
  157. metadata +62 -15
  158. data/doc/contributors/dwim_dx.md +0 -366
  159. data/doc/contributors/examples_audit/p1_high.md +0 -21
  160. data/doc/contributors/examples_audit/p2_moderate.md +0 -81
  161. data/doc/contributors/examples_audit.md +0 -41
  162. data/doc/images/app_analytics.png +0 -0
  163. data/doc/images/app_custom_widget.png +0 -0
  164. data/doc/images/app_mouse_events.png +0 -0
  165. data/doc/images/widget_table_flex.png +0 -0
  166. data/lib/ratatui_ruby/session/autodoc.rb +0 -482
  167. data/lib/ratatui_ruby/session.rb +0 -178
  168. data/tasks/autodoc/inventory.rb +0 -63
  169. data/tasks/autodoc/notice.rb +0 -26
  170. data/tasks/autodoc/rbs.rb +0 -38
  171. data/tasks/autodoc/rdoc.rb +0 -45
@@ -0,0 +1,195 @@
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 TUI
8
+ # Widget factory methods for Session.
9
+ #
10
+ # Provides convenient access to all Widgets::* classes without
11
+ # fully qualifying the class names. This is the largest mixin,
12
+ # covering all renderable UI components.
13
+ module WidgetFactories
14
+ # Creates a Widgets::Block.
15
+ # @return [Widgets::Block]
16
+ def block(...)
17
+ Widgets::Block.new(...)
18
+ end
19
+
20
+ # Creates a Widgets::Paragraph.
21
+ # @return [Widgets::Paragraph]
22
+ def paragraph(...)
23
+ Widgets::Paragraph.new(...)
24
+ end
25
+
26
+ # Creates a Widgets::List.
27
+ # @return [Widgets::List]
28
+ def list(...)
29
+ Widgets::List.new(...)
30
+ end
31
+
32
+ # Creates a Widgets::ListItem.
33
+ # @return [Widgets::ListItem]
34
+ def list_item(...)
35
+ Widgets::ListItem.new(...)
36
+ end
37
+
38
+ # Creates a Widgets::Table.
39
+ # @return [Widgets::Table]
40
+ def table(...)
41
+ Widgets::Table.new(...)
42
+ end
43
+
44
+ # Creates a Widgets::Row (for Table rows).
45
+ # @return [Widgets::Row]
46
+ def row(...)
47
+ Widgets::Row.new(...)
48
+ end
49
+
50
+ # Creates a Widgets::Row (alias for table row).
51
+ # @return [Widgets::Row]
52
+ def table_row(...)
53
+ Widgets::Row.new(...)
54
+ end
55
+
56
+ # Creates a Widgets::Cell (for Table cells).
57
+ # @return [Widgets::Cell]
58
+ def table_cell(...)
59
+ Widgets::Cell.new(...)
60
+ end
61
+
62
+ # Creates a Widgets::Tabs.
63
+ # @return [Widgets::Tabs]
64
+ def tabs(...)
65
+ Widgets::Tabs.new(...)
66
+ end
67
+
68
+ # Creates a Widgets::Gauge.
69
+ # @return [Widgets::Gauge]
70
+ def gauge(...)
71
+ Widgets::Gauge.new(...)
72
+ end
73
+
74
+ # Creates a Widgets::LineGauge.
75
+ # @return [Widgets::LineGauge]
76
+ def line_gauge(...)
77
+ Widgets::LineGauge.new(...)
78
+ end
79
+
80
+ # Creates a Widgets::Sparkline.
81
+ # @return [Widgets::Sparkline]
82
+ def sparkline(...)
83
+ Widgets::Sparkline.new(...)
84
+ end
85
+
86
+ # Creates a Widgets::BarChart.
87
+ # @return [Widgets::BarChart]
88
+ def bar_chart(...)
89
+ Widgets::BarChart.new(...)
90
+ end
91
+
92
+ # Creates a Widgets::BarChart::Bar.
93
+ # @return [Widgets::BarChart::Bar]
94
+ def bar(...)
95
+ Widgets::BarChart::Bar.new(...)
96
+ end
97
+
98
+ # Creates a Widgets::BarChart::BarGroup.
99
+ # @return [Widgets::BarChart::BarGroup]
100
+ def bar_group(...)
101
+ Widgets::BarChart::BarGroup.new(...)
102
+ end
103
+
104
+ # Creates a Widgets::BarChart::Bar (alias).
105
+ # @return [Widgets::BarChart::Bar]
106
+ def bar_chart_bar(...)
107
+ Widgets::BarChart::Bar.new(...)
108
+ end
109
+
110
+ # Creates a Widgets::BarChart::BarGroup (alias).
111
+ # @return [Widgets::BarChart::BarGroup]
112
+ def bar_chart_bar_group(...)
113
+ Widgets::BarChart::BarGroup.new(...)
114
+ end
115
+
116
+ # Creates a Widgets::Chart.
117
+ # @return [Widgets::Chart]
118
+ def chart(...)
119
+ Widgets::Chart.new(...)
120
+ end
121
+
122
+ # Creates a Widgets::Dataset.
123
+ # @return [Widgets::Dataset]
124
+ def dataset(...)
125
+ Widgets::Dataset.new(...)
126
+ end
127
+
128
+ # Creates a Widgets::Axis.
129
+ # @return [Widgets::Axis]
130
+ def axis(...)
131
+ Widgets::Axis.new(...)
132
+ end
133
+
134
+ # Creates a Widgets::Scrollbar.
135
+ # @return [Widgets::Scrollbar]
136
+ def scrollbar(...)
137
+ Widgets::Scrollbar.new(...)
138
+ end
139
+
140
+ # Creates a Widgets::Calendar.
141
+ # @return [Widgets::Calendar]
142
+ def calendar(...)
143
+ Widgets::Calendar.new(...)
144
+ end
145
+
146
+ # Creates a Widgets::Canvas.
147
+ # @return [Widgets::Canvas]
148
+ def canvas(...)
149
+ Widgets::Canvas.new(...)
150
+ end
151
+
152
+ # Creates a Widgets::Clear.
153
+ # @return [Widgets::Clear]
154
+ def clear(...)
155
+ Widgets::Clear.new(...)
156
+ end
157
+
158
+ # Creates a Widgets::Cursor.
159
+ # @return [Widgets::Cursor]
160
+ def cursor(...)
161
+ Widgets::Cursor.new(...)
162
+ end
163
+
164
+ # Creates a Widgets::Overlay.
165
+ # @return [Widgets::Overlay]
166
+ def overlay(...)
167
+ Widgets::Overlay.new(...)
168
+ end
169
+
170
+ # Creates a Widgets::Center.
171
+ # @return [Widgets::Center]
172
+ def center(...)
173
+ Widgets::Center.new(...)
174
+ end
175
+
176
+ # Creates a Widgets::RatatuiLogo.
177
+ # @return [Widgets::RatatuiLogo]
178
+ def ratatui_logo(...)
179
+ Widgets::RatatuiLogo.new(...)
180
+ end
181
+
182
+ # Creates a Widgets::RatatuiMascot.
183
+ # @return [Widgets::RatatuiMascot]
184
+ def ratatui_mascot(...)
185
+ Widgets::RatatuiMascot.new(...)
186
+ end
187
+
188
+ # Creates a Widgets::Shape::Label.
189
+ # @return [Widgets::Shape::Label]
190
+ def shape_label(...)
191
+ Widgets::Shape::Label.new(...)
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "tui/core"
7
+ require_relative "tui/layout_factories"
8
+ require_relative "tui/style_factories"
9
+ require_relative "tui/widget_factories"
10
+ require_relative "tui/text_factories"
11
+ require_relative "tui/state_factories"
12
+ require_relative "tui/canvas_factories"
13
+ require_relative "tui/buffer_factories"
14
+
15
+ module RatatuiRuby
16
+ # Manages the terminal lifecycle and provides a concise API for the render loop.
17
+ #
18
+ # Writing a TUI loop involves repetitive boilerplate. You constantly instantiate widgets
19
+ # (<tt>RatatuiRuby::Widgets::Paragraph.new</tt>) and call global methods (<tt>RatatuiRuby.draw</tt>).
20
+ # This is verbose and hard to read.
21
+ #
22
+ # The Session object simplifies this. It acts as a factory and a facade. It provides short helper
23
+ # methods for every widget and delegates core commands to the main module.
24
+ #
25
+ # Use it within <tt>RatatuiRuby.run</tt> to build your interface cleanly.
26
+ #
27
+ # == Thread/Ractor Safety
28
+ #
29
+ # Session is an *I/O handle*, not a data object. It has side effects (draw,
30
+ # poll_event) and is intentionally *not* Ractor-shareable. Caching it in
31
+ # instance variables (<tt>@tui = tui</tt>) during your application's run loop
32
+ # is fine. However, do not include it in immutable Models/Messages or
33
+ # pass it to other Ractors.
34
+ #
35
+ # == Included Mixins
36
+ #
37
+ # [Core] Terminal operations: draw, poll_event, get_cell_at, draw_cell.
38
+ # [LayoutFactories] Layout helpers: rect, constraint_*, layout, layout_split.
39
+ # [StyleFactories] Style helpers: style.
40
+ # [WidgetFactories] Widget creation: block, paragraph, list, table, etc.
41
+ # [TextFactories] Text helpers: span, line, text_width.
42
+ # [StateFactories] State objects: list_state, table_state, scrollbar_state.
43
+ # [CanvasFactories] Canvas shapes: shape_map, shape_line, shape_point, etc.
44
+ # [BufferFactories] Buffer inspection: cell.
45
+ #
46
+ # === Examples
47
+ #
48
+ # ==== Basic Usage (Recommended)
49
+ #
50
+ # RatatuiRuby.run do |tui|
51
+ # loop do
52
+ # tui.draw \
53
+ # tui.paragraph \
54
+ # text: "Hello, Ratatui! Press 'q' to quit.",
55
+ # alignment: :center,
56
+ # block: tui.block(
57
+ # title: "My Ruby TUI App",
58
+ # borders: [:all],
59
+ # border_color: "cyan"
60
+ # )
61
+ # event = tui.poll_event
62
+ # break if event == "q" || event == :ctrl_c
63
+ # end
64
+ # end
65
+ class TUI
66
+ include Core
67
+ include LayoutFactories
68
+ include StyleFactories
69
+ include WidgetFactories
70
+ include TextFactories
71
+ include StateFactories
72
+ include CanvasFactories
73
+ include BufferFactories
74
+ end
75
+ end
@@ -6,5 +6,5 @@
6
6
  module RatatuiRuby
7
7
  # The version of the ratatui_ruby gem.
8
8
  # See https://semver.org/spec/v2.0.0.html
9
- VERSION = "0.6.0"
9
+ VERSION = "0.7.0"
10
10
  end
@@ -0,0 +1,47 @@
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
+ module Widgets
8
+ class BarChart
9
+ # A bar in a grouped bar chart.
10
+ #
11
+ # === Examples
12
+ #
13
+ # BarChart::Bar.new(value: 10, style: Style.new(fg: :red), label: "A")
14
+ class Bar < Data.define(:value, :label, :style, :value_style, :text_value)
15
+ ##
16
+ # :attr_reader: value
17
+ # The value of the bar (Integer).
18
+
19
+ ##
20
+ # :attr_reader: label
21
+ # The label of the bar (optional String, Text::Span, or Text::Line for rich styling).
22
+
23
+ ##
24
+ # :attr_reader: style
25
+ # The style of the bar (optional Style).
26
+
27
+ ##
28
+ # :attr_reader: value_style
29
+ # The style of the value (optional Style).
30
+
31
+ ##
32
+ # :attr_reader: text_value
33
+ # The text to display as the value (optional String, Text::Span, or Text::Line for rich styling).
34
+
35
+ def initialize(value:, label: nil, style: nil, value_style: nil, text_value: nil)
36
+ super(
37
+ value: Integer(value),
38
+ label:,
39
+ style:,
40
+ value_style:,
41
+ text_value:
42
+ )
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,25 @@
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
+ module Widgets
8
+ class BarChart
9
+ # A group of bars in a grouped bar chart.
10
+ #
11
+ # === Examples
12
+ #
13
+ # BarChart::BarGroup.new(label: "Q1", bars: [BarChart::Bar.new(value: 10), BarChart::Bar.new(value: 20)])
14
+ class BarGroup < Data.define(:label, :bars)
15
+ ##
16
+ # :attr_reader: label
17
+ # The label of the group (String).
18
+
19
+ ##
20
+ # :attr_reader: bars
21
+ # The bars in the group (Array of Bar).
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,239 @@
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
+ module Widgets
8
+ # Displays categorical data as bars.
9
+ #
10
+ # Raw tables of numbers are hard to scan. Comparing magnitudes requires mental arithmetic, which slows down decision-making.
11
+ #
12
+ # This widget visualizes the data. It renders vertical bars proportional to their value.
13
+ #
14
+ # Use it to compare server loads, sales figures, or any discrete datasets.
15
+ #
16
+ # {rdoc-image:/doc/images/widget_barchart_demo.png}[link:/examples/widget_barchart_demo/app_rb.html]
17
+ #
18
+ # === Example
19
+ #
20
+ # Run the interactive demo from the terminal:
21
+ #
22
+ # ruby examples/widget_barchart_demo/app.rb
23
+ #
24
+ # # Grouped Bar Chart
25
+ # BarChart.new(
26
+ # data: [
27
+ # BarGroup.new(label: "Q1", bars: [Bar.new(value: 40), Bar.new(value: 45)]),
28
+ # BarGroup.new(label: "Q2", bars: [Bar.new(value: 50), Bar.new(value: 55)])
29
+ # ],
30
+ # bar_width: 5,
31
+ # group_gap: 3
32
+ # )
33
+ class BarChart < Data.define(:data, :bar_width, :bar_gap, :group_gap, :max, :style, :block, :direction, :label_style, :value_style, :bar_set)
34
+ ##
35
+ ##
36
+ ##
37
+ ##
38
+ # :attr_reader: data
39
+ # The data to display.
40
+ #
41
+ # Supports multiple formats:
42
+ # [<tt>Hash</tt>]
43
+ # Mapping labels (<tt>String</tt> or <tt>Symbol</tt>) to values (<tt>Integer</tt>).
44
+ # [<tt>Array</tt> of tuples]
45
+ # Ordered list of <tt>["Label", Value]</tt> or <tt>["Label", Value, Style]</tt> pairs.
46
+ # [<tt>Array</tt> of <tt>BarChart::BarGroup</tt>]
47
+ # List of <tt>BarChart::BarGroup</tt> objects for grouped charts.
48
+ #
49
+ # === Examples
50
+ #
51
+ # Hash (Simple):
52
+ # { "Apples" => 10, :Oranges => 15 }
53
+ #
54
+ # Array of Tuples (Ordered):
55
+ # [["Mon", 20], ["Tue", 30], ["Wed", 25]]
56
+ #
57
+ # BarGroup (Grouped):
58
+ # [
59
+ # RatatuiRuby::BarChart::BarGroup.new(label: "Q1", bars: [
60
+ # RatatuiRuby::BarChart::Bar.new(value: 50, label: "Rev"),
61
+ # RatatuiRuby::BarChart::Bar.new(value: 30, label: "Cost")
62
+ # ])
63
+ # ]
64
+
65
+ ##
66
+ # :attr_reader: bar_width
67
+ # Width of each bar in characters.
68
+
69
+ ##
70
+ # :attr_reader: bar_gap
71
+ # Spaces between bars.
72
+
73
+ ##
74
+ # :attr_reader: group_gap
75
+ # Spaces between groups (for grouped bar charts).
76
+
77
+ ##
78
+ # :attr_reader: max
79
+ # Maximum value for the Y-axis (optional).
80
+ #
81
+ # If nil, it is calculated from the data.
82
+
83
+ ##
84
+ # :attr_reader: style
85
+ # Style for the bars.
86
+
87
+ ##
88
+ # :attr_reader: block
89
+ # Optional wrapping block.
90
+
91
+ ##
92
+ # :attr_reader: label_style
93
+ # Style for the bar labels (optional).
94
+
95
+ ##
96
+ # :attr_reader: value_style
97
+ # Style for the bar values (optional).
98
+
99
+ ##
100
+ # :attr_reader: bar_set
101
+ # Custom characters for the bars (optional).
102
+ #
103
+ # A Hash with keys defining the characters for the bars.
104
+ # 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>.
105
+ #
106
+ # You can also use integers (0-8) as keys, where 0 is empty, 4 is half, and 8 is full.
107
+ #
108
+ # Alternatively, you can pass an Array of 9 strings, where index 0 is empty and index 8 is full.
109
+ #
110
+ # === Examples
111
+ #
112
+ # bar_set: {
113
+ # empty: " ",
114
+ # one_eighth: " ",
115
+ # one_quarter: "▂",
116
+ # three_eighths: "▃",
117
+ # half: "▄",
118
+ # five_eighths: "▅",
119
+ # three_quarters: "▆",
120
+ # seven_eighths: "▇",
121
+ # full: "█"
122
+ # }
123
+ #
124
+ # # Numeric keys (0-8)
125
+ # bar_set: {
126
+ # 0 => " ", 1 => " ", 2 => "▂", 3 => "▃", 4 => "▄", 5 => "▅", 6 => "▆", 7 => "▇", 8 => "█"
127
+ # }
128
+ #
129
+ # # Array (9 items)
130
+ # bar_set: [" ", " ", "▂", "▃", "▄", "▅", "▆", "▇", "█"]
131
+
132
+ BAR_KEYS = %i[empty one_eighth one_quarter three_eighths half five_eighths three_quarters seven_eighths full].freeze
133
+
134
+ # Creates a new BarChart widget.
135
+ #
136
+ # [data]
137
+ # Data to display. Hash, Array of arrays, or Array of BarGroup.
138
+ # [bar_width]
139
+ # Width of each bar (Integer).
140
+ # [bar_gap]
141
+ # Gap between bars (Integer).
142
+ # [group_gap]
143
+ # Gap between groups (Integer).
144
+ # [max]
145
+ # Maximum value of the bar chart (Integer).
146
+ # [style]
147
+ # Base style for the widget (Style).
148
+ # [block]
149
+ # Block to render around the chart (Block).
150
+ # [direction]
151
+ # Direction of the bars (:vertical or :horizontal).
152
+ # [label_style]
153
+ # Style object for labels (optional).
154
+ # [value_style]
155
+ # Style object for values (optional).
156
+ # [bar_set]
157
+ # Hash or Array: Custom characters for the bars.
158
+ 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)
159
+ if bar_set
160
+ if bar_set.is_a?(Array) && bar_set.size == 9
161
+ # Convert Array to Hash using BAR_KEYS order
162
+ bar_set = BAR_KEYS.zip(bar_set).to_h
163
+ else
164
+ bar_set = bar_set.dup
165
+ # Normalize numeric keys (0-8) to symbolic keys
166
+ BAR_KEYS.each_with_index do |key, i|
167
+ if (val = bar_set.delete(i) || bar_set.delete(i.to_s))
168
+ bar_set[key] = val
169
+ end
170
+ end
171
+ end
172
+ end
173
+
174
+ # Normalize data to Array of BarGroup
175
+ data = if data.is_a?(Hash)
176
+ if direction == :horizontal
177
+ bars = data.map do |label, value|
178
+ Bar.new(value:, label: label.to_s)
179
+ end
180
+ [BarGroup.new(label: "", bars:)]
181
+ else
182
+ data.map do |label, value|
183
+ BarGroup.new(label: label.to_s, bars: [Bar.new(value:)])
184
+ end
185
+ end
186
+ elsif data.is_a?(Array)
187
+ if data.empty?
188
+ []
189
+ elsif data.first.is_a?(BarGroup)
190
+ data
191
+ elsif data.first.is_a?(Array)
192
+ # Tuples
193
+ if direction == :horizontal
194
+ bars = data.map do |item|
195
+ label = item[0].to_s
196
+ value = item[1]
197
+ style = item[2]
198
+
199
+ bar = Bar.new(value:, label:)
200
+ bar = bar.with(style:) if style
201
+ bar
202
+ end
203
+ [BarGroup.new(label: "", bars:)]
204
+ else
205
+ data.map do |item|
206
+ label = item[0].to_s
207
+ value = item[1]
208
+ style = item[2]
209
+
210
+ bar = Bar.new(value:)
211
+ bar = bar.with(style:) if style
212
+ BarGroup.new(label:, bars: [bar])
213
+ end
214
+ end
215
+ else
216
+ # Fallback
217
+ data
218
+ end
219
+ else
220
+ data
221
+ end
222
+
223
+ super(
224
+ data:,
225
+ bar_width: Integer(bar_width),
226
+ bar_gap: Integer(bar_gap),
227
+ group_gap: Integer(group_gap),
228
+ max: max.nil? ? nil : Integer(max),
229
+ style:,
230
+ block:,
231
+ direction:,
232
+ label_style:,
233
+ value_style:,
234
+ bar_set:
235
+ )
236
+ end
237
+ end
238
+ end
239
+ end