ratatui_ruby 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +4 -4
  7. data/CHANGELOG.md +35 -0
  8. data/README.md +26 -1
  9. data/doc/application_architecture.md +16 -16
  10. data/doc/application_testing.md +1 -1
  11. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  12. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  13. data/doc/contributors/architectural_overhaul/task.md +37 -0
  14. data/doc/contributors/design/ruby_frontend.md +277 -81
  15. data/doc/contributors/design/rust_backend.md +349 -55
  16. data/doc/contributors/developing_examples.md +5 -5
  17. data/doc/contributors/index.md +7 -5
  18. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  19. data/doc/index.md +11 -6
  20. data/doc/interactive_design.md +2 -2
  21. data/doc/quickstart.md +66 -97
  22. data/doc/v0.7.0_migration.md +236 -0
  23. data/doc/why.md +93 -0
  24. data/examples/app_all_events/README.md +6 -4
  25. data/examples/app_all_events/app.rb +1 -1
  26. data/examples/app_all_events/model/app_model.rb +1 -1
  27. data/examples/app_all_events/model/msg.rb +1 -1
  28. data/examples/app_all_events/update.rb +1 -1
  29. data/examples/app_all_events/view/app_view.rb +1 -1
  30. data/examples/app_all_events/view/controls_view.rb +1 -1
  31. data/examples/app_all_events/view/counts_view.rb +1 -1
  32. data/examples/app_all_events/view/live_view.rb +1 -1
  33. data/examples/app_all_events/view/log_view.rb +1 -1
  34. data/examples/app_color_picker/README.md +7 -5
  35. data/examples/app_color_picker/app.rb +1 -1
  36. data/examples/app_login_form/README.md +2 -0
  37. data/examples/app_stateful_interaction/README.md +2 -0
  38. data/examples/app_stateful_interaction/app.rb +1 -1
  39. data/examples/verify_quickstart_dsl/README.md +4 -3
  40. data/examples/verify_quickstart_dsl/app.rb +1 -1
  41. data/examples/verify_quickstart_layout/README.md +1 -1
  42. data/examples/verify_quickstart_lifecycle/README.md +3 -3
  43. data/examples/verify_quickstart_lifecycle/app.rb +2 -2
  44. data/examples/verify_readme_usage/README.md +1 -1
  45. data/examples/widget_barchart_demo/README.md +2 -1
  46. data/examples/widget_block_demo/README.md +2 -0
  47. data/examples/widget_box_demo/README.md +3 -3
  48. data/examples/widget_calendar_demo/README.md +3 -3
  49. data/examples/widget_calendar_demo/app.rb +5 -1
  50. data/examples/widget_canvas_demo/README.md +3 -3
  51. data/examples/widget_cell_demo/README.md +3 -3
  52. data/examples/widget_center_demo/README.md +3 -3
  53. data/examples/widget_chart_demo/README.md +3 -3
  54. data/examples/widget_gauge_demo/README.md +3 -3
  55. data/examples/widget_layout_split/README.md +3 -3
  56. data/examples/widget_line_gauge_demo/README.md +3 -3
  57. data/examples/widget_list_demo/README.md +3 -3
  58. data/examples/widget_map_demo/README.md +3 -3
  59. data/examples/widget_map_demo/app.rb +2 -2
  60. data/examples/widget_overlay_demo/README.md +36 -0
  61. data/examples/widget_popup_demo/README.md +3 -3
  62. data/examples/widget_ratatui_logo_demo/README.md +3 -3
  63. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  64. data/examples/widget_ratatui_mascot_demo/README.md +3 -3
  65. data/examples/widget_rect/README.md +3 -3
  66. data/examples/widget_render/README.md +3 -3
  67. data/examples/widget_render/app.rb +3 -3
  68. data/examples/widget_rich_text/README.md +3 -3
  69. data/examples/widget_scroll_text/README.md +3 -3
  70. data/examples/widget_scrollbar_demo/README.md +3 -3
  71. data/examples/widget_sparkline_demo/README.md +3 -3
  72. data/examples/widget_style_colors/README.md +3 -3
  73. data/examples/widget_table_demo/README.md +3 -3
  74. data/examples/widget_table_demo/app.rb +19 -4
  75. data/examples/widget_tabs_demo/README.md +3 -3
  76. data/examples/widget_text_width/README.md +3 -3
  77. data/examples/widget_text_width/app.rb +8 -1
  78. data/ext/ratatui_ruby/Cargo.lock +1 -1
  79. data/ext/ratatui_ruby/Cargo.toml +1 -1
  80. data/ext/ratatui_ruby/src/frame.rs +6 -5
  81. data/ext/ratatui_ruby/src/lib.rs +3 -2
  82. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  83. data/ext/ratatui_ruby/src/text.rs +12 -3
  84. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  85. data/ext/ratatui_ruby/src/widgets/table.rs +81 -36
  86. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  87. data/lib/ratatui_ruby/buffer.rb +15 -0
  88. data/lib/ratatui_ruby/frame.rb +8 -8
  89. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  90. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  91. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  92. data/lib/ratatui_ruby/layout.rb +19 -0
  93. data/lib/ratatui_ruby/list_state.rb +2 -2
  94. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  95. data/lib/ratatui_ruby/schema/row.rb +66 -0
  96. data/lib/ratatui_ruby/schema/table.rb +10 -10
  97. data/lib/ratatui_ruby/schema/text.rb +27 -2
  98. data/lib/ratatui_ruby/style/style.rb +81 -0
  99. data/lib/ratatui_ruby/style.rb +15 -0
  100. data/lib/ratatui_ruby/table_state.rb +1 -1
  101. data/lib/ratatui_ruby/test_helper/snapshot.rb +24 -0
  102. data/lib/ratatui_ruby/test_helper/style_assertions.rb +1 -1
  103. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  105. data/lib/ratatui_ruby/tui/core.rb +38 -0
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  107. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  108. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  109. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  110. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  111. data/lib/ratatui_ruby/tui.rb +75 -0
  112. data/lib/ratatui_ruby/version.rb +1 -1
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  115. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  116. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  117. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  118. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  119. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  120. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  121. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  122. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  126. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  127. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  136. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  137. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  138. data/lib/ratatui_ruby/widgets.rb +40 -0
  139. data/lib/ratatui_ruby.rb +23 -39
  140. data/sig/examples/app_all_events/view.rbs +1 -1
  141. data/sig/examples/app_all_events/view_state.rbs +1 -1
  142. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  143. data/sig/ratatui_ruby/schema/table.rbs +1 -1
  144. data/sig/ratatui_ruby/schema/text.rbs +1 -0
  145. data/sig/ratatui_ruby/session.rbs +29 -49
  146. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  147. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  148. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  149. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  150. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  151. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  152. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  153. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  154. data/sig/ratatui_ruby/tui.rbs +19 -0
  155. data/tasks/autodoc.rake +1 -35
  156. data/tasks/sourcehut.rake +4 -1
  157. metadata +62 -15
  158. data/doc/contributors/dwim_dx.md +0 -366
  159. data/doc/contributors/examples_audit/p1_high.md +0 -21
  160. data/doc/contributors/examples_audit/p2_moderate.md +0 -81
  161. data/doc/contributors/examples_audit.md +0 -41
  162. data/doc/images/app_analytics.png +0 -0
  163. data/doc/images/app_custom_widget.png +0 -0
  164. data/doc/images/app_mouse_events.png +0 -0
  165. data/doc/images/widget_table_flex.png +0 -0
  166. data/lib/ratatui_ruby/session/autodoc.rb +0 -482
  167. data/lib/ratatui_ruby/session.rb +0 -178
  168. data/tasks/autodoc/inventory.rb +0 -63
  169. data/tasks/autodoc/notice.rb +0 -26
  170. data/tasks/autodoc/rbs.rb +0 -38
  171. data/tasks/autodoc/rdoc.rb +0 -45
@@ -0,0 +1,60 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Table Rich Text, Row, Cell, and Line#width
8
+
9
+ ## User Review Required
10
+
11
+ > [!IMPORTANT]
12
+ > **Namespace Decision:** Following existing patterns (`Text::Span`, `BarChart::Bar`), I recommend:
13
+ > - `RatatuiRuby::Table::Cell` — table cell construction (content + style)
14
+ > - `RatatuiRuby::Table::Row` — row construction (cells + style + height)
15
+ > - `RatatuiRuby::Buffer::Cell` — buffer inspection (renamed from current `Cell`)
16
+ >
17
+ > This groups related concepts and mirrors how Ratatui separates `widgets::Cell` from `buffer::Cell`.
18
+
19
+ ### Breaking Changes
20
+
21
+ 1. `RatatuiRuby::Cell` → `RatatuiRuby::Buffer::Cell` (buffer inspection)
22
+ 2. `RatatuiRuby::Row` → `RatatuiRuby::Table::Row` (if we move it)
23
+
24
+ ---
25
+
26
+ ## Proposed Structure
27
+
28
+ ```
29
+ RatatuiRuby::
30
+ ├── Table # Table widget
31
+ │ ├── Cell # NEW: content + style for cells
32
+ │ └── Row # MOVE: cells + style + height
33
+ ├── Buffer::
34
+ │ └── Cell # RENAME: from RatatuiRuby::Cell
35
+ ├── Text::
36
+ │ ├── Span # existing
37
+ │ └── Line # existing (now with #width)
38
+ └── ... other widgets
39
+ ```
40
+
41
+ ---
42
+
43
+ ## Changes Summary
44
+
45
+ | Feature | Status |
46
+ |---------|--------|
47
+ | Table cells accept Text::Span/Line | ✅ Done |
48
+ | Row class with style/height | ✅ Done (needs move to Table::Row) |
49
+ | Line#width method | ✅ Done |
50
+ | Table::Cell class | ⏳ Pending approval |
51
+ | Buffer::Cell rename | ⏳ Pending approval |
52
+
53
+ ---
54
+
55
+ ## Verification
56
+
57
+ ```bash
58
+ bin/agent_rake
59
+ ```
60
+
@@ -0,0 +1,37 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
3
+
4
+ SPDX-License-Identifier: CC-BY-SA-4.0
5
+ -->
6
+
7
+ # Architectural Overhaul: Strict Modularization
8
+
9
+ ## Phase 1: The Great Rename
10
+ - [x] Create `lib/ratatui_ruby/layout/` module with Rect, Constraint, Layout
11
+ - [x] Create `lib/ratatui_ruby/widgets/` module with Table, List, Paragraph, Block, etc.
12
+ - [x] Create `lib/ratatui_ruby/style/` module with Style
13
+ - [x] Create `lib/ratatui_ruby/buffer/` module with Cell (renamed from current)
14
+ - [x] Update `lib/ratatui_ruby.rb` requires
15
+ - [x] Update Rust backend for new class names in rendering.rs
16
+ - [x] Update all tests for new namespaces (0 errors, from 471)
17
+ - [x] Fix RuboCop issues
18
+
19
+ ## Phase 2: Session Hardening
20
+ - [x] Rewrite Session with explicit factory methods (no metaprogramming)
21
+ - [x] Add RDoc to each factory method
22
+ - [x] Ensure IDE autocomplete works
23
+
24
+ ## Phase 3: Table Enhancements
25
+ - [x] Implement `Widgets::Cell` (content + style)
26
+ - [x] Move Row to `Widgets::Row`
27
+ - [x] Add `table_row` and `table_cell` helpers to Session
28
+ - [x] Update table.rs for new types
29
+
30
+ ## Phase 4: Fix Examples and Documentation
31
+ - [x] Update all examples, RDoc, and *.md to use new namespaces and TUI API
32
+ - [x] Update CHANGELOG with migration guide
33
+
34
+ ## Definition of Done
35
+ - [x] `bin/agent_rake` passes
36
+ - [x] CHANGELOG updated with breaking changes
37
+
@@ -5,136 +5,332 @@
5
5
 
6
6
  # Ruby Frontend Design (`ratatui_ruby`)
7
7
 
8
- This document describes the design philosophy and structure of the Ruby layer in `ratatui_ruby`.
8
+ This document describes the architectural design and guiding principles of the Ruby layer in `ratatui_ruby`. It is intended for contributors, architects, and AI agents working on the codebase.
9
9
 
10
- ## Core Philosophy: Data-Driven UI
10
+ ## Guiding Design Principles
11
11
 
12
+ ### 1. Ratatui Alignment
12
13
 
14
+ The Ruby namespace structure mirrors Ratatui's Rust module hierarchy exactly. This is a deliberate architectural choice with specific benefits:
13
15
 
14
- The Ruby frontend is designed as a **thin, declarative layer** over the Rust backend. It uses an **Immediate Mode** paradigm where the user constructs a tree of pure data objects every frame to represent the desired UI state.
16
+ - **Documentation Mapping**: A contributor reading Ratatui's docs for `ratatui::widgets::Table` immediately knows to look at `RatatuiRuby::Widgets::Table`.
17
+ - **Predictability**: No mental translation required between Rust and Ruby codebases.
18
+ - **Scalability**: As Ratatui adds new types, the Ruby placement is deterministic.
15
19
 
16
- ### 1. Separation of Configuration and Status
20
+ **Module Mapping:**
17
21
 
18
- `ratatui_ruby` strictly separates **what** a widget is (Configuration) from **where** it is (Status).
22
+ | Rust Module | Ruby Module | Purpose |
23
+ |-------------|-------------|---------|
24
+ | `ratatui::layout` | `RatatuiRuby::Layout` | Rect, Constraint, Layout |
25
+ | `ratatui::widgets` | `RatatuiRuby::Widgets` | All widgets (Table, List, Paragraph, Block, etc.) |
26
+ | `ratatui::style` | `RatatuiRuby::Style` | Style, Color |
27
+ | `ratatui::text` | `RatatuiRuby::Text` | Span, Line |
28
+ | `ratatui::buffer` | `RatatuiRuby::Buffer` | Cell (for buffer inspection) |
19
29
 
20
- #### Configuration (Input)
21
- Widgets (e.g., `RatatuiRuby::List`) are immutable value objects defining the *desired appearance* for the current frame. They are pure inputs to the renderer.
30
+ This structure resolves name collisions that would otherwise require arbitrary prefixes. For example, `Buffer::Cell` (terminal cell inspection) vs `Widgets::Cell` (table cell construction) are clearly distinct.
22
31
 
23
- * Implemented using Ruby 3.2+ `Data` classes.
24
- * Located in `lib/ratatui_ruby/schema/`.
25
- * Act as a Schema/IDL between Ruby and Rust.
32
+ ### 2. Two-Layer Architecture
33
+
34
+ The Ruby frontend implements a "Mullet Architecture": structured namespaces in the library, flat ergonomic DSL for users.
35
+
36
+ **Layer 1: Schema Classes (The Library)**
37
+
38
+ Located in `lib/ratatui_ruby/widgets/`, `lib/ratatui_ruby/layout/`, etc.
39
+
40
+ These are the actual `Data.define` classes that the Rust backend expects. They have deep, explicit namespaces that match Ratatui:
26
41
 
27
- **Example:**
28
42
  ```ruby
29
- # This is just a piece of data, not a "live" widget
30
- paragraph = RatatuiRuby::Paragraph.new(
31
- text: "Hello World",
32
- style: RatatuiRuby::Style.new(fg: :red),
33
- block: nil
34
- )
43
+ RatatuiRuby::Widgets::Paragraph.new(text: "Hello")
44
+ RatatuiRuby::Layout::Constraint.length(20)
45
+ RatatuiRuby::Style::Style.new(fg: :red)
35
46
  ```
36
47
 
37
- #### Status (Output)
38
- **Optional.** Only specific widgets (like `List` and `Table`) rely on runtime status. State objects (e.g., `RatatuiRuby::ListState`) track metrics calculated by the backend, such as scroll offsets.
48
+ **Layer 2: TUI Facade (The DSL)**
39
49
 
40
- * passed as a *secondary argument* to `render_stateful_widget`.
41
- * **The Widget Configuration is still required.** You cannot render a State without its corresponding Widget.
42
- * Updated in-place by the Rust backend to reflect the actual rendered state.
50
+ Located in `lib/ratatui_ruby/tui.rb` and `lib/ratatui_ruby/tui/*.rb` mixins.
43
51
 
44
- **Example:**
45
- ```ruby
46
- # 1. Initialize State once (Input/Output)
47
- list_state = RatatuiRuby::ListState.new
48
- list_state.select(3)
52
+ The `TUI` class provides shorthand factory methods that hide namespace verbosity:
49
53
 
54
+ ```ruby
50
55
  RatatuiRuby.run do |tui|
51
- loop do
52
- tui.draw do |frame|
53
- # 2. Define Configuration (Input)
54
- # (Note: In a real app, you'd probably use `tui.list(...)` helper)
55
- list = RatatuiRuby::List.new(items: ["A", "B", "C", "D"])
56
+ tui.paragraph(text: "Hello")
57
+ tui.constraint_length(20)
58
+ tui.style(fg: :red)
59
+ end
60
+ ```
61
+
62
+ **Why This Matters:**
63
+
64
+ Users write application code using the TUI API and rarely touch deep namespaces. Contributors maintaining the library work with explicit, documentable, IDE-friendly classes. Both audiences are served without compromise.
65
+
66
+ ### 3. Explicit Over Magic
67
+
68
+ The TUI facade uses explicit factory method definitions, not runtime metaprogramming.
69
+
70
+ **What We Do:**
71
+
72
+ ```ruby
73
+ # lib/ratatui_ruby/tui/widget_factories.rb
74
+ module RatatuiRuby
75
+ class TUI
76
+ module WidgetFactories
77
+ def paragraph(**kwargs)
78
+ Widgets::Paragraph.new(**kwargs)
79
+ end
56
80
 
57
- # 3. Render with both (Side Effect: updates list_state)
58
- frame.render_stateful_widget(list, frame.area, list_state)
81
+ def table(**kwargs)
82
+ Widgets::Table.new(**kwargs)
83
+ end
59
84
  end
60
-
61
- # 4. Read back Status (Output)
62
- # If the backend auto-scrolled to keep index 3 visible:
63
- puts "Scroll Offset: #{list_state.offset}"
64
-
65
- break if tui.poll_event == "q"
66
85
  end
67
86
  end
68
87
  ```
69
88
 
70
- ### 2. Immediate Mode Rendering
89
+ **What We Don't Do:**
90
+
91
+ ```ruby
92
+ # NO: Dynamic method generation
93
+ RatatuiRuby.constants.each do |const|
94
+ define_method(const.underscore) { |**kw| RatatuiRuby.const_get(const).new(**kw) }
95
+ end
96
+ ```
97
+
98
+ **Benefits of Explicit Definitions:**
99
+
100
+ 1. **IDE Support**: Solargraph and Ruby LSP provide autocomplete because methods exist at parse time.
101
+ 2. **RDoc**: Each method can have its own documentation with examples.
102
+ 3. **RBS Types**: Each method has an explicit type signature.
103
+ 4. **Debugging**: Stack traces show real method names, not `define_method` closures.
104
+ 5. **Decoupling**: Internal class names can change without breaking the public TUI API.
71
105
 
72
- The application loop typically looks like this:
106
+ ### 4. Data-Driven UI (Immediate Mode)
73
107
 
74
- 1. **Poll Event**: Ruby asks Rust for the next event.
75
- 2. **Update State**: Ruby application code updates its own domain state (e.g., `counter += 1`).
76
- 3. **Render**: Ruby constructs a fresh View Tree based on the current domain state and passes the root node to `RatatuiRuby.draw`.
108
+ All UI components are pure, immutable `Data.define` value objects. They describe *desired appearance* for a single frame, not live stateful objects.
109
+
110
+ **Widgets Are Inputs:**
111
+
112
+ ```ruby
113
+ # This is just data. It has no behavior, no side effects.
114
+ paragraph = RatatuiRuby::Widgets::Paragraph.new(
115
+ text: "Hello",
116
+ style: RatatuiRuby::Style::Style.new(fg: :red)
117
+ )
118
+
119
+ # Pass to renderer as input
120
+ frame.render_widget(paragraph, area)
121
+ ```
122
+
123
+ **Immediate Mode Loop:**
124
+
125
+ Every frame, the application constructs a fresh view tree and passes it to `draw`. No widget state persists between frames. This is Ratatui's core paradigm.
77
126
 
78
127
  ```ruby
79
128
  loop do
80
- # 1. & 2. Handle events and update state
81
- event = RatatuiRuby.poll_event
82
- break if event == :esc
83
-
84
- # 3. Construct View Tree & Draw
85
- RatatuiRuby.draw do |frame|
86
- frame.render_widget(
87
- RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}"),
88
- frame.area
89
- )
129
+ tui.draw do |frame|
130
+ # Fresh tree every frame
131
+ frame.render_widget(tui.paragraph(text: "Time: #{Time.now}"), frame.area)
90
132
  end
133
+ break if tui.poll_event.key? && tui.poll_event.code == "q"
91
134
  end
92
135
  ```
93
136
 
94
- ### 3. No render logic in Ruby
137
+ ### 5. Separation of Configuration and Status
138
+
139
+ Widgets (Configuration) and State (Status) are strictly separated.
140
+
141
+ **Configuration (Input):**
142
+
143
+ Widgets define *what* to render. They are created, rendered, and discarded.
144
+
145
+ ```ruby
146
+ list = tui.list(items: ["A", "B", "C", "D", "E"])
147
+ ```
148
+
149
+ **Status (Output):**
150
+
151
+ State objects track *runtime metrics* computed by the Rust backend: scroll offsets, selection positions, etc. They persist across frames.
152
+
153
+ ```ruby
154
+ # Created once
155
+ @list_state = RatatuiRuby::ListState.new
156
+
157
+ # Used every frame
158
+ frame.render_stateful_widget(list, area, @list_state)
159
+
160
+ # Read back computed values
161
+ puts "Scroll offset: #{@list_state.offset}"
162
+ ```
163
+
164
+ **Precedence Rule:**
95
165
 
96
- The Ruby classes in `lib/ratatui_ruby/schema/` should **not** contain rendering logic. They are strictly for structural definition and validation. All rendering logic resides in the Rust extension (`ext/ratatui_ruby/`), which walks this Ruby object tree and produces Ratatui primitives.
166
+ When using `render_stateful_widget`, the State object is the source of truth. Widget properties like `selected_index` are ignored.
167
+
168
+ ### 6. No Render Logic in Ruby
169
+
170
+ Ruby defines data structures. Rust renders them.
171
+
172
+ The classes in `lib/ratatui_ruby/widgets/` contain no rendering code. They are pure structural definitions that the Rust extension walks and converts to Ratatui primitives.
173
+
174
+ **Ruby's Job:**
175
+ - Define `Data.define` classes with attributes
176
+ - Validate inputs (types, ranges)
177
+ - Provide convenience constructors
178
+
179
+ **Rust's Job:**
180
+ - Walk the Ruby object tree
181
+ - Extract attributes via `funcall`
182
+ - Construct Ratatui widgets
183
+ - Render to the terminal buffer
184
+
185
+ This separation ensures rendering performance remains in Rust while Ruby handles the ergonomic API layer.
186
+
187
+ ---
188
+
189
+ ## Directory Structure
190
+
191
+ ```
192
+ lib/ratatui_ruby/
193
+ ├── tui.rb # TUI class, includes all mixins
194
+ ├── tui/ # TUI facade mixins
195
+ │ ├── core.rb # draw, poll_event, get_cell_at
196
+ │ ├── layout_factories.rb # rect, constraint_*, layout_split
197
+ │ ├── style_factories.rb # style
198
+ │ ├── widget_factories.rb # paragraph, block, table, list, etc.
199
+ │ ├── text_factories.rb # span, line, text_width
200
+ │ ├── state_factories.rb # list_state, table_state, scrollbar_state
201
+ │ ├── canvas_factories.rb # shape_map, shape_line, etc.
202
+ │ └── buffer_factories.rb # cell (for buffer inspection)
203
+ ├── layout/ # ratatui::layout
204
+ │ ├── rect.rb
205
+ │ ├── constraint.rb
206
+ │ └── layout.rb
207
+ ├── widgets/ # ratatui::widgets
208
+ │ ├── paragraph.rb
209
+ │ ├── block.rb
210
+ │ ├── table.rb
211
+ │ ├── list.rb
212
+ │ ├── row.rb # Table row wrapper
213
+ │ ├── cell.rb # Table cell wrapper (NOT buffer cell)
214
+ │ └── ...
215
+ ├── style/ # ratatui::style
216
+ │ └── style.rb
217
+ ├── text/ # ratatui::text
218
+ │ ├── span.rb
219
+ │ └── line.rb
220
+ ├── buffer/ # ratatui::buffer
221
+ │ └── cell.rb # For get_cell_at inspection
222
+ └── schema/ # Legacy location (being migrated)
223
+ ```
224
+
225
+ ---
97
226
 
98
227
  ## Adding a New Widget
99
228
 
100
- To add a new widget to the Ruby frontend:
229
+ ### Step 1: Create the Schema Class
101
230
 
102
- 1. Define the class in `lib/ratatui_ruby/schema/`.
103
- 2. Use `Data.define`.
104
- 3. Ensure attribute names match what the Rust rendering logic expects (see `ext/ratatui_ruby/src/widgets/`).
231
+ Define the Data class in the appropriate namespace directory:
105
232
 
106
233
  ```ruby
107
- # lib/ratatui_ruby/schema/my_widget.rb
234
+ # lib/ratatui_ruby/widgets/my_widget.rb
108
235
  module RatatuiRuby
109
- # A widget that does something specific.
110
- #
111
- # [some_property] The description of the property.
112
- # [style] The style to apply.
113
- # [block] Optional block widget.
114
- class MyWidget < Data.define(:some_property, :style, :block)
115
- # Creates a new MyWidget.
236
+ module Widgets
237
+ # A widget that displays foo with optional styling.
116
238
  #
117
- # [some_property] The description of the property.
118
- # [style] The style to apply.
119
- # [block] Optional block widget.
120
- def initialize(some_property:, style: nil, block: nil)
121
- super
239
+ # [content] The text content to display.
240
+ # [style] Optional styling for the content.
241
+ # [block] Optional block border wrapper.
242
+ class MyWidget < Data.define(:content, :style, :block)
243
+ def initialize(content:, style: nil, block: nil)
244
+ super
245
+ end
122
246
  end
123
247
  end
124
248
  end
125
249
  ```
126
250
 
127
- And define the types in the corresponding `.rbs` file:
251
+ ### Step 2: Add the RBS Type
128
252
 
129
253
  ```rbs
130
- # sig/ratatui_ruby/schema/my_widget.rbs
254
+ # sig/ratatui_ruby/widgets/my_widget.rbs
131
255
  module RatatuiRuby
132
- class MyWidget < Data
133
- attr_reader some_property: String
134
- attr_reader style: Style?
135
- attr_reader block: Block?
256
+ module Widgets
257
+ class MyWidget < Data
258
+ attr_reader content: String
259
+ attr_reader style: Style::Style?
260
+ attr_reader block: Block?
136
261
 
137
- def self.new: (some_property: String, ?style: Style?, ?block: Block?) -> MyWidget
262
+ def self.new: (content: String, ?style: Style::Style?, ?block: Block?) -> MyWidget
263
+ end
138
264
  end
139
265
  end
140
266
  ```
267
+
268
+ ### Step 3: Add the TUI Factory Method
269
+
270
+ ```ruby
271
+ # lib/ratatui_ruby/tui/widget_factories.rb
272
+ def my_widget(**kwargs)
273
+ Widgets::MyWidget.new(**kwargs)
274
+ end
275
+ ```
276
+
277
+ ### Step 4: Implement Rust Rendering
278
+
279
+ See `rust_backend.md` for the Rust implementation steps.
280
+
281
+ ### Step 5: Register in Requires
282
+
283
+ Add to `lib/ratatui_ruby.rb`:
284
+
285
+ ```ruby
286
+ require_relative "ratatui_ruby/widgets/my_widget"
287
+ ```
288
+
289
+ ---
290
+
291
+ ## TUI Mixin Architecture
292
+
293
+ The `TUI` class is composed of 8 focused mixins, each with a single responsibility:
294
+
295
+ | Mixin | Methods | Purpose |
296
+ |-------|---------|---------|
297
+ | `Core` | `draw`, `poll_event`, `get_cell_at`, `draw_cell` | Terminal I/O operations |
298
+ | `LayoutFactories` | `rect`, `constraint_*`, `layout`, `layout_split` | Layout construction |
299
+ | `StyleFactories` | `style` | Style construction |
300
+ | `WidgetFactories` | `paragraph`, `block`, `table`, `list`, etc. | Widget construction |
301
+ | `TextFactories` | `span`, `line`, `text_width` | Text construction |
302
+ | `StateFactories` | `list_state`, `table_state`, `scrollbar_state` | State object construction |
303
+ | `CanvasFactories` | `shape_map`, `shape_line`, `shape_circle`, etc. | Canvas shape construction |
304
+ | `BufferFactories` | `cell` | Buffer cell construction (for testing) |
305
+
306
+ This modular structure keeps each file focused (~20-50 lines) and makes it easy to locate and modify factory methods.
307
+
308
+ ---
309
+
310
+ ## Thread and Ractor Safety
311
+
312
+ ### Shareable (Frozen Data Objects)
313
+
314
+ These are deeply frozen and `Ractor.shareable?`:
315
+
316
+ - `Event::*` objects from `poll_event`
317
+ - `Buffer::Cell` objects from `get_cell_at`
318
+ - `Layout::Rect` objects from `Layout.split`
319
+
320
+ ### Not Shareable (I/O Handles)
321
+
322
+ These have side effects and are intentionally not Ractor-safe:
323
+
324
+ - `TUI` — Has terminal I/O methods
325
+ - `Frame` — Valid only during the `draw` block; invalid after
326
+
327
+ ```ruby
328
+ # OK: Cache TUI during run loop
329
+ RatatuiRuby.run do |tui|
330
+ @tui = tui
331
+ loop { render; handle_input }
332
+ end
333
+
334
+ # NOT OK: Include in immutable Model
335
+ Model = Data.define(:tui, :count) # Don't do this
336
+ ```