ratatui_ruby 0.10.1 → 0.10.2
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 +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/CHANGELOG.md +24 -0
- data/doc/concepts/application_architecture.md +2 -2
- data/doc/concepts/application_testing.md +1 -1
- data/doc/concepts/custom_widgets.md +2 -2
- data/doc/contributors/todo/align/api_completeness_audit-finished.md +375 -0
- data/doc/contributors/todo/align/api_completeness_audit-unfinished.md +206 -0
- data/doc/contributors/todo/align/terminal.md +647 -0
- data/doc/getting_started/quickstart.md +41 -41
- data/doc/images/app_cli_rich_moments.gif +0 -0
- data/examples/app_cli_rich_moments/README.md +81 -0
- data/examples/app_cli_rich_moments/app.rb +189 -0
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/frame.rs +17 -4
- data/ext/ratatui_ruby/src/lib.rs +17 -3
- data/ext/ratatui_ruby/src/lib.rs.bak +286 -0
- data/ext/ratatui_ruby/src/rendering.rs +38 -25
- data/ext/ratatui_ruby/src/rendering.rs.bak +152 -0
- data/ext/ratatui_ruby/src/terminal.rs +245 -33
- data/ext/ratatui_ruby/src/terminal.rs.bak +381 -0
- data/ext/ratatui_ruby/src/terminal.rs.orig +409 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/block.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/calendar.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/canvas.rs +7 -4
- data/ext/ratatui_ruby/src/widgets/center.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/chart.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/clear.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/cursor.rs +10 -7
- data/ext/ratatui_ruby/src/widgets/gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/layout.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -9
- data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -3
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +5 -6
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +4 -4
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +8 -4
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +10 -10
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +4 -3
- data/ext/ratatui_ruby/src/widgets/table.rs +6 -6
- data/ext/ratatui_ruby/src/widgets/tabs.rs +4 -3
- data/lib/ratatui_ruby/labs/a11y.rb +173 -0
- data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
- data/lib/ratatui_ruby/labs.rb +47 -0
- data/lib/ratatui_ruby/layout/position.rb +26 -0
- data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +164 -6
- data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
- data/lib/ratatui_ruby/test_helper/terminal.rb +8 -1
- data/lib/ratatui_ruby/tui/core.rb +16 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +82 -3
- data/migrate_to_buffer.rb +145 -0
- data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
- data/sig/ratatui_ruby/labs.rbs +87 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +12 -4
- data/sig/ratatui_ruby/terminal/viewport.rbs +19 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +13 -5
- data/sig/ratatui_ruby/tui/core.rbs +3 -0
- metadata +21 -2
- /data/doc/contributors/{future_work.md → todo/future_work.md} +0 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
use magnus::value::ReprValue;
|
|
5
|
+
use magnus::{Error, Module};
|
|
6
|
+
use ratatui::{
|
|
7
|
+
backend::{CrosstermBackend, TestBackend},
|
|
8
|
+
Terminal, TerminalOptions, Viewport,
|
|
9
|
+
};
|
|
10
|
+
use std::io;
|
|
11
|
+
use std::sync::Mutex;
|
|
12
|
+
|
|
13
|
+
pub enum TerminalWrapper {
|
|
14
|
+
Crossterm(Terminal<CrosstermBackend<io::Stdout>>),
|
|
15
|
+
Test(Terminal<TestBackend>),
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
pub static TERMINAL: Mutex<Option<TerminalWrapper>> = Mutex::new(None);
|
|
19
|
+
|
|
20
|
+
pub fn init_terminal(
|
|
21
|
+
focus_events: bool,
|
|
22
|
+
bracketed_paste: bool,
|
|
23
|
+
viewport_type: String,
|
|
24
|
+
viewport_height: Option<u16>,
|
|
25
|
+
) -> Result<(), Error> {
|
|
26
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
27
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
28
|
+
if term_lock.is_none() {
|
|
29
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
30
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
31
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
32
|
+
|
|
33
|
+
// Parse viewport type
|
|
34
|
+
let viewport = match viewport_type.as_str() {
|
|
35
|
+
"inline" => {
|
|
36
|
+
let height = viewport_height.unwrap_or(8);
|
|
37
|
+
Viewport::Inline(height)
|
|
38
|
+
}
|
|
39
|
+
_ => Viewport::Fullscreen,
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
ratatui::crossterm::terminal::enable_raw_mode()
|
|
43
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
44
|
+
let mut stdout = io::stdout();
|
|
45
|
+
|
|
46
|
+
// Only enter alternate screen for fullscreen viewports
|
|
47
|
+
if matches!(viewport, Viewport::Fullscreen) {
|
|
48
|
+
ratatui::crossterm::execute!(
|
|
49
|
+
stdout,
|
|
50
|
+
ratatui::crossterm::terminal::EnterAlternateScreen
|
|
51
|
+
)
|
|
52
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableMouseCapture)
|
|
56
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
57
|
+
|
|
58
|
+
if focus_events {
|
|
59
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
|
|
60
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
61
|
+
}
|
|
62
|
+
if bracketed_paste {
|
|
63
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
|
|
64
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let backend = CrosstermBackend::new(stdout);
|
|
68
|
+
let options = TerminalOptions { viewport };
|
|
69
|
+
let terminal = Terminal::with_options(backend, options)
|
|
70
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
71
|
+
*term_lock = Some(TerminalWrapper::Crossterm(terminal));
|
|
72
|
+
}
|
|
73
|
+
Ok(())
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
pub fn init_test_terminal(
|
|
77
|
+
width: u16,
|
|
78
|
+
height: u16,
|
|
79
|
+
viewport_type: String,
|
|
80
|
+
viewport_height: Option<u16>,
|
|
81
|
+
) -> Result<(), Error> {
|
|
82
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
83
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
84
|
+
let backend = TestBackend::new(width, height);
|
|
85
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
86
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
87
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
88
|
+
|
|
89
|
+
// Parse viewport type (same as init_terminal)
|
|
90
|
+
let viewport = match viewport_type.as_str() {
|
|
91
|
+
"inline" => {
|
|
92
|
+
let vp_height = viewport_height.unwrap_or(height);
|
|
93
|
+
Viewport::Inline(vp_height)
|
|
94
|
+
}
|
|
95
|
+
_ => Viewport::Fullscreen,
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
let options = TerminalOptions { viewport };
|
|
99
|
+
let terminal = Terminal::with_options(backend, options)
|
|
100
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
101
|
+
*term_lock = Some(TerminalWrapper::Test(terminal));
|
|
102
|
+
Ok(())
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
pub fn restore_terminal() {
|
|
106
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
107
|
+
if let Some(wrapper) = term_lock.take() {
|
|
108
|
+
match wrapper {
|
|
109
|
+
TerminalWrapper::Crossterm(mut t) => {
|
|
110
|
+
let _ = ratatui::crossterm::terminal::disable_raw_mode();
|
|
111
|
+
let _ = ratatui::crossterm::execute!(
|
|
112
|
+
t.backend_mut(),
|
|
113
|
+
ratatui::crossterm::terminal::LeaveAlternateScreen,
|
|
114
|
+
ratatui::crossterm::event::DisableMouseCapture,
|
|
115
|
+
ratatui::crossterm::event::DisableFocusChange,
|
|
116
|
+
ratatui::crossterm::event::DisableBracketedPaste
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
TerminalWrapper::Test(_) => {}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
pub fn get_buffer_content() -> Result<String, Error> {
|
|
125
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
126
|
+
let term_lock = TERMINAL.lock().unwrap();
|
|
127
|
+
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
|
|
128
|
+
let buffer = terminal.backend().buffer();
|
|
129
|
+
let area = buffer.area;
|
|
130
|
+
let mut result = String::new();
|
|
131
|
+
for y in 0..area.height {
|
|
132
|
+
for x in 0..area.width {
|
|
133
|
+
let cell = buffer.cell((x, y)).unwrap();
|
|
134
|
+
result.push_str(cell.symbol());
|
|
135
|
+
}
|
|
136
|
+
result.push('\n');
|
|
137
|
+
}
|
|
138
|
+
Ok(result)
|
|
139
|
+
} else {
|
|
140
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
141
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
142
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
143
|
+
Err(Error::new(
|
|
144
|
+
error_class,
|
|
145
|
+
"Terminal is not initialized as TestBackend",
|
|
146
|
+
))
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
pub fn insert_before(height: u16, _widget: magnus::Value) -> Result<(), Error> {
|
|
151
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
152
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
153
|
+
|
|
154
|
+
if let Some(wrapper) = term_lock.as_mut() {
|
|
155
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
156
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
157
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
158
|
+
|
|
159
|
+
match wrapper {
|
|
160
|
+
TerminalWrapper::Crossterm(term) => {
|
|
161
|
+
term.insert_before(height, |_buf| {})
|
|
162
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
163
|
+
}
|
|
164
|
+
TerminalWrapper::Test(term) => {
|
|
165
|
+
term.insert_before(height, |_buf| {})
|
|
166
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
Ok(())
|
|
170
|
+
} else {
|
|
171
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
172
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
173
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
174
|
+
Err(Error::new(error_class, "Terminal not initialized"))
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
pub fn get_terminal_area() -> Result<magnus::RHash, Error> {
|
|
179
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
180
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
181
|
+
if let Some(wrapper) = term_lock.as_mut() {
|
|
182
|
+
let hash = ruby.hash_new();
|
|
183
|
+
match wrapper {
|
|
184
|
+
TerminalWrapper::Crossterm(term) => {
|
|
185
|
+
// Call draw to capture the viewport area
|
|
186
|
+
// draw() closure must return (), so we capture area in a mutable variable
|
|
187
|
+
let mut captured_area = ratatui::layout::Rect::default();
|
|
188
|
+
term.draw(|f| {
|
|
189
|
+
captured_area = f.area();
|
|
190
|
+
}).map_err(|e| {
|
|
191
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
192
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
193
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
194
|
+
Error::new(error_class, e.to_string())
|
|
195
|
+
})?;
|
|
196
|
+
hash.aset("x", captured_area.x)?;
|
|
197
|
+
hash.aset("y", captured_area.y)?;
|
|
198
|
+
hash.aset("width", captured_area.width)?;
|
|
199
|
+
hash.aset("height", captured_area.height)?;
|
|
200
|
+
}
|
|
201
|
+
TerminalWrapper::Test(term) => {
|
|
202
|
+
// Same for TestBackend
|
|
203
|
+
let mut captured_area = ratatui::layout::Rect::default();
|
|
204
|
+
term.draw(|f| {
|
|
205
|
+
captured_area = f.area();
|
|
206
|
+
}).map_err(|e| {
|
|
207
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
208
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
209
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
210
|
+
Error::new(error_class, e.to_string())
|
|
211
|
+
})?;
|
|
212
|
+
hash.aset("x", captured_area.x)?;
|
|
213
|
+
hash.aset("y", captured_area.y)?;
|
|
214
|
+
hash.aset("width", captured_area.width)?;
|
|
215
|
+
hash.aset("height", captured_area.height)?;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
Ok(hash)
|
|
219
|
+
} else {
|
|
220
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
221
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
222
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
223
|
+
Err(Error::new(error_class, "Terminal is not initialized"))
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
pub fn get_viewport_type() -> Result<String, Error> {
|
|
228
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
229
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
230
|
+
|
|
231
|
+
if let Some(wrapper) = term_lock.as_mut() {
|
|
232
|
+
match wrapper {
|
|
233
|
+
TerminalWrapper::Crossterm(term) => {
|
|
234
|
+
let vp_area = term.draw(|f| f.area()).map_err(|e| {
|
|
235
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
236
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
237
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
238
|
+
Error::new(error_class, e.to_string())
|
|
239
|
+
})?;
|
|
240
|
+
let backend_size = term.size().unwrap_or_default();
|
|
241
|
+
|
|
242
|
+
if vp_area.height < backend_size.height {
|
|
243
|
+
Ok("inline".to_string())
|
|
244
|
+
} else {
|
|
245
|
+
Ok("fullscreen".to_string())
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
TerminalWrapper::Test(term) => {
|
|
249
|
+
let vp_area = term.draw(|f| f.area()).map_err(|e| {
|
|
250
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
251
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
252
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
253
|
+
Error::new(error_class, e.to_string())
|
|
254
|
+
})?;
|
|
255
|
+
let backend_size = term.size().unwrap_or_default();
|
|
256
|
+
|
|
257
|
+
if vp_area.height < backend_size.height {
|
|
258
|
+
Ok("inline".to_string())
|
|
259
|
+
} else {
|
|
260
|
+
Ok("fullscreen".to_string())
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
} else {
|
|
265
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
266
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
267
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
268
|
+
Err(Error::new(error_class, "Terminal not initialized"))
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
pub fn get_cursor_position() -> Result<Option<(u16, u16)>, Error> {
|
|
273
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
274
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
275
|
+
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_mut() {
|
|
276
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
277
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
278
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
279
|
+
let pos = terminal
|
|
280
|
+
.get_cursor_position()
|
|
281
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
282
|
+
Ok(Some(pos.into()))
|
|
283
|
+
} else {
|
|
284
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
285
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
286
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
287
|
+
Err(Error::new(
|
|
288
|
+
error_class,
|
|
289
|
+
"Terminal is not initialized as TestBackend",
|
|
290
|
+
))
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
pub fn resize_terminal(width: u16, height: u16) -> Result<(), Error> {
|
|
295
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
296
|
+
let mut term_lock = TERMINAL.lock().unwrap();
|
|
297
|
+
if let Some(wrapper) = term_lock.as_mut() {
|
|
298
|
+
match wrapper {
|
|
299
|
+
TerminalWrapper::Crossterm(_) => {}
|
|
300
|
+
TerminalWrapper::Test(terminal) => {
|
|
301
|
+
terminal.backend_mut().resize(width, height);
|
|
302
|
+
if let Err(e) = terminal.resize(ratatui::layout::Rect::new(0, 0, width, height)) {
|
|
303
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
304
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
305
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
306
|
+
return Err(Error::new(error_class, e.to_string()));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
Ok(())
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
use magnus::Value;
|
|
315
|
+
|
|
316
|
+
pub fn get_cell_at(x: u16, y: u16) -> Result<magnus::RHash, Error> {
|
|
317
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
318
|
+
let term_lock = TERMINAL.lock().unwrap();
|
|
319
|
+
if let Some(TerminalWrapper::Test(terminal)) = term_lock.as_ref() {
|
|
320
|
+
let buffer = terminal.backend().buffer();
|
|
321
|
+
if let Some(cell) = buffer.cell((x, y)) {
|
|
322
|
+
let hash = ruby.hash_new();
|
|
323
|
+
hash.aset("char", cell.symbol())?;
|
|
324
|
+
hash.aset("fg", color_to_value(cell.fg))?;
|
|
325
|
+
hash.aset("bg", color_to_value(cell.bg))?;
|
|
326
|
+
hash.aset("underline_color", color_to_value(cell.underline_color))?;
|
|
327
|
+
hash.aset("modifiers", modifiers_to_value(cell.modifier))?;
|
|
328
|
+
Ok(hash)
|
|
329
|
+
} else {
|
|
330
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
331
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
332
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
333
|
+
Err(Error::new(
|
|
334
|
+
error_class,
|
|
335
|
+
format!("Coordinates ({x}, {y}) out of bounds"),
|
|
336
|
+
))
|
|
337
|
+
}
|
|
338
|
+
} else {
|
|
339
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
340
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
341
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
342
|
+
Err(Error::new(
|
|
343
|
+
error_class,
|
|
344
|
+
"Terminal is not initialized as TestBackend",
|
|
345
|
+
))
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
fn color_to_value(color: ratatui::style::Color) -> Value {
|
|
350
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
351
|
+
match color {
|
|
352
|
+
ratatui::style::Color::Reset => ruby.qnil().as_value(),
|
|
353
|
+
ratatui::style::Color::Black => ruby.to_symbol("black").as_value(),
|
|
354
|
+
ratatui::style::Color::Red => ruby.to_symbol("red").as_value(),
|
|
355
|
+
ratatui::style::Color::Green => ruby.to_symbol("green").as_value(),
|
|
356
|
+
ratatui::style::Color::Yellow => ruby.to_symbol("yellow").as_value(),
|
|
357
|
+
ratatui::style::Color::Blue => ruby.to_symbol("blue").as_value(),
|
|
358
|
+
ratatui::style::Color::Magenta => ruby.to_symbol("magenta").as_value(),
|
|
359
|
+
ratatui::style::Color::Cyan => ruby.to_symbol("cyan").as_value(),
|
|
360
|
+
ratatui::style::Color::Gray => ruby.to_symbol("gray").as_value(),
|
|
361
|
+
ratatui::style::Color::DarkGray => ruby.to_symbol("dark_gray").as_value(),
|
|
362
|
+
ratatui::style::Color::LightRed => ruby.to_symbol("light_red").as_value(),
|
|
363
|
+
ratatui::style::Color::LightGreen => ruby.to_symbol("light_green").as_value(),
|
|
364
|
+
ratatui::style::Color::LightYellow => ruby.to_symbol("light_yellow").as_value(),
|
|
365
|
+
ratatui::style::Color::LightBlue => ruby.to_symbol("light_blue").as_value(),
|
|
366
|
+
ratatui::style::Color::LightMagenta => ruby.to_symbol("light_magenta").as_value(),
|
|
367
|
+
ratatui::style::Color::LightCyan => ruby.to_symbol("light_cyan").as_value(),
|
|
368
|
+
ratatui::style::Color::White => ruby.to_symbol("white").as_value(),
|
|
369
|
+
ratatui::style::Color::Rgb(r, g, b) => ruby
|
|
370
|
+
.str_new(&(format!("#{r:02x}{g:02x}{b:02x}")))
|
|
371
|
+
.as_value(),
|
|
372
|
+
ratatui::style::Color::Indexed(i) => ruby.to_symbol(format!("indexed_{i}")).as_value(),
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
fn modifiers_to_value(modifier: ratatui::style::Modifier) -> Value {
|
|
377
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
378
|
+
let ary = ruby.ary_new();
|
|
379
|
+
|
|
380
|
+
if modifier.contains(ratatui::style::Modifier::BOLD) {
|
|
381
|
+
let _ = ary.push(ruby.to_symbol("bold"));
|
|
382
|
+
}
|
|
383
|
+
if modifier.contains(ratatui::style::Modifier::ITALIC) {
|
|
384
|
+
let _ = ary.push(ruby.to_symbol("italic"));
|
|
385
|
+
}
|
|
386
|
+
if modifier.contains(ratatui::style::Modifier::DIM) {
|
|
387
|
+
let _ = ary.push(ruby.to_symbol("dim"));
|
|
388
|
+
}
|
|
389
|
+
if modifier.contains(ratatui::style::Modifier::UNDERLINED) {
|
|
390
|
+
let _ = ary.push(ruby.to_symbol("underlined"));
|
|
391
|
+
}
|
|
392
|
+
if modifier.contains(ratatui::style::Modifier::REVERSED) {
|
|
393
|
+
let _ = ary.push(ruby.to_symbol("reversed"));
|
|
394
|
+
}
|
|
395
|
+
if modifier.contains(ratatui::style::Modifier::HIDDEN) {
|
|
396
|
+
let _ = ary.push(ruby.to_symbol("hidden"));
|
|
397
|
+
}
|
|
398
|
+
if modifier.contains(ratatui::style::Modifier::CROSSED_OUT) {
|
|
399
|
+
let _ = ary.push(ruby.to_symbol("crossed_out"));
|
|
400
|
+
}
|
|
401
|
+
if modifier.contains(ratatui::style::Modifier::SLOW_BLINK) {
|
|
402
|
+
let _ = ary.push(ruby.to_symbol("slow_blink"));
|
|
403
|
+
}
|
|
404
|
+
if modifier.contains(ratatui::style::Modifier::RAPID_BLINK) {
|
|
405
|
+
let _ = ary.push(ruby.to_symbol("rapid_blink"));
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ary.as_value()
|
|
409
|
+
}
|
|
@@ -5,15 +5,16 @@ use crate::style::{parse_bar_set, parse_block, parse_style};
|
|
|
5
5
|
use crate::text::{parse_line, parse_span};
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, RArray, Symbol, Value};
|
|
8
|
+
use ratatui::buffer::Buffer;
|
|
8
9
|
use ratatui::{
|
|
9
10
|
layout::{Direction, Rect},
|
|
10
11
|
text::Line,
|
|
12
|
+
widgets::Widget,
|
|
11
13
|
widgets::{Bar, BarChart, BarGroup},
|
|
12
|
-
Frame,
|
|
13
14
|
};
|
|
14
15
|
|
|
15
16
|
#[allow(clippy::too_many_lines)]
|
|
16
|
-
pub fn render(
|
|
17
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
17
18
|
let bump = Bump::new();
|
|
18
19
|
let data_val: Value = node.funcall("data", ())?;
|
|
19
20
|
let bar_width: u16 = node.funcall("bar_width", ())?;
|
|
@@ -144,7 +145,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
144
145
|
bar_chart = bar_chart.bar_set(parse_bar_set(bar_set_val, &bump)?);
|
|
145
146
|
}
|
|
146
147
|
|
|
147
|
-
|
|
148
|
+
bar_chart.render(area, buffer);
|
|
148
149
|
Ok(())
|
|
149
150
|
}
|
|
150
151
|
|
|
@@ -5,15 +5,15 @@ use crate::rendering::render_node;
|
|
|
5
5
|
use crate::style::parse_block;
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, Value};
|
|
8
|
-
use ratatui::{layout::Rect, widgets::Widget
|
|
8
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
9
9
|
|
|
10
|
-
pub fn render(
|
|
10
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
11
|
let bump = Bump::new();
|
|
12
12
|
let block = parse_block(node, &bump)?;
|
|
13
13
|
let block_clone = block.clone();
|
|
14
14
|
|
|
15
15
|
// Render the block itself (borders, styling)
|
|
16
|
-
block_clone.render(area,
|
|
16
|
+
block_clone.render(area, buffer);
|
|
17
17
|
|
|
18
18
|
// Get children and render them within the block's inner area
|
|
19
19
|
let children_val: Value = node.funcall("children", ())?;
|
|
@@ -30,7 +30,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
30
30
|
let index = isize::try_from(i)
|
|
31
31
|
.map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
|
|
32
32
|
let child: Value = arr.entry(index)?;
|
|
33
|
-
if let Err(e) = render_node(
|
|
33
|
+
if let Err(e) = render_node(buffer, inner, child) {
|
|
34
34
|
eprintln!("Error rendering block child {i}: {e:?}");
|
|
35
35
|
}
|
|
36
36
|
}
|
|
@@ -5,14 +5,15 @@ use crate::style::{parse_block, parse_style};
|
|
|
5
5
|
use bumpalo::Bump;
|
|
6
6
|
use magnus::{prelude::*, Error, Value};
|
|
7
7
|
use ratatui::{
|
|
8
|
+
buffer::Buffer,
|
|
8
9
|
layout::Rect,
|
|
9
10
|
widgets::calendar::{CalendarEventStore, Monthly},
|
|
10
|
-
|
|
11
|
+
widgets::Widget,
|
|
11
12
|
};
|
|
12
13
|
use std::convert::TryFrom;
|
|
13
14
|
use time::{Date, Month};
|
|
14
15
|
|
|
15
|
-
pub fn render(
|
|
16
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
16
17
|
let bump = Bump::new();
|
|
17
18
|
let ruby = magnus::Ruby::get().unwrap();
|
|
18
19
|
let year: i32 = node.funcall("year", ())?;
|
|
@@ -78,6 +79,6 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
78
79
|
calendar = calendar.block(parse_block(block_val, &bump)?);
|
|
79
80
|
}
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
calendar.render(area, buffer);
|
|
82
83
|
Ok(())
|
|
83
84
|
}
|
|
@@ -5,14 +5,17 @@ use crate::style::{parse_block, parse_color, parse_style};
|
|
|
5
5
|
use crate::text::parse_text;
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, RArray, Symbol, Value};
|
|
8
|
+
use ratatui::buffer::Buffer;
|
|
8
9
|
use ratatui::{
|
|
9
10
|
symbols::Marker,
|
|
10
|
-
widgets::
|
|
11
|
-
|
|
11
|
+
widgets::{
|
|
12
|
+
canvas::{Canvas, Circle, Line, Map, MapResolution, Rectangle},
|
|
13
|
+
Widget,
|
|
14
|
+
},
|
|
12
15
|
};
|
|
13
16
|
|
|
14
17
|
#[allow(clippy::too_many_lines)]
|
|
15
|
-
pub fn render(
|
|
18
|
+
pub fn render(buffer: &mut Buffer, area: ratatui::layout::Rect, node: Value) -> Result<(), Error> {
|
|
16
19
|
let bump = Bump::new();
|
|
17
20
|
let shapes_val: RArray = node.funcall("shapes", ())?;
|
|
18
21
|
let x_bounds_val: RArray = node.funcall("x_bounds", ())?;
|
|
@@ -139,7 +142,7 @@ pub fn render(frame: &mut Frame, area: ratatui::layout::Rect, node: Value) -> Re
|
|
|
139
142
|
}
|
|
140
143
|
});
|
|
141
144
|
|
|
142
|
-
|
|
145
|
+
Widget::render(canvas, area, buffer);
|
|
143
146
|
Ok(())
|
|
144
147
|
}
|
|
145
148
|
|
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
use crate::rendering::render_node;
|
|
5
5
|
use magnus::{prelude::*, Error, Value};
|
|
6
6
|
use ratatui::{
|
|
7
|
+
buffer::Buffer,
|
|
7
8
|
layout::{Constraint, Direction, Layout, Rect},
|
|
8
|
-
Frame,
|
|
9
9
|
};
|
|
10
10
|
|
|
11
|
-
pub fn render(
|
|
11
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
12
12
|
let child: Value = node.funcall("child", ())?;
|
|
13
13
|
let width_percent: u16 = node.funcall("width_percent", ())?;
|
|
14
14
|
let height_percent: u16 = node.funcall("height_percent", ())?;
|
|
@@ -35,7 +35,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
35
35
|
|
|
36
36
|
let center_area = popup_layout_horizontal[1];
|
|
37
37
|
|
|
38
|
-
render_node(
|
|
38
|
+
render_node(buffer, center_area, child)?;
|
|
39
39
|
Ok(())
|
|
40
40
|
}
|
|
41
41
|
|
|
@@ -6,15 +6,15 @@ use crate::style::{parse_block, parse_style};
|
|
|
6
6
|
use crate::text::parse_line;
|
|
7
7
|
use bumpalo::Bump;
|
|
8
8
|
use magnus::{prelude::*, Error, Symbol, Value};
|
|
9
|
+
use ratatui::buffer::Buffer;
|
|
9
10
|
use ratatui::{
|
|
10
11
|
layout::Rect,
|
|
11
12
|
symbols,
|
|
12
|
-
widgets::{Axis, Chart, Dataset, GraphType, LegendPosition},
|
|
13
|
-
Frame,
|
|
13
|
+
widgets::{Axis, Chart, Dataset, GraphType, LegendPosition, Widget},
|
|
14
14
|
};
|
|
15
15
|
|
|
16
16
|
#[allow(clippy::too_many_lines)]
|
|
17
|
-
pub fn render(
|
|
17
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
18
18
|
let bump = Bump::new();
|
|
19
19
|
let ruby = magnus::Ruby::get().unwrap();
|
|
20
20
|
|
|
@@ -133,7 +133,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
-
|
|
136
|
+
Widget::render(chart, area, buffer);
|
|
137
137
|
Ok(())
|
|
138
138
|
}
|
|
139
139
|
|
|
@@ -3,17 +3,17 @@
|
|
|
3
3
|
|
|
4
4
|
use bumpalo::Bump;
|
|
5
5
|
use magnus::{prelude::*, Error, Value};
|
|
6
|
-
use ratatui::{layout::Rect, widgets::Widget
|
|
6
|
+
use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
|
|
7
7
|
|
|
8
|
-
pub fn render(
|
|
9
|
-
|
|
8
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
9
|
+
ratatui::widgets::Clear.render(area, buffer);
|
|
10
10
|
|
|
11
11
|
// If a block is provided, render it on top of the cleared area
|
|
12
12
|
if let Ok(block_val) = node.funcall::<_, _, Value>("block", ()) {
|
|
13
13
|
if !block_val.is_nil() {
|
|
14
14
|
let bump = Bump::new();
|
|
15
15
|
let block = crate::style::parse_block(block_val, &bump)?;
|
|
16
|
-
block.render(area,
|
|
16
|
+
block.render(area, buffer);
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
@@ -22,7 +22,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
22
22
|
|
|
23
23
|
#[cfg(test)]
|
|
24
24
|
mod tests {
|
|
25
|
-
use ratatui::{backend::TestBackend, layout::Rect, Terminal};
|
|
25
|
+
use ratatui::{backend::TestBackend, layout::Rect, widgets::Widget, Terminal};
|
|
26
26
|
|
|
27
27
|
#[test]
|
|
28
28
|
fn test_clear_renders_without_error() {
|
|
@@ -32,7 +32,7 @@ mod tests {
|
|
|
32
32
|
terminal
|
|
33
33
|
.draw(|frame| {
|
|
34
34
|
let area = Rect::new(0, 0, 10, 5);
|
|
35
|
-
|
|
35
|
+
ratatui::widgets::Clear.render(area, frame.buffer_mut());
|
|
36
36
|
})
|
|
37
37
|
.unwrap();
|
|
38
38
|
}
|
|
@@ -1,19 +1,22 @@
|
|
|
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::{
|
|
5
|
-
use ratatui::{layout::Rect
|
|
4
|
+
use magnus::{Error, Value};
|
|
5
|
+
use ratatui::{buffer::Buffer, layout::Rect};
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
/// Cursor widget requires Frame for `set_cursor_position` - cannot be rendered to buffer alone
|
|
8
|
+
/// This is a no-op when rendering to buffer directly (e.g., in `insert_before`)
|
|
9
|
+
/// For Frame-based rendering, use `frame.set_cursor_position()` directly
|
|
10
|
+
#[allow(dead_code, clippy::unnecessary_wraps)]
|
|
11
|
+
pub fn render(_buffer: &mut Buffer, _area: Rect, _node: Value) -> Result<(), Error> {
|
|
12
|
+
// Cursor positioning requires Frame.set_cursor_position(), not Buffer
|
|
13
|
+
// This is intentionally a no-op for buffer-only rendering
|
|
14
|
+
// The Frame wrapper in frame.rs should handle Cursor widgets specially
|
|
11
15
|
Ok(())
|
|
12
16
|
}
|
|
13
17
|
|
|
14
18
|
#[cfg(test)]
|
|
15
19
|
mod tests {
|
|
16
|
-
|
|
17
20
|
use ratatui::layout::Rect;
|
|
18
21
|
|
|
19
22
|
#[test]
|
|
@@ -5,9 +5,10 @@ use crate::style::{parse_block, parse_style};
|
|
|
5
5
|
use crate::text::parse_span;
|
|
6
6
|
use bumpalo::Bump;
|
|
7
7
|
use magnus::{prelude::*, Error, Value};
|
|
8
|
-
use ratatui::
|
|
8
|
+
use ratatui::buffer::Buffer;
|
|
9
|
+
use ratatui::{layout::Rect, widgets::Gauge, widgets::Widget};
|
|
9
10
|
|
|
10
|
-
pub fn render(
|
|
11
|
+
pub fn render(buffer: &mut Buffer, area: Rect, node: Value) -> Result<(), Error> {
|
|
11
12
|
let bump = Bump::new();
|
|
12
13
|
let ratio: f64 = node.funcall("ratio", ())?;
|
|
13
14
|
let label_val: Value = node.funcall("label", ())?;
|
|
@@ -40,7 +41,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
|
|
|
40
41
|
gauge = gauge.block(parse_block(block_val, &bump)?);
|
|
41
42
|
}
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
gauge.render(area, buffer);
|
|
44
45
|
Ok(())
|
|
45
46
|
}
|
|
46
47
|
|