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
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Rich Text Example
7
7
 
8
+ [![widget_rich_text](../../doc/images/widget_rich_text.png)](app.rb)
9
+
8
10
  Demonstrates styling individual words and characters.
9
11
 
10
12
  Standard strings are monochromatic. "Rich Text" is composed of `Lines` containing multiple `Spans`, where each Span has its own style. This allows for multi-colored, multi-styled text blocks.
@@ -30,6 +32,4 @@ ruby examples/widget_rich_text/app.rb
30
32
  Use this example if you need to...
31
33
  - Highlight keywords in code (Syntax highlighting).
32
34
  - Create status lines with icons (e.g., "✔ Success" where the checkmark is green).
33
- - Emphasize specific data points in a paragraph.
34
-
35
- ![Demo](/doc/images/widget_rich_text.png)
35
+ - Emphasize specific data points in a paragraph.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Scroll Text Example
7
7
 
8
+ [![widget_scroll_text](../../doc/images/widget_scroll_text.png)](app.rb)
9
+
8
10
  Demonstrates scrolling long text content within a fixed viewport.
9
11
 
10
12
  Sometimes text exceeds the available space. The `Paragraph` widget supports a `scroll` parameter to simulate a viewport, allowing users to pan vertically and horizontally.
@@ -32,6 +34,4 @@ ruby examples/widget_scroll_text/app.rb
32
34
  Use this example if you need to...
33
35
  - Build a log viewer.
34
36
  - Create a "terms and conditions" scrollbox.
35
- - Display code snippets that might be wider than the terminal.
36
-
37
- ![Demo](/doc/images/widget_scroll_text.png)
37
+ - Display code snippets that might be wider than the terminal.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Scrollbar Widget Example
7
7
 
8
+ [![widget_scrollbar_demo](../../doc/images/widget_scrollbar_demo.png)](app.rb)
9
+
8
10
  Demonstrates explicit scrollbars for navigation feedback.
9
11
 
10
12
  Content overflows. Users get lost in long lists. Scrollbars provide essential spatial awareness ("How far down am I?") and navigation controls.
@@ -32,6 +34,4 @@ ruby examples/widget_scrollbar_demo/app.rb
32
34
 
33
35
  Use this example if you need to...
34
36
  - Add visual scroll indicators to Lists or Tables.
35
- - Implement specialized inputs like sliders or volume controls.
36
-
37
- ![Demo](/doc/images/widget_scrollbar_demo.png)
37
+ - Implement specialized inputs like sliders or volume controls.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Sparkline Widget Example
7
7
 
8
+ [![widget_sparkline_demo](../../doc/images/widget_sparkline_demo.png)](app.rb)
9
+
8
10
  Demonstrates high-density data visualization in a condensed footprint.
9
11
 
10
12
  Users need context. A single number ("90% CPU") tells you status, but not the trend. Full charts take up too much space. Sparklines condense history into a single line, perfect for headers and dashboards.
@@ -37,6 +39,4 @@ ruby examples/widget_sparkline_demo/app.rb
37
39
  Use this example if you need to...
38
40
  - Add a "CPU Load" graph to your header.
39
41
  - Visualize stock price trends in a list row.
40
- - Monitor memory usage over the last 60 seconds.
41
-
42
- ![Demo](/doc/images/widget_sparkline_demo.png)
42
+ - Monitor memory usage over the last 60 seconds.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Style Colors Example
7
7
 
8
+ [![widget_style_colors](../../doc/images/widget_style_colors.png)](app.rb)
9
+
8
10
  Demonstrates high-fidelity color support.
9
11
 
10
12
  Terminals support millions of colors. This example generates a mathematically precise HSL gradient to prove the rendering engine's color fidelity.
@@ -29,6 +31,4 @@ ruby examples/widget_style_colors/app.rb
29
31
  Use this example if you need to...
30
32
  - Create meaningful heatmaps.
31
33
  - Generate color palettes dynamically.
32
- - Test your terminal's color support capabilities.
33
-
34
- ![Demo](/doc/images/widget_style_colors.png)
34
+ - Test your terminal's color support capabilities.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Table Widget Example
7
7
 
8
+ [![widget_table_demo](../../doc/images/widget_table_demo.png)](app.rb)
9
+
8
10
  Demonstrates advanced options for the `Table` widget, including selection, row-level highlighting, and column-level highlighting.
9
11
 
10
12
  Data grids are complex. Users expect to navigate them with keys, select rows, and clearly see which cell is active. The `Table` widget provides these features out of the box efficiently.
@@ -43,6 +45,4 @@ ruby examples/widget_table_demo/app.rb
43
45
  Use this example if you need to...
44
46
  - Build a file explorer or process list.
45
47
  - Create a data-heavy dashboard.
46
- - Handle conflicting style requirements (e.g., "Highlight this row, but make this error cell red").
47
-
48
- ![Demo](/doc/images/widget_table_demo.png)
48
+ - Handle conflicting style requirements (e.g., "Highlight this row, but make this error cell red").
@@ -82,8 +82,23 @@ class WidgetTableDemo
82
82
  end
83
83
 
84
84
  private def render(frame)
85
- # Create table rows from process data
86
- rows = PROCESSES.map { |p| [p[:pid].to_s, p[:name], "#{p[:cpu]}%"] }
85
+ # v0.7.0: Create table rows using table_row and table_cell for per-cell styling
86
+ rows = PROCESSES.map do |p|
87
+ cpu_style = case p[:cpu]
88
+ when 0...10 then @tui.style(fg: :green)
89
+ when 10...30 then @tui.style(fg: :yellow)
90
+ else @tui.style(fg: :red, modifiers: [:bold])
91
+ end
92
+ @tui.table_row(
93
+ cells: [
94
+ p[:pid].to_s,
95
+ p[:name],
96
+ @tui.table_cell(content: "#{p[:cpu]}%", style: cpu_style),
97
+ ],
98
+ # Apply alternating row backgrounds for readability
99
+ style: p[:pid].even? ? @tui.style(bg: :dark_gray) : nil
100
+ )
101
+ end
87
102
 
88
103
  # Define column widths
89
104
  widths = [
@@ -93,7 +108,7 @@ class WidgetTableDemo
93
108
  ]
94
109
 
95
110
  # Create highlight style (yellow text)
96
- highlight_style = @tui.style(fg: :yellow)
111
+ row_highlight_style = @tui.style(fg: :yellow)
97
112
 
98
113
  current_style_entry = @styles[@current_style_index]
99
114
  current_spacing_entry = HIGHLIGHT_SPACINGS[@highlight_spacing_index]
@@ -114,7 +129,7 @@ class WidgetTableDemo
114
129
  selected_row: effective_selection,
115
130
  selected_column: @selected_col,
116
131
  offset: effective_offset,
117
- highlight_style:,
132
+ row_highlight_style:,
118
133
  highlight_symbol: "> ",
119
134
  highlight_spacing: current_spacing_entry[:spacing],
120
135
  column_highlight_style: @show_column_highlight ? @column_highlight_style : nil,
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Tabs Widget Example
7
7
 
8
+ [![widget_tabs_demo](../../doc/images/widget_tabs_demo.png)](app.rb)
9
+
8
10
  Demonstrates view segregation with interactive navigation.
9
11
 
10
12
  Screen real estate is limited. You cannot show everything at once. Tabs segregate content into specialized views (modes), allowing users to switch contexts easily.
@@ -36,6 +38,4 @@ ruby examples/widget_tabs_demo/app.rb
36
38
  Use this example if you need to...
37
39
  - Build a multi-pane dashboard.
38
40
  - Create a "Settings" screen with different categories.
39
- - Implement a "wizard" interface with steps.
40
-
41
- ![Demo](/doc/images/widget_tabs_demo.png)
41
+ - Implement a "wizard" interface with steps.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Text Width Calculator
7
7
 
8
+ [![widget_text_width](../../doc/images/widget_text_width.png)](app.rb)
9
+
8
10
  Demonstrates string width calculation in a terminal environment.
9
11
 
10
12
  Not all characters are created equal. In a TUI, "Width" means cell count, not string length. Emoji (`👍`) take 2 cells. Chinese characters (`你`) take 2 cells. The `tui.text_width` helper tells you the visual width of a string.
@@ -30,6 +32,4 @@ ruby examples/widget_text_width/app.rb
30
32
  Use this example if you need to...
31
33
  - Align text correctly in columns.
32
34
  - Truncate strings that are too long for a widget.
33
- - Build your own custom layout engine.
34
-
35
- ![Demo](/doc/images/widget_text_width.png)
35
+ - Build your own custom layout engine.
@@ -49,11 +49,18 @@ class WidgetTextWidth
49
49
  sample = @text_samples[@selected_index]
50
50
  measured_width = @tui.text_width(sample[:text])
51
51
 
52
+ # v0.7.0: Text::Line#width instance method for rich text measurement
53
+ styled_line = @tui.text_line(spans: [
54
+ @tui.text_span(content: sample[:text], style: @tui.style(fg: :cyan)),
55
+ ])
56
+ line_width = styled_line.width
57
+
52
58
  # Build content text with newlines
53
59
  content = []
54
60
  content << "Sample: #{sample[:text]}"
55
61
  content << ""
56
- content << "Display Width: #{measured_width} cells"
62
+ content << "Display Width (text_width): #{measured_width} cells"
63
+ content << "Display Width (line.width): #{line_width} cells"
57
64
  content << "Character Count: #{sample[:text].length}"
58
65
  content << ""
59
66
  content << sample[:desc]
@@ -1012,7 +1012,7 @@ dependencies = [
1012
1012
 
1013
1013
  [[package]]
1014
1014
  name = "ratatui_ruby"
1015
- version = "0.6.0"
1015
+ version = "0.7.0"
1016
1016
  dependencies = [
1017
1017
  "bumpalo",
1018
1018
  "lazy_static",
@@ -3,7 +3,7 @@
3
3
 
4
4
  [package]
5
5
  name = "ratatui_ruby"
6
- version = "0.6.0"
6
+ version = "0.7.0"
7
7
  edition = "2021"
8
8
 
9
9
  [lib]
@@ -118,9 +118,10 @@ impl RubyFrame {
118
118
  // The ensure_active() check above guarantees we're still in the callback.
119
119
  let area = unsafe { (*self.inner.get()).as_ref().area() };
120
120
 
121
- // Create a Ruby Rect object
121
+ // Create a Ruby Layout::Rect object
122
122
  let module = ruby.define_module("RatatuiRuby")?;
123
- let class = module.const_get::<_, magnus::RClass>("Rect")?;
123
+ let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
124
+ let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
124
125
  class.funcall("new", (area.x, area.y, area.width, area.height))
125
126
  }
126
127
 
@@ -190,13 +191,13 @@ impl RubyFrame {
190
191
  let state_class = unsafe { state.class().name() }.into_owned();
191
192
 
192
193
  match (widget_class.as_str(), state_class.as_str()) {
193
- ("RatatuiRuby::List", "RatatuiRuby::ListState") => {
194
+ ("RatatuiRuby::Widgets::List", "RatatuiRuby::ListState") => {
194
195
  widgets::list::render_stateful(frame, rect, widget, state)
195
196
  }
196
- ("RatatuiRuby::Table", "RatatuiRuby::TableState") => {
197
+ ("RatatuiRuby::Widgets::Table", "RatatuiRuby::TableState") => {
197
198
  widgets::table::render_stateful(frame, rect, widget, state)
198
199
  }
199
- ("RatatuiRuby::Scrollbar", "RatatuiRuby::ScrollbarState") => {
200
+ ("RatatuiRuby::Widgets::Scrollbar", "RatatuiRuby::ScrollbarState") => {
200
201
  widgets::scrollbar::render_stateful(frame, rect, widget, state)
201
202
  }
202
203
  _ => Err(Error::new(
@@ -161,8 +161,9 @@ fn init() -> Result<(), Error> {
161
161
  m.define_module_function("_get_cell_at", function!(terminal::get_cell_at, 2))?;
162
162
  m.define_module_function("resize_terminal", function!(terminal::resize_terminal, 2))?;
163
163
 
164
- // Register Layout.split on the Layout class
165
- let layout_class = m.const_get::<_, magnus::RClass>("Layout")?;
164
+ // Register Layout.split on the Layout::Layout class (inside the Layout module)
165
+ let layout_mod = m.const_get::<_, magnus::RModule>("Layout")?;
166
+ let layout_class = layout_mod.const_get::<_, magnus::RClass>("Layout")?;
166
167
  layout_class.define_singleton_method("_split", function!(widgets::layout::split_layout, 4))?;
167
168
 
168
169
  // Paragraph metrics
@@ -11,7 +11,8 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
11
11
  let ruby = magnus::Ruby::get().unwrap();
12
12
  let ruby_area = {
13
13
  let module = ruby.define_module("RatatuiRuby")?;
14
- let class = module.const_get::<_, magnus::RClass>("Rect")?;
14
+ let layout_mod = module.const_get::<_, magnus::RModule>("Layout")?;
15
+ let class = layout_mod.const_get::<_, magnus::RClass>("Rect")?;
15
16
  class.funcall::<_, _, Value>("new", (area.x, area.y, area.width, area.height))?
16
17
  };
17
18
 
@@ -35,28 +36,28 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
35
36
  let class_name = unsafe { node.class().name() }.into_owned();
36
37
 
37
38
  match class_name.as_str() {
38
- "RatatuiRuby::Paragraph" => widgets::paragraph::render(frame, area, node)?,
39
- "RatatuiRuby::Clear" => widgets::clear::render(frame, area, node)?,
40
- "RatatuiRuby::Cursor" => widgets::cursor::render(frame, area, node)?,
41
- "RatatuiRuby::Overlay" => widgets::overlay::render(frame, area, node)?,
42
- "RatatuiRuby::Center" => widgets::center::render(frame, area, node)?,
43
- "RatatuiRuby::Layout" => widgets::layout::render(frame, area, node)?,
44
- "RatatuiRuby::List" => widgets::list::render(frame, area, node)?,
45
- "RatatuiRuby::Gauge" => widgets::gauge::render(frame, area, node)?,
46
- "RatatuiRuby::LineGauge" => widgets::line_gauge::render(frame, area, node)?,
47
- "RatatuiRuby::Table" => widgets::table::render(frame, area, node)?,
48
- "RatatuiRuby::Block" => widgets::block::render(frame, area, node)?,
49
- "RatatuiRuby::Tabs" => widgets::tabs::render(frame, area, node)?,
50
- "RatatuiRuby::Scrollbar" => widgets::scrollbar::render(frame, area, node)?,
51
- "RatatuiRuby::BarChart" => widgets::barchart::render(frame, area, node)?,
52
- "RatatuiRuby::Canvas" => widgets::canvas::render(frame, area, node)?,
53
- "RatatuiRuby::Calendar" => widgets::calendar::render(frame, area, node)?,
54
- "RatatuiRuby::Sparkline" => widgets::sparkline::render(frame, area, node)?,
55
- "RatatuiRuby::Chart" | "RatatuiRuby::LineChart" => {
39
+ "RatatuiRuby::Widgets::Paragraph" => widgets::paragraph::render(frame, area, node)?,
40
+ "RatatuiRuby::Widgets::Clear" => widgets::clear::render(frame, area, node)?,
41
+ "RatatuiRuby::Widgets::Cursor" => widgets::cursor::render(frame, area, node)?,
42
+ "RatatuiRuby::Widgets::Overlay" => widgets::overlay::render(frame, area, node)?,
43
+ "RatatuiRuby::Widgets::Center" => widgets::center::render(frame, area, node)?,
44
+ "RatatuiRuby::Layout::Layout" => widgets::layout::render(frame, area, node)?,
45
+ "RatatuiRuby::Widgets::List" => widgets::list::render(frame, area, node)?,
46
+ "RatatuiRuby::Widgets::Gauge" => widgets::gauge::render(frame, area, node)?,
47
+ "RatatuiRuby::Widgets::LineGauge" => widgets::line_gauge::render(frame, area, node)?,
48
+ "RatatuiRuby::Widgets::Table" => widgets::table::render(frame, area, node)?,
49
+ "RatatuiRuby::Widgets::Block" => widgets::block::render(frame, area, node)?,
50
+ "RatatuiRuby::Widgets::Tabs" => widgets::tabs::render(frame, area, node)?,
51
+ "RatatuiRuby::Widgets::Scrollbar" => widgets::scrollbar::render(frame, area, node)?,
52
+ "RatatuiRuby::Widgets::BarChart" => widgets::barchart::render(frame, area, node)?,
53
+ "RatatuiRuby::Widgets::Canvas" => widgets::canvas::render(frame, area, node)?,
54
+ "RatatuiRuby::Widgets::Calendar" => widgets::calendar::render(frame, area, node)?,
55
+ "RatatuiRuby::Widgets::Sparkline" => widgets::sparkline::render(frame, area, node)?,
56
+ "RatatuiRuby::Widgets::Chart" | "RatatuiRuby::LineChart" => {
56
57
  widgets::chart::render(frame, area, node)?;
57
58
  }
58
- "RatatuiRuby::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
59
- "RatatuiRuby::RatatuiMascot" => {
59
+ "RatatuiRuby::Widgets::RatatuiLogo" => widgets::ratatui_logo::render(frame, area, node),
60
+ "RatatuiRuby::Widgets::RatatuiMascot" => {
60
61
  widgets::ratatui_mascot::render_ratatui_mascot(frame, area, node)?;
61
62
  }
62
63
  _ => {}
@@ -131,6 +131,8 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
131
131
 
132
132
  // Extract spans from the Ruby Line
133
133
  let spans_val: Value = value.funcall("spans", ())?;
134
+ // v0.7.0: Extract style from the Ruby Line
135
+ let style_val: Value = value.funcall("style", ())?;
134
136
 
135
137
  if spans_val.is_nil() {
136
138
  return Ok(Line::from(""));
@@ -164,11 +166,18 @@ pub fn parse_line(value: Value) -> Result<Line<'static>, Error> {
164
166
  }
165
167
  }
166
168
 
167
- if spans.is_empty() {
168
- Ok(Line::from(""))
169
+ let mut line = if spans.is_empty() {
170
+ Line::from("")
169
171
  } else {
170
- Ok(Line::from(spans))
172
+ Line::from(spans)
173
+ };
174
+
175
+ // v0.7.0: Apply line-level style if present
176
+ if !style_val.is_nil() {
177
+ line = line.style(parse_style(style_val)?);
171
178
  }
179
+
180
+ Ok(line)
172
181
  }
173
182
 
174
183
  #[cfg(test)]
@@ -57,7 +57,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
57
57
  let class_name = unsafe { class.name() }.into_owned();
58
58
 
59
59
  match class_name.as_str() {
60
- "RatatuiRuby::Shape::Line" => {
60
+ "RatatuiRuby::Widgets::Shape::Line" => {
61
61
  let x1: f64 = shape_val.funcall("x1", ()).unwrap_or(0.0);
62
62
  let y1: f64 = shape_val.funcall("y1", ()).unwrap_or(0.0);
63
63
  let x2: f64 = shape_val.funcall("x2", ()).unwrap_or(0.0);
@@ -73,7 +73,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
73
73
  color,
74
74
  });
75
75
  }
76
- "RatatuiRuby::Shape::Rectangle" => {
76
+ "RatatuiRuby::Widgets::Shape::Rectangle" => {
77
77
  let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
78
78
  let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
79
79
  let width: f64 = shape_val.funcall("width", ()).unwrap_or(0.0);
@@ -89,7 +89,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
89
89
  color,
90
90
  });
91
91
  }
92
- "RatatuiRuby::Shape::Circle" => {
92
+ "RatatuiRuby::Widgets::Shape::Circle" => {
93
93
  let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
94
94
  let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
95
95
  let radius: f64 = shape_val.funcall("radius", ()).unwrap_or(0.0);
@@ -103,7 +103,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
103
103
  color,
104
104
  });
105
105
  }
106
- "RatatuiRuby::Shape::Map" => {
106
+ "RatatuiRuby::Widgets::Shape::Map" => {
107
107
  let color_val: Value = shape_val.funcall("color", ()).unwrap();
108
108
  let color =
109
109
  parse_color(&color_val.to_string()).unwrap_or(ratatui::style::Color::Reset);
@@ -114,7 +114,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
114
114
  };
115
115
  ctx.draw(&Map { color, resolution });
116
116
  }
117
- "RatatuiRuby::Shape::Label" => {
117
+ "RatatuiRuby::Widgets::Shape::Label" => {
118
118
  let x: f64 = shape_val.funcall("x", ()).unwrap_or(0.0);
119
119
  let y: f64 = shape_val.funcall("y", ()).unwrap_or(0.0);
120
120
  let text_val: Value = shape_val.funcall("text", ()).unwrap();
@@ -2,6 +2,7 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use crate::text::{parse_line, parse_span};
5
6
  use crate::widgets::table_state::RubyTableState;
6
7
  use bumpalo::Bump;
7
8
  use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
@@ -22,7 +23,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
22
23
  let widths_val: Value = node.funcall("widths", ())?;
23
24
  let widths_array = magnus::RArray::from_value(widths_val)
24
25
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
25
- let highlight_style_val: Value = node.funcall("highlight_style", ())?;
26
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
26
27
  let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
27
28
  let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
28
29
  let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
@@ -73,8 +74,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
73
74
  table = table.block(parse_block(block_val, &bump)?);
74
75
  }
75
76
 
76
- if !highlight_style_val.is_nil() {
77
- table = table.row_highlight_style(parse_style(highlight_style_val)?);
77
+ if !row_highlight_style_val.is_nil() {
78
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
78
79
  }
79
80
 
80
81
  if !column_highlight_style_val.is_nil() {
@@ -158,7 +159,7 @@ pub fn render_stateful(
158
159
  // Build table (ignoring selected_row, selected_column, offset — State is truth)
159
160
  let header_val: Value = node.funcall("header", ())?;
160
161
  let footer_val: Value = node.funcall("footer", ())?;
161
- let highlight_style_val: Value = node.funcall("highlight_style", ())?;
162
+ let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
162
163
  let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
163
164
  let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
164
165
  let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
@@ -196,8 +197,8 @@ pub fn render_stateful(
196
197
  if !block_val.is_nil() {
197
198
  table = table.block(parse_block(block_val, &bump)?);
198
199
  }
199
- if !highlight_style_val.is_nil() {
200
- table = table.row_highlight_style(parse_style(highlight_style_val)?);
200
+ if !row_highlight_style_val.is_nil() {
201
+ table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
201
202
  }
202
203
  if !column_highlight_style_val.is_nil() {
203
204
  table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
@@ -228,6 +229,53 @@ pub fn render_stateful(
228
229
 
229
230
  fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
230
231
  let ruby = magnus::Ruby::get().unwrap();
232
+
233
+ // Check if this is a RatatuiRuby::Row object with cells + style + height + margins
234
+ let class = row_val.class();
235
+ // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
236
+ let class_name = unsafe { class.name() }.into_owned();
237
+
238
+ if class_name == "RatatuiRuby::Widgets::Row" {
239
+ let cells_val: Value = row_val.funcall("cells", ())?;
240
+ let style_val: Value = row_val.funcall("style", ())?;
241
+ let height_val: Value = row_val.funcall("height", ())?;
242
+ let top_margin_val: Value = row_val.funcall("top_margin", ())?;
243
+ let bottom_margin_val: Value = row_val.funcall("bottom_margin", ())?;
244
+
245
+ let cells_array = magnus::RArray::from_value(cells_val).ok_or_else(|| {
246
+ Error::new(ruby.exception_type_error(), "expected array for Row.cells")
247
+ })?;
248
+
249
+ let mut cells = Vec::new();
250
+ for i in 0..cells_array.len() {
251
+ let index = isize::try_from(i)
252
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
253
+ let entry_val: Value = cells_array.entry(index)?;
254
+ cells.push(parse_cell(entry_val)?);
255
+ }
256
+
257
+ let mut row = Row::new(cells);
258
+
259
+ if !style_val.is_nil() {
260
+ row = row.style(parse_style(style_val)?);
261
+ }
262
+ if !height_val.is_nil() {
263
+ let h: u16 = height_val.funcall("to_int", ())?;
264
+ row = row.height(h);
265
+ }
266
+ if !top_margin_val.is_nil() {
267
+ let m: u16 = top_margin_val.funcall("to_int", ())?;
268
+ row = row.top_margin(m);
269
+ }
270
+ if !bottom_margin_val.is_nil() {
271
+ let m: u16 = bottom_margin_val.funcall("to_int", ())?;
272
+ row = row.bottom_margin(m);
273
+ }
274
+
275
+ return Ok(row);
276
+ }
277
+
278
+ // Fallback: plain array of cells
231
279
  let row_array = magnus::RArray::from_value(row_val)
232
280
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
233
281
 
@@ -246,42 +294,39 @@ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
246
294
  // SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
247
295
  let class_name = unsafe { class.name() }.into_owned();
248
296
 
249
- if class_name == "RatatuiRuby::Paragraph" {
297
+ // Try Text::Line first (contains multiple spans)
298
+ if class_name.contains("Line") {
299
+ if let Ok(line) = parse_line(cell_val) {
300
+ return Ok(Cell::from(line));
301
+ }
302
+ }
303
+
304
+ // Try Text::Span
305
+ if class_name.contains("Span") {
306
+ if let Ok(span) = parse_span(cell_val) {
307
+ return Ok(Cell::from(ratatui::text::Line::from(vec![span])));
308
+ }
309
+ }
310
+
311
+ if class_name == "RatatuiRuby::Widgets::Paragraph" {
250
312
  let text: String = cell_val.funcall("text", ())?;
251
313
  let style_val: Value = cell_val.funcall("style", ())?;
252
314
  let cell_style = parse_style(style_val)?;
253
315
  Ok(Cell::from(text).style(cell_style))
254
- } else if class_name == "RatatuiRuby::Style" {
316
+ } else if class_name == "RatatuiRuby::Style::Style" {
255
317
  Ok(Cell::from("").style(parse_style(cell_val)?))
256
- } else if class_name == "RatatuiRuby::Cell" {
257
- let symbol: String = cell_val.funcall("char", ())?;
258
- let fg_val: Value = cell_val.funcall("fg", ())?;
259
- let bg_val: Value = cell_val.funcall("bg", ())?;
260
- let modifiers_val: Value = cell_val.funcall("modifiers", ())?;
261
-
262
- let mut style = ratatui::style::Style::default();
263
- if !fg_val.is_nil() {
264
- if let Some(color) = crate::style::parse_color_value(fg_val)? {
265
- style = style.fg(color);
266
- }
267
- }
268
- if !bg_val.is_nil() {
269
- if let Some(color) = crate::style::parse_color_value(bg_val)? {
270
- style = style.bg(color);
271
- }
272
- }
273
- if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
274
- let ruby = magnus::Ruby::get().unwrap();
275
- for i in 0..mods_array.len() {
276
- let index = isize::try_from(i)
277
- .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
278
- let mod_str: String = mods_array.entry::<String>(index)?;
279
- if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
280
- style = style.add_modifier(modifier);
281
- }
282
- }
318
+ } else if class_name == "RatatuiRuby::Widgets::Cell" {
319
+ // Widgets::Cell has content (String/Span/Line) and optional style
320
+ let content_val: Value = cell_val.funcall("content", ())?;
321
+ let style_val: Value = cell_val.funcall("style", ())?;
322
+
323
+ // Recursively parse the content (could be String, Span, or Line)
324
+ let mut cell = parse_cell(content_val)?;
325
+
326
+ if !style_val.is_nil() {
327
+ cell = cell.style(parse_style(style_val)?);
283
328
  }
284
- Ok(Cell::from(symbol).style(style))
329
+ Ok(cell)
285
330
  } else {
286
331
  let cell_str: String = cell_val.funcall("to_s", ())?;
287
332
  Ok(Cell::from(cell_str))