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.
- 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 +48 -0
- data/README.md +26 -1
- data/doc/application_architecture.md +16 -16
- data/doc/application_testing.md +1 -1
- data/doc/async.md +160 -0
- 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/debugging.md +71 -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/style.rs +25 -9
- 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/bump/changelog.rb +8 -0
- data/tasks/bump/ruby_gem.rb +12 -0
- data/tasks/bump/unreleased_section.rb +16 -0
- data/tasks/sourcehut.rake +4 -1
- metadata +64 -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
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use crate::style::{parse_block, parse_style};
|
|
5
|
+
use crate::text::{parse_line, parse_span};
|
|
5
6
|
use crate::widgets::table_state::RubyTableState;
|
|
6
7
|
use bumpalo::Bump;
|
|
7
8
|
use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
|
|
@@ -22,7 +23,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
22
23
|
let widths_val: Value = node.funcall("widths", ())?;
|
|
23
24
|
let widths_array = magnus::RArray::from_value(widths_val)
|
|
24
25
|
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
|
|
25
|
-
let
|
|
26
|
+
let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
|
|
26
27
|
let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
|
|
27
28
|
let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
|
|
28
29
|
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
@@ -73,8 +74,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
73
74
|
table = table.block(parse_block(block_val, &bump)?);
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
if !
|
|
77
|
-
table = table.row_highlight_style(parse_style(
|
|
77
|
+
if !row_highlight_style_val.is_nil() {
|
|
78
|
+
table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
|
|
78
79
|
}
|
|
79
80
|
|
|
80
81
|
if !column_highlight_style_val.is_nil() {
|
|
@@ -158,7 +159,7 @@ pub fn render_stateful(
|
|
|
158
159
|
// Build table (ignoring selected_row, selected_column, offset — State is truth)
|
|
159
160
|
let header_val: Value = node.funcall("header", ())?;
|
|
160
161
|
let footer_val: Value = node.funcall("footer", ())?;
|
|
161
|
-
let
|
|
162
|
+
let row_highlight_style_val: Value = node.funcall("row_highlight_style", ())?;
|
|
162
163
|
let column_highlight_style_val: Value = node.funcall("column_highlight_style", ())?;
|
|
163
164
|
let cell_highlight_style_val: Value = node.funcall("cell_highlight_style", ())?;
|
|
164
165
|
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
@@ -196,8 +197,8 @@ pub fn render_stateful(
|
|
|
196
197
|
if !block_val.is_nil() {
|
|
197
198
|
table = table.block(parse_block(block_val, &bump)?);
|
|
198
199
|
}
|
|
199
|
-
if !
|
|
200
|
-
table = table.row_highlight_style(parse_style(
|
|
200
|
+
if !row_highlight_style_val.is_nil() {
|
|
201
|
+
table = table.row_highlight_style(parse_style(row_highlight_style_val)?);
|
|
201
202
|
}
|
|
202
203
|
if !column_highlight_style_val.is_nil() {
|
|
203
204
|
table = table.column_highlight_style(parse_style(column_highlight_style_val)?);
|
|
@@ -228,6 +229,53 @@ pub fn render_stateful(
|
|
|
228
229
|
|
|
229
230
|
fn parse_row(row_val: Value) -> Result<Row<'static>, Error> {
|
|
230
231
|
let ruby = magnus::Ruby::get().unwrap();
|
|
232
|
+
|
|
233
|
+
// Check if this is a RatatuiRuby::Row object with cells + style + height + margins
|
|
234
|
+
let class = row_val.class();
|
|
235
|
+
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
236
|
+
let class_name = unsafe { class.name() }.into_owned();
|
|
237
|
+
|
|
238
|
+
if class_name == "RatatuiRuby::Widgets::Row" {
|
|
239
|
+
let cells_val: Value = row_val.funcall("cells", ())?;
|
|
240
|
+
let style_val: Value = row_val.funcall("style", ())?;
|
|
241
|
+
let height_val: Value = row_val.funcall("height", ())?;
|
|
242
|
+
let top_margin_val: Value = row_val.funcall("top_margin", ())?;
|
|
243
|
+
let bottom_margin_val: Value = row_val.funcall("bottom_margin", ())?;
|
|
244
|
+
|
|
245
|
+
let cells_array = magnus::RArray::from_value(cells_val).ok_or_else(|| {
|
|
246
|
+
Error::new(ruby.exception_type_error(), "expected array for Row.cells")
|
|
247
|
+
})?;
|
|
248
|
+
|
|
249
|
+
let mut cells = Vec::new();
|
|
250
|
+
for i in 0..cells_array.len() {
|
|
251
|
+
let index = isize::try_from(i)
|
|
252
|
+
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
253
|
+
let entry_val: Value = cells_array.entry(index)?;
|
|
254
|
+
cells.push(parse_cell(entry_val)?);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let mut row = Row::new(cells);
|
|
258
|
+
|
|
259
|
+
if !style_val.is_nil() {
|
|
260
|
+
row = row.style(parse_style(style_val)?);
|
|
261
|
+
}
|
|
262
|
+
if !height_val.is_nil() {
|
|
263
|
+
let h: u16 = height_val.funcall("to_int", ())?;
|
|
264
|
+
row = row.height(h);
|
|
265
|
+
}
|
|
266
|
+
if !top_margin_val.is_nil() {
|
|
267
|
+
let m: u16 = top_margin_val.funcall("to_int", ())?;
|
|
268
|
+
row = row.top_margin(m);
|
|
269
|
+
}
|
|
270
|
+
if !bottom_margin_val.is_nil() {
|
|
271
|
+
let m: u16 = bottom_margin_val.funcall("to_int", ())?;
|
|
272
|
+
row = row.bottom_margin(m);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
return Ok(row);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Fallback: plain array of cells
|
|
231
279
|
let row_array = magnus::RArray::from_value(row_val)
|
|
232
280
|
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
|
|
233
281
|
|
|
@@ -246,42 +294,39 @@ fn parse_cell(cell_val: Value) -> Result<Cell<'static>, Error> {
|
|
|
246
294
|
// SAFETY: Immediate conversion to owned string avoids GC-unsafe borrowed reference.
|
|
247
295
|
let class_name = unsafe { class.name() }.into_owned();
|
|
248
296
|
|
|
249
|
-
|
|
297
|
+
// Try Text::Line first (contains multiple spans)
|
|
298
|
+
if class_name.contains("Line") {
|
|
299
|
+
if let Ok(line) = parse_line(cell_val) {
|
|
300
|
+
return Ok(Cell::from(line));
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Try Text::Span
|
|
305
|
+
if class_name.contains("Span") {
|
|
306
|
+
if let Ok(span) = parse_span(cell_val) {
|
|
307
|
+
return Ok(Cell::from(ratatui::text::Line::from(vec![span])));
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if class_name == "RatatuiRuby::Widgets::Paragraph" {
|
|
250
312
|
let text: String = cell_val.funcall("text", ())?;
|
|
251
313
|
let style_val: Value = cell_val.funcall("style", ())?;
|
|
252
314
|
let cell_style = parse_style(style_val)?;
|
|
253
315
|
Ok(Cell::from(text).style(cell_style))
|
|
254
|
-
} else if class_name == "RatatuiRuby::Style" {
|
|
316
|
+
} else if class_name == "RatatuiRuby::Style::Style" {
|
|
255
317
|
Ok(Cell::from("").style(parse_style(cell_val)?))
|
|
256
|
-
} else if class_name == "RatatuiRuby::Cell" {
|
|
257
|
-
|
|
258
|
-
let
|
|
259
|
-
let
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
let mut
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
if !bg_val.is_nil() {
|
|
269
|
-
if let Some(color) = crate::style::parse_color_value(bg_val)? {
|
|
270
|
-
style = style.bg(color);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
if let Some(mods_array) = magnus::RArray::from_value(modifiers_val) {
|
|
274
|
-
let ruby = magnus::Ruby::get().unwrap();
|
|
275
|
-
for i in 0..mods_array.len() {
|
|
276
|
-
let index = isize::try_from(i)
|
|
277
|
-
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
278
|
-
let mod_str: String = mods_array.entry::<String>(index)?;
|
|
279
|
-
if let Some(modifier) = crate::style::parse_modifier_str(&mod_str) {
|
|
280
|
-
style = style.add_modifier(modifier);
|
|
281
|
-
}
|
|
282
|
-
}
|
|
318
|
+
} else if class_name == "RatatuiRuby::Widgets::Cell" {
|
|
319
|
+
// Widgets::Cell has content (String/Span/Line) and optional style
|
|
320
|
+
let content_val: Value = cell_val.funcall("content", ())?;
|
|
321
|
+
let style_val: Value = cell_val.funcall("style", ())?;
|
|
322
|
+
|
|
323
|
+
// Recursively parse the content (could be String, Span, or Line)
|
|
324
|
+
let mut cell = parse_cell(content_val)?;
|
|
325
|
+
|
|
326
|
+
if !style_val.is_nil() {
|
|
327
|
+
cell = cell.style(parse_style(style_val)?);
|
|
283
328
|
}
|
|
284
|
-
Ok(
|
|
329
|
+
Ok(cell)
|
|
285
330
|
} else {
|
|
286
331
|
let cell_str: String = cell_val.funcall("to_s", ())?;
|
|
287
332
|
Ok(Cell::from(cell_str))
|
|
@@ -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
|