ratatui_ruby 0.9.1 → 0.10.1

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 +113 -0
  8. data/README.md +17 -0
  9. data/REUSE.toml +5 -0
  10. data/Rakefile +1 -1
  11. data/Steepfile +49 -0
  12. data/doc/concepts/debugging.md +401 -0
  13. data/doc/getting_started/quickstart.md +8 -3
  14. data/doc/images/app_all_events.png +0 -0
  15. data/doc/images/app_color_picker.png +0 -0
  16. data/doc/images/app_debugging_showcase.gif +0 -0
  17. data/doc/images/app_debugging_showcase.png +0 -0
  18. data/doc/images/app_login_form.png +0 -0
  19. data/doc/images/app_stateful_interaction.png +0 -0
  20. data/doc/images/verify_quickstart_dsl.png +0 -0
  21. data/doc/images/verify_quickstart_layout.png +0 -0
  22. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  23. data/doc/images/verify_readme_usage.png +0 -0
  24. data/doc/images/widget_barchart.png +0 -0
  25. data/doc/images/widget_block.png +0 -0
  26. data/doc/images/widget_box.png +0 -0
  27. data/doc/images/widget_calendar.png +0 -0
  28. data/doc/images/widget_canvas.png +0 -0
  29. data/doc/images/widget_cell.png +0 -0
  30. data/doc/images/widget_center.png +0 -0
  31. data/doc/images/widget_chart.png +0 -0
  32. data/doc/images/widget_gauge.png +0 -0
  33. data/doc/images/widget_layout_split.png +0 -0
  34. data/doc/images/widget_line_gauge.png +0 -0
  35. data/doc/images/widget_list.png +0 -0
  36. data/doc/images/widget_map.png +0 -0
  37. data/doc/images/widget_overlay.png +0 -0
  38. data/doc/images/widget_popup.png +0 -0
  39. data/doc/images/widget_ratatui_logo.png +0 -0
  40. data/doc/images/widget_ratatui_mascot.png +0 -0
  41. data/doc/images/widget_rect.png +0 -0
  42. data/doc/images/widget_render.png +0 -0
  43. data/doc/images/widget_rich_text.png +0 -0
  44. data/doc/images/widget_scroll_text.png +0 -0
  45. data/doc/images/widget_scrollbar.png +0 -0
  46. data/doc/images/widget_sparkline.png +0 -0
  47. data/doc/images/widget_style_colors.png +0 -0
  48. data/doc/images/widget_table.png +0 -0
  49. data/doc/images/widget_tabs.png +0 -0
  50. data/doc/images/widget_text_width.png +0 -0
  51. data/doc/troubleshooting/async.md +4 -0
  52. data/examples/app_debugging_showcase/README.md +119 -0
  53. data/examples/app_debugging_showcase/app.rb +318 -0
  54. data/examples/widget_canvas/app.rb +19 -14
  55. data/examples/widget_gauge/app.rb +18 -3
  56. data/examples/widget_layout_split/app.rb +10 -4
  57. data/examples/widget_list/app.rb +22 -6
  58. data/examples/widget_rect/app.rb +7 -6
  59. data/examples/widget_rich_text/app.rb +62 -37
  60. data/examples/widget_style_colors/app.rb +26 -47
  61. data/examples/widget_table/app.rb +28 -5
  62. data/examples/widget_text_width/app.rb +6 -4
  63. data/ext/ratatui_ruby/Cargo.lock +48 -1
  64. data/ext/ratatui_ruby/Cargo.toml +6 -2
  65. data/ext/ratatui_ruby/src/color.rs +82 -0
  66. data/ext/ratatui_ruby/src/errors.rs +28 -0
  67. data/ext/ratatui_ruby/src/events.rs +15 -14
  68. data/ext/ratatui_ruby/src/lib.rs +56 -0
  69. data/ext/ratatui_ruby/src/rendering.rs +3 -1
  70. data/ext/ratatui_ruby/src/style.rs +48 -21
  71. data/ext/ratatui_ruby/src/terminal.rs +40 -9
  72. data/ext/ratatui_ruby/src/text.rs +21 -9
  73. data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
  74. data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
  75. data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
  76. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  77. data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
  78. data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
  79. data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
  80. data/lib/ratatui_ruby/buffer/cell.rb +25 -15
  81. data/lib/ratatui_ruby/buffer.rb +134 -2
  82. data/lib/ratatui_ruby/cell.rb +13 -5
  83. data/lib/ratatui_ruby/debug.rb +215 -0
  84. data/lib/ratatui_ruby/event/key.rb +3 -2
  85. data/lib/ratatui_ruby/event.rb +1 -1
  86. data/lib/ratatui_ruby/layout/constraint.rb +49 -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 +1 -1
  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 +6 -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 +42 -11
  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 +84 -5
  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 +80 -63
  208. data/doc/contributors/v1.0.0_blockers.md +0 -870
  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 -30
  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
@@ -41,6 +41,109 @@ module RatatuiRuby
41
41
  def shape_rectangle(...)
42
42
  Widgets::Shape::Rectangle.new(...)
43
43
  end
44
+
45
+ # =====================================
46
+ # Terse Shape Aliases (DWIM)
47
+ # =====================================
48
+
49
+ # Creates a circle shape (terse alias).
50
+ # @return [Widgets::Shape::Circle]
51
+ alias circle shape_circle
52
+
53
+ # Creates a point shape (terse alias).
54
+ # @return [Widgets::Shape::Point]
55
+ alias point shape_point
56
+
57
+ # NOTE: No terse 'rectangle' alias - would conflict with Layout::Rect concept.
58
+ # Use shape_rectangle() explicitly.
59
+
60
+ # Creates a map shape (terse alias).
61
+ # @return [Widgets::Shape::Map]
62
+ alias map shape_map
63
+
64
+ # Creates a label shape (terse alias).
65
+ #
66
+ # Note: shape_label is defined in WidgetFactories.
67
+ # @return [Widgets::Shape::Label]
68
+ def label(first = nil, **kwargs)
69
+ Widgets::Shape::Label.coerce_args(first, kwargs)
70
+ end
71
+
72
+ # =====================================
73
+ # Bidirectional Shape Aliases (*_shape)
74
+ # =====================================
75
+
76
+ # Creates a circle shape (bidirectional alias).
77
+ # @return [Widgets::Shape::Circle]
78
+ alias circle_shape shape_circle
79
+
80
+ # Creates a point shape (bidirectional alias).
81
+ # @return [Widgets::Shape::Point]
82
+ alias point_shape shape_point
83
+
84
+ # Creates a rectangle shape (bidirectional alias).
85
+ # Note: Terse 'rectangle' is intentionally excluded to avoid confusion with Layout::Rect,
86
+ # but 'rectangle_shape' is unambiguous.
87
+ # @return [Widgets::Shape::Rectangle]
88
+ alias rectangle_shape shape_rectangle
89
+
90
+ # Creates a map shape (bidirectional alias).
91
+ # @return [Widgets::Shape::Map]
92
+ alias map_shape shape_map
93
+
94
+ # Creates a label shape (bidirectional alias).
95
+ # @return [Widgets::Shape::Label]
96
+ alias label_shape label
97
+
98
+ # =====================================
99
+ # Shape Dispatcher (TIMTOWTDI)
100
+ # =====================================
101
+
102
+ # Creates a shape by type symbol.
103
+ #
104
+ # Hard-coding method names limits flexibility. Programmatic shape creation
105
+ # from user input or config files requires tedious case statements.
106
+ #
107
+ # This dispatcher routes shape creation through a single entry point.
108
+ # Pass the type as a symbol and the remaining parameters as kwargs.
109
+ #
110
+ # Use it for dynamic shape generation, config-driven UIs, or when you
111
+ # prefer explicit type specification over method names.
112
+ #
113
+ # Also available as: <tt>tui.circle</tt>, <tt>tui.shape_circle</tt>
114
+ #
115
+ # === Examples
116
+ #
117
+ #--
118
+ # SPDX-SnippetBegin
119
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
120
+ # SPDX-License-Identifier: MIT-0
121
+ #++
122
+ # # Direct dispatch
123
+ # tui.shape(:circle, x: 5.0, y: 5.0, radius: 2.5, color: :red)
124
+ #
125
+ # # Dynamic shape creation from config
126
+ # config = { type: :rectangle, x: 0.0, y: 0.0, width: 10.0, height: 10.0 }
127
+ # tui.shape(config[:type], **config.except(:type))
128
+ #--
129
+ # SPDX-SnippetEnd
130
+ #++
131
+ #
132
+ # @param type [Symbol] Shape type: :circle, :line, :point, :rectangle, :map, :label
133
+ # @return [Widgets::Shape::*]
134
+ def shape(type, **)
135
+ case type
136
+ when :circle then shape_circle(**)
137
+ when :line then shape_line(**)
138
+ when :point then shape_point(**)
139
+ when :rectangle then shape_rectangle(**)
140
+ when :map then shape_map(**)
141
+ when :label then label(**)
142
+ else
143
+ raise ArgumentError, "Unknown shape type: #{type.inspect}. " \
144
+ "Valid types: :circle, :line, :point, :rectangle, :map, :label"
145
+ end
146
+ end
44
147
  end
45
148
  end
46
149
  end
@@ -14,8 +14,19 @@ module RatatuiRuby
14
14
  module Core
15
15
  # Draws the given UI node tree to the terminal.
16
16
  # @see RatatuiRuby.draw
17
- def draw(tree = nil, &)
18
- RatatuiRuby.draw(tree, &)
17
+ def draw(tree = nil, &block)
18
+ if tree && block
19
+ raise ArgumentError, "Cannot provide both a tree and a block to draw"
20
+ end
21
+ unless tree || block
22
+ raise ArgumentError, "Must provide either a tree or a block to draw"
23
+ end
24
+
25
+ if block
26
+ RatatuiRuby.draw(&block)
27
+ else
28
+ RatatuiRuby.draw(tree)
29
+ end
19
30
  end
20
31
 
21
32
  # Checks for user input.
@@ -18,10 +18,57 @@ module RatatuiRuby
18
18
  Layout::Rect.new(...)
19
19
  end
20
20
 
21
- # Creates a Layout::Constraint.
21
+ # Creates a Layout::Constraint, with optional type-based dispatch.
22
+ #
23
+ # When called with a type symbol as the first argument, dispatches to
24
+ # the appropriate constraint factory (TIMTOWTDI pattern).
25
+ #
26
+ # Also available as: <tt>tui.percent(50)</tt>, <tt>tui.flex(1)</tt>
27
+ #
28
+ # === Examples
29
+ #
30
+ #--
31
+ # SPDX-SnippetBegin
32
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
33
+ # SPDX-License-Identifier: MIT-0
34
+ #++
35
+ # tui.constraint(:length, 10) # => Constraint.length(10)
36
+ # tui.constraint(:percentage, 50) # => Constraint.percentage(50)
37
+ # tui.constraint(:min, 5) # => Constraint.min(5)
38
+ # tui.constraint(:fill, 2) # => Constraint.fill(2)
39
+ # tui.constraint(:ratio, 1, 3) # => Constraint.ratio(1, 3)
40
+ #--
41
+ # SPDX-SnippetEnd
42
+ #++
22
43
  # @return [Layout::Constraint]
23
- def constraint(...)
24
- Layout::Constraint.new(...)
44
+ def constraint(type_or_arg = nil, arg1 = nil, arg2 = nil, **)
45
+ # Type-based dispatch when first arg is a symbol
46
+ if type_or_arg.is_a?(Symbol)
47
+ case type_or_arg
48
+ when :length, :fixed
49
+ constraint_length(arg1 || raise(ArgumentError, "#{type_or_arg} requires a value"))
50
+ when :percentage, :percent
51
+ constraint_percentage(arg1 || raise(ArgumentError, "#{type_or_arg} requires a value"))
52
+ when :min
53
+ constraint_min(arg1 || raise(ArgumentError, "min requires a value"))
54
+ when :max
55
+ constraint_max(arg1 || raise(ArgumentError, "max requires a value"))
56
+ when :fill, :flex, :fr
57
+ constraint_fill(arg1 || 1)
58
+ when :ratio, :aspect
59
+ n = arg1 || raise(ArgumentError, "ratio requires numerator")
60
+ d = arg2 || raise(ArgumentError, "ratio requires denominator")
61
+ constraint_ratio(n, d)
62
+ else
63
+ # Use to_s since type_or_arg must be a Symbol here
64
+ raise ArgumentError, "Unknown constraint type: :#{type_or_arg}. " \
65
+ "Valid types: :length, :percentage, :min, :max, :fill, :ratio"
66
+ end
67
+ elsif type_or_arg.nil? && arg1.nil?
68
+ Layout::Constraint.new(**)
69
+ else
70
+ Layout::Constraint.new(type_or_arg, arg1, arg2, **)
71
+ end
25
72
  end
26
73
 
27
74
  # Creates a Layout::Constraint.length.
@@ -30,6 +30,48 @@ module RatatuiRuby
30
30
  def scrollbar_state(...)
31
31
  ScrollbarState.new(...)
32
32
  end
33
+
34
+ # =====================================
35
+ # State Dispatcher (TIMTOWTDI)
36
+ # =====================================
37
+
38
+ # Creates a state object by type symbol.
39
+ #
40
+ # Stateful widgets need companion state objects. When building dynamic UIs,
41
+ # the state type must be determined at runtime.
42
+ #
43
+ # This dispatcher routes state creation through a single entry point.
44
+ # Pass the type as a symbol and the remaining parameters.
45
+ #
46
+ # Use it for generic list/table factories or config-driven components.
47
+ #
48
+ # Also available as: <tt>tui.list_state</tt>, <tt>tui.table_state</tt>
49
+ #
50
+ # === Examples
51
+ #
52
+ #--
53
+ # SPDX-SnippetBegin
54
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
55
+ # SPDX-License-Identifier: MIT-0
56
+ #++
57
+ # tui.state(:list, nil) # => ListState with no selection
58
+ # tui.state(:table, 0) # => TableState with row 0 selected
59
+ # tui.state(:scrollbar, 100) # => ScrollbarState with 100 content length
60
+ #--
61
+ # SPDX-SnippetEnd
62
+ #++
63
+ #
64
+ # @param type [Symbol] State type: :list, :table, :scrollbar
65
+ # @return [ListState, TableState, ScrollbarState]
66
+ def state(type, arg = nil)
67
+ case type
68
+ when :list then list_state(arg)
69
+ when :table then table_state(arg)
70
+ when :scrollbar then scrollbar_state(arg || 0)
71
+ else
72
+ raise ArgumentError, "Unknown state type: :#{type}. Valid types: :list, :table, :scrollbar"
73
+ end
74
+ end
33
75
  end
34
76
  end
35
77
  end
@@ -41,6 +41,46 @@ module RatatuiRuby
41
41
  def text_width(string)
42
42
  Text.width(string)
43
43
  end
44
+
45
+ # =====================================
46
+ # Text Dispatcher (TIMTOWTDI)
47
+ # =====================================
48
+
49
+ # Creates a text element by type symbol.
50
+ #
51
+ # Building text programmatically requires knowing which method to call.
52
+ # When the text type comes from config or user input, you need a dispatcher.
53
+ #
54
+ # This method routes text creation through a single entry point.
55
+ # Pass the type as a symbol and the remaining parameters as kwargs.
56
+ #
57
+ # Use it for dynamic text generation or config-driven rendering.
58
+ #
59
+ # Also available as: <tt>tui.span</tt>, <tt>tui.text_span</tt>
60
+ #
61
+ # === Examples
62
+ #
63
+ #--
64
+ # SPDX-SnippetBegin
65
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
66
+ # SPDX-License-Identifier: MIT-0
67
+ #++
68
+ # tui.text(:span, content: "Hello", style: Style.with(fg: :blue))
69
+ # tui.text(:line, spans: [tui.span(content: "World")])
70
+ #--
71
+ # SPDX-SnippetEnd
72
+ #++
73
+ #
74
+ # @param type [Symbol] Text type: :span, :line
75
+ # @return [Text::Span, Text::Line]
76
+ def text(type, **)
77
+ case type
78
+ when :span then text_span(**)
79
+ when :line then text_line(**)
80
+ else
81
+ raise ArgumentError, "Unknown text type: #{type.inspect}. Valid types: :span, :line"
82
+ end
83
+ end
44
84
  end
45
85
  end
46
86
  end
@@ -12,185 +12,260 @@ module RatatuiRuby
12
12
  # Provides convenient access to all Widgets::* classes without
13
13
  # fully qualifying the class names. This is the largest mixin,
14
14
  # covering all renderable UI components.
15
+ #
16
+ # All factories use DWIM hash coercion: both `tui.table(hash)` and
17
+ # `tui.table(**hash)` work correctly.
15
18
  module WidgetFactories
16
19
  # Creates a Widgets::Block.
17
20
  # @return [Widgets::Block]
18
- def block(...)
19
- Widgets::Block.new(...)
21
+ def block(first = nil, **kwargs)
22
+ Widgets::Block.coerce_args(first, kwargs)
20
23
  end
21
24
 
22
25
  # Creates a Widgets::Paragraph.
23
26
  # @return [Widgets::Paragraph]
24
- def paragraph(...)
25
- Widgets::Paragraph.new(...)
27
+ def paragraph(first = nil, **kwargs)
28
+ Widgets::Paragraph.coerce_args(first, kwargs)
26
29
  end
27
30
 
28
31
  # Creates a Widgets::List.
29
32
  # @return [Widgets::List]
30
- def list(...)
31
- Widgets::List.new(...)
33
+ def list(first = nil, **kwargs)
34
+ Widgets::List.coerce_args(first, kwargs)
32
35
  end
33
36
 
34
37
  # Creates a Widgets::ListItem.
35
38
  # @return [Widgets::ListItem]
36
- def list_item(...)
37
- Widgets::ListItem.new(...)
39
+ def list_item(first = nil, **kwargs)
40
+ Widgets::ListItem.coerce_args(first, kwargs)
38
41
  end
39
42
 
43
+ # Creates a Widgets::ListItem (DWIM alias).
44
+ #
45
+ # Terse alias for list_item. Clear in list context.
46
+ #
47
+ # @return [Widgets::ListItem]
48
+ alias item list_item
49
+
40
50
  # Creates a Widgets::Table.
41
51
  # @return [Widgets::Table]
42
- def table(...)
43
- Widgets::Table.new(...)
52
+ def table(first = nil, **kwargs)
53
+ Widgets::Table.coerce_args(first, kwargs)
44
54
  end
45
55
 
46
56
  # Creates a Widgets::Row (for Table rows).
47
57
  # @return [Widgets::Row]
48
- def row(...)
49
- Widgets::Row.new(...)
58
+ def row(first = nil, **kwargs)
59
+ Widgets::Row.coerce_args(first, kwargs)
50
60
  end
51
61
 
52
62
  # Creates a Widgets::Row (alias for table row).
53
63
  # @return [Widgets::Row]
54
- def table_row(...)
55
- Widgets::Row.new(...)
64
+ def table_row(first = nil, **kwargs)
65
+ Widgets::Row.coerce_args(first, kwargs)
56
66
  end
57
67
 
58
68
  # Creates a Widgets::Cell (for Table cells).
59
69
  # @return [Widgets::Cell]
60
- def table_cell(...)
61
- Widgets::Cell.new(...)
70
+ def table_cell(first = nil, **kwargs)
71
+ Widgets::Cell.coerce_args(first, kwargs)
62
72
  end
63
73
 
64
74
  # Creates a Widgets::Tabs.
65
75
  # @return [Widgets::Tabs]
66
- def tabs(...)
67
- Widgets::Tabs.new(...)
76
+ def tabs(first = nil, **kwargs)
77
+ Widgets::Tabs.coerce_args(first, kwargs)
68
78
  end
69
79
 
70
80
  # Creates a Widgets::Gauge.
71
81
  # @return [Widgets::Gauge]
72
- def gauge(...)
73
- Widgets::Gauge.new(...)
82
+ def gauge(first = nil, **kwargs)
83
+ Widgets::Gauge.coerce_args(first, kwargs)
74
84
  end
75
85
 
76
86
  # Creates a Widgets::LineGauge.
77
87
  # @return [Widgets::LineGauge]
78
- def line_gauge(...)
79
- Widgets::LineGauge.new(...)
88
+ def line_gauge(first = nil, **kwargs)
89
+ Widgets::LineGauge.coerce_args(first, kwargs)
80
90
  end
81
91
 
82
92
  # Creates a Widgets::Sparkline.
83
93
  # @return [Widgets::Sparkline]
84
- def sparkline(...)
85
- Widgets::Sparkline.new(...)
94
+ def sparkline(first = nil, **kwargs)
95
+ Widgets::Sparkline.coerce_args(first, kwargs)
86
96
  end
87
97
 
88
98
  # Creates a Widgets::BarChart.
89
99
  # @return [Widgets::BarChart]
90
- def bar_chart(...)
91
- Widgets::BarChart.new(...)
100
+ def bar_chart(first = nil, **kwargs)
101
+ Widgets::BarChart.coerce_args(first, kwargs)
92
102
  end
93
103
 
94
104
  # Creates a Widgets::BarChart::Bar.
95
105
  # @return [Widgets::BarChart::Bar]
96
- def bar(...)
97
- Widgets::BarChart::Bar.new(...)
106
+ def bar(first = nil, **kwargs)
107
+ Widgets::BarChart::Bar.coerce_args(first, kwargs)
98
108
  end
99
109
 
100
110
  # Creates a Widgets::BarChart::BarGroup.
101
111
  # @return [Widgets::BarChart::BarGroup]
102
- def bar_group(...)
103
- Widgets::BarChart::BarGroup.new(...)
112
+ def bar_group(first = nil, **kwargs)
113
+ Widgets::BarChart::BarGroup.coerce_args(first, kwargs)
104
114
  end
105
115
 
106
116
  # Creates a Widgets::BarChart::Bar (alias).
107
117
  # @return [Widgets::BarChart::Bar]
108
- def bar_chart_bar(...)
109
- Widgets::BarChart::Bar.new(...)
118
+ def bar_chart_bar(first = nil, **kwargs)
119
+ Widgets::BarChart::Bar.coerce_args(first, kwargs)
110
120
  end
111
121
 
112
122
  # Creates a Widgets::BarChart::BarGroup (alias).
113
123
  # @return [Widgets::BarChart::BarGroup]
114
- def bar_chart_bar_group(...)
115
- Widgets::BarChart::BarGroup.new(...)
124
+ def bar_chart_bar_group(first = nil, **kwargs)
125
+ Widgets::BarChart::BarGroup.coerce_args(first, kwargs)
116
126
  end
117
127
 
118
128
  # Creates a Widgets::Chart.
119
129
  # @return [Widgets::Chart]
120
- def chart(...)
121
- Widgets::Chart.new(...)
130
+ def chart(first = nil, **kwargs)
131
+ Widgets::Chart.coerce_args(first, kwargs)
122
132
  end
123
133
 
124
134
  # Creates a Widgets::Dataset.
125
135
  # @return [Widgets::Dataset]
126
- def dataset(...)
127
- Widgets::Dataset.new(...)
136
+ def dataset(first = nil, **kwargs)
137
+ Widgets::Dataset.coerce_args(first, kwargs)
128
138
  end
129
139
 
130
140
  # Creates a Widgets::Axis.
131
141
  # @return [Widgets::Axis]
132
- def axis(...)
133
- Widgets::Axis.new(...)
142
+ def axis(first = nil, **kwargs)
143
+ Widgets::Axis.coerce_args(first, kwargs)
134
144
  end
135
145
 
136
146
  # Creates a Widgets::Scrollbar.
137
147
  # @return [Widgets::Scrollbar]
138
- def scrollbar(...)
139
- Widgets::Scrollbar.new(...)
148
+ def scrollbar(first = nil, **kwargs)
149
+ Widgets::Scrollbar.coerce_args(first, kwargs)
140
150
  end
141
151
 
142
152
  # Creates a Widgets::Calendar.
143
153
  # @return [Widgets::Calendar]
144
- def calendar(...)
145
- Widgets::Calendar.new(...)
154
+ def calendar(first = nil, **kwargs)
155
+ Widgets::Calendar.coerce_args(first, kwargs)
146
156
  end
147
157
 
148
158
  # Creates a Widgets::Canvas.
149
159
  # @return [Widgets::Canvas]
150
- def canvas(...)
151
- Widgets::Canvas.new(...)
160
+ def canvas(first = nil, **kwargs)
161
+ Widgets::Canvas.coerce_args(first, kwargs)
152
162
  end
153
163
 
154
164
  # Creates a Widgets::Clear.
155
165
  # @return [Widgets::Clear]
156
- def clear(...)
157
- Widgets::Clear.new(...)
166
+ def clear(first = nil, **kwargs)
167
+ Widgets::Clear.coerce_args(first, kwargs)
158
168
  end
159
169
 
160
170
  # Creates a Widgets::Cursor.
161
171
  # @return [Widgets::Cursor]
162
- def cursor(...)
163
- Widgets::Cursor.new(...)
172
+ def cursor(first = nil, **kwargs)
173
+ Widgets::Cursor.coerce_args(first, kwargs)
164
174
  end
165
175
 
166
176
  # Creates a Widgets::Overlay.
167
177
  # @return [Widgets::Overlay]
168
- def overlay(...)
169
- Widgets::Overlay.new(...)
178
+ def overlay(first = nil, **kwargs)
179
+ Widgets::Overlay.coerce_args(first, kwargs)
170
180
  end
171
181
 
172
182
  # Creates a Widgets::Center.
173
183
  # @return [Widgets::Center]
174
- def center(...)
175
- Widgets::Center.new(...)
184
+ def center(first = nil, **kwargs)
185
+ Widgets::Center.coerce_args(first, kwargs)
176
186
  end
177
187
 
178
188
  # Creates a Widgets::RatatuiLogo.
179
189
  # @return [Widgets::RatatuiLogo]
180
- def ratatui_logo(...)
181
- Widgets::RatatuiLogo.new(...)
190
+ def ratatui_logo(first = nil, **kwargs)
191
+ Widgets::RatatuiLogo.coerce_args(first, kwargs)
182
192
  end
183
193
 
184
194
  # Creates a Widgets::RatatuiMascot.
185
195
  # @return [Widgets::RatatuiMascot]
186
- def ratatui_mascot(...)
187
- Widgets::RatatuiMascot.new(...)
196
+ def ratatui_mascot(first = nil, **kwargs)
197
+ Widgets::RatatuiMascot.coerce_args(first, kwargs)
188
198
  end
189
199
 
190
200
  # Creates a Widgets::Shape::Label.
191
201
  # @return [Widgets::Shape::Label]
192
- def shape_label(...)
193
- Widgets::Shape::Label.new(...)
202
+ def shape_label(first = nil, **kwargs)
203
+ Widgets::Shape::Label.coerce_args(first, kwargs)
204
+ end
205
+
206
+ # =====================================
207
+ # Widget Dispatcher (TIMTOWTDI)
208
+ # =====================================
209
+
210
+ # Creates a widget by type symbol.
211
+ #
212
+ # Plugin systems and config-driven UIs need to instantiate widgets by name.
213
+ # Without a dispatcher, you need tedious case statements or reflection.
214
+ #
215
+ # This method routes widget creation through a single entry point.
216
+ # Pass the type as a symbol and the remaining parameters as kwargs.
217
+ #
218
+ # Use it for dynamic UI generation, dashboard builders, or plugin architectures.
219
+ #
220
+ # Also available as: <tt>tui.paragraph</tt>, <tt>tui.list</tt>, etc.
221
+ #
222
+ # === Examples
223
+ #
224
+ #--
225
+ # SPDX-SnippetBegin
226
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
227
+ # SPDX-License-Identifier: MIT-0
228
+ #++
229
+ # tui.widget(:paragraph, text: "Hello World")
230
+ # tui.widget(:list, items: %w[One Two Three])
231
+ #
232
+ # # Config-driven widget creation
233
+ # widgets_config.each do |cfg|
234
+ # frame.render_widget(tui.widget(cfg[:type], **cfg[:options]), area)
235
+ # end
236
+ #--
237
+ # SPDX-SnippetEnd
238
+ #++
239
+ #
240
+ # @param type [Symbol] Widget type (see error message for full list)
241
+ # @return [Widgets::*]
242
+ def widget(type, first = nil, **)
243
+ case type
244
+ when :block then block(first, **)
245
+ when :paragraph then paragraph(first, **)
246
+ when :list then list(first, **)
247
+ when :table then table(first, **)
248
+ when :tabs then tabs(first, **)
249
+ when :gauge then gauge(first, **)
250
+ when :line_gauge then line_gauge(first, **)
251
+ when :sparkline then sparkline(first, **)
252
+ when :bar_chart then bar_chart(first, **)
253
+ when :chart then chart(first, **)
254
+ when :scrollbar then scrollbar(first, **)
255
+ when :calendar then calendar(first, **)
256
+ when :canvas then canvas(first, **)
257
+ when :clear then clear(first, **)
258
+ when :cursor then cursor(first, **)
259
+ when :overlay then overlay(first, **)
260
+ when :center then center(first, **)
261
+ when :ratatui_logo then ratatui_logo(first, **)
262
+ when :ratatui_mascot then ratatui_mascot(first, **)
263
+ else
264
+ raise ArgumentError, "Unknown widget type: #{type.inspect}. " \
265
+ "Valid types: :block, :paragraph, :list, :table, :tabs, :gauge, :line_gauge, " \
266
+ ":sparkline, :bar_chart, :chart, :scrollbar, :calendar, :canvas, :clear, " \
267
+ ":cursor, :overlay, :center, :ratatui_logo, :ratatui_mascot"
268
+ end
194
269
  end
195
270
  end
196
271
  end
@@ -26,6 +26,27 @@ module RatatuiRuby
26
26
  #
27
27
  # Use it within <tt>RatatuiRuby.run</tt> to build your interface cleanly.
28
28
  #
29
+ # == "Do What I Mean" (DWIM) Coercion
30
+ #
31
+ # The TUI factories add a *DWIM argument coercion layer* that the underlying
32
+ # Widget classes don't have. This means:
33
+ #
34
+ # - <tt>tui.table(rows: [...])</tt> coerces types, normalizes arrays, etc.
35
+ # - <tt>RatatuiRuby::Widgets::Table.new(rows: [...])</tt> passes arguments directly
36
+ #--
37
+ # SPDX-SnippetBegin
38
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
39
+ # SPDX-License-Identifier: MIT-0
40
+ #++
41
+ # to Rust without coercion — invalid types will raise <tt>TypeError</tt>.
42
+ #
43
+ #--
44
+ # SPDX-SnippetEnd
45
+ #++
46
+ # If you bypass the factories and call <tt>Widgets::Table.new</tt> directly,
47
+ # you're responsible for providing correctly-typed arguments. This is useful
48
+ # for debugging (to trigger real Rust TypeErrors) or performance-critical code.
49
+ #
29
50
  # == Thread/Ractor Safety
30
51
  #
31
52
  # Session is an *I/O handle*, not a data object. It has side effects (draw,
@@ -39,7 +60,7 @@ module RatatuiRuby
39
60
  # [Core] Terminal operations: draw, poll_event, get_cell_at, draw_cell.
40
61
  # [LayoutFactories] Layout helpers: rect, constraint_*, layout, layout_split.
41
62
  # [StyleFactories] Style helpers: style.
42
- # [WidgetFactories] Widget creation: block, paragraph, list, table, etc.
63
+ # [WidgetFactories] Widget creation: block, paragraph, list, table, etc. (DWIM coercion)
43
64
  # [TextFactories] Text helpers: span, line, text_width.
44
65
  # [StateFactories] State objects: list_state, table_state, scrollbar_state.
45
66
  # [CanvasFactories] Canvas shapes: shape_map, shape_line, shape_point, etc.
@@ -8,5 +8,5 @@
8
8
  module RatatuiRuby
9
9
  # The version of the ratatui_ruby gem.
10
10
  # See https://semver.org/spec/v2.0.0.html
11
- VERSION = "0.9.1"
11
+ VERSION = "0.10.1"
12
12
  end
@@ -14,6 +14,8 @@ module RatatuiRuby
14
14
  #
15
15
  # BarChart::Bar.new(value: 10, style: Style.new(fg: :red), label: "A")
16
16
  class Bar < Data.define(:value, :label, :style, :value_style, :text_value)
17
+ include CoerceableWidget
18
+
17
19
  ##
18
20
  # :attr_reader: value
19
21
  # The value of the bar (Integer).
@@ -14,6 +14,8 @@ module RatatuiRuby
14
14
  #
15
15
  # BarChart::BarGroup.new(label: "Q1", bars: [BarChart::Bar.new(value: 10), BarChart::Bar.new(value: 20)])
16
16
  class BarGroup < Data.define(:label, :bars)
17
+ include CoerceableWidget
18
+
17
19
  ##
18
20
  # :attr_reader: label
19
21
  # The label of the group (String).