ratatui_ruby 0.6.0 → 0.7.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 (177) 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 +48 -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/async.md +160 -0
  12. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  13. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  14. data/doc/contributors/architectural_overhaul/task.md +37 -0
  15. data/doc/contributors/design/ruby_frontend.md +277 -81
  16. data/doc/contributors/design/rust_backend.md +349 -55
  17. data/doc/contributors/developing_examples.md +5 -5
  18. data/doc/contributors/index.md +7 -5
  19. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  20. data/doc/debugging.md +71 -0
  21. data/doc/index.md +11 -6
  22. data/doc/interactive_design.md +2 -2
  23. data/doc/quickstart.md +66 -97
  24. data/doc/v0.7.0_migration.md +236 -0
  25. data/doc/why.md +93 -0
  26. data/examples/app_all_events/README.md +6 -4
  27. data/examples/app_all_events/app.rb +1 -1
  28. data/examples/app_all_events/model/app_model.rb +1 -1
  29. data/examples/app_all_events/model/msg.rb +1 -1
  30. data/examples/app_all_events/update.rb +1 -1
  31. data/examples/app_all_events/view/app_view.rb +1 -1
  32. data/examples/app_all_events/view/controls_view.rb +1 -1
  33. data/examples/app_all_events/view/counts_view.rb +1 -1
  34. data/examples/app_all_events/view/live_view.rb +1 -1
  35. data/examples/app_all_events/view/log_view.rb +1 -1
  36. data/examples/app_color_picker/README.md +7 -5
  37. data/examples/app_color_picker/app.rb +1 -1
  38. data/examples/app_login_form/README.md +2 -0
  39. data/examples/app_stateful_interaction/README.md +2 -0
  40. data/examples/app_stateful_interaction/app.rb +1 -1
  41. data/examples/verify_quickstart_dsl/README.md +4 -3
  42. data/examples/verify_quickstart_dsl/app.rb +1 -1
  43. data/examples/verify_quickstart_layout/README.md +1 -1
  44. data/examples/verify_quickstart_lifecycle/README.md +3 -3
  45. data/examples/verify_quickstart_lifecycle/app.rb +2 -2
  46. data/examples/verify_readme_usage/README.md +1 -1
  47. data/examples/widget_barchart_demo/README.md +2 -1
  48. data/examples/widget_block_demo/README.md +2 -0
  49. data/examples/widget_box_demo/README.md +3 -3
  50. data/examples/widget_calendar_demo/README.md +3 -3
  51. data/examples/widget_calendar_demo/app.rb +5 -1
  52. data/examples/widget_canvas_demo/README.md +3 -3
  53. data/examples/widget_cell_demo/README.md +3 -3
  54. data/examples/widget_center_demo/README.md +3 -3
  55. data/examples/widget_chart_demo/README.md +3 -3
  56. data/examples/widget_gauge_demo/README.md +3 -3
  57. data/examples/widget_layout_split/README.md +3 -3
  58. data/examples/widget_line_gauge_demo/README.md +3 -3
  59. data/examples/widget_list_demo/README.md +3 -3
  60. data/examples/widget_map_demo/README.md +3 -3
  61. data/examples/widget_map_demo/app.rb +2 -2
  62. data/examples/widget_overlay_demo/README.md +36 -0
  63. data/examples/widget_popup_demo/README.md +3 -3
  64. data/examples/widget_ratatui_logo_demo/README.md +3 -3
  65. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  66. data/examples/widget_ratatui_mascot_demo/README.md +3 -3
  67. data/examples/widget_rect/README.md +3 -3
  68. data/examples/widget_render/README.md +3 -3
  69. data/examples/widget_render/app.rb +3 -3
  70. data/examples/widget_rich_text/README.md +3 -3
  71. data/examples/widget_scroll_text/README.md +3 -3
  72. data/examples/widget_scrollbar_demo/README.md +3 -3
  73. data/examples/widget_sparkline_demo/README.md +3 -3
  74. data/examples/widget_style_colors/README.md +3 -3
  75. data/examples/widget_table_demo/README.md +3 -3
  76. data/examples/widget_table_demo/app.rb +19 -4
  77. data/examples/widget_tabs_demo/README.md +3 -3
  78. data/examples/widget_text_width/README.md +3 -3
  79. data/examples/widget_text_width/app.rb +8 -1
  80. data/ext/ratatui_ruby/Cargo.lock +1 -1
  81. data/ext/ratatui_ruby/Cargo.toml +1 -1
  82. data/ext/ratatui_ruby/src/frame.rs +6 -5
  83. data/ext/ratatui_ruby/src/lib.rs +3 -2
  84. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  85. data/ext/ratatui_ruby/src/style.rs +25 -9
  86. data/ext/ratatui_ruby/src/text.rs +12 -3
  87. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  88. data/ext/ratatui_ruby/src/widgets/table.rs +81 -36
  89. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  90. data/lib/ratatui_ruby/buffer.rb +15 -0
  91. data/lib/ratatui_ruby/frame.rb +8 -8
  92. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  93. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  94. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  95. data/lib/ratatui_ruby/layout.rb +19 -0
  96. data/lib/ratatui_ruby/list_state.rb +2 -2
  97. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  98. data/lib/ratatui_ruby/schema/row.rb +66 -0
  99. data/lib/ratatui_ruby/schema/table.rb +10 -10
  100. data/lib/ratatui_ruby/schema/text.rb +27 -2
  101. data/lib/ratatui_ruby/style/style.rb +81 -0
  102. data/lib/ratatui_ruby/style.rb +15 -0
  103. data/lib/ratatui_ruby/table_state.rb +1 -1
  104. data/lib/ratatui_ruby/test_helper/snapshot.rb +24 -0
  105. data/lib/ratatui_ruby/test_helper/style_assertions.rb +1 -1
  106. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  107. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  108. data/lib/ratatui_ruby/tui/core.rb +38 -0
  109. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  110. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  111. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  112. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  113. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  114. data/lib/ratatui_ruby/tui.rb +75 -0
  115. data/lib/ratatui_ruby/version.rb +1 -1
  116. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  117. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  118. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  119. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  120. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  121. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  122. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  123. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  124. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  125. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  126. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  127. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  128. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  129. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  130. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  131. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  132. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  133. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  134. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  135. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  136. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  137. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  138. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  139. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  140. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  141. data/lib/ratatui_ruby/widgets.rb +40 -0
  142. data/lib/ratatui_ruby.rb +23 -39
  143. data/sig/examples/app_all_events/view.rbs +1 -1
  144. data/sig/examples/app_all_events/view_state.rbs +1 -1
  145. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  146. data/sig/ratatui_ruby/schema/table.rbs +1 -1
  147. data/sig/ratatui_ruby/schema/text.rbs +1 -0
  148. data/sig/ratatui_ruby/session.rbs +29 -49
  149. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  150. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  151. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  152. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  153. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  154. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  155. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  156. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  157. data/sig/ratatui_ruby/tui.rbs +19 -0
  158. data/tasks/autodoc.rake +1 -35
  159. data/tasks/bump/changelog.rb +8 -0
  160. data/tasks/bump/ruby_gem.rb +12 -0
  161. data/tasks/bump/unreleased_section.rb +16 -0
  162. data/tasks/sourcehut.rake +4 -1
  163. metadata +64 -15
  164. data/doc/contributors/dwim_dx.md +0 -366
  165. data/doc/contributors/examples_audit/p1_high.md +0 -21
  166. data/doc/contributors/examples_audit/p2_moderate.md +0 -81
  167. data/doc/contributors/examples_audit.md +0 -41
  168. data/doc/images/app_analytics.png +0 -0
  169. data/doc/images/app_custom_widget.png +0 -0
  170. data/doc/images/app_mouse_events.png +0 -0
  171. data/doc/images/widget_table_flex.png +0 -0
  172. data/lib/ratatui_ruby/session/autodoc.rb +0 -482
  173. data/lib/ratatui_ruby/session.rb +0 -178
  174. data/tasks/autodoc/inventory.rb +0 -63
  175. data/tasks/autodoc/notice.rb +0 -26
  176. data/tasks/autodoc/rbs.rb +0 -38
  177. data/tasks/autodoc/rdoc.rb +0 -45
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Ratatui Mascot Example
7
7
 
8
+ [![widget_ratatui_mascot_demo](../../doc/images/widget_ratatui_mascot_demo.png)](app.rb)
9
+
8
10
  Demonstrates the project mascot widget for adding personality.
9
11
 
10
12
  Interfaces can feel clinical. A friendly mascot adds charm and brand identity to your terminal application.
@@ -29,6 +31,4 @@ ruby examples/widget_ratatui_mascot_demo/app.rb
29
31
 
30
32
  Use this example if you need to...
31
33
  - Add visual flair to your UI.
32
- - Create a friendly empty state or success screen.
33
-
34
- ![Demo](/doc/images/widget_ratatui_mascot_demo.png)
34
+ - Create a friendly empty state or success screen.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Rect (Geometry) Widget Example
7
7
 
8
+ [![widget_rect](../../doc/images/widget_rect.png)](app.rb)
9
+
8
10
  Demonstrates the Rect geometry primitive and hit-testing patterns.
9
11
 
10
12
  TUI layouts are composed of rectangles. Understanding how to manipulate `Rect` objects, reuse them from the layout phase, and use them for mouse interaction is critical for building interactive apps.
@@ -33,6 +35,4 @@ ruby examples/widget_rect/app.rb
33
35
  Use this example if you need to...
34
36
  - Handle mouse clicks on specific buttons or areas.
35
37
  - Create resizable panes (like a split pane in an IDE).
36
- - Debug layout issues by inspecting Rect coordinates.
37
-
38
- ![Demo](/doc/images/widget_rect.png)
38
+ - Debug layout issues by inspecting Rect coordinates.
@@ -5,6 +5,8 @@ SPDX-License-Identifier: CC-BY-SA-4.0
5
5
 
6
6
  # Render (Custom Widget) Example
7
7
 
8
+ [![widget_render](../../doc/images/widget_render.png)](app.rb)
9
+
8
10
  Demonstrates how to build Custom Widgets using absolute coordinates.
9
11
 
10
12
  Sometimes standard widgets aren't enough. You need to draw custom shapes, games, or graphs. This example shows how to implement the `render(area)` contract to draw anything you want while respecting layout boundaries.
@@ -32,6 +34,4 @@ ruby examples/widget_render/app.rb
32
34
  Use this example if you need to...
33
35
  - Build a game (Snake, Tetris) inside the terminal.
34
36
  - Create a specialized visualization (Network topology graph).
35
- - Draw custom UI elements not provided by the library.
36
-
37
- ![Demo](/doc/images/widget_render.png)
37
+ - Draw custom UI elements not provided by the library.
@@ -21,7 +21,7 @@ class DiagonalWidget
21
21
  area.x + i,
22
22
  area.y + i,
23
23
  "\\",
24
- RatatuiRuby::Style.new(fg: :red, modifiers: [:bold])
24
+ RatatuiRuby::Style::Style.new(fg: :red, modifiers: [:bold])
25
25
  )
26
26
  end
27
27
  end
@@ -47,7 +47,7 @@ class CheckerboardWidget
47
47
  area.x + col,
48
48
  area.y + row,
49
49
  @char,
50
- RatatuiRuby::Style.new(fg: :cyan)
50
+ RatatuiRuby::Style::Style.new(fg: :cyan)
51
51
  )
52
52
  end
53
53
  end
@@ -63,7 +63,7 @@ end
63
63
  class BorderWidget
64
64
  def render(area)
65
65
  result = []
66
- style = RatatuiRuby::Style.new(fg: :green)
66
+ style = RatatuiRuby::Style::Style.new(fg: :green)
67
67
 
68
68
  # Top and bottom
69
69
  (0...area.width).each do |x| # rubocop:disable Lint/AmbiguousRange
@@ -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.1"
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.1"
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
  _ => {}
@@ -12,6 +12,8 @@ use ratatui::{
12
12
  widgets::{Block, BorderType, Borders, Padding},
13
13
  };
14
14
 
15
+ use crate::text::parse_line;
16
+
15
17
  pub fn parse_color(color_str: &str) -> Option<Color> {
16
18
  // Try standard ratatui parsing first (named colors, indexed, etc.)
17
19
  if let Ok(color) = color_str.parse::<Color>() {
@@ -274,15 +276,22 @@ fn parse_titles(block_val: Value, mut block: Block<'_>) -> Result<Block<'_>, Err
274
276
  let index = isize::try_from(i)
275
277
  .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
276
278
  let title_item: Value = titles_array.entry(index)?;
277
- let mut content = String::new();
278
279
  let mut alignment = Alignment::Left;
279
280
  let mut is_bottom = false;
280
281
  let mut style = Style::default();
282
+ let mut line: Option<Line<'static>> = None;
281
283
 
282
284
  if let Some(hash) = magnus::RHash::from_value(title_item) {
283
285
  if let Ok(v) = hash.lookup::<_, Value>(ruby.to_symbol("content")) {
284
286
  if !v.is_nil() {
285
- content = v.funcall("to_s", ())?;
287
+ // First, try to parse as a Line object (preserves styling)
288
+ if let Ok(parsed_line) = parse_line(v) {
289
+ line = Some(parsed_line);
290
+ } else {
291
+ // Fallback to string
292
+ let content: String = v.funcall("to_s", ())?;
293
+ line = Some(Line::from(content));
294
+ }
286
295
  }
287
296
  }
288
297
  if let Ok(v) = hash.lookup::<_, Value>(ruby.to_symbol("alignment")) {
@@ -307,15 +316,22 @@ fn parse_titles(block_val: Value, mut block: Block<'_>) -> Result<Block<'_>, Err
307
316
  }
308
317
  }
309
318
  } else {
310
- content = title_item.funcall("to_s", ())?;
319
+ let content: String = title_item.funcall("to_s", ())?;
320
+ line = Some(Line::from(content));
311
321
  }
312
322
 
313
- let line = Line::from(content).alignment(alignment).style(style);
314
- block = if is_bottom {
315
- block.title_bottom(line)
316
- } else {
317
- block.title_top(line)
318
- };
323
+ if let Some(mut l) = line {
324
+ l = l.alignment(alignment);
325
+ // Only apply style if the line doesn't already have styled spans
326
+ if style != Style::default() {
327
+ l = l.style(style);
328
+ }
329
+ block = if is_bottom {
330
+ block.title_bottom(l)
331
+ } else {
332
+ block.title_top(l)
333
+ };
334
+ }
319
335
  }
320
336
  }
321
337
  }
@@ -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();