ratatui_ruby 0.2.0 → 0.3.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 +2 -2
- data/.builds/ruby-3.3.yml +2 -2
- data/.builds/ruby-3.4.yml +2 -2
- data/.builds/{ruby-4.0.0-preview3.yml → ruby-4.0.0.yml} +8 -9
- data/.pre-commit-config.yaml +9 -2
- data/AGENTS.md +59 -4
- data/CHANGELOG.md +58 -1
- data/README.md +6 -6
- data/REUSE.toml +1 -6
- data/{docs → doc}/contributors/index.md +2 -1
- data/doc/custom.css +8 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/{docs → doc}/index.md +1 -1
- data/{docs → doc}/quickstart.md +24 -0
- data/examples/custom_widget.rb +43 -0
- data/examples/popup_demo.rb +105 -0
- data/examples/scroll_text.rb +74 -0
- data/examples/table_select.rb +70 -0
- data/examples/test_popup_demo.rb +62 -0
- data/examples/test_scroll_text.rb +130 -0
- data/examples/test_table_select.rb +37 -0
- data/ext/ratatui_ruby/Cargo.lock +167 -50
- data/ext/ratatui_ruby/Cargo.toml +4 -4
- data/ext/ratatui_ruby/src/buffer.rs +54 -0
- data/ext/ratatui_ruby/src/events.rs +111 -106
- data/ext/ratatui_ruby/src/lib.rs +15 -6
- data/ext/ratatui_ruby/src/rendering.rs +15 -0
- data/ext/ratatui_ruby/src/style.rs +2 -1
- data/ext/ratatui_ruby/src/terminal.rs +24 -19
- data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
- data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
- data/ext/ratatui_ruby/src/widgets/chart.rs +11 -4
- data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -4
- data/ext/ratatui_ruby/src/widgets/mod.rs +1 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
- data/lib/ratatui_ruby/dsl.rb +2 -0
- data/lib/ratatui_ruby/schema/clear.rb +83 -0
- data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
- data/lib/ratatui_ruby/schema/rect.rb +24 -0
- data/lib/ratatui_ruby/schema/table.rb +8 -2
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +3 -1
- data/mise.toml +1 -1
- data/sig/ratatui_ruby/buffer.rbs +11 -0
- data/sig/ratatui_ruby/schema/rect.rbs +14 -0
- data/tasks/bump/changelog.rb +37 -0
- data/tasks/bump/comparison_links.rb +41 -0
- data/tasks/bump/header.rb +30 -0
- data/tasks/bump/history.rb +30 -0
- data/tasks/bump/manifest.rb +8 -0
- data/tasks/bump/ruby_gem.rb +6 -10
- data/tasks/bump/sem_ver.rb +6 -0
- data/tasks/bump/unreleased_section.rb +38 -0
- data/tasks/bump.rake +5 -1
- data/tasks/doc.rake +5 -4
- data/tasks/resources/build.yml.erb +1 -14
- data/tasks/resources/rubies.yml +1 -1
- data/tasks/sourcehut.rake +11 -2
- data/tasks/website/version.rb +1 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +2 -1
- data/tasks/website/website.rb +4 -1
- data/tasks/website.rake +3 -3
- metadata +76 -26
- data/CODE_OF_CONDUCT.md +0 -30
- data/CONTRIBUTING.md +0 -40
- /data/{docs → doc}/application_testing.md +0 -0
- /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
- /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
- /data/{docs → doc}/contributors/design.md +0 -0
- /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
- /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-calendar_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-chart_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
- /data/{docs → doc}/images/examples-list_styles.rb.png +0 -0
- /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
- /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
- /data/{docs → doc}/images/examples-quickstart_lifecycle.rb.png +0 -0
- /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-stock_ticker.rb.png +0 -0
- /data/{docs → doc}/images/examples-system_monitor.rb.png +0 -0
|
@@ -5,7 +5,6 @@ use crate::rendering::render_node;
|
|
|
5
5
|
use magnus::{prelude::*, Error, Value};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
layout::{Constraint, Direction, Layout, Rect},
|
|
8
|
-
widgets::Clear,
|
|
9
8
|
Frame,
|
|
10
9
|
};
|
|
11
10
|
|
|
@@ -36,7 +35,6 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
36
35
|
|
|
37
36
|
let center_area = popup_layout_horizontal[1];
|
|
38
37
|
|
|
39
|
-
frame.render_widget(Clear, center_area);
|
|
40
38
|
render_node(frame, center_area, child)?;
|
|
41
39
|
Ok(())
|
|
42
40
|
}
|
|
@@ -12,6 +12,7 @@ use ratatui::{
|
|
|
12
12
|
};
|
|
13
13
|
|
|
14
14
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
15
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
15
16
|
let class = node.class();
|
|
16
17
|
let class_name = unsafe { class.name() };
|
|
17
18
|
|
|
@@ -37,7 +38,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
37
38
|
for j in 0..data_array.len() {
|
|
38
39
|
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
39
40
|
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
40
|
-
Error::new(
|
|
41
|
+
Error::new(ruby.exception_type_error(), "expected array for point")
|
|
41
42
|
})?;
|
|
42
43
|
let x: f64 = point_array.entry(0)?;
|
|
43
44
|
let y: f64 = point_array.entry(1)?;
|
|
@@ -125,6 +126,7 @@ fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
|
|
|
125
126
|
}
|
|
126
127
|
|
|
127
128
|
fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
129
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
128
130
|
let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
|
|
129
131
|
let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
|
|
130
132
|
let y_labels_val: magnus::RArray = node.funcall("y_labels", ())?;
|
|
@@ -142,7 +144,7 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
|
|
|
142
144
|
for j in 0..data_array.len() {
|
|
143
145
|
let point_array_val: Value = data_array.entry(j as isize)?;
|
|
144
146
|
let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
|
|
145
|
-
Error::new(
|
|
147
|
+
Error::new(ruby.exception_type_error(), "expected array for point")
|
|
146
148
|
})?;
|
|
147
149
|
let x: f64 = point_array.entry(0)?;
|
|
148
150
|
let y: f64 = point_array.entry(1)?;
|
|
@@ -181,6 +183,11 @@ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), E
|
|
|
181
183
|
let label: String = y_labels_val.entry(i as isize)?;
|
|
182
184
|
y_labels.push(Span::from(label));
|
|
183
185
|
}
|
|
186
|
+
// Ratatui 0.29+ requires labels to be present for the axis line to render
|
|
187
|
+
if y_labels.is_empty() {
|
|
188
|
+
y_labels.push(Span::from(""));
|
|
189
|
+
y_labels.push(Span::from(""));
|
|
190
|
+
}
|
|
184
191
|
|
|
185
192
|
let y_bounds: [f64; 2] = [y_bounds_val.entry(0)?, y_bounds_val.entry(1)?];
|
|
186
193
|
|
|
@@ -233,12 +240,12 @@ mod tests {
|
|
|
233
240
|
.x_axis(
|
|
234
241
|
Axis::default()
|
|
235
242
|
.bounds([0.0, 1.0])
|
|
236
|
-
.labels(vec!["XMIN".into(), "XMAX".into()]),
|
|
243
|
+
.labels(vec!["XMIN".into(), "XMAX".into()] as Vec<ratatui::text::Line>),
|
|
237
244
|
)
|
|
238
245
|
.y_axis(
|
|
239
246
|
Axis::default()
|
|
240
247
|
.bounds([0.0, 1.0])
|
|
241
|
-
.labels(vec!["YMIN".into(), "YMAX".into()]),
|
|
248
|
+
.labels(vec!["YMIN".into(), "YMAX".into()] as Vec<ratatui::text::Line>),
|
|
242
249
|
);
|
|
243
250
|
let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
|
|
244
251
|
chart.render(Rect::new(0, 0, 40, 20), &mut buf);
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use magnus::{prelude::*, Error, Value};
|
|
5
|
+
use ratatui::{layout::Rect, widgets::Widget, Frame};
|
|
6
|
+
|
|
7
|
+
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
8
|
+
frame.render_widget(ratatui::widgets::Clear, area);
|
|
9
|
+
|
|
10
|
+
// If a block is provided, render it on top of the cleared area
|
|
11
|
+
if let Ok(block_val) = node.funcall::<_, _, Value>("block", ()) {
|
|
12
|
+
if !block_val.is_nil() {
|
|
13
|
+
let block = crate::style::parse_block(block_val)?;
|
|
14
|
+
block.render(area, frame.buffer_mut());
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
Ok(())
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
#[cfg(test)]
|
|
22
|
+
mod tests {
|
|
23
|
+
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
|
|
24
|
+
|
|
25
|
+
#[test]
|
|
26
|
+
fn test_clear_renders_without_error() {
|
|
27
|
+
let backend = TestBackend::new(10, 5);
|
|
28
|
+
let mut terminal = Terminal::new(backend).unwrap();
|
|
29
|
+
|
|
30
|
+
terminal
|
|
31
|
+
.draw(|frame| {
|
|
32
|
+
let area = Rect::new(0, 0, 10, 5);
|
|
33
|
+
frame.render_widget(ratatui::widgets::Clear, area);
|
|
34
|
+
})
|
|
35
|
+
.unwrap();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -7,7 +7,7 @@ use ratatui::{layout::Rect, Frame};
|
|
|
7
7
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
8
8
|
let x: u16 = node.funcall("x", ())?;
|
|
9
9
|
let y: u16 = node.funcall("y", ())?;
|
|
10
|
-
frame.
|
|
10
|
+
frame.set_cursor_position((area.x + x, area.y + y));
|
|
11
11
|
Ok(())
|
|
12
12
|
}
|
|
13
13
|
|
|
@@ -9,10 +9,11 @@ use ratatui::{
|
|
|
9
9
|
};
|
|
10
10
|
|
|
11
11
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
12
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
12
13
|
let direction_sym: Symbol = node.funcall("direction", ())?;
|
|
13
14
|
let children_val: Value = node.funcall("children", ())?;
|
|
14
15
|
let children_array = magnus::RArray::from_value(children_val)
|
|
15
|
-
.ok_or_else(|| Error::new(
|
|
16
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
16
17
|
|
|
17
18
|
let constraints_val: Value = node.funcall("constraints", ())?;
|
|
18
19
|
let constraints_array = magnus::RArray::from_value(constraints_val);
|
|
@@ -10,9 +10,10 @@ use ratatui::{
|
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
13
14
|
let items_val: Value = node.funcall("items", ())?;
|
|
14
15
|
let items_array = magnus::RArray::from_value(items_val)
|
|
15
|
-
.ok_or_else(|| Error::new(
|
|
16
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
|
|
16
17
|
let selected_index_val: Value = node.funcall("selected_index", ())?;
|
|
17
18
|
let style_val: Value = node.funcall("style", ())?;
|
|
18
19
|
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
@@ -89,10 +90,11 @@ mod tests {
|
|
|
89
90
|
assert!(content.contains(">> Item 2"));
|
|
90
91
|
|
|
91
92
|
// Check colors
|
|
92
|
-
assert_eq!(buf.
|
|
93
|
-
assert_eq!(buf.
|
|
93
|
+
assert_eq!(buf.cell((0, 0)).unwrap().fg, ratatui::style::Color::White);
|
|
94
|
+
assert_eq!(buf.cell((0, 1)).unwrap().fg, ratatui::style::Color::Yellow);
|
|
94
95
|
assert!(buf
|
|
95
|
-
.
|
|
96
|
+
.cell((0, 1))
|
|
97
|
+
.unwrap()
|
|
96
98
|
.modifier
|
|
97
99
|
.contains(ratatui::style::Modifier::BOLD));
|
|
98
100
|
}
|
|
@@ -6,9 +6,10 @@ use magnus::{prelude::*, Error, Value};
|
|
|
6
6
|
use ratatui::{layout::Rect, Frame};
|
|
7
7
|
|
|
8
8
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
9
10
|
let layers_val: Value = node.funcall("layers", ())?;
|
|
10
11
|
let layers_array = magnus::RArray::from_value(layers_val)
|
|
11
|
-
.ok_or_else(|| Error::new(
|
|
12
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for layers"))?;
|
|
12
13
|
|
|
13
14
|
for i in 0..layers_array.len() {
|
|
14
15
|
let layer: Value = layers_array.entry(i as isize)?;
|
|
@@ -15,6 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
15
15
|
let block_val: Value = node.funcall("block", ())?;
|
|
16
16
|
let wrap: bool = node.funcall("wrap", ())?;
|
|
17
17
|
let align_sym: Symbol = node.funcall("align", ())?;
|
|
18
|
+
let scroll_val: Value = node.funcall("scroll", ())?;
|
|
18
19
|
|
|
19
20
|
let style = parse_style(style_val)?;
|
|
20
21
|
let mut paragraph = Paragraph::new(text).style(style);
|
|
@@ -33,6 +34,15 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
33
34
|
_ => {}
|
|
34
35
|
}
|
|
35
36
|
|
|
37
|
+
// Apply scroll offset if provided
|
|
38
|
+
// Ruby passes (y, x) array matching ratatui's convention
|
|
39
|
+
if !scroll_val.is_nil() {
|
|
40
|
+
let scroll_array: Vec<u16> = Vec::<u16>::try_convert(scroll_val)?;
|
|
41
|
+
if scroll_array.len() >= 2 {
|
|
42
|
+
paragraph = paragraph.scroll((scroll_array[0], scroll_array[1]));
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
36
46
|
frame.render_widget(paragraph, area);
|
|
37
47
|
Ok(())
|
|
38
48
|
}
|
|
@@ -5,25 +5,29 @@ use crate::style::{parse_block, parse_style};
|
|
|
5
5
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
6
6
|
use ratatui::{
|
|
7
7
|
layout::{Constraint, Rect},
|
|
8
|
-
widgets::{Cell, Row, Table},
|
|
8
|
+
widgets::{Cell, Row, Table, TableState},
|
|
9
9
|
Frame,
|
|
10
10
|
};
|
|
11
11
|
|
|
12
12
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
13
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
13
14
|
let header_val: Value = node.funcall("header", ())?;
|
|
14
15
|
let rows_val: Value = node.funcall("rows", ())?;
|
|
15
16
|
let rows_array = magnus::RArray::from_value(rows_val)
|
|
16
|
-
.ok_or_else(|| Error::new(
|
|
17
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for rows"))?;
|
|
17
18
|
let widths_val: Value = node.funcall("widths", ())?;
|
|
18
19
|
let widths_array = magnus::RArray::from_value(widths_val)
|
|
19
|
-
.ok_or_else(|| Error::new(
|
|
20
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for widths"))?;
|
|
21
|
+
let highlight_style_val: Value = node.funcall("highlight_style", ())?;
|
|
22
|
+
let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
|
|
23
|
+
let selected_row_val: Value = node.funcall("selected_row", ())?;
|
|
20
24
|
let block_val: Value = node.funcall("block", ())?;
|
|
21
25
|
|
|
22
26
|
let mut rows = Vec::new();
|
|
23
27
|
for i in 0..rows_array.len() {
|
|
24
28
|
let row_val: Value = rows_array.entry(i as isize)?;
|
|
25
29
|
let row_array = magnus::RArray::from_value(row_val)
|
|
26
|
-
.ok_or_else(|| Error::new(
|
|
30
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for row"))?;
|
|
27
31
|
|
|
28
32
|
let mut cells = Vec::new();
|
|
29
33
|
for j in 0..row_array.len() {
|
|
@@ -64,7 +68,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
64
68
|
|
|
65
69
|
if !header_val.is_nil() {
|
|
66
70
|
let header_array = magnus::RArray::from_value(header_val).ok_or_else(|| {
|
|
67
|
-
Error::new(
|
|
71
|
+
Error::new(ruby.exception_type_error(), "expected array for header")
|
|
68
72
|
})?;
|
|
69
73
|
let mut header_cells = Vec::new();
|
|
70
74
|
for i in 0..header_array.len() {
|
|
@@ -89,7 +93,22 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
89
93
|
table = table.block(parse_block(block_val)?);
|
|
90
94
|
}
|
|
91
95
|
|
|
92
|
-
|
|
96
|
+
if !highlight_style_val.is_nil() {
|
|
97
|
+
table = table.row_highlight_style(parse_style(highlight_style_val)?);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if !highlight_symbol_val.is_nil() {
|
|
101
|
+
let symbol: String = highlight_symbol_val.funcall("to_s", ())?;
|
|
102
|
+
table = table.highlight_symbol(symbol);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let mut state = TableState::default();
|
|
106
|
+
if !selected_row_val.is_nil() {
|
|
107
|
+
let index: usize = selected_row_val.funcall("to_int", ())?;
|
|
108
|
+
state.select(Some(index));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
frame.render_stateful_widget(table, area, &mut state);
|
|
93
112
|
Ok(())
|
|
94
113
|
}
|
|
95
114
|
|
|
@@ -6,12 +6,13 @@ use magnus::{prelude::*, Error, Value};
|
|
|
6
6
|
use ratatui::{layout::Rect, text::Line, widgets::Tabs, Frame};
|
|
7
7
|
|
|
8
8
|
pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
9
10
|
let titles_val: Value = node.funcall("titles", ())?;
|
|
10
11
|
let selected_index: usize = node.funcall("selected_index", ())?;
|
|
11
12
|
let block_val: Value = node.funcall("block", ())?;
|
|
12
13
|
|
|
13
14
|
let titles_array = magnus::RArray::from_value(titles_val)
|
|
14
|
-
.ok_or_else(|| Error::new(
|
|
15
|
+
.ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for titles"))?;
|
|
15
16
|
|
|
16
17
|
let mut titles = Vec::new();
|
|
17
18
|
for i in 0..titles_array.len() {
|
data/lib/ratatui_ruby/dsl.rb
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
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 widget that clears (resets) the terminal buffer in the area it is rendered into.
|
|
8
|
+
#
|
|
9
|
+
# The Clear widget is essential for creating opaque popups and modals. Without it,
|
|
10
|
+
# background content or styles (like background colors) will "bleed through"
|
|
11
|
+
# empty spaces or transparent widgets.
|
|
12
|
+
#
|
|
13
|
+
# > [!TIP]
|
|
14
|
+
# > Use `Clear` to prevent "Style Bleed". If a widget rendered behind the popup
|
|
15
|
+
# > has a background color, widgets rendered on top with `Style.default` will
|
|
16
|
+
# > inherit that background color unless you `Clear` the area first.
|
|
17
|
+
#
|
|
18
|
+
# == Usage with Overlay
|
|
19
|
+
#
|
|
20
|
+
# Because RatatuiRuby uses an immediate-mode UI pattern, you must use {Overlay} to
|
|
21
|
+
# layer widgets properly. The typical pattern for creating an opaque popup is:
|
|
22
|
+
#
|
|
23
|
+
# background = Paragraph.new(text: "Background content...")
|
|
24
|
+
# popup = Paragraph.new(
|
|
25
|
+
# text: "Popup content",
|
|
26
|
+
# block: Block.new(title: "Popup", borders: [:all])
|
|
27
|
+
# )
|
|
28
|
+
#
|
|
29
|
+
# # Create an opaque popup by layering: background -> Clear -> popup
|
|
30
|
+
# ui = Overlay.new(
|
|
31
|
+
# layers: [
|
|
32
|
+
# background,
|
|
33
|
+
# Center.new(
|
|
34
|
+
# child: Overlay.new(
|
|
35
|
+
# layers: [
|
|
36
|
+
# Clear.new, # Erases background in this area
|
|
37
|
+
# popup # Draws on top of cleared area
|
|
38
|
+
# ]
|
|
39
|
+
# ),
|
|
40
|
+
# width_percent: 50,
|
|
41
|
+
# height_percent: 40
|
|
42
|
+
# )
|
|
43
|
+
# ]
|
|
44
|
+
# )
|
|
45
|
+
#
|
|
46
|
+
# Without the Clear widget, the background text would be visible through the
|
|
47
|
+
# empty spaces in the popup.
|
|
48
|
+
#
|
|
49
|
+
# == Optional Block Parameter
|
|
50
|
+
#
|
|
51
|
+
# You can optionally provide a {Block} to draw borders around the cleared area:
|
|
52
|
+
#
|
|
53
|
+
# Clear.new(block: Block.new(title: "Cleared Area", borders: [:all]))
|
|
54
|
+
#
|
|
55
|
+
# This is equivalent to:
|
|
56
|
+
#
|
|
57
|
+
# Overlay.new(
|
|
58
|
+
# layers: [
|
|
59
|
+
# Clear.new,
|
|
60
|
+
# Block.new(title: "Cleared Area", borders: [:all])
|
|
61
|
+
# ]
|
|
62
|
+
# )
|
|
63
|
+
#
|
|
64
|
+
# [block] Optional {Block} widget to render on top of the cleared area.
|
|
65
|
+
#
|
|
66
|
+
# @see Overlay
|
|
67
|
+
# @see Center
|
|
68
|
+
# @see Block
|
|
69
|
+
class Clear < Data.define(:block)
|
|
70
|
+
# Creates a new Clear widget.
|
|
71
|
+
#
|
|
72
|
+
# @param block [Block, nil] Optional block widget to render on top of the cleared area.
|
|
73
|
+
#
|
|
74
|
+
# @example Basic usage
|
|
75
|
+
# Clear.new
|
|
76
|
+
#
|
|
77
|
+
# @example With a border
|
|
78
|
+
# Clear.new(block: Block.new(title: "Modal", borders: [:all]))
|
|
79
|
+
def initialize(block: nil)
|
|
80
|
+
super
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -9,7 +9,8 @@ module RatatuiRuby
|
|
|
9
9
|
# [text] the text to display.
|
|
10
10
|
# [style] the style to apply (Style object).
|
|
11
11
|
# [block] an optional Block widget to wrap the paragraph.
|
|
12
|
-
|
|
12
|
+
# [scroll] scroll offset as (y, x) array matching ratatui convention.
|
|
13
|
+
class Paragraph < Data.define(:text, :style, :block, :wrap, :align, :scroll)
|
|
13
14
|
# Creates a new Paragraph.
|
|
14
15
|
#
|
|
15
16
|
# [text] the text to display.
|
|
@@ -17,7 +18,8 @@ module RatatuiRuby
|
|
|
17
18
|
# [block] the block to wrap the paragraph.
|
|
18
19
|
# [wrap] whether to wrap text at width.
|
|
19
20
|
# [align] alignment (:left, :center, :right).
|
|
20
|
-
|
|
21
|
+
# [scroll] scroll offset as (y, x) array (default: [0, 0]).
|
|
22
|
+
def initialize(text:, style: Style.default, block: nil, wrap: false, align: :left, scroll: [0, 0])
|
|
21
23
|
super
|
|
22
24
|
end
|
|
23
25
|
|
|
@@ -29,9 +31,10 @@ module RatatuiRuby
|
|
|
29
31
|
# [block] the block to wrap the paragraph.
|
|
30
32
|
# [wrap] whether to wrap text at width.
|
|
31
33
|
# [align] alignment (:left, :center, :right).
|
|
32
|
-
|
|
34
|
+
# [scroll] scroll offset as (y, x) array (default: [0, 0]).
|
|
35
|
+
def self.new(text:, style: nil, fg: nil, bg: nil, block: nil, wrap: false, align: :left, scroll: [0, 0])
|
|
33
36
|
style ||= Style.new(fg:, bg:)
|
|
34
|
-
super(text:, style:, block:, wrap:, align:)
|
|
37
|
+
super(text:, style:, block:, wrap:, align:, scroll:)
|
|
35
38
|
end
|
|
36
39
|
end
|
|
37
40
|
end
|
|
@@ -0,0 +1,24 @@
|
|
|
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 rectangle in the terminal grid.
|
|
8
|
+
#
|
|
9
|
+
# [x] The x-coordinate of the top-left corner.
|
|
10
|
+
# [y] The y-coordinate of the top-left corner.
|
|
11
|
+
# [width] The width of the rectangle.
|
|
12
|
+
# [height] The height of the rectangle.
|
|
13
|
+
class Rect < Data.define(:x, :y, :width, :height)
|
|
14
|
+
# Creates a new Rect.
|
|
15
|
+
#
|
|
16
|
+
# [x] The x-coordinate of the top-left corner.
|
|
17
|
+
# [y] The y-coordinate of the top-left corner.
|
|
18
|
+
# [width] The width of the rectangle.
|
|
19
|
+
# [height] The height of the rectangle.
|
|
20
|
+
def initialize(x: 0, y: 0, width: 0, height: 0)
|
|
21
|
+
super
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -9,15 +9,21 @@ module RatatuiRuby
|
|
|
9
9
|
# [header] An array of strings or Paragraphs representing the header row.
|
|
10
10
|
# [rows] An array of arrays of strings or Paragraphs representing the data rows.
|
|
11
11
|
# [widths] An array of Constraint objects defining column widths.
|
|
12
|
+
# [highlight_style] The style for the selected row.
|
|
13
|
+
# [highlight_symbol] The symbol to display in front of the selected row.
|
|
14
|
+
# [selected_row] The index of the currently selected row, or nil if none.
|
|
12
15
|
# [block] An optional Block widget to wrap the table.
|
|
13
|
-
class Table < Data.define(:header, :rows, :widths, :block)
|
|
16
|
+
class Table < Data.define(:header, :rows, :widths, :highlight_style, :highlight_symbol, :selected_row, :block)
|
|
14
17
|
# Creates a new Table.
|
|
15
18
|
#
|
|
16
19
|
# [header] An array of strings or Paragraphs representing the header row.
|
|
17
20
|
# [rows] An array of arrays of strings or Paragraphs representing the data rows.
|
|
18
21
|
# [widths] An array of Constraint objects defining column widths.
|
|
22
|
+
# [highlight_style] The style for the selected row.
|
|
23
|
+
# [highlight_symbol] The symbol to display in front of the selected row.
|
|
24
|
+
# [selected_row] The index of the currently selected row, or nil if none.
|
|
19
25
|
# [block] An optional Block widget to wrap the table.
|
|
20
|
-
def initialize(header: nil, rows: [], widths: [], block: nil)
|
|
26
|
+
def initialize(header: nil, rows: [], widths: [], highlight_style: nil, highlight_symbol: "> ", selected_row: nil, block: nil)
|
|
21
27
|
super
|
|
22
28
|
end
|
|
23
29
|
end
|
data/lib/ratatui_ruby/version.rb
CHANGED
data/lib/ratatui_ruby.rb
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
|
|
6
6
|
require_relative "ratatui_ruby/version"
|
|
7
|
+
require_relative "ratatui_ruby/schema/rect"
|
|
7
8
|
require_relative "ratatui_ruby/schema/paragraph"
|
|
8
9
|
require_relative "ratatui_ruby/schema/layout"
|
|
9
10
|
require_relative "ratatui_ruby/schema/block"
|
|
@@ -16,6 +17,7 @@ require_relative "ratatui_ruby/schema/tabs"
|
|
|
16
17
|
require_relative "ratatui_ruby/schema/bar_chart"
|
|
17
18
|
require_relative "ratatui_ruby/schema/sparkline"
|
|
18
19
|
require_relative "ratatui_ruby/schema/chart"
|
|
20
|
+
require_relative "ratatui_ruby/schema/clear"
|
|
19
21
|
require_relative "ratatui_ruby/schema/cursor"
|
|
20
22
|
require_relative "ratatui_ruby/schema/overlay"
|
|
21
23
|
require_relative "ratatui_ruby/schema/center"
|
|
@@ -70,7 +72,7 @@ module RatatuiRuby
|
|
|
70
72
|
# Polls for a keyboard event.
|
|
71
73
|
#
|
|
72
74
|
# poll_event
|
|
73
|
-
# # => { type:
|
|
75
|
+
# # => { type: :key, code: "a", modifiers: ["ctrl"] }
|
|
74
76
|
#
|
|
75
77
|
# (Native method implemented in Rust)
|
|
76
78
|
|
data/mise.toml
CHANGED
|
@@ -0,0 +1,11 @@
|
|
|
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
|
+
class Buffer
|
|
8
|
+
def set_string: (Integer x, Integer y, String string, Style style) -> void
|
|
9
|
+
def area: () -> Rect
|
|
10
|
+
end
|
|
11
|
+
end
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
class Rect < Data
|
|
8
|
+
attr_reader x: Integer
|
|
9
|
+
attr_reader y: Integer
|
|
10
|
+
attr_reader width: Integer
|
|
11
|
+
attr_reader height: Integer
|
|
12
|
+
def self.new: (x: Integer, y: Integer, width: Integer, height: Integer) -> instance
|
|
13
|
+
end
|
|
14
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
require_relative "comparison_links"
|
|
7
|
+
require_relative "unreleased_section"
|
|
8
|
+
require_relative "history"
|
|
9
|
+
require_relative "header"
|
|
10
|
+
|
|
11
|
+
# Changelog manages the project's CHANGELOG.md file.
|
|
12
|
+
class Changelog
|
|
13
|
+
# Creates a new Changelog for the file at the given path.
|
|
14
|
+
def initialize(path: "CHANGELOG.md")
|
|
15
|
+
@path = path
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Releases a new version in the changelog.
|
|
19
|
+
# This moves the unreleased changes to a new version heading and resets the unreleased section.
|
|
20
|
+
def release(new_version)
|
|
21
|
+
content = File.read(@path)
|
|
22
|
+
|
|
23
|
+
header = Header.parse(content)
|
|
24
|
+
unreleased = UnreleasedSection.parse(content)
|
|
25
|
+
links = ComparisonLinks.parse(content)
|
|
26
|
+
|
|
27
|
+
raise "Could not parse CHANGELOG.md" unless header && unreleased && links
|
|
28
|
+
|
|
29
|
+
history = History.parse(content, header.length, unreleased.to_s.length, links.to_s)
|
|
30
|
+
|
|
31
|
+
links.update(new_version)
|
|
32
|
+
history.add(unreleased.as_version(new_version))
|
|
33
|
+
|
|
34
|
+
File.write(@path, "#{header}#{UnreleasedSection.fresh}\n\n#{history}\n#{links}")
|
|
35
|
+
nil
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
# ComparisonLinks manages the git comparison links at the bottom of the changelog.
|
|
7
|
+
class ComparisonLinks
|
|
8
|
+
PATTERN = /^(\[Unreleased\]: .*)$/m
|
|
9
|
+
|
|
10
|
+
# Extracts the comparison links from the given content.
|
|
11
|
+
def self.parse(content)
|
|
12
|
+
match = content.match(PATTERN)
|
|
13
|
+
new(match[1].strip) if match
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Creates a new ComparisonLinks from the given links text.
|
|
17
|
+
def initialize(links)
|
|
18
|
+
@links = links.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Updates the comparison links for the new version.
|
|
22
|
+
def update(new_version)
|
|
23
|
+
pattern = %r{^\[Unreleased\]: (.*?/compare/)v(.*)\.\.\.HEAD$}
|
|
24
|
+
match = @links.match(pattern)
|
|
25
|
+
return unless match
|
|
26
|
+
|
|
27
|
+
base_url = match[1]
|
|
28
|
+
prev_version = match[2]
|
|
29
|
+
|
|
30
|
+
new_unreleased = "[Unreleased]: #{base_url}v#{new_version}...HEAD"
|
|
31
|
+
new_version_link = "[#{new_version}]: #{base_url}v#{prev_version}...v#{new_version}"
|
|
32
|
+
|
|
33
|
+
@links.sub!(pattern, "#{new_unreleased}\n#{new_version_link}")
|
|
34
|
+
nil
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Returns the current state of the links as a string.
|
|
38
|
+
def to_s
|
|
39
|
+
@links
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
# Header manages the header section of the changelog.
|
|
7
|
+
class Header
|
|
8
|
+
PATTERN = /^(.*?)(?=## \[Unreleased\])/m
|
|
9
|
+
|
|
10
|
+
# Extracts the header section from the given content.
|
|
11
|
+
def self.parse(content)
|
|
12
|
+
match = content.match(PATTERN)
|
|
13
|
+
new(match[1]) if match
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Creates a new Header from the given content.
|
|
17
|
+
def initialize(content)
|
|
18
|
+
@content = content.dup
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Returns the length of the header content.
|
|
22
|
+
def length
|
|
23
|
+
@content.length
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Returns the current state of the header as a string.
|
|
27
|
+
def to_s
|
|
28
|
+
@content
|
|
29
|
+
end
|
|
30
|
+
end
|