Skip to main content

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}