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.
Files changed (94) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +2 -2
  3. data/.builds/ruby-3.3.yml +2 -2
  4. data/.builds/ruby-3.4.yml +2 -2
  5. data/.builds/{ruby-4.0.0-preview3.yml → ruby-4.0.0.yml} +8 -9
  6. data/.pre-commit-config.yaml +9 -2
  7. data/AGENTS.md +59 -4
  8. data/CHANGELOG.md +58 -1
  9. data/README.md +6 -6
  10. data/REUSE.toml +1 -6
  11. data/{docs → doc}/contributors/index.md +2 -1
  12. data/doc/custom.css +8 -0
  13. data/doc/images/examples-custom_widget.rb.png +0 -0
  14. data/doc/images/examples-popup_demo.rb.gif +0 -0
  15. data/doc/images/examples-scroll_text.rb.png +0 -0
  16. data/doc/images/examples-table_select.rb.png +0 -0
  17. data/{docs → doc}/index.md +1 -1
  18. data/{docs → doc}/quickstart.md +24 -0
  19. data/examples/custom_widget.rb +43 -0
  20. data/examples/popup_demo.rb +105 -0
  21. data/examples/scroll_text.rb +74 -0
  22. data/examples/table_select.rb +70 -0
  23. data/examples/test_popup_demo.rb +62 -0
  24. data/examples/test_scroll_text.rb +130 -0
  25. data/examples/test_table_select.rb +37 -0
  26. data/ext/ratatui_ruby/Cargo.lock +167 -50
  27. data/ext/ratatui_ruby/Cargo.toml +4 -4
  28. data/ext/ratatui_ruby/src/buffer.rs +54 -0
  29. data/ext/ratatui_ruby/src/events.rs +111 -106
  30. data/ext/ratatui_ruby/src/lib.rs +15 -6
  31. data/ext/ratatui_ruby/src/rendering.rs +15 -0
  32. data/ext/ratatui_ruby/src/style.rs +2 -1
  33. data/ext/ratatui_ruby/src/terminal.rs +24 -19
  34. data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
  35. data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
  36. data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
  37. data/ext/ratatui_ruby/src/widgets/chart.rs +11 -4
  38. data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
  39. data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
  40. data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
  41. data/ext/ratatui_ruby/src/widgets/list.rs +6 -4
  42. data/ext/ratatui_ruby/src/widgets/mod.rs +1 -0
  43. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  44. data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
  45. data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
  46. data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
  47. data/lib/ratatui_ruby/dsl.rb +2 -0
  48. data/lib/ratatui_ruby/schema/clear.rb +83 -0
  49. data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
  50. data/lib/ratatui_ruby/schema/rect.rb +24 -0
  51. data/lib/ratatui_ruby/schema/table.rb +8 -2
  52. data/lib/ratatui_ruby/version.rb +1 -1
  53. data/lib/ratatui_ruby.rb +3 -1
  54. data/mise.toml +1 -1
  55. data/sig/ratatui_ruby/buffer.rbs +11 -0
  56. data/sig/ratatui_ruby/schema/rect.rbs +14 -0
  57. data/tasks/bump/changelog.rb +37 -0
  58. data/tasks/bump/comparison_links.rb +41 -0
  59. data/tasks/bump/header.rb +30 -0
  60. data/tasks/bump/history.rb +30 -0
  61. data/tasks/bump/manifest.rb +8 -0
  62. data/tasks/bump/ruby_gem.rb +6 -10
  63. data/tasks/bump/sem_ver.rb +6 -0
  64. data/tasks/bump/unreleased_section.rb +38 -0
  65. data/tasks/bump.rake +5 -1
  66. data/tasks/doc.rake +5 -4
  67. data/tasks/resources/build.yml.erb +1 -14
  68. data/tasks/resources/rubies.yml +1 -1
  69. data/tasks/sourcehut.rake +11 -2
  70. data/tasks/website/version.rb +1 -0
  71. data/tasks/website/version_menu.rb +68 -0
  72. data/tasks/website/versioned_documentation.rb +2 -1
  73. data/tasks/website/website.rb +4 -1
  74. data/tasks/website.rake +3 -3
  75. metadata +76 -26
  76. data/CODE_OF_CONDUCT.md +0 -30
  77. data/CONTRIBUTING.md +0 -40
  78. /data/{docs → doc}/application_testing.md +0 -0
  79. /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
  80. /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
  81. /data/{docs → doc}/contributors/design.md +0 -0
  82. /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
  83. /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
  84. /data/{docs → doc}/images/examples-calendar_demo.rb.png +0 -0
  85. /data/{docs → doc}/images/examples-chart_demo.rb.png +0 -0
  86. /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
  87. /data/{docs → doc}/images/examples-list_styles.rb.png +0 -0
  88. /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
  89. /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
  90. /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
  91. /data/{docs → doc}/images/examples-quickstart_lifecycle.rb.png +0 -0
  92. /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
  93. /data/{docs → doc}/images/examples-stock_ticker.rb.png +0 -0
  94. /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(magnus::exception::type_error(), "expected array for point")
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(magnus::exception::type_error(), "expected array for point")
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.set_cursor(area.x + x, area.y + y);
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(magnus::exception::type_error(), "expected array"))?;
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(magnus::exception::type_error(), "expected array"))?;
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.get(0, 0).fg, ratatui::style::Color::White);
93
- assert_eq!(buf.get(0, 1).fg, ratatui::style::Color::Yellow);
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
- .get(0, 1)
96
+ .cell((0, 1))
97
+ .unwrap()
96
98
  .modifier
97
99
  .contains(ratatui::style::Modifier::BOLD));
98
100
  }
@@ -7,6 +7,7 @@ pub mod calendar;
7
7
  pub mod canvas;
8
8
  pub mod center;
9
9
  pub mod chart;
10
+ pub mod clear;
10
11
  pub mod cursor;
11
12
  pub mod gauge;
12
13
  pub mod layout;
@@ -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(magnus::exception::type_error(), "expected array for layers"))?;
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(magnus::exception::type_error(), "expected array for rows"))?;
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(magnus::exception::type_error(), "expected array for widths"))?;
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(magnus::exception::type_error(), "expected array for row"))?;
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(magnus::exception::type_error(), "expected array for header")
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
- frame.render_widget(table, area);
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(magnus::exception::type_error(), "expected array for titles"))?;
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() {
@@ -46,6 +46,8 @@ module RatatuiRuby
46
46
 
47
47
  # Wrap classes as snake_case factories
48
48
  RatatuiRuby.constants.each do |const_name|
49
+ next if const_name == :Buffer
50
+
49
51
  klass = RatatuiRuby.const_get(const_name)
50
52
  next unless klass.is_a?(Class)
51
53
 
@@ -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
- class Paragraph < Data.define(:text, :style, :block, :wrap, :align)
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
- def initialize(text:, style: Style.default, block: nil, wrap: false, align: :left)
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
- def self.new(text:, style: nil, fg: nil, bg: nil, block: nil, wrap: false, align: :left)
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
@@ -6,5 +6,5 @@
6
6
  module RatatuiRuby
7
7
  # The version of the ratatui_ruby gem.
8
8
  # See https://semver.org/spec/v2.0.0.html
9
- VERSION = "0.2.0"
9
+ VERSION = "0.3.1"
10
10
  end
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: "key", code: "char", value: "a", modifiers: [] }
75
+ # # => { type: :key, code: "a", modifiers: ["ctrl"] }
74
76
  #
75
77
  # (Native method implemented in Rust)
76
78
 
data/mise.toml CHANGED
@@ -2,7 +2,7 @@
2
2
  # SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  [tools]
5
- ruby = "3.4.8"
5
+ ruby = "4.0.0"
6
6
  rust = "1.91.1"
7
7
  python = "3.12"
8
8
  pre-commit = "latest"
@@ -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