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
@@ -1,112 +1,113 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
- use magnus::{Error, IntoValue, Symbol, TryConvert, Value};
4
+ use magnus::{Error, IntoValue, TryConvert, Value};
5
5
  use std::sync::Mutex;
6
6
 
7
7
  lazy_static::lazy_static! {
8
- static ref EVENT_QUEUE: Mutex<Vec<crossterm::event::Event>> = Mutex::new(Vec::new());
8
+ static ref EVENT_QUEUE: Mutex<Vec<ratatui::crossterm::event::Event>> = Mutex::new(Vec::new());
9
9
  }
10
10
 
11
11
  pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(), Error> {
12
+ let ruby = magnus::Ruby::get().unwrap();
12
13
  let event = match event_type.as_str() {
13
14
  "key" => {
14
- let code_val: Value = data.get(Symbol::new("code")).ok_or_else(|| {
15
+ let code_val: Value = data.get(ruby.to_symbol("code")).ok_or_else(|| {
15
16
  Error::new(
16
- magnus::exception::arg_error(),
17
+ ruby.exception_arg_error(),
17
18
  "Missing 'code' in key event",
18
19
  )
19
20
  })?;
20
21
  let code_str: String = String::try_convert(code_val)?;
21
22
  let code = match code_str.as_str() {
22
- "up" => crossterm::event::KeyCode::Up,
23
- "down" => crossterm::event::KeyCode::Down,
24
- "left" => crossterm::event::KeyCode::Left,
25
- "right" => crossterm::event::KeyCode::Right,
26
- "enter" => crossterm::event::KeyCode::Enter,
27
- "esc" => crossterm::event::KeyCode::Esc,
28
- "backspace" => crossterm::event::KeyCode::Backspace,
29
- "tab" => crossterm::event::KeyCode::Tab,
30
- c if c.len() == 1 => crossterm::event::KeyCode::Char(c.chars().next().unwrap()),
31
- _ => crossterm::event::KeyCode::Null,
23
+ "up" => ratatui::crossterm::event::KeyCode::Up,
24
+ "down" => ratatui::crossterm::event::KeyCode::Down,
25
+ "left" => ratatui::crossterm::event::KeyCode::Left,
26
+ "right" => ratatui::crossterm::event::KeyCode::Right,
27
+ "enter" => ratatui::crossterm::event::KeyCode::Enter,
28
+ "esc" => ratatui::crossterm::event::KeyCode::Esc,
29
+ "backspace" => ratatui::crossterm::event::KeyCode::Backspace,
30
+ "tab" => ratatui::crossterm::event::KeyCode::Tab,
31
+ c if c.len() == 1 => ratatui::crossterm::event::KeyCode::Char(c.chars().next().unwrap()),
32
+ _ => ratatui::crossterm::event::KeyCode::Null,
32
33
  };
33
34
 
34
- let mut modifiers = crossterm::event::KeyModifiers::empty();
35
- if let Some(mods_val) = data.get(Symbol::new("modifiers")) {
35
+ let mut modifiers = ratatui::crossterm::event::KeyModifiers::empty();
36
+ if let Some(mods_val) = data.get(ruby.to_symbol("modifiers")) {
36
37
  let mods: Vec<String> = Vec::try_convert(mods_val)?;
37
38
  for m in mods {
38
39
  match m.as_str() {
39
- "ctrl" => modifiers |= crossterm::event::KeyModifiers::CONTROL,
40
- "alt" => modifiers |= crossterm::event::KeyModifiers::ALT,
41
- "shift" => modifiers |= crossterm::event::KeyModifiers::SHIFT,
40
+ "ctrl" => modifiers |= ratatui::crossterm::event::KeyModifiers::CONTROL,
41
+ "alt" => modifiers |= ratatui::crossterm::event::KeyModifiers::ALT,
42
+ "shift" => modifiers |= ratatui::crossterm::event::KeyModifiers::SHIFT,
42
43
  _ => {}
43
44
  }
44
45
  }
45
46
  }
46
47
 
47
- crossterm::event::Event::Key(crossterm::event::KeyEvent::new(code, modifiers))
48
+ ratatui::crossterm::event::Event::Key(ratatui::crossterm::event::KeyEvent::new(code, modifiers))
48
49
  }
49
50
  "mouse" => {
50
- let kind_val: Value = data.get(Symbol::new("kind")).ok_or_else(|| {
51
+ let kind_val: Value = data.get(ruby.to_symbol("kind")).ok_or_else(|| {
51
52
  Error::new(
52
- magnus::exception::arg_error(),
53
+ ruby.exception_arg_error(),
53
54
  "Missing 'kind' in mouse event",
54
55
  )
55
56
  })?;
56
57
  let kind_str: String = String::try_convert(kind_val)?;
57
58
 
58
- let button = if let Some(btn_val) = data.get(Symbol::new("button")) {
59
+ let button = if let Some(btn_val) = data.get(ruby.to_symbol("button")) {
59
60
  let button_str: String = String::try_convert(btn_val)?;
60
61
  match button_str.as_str() {
61
- "right" => crossterm::event::MouseButton::Right,
62
- "middle" => crossterm::event::MouseButton::Middle,
63
- _ => crossterm::event::MouseButton::Left,
62
+ "right" => ratatui::crossterm::event::MouseButton::Right,
63
+ "middle" => ratatui::crossterm::event::MouseButton::Middle,
64
+ _ => ratatui::crossterm::event::MouseButton::Left,
64
65
  }
65
66
  } else {
66
- crossterm::event::MouseButton::Left
67
+ ratatui::crossterm::event::MouseButton::Left
67
68
  };
68
69
 
69
- let x_val: Value = data.get(Symbol::new("x")).ok_or_else(|| {
70
- Error::new(magnus::exception::arg_error(), "Missing 'x' in mouse event")
70
+ let x_val: Value = data.get(ruby.to_symbol("x")).ok_or_else(|| {
71
+ Error::new(ruby.exception_arg_error(), "Missing 'x' in mouse event")
71
72
  })?;
72
73
  let x: u16 = u16::try_convert(x_val)?;
73
74
 
74
- let y_val: Value = data.get(Symbol::new("y")).ok_or_else(|| {
75
- Error::new(magnus::exception::arg_error(), "Missing 'y' in mouse event")
75
+ let y_val: Value = data.get(ruby.to_symbol("y")).ok_or_else(|| {
76
+ Error::new(ruby.exception_arg_error(), "Missing 'y' in mouse event")
76
77
  })?;
77
78
  let y: u16 = u16::try_convert(y_val)?;
78
79
 
79
80
  let kind = match kind_str.as_str() {
80
- "down" => crossterm::event::MouseEventKind::Down(button),
81
- "up" => crossterm::event::MouseEventKind::Up(button),
82
- "drag" => crossterm::event::MouseEventKind::Drag(button),
83
- "moved" => crossterm::event::MouseEventKind::Moved,
84
- "scroll_down" => crossterm::event::MouseEventKind::ScrollDown,
85
- "scroll_up" => crossterm::event::MouseEventKind::ScrollUp,
86
- "scroll_left" => crossterm::event::MouseEventKind::ScrollLeft,
87
- "scroll_right" => crossterm::event::MouseEventKind::ScrollRight,
81
+ "down" => ratatui::crossterm::event::MouseEventKind::Down(button),
82
+ "up" => ratatui::crossterm::event::MouseEventKind::Up(button),
83
+ "drag" => ratatui::crossterm::event::MouseEventKind::Drag(button),
84
+ "moved" => ratatui::crossterm::event::MouseEventKind::Moved,
85
+ "scroll_down" => ratatui::crossterm::event::MouseEventKind::ScrollDown,
86
+ "scroll_up" => ratatui::crossterm::event::MouseEventKind::ScrollUp,
87
+ "scroll_left" => ratatui::crossterm::event::MouseEventKind::ScrollLeft,
88
+ "scroll_right" => ratatui::crossterm::event::MouseEventKind::ScrollRight,
88
89
  _ => {
89
90
  return Err(Error::new(
90
- magnus::exception::arg_error(),
91
+ ruby.exception_arg_error(),
91
92
  format!("Unknown mouse kind: {}", kind_str),
92
93
  ))
93
94
  }
94
95
  };
95
96
 
96
- let mut modifiers = crossterm::event::KeyModifiers::empty();
97
- if let Some(mods_val) = data.get(Symbol::new("modifiers")) {
97
+ let mut modifiers = ratatui::crossterm::event::KeyModifiers::empty();
98
+ if let Some(mods_val) = data.get(ruby.to_symbol("modifiers")) {
98
99
  let mods: Vec<String> = Vec::try_convert(mods_val)?;
99
100
  for m in mods {
100
101
  match m.as_str() {
101
- "ctrl" => modifiers |= crossterm::event::KeyModifiers::CONTROL,
102
- "alt" => modifiers |= crossterm::event::KeyModifiers::ALT,
103
- "shift" => modifiers |= crossterm::event::KeyModifiers::SHIFT,
102
+ "ctrl" => modifiers |= ratatui::crossterm::event::KeyModifiers::CONTROL,
103
+ "alt" => modifiers |= ratatui::crossterm::event::KeyModifiers::ALT,
104
+ "shift" => modifiers |= ratatui::crossterm::event::KeyModifiers::SHIFT,
104
105
  _ => {}
105
106
  }
106
107
  }
107
108
  }
108
109
 
109
- crossterm::event::Event::Mouse(crossterm::event::MouseEvent {
110
+ ratatui::crossterm::event::Event::Mouse(ratatui::crossterm::event::MouseEvent {
110
111
  kind,
111
112
  column: x,
112
113
  row: y,
@@ -115,7 +116,7 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
115
116
  }
116
117
  _ => {
117
118
  return Err(Error::new(
118
- magnus::exception::arg_error(),
119
+ ruby.exception_arg_error(),
119
120
  format!("Unknown event type: {}", event_type),
120
121
  ))
121
122
  }
@@ -126,6 +127,7 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
126
127
  }
127
128
 
128
129
  pub fn poll_event() -> Result<Value, Error> {
130
+ let ruby = magnus::Ruby::get().unwrap();
129
131
  let event = {
130
132
  let mut queue = EVENT_QUEUE.lock().unwrap();
131
133
  if !queue.is_empty() {
@@ -149,134 +151,137 @@ pub fn poll_event() -> Result<Value, Error> {
149
151
  };
150
152
 
151
153
  if is_test_mode {
152
- return Ok(magnus::value::qnil().into_value());
154
+ return Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()));
153
155
  }
154
156
 
155
- if crossterm::event::poll(std::time::Duration::from_millis(16))
156
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?
157
+ if ratatui::crossterm::event::poll(std::time::Duration::from_millis(16))
158
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?
157
159
  {
158
- let event = crossterm::event::read()
159
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
160
+ let event = ratatui::crossterm::event::read()
161
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
160
162
  handle_event(event)
161
163
  } else {
162
- Ok(magnus::value::qnil().into_value())
164
+ Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
163
165
  }
164
166
  }
165
167
 
166
- fn handle_event(event: crossterm::event::Event) -> Result<Value, Error> {
168
+ fn handle_event(event: ratatui::crossterm::event::Event) -> Result<Value, Error> {
169
+ let ruby = magnus::Ruby::get().unwrap();
167
170
  match event {
168
- crossterm::event::Event::Key(key) => {
169
- if key.kind == crossterm::event::KeyEventKind::Press {
170
- let hash = magnus::RHash::new();
171
- hash.aset(Symbol::new("type"), Symbol::new("key"))?;
171
+ ratatui::crossterm::event::Event::Key(key) => {
172
+ if key.kind == ratatui::crossterm::event::KeyEventKind::Press {
173
+ let ruby = magnus::Ruby::get().unwrap();
174
+ let hash = ruby.hash_new();
175
+ hash.aset(ruby.to_symbol("type"), ruby.to_symbol("key"))?;
172
176
 
173
177
  let code = match key.code {
174
- crossterm::event::KeyCode::Char(c) => c.to_string(),
175
- crossterm::event::KeyCode::Up => "up".to_string(),
176
- crossterm::event::KeyCode::Down => "down".to_string(),
177
- crossterm::event::KeyCode::Left => "left".to_string(),
178
- crossterm::event::KeyCode::Right => "right".to_string(),
179
- crossterm::event::KeyCode::Enter => "enter".to_string(),
180
- crossterm::event::KeyCode::Esc => "esc".to_string(),
181
- crossterm::event::KeyCode::Backspace => "backspace".to_string(),
182
- crossterm::event::KeyCode::Tab => "tab".to_string(),
178
+ ratatui::crossterm::event::KeyCode::Char(c) => c.to_string(),
179
+ ratatui::crossterm::event::KeyCode::Up => "up".to_string(),
180
+ ratatui::crossterm::event::KeyCode::Down => "down".to_string(),
181
+ ratatui::crossterm::event::KeyCode::Left => "left".to_string(),
182
+ ratatui::crossterm::event::KeyCode::Right => "right".to_string(),
183
+ ratatui::crossterm::event::KeyCode::Enter => "enter".to_string(),
184
+ ratatui::crossterm::event::KeyCode::Esc => "esc".to_string(),
185
+ ratatui::crossterm::event::KeyCode::Backspace => "backspace".to_string(),
186
+ ratatui::crossterm::event::KeyCode::Tab => "tab".to_string(),
183
187
  _ => "unknown".to_string(),
184
188
  };
185
- hash.aset(Symbol::new("code"), code)?;
189
+ hash.aset(ruby.to_symbol("code"), code)?;
186
190
 
187
191
  let mut modifiers = Vec::new();
188
192
  if key
189
193
  .modifiers
190
- .contains(crossterm::event::KeyModifiers::CONTROL)
194
+ .contains(ratatui::crossterm::event::KeyModifiers::CONTROL)
191
195
  {
192
196
  modifiers.push("ctrl");
193
197
  }
194
- if key.modifiers.contains(crossterm::event::KeyModifiers::ALT) {
198
+ if key.modifiers.contains(ratatui::crossterm::event::KeyModifiers::ALT) {
195
199
  modifiers.push("alt");
196
200
  }
197
201
  if key
198
202
  .modifiers
199
- .contains(crossterm::event::KeyModifiers::SHIFT)
203
+ .contains(ratatui::crossterm::event::KeyModifiers::SHIFT)
200
204
  {
201
205
  modifiers.push("shift");
202
206
  }
203
207
  if !modifiers.is_empty() {
204
- hash.aset(Symbol::new("modifiers"), modifiers)?;
208
+ hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
205
209
  }
206
210
 
207
- return Ok(hash.into_value());
211
+ return Ok(hash.into_value_with(&ruby));
208
212
  }
209
213
  }
210
- crossterm::event::Event::Mouse(event) => {
211
- let hash = magnus::RHash::new();
212
- hash.aset(Symbol::new("type"), Symbol::new("mouse"))?;
214
+ ratatui::crossterm::event::Event::Mouse(event) => {
215
+ let ruby = magnus::Ruby::get().unwrap();
216
+ let hash = ruby.hash_new();
217
+ hash.aset(ruby.to_symbol("type"), ruby.to_symbol("mouse"))?;
213
218
 
214
219
  let (kind, button) = match event.kind {
215
- crossterm::event::MouseEventKind::Down(btn) => ("down", btn),
216
- crossterm::event::MouseEventKind::Up(btn) => ("up", btn),
217
- crossterm::event::MouseEventKind::Drag(btn) => ("drag", btn),
218
- crossterm::event::MouseEventKind::Moved => {
219
- ("moved", crossterm::event::MouseButton::Left)
220
+ ratatui::crossterm::event::MouseEventKind::Down(btn) => ("down", btn),
221
+ ratatui::crossterm::event::MouseEventKind::Up(btn) => ("up", btn),
222
+ ratatui::crossterm::event::MouseEventKind::Drag(btn) => ("drag", btn),
223
+ ratatui::crossterm::event::MouseEventKind::Moved => {
224
+ ("moved", ratatui::crossterm::event::MouseButton::Left)
220
225
  } // button is ignored for moved
221
- crossterm::event::MouseEventKind::ScrollDown => {
222
- ("scroll_down", crossterm::event::MouseButton::Left)
226
+ ratatui::crossterm::event::MouseEventKind::ScrollDown => {
227
+ ("scroll_down", ratatui::crossterm::event::MouseButton::Left)
223
228
  } // button is ignored for scroll
224
- crossterm::event::MouseEventKind::ScrollUp => {
225
- ("scroll_up", crossterm::event::MouseButton::Left)
229
+ ratatui::crossterm::event::MouseEventKind::ScrollUp => {
230
+ ("scroll_up", ratatui::crossterm::event::MouseButton::Left)
226
231
  } // button is ignored for scroll
227
- crossterm::event::MouseEventKind::ScrollLeft => {
228
- ("scroll_left", crossterm::event::MouseButton::Left)
232
+ ratatui::crossterm::event::MouseEventKind::ScrollLeft => {
233
+ ("scroll_left", ratatui::crossterm::event::MouseButton::Left)
229
234
  } // button is ignored for scroll
230
- crossterm::event::MouseEventKind::ScrollRight => {
231
- ("scroll_right", crossterm::event::MouseButton::Left)
235
+ ratatui::crossterm::event::MouseEventKind::ScrollRight => {
236
+ ("scroll_right", ratatui::crossterm::event::MouseButton::Left)
232
237
  } // button is ignored for scroll
233
238
  };
234
239
 
235
- hash.aset(Symbol::new("kind"), Symbol::new(kind))?;
240
+ hash.aset(ruby.to_symbol("kind"), ruby.to_symbol(kind))?;
236
241
 
237
242
  if matches!(
238
243
  event.kind,
239
- crossterm::event::MouseEventKind::Down(_)
240
- | crossterm::event::MouseEventKind::Up(_)
241
- | crossterm::event::MouseEventKind::Drag(_)
244
+ ratatui::crossterm::event::MouseEventKind::Down(_)
245
+ | ratatui::crossterm::event::MouseEventKind::Up(_)
246
+ | ratatui::crossterm::event::MouseEventKind::Drag(_)
242
247
  ) {
243
248
  let btn_sym = match button {
244
- crossterm::event::MouseButton::Left => "left",
245
- crossterm::event::MouseButton::Right => "right",
246
- crossterm::event::MouseButton::Middle => "middle",
249
+ ratatui::crossterm::event::MouseButton::Left => "left",
250
+ ratatui::crossterm::event::MouseButton::Right => "right",
251
+ ratatui::crossterm::event::MouseButton::Middle => "middle",
247
252
  };
248
- hash.aset(Symbol::new("button"), Symbol::new(btn_sym))?;
253
+ hash.aset(ruby.to_symbol("button"), ruby.to_symbol(btn_sym))?;
249
254
  } else {
250
- hash.aset(Symbol::new("button"), Symbol::new("none"))?;
255
+ hash.aset(ruby.to_symbol("button"), ruby.to_symbol("none"))?;
251
256
  }
252
257
 
253
- hash.aset(Symbol::new("x"), event.column)?;
254
- hash.aset(Symbol::new("y"), event.row)?;
258
+ hash.aset(ruby.to_symbol("x"), event.column)?;
259
+ hash.aset(ruby.to_symbol("y"), event.row)?;
255
260
 
256
261
  let mut modifiers = Vec::new();
257
262
  if event
258
263
  .modifiers
259
- .contains(crossterm::event::KeyModifiers::CONTROL)
264
+ .contains(ratatui::crossterm::event::KeyModifiers::CONTROL)
260
265
  {
261
266
  modifiers.push("ctrl");
262
267
  }
263
268
  if event
264
269
  .modifiers
265
- .contains(crossterm::event::KeyModifiers::ALT)
270
+ .contains(ratatui::crossterm::event::KeyModifiers::ALT)
266
271
  {
267
272
  modifiers.push("alt");
268
273
  }
269
274
  if event
270
275
  .modifiers
271
- .contains(crossterm::event::KeyModifiers::SHIFT)
276
+ .contains(ratatui::crossterm::event::KeyModifiers::SHIFT)
272
277
  {
273
278
  modifiers.push("shift");
274
279
  }
275
- hash.aset(Symbol::new("modifiers"), modifiers)?;
280
+ hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
276
281
 
277
- return Ok(hash.into_value());
282
+ return Ok(hash.into_value_with(&ruby));
278
283
  }
279
284
  _ => {}
280
285
  }
281
- Ok(magnus::value::qnil().into_value())
286
+ Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
282
287
  }
@@ -1,34 +1,36 @@
1
1
  // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
+ mod buffer;
4
5
  mod events;
5
6
  mod rendering;
6
7
  mod style;
7
8
  mod terminal;
8
9
  mod widgets;
9
10
 
10
- use magnus::{define_module, function, Error, Value};
11
+ use magnus::{function, Class, Error, Module, Value};
11
12
  use terminal::{init_terminal, restore_terminal, TERMINAL};
12
13
 
13
14
  fn draw(tree: Value) -> Result<(), Error> {
15
+ let ruby = magnus::Ruby::get().unwrap();
14
16
  let mut term_lock = TERMINAL.lock().unwrap();
15
17
  if let Some(wrapper) = term_lock.as_mut() {
16
18
  match wrapper {
17
19
  terminal::TerminalWrapper::Crossterm(term) => {
18
20
  term.draw(|f| {
19
- if let Err(e) = rendering::render_node(f, f.size(), tree) {
21
+ if let Err(e) = rendering::render_node(f, f.area(), tree) {
20
22
  eprintln!("Render error: {:?}", e);
21
23
  }
22
24
  })
23
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
25
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
24
26
  }
25
27
  terminal::TerminalWrapper::Test(term) => {
26
28
  term.draw(|f| {
27
- if let Err(e) = rendering::render_node(f, f.size(), tree) {
29
+ if let Err(e) = rendering::render_node(f, f.area(), tree) {
28
30
  eprintln!("Render error: {:?}", e);
29
31
  }
30
32
  })
31
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
33
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
32
34
  }
33
35
  }
34
36
  } else {
@@ -39,7 +41,14 @@ fn draw(tree: Value) -> Result<(), Error> {
39
41
 
40
42
  #[magnus::init]
41
43
  fn init() -> Result<(), Error> {
42
- let m = define_module("RatatuiRuby")?;
44
+ let ruby = magnus::Ruby::get().unwrap();
45
+ let m = ruby.define_module("RatatuiRuby")?;
46
+
47
+ let buffer_class = m.define_class("Buffer", ruby.class_object())?;
48
+ buffer_class.undef_default_alloc_func();
49
+ buffer_class.define_method("set_string", magnus::method!(buffer::BufferWrapper::set_string, 4))?;
50
+ buffer_class.define_method("area", magnus::method!(buffer::BufferWrapper::area, 0))?;
51
+
43
52
  m.define_module_function("init_terminal", function!(init_terminal, 0))?;
44
53
  m.define_module_function("restore_terminal", function!(restore_terminal, 0))?;
45
54
  m.define_module_function("draw", function!(draw, 1))?;
@@ -1,16 +1,31 @@
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::buffer::BufferWrapper;
4
5
  use crate::widgets;
5
6
  use magnus::{prelude::*, Error, Value};
6
7
  use ratatui::{layout::Rect, Frame};
7
8
 
8
9
  pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
10
+ if node.respond_to("render", true)? {
11
+ let wrapper = BufferWrapper::new(frame.buffer_mut());
12
+ let ruby = magnus::Ruby::get().unwrap();
13
+ let ruby_area = {
14
+ let module = ruby.define_module("RatatuiRuby")?;
15
+ let class = module.const_get::<_, magnus::RClass>("Rect")?;
16
+ class.funcall::<_, _, Value>("new", (area.x, area.y, area.width, area.height))?
17
+ };
18
+ let wrapper_obj = ruby.obj_wrap(wrapper);
19
+ node.funcall::<_, _, Value>("render", (ruby_area, wrapper_obj))?;
20
+ return Ok(());
21
+ }
22
+
9
23
  let class = node.class();
10
24
  let class_name = unsafe { class.name() };
11
25
 
12
26
  match class_name.as_ref() {
13
27
  "RatatuiRuby::Paragraph" => widgets::paragraph::render(frame, area, node)?,
28
+ "RatatuiRuby::Clear" => widgets::clear::render(frame, area, node)?,
14
29
  "RatatuiRuby::Cursor" => widgets::cursor::render(frame, area, node)?,
15
30
  "RatatuiRuby::Overlay" => widgets::overlay::render(frame, area, node)?,
16
31
  "RatatuiRuby::Center" => widgets::center::render(frame, area, node)?,
@@ -12,6 +12,7 @@ pub fn parse_color(color_str: &str) -> Option<Color> {
12
12
  }
13
13
 
14
14
  pub fn parse_style(style_val: Value) -> Result<Style, Error> {
15
+ let ruby = magnus::Ruby::get().unwrap();
15
16
  if style_val.is_nil() {
16
17
  return Ok(Style::default());
17
18
  }
@@ -38,7 +39,7 @@ pub fn parse_style(style_val: Value) -> Result<Style, Error> {
38
39
  if !modifiers_val.is_nil() {
39
40
  let modifiers_array = magnus::RArray::from_value(modifiers_val).ok_or_else(|| {
40
41
  Error::new(
41
- magnus::exception::type_error(),
42
+ ruby.exception_type_error(),
42
43
  "expected array for modifiers",
43
44
  )
44
45
  })?;
@@ -19,30 +19,32 @@ lazy_static::lazy_static! {
19
19
  }
20
20
 
21
21
  pub fn init_terminal() -> Result<(), Error> {
22
+ let ruby = magnus::Ruby::get().unwrap();
22
23
  let mut term_lock = TERMINAL.lock().unwrap();
23
24
  if term_lock.is_none() {
24
- crossterm::terminal::enable_raw_mode()
25
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
25
+ ratatui::crossterm::terminal::enable_raw_mode()
26
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
26
27
  let mut stdout = io::stdout();
27
- crossterm::execute!(
28
+ ratatui::crossterm::execute!(
28
29
  stdout,
29
- crossterm::terminal::EnterAlternateScreen,
30
- crossterm::event::EnableMouseCapture
30
+ ratatui::crossterm::terminal::EnterAlternateScreen,
31
+ ratatui::crossterm::event::EnableMouseCapture
31
32
  )
32
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
33
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
33
34
  let backend = CrosstermBackend::new(stdout);
34
35
  let terminal = Terminal::new(backend)
35
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
36
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
36
37
  *term_lock = Some(TerminalWrapper::Crossterm(terminal));
37
38
  }
38
39
  Ok(())
39
40
  }
40
41
 
41
42
  pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
43
+ let ruby = magnus::Ruby::get().unwrap();
42
44
  let mut term_lock = TERMINAL.lock().unwrap();
43
45
  let backend = TestBackend::new(width, height);
44
46
  let terminal = Terminal::new(backend)
45
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
47
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
46
48
  *term_lock = Some(TerminalWrapper::Test(terminal));
47
49
  Ok(())
48
50
  }
@@ -50,17 +52,18 @@ pub fn init_test_terminal(width: u16, height: u16) -> Result<(), Error> {
50
52
  pub fn restore_terminal() -> Result<(), Error> {
51
53
  let mut term_lock = TERMINAL.lock().unwrap();
52
54
  if let Some(TerminalWrapper::Crossterm(mut terminal)) = term_lock.take() {
53
- let _ = crossterm::terminal::disable_raw_mode();
54
- let _ = crossterm::execute!(
55
+ let _ = ratatui::crossterm::terminal::disable_raw_mode();
56
+ let _ = ratatui::crossterm::execute!(
55
57
  terminal.backend_mut(),
56
- crossterm::terminal::LeaveAlternateScreen,
57
- crossterm::event::DisableMouseCapture
58
+ ratatui::crossterm::terminal::LeaveAlternateScreen,
59
+ ratatui::crossterm::event::DisableMouseCapture
58
60
  );
59
61
  }
60
62
  Ok(())
61
63
  }
62
64
 
63
65
  pub fn get_buffer_content() -> Result<String, Error> {
66
+ let ruby = magnus::Ruby::get().unwrap();
64
67
  let term_lock = TERMINAL.lock().unwrap();
65
68
  if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
66
69
  // We need to access the buffer.
@@ -74,7 +77,7 @@ pub fn get_buffer_content() -> Result<String, Error> {
74
77
  let mut result = String::new();
75
78
  for y in 0..area.height {
76
79
  for x in 0..area.width {
77
- let cell = buffer.get(x, y);
80
+ let cell = buffer.cell((x, y)).unwrap();
78
81
  result.push_str(cell.symbol());
79
82
  }
80
83
  result.push('\n');
@@ -82,28 +85,30 @@ pub fn get_buffer_content() -> Result<String, Error> {
82
85
  Ok(result)
83
86
  } else {
84
87
  Err(Error::new(
85
- magnus::exception::runtime_error(),
88
+ ruby.exception_runtime_error(),
86
89
  "Terminal is not initialized as TestBackend",
87
90
  ))
88
91
  }
89
92
  }
90
93
 
91
94
  pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
95
+ let ruby = magnus::Ruby::get().unwrap();
92
96
  let mut term_lock = TERMINAL.lock().unwrap();
93
97
  if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
94
98
  let pos = terminal
95
- .get_cursor()
96
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
97
- Ok(Some(pos))
99
+ .get_cursor_position()
100
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
101
+ Ok(Some(pos.into()))
98
102
  } else {
99
103
  Err(Error::new(
100
- magnus::exception::runtime_error(),
104
+ ruby.exception_runtime_error(),
101
105
  "Terminal is not initialized as TestBackend",
102
106
  ))
103
107
  }
104
108
  }
105
109
 
106
110
  pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
111
+ let ruby = magnus::Ruby::get().unwrap();
107
112
  let mut term_lock = TERMINAL.lock().unwrap();
108
113
  if let Some(wrapper) = term_lock.as_mut() {
109
114
  match wrapper {
@@ -118,7 +123,7 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
118
123
  // We might need to call terminal.resize() too if Ratatui caches the size.
119
124
  if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
120
125
  return Err(Error::new(
121
- magnus::exception::runtime_error(),
126
+ ruby.exception_runtime_error(),
122
127
  e.to_string(),
123
128
  ));
124
129
  }
@@ -12,6 +12,7 @@ use std::convert::TryFrom;
12
12
  use time::{Date, Month};
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 year: i32 = node.funcall("year", ())?;
16
17
  let month_u8: u8 = node.funcall("month", ())?;
17
18
  let day_style_val: Value = node.funcall("day_style", ())?;
@@ -19,10 +20,10 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
19
20
  let block_val: Value = node.funcall("block", ())?;
20
21
 
21
22
  let month = Month::try_from(month_u8)
22
- .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
23
+ .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
23
24
 
24
25
  let date = Date::from_calendar_date(year, month, 1)
25
- .map_err(|e| Error::new(magnus::exception::arg_error(), e.to_string()))?;
26
+ .map_err(|e| Error::new(ruby.exception_arg_error(), e.to_string()))?;
26
27
 
27
28
  let mut calendar = Monthly::new(date, CalendarEventStore::default());
28
29
 
@@ -63,7 +64,7 @@ mod tests {
63
64
  let mut content = String::new();
64
65
  for y in 0..20 {
65
66
  for x in 0..40 {
66
- content.push_str(buf.get(x, y).symbol());
67
+ content.push_str(buf.cell((x, y)).unwrap().symbol());
67
68
  }
68
69
  content.push('\n');
69
70
  }
@@ -36,8 +36,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
36
36
  }
37
37
 
38
38
  let canvas = canvas.paint(|ctx| {
39
- for shape_val in shapes_val.each() {
40
- let shape_val = shape_val.unwrap();
39
+ for shape_val in shapes_val {
41
40
  let class = shape_val.class();
42
41
  let class_name = unsafe { class.name() };
43
42