ratatui_ruby 0.1.0 → 0.3.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 (127) 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.yml +53 -0
  6. data/.pre-commit-config.yaml +9 -2
  7. data/AGENTS.md +53 -5
  8. data/CHANGELOG.md +51 -1
  9. data/README.md +38 -18
  10. data/REUSE.toml +5 -0
  11. data/Rakefile +3 -100
  12. data/{docs → doc}/contributors/index.md +2 -1
  13. data/doc/custom.css +8 -0
  14. data/doc/images/examples-calendar_demo.rb.png +0 -0
  15. data/doc/images/examples-chart_demo.rb.png +0 -0
  16. data/doc/images/examples-custom_widget.rb.png +0 -0
  17. data/doc/images/examples-list_styles.rb.png +0 -0
  18. data/doc/images/examples-popup_demo.rb.gif +0 -0
  19. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  20. data/doc/images/examples-scroll_text.rb.png +0 -0
  21. data/doc/images/examples-stock_ticker.rb.png +0 -0
  22. data/doc/images/examples-table_select.rb.png +0 -0
  23. data/{docs → doc}/index.md +1 -1
  24. data/{docs → doc}/quickstart.md +81 -11
  25. data/examples/analytics.rb +2 -1
  26. data/examples/calendar_demo.rb +55 -0
  27. data/examples/chart_demo.rb +84 -0
  28. data/examples/custom_widget.rb +43 -0
  29. data/examples/list_styles.rb +66 -0
  30. data/examples/login_form.rb +2 -1
  31. data/examples/popup_demo.rb +105 -0
  32. data/examples/quickstart_dsl.rb +30 -0
  33. data/examples/quickstart_lifecycle.rb +40 -0
  34. data/examples/readme_usage.rb +21 -0
  35. data/examples/scroll_text.rb +74 -0
  36. data/examples/stock_ticker.rb +13 -5
  37. data/examples/system_monitor.rb +2 -1
  38. data/examples/table_select.rb +70 -0
  39. data/examples/test_calendar_demo.rb +66 -0
  40. data/examples/test_list_styles.rb +61 -0
  41. data/examples/test_popup_demo.rb +62 -0
  42. data/examples/test_scroll_text.rb +130 -0
  43. data/examples/test_table_select.rb +37 -0
  44. data/ext/ratatui_ruby/.cargo/config.toml +5 -0
  45. data/ext/ratatui_ruby/Cargo.lock +260 -50
  46. data/ext/ratatui_ruby/Cargo.toml +5 -4
  47. data/ext/ratatui_ruby/extconf.rb +1 -1
  48. data/ext/ratatui_ruby/src/buffer.rs +54 -0
  49. data/ext/ratatui_ruby/src/events.rs +115 -107
  50. data/ext/ratatui_ruby/src/lib.rs +15 -6
  51. data/ext/ratatui_ruby/src/rendering.rs +18 -1
  52. data/ext/ratatui_ruby/src/style.rs +2 -1
  53. data/ext/ratatui_ruby/src/terminal.rs +27 -24
  54. data/ext/ratatui_ruby/src/widgets/calendar.rs +82 -0
  55. data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
  56. data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
  57. data/ext/ratatui_ruby/src/widgets/chart.rs +260 -0
  58. data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
  59. data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
  60. data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
  61. data/ext/ratatui_ruby/src/widgets/list.rs +44 -5
  62. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -1
  63. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  64. data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
  65. data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
  66. data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
  67. data/lib/ratatui_ruby/dsl.rb +64 -0
  68. data/lib/ratatui_ruby/schema/calendar.rb +26 -0
  69. data/lib/ratatui_ruby/schema/chart.rb +81 -0
  70. data/lib/ratatui_ruby/schema/clear.rb +83 -0
  71. data/lib/ratatui_ruby/schema/list.rb +8 -2
  72. data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
  73. data/lib/ratatui_ruby/schema/rect.rb +24 -0
  74. data/lib/ratatui_ruby/schema/table.rb +8 -2
  75. data/lib/ratatui_ruby/version.rb +1 -1
  76. data/lib/ratatui_ruby.rb +24 -2
  77. data/mise.toml +8 -0
  78. data/sig/ratatui_ruby/buffer.rbs +11 -0
  79. data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
  80. data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
  81. data/sig/ratatui_ruby/schema/list.rbs +4 -1
  82. data/sig/ratatui_ruby/schema/rect.rbs +14 -0
  83. data/tasks/bump/cargo_lockfile.rb +19 -0
  84. data/tasks/bump/changelog.rb +37 -0
  85. data/tasks/bump/comparison_links.rb +41 -0
  86. data/tasks/bump/header.rb +30 -0
  87. data/tasks/bump/history.rb +30 -0
  88. data/tasks/bump/manifest.rb +31 -0
  89. data/tasks/bump/ruby_gem.rb +35 -0
  90. data/tasks/bump/sem_ver.rb +34 -0
  91. data/tasks/bump/unreleased_section.rb +38 -0
  92. data/tasks/bump.rake +49 -0
  93. data/tasks/doc.rake +25 -0
  94. data/tasks/extension.rake +12 -0
  95. data/tasks/lint.rake +49 -0
  96. data/tasks/rdoc_config.rb +15 -0
  97. data/tasks/resources/build.yml.erb +65 -0
  98. data/tasks/resources/index.html.erb +38 -0
  99. data/tasks/resources/rubies.yml +7 -0
  100. data/tasks/sourcehut.rake +38 -0
  101. data/tasks/test.rake +31 -0
  102. data/tasks/website/index_page.rb +28 -0
  103. data/tasks/website/version.rb +117 -0
  104. data/tasks/website/version_menu.rb +68 -0
  105. data/tasks/website/versioned_documentation.rb +49 -0
  106. data/tasks/website/website.rb +53 -0
  107. data/tasks/website.rake +26 -0
  108. metadata +119 -28
  109. data/.build.yml +0 -34
  110. data/.ruby-version +0 -1
  111. data/CODE_OF_CONDUCT.md +0 -30
  112. data/CONTRIBUTING.md +0 -40
  113. data/docs/images/examples-stock_ticker.rb.png +0 -0
  114. data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
  115. data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
  116. /data/{docs → doc}/application_testing.md +0 -0
  117. /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
  118. /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
  119. /data/{docs → doc}/contributors/design.md +0 -0
  120. /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
  121. /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
  122. /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
  123. /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
  124. /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
  125. /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
  126. /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
  127. /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() {
@@ -142,138 +144,144 @@ pub fn poll_event() -> Result<Value, Error> {
142
144
  // Check if we are in test mode. If so, don't poll crossterm.
143
145
  let is_test_mode = {
144
146
  let term_lock = crate::terminal::TERMINAL.lock().unwrap();
145
- matches!(term_lock.as_ref(), Some(crate::terminal::TerminalWrapper::Test(_)))
147
+ matches!(
148
+ term_lock.as_ref(),
149
+ Some(crate::terminal::TerminalWrapper::Test(_))
150
+ )
146
151
  };
147
152
 
148
153
  if is_test_mode {
149
- return Ok(magnus::value::qnil().into_value());
154
+ return Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()));
150
155
  }
151
156
 
152
- if crossterm::event::poll(std::time::Duration::from_millis(16))
153
- .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()))?
154
159
  {
155
- let event = crossterm::event::read()
156
- .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()))?;
157
162
  handle_event(event)
158
163
  } else {
159
- Ok(magnus::value::qnil().into_value())
164
+ Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
160
165
  }
161
166
  }
162
167
 
163
- 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();
164
170
  match event {
165
- crossterm::event::Event::Key(key) => {
166
- if key.kind == crossterm::event::KeyEventKind::Press {
167
- let hash = magnus::RHash::new();
168
- 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"))?;
169
176
 
170
177
  let code = match key.code {
171
- crossterm::event::KeyCode::Char(c) => c.to_string(),
172
- crossterm::event::KeyCode::Up => "up".to_string(),
173
- crossterm::event::KeyCode::Down => "down".to_string(),
174
- crossterm::event::KeyCode::Left => "left".to_string(),
175
- crossterm::event::KeyCode::Right => "right".to_string(),
176
- crossterm::event::KeyCode::Enter => "enter".to_string(),
177
- crossterm::event::KeyCode::Esc => "esc".to_string(),
178
- crossterm::event::KeyCode::Backspace => "backspace".to_string(),
179
- 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(),
180
187
  _ => "unknown".to_string(),
181
188
  };
182
- hash.aset(Symbol::new("code"), code)?;
189
+ hash.aset(ruby.to_symbol("code"), code)?;
183
190
 
184
191
  let mut modifiers = Vec::new();
185
192
  if key
186
193
  .modifiers
187
- .contains(crossterm::event::KeyModifiers::CONTROL)
194
+ .contains(ratatui::crossterm::event::KeyModifiers::CONTROL)
188
195
  {
189
196
  modifiers.push("ctrl");
190
197
  }
191
- if key.modifiers.contains(crossterm::event::KeyModifiers::ALT) {
198
+ if key.modifiers.contains(ratatui::crossterm::event::KeyModifiers::ALT) {
192
199
  modifiers.push("alt");
193
200
  }
194
201
  if key
195
202
  .modifiers
196
- .contains(crossterm::event::KeyModifiers::SHIFT)
203
+ .contains(ratatui::crossterm::event::KeyModifiers::SHIFT)
197
204
  {
198
205
  modifiers.push("shift");
199
206
  }
200
207
  if !modifiers.is_empty() {
201
- hash.aset(Symbol::new("modifiers"), modifiers)?;
208
+ hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
202
209
  }
203
210
 
204
- return Ok(hash.into_value());
211
+ return Ok(hash.into_value_with(&ruby));
205
212
  }
206
213
  }
207
- crossterm::event::Event::Mouse(event) => {
208
- let hash = magnus::RHash::new();
209
- 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"))?;
210
218
 
211
219
  let (kind, button) = match event.kind {
212
- crossterm::event::MouseEventKind::Down(btn) => ("down", btn),
213
- crossterm::event::MouseEventKind::Up(btn) => ("up", btn),
214
- crossterm::event::MouseEventKind::Drag(btn) => ("drag", btn),
215
- crossterm::event::MouseEventKind::Moved => {
216
- ("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)
217
225
  } // button is ignored for moved
218
- crossterm::event::MouseEventKind::ScrollDown => {
219
- ("scroll_down", crossterm::event::MouseButton::Left)
226
+ ratatui::crossterm::event::MouseEventKind::ScrollDown => {
227
+ ("scroll_down", ratatui::crossterm::event::MouseButton::Left)
220
228
  } // button is ignored for scroll
221
- crossterm::event::MouseEventKind::ScrollUp => {
222
- ("scroll_up", crossterm::event::MouseButton::Left)
229
+ ratatui::crossterm::event::MouseEventKind::ScrollUp => {
230
+ ("scroll_up", ratatui::crossterm::event::MouseButton::Left)
223
231
  } // button is ignored for scroll
224
- crossterm::event::MouseEventKind::ScrollLeft => {
225
- ("scroll_left", crossterm::event::MouseButton::Left)
232
+ ratatui::crossterm::event::MouseEventKind::ScrollLeft => {
233
+ ("scroll_left", ratatui::crossterm::event::MouseButton::Left)
226
234
  } // button is ignored for scroll
227
- crossterm::event::MouseEventKind::ScrollRight => {
228
- ("scroll_right", crossterm::event::MouseButton::Left)
235
+ ratatui::crossterm::event::MouseEventKind::ScrollRight => {
236
+ ("scroll_right", ratatui::crossterm::event::MouseButton::Left)
229
237
  } // button is ignored for scroll
230
238
  };
231
239
 
232
- hash.aset(Symbol::new("kind"), Symbol::new(kind))?;
240
+ hash.aset(ruby.to_symbol("kind"), ruby.to_symbol(kind))?;
233
241
 
234
242
  if matches!(
235
243
  event.kind,
236
- crossterm::event::MouseEventKind::Down(_)
237
- | crossterm::event::MouseEventKind::Up(_)
238
- | crossterm::event::MouseEventKind::Drag(_)
244
+ ratatui::crossterm::event::MouseEventKind::Down(_)
245
+ | ratatui::crossterm::event::MouseEventKind::Up(_)
246
+ | ratatui::crossterm::event::MouseEventKind::Drag(_)
239
247
  ) {
240
248
  let btn_sym = match button {
241
- crossterm::event::MouseButton::Left => "left",
242
- crossterm::event::MouseButton::Right => "right",
243
- 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",
244
252
  };
245
- hash.aset(Symbol::new("button"), Symbol::new(btn_sym))?;
253
+ hash.aset(ruby.to_symbol("button"), ruby.to_symbol(btn_sym))?;
246
254
  } else {
247
- hash.aset(Symbol::new("button"), Symbol::new("none"))?;
255
+ hash.aset(ruby.to_symbol("button"), ruby.to_symbol("none"))?;
248
256
  }
249
257
 
250
- hash.aset(Symbol::new("x"), event.column)?;
251
- 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)?;
252
260
 
253
261
  let mut modifiers = Vec::new();
254
262
  if event
255
263
  .modifiers
256
- .contains(crossterm::event::KeyModifiers::CONTROL)
264
+ .contains(ratatui::crossterm::event::KeyModifiers::CONTROL)
257
265
  {
258
266
  modifiers.push("ctrl");
259
267
  }
260
268
  if event
261
269
  .modifiers
262
- .contains(crossterm::event::KeyModifiers::ALT)
270
+ .contains(ratatui::crossterm::event::KeyModifiers::ALT)
263
271
  {
264
272
  modifiers.push("alt");
265
273
  }
266
274
  if event
267
275
  .modifiers
268
- .contains(crossterm::event::KeyModifiers::SHIFT)
276
+ .contains(ratatui::crossterm::event::KeyModifiers::SHIFT)
269
277
  {
270
278
  modifiers.push("shift");
271
279
  }
272
- hash.aset(Symbol::new("modifiers"), modifiers)?;
280
+ hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
273
281
 
274
- return Ok(hash.into_value());
282
+ return Ok(hash.into_value_with(&ruby));
275
283
  }
276
284
  _ => {}
277
285
  }
278
- Ok(magnus::value::qnil().into_value())
286
+ Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
279
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,30 @@
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_area = {
13
+ let ruby = magnus::Ruby::get().unwrap();
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
+ node.funcall::<_, _, Value>("render", (ruby_area, wrapper))?;
19
+ return Ok(());
20
+ }
21
+
9
22
  let class = node.class();
10
23
  let class_name = unsafe { class.name() };
11
24
 
12
25
  match class_name.as_ref() {
13
26
  "RatatuiRuby::Paragraph" => widgets::paragraph::render(frame, area, node)?,
27
+ "RatatuiRuby::Clear" => widgets::clear::render(frame, area, node)?,
14
28
  "RatatuiRuby::Cursor" => widgets::cursor::render(frame, area, node)?,
15
29
  "RatatuiRuby::Overlay" => widgets::overlay::render(frame, area, node)?,
16
30
  "RatatuiRuby::Center" => widgets::center::render(frame, area, node)?,
@@ -23,8 +37,11 @@ pub fn render_node(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Err
23
37
  "RatatuiRuby::Scrollbar" => widgets::scrollbar::render(frame, area, node)?,
24
38
  "RatatuiRuby::BarChart" => widgets::barchart::render(frame, area, node)?,
25
39
  "RatatuiRuby::Canvas" => widgets::canvas::render(frame, area, node)?,
40
+ "RatatuiRuby::Calendar" => widgets::calendar::render(frame, area, node)?,
26
41
  "RatatuiRuby::Sparkline" => widgets::sparkline::render(frame, area, node)?,
27
- "RatatuiRuby::LineChart" => widgets::linechart::render(frame, area, node)?,
42
+ "RatatuiRuby::Chart" | "RatatuiRuby::LineChart" => {
43
+ widgets::chart::render(frame, area, node)?
44
+ }
28
45
  _ => {}
29
46
  }
30
47
  Ok(())
@@ -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,50 +19,51 @@ 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
- if term_lock.is_none() {
44
- let backend = TestBackend::new(width, height);
45
- let terminal = Terminal::new(backend)
46
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
47
- *term_lock = Some(TerminalWrapper::Test(terminal));
48
- }
45
+ let backend = TestBackend::new(width, height);
46
+ let terminal = Terminal::new(backend)
47
+ .map_err(|e| Error::new(ruby.exception_runtime_error(), e.to_string()))?;
48
+ *term_lock = Some(TerminalWrapper::Test(terminal));
49
49
  Ok(())
50
50
  }
51
51
 
52
52
  pub fn restore_terminal() -> Result<(), Error> {
53
53
  let mut term_lock = TERMINAL.lock().unwrap();
54
54
  if let Some(TerminalWrapper::Crossterm(mut terminal)) = term_lock.take() {
55
- let _ = crossterm::terminal::disable_raw_mode();
56
- let _ = crossterm::execute!(
55
+ let _ = ratatui::crossterm::terminal::disable_raw_mode();
56
+ let _ = ratatui::crossterm::execute!(
57
57
  terminal.backend_mut(),
58
- crossterm::terminal::LeaveAlternateScreen,
59
- crossterm::event::DisableMouseCapture
58
+ ratatui::crossterm::terminal::LeaveAlternateScreen,
59
+ ratatui::crossterm::event::DisableMouseCapture
60
60
  );
61
61
  }
62
62
  Ok(())
63
63
  }
64
64
 
65
65
  pub fn get_buffer_content() -> Result<String, Error> {
66
+ let ruby = magnus::Ruby::get().unwrap();
66
67
  let term_lock = TERMINAL.lock().unwrap();
67
68
  if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
68
69
  // We need to access the buffer.
@@ -76,7 +77,7 @@ pub fn get_buffer_content() -> Result<String, Error> {
76
77
  let mut result = String::new();
77
78
  for y in 0..area.height {
78
79
  for x in 0..area.width {
79
- let cell = buffer.get(x, y);
80
+ let cell = buffer.cell((x, y)).unwrap();
80
81
  result.push_str(cell.symbol());
81
82
  }
82
83
  result.push('\n');
@@ -84,28 +85,30 @@ pub fn get_buffer_content() -> Result<String, Error> {
84
85
  Ok(result)
85
86
  } else {
86
87
  Err(Error::new(
87
- magnus::exception::runtime_error(),
88
+ ruby.exception_runtime_error(),
88
89
  "Terminal is not initialized as TestBackend",
89
90
  ))
90
91
  }
91
92
  }
92
93
 
93
94
  pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
95
+ let ruby = magnus::Ruby::get().unwrap();
94
96
  let mut term_lock = TERMINAL.lock().unwrap();
95
97
  if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
96
98
  let pos = terminal
97
- .get_cursor()
98
- .map_err(|e| Error::new(magnus::exception::runtime_error(), e.to_string()))?;
99
- 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()))
100
102
  } else {
101
103
  Err(Error::new(
102
- magnus::exception::runtime_error(),
104
+ ruby.exception_runtime_error(),
103
105
  "Terminal is not initialized as TestBackend",
104
106
  ))
105
107
  }
106
108
  }
107
109
 
108
110
  pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
111
+ let ruby = magnus::Ruby::get().unwrap();
109
112
  let mut term_lock = TERMINAL.lock().unwrap();
110
113
  if let Some(wrapper) = term_lock.as_mut() {
111
114
  match wrapper {
@@ -120,7 +123,7 @@ pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
120
123
  // We might need to call terminal.resize() too if Ratatui caches the size.
121
124
  if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
122
125
  return Err(Error::new(
123
- magnus::exception::runtime_error(),
126
+ ruby.exception_runtime_error(),
124
127
  e.to_string(),
125
128
  ));
126
129
  }