rich_rs/screen_context.rs
1//! ScreenContext: RAII context for alternate screen mode.
2//!
3//! This module provides a context guard for alternate screen mode operations.
4//! When entering the context, the terminal switches to an alternate screen buffer.
5//! When exiting (via drop or explicit exit), the terminal returns to the normal buffer.
6//!
7//! # Example
8//!
9//! ```ignore
10//! use rich_rs::{Console, Panel, Align, Text};
11//! use std::thread::sleep;
12//! use std::time::Duration;
13//!
14//! let mut console = Console::new();
15//!
16//! // Enter alternate screen mode
17//! let mut screen = console.screen(true, None)?;
18//!
19//! // Update the screen content
20//! let text = Align::center(Text::from_markup("[blink]Don't Panic![/blink]", false)?, "middle");
21//! screen.update(Panel::new(text))?;
22//!
23//! sleep(Duration::from_secs(5));
24//!
25//! // Dropping `screen` automatically exits alternate screen mode
26//! ```
27
28use std::io::{self, Stdout, Write};
29
30use crate::Renderable;
31use crate::console::Console;
32use crate::group::Group;
33use crate::screen::Screen;
34use crate::style::Style;
35
36/// Context guard for alternate screen mode.
37///
38/// This struct provides RAII semantics for alternate screen operations.
39/// When dropped, it automatically leaves alternate screen mode and restores
40/// the cursor if it was hidden.
41///
42/// Use [`Console::screen()`] to create a `ScreenContext`.
43pub struct ScreenContext<'a, W: Write = Stdout> {
44 /// Reference to the console.
45 console: &'a mut Console<W>,
46 /// Whether the cursor should be hidden.
47 hide_cursor: bool,
48 /// Optional style for the screen background.
49 style: Option<Style>,
50 /// Whether entering alternate screen actually changed state.
51 changed: bool,
52}
53
54impl<'a, W: Write> ScreenContext<'a, W> {
55 /// Create a new ScreenContext.
56 ///
57 /// This is called by [`Console::screen()`] and should not be called directly.
58 pub(crate) fn new(
59 console: &'a mut Console<W>,
60 hide_cursor: bool,
61 style: Option<Style>,
62 ) -> io::Result<Self> {
63 // Enter alternate screen mode
64 let changed = console.set_alt_screen(true)?;
65
66 // Hide cursor if requested and we actually entered alt screen
67 if changed && hide_cursor {
68 let _ = console.show_cursor(false);
69 }
70
71 Ok(Self {
72 console,
73 hide_cursor,
74 style,
75 changed,
76 })
77 }
78
79 /// Update the screen with new content.
80 ///
81 /// This renders the given renderable to fill the terminal dimensions.
82 ///
83 /// # Arguments
84 ///
85 /// * `renderable` - The content to display on the screen.
86 ///
87 /// # Example
88 ///
89 /// ```ignore
90 /// let mut screen = console.screen(true, None)?;
91 /// screen.update(Text::plain("Hello, World!"))?;
92 /// ```
93 pub fn update<R: Renderable + 'static>(&mut self, renderable: R) -> io::Result<()> {
94 self.update_with_style(renderable, None)
95 }
96
97 /// Update the screen with new content and optional style override.
98 ///
99 /// # Arguments
100 ///
101 /// * `renderable` - The content to display on the screen.
102 /// * `style` - Optional style override for the screen background.
103 pub fn update_with_style<R: Renderable + 'static>(
104 &mut self,
105 renderable: R,
106 style: Option<Style>,
107 ) -> io::Result<()> {
108 // Create a new Screen with the renderable
109 let mut screen = Screen::new(renderable);
110
111 // Apply style from the override or the context's default
112 if let Some(s) = style.or(self.style) {
113 screen = screen.with_style(s);
114 }
115
116 // Enable application mode for alternate screen
117 screen = screen.with_application_mode(true);
118
119 // Print the screen (no newline at end since Screen handles its own layout)
120 self.console.print(&screen, None, None, None, false, "")
121 }
122
123 /// Update the screen with multiple renderables.
124 ///
125 /// The renderables are grouped together and rendered as a single unit.
126 ///
127 /// # Arguments
128 ///
129 /// * `renderables` - Iterator of renderables to display.
130 pub fn update_many<I, R>(&mut self, renderables: I) -> io::Result<()>
131 where
132 I: IntoIterator<Item = R>,
133 R: Renderable + 'static,
134 {
135 self.update_many_with_style(renderables, None)
136 }
137
138 /// Update the screen with multiple renderables and optional style.
139 ///
140 /// # Arguments
141 ///
142 /// * `renderables` - Iterator of renderables to display.
143 /// * `style` - Optional style override for the screen background.
144 pub fn update_many_with_style<I, R>(
145 &mut self,
146 renderables: I,
147 style: Option<Style>,
148 ) -> io::Result<()>
149 where
150 I: IntoIterator<Item = R>,
151 R: Renderable + 'static,
152 {
153 let group = Group::new(renderables);
154 self.update_with_style(group, style)
155 }
156
157 /// Set the default style for the screen.
158 ///
159 /// This style will be used for all subsequent `update()` calls unless
160 /// overridden with `update_with_style()`.
161 pub fn set_style(&mut self, style: Option<Style>) {
162 self.style = style;
163 }
164
165 /// Get the current default style.
166 pub fn style(&self) -> Option<Style> {
167 self.style
168 }
169
170 /// Check if alternate screen mode is active.
171 pub fn is_active(&self) -> bool {
172 self.changed && self.console.is_alt_screen()
173 }
174
175 /// Get a reference to the underlying console.
176 pub fn console(&self) -> &Console<W> {
177 self.console
178 }
179
180 /// Get a mutable reference to the underlying console.
181 pub fn console_mut(&mut self) -> &mut Console<W> {
182 self.console
183 }
184}
185
186impl<W: Write> Drop for ScreenContext<'_, W> {
187 fn drop(&mut self) {
188 if self.changed {
189 // Leave alternate screen mode
190 let _ = self.console.set_alt_screen(false);
191
192 // Restore cursor if it was hidden
193 if self.hide_cursor {
194 let _ = self.console.show_cursor(true);
195 }
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::Text;
204
205 #[test]
206 fn test_screen_context_creation() {
207 // Test with a capture console (non-terminal, so alt screen won't change)
208 let mut console = Console::capture();
209 let ctx = ScreenContext::new(&mut console, true, None);
210 assert!(ctx.is_ok());
211 }
212
213 #[test]
214 fn test_screen_context_update() {
215 let mut console = Console::capture();
216 let mut ctx = ScreenContext::new(&mut console, false, None).unwrap();
217
218 // Update should succeed even on capture console
219 let result = ctx.update(Text::plain("Hello, World!"));
220 assert!(result.is_ok());
221 }
222
223 #[test]
224 fn test_screen_context_with_style() {
225 use crate::SimpleColor;
226
227 let style = Style::new().with_bgcolor(SimpleColor::Standard(1));
228 let mut console = Console::capture();
229 let ctx = ScreenContext::new(&mut console, true, Some(style));
230 assert!(ctx.is_ok());
231 }
232
233 #[test]
234 fn test_screen_context_update_many() {
235 let mut console = Console::capture();
236 let mut ctx = ScreenContext::new(&mut console, false, None).unwrap();
237
238 let result = ctx.update_many([Text::plain("Line 1"), Text::plain("Line 2")]);
239 assert!(result.is_ok());
240 }
241
242 #[test]
243 fn test_screen_context_set_style() {
244 use crate::SimpleColor;
245
246 let mut console = Console::capture();
247 let mut ctx = ScreenContext::new(&mut console, false, None).unwrap();
248
249 assert!(ctx.style().is_none());
250
251 let style = Style::new().with_bgcolor(SimpleColor::Standard(2));
252 ctx.set_style(Some(style));
253 assert!(ctx.style().is_some());
254 }
255
256 #[test]
257 fn test_screen_context_is_active() {
258 // With capture console, alt screen won't actually be entered
259 let mut console = Console::capture();
260 let ctx = ScreenContext::new(&mut console, false, None).unwrap();
261
262 // Since capture console is not a terminal, changed will be false
263 assert!(!ctx.is_active());
264 }
265}