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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +4 -4
- data/CHANGELOG.md +35 -0
- data/README.md +26 -1
- data/doc/application_architecture.md +16 -16
- data/doc/application_testing.md +1 -1
- data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
- data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
- data/doc/contributors/architectural_overhaul/task.md +37 -0
- data/doc/contributors/design/ruby_frontend.md +277 -81
- data/doc/contributors/design/rust_backend.md +349 -55
- data/doc/contributors/developing_examples.md +5 -5
- data/doc/contributors/index.md +7 -5
- data/doc/contributors/v1.0.0_blockers.md +1729 -0
- data/doc/index.md +11 -6
- data/doc/interactive_design.md +2 -2
- data/doc/quickstart.md +66 -97
- data/doc/v0.7.0_migration.md +236 -0
- data/doc/why.md +93 -0
- data/examples/app_all_events/README.md +6 -4
- data/examples/app_all_events/app.rb +1 -1
- data/examples/app_all_events/model/app_model.rb +1 -1
- data/examples/app_all_events/model/msg.rb +1 -1
- data/examples/app_all_events/update.rb +1 -1
- data/examples/app_all_events/view/app_view.rb +1 -1
- data/examples/app_all_events/view/controls_view.rb +1 -1
- data/examples/app_all_events/view/counts_view.rb +1 -1
- data/examples/app_all_events/view/live_view.rb +1 -1
- data/examples/app_all_events/view/log_view.rb +1 -1
- data/examples/app_color_picker/README.md +7 -5
- data/examples/app_color_picker/app.rb +1 -1
- data/examples/app_login_form/README.md +2 -0
- data/examples/app_stateful_interaction/README.md +2 -0
- data/examples/app_stateful_interaction/app.rb +1 -1
- data/examples/verify_quickstart_dsl/README.md +4 -3
- data/examples/verify_quickstart_dsl/app.rb +1 -1
- data/examples/verify_quickstart_layout/README.md +1 -1
- data/examples/verify_quickstart_lifecycle/README.md +3 -3
- data/examples/verify_quickstart_lifecycle/app.rb +2 -2
- data/examples/verify_readme_usage/README.md +1 -1
- data/examples/widget_barchart_demo/README.md +2 -1
- data/examples/widget_block_demo/README.md +2 -0
- data/examples/widget_box_demo/README.md +3 -3
- data/examples/widget_calendar_demo/README.md +3 -3
- data/examples/widget_calendar_demo/app.rb +5 -1
- data/examples/widget_canvas_demo/README.md +3 -3
- data/examples/widget_cell_demo/README.md +3 -3
- data/examples/widget_center_demo/README.md +3 -3
- data/examples/widget_chart_demo/README.md +3 -3
- data/examples/widget_gauge_demo/README.md +3 -3
- data/examples/widget_layout_split/README.md +3 -3
- data/examples/widget_line_gauge_demo/README.md +3 -3
- data/examples/widget_list_demo/README.md +3 -3
- data/examples/widget_map_demo/README.md +3 -3
- data/examples/widget_map_demo/app.rb +2 -2
- data/examples/widget_overlay_demo/README.md +36 -0
- data/examples/widget_popup_demo/README.md +3 -3
- data/examples/widget_ratatui_logo_demo/README.md +3 -3
- data/examples/widget_ratatui_logo_demo/app.rb +1 -1
- data/examples/widget_ratatui_mascot_demo/README.md +3 -3
- data/examples/widget_rect/README.md +3 -3
- data/examples/widget_render/README.md +3 -3
- data/examples/widget_render/app.rb +3 -3
- data/examples/widget_rich_text/README.md +3 -3
- data/examples/widget_scroll_text/README.md +3 -3
- data/examples/widget_scrollbar_demo/README.md +3 -3
- data/examples/widget_sparkline_demo/README.md +3 -3
- data/examples/widget_style_colors/README.md +3 -3
- data/examples/widget_table_demo/README.md +3 -3
- data/examples/widget_table_demo/app.rb +19 -4
- data/examples/widget_tabs_demo/README.md +3 -3
- data/examples/widget_text_width/README.md +3 -3
- data/examples/widget_text_width/app.rb +8 -1
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/frame.rs +6 -5
- data/ext/ratatui_ruby/src/lib.rs +3 -2
- data/ext/ratatui_ruby/src/rendering.rs +22 -21
- data/ext/ratatui_ruby/src/text.rs +12 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
- data/ext/ratatui_ruby/src/widgets/table.rs +81 -36
- data/lib/ratatui_ruby/buffer/cell.rb +168 -0
- data/lib/ratatui_ruby/buffer.rb +15 -0
- data/lib/ratatui_ruby/frame.rb +8 -8
- data/lib/ratatui_ruby/layout/constraint.rb +95 -0
- data/lib/ratatui_ruby/layout/layout.rb +106 -0
- data/lib/ratatui_ruby/layout/rect.rb +118 -0
- data/lib/ratatui_ruby/layout.rb +19 -0
- data/lib/ratatui_ruby/list_state.rb +2 -2
- data/lib/ratatui_ruby/schema/layout.rb +1 -1
- data/lib/ratatui_ruby/schema/row.rb +66 -0
- data/lib/ratatui_ruby/schema/table.rb +10 -10
- data/lib/ratatui_ruby/schema/text.rb +27 -2
- data/lib/ratatui_ruby/style/style.rb +81 -0
- data/lib/ratatui_ruby/style.rb +15 -0
- data/lib/ratatui_ruby/table_state.rb +1 -1
- data/lib/ratatui_ruby/test_helper/snapshot.rb +24 -0
- data/lib/ratatui_ruby/test_helper/style_assertions.rb +1 -1
- data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/core.rb +38 -0
- data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
- data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
- data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
- data/lib/ratatui_ruby/tui.rb +75 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
- data/lib/ratatui_ruby/widgets/block.rb +192 -0
- data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
- data/lib/ratatui_ruby/widgets/cell.rb +47 -0
- data/lib/ratatui_ruby/widgets/center.rb +59 -0
- data/lib/ratatui_ruby/widgets/chart.rb +185 -0
- data/lib/ratatui_ruby/widgets/clear.rb +54 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
- data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
- data/lib/ratatui_ruby/widgets/list.rb +127 -0
- data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
- data/lib/ratatui_ruby/widgets/row.rb +68 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
- data/lib/ratatui_ruby/widgets/table.rb +141 -0
- data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
- data/lib/ratatui_ruby/widgets.rb +40 -0
- data/lib/ratatui_ruby.rb +23 -39
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_all_events/view_state.rbs +1 -1
- data/sig/ratatui_ruby/schema/row.rbs +22 -0
- data/sig/ratatui_ruby/schema/table.rbs +1 -1
- data/sig/ratatui_ruby/schema/text.rbs +1 -0
- data/sig/ratatui_ruby/session.rbs +29 -49
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/core.rbs +14 -0
- data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
- data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
- data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
- data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
- data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
- data/sig/ratatui_ruby/tui.rbs +19 -0
- data/tasks/autodoc.rake +1 -35
- data/tasks/sourcehut.rake +4 -1
- metadata +62 -15
- data/doc/contributors/dwim_dx.md +0 -366
- data/doc/contributors/examples_audit/p1_high.md +0 -21
- data/doc/contributors/examples_audit/p2_moderate.md +0 -81
- data/doc/contributors/examples_audit.md +0 -41
- data/doc/images/app_analytics.png +0 -0
- data/doc/images/app_custom_widget.png +0 -0
- data/doc/images/app_mouse_events.png +0 -0
- data/doc/images/widget_table_flex.png +0 -0
- data/lib/ratatui_ruby/session/autodoc.rb +0 -482
- data/lib/ratatui_ruby/session.rb +0 -178
- data/tasks/autodoc/inventory.rb +0 -63
- data/tasks/autodoc/notice.rb +0 -26
- data/tasks/autodoc/rbs.rb +0 -38
- data/tasks/autodoc/rdoc.rb +0 -45
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module Buffer
|
|
8
|
+
# Represents a single cell in the terminal buffer.
|
|
9
|
+
#
|
|
10
|
+
# A terminal grid is made of cells. Each cell contains a character (symbol) and styling (colors, modifiers).
|
|
11
|
+
# When testing, you often need to verify that a specific cell renders correctly.
|
|
12
|
+
#
|
|
13
|
+
# This object encapsulates that state. It provides predicate methods for modifiers, making assertions readable.
|
|
14
|
+
#
|
|
15
|
+
# Use it to inspect the visual state of your application in tests.
|
|
16
|
+
#
|
|
17
|
+
# === Examples
|
|
18
|
+
#
|
|
19
|
+
# cell = RatatuiRuby.get_cell_at(0, 0)
|
|
20
|
+
# cell.char # => "H"
|
|
21
|
+
# cell.fg # => :red
|
|
22
|
+
# cell.bold? # => true
|
|
23
|
+
#
|
|
24
|
+
class Cell
|
|
25
|
+
# The character displayed in the cell.
|
|
26
|
+
#
|
|
27
|
+
# Named to match Ratatui's Cell::symbol() method.
|
|
28
|
+
attr_reader :symbol
|
|
29
|
+
|
|
30
|
+
# Alias for Rubyists who prefer a shorter name.
|
|
31
|
+
alias char symbol
|
|
32
|
+
|
|
33
|
+
# The foreground color of the cell (e.g., :red, :blue, "#ff0000").
|
|
34
|
+
attr_reader :fg
|
|
35
|
+
|
|
36
|
+
# The background color of the cell (e.g., :black, nil).
|
|
37
|
+
attr_reader :bg
|
|
38
|
+
|
|
39
|
+
# The list of active modifiers (e.g., ["bold", "italic"]).
|
|
40
|
+
attr_reader :modifiers
|
|
41
|
+
|
|
42
|
+
# Returns an empty cell (space character, no styles).
|
|
43
|
+
#
|
|
44
|
+
# === Example
|
|
45
|
+
#
|
|
46
|
+
# Buffer::Cell.empty # => #<RatatuiRuby::Buffer::Cell char=" ">
|
|
47
|
+
#
|
|
48
|
+
def self.empty
|
|
49
|
+
new(symbol: " ", fg: nil, bg: nil, modifiers: [])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Returns a default cell (alias for empty).
|
|
53
|
+
#
|
|
54
|
+
# === Example
|
|
55
|
+
#
|
|
56
|
+
# Buffer::Cell.default # => #<RatatuiRuby::Buffer::Cell char=" ">
|
|
57
|
+
#
|
|
58
|
+
def self.default
|
|
59
|
+
empty
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Returns a cell with a specific character and no styles.
|
|
63
|
+
#
|
|
64
|
+
# [symbol] String (single character).
|
|
65
|
+
#
|
|
66
|
+
# === Example
|
|
67
|
+
#
|
|
68
|
+
# Buffer::Cell.symbol("X") # => #<RatatuiRuby::Buffer::Cell symbol="X">
|
|
69
|
+
#
|
|
70
|
+
def self.symbol(symbol)
|
|
71
|
+
new(symbol:, fg: nil, bg: nil, modifiers: [])
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Alias for Rubyists who prefer a shorter name.
|
|
75
|
+
def self.char(char)
|
|
76
|
+
symbol(char)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Creates a new Cell.
|
|
80
|
+
#
|
|
81
|
+
# [symbol] String (single character). Aliased as <tt>char:</tt>.
|
|
82
|
+
# [fg] Symbol or String (nullable).
|
|
83
|
+
# [bg] Symbol or String (nullable).
|
|
84
|
+
# [modifiers] Array of Strings.
|
|
85
|
+
def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
|
|
86
|
+
@symbol = (symbol || char || " ").freeze
|
|
87
|
+
@fg = fg&.freeze
|
|
88
|
+
@bg = bg&.freeze
|
|
89
|
+
@modifiers = modifiers.map(&:freeze).freeze
|
|
90
|
+
freeze
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns true if the cell has the bold modifier.
|
|
94
|
+
def bold?
|
|
95
|
+
modifiers.include?("bold")
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Returns true if the cell has the dim modifier.
|
|
99
|
+
def dim?
|
|
100
|
+
modifiers.include?("dim")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
# Returns true if the cell has the italic modifier.
|
|
104
|
+
def italic?
|
|
105
|
+
modifiers.include?("italic")
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Returns true if the cell has the underlined modifier.
|
|
109
|
+
def underlined?
|
|
110
|
+
modifiers.include?("underlined")
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Returns true if the cell has the slow_blink modifier.
|
|
114
|
+
def slow_blink?
|
|
115
|
+
modifiers.include?("slow_blink")
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Returns true if the cell has the rapid_blink modifier.
|
|
119
|
+
def rapid_blink?
|
|
120
|
+
modifiers.include?("rapid_blink")
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Returns true if the cell has the reversed modifier.
|
|
124
|
+
def reversed?
|
|
125
|
+
modifiers.include?("reversed")
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Returns true if the cell has the hidden modifier.
|
|
129
|
+
def hidden?
|
|
130
|
+
modifiers.include?("hidden")
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Returns true if the cell has the crossed_out modifier.
|
|
134
|
+
def crossed_out?
|
|
135
|
+
modifiers.include?("crossed_out")
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Checks equality with another Cell.
|
|
139
|
+
def ==(other)
|
|
140
|
+
other.is_a?(Cell) &&
|
|
141
|
+
char == other.char &&
|
|
142
|
+
fg == other.fg &&
|
|
143
|
+
bg == other.bg &&
|
|
144
|
+
modifiers == other.modifiers
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Returns a string representation of the cell.
|
|
148
|
+
def inspect
|
|
149
|
+
parts = ["symbol=#{symbol.inspect}"]
|
|
150
|
+
parts << "fg=#{fg.inspect}" if fg
|
|
151
|
+
parts << "bg=#{bg.inspect}" if bg
|
|
152
|
+
parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
|
|
153
|
+
"#<#{self.class} #{parts.join(' ')}>"
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Returns the cell's character.
|
|
157
|
+
def to_s
|
|
158
|
+
symbol
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# Support for pattern matching.
|
|
162
|
+
# Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
|
|
163
|
+
def deconstruct_keys(keys)
|
|
164
|
+
{ symbol:, char: symbol, fg:, bg:, modifiers: }
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
# Buffer primitives for terminal cell inspection.
|
|
8
|
+
#
|
|
9
|
+
# This module mirrors +ratatui::buffer+ and contains:
|
|
10
|
+
# - {Cell} — Single terminal cell (for inspection)
|
|
11
|
+
module Buffer
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
require_relative "buffer/cell"
|
data/lib/ratatui_ruby/frame.rb
CHANGED
|
@@ -21,7 +21,7 @@ module RatatuiRuby
|
|
|
21
21
|
# Frame is an *I/O handle*, not a data object. It has side effects
|
|
22
22
|
# (render_widget, set_cursor_position) and is intentionally *not*
|
|
23
23
|
# Ractor-shareable. Passing it to helper methods during the draw block is
|
|
24
|
-
# fine. However, do not include it in immutable
|
|
24
|
+
# fine. However, do not include it in immutable Models/Messages or pass
|
|
25
25
|
# it to other Ractors. Frame is only valid during the draw block's execution.
|
|
26
26
|
#
|
|
27
27
|
# === Examples
|
|
@@ -29,7 +29,7 @@ module RatatuiRuby
|
|
|
29
29
|
# Basic usage with a single widget:
|
|
30
30
|
#
|
|
31
31
|
# RatatuiRuby.draw do |frame|
|
|
32
|
-
# paragraph = RatatuiRuby::Paragraph.new(text: "Hello, world!")
|
|
32
|
+
# paragraph = RatatuiRuby::Widgets::Paragraph.new(text: "Hello, world!")
|
|
33
33
|
# frame.render_widget(paragraph, frame.area)
|
|
34
34
|
# end
|
|
35
35
|
#
|
|
@@ -40,8 +40,8 @@ module RatatuiRuby
|
|
|
40
40
|
# frame.area,
|
|
41
41
|
# direction: :horizontal,
|
|
42
42
|
# constraints: [
|
|
43
|
-
# RatatuiRuby::Constraint.length(20),
|
|
44
|
-
# RatatuiRuby::Constraint.fill(1)
|
|
43
|
+
# RatatuiRuby::Layout::Constraint.length(20),
|
|
44
|
+
# RatatuiRuby::Layout::Constraint.fill(1)
|
|
45
45
|
# ]
|
|
46
46
|
# )
|
|
47
47
|
#
|
|
@@ -86,7 +86,7 @@ module RatatuiRuby
|
|
|
86
86
|
# === Example
|
|
87
87
|
#
|
|
88
88
|
# RatatuiRuby.draw do |frame|
|
|
89
|
-
# para = RatatuiRuby::Paragraph.new(text: "Content")
|
|
89
|
+
# para = RatatuiRuby::Widgets::Paragraph.new(text: "Content")
|
|
90
90
|
# frame.render_widget(para, frame.area)
|
|
91
91
|
# end
|
|
92
92
|
#
|
|
@@ -122,7 +122,7 @@ module RatatuiRuby
|
|
|
122
122
|
# @list_state = RatatuiRuby::ListState.new
|
|
123
123
|
#
|
|
124
124
|
# RatatuiRuby.draw do |frame|
|
|
125
|
-
# list = RatatuiRuby::List.new(items: ["A", "B"])
|
|
125
|
+
# list = RatatuiRuby::Widgets::List.new(items: ["A", "B"])
|
|
126
126
|
# frame.render_stateful_widget(list, frame.area, @list_state)
|
|
127
127
|
# end
|
|
128
128
|
#
|
|
@@ -160,9 +160,9 @@ module RatatuiRuby
|
|
|
160
160
|
#
|
|
161
161
|
# RatatuiRuby.draw do |frame|
|
|
162
162
|
# # Render the input field
|
|
163
|
-
# prompt = RatatuiRuby::Paragraph.new(
|
|
163
|
+
# prompt = RatatuiRuby::Widgets::Paragraph.new(
|
|
164
164
|
# text: "#{PREFIX}#{username} ]",
|
|
165
|
-
# block: RatatuiRuby::Block.new(borders: :all)
|
|
165
|
+
# block: RatatuiRuby::Widgets::Block.new(borders: :all)
|
|
166
166
|
# )
|
|
167
167
|
# frame.render_widget(prompt, frame.area)
|
|
168
168
|
#
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module Layout
|
|
8
|
+
# Defines the sizing rule for a layout section.
|
|
9
|
+
#
|
|
10
|
+
# Flexible layouts need rules. You can't just place widgets at absolute coordinates; they must adapt to changing terminal sizes.
|
|
11
|
+
#
|
|
12
|
+
# This class defines the rules of engagement. It tells the layout engine exactly how much space a section requires relative to others.
|
|
13
|
+
#
|
|
14
|
+
# Mix and match fixed lengths, percentages, ratios, and minimums. Build layouts that breathe.
|
|
15
|
+
#
|
|
16
|
+
# === Examples
|
|
17
|
+
#
|
|
18
|
+
# Layout::Constraint.length(5) # Exactly 5 cells
|
|
19
|
+
# Layout::Constraint.percentage(50) # Half the available space
|
|
20
|
+
# Layout::Constraint.min(10) # At least 10 cells, maybe more
|
|
21
|
+
# Layout::Constraint.fill(1) # Fill remaining space (weight 1)
|
|
22
|
+
class Constraint < Data.define(:type, :value)
|
|
23
|
+
##
|
|
24
|
+
# :attr_reader: type
|
|
25
|
+
# The type of constraint.
|
|
26
|
+
#
|
|
27
|
+
# <tt>:length</tt>, <tt>:percentage</tt>, <tt>:min</tt>, <tt>:max</tt>, <tt>:fill</tt>, or <tt>:ratio</tt>.
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# :attr_reader: value
|
|
31
|
+
# The numeric value (or array for ratio) associated with the rule.
|
|
32
|
+
|
|
33
|
+
# Requests a fixed size.
|
|
34
|
+
#
|
|
35
|
+
# Layout::Constraint.length(10) # 10 characters wide/high
|
|
36
|
+
#
|
|
37
|
+
# [v] Number of cells (Integer).
|
|
38
|
+
def self.length(v)
|
|
39
|
+
new(type: :length, value: Integer(v))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Requests a percentage of available space.
|
|
43
|
+
#
|
|
44
|
+
# Layout::Constraint.percentage(25) # 25% of the area
|
|
45
|
+
#
|
|
46
|
+
# [v] Percentage 0-100 (Integer).
|
|
47
|
+
def self.percentage(v)
|
|
48
|
+
new(type: :percentage, value: Integer(v))
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Enforces a minimum size.
|
|
52
|
+
#
|
|
53
|
+
# Layout::Constraint.min(5) # At least 5 cells
|
|
54
|
+
#
|
|
55
|
+
# This section will grow if space permits, but never shrink below +v+.
|
|
56
|
+
#
|
|
57
|
+
# [v] Minimum cells (Integer).
|
|
58
|
+
def self.min(v)
|
|
59
|
+
new(type: :min, value: Integer(v))
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Enforces a maximum size.
|
|
63
|
+
#
|
|
64
|
+
# Layout::Constraint.max(10) # At most 10 cells
|
|
65
|
+
#
|
|
66
|
+
# [v] Maximum cells (Integer).
|
|
67
|
+
def self.max(v)
|
|
68
|
+
new(type: :max, value: Integer(v))
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Fills remaining space proportionally.
|
|
72
|
+
#
|
|
73
|
+
# Layout::Constraint.fill(1) # Equal share
|
|
74
|
+
# Layout::Constraint.fill(2) # Double share
|
|
75
|
+
#
|
|
76
|
+
# Fill constraints distribute any space left after satisfying strict rules.
|
|
77
|
+
# They behave like flex-grow. A fill(2) takes twice as much space as a fill(1).
|
|
78
|
+
#
|
|
79
|
+
# [v] Proportional weight (Integer, default: 1).
|
|
80
|
+
def self.fill(v = 1)
|
|
81
|
+
new(type: :fill, value: Integer(v))
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Requests a specific ratio of the total space.
|
|
85
|
+
#
|
|
86
|
+
# Layout::Constraint.ratio(1, 3) # 1/3rd of the area
|
|
87
|
+
#
|
|
88
|
+
# [numerator] Top part of fraction (Integer).
|
|
89
|
+
# [denominator] Bottom part of fraction (Integer).
|
|
90
|
+
def self.ratio(numerator, denominator)
|
|
91
|
+
new(type: :ratio, value: [Integer(numerator), Integer(denominator)])
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module Layout
|
|
8
|
+
# Divides an area into smaller chunks.
|
|
9
|
+
#
|
|
10
|
+
# Terminal screens vary in size. Hardcoded positions break when the window resizes. You need a way to organize space dynamically.
|
|
11
|
+
#
|
|
12
|
+
# This class manages geometry. It splits a given area into multiple sections based on a list of constraints.
|
|
13
|
+
#
|
|
14
|
+
# Use layouts to build responsive grids. Stack sections vertically for a sidebar-main structure. Partition them horizontally for headers and footers. Let the layout engine do the math.
|
|
15
|
+
#
|
|
16
|
+
# {rdoc-image:/doc/images/widget_layout_split.png}[link:/examples/widget_layout_split/app_rb.html]
|
|
17
|
+
#
|
|
18
|
+
# === Example
|
|
19
|
+
#
|
|
20
|
+
# Run the interactive demo from the terminal:
|
|
21
|
+
#
|
|
22
|
+
# ruby examples/widget_layout_split/app.rb
|
|
23
|
+
class Layout < Data.define(:direction, :constraints, :children, :flex)
|
|
24
|
+
##
|
|
25
|
+
# :attr_reader: direction
|
|
26
|
+
# Direction of the split.
|
|
27
|
+
#
|
|
28
|
+
# Either <tt>:vertical</tt> (top to bottom) or <tt>:horizontal</tt> (left to right).
|
|
29
|
+
#
|
|
30
|
+
# layout.direction # => :vertical
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# :attr_reader: constraints
|
|
34
|
+
# Array of rules defining section sizes.
|
|
35
|
+
#
|
|
36
|
+
# See RatatuiRuby::Layout::Constraint.
|
|
37
|
+
|
|
38
|
+
##
|
|
39
|
+
# :attr_reader: children
|
|
40
|
+
# Widgets to render in each section (optional).
|
|
41
|
+
#
|
|
42
|
+
# If provided, `children[i]` is rendered into the area defined by `constraints[i]`.
|
|
43
|
+
|
|
44
|
+
##
|
|
45
|
+
# :attr_reader: flex
|
|
46
|
+
# Strategy for distributing extra space.
|
|
47
|
+
#
|
|
48
|
+
# One of <tt>:legacy</tt>, <tt>:start</tt>, <tt>:center</tt>, <tt>:end</tt>, <tt>:space_between</tt>, <tt>:space_around</tt>.
|
|
49
|
+
|
|
50
|
+
# :nodoc:
|
|
51
|
+
FLEX_MODES = %i[legacy start center end space_between space_around space_evenly].freeze
|
|
52
|
+
|
|
53
|
+
# Creates a new Layout.
|
|
54
|
+
#
|
|
55
|
+
# [direction]
|
|
56
|
+
# <tt>:vertical</tt> or <tt>:horizontal</tt> (default: <tt>:vertical</tt>).
|
|
57
|
+
# [constraints]
|
|
58
|
+
# list of Constraint objects.
|
|
59
|
+
# [children]
|
|
60
|
+
# List of widgets to render (optional).
|
|
61
|
+
# [flex]
|
|
62
|
+
# Flex mode for spacing (default: <tt>:legacy</tt>).
|
|
63
|
+
def initialize(direction: :vertical, constraints: [], children: [], flex: :legacy)
|
|
64
|
+
super
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Splits an area into multiple rectangles.
|
|
68
|
+
#
|
|
69
|
+
# This is a pure calculation helper for hit testing. It computes where
|
|
70
|
+
# widgets *would* be placed without actually rendering them.
|
|
71
|
+
#
|
|
72
|
+
# rects = Layout::Layout.split(
|
|
73
|
+
# area,
|
|
74
|
+
# direction: :horizontal,
|
|
75
|
+
# constraints: [Layout::Constraint.percentage(50), Layout::Constraint.percentage(50)]
|
|
76
|
+
# )
|
|
77
|
+
# left, right = rects
|
|
78
|
+
#
|
|
79
|
+
# [area]
|
|
80
|
+
# The area to split. Can be a <tt>Rect</tt> or a <tt>Hash</tt> containing <tt>:x</tt>, <tt>:y</tt>, <tt>:width</tt>, and <tt>:height</tt>.
|
|
81
|
+
# [direction]
|
|
82
|
+
# <tt>:vertical</tt> or <tt>:horizontal</tt> (default: <tt>:vertical</tt>).
|
|
83
|
+
# [constraints]
|
|
84
|
+
# Array of <tt>Constraint</tt> objects defining section sizes.
|
|
85
|
+
# [flex]
|
|
86
|
+
# Flex mode for spacing (default: <tt>:legacy</tt>).
|
|
87
|
+
#
|
|
88
|
+
# Returns an Array of <tt>Rect</tt> objects.
|
|
89
|
+
def self.split(area, direction: :vertical, constraints:, flex: :legacy)
|
|
90
|
+
# Duck-typing: If it lacks geometry methods but can be a Hash, convert it.
|
|
91
|
+
if !area.respond_to?(:x) && area.respond_to?(:to_h)
|
|
92
|
+
# Assume it's a Hash-like object with :x, :y, etc.
|
|
93
|
+
hash = area.to_h
|
|
94
|
+
area = Rect.new(
|
|
95
|
+
x: hash.fetch(:x, 0),
|
|
96
|
+
y: hash.fetch(:y, 0),
|
|
97
|
+
width: hash.fetch(:width, 0),
|
|
98
|
+
height: hash.fetch(:height, 0)
|
|
99
|
+
)
|
|
100
|
+
end
|
|
101
|
+
raw_rects = _split(area, direction, constraints, flex)
|
|
102
|
+
raw_rects.map { |r| Rect.new(x: r[:x], y: r[:y], width: r[:width], height: r[:height]) }
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
module Layout
|
|
8
|
+
# Defines a rectangular area in the terminal grid.
|
|
9
|
+
#
|
|
10
|
+
# Geometry management involves passing groups of four integers (`x, y, width, height`) repeatedly.
|
|
11
|
+
# This is verbose and prone to parameter mismatch errors.
|
|
12
|
+
#
|
|
13
|
+
# This class encapsulates the geometry. It provides a standard primitive for passing area definitions
|
|
14
|
+
# between layout engines and rendering functions.
|
|
15
|
+
#
|
|
16
|
+
# Use it when manual positioning is required or when querying layout results.
|
|
17
|
+
#
|
|
18
|
+
# === Examples
|
|
19
|
+
#
|
|
20
|
+
# area = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
|
|
21
|
+
# puts area.width # => 80
|
|
22
|
+
class Rect < Data.define(:x, :y, :width, :height)
|
|
23
|
+
##
|
|
24
|
+
# :attr_reader: x
|
|
25
|
+
# X coordinate (column) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
|
|
26
|
+
|
|
27
|
+
##
|
|
28
|
+
# :attr_reader: y
|
|
29
|
+
# Y coordinate (row) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
|
|
30
|
+
|
|
31
|
+
##
|
|
32
|
+
# :attr_reader: width
|
|
33
|
+
# Width in characters (Integer, coerced via +to_int+ or +to_i+).
|
|
34
|
+
|
|
35
|
+
##
|
|
36
|
+
# :attr_reader: height
|
|
37
|
+
# Height in characters (Integer, coerced via +to_int+ or +to_i+).
|
|
38
|
+
|
|
39
|
+
# Creates a new Rect.
|
|
40
|
+
#
|
|
41
|
+
# All parameters accept any object responding to +to_int+ or +to_i+ (duck-typed).
|
|
42
|
+
#
|
|
43
|
+
# [x] Column index (Numeric).
|
|
44
|
+
# [y] Row index (Numeric).
|
|
45
|
+
# [width] Width in columns (Numeric).
|
|
46
|
+
# [height] Height in rows (Numeric).
|
|
47
|
+
def initialize(x: 0, y: 0, width: 0, height: 0)
|
|
48
|
+
super(
|
|
49
|
+
x: Integer(x),
|
|
50
|
+
y: Integer(y),
|
|
51
|
+
width: Integer(width),
|
|
52
|
+
height: Integer(height)
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
# Tests whether a point is inside this rectangle.
|
|
57
|
+
#
|
|
58
|
+
# Essential for hit testing mouse clicks against layout regions.
|
|
59
|
+
#
|
|
60
|
+
# area = Layout::Rect.new(x: 10, y: 5, width: 20, height: 10)
|
|
61
|
+
# area.contains?(15, 8) # => true
|
|
62
|
+
# area.contains?(5, 8) # => false
|
|
63
|
+
#
|
|
64
|
+
# [px]
|
|
65
|
+
# X coordinate to test (column).
|
|
66
|
+
# [py]
|
|
67
|
+
# Y coordinate to test (row).
|
|
68
|
+
#
|
|
69
|
+
# Returns true if the point (px, py) is within the rectangle bounds.
|
|
70
|
+
def contains?(px, py)
|
|
71
|
+
px >= x && px < x + width && py >= y && py < y + height
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Tests whether this rectangle overlaps with another.
|
|
75
|
+
#
|
|
76
|
+
# Essential for determining if a widget is visible within a viewport or clipping area.
|
|
77
|
+
#
|
|
78
|
+
# viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
|
|
79
|
+
# widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
|
|
80
|
+
# viewport.intersects?(widget) # => true (partial overlap)
|
|
81
|
+
#
|
|
82
|
+
# [other]
|
|
83
|
+
# Another Rect to test against.
|
|
84
|
+
#
|
|
85
|
+
# Returns true if the rectangles overlap.
|
|
86
|
+
def intersects?(other)
|
|
87
|
+
x < other.x + other.width &&
|
|
88
|
+
x + width > other.x &&
|
|
89
|
+
y < other.y + other.height &&
|
|
90
|
+
y + height > other.y
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Returns the overlapping area between this rectangle and another.
|
|
94
|
+
#
|
|
95
|
+
# Essential for calculating visible portions of widgets inside scroll views.
|
|
96
|
+
#
|
|
97
|
+
# viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
|
|
98
|
+
# widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
|
|
99
|
+
# visible = viewport.intersection(widget)
|
|
100
|
+
# # => Rect(x: 70, y: 20, width: 10, height: 4)
|
|
101
|
+
#
|
|
102
|
+
# [other]
|
|
103
|
+
# Another Rect to intersect with.
|
|
104
|
+
#
|
|
105
|
+
# Returns a new Rect representing the intersection, or +nil+ if no overlap.
|
|
106
|
+
def intersection(other)
|
|
107
|
+
return nil unless intersects?(other)
|
|
108
|
+
|
|
109
|
+
new_x = [x, other.x].max
|
|
110
|
+
new_y = [y, other.y].max
|
|
111
|
+
new_right = [x + width, other.x + other.width].min
|
|
112
|
+
new_bottom = [y + height, other.y + other.height].min
|
|
113
|
+
|
|
114
|
+
Rect.new(x: new_x, y: new_y, width: new_right - new_x, height: new_bottom - new_y)
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
# Layout primitives for geometry and space distribution.
|
|
8
|
+
#
|
|
9
|
+
# This module mirrors +ratatui::layout+ and contains:
|
|
10
|
+
# - {Rect} — Rectangle geometry
|
|
11
|
+
# - {Constraint} — Sizing rules
|
|
12
|
+
# - {Layout} — Space distribution
|
|
13
|
+
module Layout
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
require_relative "layout/rect"
|
|
18
|
+
require_relative "layout/constraint"
|
|
19
|
+
require_relative "layout/layout"
|
|
@@ -18,7 +18,7 @@ module RatatuiRuby
|
|
|
18
18
|
# == Thread/Ractor Safety
|
|
19
19
|
#
|
|
20
20
|
# ListState is *not* Ractor-shareable. It contains mutable internal state.
|
|
21
|
-
# Store it in instance variables, not in immutable
|
|
21
|
+
# Store it in instance variables, not in immutable Models.
|
|
22
22
|
#
|
|
23
23
|
# == Example
|
|
24
24
|
#
|
|
@@ -26,7 +26,7 @@ module RatatuiRuby
|
|
|
26
26
|
# @list_state.select(2) # Select third item
|
|
27
27
|
#
|
|
28
28
|
# RatatuiRuby.draw do |frame|
|
|
29
|
-
# list = RatatuiRuby::List.new(items: ["A", "B", "C", "D", "E"])
|
|
29
|
+
# list = RatatuiRuby::Widgets::List.new(items: ["A", "B", "C", "D", "E"])
|
|
30
30
|
# frame.render_stateful_widget(list, frame.area, @list_state)
|
|
31
31
|
# end
|
|
32
32
|
#
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
4
|
+
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
|
+
|
|
6
|
+
module RatatuiRuby
|
|
7
|
+
# A styled table row combining cells with optional row-level styling.
|
|
8
|
+
#
|
|
9
|
+
# By default, Table rows are arrays of cell content. For more control over styling
|
|
10
|
+
# individual rows, wrap the cells in a Row object to apply row-level style.
|
|
11
|
+
#
|
|
12
|
+
# The cells can be Strings, Text::Spans, Text::Lines, Paragraphs, or Cells.
|
|
13
|
+
# The style applies to the entire row background.
|
|
14
|
+
#
|
|
15
|
+
# === Examples
|
|
16
|
+
#
|
|
17
|
+
# # Row with red background
|
|
18
|
+
# Row.new(cells: ["Error", "Something went wrong"], style: Style.new(bg: :red))
|
|
19
|
+
#
|
|
20
|
+
# # Row with styled cells and custom height
|
|
21
|
+
# Row.new(
|
|
22
|
+
# cells: [
|
|
23
|
+
# Text::Span.new(content: "Status", style: Style.new(modifiers: [:bold])),
|
|
24
|
+
# Text::Span.new(content: "OK", style: Style.new(fg: :green))
|
|
25
|
+
# ],
|
|
26
|
+
# height: 2
|
|
27
|
+
# )
|
|
28
|
+
class Row < Data.define(:cells, :style, :height, :top_margin, :bottom_margin)
|
|
29
|
+
##
|
|
30
|
+
# :attr_reader: cells
|
|
31
|
+
# The cells to display (Array of Strings, Text::Spans, Text::Lines, Paragraphs, or Cells).
|
|
32
|
+
|
|
33
|
+
##
|
|
34
|
+
# :attr_reader: style
|
|
35
|
+
# The style to apply to the row (optional Style).
|
|
36
|
+
|
|
37
|
+
##
|
|
38
|
+
# :attr_reader: height
|
|
39
|
+
# Fixed row height in lines (optional Integer).
|
|
40
|
+
|
|
41
|
+
##
|
|
42
|
+
# :attr_reader: top_margin
|
|
43
|
+
# Margin above the row in lines (optional Integer).
|
|
44
|
+
|
|
45
|
+
##
|
|
46
|
+
# :attr_reader: bottom_margin
|
|
47
|
+
# Margin below the row in lines (optional Integer).
|
|
48
|
+
|
|
49
|
+
# Creates a new Row.
|
|
50
|
+
#
|
|
51
|
+
# [cells] Array of Strings, Text::Spans, Text::Lines, Paragraphs, or Cells.
|
|
52
|
+
# [style] Style object (optional).
|
|
53
|
+
# [height] Integer for fixed height (optional).
|
|
54
|
+
# [top_margin] Integer for top margin (optional).
|
|
55
|
+
# [bottom_margin] Integer for bottom margin (optional).
|
|
56
|
+
def initialize(cells:, style: nil, height: nil, top_margin: nil, bottom_margin: nil)
|
|
57
|
+
super(
|
|
58
|
+
cells:,
|
|
59
|
+
style:,
|
|
60
|
+
height: height.nil? ? nil : Integer(height),
|
|
61
|
+
top_margin: top_margin.nil? ? nil : Integer(top_margin),
|
|
62
|
+
bottom_margin: bottom_margin.nil? ? nil : Integer(bottom_margin)
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|