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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +52 -0
- data/.builds/ruby-3.3.yml +52 -0
- data/.builds/ruby-3.4.yml +52 -0
- data/.builds/ruby-4.0.0.yml +53 -0
- data/.pre-commit-config.yaml +9 -2
- data/AGENTS.md +53 -5
- data/CHANGELOG.md +51 -1
- data/README.md +38 -18
- data/REUSE.toml +5 -0
- data/Rakefile +3 -100
- data/{docs → doc}/contributors/index.md +2 -1
- data/doc/custom.css +8 -0
- data/doc/images/examples-calendar_demo.rb.png +0 -0
- data/doc/images/examples-chart_demo.rb.png +0 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-list_styles.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-stock_ticker.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 +81 -11
- data/examples/analytics.rb +2 -1
- data/examples/calendar_demo.rb +55 -0
- data/examples/chart_demo.rb +84 -0
- data/examples/custom_widget.rb +43 -0
- data/examples/list_styles.rb +66 -0
- data/examples/login_form.rb +2 -1
- data/examples/popup_demo.rb +105 -0
- data/examples/quickstart_dsl.rb +30 -0
- data/examples/quickstart_lifecycle.rb +40 -0
- data/examples/readme_usage.rb +21 -0
- data/examples/scroll_text.rb +74 -0
- data/examples/stock_ticker.rb +13 -5
- data/examples/system_monitor.rb +2 -1
- data/examples/table_select.rb +70 -0
- data/examples/test_calendar_demo.rb +66 -0
- data/examples/test_list_styles.rb +61 -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/config.toml +5 -0
- data/ext/ratatui_ruby/Cargo.lock +260 -50
- data/ext/ratatui_ruby/Cargo.toml +5 -4
- data/ext/ratatui_ruby/extconf.rb +1 -1
- data/ext/ratatui_ruby/src/buffer.rs +54 -0
- data/ext/ratatui_ruby/src/events.rs +115 -107
- data/ext/ratatui_ruby/src/lib.rs +15 -6
- data/ext/ratatui_ruby/src/rendering.rs +18 -1
- data/ext/ratatui_ruby/src/style.rs +2 -1
- data/ext/ratatui_ruby/src/terminal.rs +27 -24
- data/ext/ratatui_ruby/src/widgets/calendar.rs +82 -0
- 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 +260 -0
- 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 +44 -5
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -1
- 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 +64 -0
- data/lib/ratatui_ruby/schema/calendar.rb +26 -0
- data/lib/ratatui_ruby/schema/chart.rb +81 -0
- data/lib/ratatui_ruby/schema/clear.rb +83 -0
- data/lib/ratatui_ruby/schema/list.rb +8 -2
- 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 +24 -2
- data/mise.toml +8 -0
- data/sig/ratatui_ruby/buffer.rbs +11 -0
- data/sig/ratatui_ruby/schema/calendar.rbs +13 -0
- data/sig/ratatui_ruby/schema/{line_chart.rbs → chart.rbs} +20 -1
- data/sig/ratatui_ruby/schema/list.rbs +4 -1
- data/sig/ratatui_ruby/schema/rect.rbs +14 -0
- data/tasks/bump/cargo_lockfile.rb +19 -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 +31 -0
- data/tasks/bump/ruby_gem.rb +35 -0
- data/tasks/bump/sem_ver.rb +34 -0
- data/tasks/bump/unreleased_section.rb +38 -0
- data/tasks/bump.rake +49 -0
- data/tasks/doc.rake +25 -0
- data/tasks/extension.rake +12 -0
- data/tasks/lint.rake +49 -0
- data/tasks/rdoc_config.rb +15 -0
- data/tasks/resources/build.yml.erb +65 -0
- data/tasks/resources/index.html.erb +38 -0
- data/tasks/resources/rubies.yml +7 -0
- data/tasks/sourcehut.rake +38 -0
- data/tasks/test.rake +31 -0
- data/tasks/website/index_page.rb +28 -0
- data/tasks/website/version.rb +117 -0
- data/tasks/website/version_menu.rb +68 -0
- data/tasks/website/versioned_documentation.rb +49 -0
- data/tasks/website/website.rb +53 -0
- data/tasks/website.rake +26 -0
- metadata +119 -28
- data/.build.yml +0 -34
- data/.ruby-version +0 -1
- data/CODE_OF_CONDUCT.md +0 -30
- data/CONTRIBUTING.md +0 -40
- data/docs/images/examples-stock_ticker.rb.png +0 -0
- data/ext/ratatui_ruby/src/widgets/linechart.rs +0 -154
- data/lib/ratatui_ruby/schema/line_chart.rb +0 -41
- /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-dashboard.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-scrollbar_demo.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() {
|
|
@@ -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!(
|
|
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::
|
|
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(
|
|
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(
|
|
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::
|
|
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
|
|
168
|
-
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"))?;
|
|
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(
|
|
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(
|
|
208
|
+
hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
|
|
202
209
|
}
|
|
203
210
|
|
|
204
|
-
|
|
211
|
+
return Ok(hash.into_value_with(&ruby));
|
|
205
212
|
}
|
|
206
213
|
}
|
|
207
|
-
crossterm::event::Event::Mouse(event) => {
|
|
208
|
-
let
|
|
209
|
-
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"))?;
|
|
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(
|
|
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(
|
|
253
|
+
hash.aset(ruby.to_symbol("button"), ruby.to_symbol(btn_sym))?;
|
|
246
254
|
} else {
|
|
247
|
-
hash.aset(
|
|
255
|
+
hash.aset(ruby.to_symbol("button"), ruby.to_symbol("none"))?;
|
|
248
256
|
}
|
|
249
257
|
|
|
250
|
-
hash.aset(
|
|
251
|
-
hash.aset(
|
|
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(
|
|
280
|
+
hash.aset(ruby.to_symbol("modifiers"), modifiers)?;
|
|
273
281
|
|
|
274
|
-
return Ok(hash.
|
|
282
|
+
return Ok(hash.into_value_with(&ruby));
|
|
275
283
|
}
|
|
276
284
|
_ => {}
|
|
277
285
|
}
|
|
278
|
-
Ok(magnus::
|
|
286
|
+
Ok(ruby.qnil().into_value_with(&magnus::Ruby::get().unwrap()))
|
|
279
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,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::
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
.
|
|
98
|
-
.map_err(|e| Error::new(
|
|
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
|
-
|
|
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
|
-
|
|
126
|
+
ruby.exception_runtime_error(),
|
|
124
127
|
e.to_string(),
|
|
125
128
|
));
|
|
126
129
|
}
|