ratatui_ruby 0.1.0 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +52 -0
  3. data/.builds/ruby-3.3.yml +52 -0
  4. data/.builds/ruby-3.4.yml +52 -0
  5. data/.builds/ruby-4.0.0-preview3.yml +53 -0
  6. data/AGENTS.md +2 -2
  7. data/README.md +33 -13
  8. data/REUSE.toml +5 -0
  9. data/Rakefile +3 -100
  10. data/docs/images/examples-calendar_demo.rb.png +0 -0
  11. data/docs/images/examples-chart_demo.rb.png +0 -0
  12. data/docs/images/examples-list_styles.rb.png +0 -0
  13. data/docs/images/examples-quickstart_lifecycle.rb.png +0 -0
  14. data/docs/images/examples-stock_ticker.rb.png +0 -0
  15. data/docs/quickstart.md +57 -11
  16. data/examples/analytics.rb +2 -1
  17. data/examples/calendar_demo.rb +55 -0
  18. data/examples/chart_demo.rb +84 -0
  19. data/examples/list_styles.rb +66 -0
  20. data/examples/login_form.rb +2 -1
  21. data/examples/quickstart_dsl.rb +30 -0
  22. data/examples/quickstart_lifecycle.rb +40 -0
  23. data/examples/readme_usage.rb +21 -0
  24. data/examples/stock_ticker.rb +13 -5
  25. data/examples/system_monitor.rb +2 -1
  26. data/examples/test_calendar_demo.rb +66 -0
  27. data/examples/test_list_styles.rb +61 -0
  28. data/ext/ratatui_ruby/.cargo/config.toml +5 -0
  29. data/ext/ratatui_ruby/Cargo.lock +94 -1
  30. data/ext/ratatui_ruby/Cargo.toml +3 -2
  31. data/ext/ratatui_ruby/extconf.rb +1 -1
  32. data/ext/ratatui_ruby/src/events.rs +4 -1
  33. data/ext/ratatui_ruby/src/rendering.rs +4 -1
  34. data/ext/ratatui_ruby/src/terminal.rs +4 -6
  35. data/ext/ratatui_ruby/src/widgets/calendar.rs +81 -0
  36. data/ext/ratatui_ruby/src/widgets/chart.rs +253 -0
  37. data/ext/ratatui_ruby/src/widgets/list.rs +41 -4
  38. data/ext/ratatui_ruby/src/widgets/mod.rs +2 -1
  39. data/lib/ratatui_ruby/dsl.rb +62 -0
  40. data/lib/ratatui_ruby/schema/calendar.rb +26 -0
  41. data/lib/ratatui_ruby/schema/chart.rb +81 -0
  42. data/lib/ratatui_ruby/schema/list.rb +8 -2
  43. data/lib/ratatui_ruby/version.rb +1 -1
  44. data/lib/ratatui_ruby.rb +21 -1
  45. data/mise.toml +8 -0
  46. data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
  47. data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
  48. data/sig/ratatui_ruby/schema/list.rbs +4 -1
  49. data/tasks/bump/cargo_lockfile.rb +19 -0
  50. data/tasks/bump/manifest.rb +23 -0
  51. data/tasks/bump/ruby_gem.rb +39 -0
  52. data/tasks/bump/sem_ver.rb +28 -0
  53. data/tasks/bump.rake +45 -0
  54. data/tasks/doc.rake +24 -0
  55. data/tasks/extension.rake +12 -0
  56. data/tasks/lint.rake +49 -0
  57. data/tasks/rdoc_config.rb +15 -0
  58. data/tasks/resources/build.yml.erb +65 -0
  59. data/tasks/resources/index.html.erb +38 -0
  60. data/tasks/resources/rubies.yml +7 -0
  61. data/tasks/sourcehut.rake +29 -0
  62. data/tasks/test.rake +31 -0
  63. data/tasks/website/index_page.rb +28 -0
  64. data/tasks/website/version.rb +116 -0
  65. data/tasks/website/versioned_documentation.rb +48 -0
  66. data/tasks/website/website.rb +50 -0
  67. data/tasks/website.rake +26 -0
  68. metadata +51 -10
  69. data/.build.yml +0 -34
  70. data/.ruby-version +0 -1
  71. data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
  72. data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
@@ -0,0 +1,81 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::style::{parse_block, parse_style};
5
+ use magnus::{prelude::*, Error, Value};
6
+ use ratatui::{
7
+ layout::Rect,
8
+ widgets::calendar::{CalendarEventStore, Monthly},
9
+ Frame,
10
+ };
11
+ use std::convert::TryFrom;
12
+ use time::{Date, Month};
13
+
14
+ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
15
+ let year: i32 = node.funcall("year", ())?;
16
+ let month_u8: u8 = node.funcall("month", ())?;
17
+ let day_style_val: Value = node.funcall("day_style", ())?;
18
+ let header_style_val: Value = node.funcall("header_style", ())?;
19
+ let block_val: Value = node.funcall("block", ())?;
20
+
21
+ let month = Month::try_from(month_u8)
22
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
23
+
24
+ let date = Date::from_calendar_date(year, month, 1)
25
+ .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
26
+
27
+ let mut calendar = Monthly::new(date, CalendarEventStore::default());
28
+
29
+ let header_style = if !header_style_val.is_nil() {
30
+ parse_style(header_style_val)?
31
+ } else {
32
+ ratatui::style::Style::default()
33
+ };
34
+ calendar = calendar
35
+ .show_month_header(header_style)
36
+ .show_weekdays_header(header_style);
37
+
38
+ if !day_style_val.is_nil() {
39
+ calendar = calendar.default_style(parse_style(day_style_val)?);
40
+ }
41
+
42
+ if !block_val.is_nil() {
43
+ calendar = calendar.block(parse_block(block_val)?);
44
+ }
45
+
46
+ frame.render_widget(calendar, area);
47
+ Ok(())
48
+ }
49
+
50
+ #[cfg(test)]
51
+ mod tests {
52
+ use super::*;
53
+ use ratatui::buffer::Buffer;
54
+ use ratatui::widgets::Widget;
55
+
56
+ #[test]
57
+ fn test_calendar_rendering() {
58
+ let date = Date::from_calendar_date(2025, Month::December, 1).unwrap();
59
+ let calendar = Monthly::new(date, CalendarEventStore::default())
60
+ .show_month_header(ratatui::style::Style::default());
61
+ let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
62
+ calendar.render(Rect::new(0, 0, 40, 20), &mut buf);
63
+ let mut content = String::new();
64
+ for y in 0..20 {
65
+ for x in 0..40 {
66
+ content.push_str(buf.get(x, y).symbol());
67
+ }
68
+ content.push('\n');
69
+ }
70
+ assert!(
71
+ content.contains("December"),
72
+ "Content did not contain December: \n{}",
73
+ content
74
+ );
75
+ assert!(
76
+ content.contains("2025"),
77
+ "Content did not contain 2025: \n{}",
78
+ content
79
+ );
80
+ }
81
+ }
@@ -0,0 +1,253 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ use crate::style::{parse_block, parse_style};
5
+ use magnus::{prelude::*, Error, Symbol, Value};
6
+ use ratatui::{
7
+ layout::Rect,
8
+ symbols,
9
+ text::Span,
10
+ widgets::{Axis, Chart, Dataset, GraphType},
11
+ Frame,
12
+ };
13
+
14
+ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
15
+ let class = node.class();
16
+ let class_name = unsafe { class.name() };
17
+
18
+ if class_name == "RatatuiRuby::LineChart" {
19
+ return render_line_chart(frame, area, node);
20
+ }
21
+
22
+ let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
23
+ let x_axis_val: Value = node.funcall("x_axis", ())?;
24
+ let y_axis_val: Value = node.funcall("y_axis", ())?;
25
+ let block_val: Value = node.funcall("block", ())?;
26
+ let style_val: Value = node.funcall("style", ())?;
27
+
28
+ let mut datasets = Vec::new();
29
+ // We need to keep the data alive until the chart is rendered
30
+ let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
31
+
32
+ for i in 0..datasets_val.len() {
33
+ let ds_val: Value = datasets_val.entry(i as isize)?;
34
+ let data_array: magnus::RArray = ds_val.funcall("data", ())?;
35
+
36
+ let mut points = Vec::new();
37
+ for j in 0..data_array.len() {
38
+ let point_array_val: Value = data_array.entry(j as isize)?;
39
+ 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
+ })?;
42
+ let x: f64 = point_array.entry(0)?;
43
+ let y: f64 = point_array.entry(1)?;
44
+ points.push((x, y));
45
+ }
46
+ data_storage.push(points);
47
+ }
48
+
49
+ for (i, points) in data_storage.iter().enumerate() {
50
+ let ds_val: Value = datasets_val.entry(i as isize)?;
51
+ let name: String = ds_val.funcall("name", ())?;
52
+ let marker_sym: Symbol = ds_val.funcall("marker", ())?;
53
+ let graph_type_sym: Symbol = ds_val.funcall("graph_type", ())?;
54
+
55
+ let marker = match marker_sym.to_string().as_str() {
56
+ "dot" => symbols::Marker::Dot,
57
+ "braille" => symbols::Marker::Braille,
58
+ "block" => symbols::Marker::Block,
59
+ "bar" => symbols::Marker::Bar,
60
+ _ => symbols::Marker::Dot,
61
+ };
62
+
63
+ let graph_type = match graph_type_sym.to_string().as_str() {
64
+ "line" => GraphType::Line,
65
+ "scatter" => GraphType::Scatter,
66
+ _ => GraphType::Line,
67
+ };
68
+
69
+ let mut ds_style = ratatui::style::Style::default();
70
+ let color_val: Value = ds_val.funcall("color", ())?;
71
+ if !color_val.is_nil() {
72
+ let color_str: String = color_val.funcall("to_s", ())?;
73
+ if let Some(color) = crate::style::parse_color(&color_str) {
74
+ ds_style = ds_style.fg(color);
75
+ }
76
+ }
77
+
78
+ let ds = Dataset::default()
79
+ .name(name)
80
+ .marker(marker)
81
+ .graph_type(graph_type)
82
+ .style(ds_style)
83
+ .data(points);
84
+ datasets.push(ds);
85
+ }
86
+
87
+ let x_axis = parse_axis(x_axis_val)?;
88
+ let y_axis = parse_axis(y_axis_val)?;
89
+
90
+ let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
91
+
92
+ if !block_val.is_nil() {
93
+ chart = chart.block(parse_block(block_val)?);
94
+ }
95
+
96
+ if !style_val.is_nil() {
97
+ chart = chart.style(parse_style(style_val)?);
98
+ }
99
+
100
+ frame.render_widget(chart, area);
101
+ Ok(())
102
+ }
103
+
104
+ fn parse_axis(axis_val: Value) -> Result<Axis<'static>, Error> {
105
+ let title: String = axis_val.funcall("title", ())?;
106
+ let bounds_val: magnus::RArray = axis_val.funcall("bounds", ())?;
107
+ let labels_val: magnus::RArray = axis_val.funcall("labels", ())?;
108
+ let style_val: Value = axis_val.funcall("style", ())?;
109
+
110
+ let bounds: [f64; 2] = [bounds_val.entry(0)?, bounds_val.entry(1)?];
111
+
112
+ let mut labels = Vec::new();
113
+ for i in 0..labels_val.len() {
114
+ let label: String = labels_val.entry(i as isize)?;
115
+ labels.push(Span::from(label));
116
+ }
117
+
118
+ let mut axis = Axis::default().title(title).bounds(bounds).labels(labels);
119
+
120
+ if !style_val.is_nil() {
121
+ axis = axis.style(parse_style(style_val)?);
122
+ }
123
+
124
+ Ok(axis)
125
+ }
126
+
127
+ fn render_line_chart(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
128
+ let datasets_val: magnus::RArray = node.funcall("datasets", ())?;
129
+ let x_labels_val: magnus::RArray = node.funcall("x_labels", ())?;
130
+ let y_labels_val: magnus::RArray = node.funcall("y_labels", ())?;
131
+ let y_bounds_val: magnus::RArray = node.funcall("y_bounds", ())?;
132
+ let block_val: Value = node.funcall("block", ())?;
133
+
134
+ let mut datasets = Vec::new();
135
+ let mut data_storage: Vec<Vec<(f64, f64)>> = Vec::new();
136
+
137
+ for i in 0..datasets_val.len() {
138
+ let ds_val: Value = datasets_val.entry(i as isize)?;
139
+ let data_array: magnus::RArray = ds_val.funcall("data", ())?;
140
+
141
+ let mut points = Vec::new();
142
+ for j in 0..data_array.len() {
143
+ let point_array_val: Value = data_array.entry(j as isize)?;
144
+ let point_array = magnus::RArray::from_value(point_array_val).ok_or_else(|| {
145
+ Error::new(magnus::exception::type_error(), "expected array for point")
146
+ })?;
147
+ let x: f64 = point_array.entry(0)?;
148
+ let y: f64 = point_array.entry(1)?;
149
+ points.push((x, y));
150
+ }
151
+ data_storage.push(points);
152
+ }
153
+
154
+ for (i, points) in data_storage.iter().enumerate() {
155
+ let ds_val: Value = datasets_val.entry(i as isize)?;
156
+ let name: String = ds_val.funcall("name", ())?;
157
+
158
+ let mut ds_style = ratatui::style::Style::default();
159
+ let color_val: Value = ds_val.funcall("color", ())?;
160
+ let color_str: String = color_val.funcall("to_s", ())?;
161
+ if let Some(color) = crate::style::parse_color(&color_str) {
162
+ ds_style = ds_style.fg(color);
163
+ }
164
+
165
+ let ds = Dataset::default()
166
+ .name(name)
167
+ .marker(symbols::Marker::Braille)
168
+ .style(ds_style)
169
+ .data(points);
170
+ datasets.push(ds);
171
+ }
172
+
173
+ let mut x_labels = Vec::new();
174
+ for i in 0..x_labels_val.len() {
175
+ let label: String = x_labels_val.entry(i as isize)?;
176
+ x_labels.push(Span::from(label));
177
+ }
178
+
179
+ let mut y_labels = Vec::new();
180
+ for i in 0..y_labels_val.len() {
181
+ let label: String = y_labels_val.entry(i as isize)?;
182
+ y_labels.push(Span::from(label));
183
+ }
184
+
185
+ let y_bounds: [f64; 2] = [y_bounds_val.entry(0)?, y_bounds_val.entry(1)?];
186
+
187
+ let mut min_x = 0.0;
188
+ let mut max_x = 0.0;
189
+ let mut first = true;
190
+ for ds_data in &data_storage {
191
+ for (x, _) in ds_data {
192
+ if first {
193
+ min_x = *x;
194
+ max_x = *x;
195
+ first = false;
196
+ } else {
197
+ if *x < min_x {
198
+ min_x = *x;
199
+ }
200
+ if *x > max_x {
201
+ max_x = *x;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ if min_x == max_x {
207
+ max_x = min_x + 1.0;
208
+ }
209
+
210
+ let x_axis = Axis::default().labels(x_labels).bounds([min_x, max_x]);
211
+ let y_axis = Axis::default().labels(y_labels).bounds(y_bounds);
212
+
213
+ let mut chart = Chart::new(datasets).x_axis(x_axis).y_axis(y_axis);
214
+ if !block_val.is_nil() {
215
+ chart = chart.block(parse_block(block_val)?);
216
+ }
217
+
218
+ frame.render_widget(chart, area);
219
+ Ok(())
220
+ }
221
+
222
+ #[cfg(test)]
223
+ mod tests {
224
+ use super::*;
225
+ use ratatui::buffer::Buffer;
226
+ use ratatui::widgets::Widget;
227
+
228
+ #[test]
229
+ fn test_chart_rendering() {
230
+ let data = vec![(0.0, 0.0), (1.0, 1.0)];
231
+ let datasets = vec![Dataset::default().name("TestDS").data(&data)];
232
+ let chart = Chart::new(datasets)
233
+ .x_axis(
234
+ Axis::default()
235
+ .bounds([0.0, 1.0])
236
+ .labels(vec!["XMIN".into(), "XMAX".into()]),
237
+ )
238
+ .y_axis(
239
+ Axis::default()
240
+ .bounds([0.0, 1.0])
241
+ .labels(vec!["YMIN".into(), "YMAX".into()]),
242
+ );
243
+ let mut buf = Buffer::empty(Rect::new(0, 0, 40, 20));
244
+ chart.render(Rect::new(0, 0, 40, 20), &mut buf);
245
+ assert!(buf.content().iter().any(|c| c.symbol() != " "));
246
+ let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
247
+ assert!(content.contains("XMIN"));
248
+ assert!(content.contains("XMAX"));
249
+ assert!(content.contains("YMIN"));
250
+ assert!(content.contains("YMAX"));
251
+ assert!(content.contains("TestDS"));
252
+ }
253
+ }
@@ -1,8 +1,8 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- use crate::style::parse_block;
5
- use magnus::{prelude::*, Error, Value};
4
+ use crate::style::{parse_block, parse_style};
5
+ use magnus::{prelude::*, Error, TryConvert, Value};
6
6
  use ratatui::{
7
7
  layout::Rect,
8
8
  widgets::{List, ListState},
@@ -14,6 +14,9 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
14
14
  let items_array = magnus::RArray::from_value(items_val)
15
15
  .ok_or_else(|| Error::new(magnus::exception::type_error(), "expected array"))?;
16
16
  let selected_index_val: Value = node.funcall("selected_index", ())?;
17
+ let style_val: Value = node.funcall("style", ())?;
18
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
19
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
17
20
  let block_val: Value = node.funcall("block", ())?;
18
21
 
19
22
  let mut items = Vec::new();
@@ -22,13 +25,32 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
22
25
  items.push(item);
23
26
  }
24
27
 
28
+ let symbol: String = if !highlight_symbol_val.is_nil() {
29
+ let s: String = String::try_convert(highlight_symbol_val)?;
30
+ s
31
+ } else {
32
+ String::new()
33
+ };
34
+
25
35
  let mut state = ListState::default();
26
36
  if !selected_index_val.is_nil() {
27
37
  let index: usize = selected_index_val.funcall("to_int", ())?;
28
38
  state.select(Some(index));
29
39
  }
30
40
 
31
- let mut list = List::new(items).highlight_symbol(">> ");
41
+ let mut list = List::new(items);
42
+
43
+ if !highlight_symbol_val.is_nil() {
44
+ list = list.highlight_symbol(&symbol);
45
+ }
46
+
47
+ if !style_val.is_nil() {
48
+ list = list.style(parse_style(style_val)?);
49
+ }
50
+
51
+ if !highlight_style_val.is_nil() {
52
+ list = list.highlight_style(parse_style(highlight_style_val)?);
53
+ }
32
54
 
33
55
  if !block_val.is_nil() {
34
56
  list = list.block(parse_block(block_val)?);
@@ -47,7 +69,14 @@ mod tests {
47
69
  #[test]
48
70
  fn test_list_rendering() {
49
71
  let items = vec!["Item 1", "Item 2"];
50
- let list = List::new(items).highlight_symbol(">> ");
72
+ let list = List::new(items)
73
+ .highlight_symbol(">> ")
74
+ .style(ratatui::style::Style::default().fg(ratatui::style::Color::White))
75
+ .highlight_style(
76
+ ratatui::style::Style::default()
77
+ .fg(ratatui::style::Color::Yellow)
78
+ .add_modifier(ratatui::style::Modifier::BOLD),
79
+ );
51
80
  let mut state = ListState::default();
52
81
  state.select(Some(1));
53
82
 
@@ -58,5 +87,13 @@ mod tests {
58
87
  let content = buf.content().iter().map(|c| c.symbol()).collect::<String>();
59
88
  assert!(content.contains("Item 1"));
60
89
  assert!(content.contains(">> Item 2"));
90
+
91
+ // 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);
94
+ assert!(buf
95
+ .get(0, 1)
96
+ .modifier
97
+ .contains(ratatui::style::Modifier::BOLD));
61
98
  }
62
99
  }
@@ -3,12 +3,13 @@
3
3
 
4
4
  pub mod barchart;
5
5
  pub mod block;
6
+ pub mod calendar;
6
7
  pub mod canvas;
7
8
  pub mod center;
9
+ pub mod chart;
8
10
  pub mod cursor;
9
11
  pub mod gauge;
10
12
  pub mod layout;
11
- pub mod linechart;
12
13
  pub mod list;
13
14
  pub mod overlay;
14
15
  pub mod paragraph;
@@ -0,0 +1,62 @@
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
+ ##
8
+ # A wrapper class that provides a concise DSL for creating widgets and interacting
9
+ # with the terminal within the +main_loop+.
10
+ #
11
+ # This class is yielded to the block provided to {RatatuiRuby.main_loop}.
12
+ # It uses metaprogramming to delegate method calls to {RatatuiRuby} module functions
13
+ # and to act as a factory for {RatatuiRuby} widget classes.
14
+ #
15
+ # == Features
16
+ #
17
+ # 1. **Widget Shorthand**: Provides factory methods for every widget class.
18
+ # Converts snake_case method calls (e.g., +paragraph+) into CamelCase class instantiations
19
+ # (e.g., +RatatuiRuby::Paragraph.new+).
20
+ #
21
+ # 2. **Method Shorthand**: Aliases module functions of {RatatuiRuby}, allowing you
22
+ # to call methods like +draw+ and +poll_event+ directly on the DSL object.
23
+ #
24
+ # == Example
25
+ #
26
+ # RatatuiRuby.main_loop do |tui|
27
+ # # Create UI using shorthand methods
28
+ # view = tui.paragraph(
29
+ # text: "Hello World",
30
+ # block: tui.block(borders: [:all])
31
+ # )
32
+ #
33
+ # # Use module aliases to draw and handle events
34
+ # tui.draw(view)
35
+ # event = tui.poll_event
36
+ #
37
+ # break if event && event[:code] == "q"
38
+ # end
39
+ class DSL
40
+ # Wrap methods directly
41
+ RatatuiRuby.singleton_methods(false).each do |method_name|
42
+ define_method(method_name) do |*args, **kwargs, &block|
43
+ RatatuiRuby.public_send(method_name, *args, **kwargs, &block)
44
+ end
45
+ end
46
+
47
+ # Wrap classes as snake_case factories
48
+ RatatuiRuby.constants.each do |const_name|
49
+ klass = RatatuiRuby.const_get(const_name)
50
+ next unless klass.is_a?(Class)
51
+
52
+ method_name = const_name.to_s
53
+ .gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
54
+ .gsub(/([a-z\d])([A-Z])/, '\1_\2')
55
+ .downcase
56
+
57
+ define_method(method_name) do |*args, **kwargs, &block|
58
+ klass.new(*args, **kwargs, &block)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,26 @@
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 Monthly Calendar widget.
8
+ #
9
+ # [year] Integer (e.g., 2025)
10
+ # [month] Integer (1-12)
11
+ # [day_style] Style (Style for regular days)
12
+ # [header_style] Style (Style for the month title)
13
+ # [block] Block
14
+ class Calendar < Data.define(:year, :month, :day_style, :header_style, :block)
15
+ # Creates a new Calendar.
16
+ #
17
+ # [year] Integer (e.g., 2025)
18
+ # [month] Integer (1-12)
19
+ # [day_style] Style (Style for regular days)
20
+ # [header_style] Style (Style for the month title)
21
+ # [block] Block
22
+ def initialize(year:, month:, day_style: nil, header_style: nil, block: nil)
23
+ super
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,81 @@
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
+ # Defines an Axis for a Chart
8
+ # [title] String
9
+ # [bounds] Array<Float> [min, max]
10
+ # [labels] Array<String>
11
+ # [style] Style
12
+ class Axis < Data.define(:title, :bounds, :labels, :style)
13
+ # Creates a new Axis.
14
+ #
15
+ # [title] String
16
+ # [bounds] Array<Float> [min, max]
17
+ # [labels] Array<String>
18
+ # [style] Style
19
+ def initialize(title: "", bounds: [0.0, 10.0], labels: [], style: nil)
20
+ super
21
+ end
22
+ end
23
+
24
+ # Defines a Dataset for a Chart.
25
+ # [name] The name of the dataset.
26
+ # [data] Array of arrays [[x, y], [x, y]] (Floats).
27
+ # [color] The color of the line.
28
+ # [marker] Symbol (:dot, :braille, :block, :bar)
29
+ # [graph_type] Symbol (:line, :scatter)
30
+ class Dataset < Data.define(:name, :data, :color, :marker, :graph_type)
31
+ # Creates a new Dataset.
32
+ #
33
+ # [name] The name of the dataset.
34
+ # [data] Array of arrays [[x, y], [x, y]] (Floats).
35
+ # [color] The color of the line.
36
+ # [marker] Symbol (:dot, :braille, :block, :bar)
37
+ # [graph_type] Symbol (:line, :scatter)
38
+ def initialize(name:, data:, color: "reset", marker: :dot, graph_type: :line)
39
+ super
40
+ end
41
+ end
42
+
43
+ # A generic Cartesian chart.
44
+ # [datasets] Array<Dataset>
45
+ # [x_axis] Axis
46
+ # [y_axis] Axis
47
+ # [block] Block
48
+ # [style] Style (base style)
49
+ class Chart < Data.define(:datasets, :x_axis, :y_axis, :block, :style)
50
+ # Creates a new Chart widget.
51
+ #
52
+ # [datasets] Array<Dataset>
53
+ # [x_axis] Axis
54
+ # [y_axis] Axis
55
+ # [block] Block
56
+ # [style] Style (base style)
57
+ def initialize(datasets:, x_axis:, y_axis:, block: nil, style: nil)
58
+ super
59
+ end
60
+ end
61
+
62
+ # A complex chart widget. (Legacy/Alias for Chart)
63
+ #
64
+ # [datasets] Array of Dataset objects.
65
+ # [x_labels] Array of Strings for the X-axis labels.
66
+ # [y_labels] Array of Strings for the Y-axis labels.
67
+ # [y_bounds] Array of two Floats [min, max] for the Y-axis.
68
+ # [block] Optional block widget to wrap the chart.
69
+ class LineChart < Data.define(:datasets, :x_labels, :y_labels, :y_bounds, :block)
70
+ # Creates a new LineChart widget.
71
+ #
72
+ # [datasets] Array of Dataset objects.
73
+ # [x_labels] Array of Strings for the X-axis labels.
74
+ # [y_labels] Array of Strings for the Y-axis labels.
75
+ # [y_bounds] Array of two Floats [min, max] for the Y-axis.
76
+ # [block] Optional block widget to wrap the chart.
77
+ def initialize(datasets:, x_labels: [], y_labels: [], y_bounds: [0.0, 100.0], block: nil)
78
+ super
79
+ end
80
+ end
81
+ end
@@ -8,14 +8,20 @@ module RatatuiRuby
8
8
  #
9
9
  # [items] An array of strings to display in the list.
10
10
  # [selected_index] The index of the currently selected item, or nil if none.
11
+ # [style] The base style for all items.
12
+ # [highlight_style] The style for the selected item.
13
+ # [highlight_symbol] The symbol to display in front of the selected item.
11
14
  # [block] An optional Block widget to wrap the list.
12
- class List < Data.define(:items, :selected_index, :block)
15
+ class List < Data.define(:items, :selected_index, :style, :highlight_style, :highlight_symbol, :block)
13
16
  # Creates a new List.
14
17
  #
15
18
  # [items] An array of strings to display in the list.
16
19
  # [selected_index] The index of the currently selected item, or nil if none.
20
+ # [style] The base style for all items.
21
+ # [highlight_style] The style for the selected item.
22
+ # [highlight_symbol] The symbol to display in front of the selected item.
17
23
  # [block] An optional Block widget to wrap the list.
18
- def initialize(items: [], selected_index: nil, block: nil)
24
+ def initialize(items: [], selected_index: nil, style: nil, highlight_style: nil, highlight_symbol: "> ", block: nil)
19
25
  super
20
26
  end
21
27
  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.1.0"
9
+ VERSION = "0.2.0"
10
10
  end
data/lib/ratatui_ruby.rb CHANGED
@@ -15,12 +15,13 @@ require_relative "ratatui_ruby/schema/table"
15
15
  require_relative "ratatui_ruby/schema/tabs"
16
16
  require_relative "ratatui_ruby/schema/bar_chart"
17
17
  require_relative "ratatui_ruby/schema/sparkline"
18
- require_relative "ratatui_ruby/schema/line_chart"
18
+ require_relative "ratatui_ruby/schema/chart"
19
19
  require_relative "ratatui_ruby/schema/cursor"
20
20
  require_relative "ratatui_ruby/schema/overlay"
21
21
  require_relative "ratatui_ruby/schema/center"
22
22
  require_relative "ratatui_ruby/schema/scrollbar"
23
23
  require_relative "ratatui_ruby/schema/canvas"
24
+ require_relative "ratatui_ruby/schema/calendar"
24
25
 
25
26
  begin
26
27
  require "ratatui_ruby/ratatui_ruby"
@@ -84,4 +85,23 @@ module RatatuiRuby
84
85
  # inject_test_event("key", { code: "a" })
85
86
  #
86
87
  # (Native method implemented in Rust)
88
+
89
+ ##
90
+ # Provides a convenience wrapper for the main TUI loop.
91
+ # Initializes the terminal, runs the loop, and ensures the terminal is restored.
92
+ #
93
+ # RatatuiRuby.main_loop do
94
+ # draw RatatuiRuby::Paragraph.new(text: "Hello")
95
+ # event = RatatuiRuby::poll_event
96
+ # break if event && event[:type] == :key && event[:code] == "q"
97
+ # end
98
+ def self.main_loop
99
+ require_relative "ratatui_ruby/dsl"
100
+ init_terminal
101
+ loop do
102
+ yield DSL.new
103
+ end
104
+ ensure
105
+ restore_terminal
106
+ end
87
107
  end
data/mise.toml ADDED
@@ -0,0 +1,8 @@
1
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ # SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ [tools]
5
+ ruby = "3.4.8"
6
+ rust = "1.91.1"
7
+ python = "3.12"
8
+ pre-commit = "latest"