ratatui_ruby 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +2 -2
- data/.builds/ruby-3.3.yml +2 -2
- data/.builds/ruby-3.4.yml +2 -2
- data/.builds/{ruby-4.0.0-preview3.yml → ruby-4.0.0.yml} +8 -9
- data/.pre-commit-config.yaml +9 -2
- data/AGENTS.md +59 -4
- data/CHANGELOG.md +58 -1
- data/README.md +6 -6
- data/REUSE.toml +1 -6
- data/{docs → doc}/contributors/index.md +2 -1
- data/doc/custom.css +8 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/{docs → doc}/index.md +1 -1
- data/{docs → doc}/quickstart.md +24 -0
- data/examples/custom_widget.rb +43 -0
- data/examples/popup_demo.rb +105 -0
- data/examples/scroll_text.rb +74 -0
- data/examples/table_select.rb +70 -0
- data/examples/test_popup_demo.rb +62 -0
- data/examples/test_scroll_text.rb +130 -0
- data/examples/test_table_select.rb +37 -0
- data/ext/ratatui_ruby/Cargo.lock +167 -50
- data/ext/ratatui_ruby/Cargo.toml +4 -4
- data/ext/ratatui_ruby/src/buffer.rs +54 -0
- data/ext/ratatui_ruby/src/events.rs +111 -106
- data/ext/ratatui_ruby/src/lib.rs +15 -6
- data/ext/ratatui_ruby/src/rendering.rs +15 -0
- data/ext/ratatui_ruby/src/style.rs +2 -1
- data/ext/ratatui_ruby/src/terminal.rs +24 -19
- data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +1 -2
- data/ext/ratatui_ruby/src/widgets/center.rs +0 -2
- data/ext/ratatui_ruby/src/widgets/chart.rs +11 -4
- data/ext/ratatui_ruby/src/widgets/clear.rs +37 -0
- data/ext/ratatui_ruby/src/widgets/cursor.rs +1 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -4
- data/ext/ratatui_ruby/src/widgets/mod.rs +1 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +10 -0
- data/ext/ratatui_ruby/src/widgets/table.rs +25 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +2 -1
- data/lib/ratatui_ruby/dsl.rb +2 -0
- data/lib/ratatui_ruby/schema/clear.rb +83 -0
- data/lib/ratatui_ruby/schema/paragraph.rb +7 -4
- data/lib/ratatui_ruby/schema/rect.rb +24 -0
- data/lib/ratatui_ruby/schema/table.rb +8 -2
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +3 -1
- data/mise.toml +1 -1
- data/sig/ratatui_ruby/buffer.rbs +11 -0
- data/sig/ratatui_ruby/schema/rect.rbs +14 -0
- data/tasks/bump/changelog.rb +37 -0
- data/tasks/bump/comparison_links.rb +41 -0
- data/tasks/bump/header.rb +30 -0
- data/tasks/bump/history.rb +30 -0
- data/tasks/bump/manifest.rb +8 -0
- data/tasks/bump/ruby_gem.rb +6 -10
- data/tasks/bump/sem_ver.rb +6 -0
- data/tasks/bump/unreleased_section.rb +38 -0
- data/tasks/bump.rake +5 -1
- data/tasks/doc.rake +5 -4
- data/tasks/resources/build.yml.erb +1 -14
- data/tasks/resources/rubies.yml +1 -1
- data/tasks/sourcehut.rake +11 -2
- data/tasks/website/version.rb +1 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +2 -1
- data/tasks/website/website.rb +4 -1
- data/tasks/website.rake +3 -3
- metadata +76 -26
- data/CODE_OF_CONDUCT.md +0 -30
- data/CONTRIBUTING.md +0 -40
- /data/{docs → doc}/application_testing.md +0 -0
- /data/{docs → doc}/contributors/design/ruby_frontend.md +0 -0
- /data/{docs → doc}/contributors/design/rust_backend.md +0 -0
- /data/{docs → doc}/contributors/design.md +0 -0
- /data/{docs → doc}/images/examples-analytics.rb.png +0 -0
- /data/{docs → doc}/images/examples-box_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-calendar_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-chart_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-dashboard.rb.png +0 -0
- /data/{docs → doc}/images/examples-list_styles.rb.png +0 -0
- /data/{docs → doc}/images/examples-login_form.rb.png +0 -0
- /data/{docs → doc}/images/examples-map_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-mouse_events.rb.png +0 -0
- /data/{docs → doc}/images/examples-quickstart_lifecycle.rb.png +0 -0
- /data/{docs → doc}/images/examples-scrollbar_demo.rb.png +0 -0
- /data/{docs → doc}/images/examples-stock_ticker.rb.png +0 -0
- /data/{docs → doc}/images/examples-system_monitor.rb.png +0 -0
|
@@ -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,
|
|
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(
|
|
15
|
+
let code_val: Value = data.get(ruby.to_symbol("code")).ok_or_else(|| {
|
|
15
16
|
Error::new(
|
|
16
|
-
|
|
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(
|
|
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(
|
|
51
|
+
let kind_val: Value = data.get(ruby.to_symbol("kind")).ok_or_else(|| {
|
|
51
52
|
Error::new(
|
|
52
|
-
|
|
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(
|
|
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(
|
|
70
|
-
Error::new(
|
|
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(
|
|
75
|
-
Error::new(
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
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::
|
|
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(
|
|
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(
|
|
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::
|
|
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
|
|
171
|
-
hash.
|
|
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(
|
|
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(
|
|
208
|
+
hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
|
|
205
209
|
}
|
|
206
210
|
|
|
207
|
-
|
|
211
|
+
return Ok(hash.into_value_with(&ruby));
|
|
208
212
|
}
|
|
209
213
|
}
|
|
210
|
-
crossterm::event::Event::Mouse(event) => {
|
|
211
|
-
let
|
|
212
|
-
hash.
|
|
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(
|
|
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(
|
|
253
|
+
hash.aset(ruby.to_symbol("button"), ruby.to_symbol(btn_sym))?;
|
|
249
254
|
} else {
|
|
250
|
-
hash.aset(
|
|
255
|
+
hash.aset(ruby.to_symbol("button"), ruby.to_symbol("none"))?;
|
|
251
256
|
}
|
|
252
257
|
|
|
253
|
-
hash.aset(
|
|
254
|
-
hash.aset(
|
|
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(
|
|
280
|
+
hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
|
|
276
281
|
|
|
277
|
-
return Ok(hash.
|
|
282
|
+
return Ok(hash.into_value_with(&ruby));
|
|
278
283
|
}
|
|
279
284
|
_ => {}
|
|
280
285
|
}
|
|
281
|
-
Ok(magnus::
|
|
286
|
+
Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
|
|
282
287
|
}
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -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::{
|
|
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.
|
|
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(
|
|
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.
|
|
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(
|
|
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
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
-
|
|
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
|
-
.
|
|
96
|
-
.map_err(|e| Error::new(
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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.
|
|
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
|
|
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
|
|