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
@@ -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.0"
11
+ VERSION = "0.10.0"
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).
@@ -41,6 +41,8 @@ module RatatuiRuby
41
41
  # SPDX-SnippetEnd
42
42
  #++
43
43
  class BarChart < Data.define(:data, :bar_width, :bar_gap, :group_gap, :max, :style, :block, :direction, :label_style, :value_style, :bar_set)
44
+ include CoerceableWidget
45
+
44
46
  ##
45
47
  ##
46
48
  ##
@@ -215,19 +217,25 @@ module RatatuiRuby
215
217
  # SPDX-SnippetEnd
216
218
  #++
217
219
  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)
218
- if bar_set && !bar_set.is_a?(Symbol)
219
- if bar_set.is_a?(Array) && bar_set.size == 9
220
- # Convert Array to Hash using BAR_KEYS order
221
- bar_set = BAR_KEYS.zip(bar_set).to_h
222
- else
223
- bar_set = bar_set.dup
224
- # Normalize numeric keys (0-8) to symbolic keys
225
- BAR_KEYS.each_with_index do |key, i|
226
- if (val = bar_set.delete(i) || bar_set.delete(i.to_s))
227
- bar_set[key] = val
228
- end
229
- end
230
- end
220
+ # Normalize bar_set to Hash[Symbol, String] if provided as Array or Hash
221
+ bar_set = case bar_set
222
+ when Symbol, nil
223
+ bar_set
224
+ when Array
225
+ # Convert Array to Hash using BAR_KEYS order
226
+ BAR_KEYS.zip(bar_set).to_h
227
+ when Hash
228
+ # @type var raw_hash: Hash[untyped, untyped]
229
+ raw_hash = bar_set.dup
230
+ normalized = {} #: Hash[Symbol, String]
231
+ # Normalize numeric keys (0-8) to symbolic keys
232
+ BAR_KEYS.each_with_index do |key, i|
233
+ val = raw_hash.delete(i) || raw_hash.delete(i.to_s) || raw_hash.delete(key)
234
+ normalized[key] = val.to_s if val
235
+ end
236
+ normalized
237
+ else
238
+ bar_set
231
239
  end
232
240
 
233
241
  # Normalize data to Array of BarGroup
@@ -248,12 +256,13 @@ module RatatuiRuby
248
256
  elsif data.first.is_a?(BarGroup)
249
257
  data
250
258
  elsif data.first.is_a?(Array)
251
- # Tuples
259
+ # Tuples - use type assertion for Steep
252
260
  if direction == :horizontal
253
261
  bars = data.map do |item|
254
- label = item[0].to_s
255
- value = item[1]
256
- style = item[2]
262
+ tuple = item #: Array[untyped]
263
+ label = tuple[0].to_s
264
+ value = tuple[1]
265
+ style = tuple[2]
257
266
 
258
267
  bar = Bar.new(value:, label:)
259
268
  bar = bar.with(style:) if style
@@ -262,9 +271,10 @@ module RatatuiRuby
262
271
  [BarGroup.new(label: "", bars:)]
263
272
  else
264
273
  data.map do |item|
265
- label = item[0].to_s
266
- value = item[1]
267
- style = item[2]
274
+ tuple = item #: Array[untyped]
275
+ label = tuple[0].to_s
276
+ value = tuple[1]
277
+ style = tuple[2]
268
278
 
269
279
  bar = Bar.new(value:)
270
280
  bar = bar.with(style:) if style
@@ -23,6 +23,8 @@ module RatatuiRuby
23
23
  #
24
24
  # ruby examples/widget_box/app.rb
25
25
  class Block < Data.define(:title, :titles, :title_alignment, :title_style, :borders, :border_style, :border_type, :border_set, :style, :padding, :children)
26
+ include CoerceableWidget
27
+
26
28
  ##
27
29
  # :attr_reader: title
28
30
  # The main title displayed on the top border.
@@ -170,8 +172,8 @@ module RatatuiRuby
170
172
  if border_set
171
173
  border_set = border_set.dup
172
174
  %i[top_left top_right bottom_left bottom_right vertical_left vertical_right horizontal_top horizontal_bottom].each do |long_key|
173
- short_key = long_key.to_s.split("_").map { |s| s[0] }.join
174
- if (val = border_set.delete(short_key.to_sym) || border_set.delete(short_key))
175
+ short_key = long_key.to_s.split("_").map { |s| s[0] }.join.to_sym
176
+ if (val = border_set.delete(short_key))
175
177
  border_set[long_key] = val
176
178
  end
177
179
  end
@@ -237,12 +239,18 @@ module RatatuiRuby
237
239
  top_border = has_border.call(:top) ? 1 : 0
238
240
  bottom_border = has_border.call(:bottom) ? 1 : 0
239
241
 
240
- # Calculate padding offsets
241
- if padding.is_a?(Array)
242
+ # Calculate padding offsets - ensure all are Integer
243
+ pad_left, pad_right, pad_top, pad_bottom = if padding.is_a?(Array)
242
244
  # [left, right, top, bottom]
243
- pad_left, pad_right, pad_top, pad_bottom = padding
245
+ [
246
+ Integer(padding[0] || 0),
247
+ Integer(padding[1] || 0),
248
+ Integer(padding[2] || 0),
249
+ Integer(padding[3] || 0),
250
+ ]
244
251
  else
245
- pad_left = pad_right = pad_top = pad_bottom = padding
252
+ p = Integer(padding)
253
+ [p, p, p, p]
246
254
  end
247
255
 
248
256
  # Compute inner area
@@ -24,6 +24,8 @@ module RatatuiRuby
24
24
  #
25
25
  # ruby examples/widget_calendar/app.rb
26
26
  class Calendar < Data.define(:year, :month, :events, :default_style, :header_style, :block, :show_weekdays_header, :show_surrounding, :show_month_header)
27
+ include CoerceableWidget
28
+
27
29
  ##
28
30
  # :attr_reader: year
29
31
  # The year to display (Integer).