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.
@@ -0,0 +1,338 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal query trait and implementations.
5
+ //!
6
+ //! This module provides a compiler-enforced interface for querying terminal state.
7
+ //! All queries go through `TerminalQuery` trait which has two implementations:
8
+ //! - `LiveTerminal`: Used outside draw, queries actual terminal
9
+ //! - `DrawSnapshot`: Used during draw, queries pre-captured snapshot
10
+
11
+ use ratatui::buffer::Cell;
12
+ use ratatui::layout::Rect;
13
+
14
+ /// All terminal queries MUST go through this trait.
15
+ /// Compiler enforces both implementations when adding new queries.
16
+ pub trait TerminalQuery {
17
+ fn size(&self) -> Rect;
18
+ fn viewport_area(&self) -> Rect;
19
+ fn is_test_mode(&self) -> bool;
20
+ fn cursor_position(&self) -> Option<(u16, u16)>;
21
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell>;
22
+ }
23
+
24
+ /// Snapshot of terminal state captured before draw.
25
+ /// Answers queries during draw without accessing terminal.
26
+ #[derive(Clone)]
27
+ pub struct DrawSnapshot {
28
+ pub size: Rect,
29
+ pub viewport_area: Rect,
30
+ pub is_test_mode: bool,
31
+ pub cursor_position: Option<(u16, u16)>,
32
+ pub buffer: Option<ratatui::buffer::Buffer>,
33
+ }
34
+
35
+ impl DrawSnapshot {
36
+ /// Capture snapshot from a `TerminalWrapper`.
37
+ pub fn capture(wrapper: &mut super::wrapper::TerminalWrapper) -> Self {
38
+ match wrapper {
39
+ super::wrapper::TerminalWrapper::Crossterm(t) => {
40
+ let size = t.size().unwrap_or_default();
41
+ let viewport = t.get_frame().area();
42
+ Self {
43
+ size: Rect::new(0, 0, size.width, size.height),
44
+ viewport_area: viewport,
45
+ is_test_mode: false,
46
+ cursor_position: None,
47
+ buffer: None,
48
+ }
49
+ }
50
+ super::wrapper::TerminalWrapper::Test(t) => {
51
+ let size = t.size().unwrap_or_default();
52
+ let viewport = t.get_frame().area();
53
+ let cursor = t.get_cursor_position().ok().map(Into::into);
54
+ let buffer = t.backend().buffer().clone();
55
+ Self {
56
+ size: Rect::new(0, 0, size.width, size.height),
57
+ viewport_area: viewport,
58
+ is_test_mode: true,
59
+ cursor_position: cursor,
60
+ buffer: Some(buffer),
61
+ }
62
+ }
63
+ }
64
+ }
65
+ }
66
+
67
+ impl TerminalQuery for DrawSnapshot {
68
+ fn size(&self) -> Rect {
69
+ self.size
70
+ }
71
+ fn viewport_area(&self) -> Rect {
72
+ self.viewport_area
73
+ }
74
+ fn is_test_mode(&self) -> bool {
75
+ self.is_test_mode
76
+ }
77
+ fn cursor_position(&self) -> Option<(u16, u16)> {
78
+ self.cursor_position
79
+ }
80
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell> {
81
+ self.buffer.as_ref()?.cell((x, y)).cloned()
82
+ }
83
+ }
84
+
85
+ /// Live queries against actual terminal (outside draw).
86
+ /// Uses interior mutability since ratatui's API requires &mut for some queries.
87
+ pub struct LiveTerminal<'a>(std::cell::RefCell<&'a mut super::wrapper::TerminalWrapper>);
88
+
89
+ impl<'a> LiveTerminal<'a> {
90
+ pub fn new(wrapper: &'a mut super::wrapper::TerminalWrapper) -> Self {
91
+ Self(std::cell::RefCell::new(wrapper))
92
+ }
93
+ }
94
+
95
+ impl TerminalQuery for LiveTerminal<'_> {
96
+ fn size(&self) -> Rect {
97
+ match *self.0.borrow() {
98
+ super::wrapper::TerminalWrapper::Crossterm(ref t) => {
99
+ let s = t.size().unwrap_or_default();
100
+ Rect::new(0, 0, s.width, s.height)
101
+ }
102
+ super::wrapper::TerminalWrapper::Test(ref t) => {
103
+ let s = t.size().unwrap_or_default();
104
+ Rect::new(0, 0, s.width, s.height)
105
+ }
106
+ }
107
+ }
108
+
109
+ fn viewport_area(&self) -> Rect {
110
+ match *self.0.borrow_mut() {
111
+ super::wrapper::TerminalWrapper::Crossterm(ref mut t) => t.get_frame().area(),
112
+ super::wrapper::TerminalWrapper::Test(ref mut t) => t.get_frame().area(),
113
+ }
114
+ }
115
+ fn is_test_mode(&self) -> bool {
116
+ matches!(*self.0.borrow(), super::wrapper::TerminalWrapper::Test(_))
117
+ }
118
+ fn cursor_position(&self) -> Option<(u16, u16)> {
119
+ match *self.0.borrow_mut() {
120
+ super::wrapper::TerminalWrapper::Test(ref mut t) => {
121
+ t.get_cursor_position().ok().map(Into::into)
122
+ }
123
+ super::wrapper::TerminalWrapper::Crossterm(_) => None,
124
+ }
125
+ }
126
+ fn cell_at(&self, x: u16, y: u16) -> Option<Cell> {
127
+ match &*self.0.borrow() {
128
+ super::wrapper::TerminalWrapper::Test(t) => t.backend().buffer().cell((x, y)).cloned(),
129
+ super::wrapper::TerminalWrapper::Crossterm(_) => None,
130
+ }
131
+ }
132
+ }
133
+
134
+ #[cfg(test)]
135
+ mod tests {
136
+ use super::*;
137
+
138
+ // Test: TerminalQuery trait can be implemented by a simple struct
139
+ struct MockQuery {
140
+ size: Rect,
141
+ }
142
+
143
+ impl TerminalQuery for MockQuery {
144
+ fn size(&self) -> Rect {
145
+ self.size
146
+ }
147
+ fn viewport_area(&self) -> Rect {
148
+ Rect::default()
149
+ }
150
+ fn is_test_mode(&self) -> bool {
151
+ false
152
+ }
153
+ fn cursor_position(&self) -> Option<(u16, u16)> {
154
+ None
155
+ }
156
+ fn cell_at(&self, _x: u16, _y: u16) -> Option<Cell> {
157
+ None
158
+ }
159
+ }
160
+
161
+ #[test]
162
+ fn test_trait_can_be_implemented() {
163
+ let query = MockQuery {
164
+ size: Rect::new(0, 0, 80, 24),
165
+ };
166
+ assert_eq!(query.size(), Rect::new(0, 0, 80, 24));
167
+ }
168
+
169
+ #[test]
170
+ fn test_draw_snapshot_returns_size() {
171
+ let snapshot = super::DrawSnapshot {
172
+ size: Rect::new(0, 0, 120, 40),
173
+ viewport_area: Rect::new(0, 0, 120, 40),
174
+ is_test_mode: false,
175
+ cursor_position: None,
176
+ buffer: None,
177
+ };
178
+ assert_eq!(snapshot.size(), Rect::new(0, 0, 120, 40));
179
+ }
180
+
181
+ #[test]
182
+ fn test_draw_snapshot_returns_viewport_area() {
183
+ let snapshot = super::DrawSnapshot {
184
+ size: Rect::new(0, 0, 120, 40),
185
+ viewport_area: Rect::new(5, 10, 80, 20), // Different from size!
186
+ is_test_mode: false,
187
+ cursor_position: None,
188
+ buffer: None,
189
+ };
190
+ // This should return the stored viewport_area, not Rect::default()
191
+ assert_eq!(snapshot.viewport_area(), Rect::new(5, 10, 80, 20));
192
+ }
193
+
194
+ #[test]
195
+ fn test_draw_snapshot_returns_is_test_mode() {
196
+ let snapshot = super::DrawSnapshot {
197
+ size: Rect::default(),
198
+ viewport_area: Rect::default(),
199
+ is_test_mode: true, // TRUE!
200
+ cursor_position: None,
201
+ buffer: None,
202
+ };
203
+ assert!(snapshot.is_test_mode());
204
+ }
205
+
206
+ #[test]
207
+ fn test_draw_snapshot_returns_cursor_position() {
208
+ let snapshot = super::DrawSnapshot {
209
+ size: Rect::default(),
210
+ viewport_area: Rect::default(),
211
+ is_test_mode: false,
212
+ cursor_position: Some((15, 20)), // Not None!
213
+ buffer: None,
214
+ };
215
+ assert_eq!(snapshot.cursor_position(), Some((15, 20)));
216
+ }
217
+
218
+ #[test]
219
+ fn test_draw_snapshot_returns_cell_at() {
220
+ use ratatui::buffer::Buffer;
221
+
222
+ // Create a buffer with a known cell
223
+ let mut buffer = Buffer::empty(Rect::new(0, 0, 10, 10));
224
+ buffer[(5, 5)].set_char('X');
225
+
226
+ let snapshot = super::DrawSnapshot {
227
+ size: Rect::new(0, 0, 10, 10),
228
+ viewport_area: Rect::new(0, 0, 10, 10),
229
+ is_test_mode: true,
230
+ cursor_position: None,
231
+ buffer: Some(buffer),
232
+ };
233
+
234
+ let cell = snapshot.cell_at(5, 5);
235
+ assert!(cell.is_some());
236
+ assert_eq!(cell.unwrap().symbol(), "X");
237
+ }
238
+
239
+ #[test]
240
+ fn test_capture_from_terminal_wrapper() {
241
+ use crate::terminal::TerminalWrapper;
242
+ use ratatui::{backend::TestBackend, Terminal};
243
+
244
+ let backend = TestBackend::new(80, 24);
245
+ let terminal = Terminal::new(backend).unwrap();
246
+ let mut wrapper = TerminalWrapper::Test(terminal);
247
+
248
+ let snapshot = super::DrawSnapshot::capture(&mut wrapper);
249
+
250
+ assert_eq!(snapshot.size(), Rect::new(0, 0, 80, 24));
251
+ assert!(snapshot.is_test_mode());
252
+ }
253
+
254
+ #[test]
255
+ fn test_live_terminal_exists() {
256
+ use crate::terminal::TerminalWrapper;
257
+ use ratatui::{backend::TestBackend, Terminal};
258
+
259
+ let backend = TestBackend::new(80, 24);
260
+ let terminal = Terminal::new(backend).unwrap();
261
+ let mut wrapper = TerminalWrapper::Test(terminal);
262
+
263
+ // LiveTerminal should exist and wrap a TerminalWrapper reference
264
+ let _live = super::LiveTerminal::new(&mut wrapper);
265
+ }
266
+
267
+ #[test]
268
+ fn test_live_terminal_returns_size() {
269
+ use crate::terminal::TerminalWrapper;
270
+ use ratatui::{backend::TestBackend, Terminal};
271
+
272
+ let backend = TestBackend::new(100, 50);
273
+ let terminal = Terminal::new(backend).unwrap();
274
+ let mut wrapper = TerminalWrapper::Test(terminal);
275
+
276
+ let live = super::LiveTerminal::new(&mut wrapper);
277
+ // LiveTerminal must implement TerminalQuery
278
+ let size = live.size();
279
+ assert_eq!(size.width, 100);
280
+ assert_eq!(size.height, 50);
281
+ }
282
+
283
+ #[test]
284
+ fn test_live_terminal_returns_is_test_mode() {
285
+ use crate::terminal::TerminalWrapper;
286
+ use ratatui::{backend::TestBackend, Terminal};
287
+
288
+ let backend = TestBackend::new(80, 24);
289
+ let terminal = Terminal::new(backend).unwrap();
290
+ let mut wrapper = TerminalWrapper::Test(terminal);
291
+
292
+ let live = super::LiveTerminal::new(&mut wrapper);
293
+ // TestBackend should return true for is_test_mode
294
+ assert!(live.is_test_mode());
295
+ }
296
+
297
+ #[test]
298
+ fn test_live_terminal_returns_viewport_area() {
299
+ use crate::terminal::TerminalWrapper;
300
+ use ratatui::{backend::TestBackend, Terminal};
301
+
302
+ let backend = TestBackend::new(120, 40);
303
+ let terminal = Terminal::new(backend).unwrap();
304
+ let mut wrapper = TerminalWrapper::Test(terminal);
305
+
306
+ let live = super::LiveTerminal::new(&mut wrapper);
307
+ let viewport = live.viewport_area();
308
+ // Viewport should match the terminal size for fullscreen mode
309
+ assert_eq!(viewport.width, 120);
310
+ assert_eq!(viewport.height, 40);
311
+ }
312
+
313
+ #[test]
314
+ fn test_live_terminal_viewport_area_differs_from_size_for_inline() {
315
+ use crate::terminal::TerminalWrapper;
316
+ use ratatui::{backend::TestBackend, Terminal, Viewport};
317
+
318
+ // Create terminal with inline viewport of height 5 in a 40-line terminal
319
+ let backend = TestBackend::new(80, 40);
320
+ let options = ratatui::TerminalOptions {
321
+ viewport: Viewport::Inline(5),
322
+ };
323
+ let terminal = Terminal::with_options(backend, options).unwrap();
324
+ let mut wrapper = TerminalWrapper::Test(terminal);
325
+
326
+ let live = super::LiveTerminal::new(&mut wrapper);
327
+ let size = live.size();
328
+ let viewport = live.viewport_area();
329
+
330
+ // Terminal size is 80x40
331
+ assert_eq!(size.width, 80);
332
+ assert_eq!(size.height, 40);
333
+
334
+ // But viewport is only 5 lines high (inline mode)
335
+ assert_eq!(viewport.width, 80);
336
+ assert_eq!(viewport.height, 5);
337
+ }
338
+ }
@@ -0,0 +1,109 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Private terminal storage with safe accessor functions.
5
+ //!
6
+ //! This module provides thread-local storage for the terminal and draw snapshot,
7
+ //! with functions that route queries correctly:
8
+ //! - During draw: queries go to snapshot (no lock needed)
9
+ //! - Outside draw: queries go to live terminal
10
+
11
+ use super::query::{DrawSnapshot, LiveTerminal, TerminalQuery};
12
+ use super::wrapper::TerminalWrapper;
13
+ use std::cell::RefCell;
14
+
15
+ thread_local! {
16
+ static TERMINAL: RefCell<Option<TerminalWrapper>> = const { RefCell::new(None) };
17
+ static DRAW_SNAPSHOT: RefCell<Option<DrawSnapshot>> = const { RefCell::new(None) };
18
+ }
19
+
20
+ /// Check if we're currently inside a draw operation.
21
+ pub fn is_in_draw_mode() -> bool {
22
+ DRAW_SNAPSHOT.with(|cell| cell.borrow().is_some())
23
+ }
24
+
25
+ /// Execute a query. Routes to snapshot during draw, live terminal outside.
26
+ pub fn with_query<R, F>(f: F) -> Option<R>
27
+ where
28
+ F: Fn(&dyn TerminalQuery) -> R,
29
+ {
30
+ // During draw: use snapshot
31
+ DRAW_SNAPSHOT
32
+ .with(|cell| cell.borrow().as_ref().map(|snapshot| f(snapshot)))
33
+ .or_else(|| {
34
+ // Outside draw: use live terminal from thread-local storage
35
+ TERMINAL.with(|cell| {
36
+ cell.borrow_mut().as_mut().map(|t| {
37
+ let live = LiveTerminal::new(t);
38
+ f(&live)
39
+ })
40
+ })
41
+ })
42
+ }
43
+
44
+ /// Mutable access to terminal. Only works OUTSIDE draw.
45
+ pub fn with_terminal_mut<R, F>(f: F) -> Option<R>
46
+ where
47
+ F: FnOnce(&mut TerminalWrapper) -> R,
48
+ {
49
+ // During draw: reject
50
+ if is_in_draw_mode() {
51
+ return None;
52
+ }
53
+ TERMINAL.with(|cell| cell.borrow_mut().as_mut().map(f))
54
+ }
55
+
56
+ /// Execute draw with snapshot for queries.
57
+ pub fn lend_for_draw<R, F>(f: F) -> Option<R>
58
+ where
59
+ F: FnOnce(&mut TerminalWrapper) -> R,
60
+ {
61
+ // Take terminal out of storage
62
+ let mut terminal = TERMINAL.with(|cell| cell.borrow_mut().take())?;
63
+
64
+ // Capture snapshot BEFORE draw (while we have &mut)
65
+ let snapshot = DrawSnapshot::capture(&mut terminal);
66
+ DRAW_SNAPSHOT.with(|cell| *cell.borrow_mut() = Some(snapshot));
67
+
68
+ let result = f(&mut terminal);
69
+
70
+ // Clear snapshot, restore terminal
71
+ DRAW_SNAPSHOT.with(|cell| *cell.borrow_mut() = None);
72
+ TERMINAL.with(|cell| *cell.borrow_mut() = Some(terminal));
73
+
74
+ Some(result)
75
+ }
76
+
77
+ /// Set the terminal (used during initialization).
78
+ pub fn set_terminal(wrapper: TerminalWrapper) {
79
+ TERMINAL.with(|cell| {
80
+ *cell.borrow_mut() = Some(wrapper);
81
+ });
82
+ }
83
+
84
+ /// Take the terminal out of storage (used during cleanup).
85
+ pub fn take_terminal() -> Option<TerminalWrapper> {
86
+ TERMINAL.with(|cell| cell.borrow_mut().take())
87
+ }
88
+
89
+ /// Check if terminal is initialized.
90
+ pub fn is_initialized() -> bool {
91
+ TERMINAL.with(|cell| cell.borrow().is_some())
92
+ }
93
+
94
+ #[cfg(test)]
95
+ mod tests {
96
+ use crate::terminal::query::DrawSnapshot;
97
+ use ratatui::layout::Rect;
98
+
99
+ #[test]
100
+ fn test_is_in_draw_mode_false_by_default() {
101
+ assert!(!super::is_in_draw_mode());
102
+ }
103
+
104
+ #[test]
105
+ fn test_with_query_returns_none_when_not_initialized() {
106
+ let result = super::with_query(|q| q.size());
107
+ assert!(result.is_none());
108
+ }
109
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! Terminal wrapper enum for different backend types.
5
+
6
+ use ratatui::{
7
+ backend::{CrosstermBackend, TestBackend},
8
+ Terminal,
9
+ };
10
+ use std::io;
11
+
12
+ /// Unified terminal type supporting different backends.
13
+ pub enum TerminalWrapper {
14
+ Crossterm(Terminal<CrosstermBackend<io::Stdout>>),
15
+ Test(Terminal<TestBackend>),
16
+ }
@@ -28,6 +28,12 @@ module RatatuiRuby
28
28
 
29
29
  # Handles navigation-specific DWIM logic for method_missing.
30
30
  private def match_navigation_dwim?(key_name, key_sym)
31
+ # DWIM: back_tab?/backtab? matches back_tab code even with shift modifier
32
+ # (since back_tab semantically implies shift)
33
+ if (key_name == "back_tab" || key_name == "backtab") && @code == "back_tab" && (@modifiers.empty? || @modifiers == ["shift"])
34
+ return true
35
+ end
36
+
31
37
  # DWIM: reverse_tab? matches both BackTab key and Shift+Tab combo
32
38
  if key_name == "reverse_tab"
33
39
  return true if @code == "back_tab"
@@ -8,5 +8,5 @@
8
8
  module RatatuiRuby
9
9
  # The version of the ratatui_ruby gem.
10
10
  # See https://semver.org/spec/v2.0.0.html
11
- VERSION = "1.0.0-beta.2"
11
+ VERSION = "1.0.0"
12
12
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratatui_ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0.pre.beta.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kerrick Long
@@ -101,7 +101,7 @@ description: |2-
101
101
  Ratatui[https://ratatui.rs], a leading TUI library written in
102
102
  Rust[https://rust-lang.org]. You get native performance with the joy of Ruby.
103
103
 
104
- gem install ratatui_ruby --pre
104
+ gem install ratatui_ruby
105
105
 
106
106
  {rdoc-image:https://ratatui-ruby.dev/hero.gif}[https://www.ratatui-ruby.dev/docs/v0.10/examples/app_cli_rich_moments/README_md.html]
107
107
 
@@ -604,14 +604,16 @@ files:
604
604
  - ext/ratatui_ruby/src/events.rs
605
605
  - ext/ratatui_ruby/src/frame.rs
606
606
  - ext/ratatui_ruby/src/lib.rs
607
- - ext/ratatui_ruby/src/lib.rs.bak
608
607
  - ext/ratatui_ruby/src/rendering.rs
609
- - ext/ratatui_ruby/src/rendering.rs.bak
610
608
  - ext/ratatui_ruby/src/string_width.rs
611
609
  - ext/ratatui_ruby/src/style.rs
612
- - ext/ratatui_ruby/src/terminal.rs
613
- - ext/ratatui_ruby/src/terminal.rs.bak
614
- - ext/ratatui_ruby/src/terminal.rs.orig
610
+ - ext/ratatui_ruby/src/terminal/init.rs
611
+ - ext/ratatui_ruby/src/terminal/mod.rs
612
+ - ext/ratatui_ruby/src/terminal/mutations.rs
613
+ - ext/ratatui_ruby/src/terminal/queries.rs
614
+ - ext/ratatui_ruby/src/terminal/query.rs
615
+ - ext/ratatui_ruby/src/terminal/storage.rs
616
+ - ext/ratatui_ruby/src/terminal/wrapper.rs
615
617
  - ext/ratatui_ruby/src/text.rs
616
618
  - ext/ratatui_ruby/src/widgets/barchart.rs
617
619
  - ext/ratatui_ruby/src/widgets/block.rs