ratatui_ruby 1.0.0.pre.beta.2 → 1.0.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 +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 +10 -7
- data/README.md +1 -4
- data/README.rdoc +1 -1
- data/ext/ratatui_ruby/Cargo.lock +1 -1
- data/ext/ratatui_ruby/Cargo.toml +1 -1
- data/ext/ratatui_ruby/src/events.rs +10 -14
- data/ext/ratatui_ruby/src/lib.rs +61 -50
- data/ext/ratatui_ruby/src/terminal/init.rs +141 -0
- data/ext/ratatui_ruby/src/terminal/mod.rs +33 -0
- data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
- data/ext/ratatui_ruby/src/terminal/queries.rs +216 -0
- data/ext/ratatui_ruby/src/terminal/query.rs +338 -0
- data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
- data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
- data/lib/ratatui_ruby/event/key/navigation.rb +6 -0
- data/lib/ratatui_ruby/version.rb +1 -1
- metadata +9 -7
- data/ext/ratatui_ruby/src/lib.rs.bak +0 -286
- data/ext/ratatui_ruby/src/rendering.rs.bak +0 -152
- data/ext/ratatui_ruby/src/terminal.rs +0 -491
- data/ext/ratatui_ruby/src/terminal.rs.bak +0 -381
- data/ext/ratatui_ruby/src/terminal.rs.orig +0 -409
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c8ee9b7928a67c0f332a1fdfd2870972ba9f37c6219879d427a5225f71a24b54
|
|
4
|
+
data.tar.gz: 42ef63b4ca0dafb3220034af83a1d5d0acab49123378e0c5909b21c9baf60f52
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68ec1dcf33fa00b9bb53852d5cc4c92c680f10924b13594854c34bc3e597db6214603c1513427cad35e31895d08cc7dd59e2ac1166dcd44f525043588b23407d
|
|
7
|
+
data.tar.gz: c9cfbe2d111ca45cfe003fc28dea7d23f12dac8c14424701a8532d964d905e80d2fbdd2fdb2a749a291a23ef4e168e2ab0fac14c55ef874b5e1b357d75f4b66f
|
data/.builds/ruby-3.2.yml
CHANGED
data/.builds/ruby-3.3.yml
CHANGED
data/.builds/ruby-3.4.yml
CHANGED
data/.builds/ruby-4.0.0.yml
CHANGED
data/CHANGELOG.md
CHANGED
|
@@ -18,21 +18,22 @@ This project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.htm
|
|
|
18
18
|
|
|
19
19
|
### Removed
|
|
20
20
|
|
|
21
|
-
## [1.0.0
|
|
21
|
+
## [1.0.0] - 2026-01-25
|
|
22
|
+
This release is functionally equivalent to v0.10.3, with additional bug fixes.
|
|
23
|
+
The version bump signals the beginning of the 1.0 release series.
|
|
22
24
|
|
|
23
25
|
### Added
|
|
24
26
|
|
|
25
27
|
### Changed
|
|
26
28
|
|
|
27
29
|
### Fixed
|
|
28
|
-
|
|
30
|
+
- **back_tab? DWIM**: `back_tab?` and `backtab?` predicates now match events with code "back_tab" regardless of whether the shift modifier is explicitly present, since back_tab semantically implies shift.
|
|
31
|
+
- **Terminal Queries During Draw (Deadlock)**: Calling `viewport_area`, `terminal_size`, `get_cell_at`, `poll_event`, `get_cursor_position`, or `get_viewport_type` from inside a `draw()` block previously caused the application to freeze forever. The thread blocked on a Mutex. Ruby's `Timeout` could not interrupt it. The only recovery was `kill -9`. These reads now work via a snapshot captured before rendering.
|
|
32
|
+
- **Terminal Mutations During Draw (Deadlock)**: Calling `insert_before`, `set_cursor_position`, or `resize_terminal` from inside a `draw()` block previously caused the same silent freeze. These writes now raise `Error::Invariant` with a clear message.
|
|
33
|
+
- **Nested Draw Calls (Deadlock)**: Calling `draw()` from inside another `draw()` block previously froze the application. Nested draws now raise `Error::Invariant`.
|
|
34
|
+
- **Event Injection During Draw (Deadlock)**: Calling `inject_test_event` from inside a `draw()` block previously froze the application. Event injection now works correctly during draw.
|
|
29
35
|
- **TableState Row Navigation Methods**: Added missing row navigation methods (`select_next`, `select_previous`, `select_first`, `select_last`) that should have been included alongside the column navigation methods added in v0.10.0. These methods match `ListState`'s navigation API and are used in the `app_stateful_interaction` example.
|
|
30
36
|
|
|
31
|
-
### Removed
|
|
32
|
-
|
|
33
|
-
## [1.0.0-beta.1] - 2026-01-20
|
|
34
|
-
This release is functionally equivalent to v0.10.3. The version bump signals the beginning of the 1.0 release series.
|
|
35
|
-
|
|
36
37
|
## [0.10.3] - 2026-01-16
|
|
37
38
|
|
|
38
39
|
### Added
|
|
@@ -682,6 +683,8 @@ This release is functionally equivalent to v0.10.3. The version bump signals the
|
|
|
682
683
|
- **Testing Support**: Included `RatatuiRuby::TestHelper` and RSpec integration to make testing your TUI applications possible.
|
|
683
684
|
|
|
684
685
|
[Unreleased]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/HEAD
|
|
686
|
+
[1.0.0]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0
|
|
687
|
+
[1.0.0-beta.3]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0-beta.3
|
|
685
688
|
[1.0.0-beta.2]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0-beta.2
|
|
686
689
|
[1.0.0-beta.1]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v1.0.0-beta.1
|
|
687
690
|
[0.10.3]: https://git.sr.ht/~kerrick/ratatui_ruby/refs/v0.10.3
|
data/README.md
CHANGED
|
@@ -20,9 +20,6 @@ Mailing List: Announcements](https://img.shields.io/badge/mailing_list-announcem
|
|
|
20
20
|
**ratatui_ruby** is a Ruby wrapper for [Ratatui](https://ratatui.rs). It allows you to cook up Terminal User Interfaces in Ruby.
|
|
21
21
|
**ratatui_ruby** is a community wrapper that is not affiliated with [the Ratatui team](https://github.com/orgs/ratatui/people).
|
|
22
22
|
|
|
23
|
-
> [!WARNING]
|
|
24
|
-
> **ratatui_ruby** is currently in **BETA**. Please report any bugs you find!
|
|
25
|
-
|
|
26
23
|
**[Why RatatuiRuby?](./doc/getting_started/why.md)** — Native Rust performance, zero runtime overhead, and Ruby's expressiveness. [See how we compare](./doc/getting_started/why.md) to CharmRuby, raw Rust, and Go.
|
|
27
24
|
|
|
28
25
|
Please join the **announce** mailing list at https://lists.sr.ht/~kerrick/ratatui_ruby-announce to stay up-to-date on new releases and announcements. See the [`trunk` branch](https://git.sr.ht/~kerrick/ratatui_ruby/tree/trunk) for pre-release updates.
|
|
@@ -91,7 +88,7 @@ Or install it yourself with:
|
|
|
91
88
|
SPDX-License-Identifier: MIT-0
|
|
92
89
|
-->
|
|
93
90
|
```bash
|
|
94
|
-
gem install ratatui_ruby
|
|
91
|
+
gem install ratatui_ruby
|
|
95
92
|
```
|
|
96
93
|
<!-- SPDX-SnippetEnd -->
|
|
97
94
|
|
data/README.rdoc
CHANGED
|
@@ -5,7 +5,7 @@ RatatuiRuby[https://rubygems.org/gems/ratatui_ruby] is a RubyGem built on
|
|
|
5
5
|
Ratatui[https://ratatui.rs], a leading TUI library written in
|
|
6
6
|
Rust[https://rust-lang.org]. You get native performance with the joy of Ruby.
|
|
7
7
|
|
|
8
|
-
gem install ratatui_ruby
|
|
8
|
+
gem install ratatui_ruby
|
|
9
9
|
|
|
10
10
|
{rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html]
|
|
11
11
|
|
data/ext/ratatui_ruby/Cargo.lock
CHANGED
data/ext/ratatui_ruby/Cargo.toml
CHANGED
|
@@ -2,9 +2,11 @@
|
|
|
2
2
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
3
|
|
|
4
4
|
use magnus::{Error, IntoValue, TryConvert, Value};
|
|
5
|
-
use std::
|
|
5
|
+
use std::cell::RefCell;
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
thread_local! {
|
|
8
|
+
static EVENT_QUEUE: RefCell<Vec<ratatui::crossterm::event::Event>> = const { RefCell::new(Vec::new()) };
|
|
9
|
+
}
|
|
8
10
|
|
|
9
11
|
#[allow(clippy::needless_pass_by_value)]
|
|
10
12
|
pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(), Error> {
|
|
@@ -24,7 +26,7 @@ pub fn inject_test_event(event_type: String, data: magnus::RHash) -> Result<(),
|
|
|
24
26
|
}
|
|
25
27
|
};
|
|
26
28
|
|
|
27
|
-
EVENT_QUEUE.
|
|
29
|
+
EVENT_QUEUE.with(|q| q.borrow_mut().push(event));
|
|
28
30
|
Ok(())
|
|
29
31
|
}
|
|
30
32
|
|
|
@@ -271,30 +273,24 @@ fn parse_paste_event(
|
|
|
271
273
|
}
|
|
272
274
|
|
|
273
275
|
pub fn clear_events() {
|
|
274
|
-
EVENT_QUEUE.
|
|
276
|
+
EVENT_QUEUE.with(|q| q.borrow_mut().clear());
|
|
275
277
|
}
|
|
276
278
|
|
|
277
279
|
pub fn poll_event(ruby: &magnus::Ruby, timeout_val: Option<f64>) -> Result<Value, Error> {
|
|
278
|
-
let event = {
|
|
279
|
-
let mut queue =
|
|
280
|
+
let event = EVENT_QUEUE.with(|q| {
|
|
281
|
+
let mut queue = q.borrow_mut();
|
|
280
282
|
if queue.is_empty() {
|
|
281
283
|
None
|
|
282
284
|
} else {
|
|
283
285
|
Some(queue.remove(0))
|
|
284
286
|
}
|
|
285
|
-
};
|
|
287
|
+
});
|
|
286
288
|
|
|
287
289
|
if let Some(e) = event {
|
|
288
290
|
return handle_event(e);
|
|
289
291
|
}
|
|
290
292
|
|
|
291
|
-
let is_test_mode =
|
|
292
|
-
let term_lock = crate::terminal::TERMINAL.lock().unwrap();
|
|
293
|
-
matches!(
|
|
294
|
-
term_lock.as_ref(),
|
|
295
|
-
Some(crate::terminal::TerminalWrapper::Test(_))
|
|
296
|
-
)
|
|
297
|
-
};
|
|
293
|
+
let is_test_mode = crate::terminal::with_query(|q| q.is_test_mode()).unwrap_or(false);
|
|
298
294
|
|
|
299
295
|
if is_test_mode {
|
|
300
296
|
return Ok(ruby.qnil().into_value_with(ruby));
|
data/ext/ratatui_ruby/src/lib.rs
CHANGED
|
@@ -17,13 +17,13 @@ mod frame;
|
|
|
17
17
|
mod rendering;
|
|
18
18
|
mod string_width;
|
|
19
19
|
mod style;
|
|
20
|
-
mod terminal;
|
|
20
|
+
mod terminal; // New module with TerminalQuery trait
|
|
21
21
|
mod text;
|
|
22
22
|
mod widgets;
|
|
23
23
|
|
|
24
24
|
use frame::RubyFrame;
|
|
25
25
|
use magnus::{function, method, Error, Module, Object, Ruby, Value};
|
|
26
|
-
use terminal::{init_terminal, restore_terminal
|
|
26
|
+
use terminal::{init_terminal, restore_terminal};
|
|
27
27
|
|
|
28
28
|
/// Draw to the terminal.
|
|
29
29
|
///
|
|
@@ -63,63 +63,78 @@ fn draw(args: &[Value]) -> Result<(), Error> {
|
|
|
63
63
|
));
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
66
|
+
// Nested draw() calls are not allowed - would deadlock
|
|
67
|
+
if terminal::is_in_draw_mode() {
|
|
68
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
69
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
70
|
+
let error_class = error_base.const_get("Invariant")?;
|
|
71
|
+
return Err(Error::new(
|
|
72
|
+
error_class,
|
|
73
|
+
"draw cannot be called during another draw (nested draws not allowed)",
|
|
74
|
+
));
|
|
75
|
+
}
|
|
75
76
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
// Use lend_for_draw which handles snapshot capture/cleanup automatically
|
|
78
|
+
let result = terminal::lend_for_draw(|wrapper| {
|
|
79
|
+
let ruby = magnus::Ruby::get().expect("Ruby must be initialized");
|
|
80
|
+
let mut render_error: Option<Error> = None;
|
|
81
|
+
|
|
82
|
+
// Helper closure to execute the draw callback logic for either terminal type
|
|
83
|
+
let mut draw_callback = |f: &mut ratatui::Frame<'_>| {
|
|
84
|
+
if block_given {
|
|
85
|
+
// New API: yield RubyFrame to block
|
|
86
|
+
let active = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(true));
|
|
87
|
+
|
|
88
|
+
let ruby_frame = RubyFrame::new(f, active.clone());
|
|
89
|
+
if let Err(e) = ruby.yield_value::<_, Value>(ruby_frame) {
|
|
90
|
+
render_error = Some(e);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Invalidate frame immediately after block returns
|
|
94
|
+
active.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
95
|
+
} else if let Some(tree_value) = tree {
|
|
96
|
+
// Legacy API: render tree to full area
|
|
97
|
+
let area = f.area();
|
|
98
|
+
if let Err(e) = rendering::render_node(f.buffer_mut(), area, tree_value) {
|
|
99
|
+
render_error = Some(e);
|
|
100
|
+
}
|
|
79
101
|
}
|
|
102
|
+
};
|
|
80
103
|
|
|
81
|
-
// Invalidate frame immediately after block returns
|
|
82
|
-
// This prevents use-after-free if user stored the frame object
|
|
83
|
-
active.store(false, std::sync::atomic::Ordering::Relaxed);
|
|
84
|
-
} else if let Some(tree_value) = tree {
|
|
85
|
-
// Legacy API: render tree to full area
|
|
86
|
-
let area = f.area();
|
|
87
|
-
if let Err(e) = rendering::render_node(f.buffer_mut(), area, tree_value) {
|
|
88
|
-
render_error = Some(e);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
if let Some(wrapper) = term_lock.as_mut() {
|
|
94
104
|
match wrapper {
|
|
95
105
|
terminal::TerminalWrapper::Crossterm(term) => {
|
|
96
|
-
let module = ruby.define_module("RatatuiRuby")
|
|
97
|
-
let error_base = module.const_get::<_, magnus::RClass>("Error")
|
|
98
|
-
let error_class = error_base.const_get("Terminal")
|
|
106
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
107
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
108
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
99
109
|
term.draw(&mut draw_callback)
|
|
100
110
|
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
101
111
|
}
|
|
102
112
|
terminal::TerminalWrapper::Test(term) => {
|
|
103
|
-
let module = ruby.define_module("RatatuiRuby")
|
|
104
|
-
let error_base = module.const_get::<_, magnus::RClass>("Error")
|
|
105
|
-
let error_class = error_base.const_get("Terminal")
|
|
113
|
+
let module = ruby.define_module("RatatuiRuby").unwrap();
|
|
114
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error").unwrap();
|
|
115
|
+
let error_class = error_base.const_get("Terminal").unwrap();
|
|
106
116
|
term.draw(&mut draw_callback)
|
|
107
117
|
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
108
118
|
}
|
|
109
119
|
}
|
|
110
|
-
} else {
|
|
111
|
-
eprintln!("Terminal is None!");
|
|
112
|
-
}
|
|
113
120
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
121
|
+
if let Some(e) = render_error {
|
|
122
|
+
return Err(e);
|
|
123
|
+
}
|
|
117
124
|
|
|
118
|
-
|
|
125
|
+
Ok(())
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
result.unwrap_or_else(|| {
|
|
129
|
+
eprintln!("Terminal is None!");
|
|
130
|
+
Ok(())
|
|
131
|
+
})
|
|
119
132
|
}
|
|
120
133
|
|
|
121
|
-
|
|
122
|
-
|
|
134
|
+
// Storage for the last panic info, to be retrieved and printed after terminal restore.
|
|
135
|
+
thread_local! {
|
|
136
|
+
static LAST_PANIC: std::cell::RefCell<Option<String>> = const { std::cell::RefCell::new(None) };
|
|
137
|
+
}
|
|
123
138
|
|
|
124
139
|
/// Enables Rust backtraces and installs a custom panic hook.
|
|
125
140
|
///
|
|
@@ -131,9 +146,9 @@ fn enable_rust_backtrace(_ruby: &magnus::Ruby) {
|
|
|
131
146
|
std::panic::set_hook(Box::new(|info| {
|
|
132
147
|
let backtrace = std::backtrace::Backtrace::force_capture();
|
|
133
148
|
let message = format!("Rust panic: {info}\n{backtrace}");
|
|
134
|
-
|
|
135
|
-
*
|
|
136
|
-
}
|
|
149
|
+
LAST_PANIC.with(|p| {
|
|
150
|
+
*p.borrow_mut() = Some(message);
|
|
151
|
+
});
|
|
137
152
|
}));
|
|
138
153
|
}
|
|
139
154
|
|
|
@@ -141,11 +156,7 @@ fn enable_rust_backtrace(_ruby: &magnus::Ruby) {
|
|
|
141
156
|
///
|
|
142
157
|
/// Call this after terminal restoration to get deferred panic output.
|
|
143
158
|
fn get_last_panic(_ruby: &magnus::Ruby) -> Option<String> {
|
|
144
|
-
|
|
145
|
-
guard.take()
|
|
146
|
-
} else {
|
|
147
|
-
None
|
|
148
|
-
}
|
|
159
|
+
LAST_PANIC.with(|p| p.borrow_mut().take())
|
|
149
160
|
}
|
|
150
161
|
|
|
151
162
|
/// Intentionally panics to test backtrace output.
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! Terminal initialization and restoration functions.
|
|
5
|
+
|
|
6
|
+
use magnus::{Error, Module};
|
|
7
|
+
use ratatui::{
|
|
8
|
+
backend::{CrosstermBackend, TestBackend},
|
|
9
|
+
Terminal, TerminalOptions, Viewport,
|
|
10
|
+
};
|
|
11
|
+
use std::io;
|
|
12
|
+
|
|
13
|
+
use super::TerminalWrapper;
|
|
14
|
+
|
|
15
|
+
// Track whether we're using fullscreen viewport (for restore_terminal)
|
|
16
|
+
thread_local! {
|
|
17
|
+
static IS_FULLSCREEN: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[allow(clippy::needless_pass_by_value)] // Magnus FFI requires owned String, not &str
|
|
21
|
+
pub fn init_terminal(
|
|
22
|
+
focus_events: bool,
|
|
23
|
+
bracketed_paste: bool,
|
|
24
|
+
viewport_type: String,
|
|
25
|
+
viewport_height: Option<u16>,
|
|
26
|
+
) -> Result<(), Error> {
|
|
27
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
28
|
+
|
|
29
|
+
// Check if already initialized
|
|
30
|
+
if super::is_initialized() {
|
|
31
|
+
return Ok(());
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
35
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
36
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
37
|
+
|
|
38
|
+
// Parse viewport type
|
|
39
|
+
let viewport = match viewport_type.as_ref() {
|
|
40
|
+
"inline" => {
|
|
41
|
+
let height = viewport_height.unwrap_or(8);
|
|
42
|
+
Viewport::Inline(height)
|
|
43
|
+
}
|
|
44
|
+
_ => Viewport::Fullscreen,
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
ratatui::crossterm::terminal::enable_raw_mode()
|
|
48
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
49
|
+
let mut stdout = io::stdout();
|
|
50
|
+
|
|
51
|
+
// Only enter alternate screen for fullscreen viewports
|
|
52
|
+
if matches!(viewport, Viewport::Fullscreen) {
|
|
53
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::terminal::EnterAlternateScreen)
|
|
54
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableMouseCapture)
|
|
58
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
59
|
+
|
|
60
|
+
if focus_events {
|
|
61
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableFocusChange)
|
|
62
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
63
|
+
}
|
|
64
|
+
if bracketed_paste {
|
|
65
|
+
ratatui::crossterm::execute!(stdout, ratatui::crossterm::event::EnableBracketedPaste)
|
|
66
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
let backend = CrosstermBackend::new(stdout);
|
|
70
|
+
|
|
71
|
+
// Store whether we're using fullscreen for restore_terminal (before moving viewport)
|
|
72
|
+
let is_fullscreen = matches!(viewport, Viewport::Fullscreen);
|
|
73
|
+
IS_FULLSCREEN.with(|f| f.set(is_fullscreen));
|
|
74
|
+
|
|
75
|
+
let options = TerminalOptions { viewport };
|
|
76
|
+
let terminal = Terminal::with_options(backend, options)
|
|
77
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
78
|
+
|
|
79
|
+
super::set_terminal(TerminalWrapper::Crossterm(terminal));
|
|
80
|
+
Ok(())
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
#[allow(clippy::needless_pass_by_value)] // Magnus FFI requires owned String, not &str
|
|
84
|
+
pub fn init_test_terminal(
|
|
85
|
+
width: u16,
|
|
86
|
+
height: u16,
|
|
87
|
+
viewport_type: String,
|
|
88
|
+
viewport_height: Option<u16>,
|
|
89
|
+
) -> Result<(), Error> {
|
|
90
|
+
let ruby = magnus::Ruby::get().unwrap();
|
|
91
|
+
let backend = TestBackend::new(width, height);
|
|
92
|
+
let module = ruby.define_module("RatatuiRuby")?;
|
|
93
|
+
let error_base = module.const_get::<_, magnus::RClass>("Error")?;
|
|
94
|
+
let error_class = error_base.const_get("Terminal")?;
|
|
95
|
+
|
|
96
|
+
// Parse viewport type (same as init_terminal)
|
|
97
|
+
let viewport = match viewport_type.as_ref() {
|
|
98
|
+
"inline" => {
|
|
99
|
+
let vp_height = viewport_height.unwrap_or(height);
|
|
100
|
+
Viewport::Inline(vp_height)
|
|
101
|
+
}
|
|
102
|
+
_ => Viewport::Fullscreen,
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
let options = TerminalOptions { viewport };
|
|
106
|
+
let terminal = Terminal::with_options(backend, options)
|
|
107
|
+
.map_err(|e| Error::new(error_class, e.to_string()))?;
|
|
108
|
+
|
|
109
|
+
super::set_terminal(TerminalWrapper::Test(terminal));
|
|
110
|
+
Ok(())
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
pub fn restore_terminal() {
|
|
114
|
+
if let Some(wrapper) = super::take_terminal() {
|
|
115
|
+
match wrapper {
|
|
116
|
+
TerminalWrapper::Crossterm(mut t) => {
|
|
117
|
+
let _ = ratatui::crossterm::terminal::disable_raw_mode();
|
|
118
|
+
|
|
119
|
+
// Only leave alternate screen if we were in fullscreen mode
|
|
120
|
+
let is_fullscreen = IS_FULLSCREEN.with(std::cell::Cell::get);
|
|
121
|
+
if is_fullscreen {
|
|
122
|
+
let _ = ratatui::crossterm::execute!(
|
|
123
|
+
t.backend_mut(),
|
|
124
|
+
ratatui::crossterm::terminal::LeaveAlternateScreen,
|
|
125
|
+
ratatui::crossterm::event::DisableMouseCapture,
|
|
126
|
+
ratatui::crossterm::event::DisableFocusChange,
|
|
127
|
+
ratatui::crossterm::event::DisableBracketedPaste
|
|
128
|
+
);
|
|
129
|
+
} else {
|
|
130
|
+
let _ = ratatui::crossterm::execute!(
|
|
131
|
+
t.backend_mut(),
|
|
132
|
+
ratatui::crossterm::event::DisableMouseCapture,
|
|
133
|
+
ratatui::crossterm::event::DisableFocusChange,
|
|
134
|
+
ratatui::crossterm::event::DisableBracketedPaste
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
TerminalWrapper::Test(_) => {}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
3
|
+
|
|
4
|
+
//! Terminal management module.
|
|
5
|
+
//!
|
|
6
|
+
//! Provides thread-local terminal storage with safe accessors. Queries
|
|
7
|
+
//! during `draw()` callbacks return data from a pre-captured snapshot,
|
|
8
|
+
//! avoiding reentrancy issues with the terminal lock.
|
|
9
|
+
|
|
10
|
+
mod init;
|
|
11
|
+
mod mutations;
|
|
12
|
+
mod queries;
|
|
13
|
+
mod query;
|
|
14
|
+
mod storage;
|
|
15
|
+
mod wrapper;
|
|
16
|
+
|
|
17
|
+
pub use storage::{
|
|
18
|
+
is_in_draw_mode, is_initialized, lend_for_draw, set_terminal, take_terminal, with_query,
|
|
19
|
+
with_terminal_mut,
|
|
20
|
+
};
|
|
21
|
+
pub use wrapper::TerminalWrapper;
|
|
22
|
+
|
|
23
|
+
// Init/restore functions
|
|
24
|
+
pub use init::{init_terminal, init_test_terminal, restore_terminal};
|
|
25
|
+
|
|
26
|
+
// Query functions
|
|
27
|
+
pub use queries::{
|
|
28
|
+
get_buffer_content, get_cell_at, get_cursor_position, get_terminal_area, get_terminal_size,
|
|
29
|
+
get_viewport_type,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// Mutation functions
|
|
33
|
+
pub use mutations::{insert_before, resize_terminal, set_cursor_position};
|