Skip to main content

muster/adapter/tui/
terminal.rs

1use std::io::{self, Stdout};
2
3use crossterm::{
4    clipboard::CopyToClipboard,
5    event::{DisableMouseCapture, EnableMouseCapture},
6    execute,
7    style::Print,
8    terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode},
9};
10use getset::MutGetters;
11use ratatui::{Terminal, backend::CrosstermBackend};
12
13use super::pointer_shape::PointerShape;
14use crate::{adapter::clipboard, error::Result};
15
16/// OSC prefix that sets the host pointer shape (xterm OSC 22).
17const POINTER_SHAPE_PREFIX: &str = "\x1b]22;";
18/// String terminator closing an OSC sequence.
19const OSC_TERMINATOR: &str = "\x1b\\";
20
21/// The concrete ratatui terminal type: a crossterm backend on stdout.
22pub type Tui = Terminal<CrosstermBackend<Stdout>>;
23
24/// RAII guard that enters raw mode + the alternate screen on construction and
25/// restores the original terminal state on drop.
26#[derive(MutGetters)]
27pub struct TerminalGuard {
28    #[getset(get_mut = "pub")]
29    terminal: Tui,
30}
31
32impl TerminalGuard {
33    /// Enters raw mode and the alternate screen, and captures the mouse so
34    /// Muster owns pane-scoped selection and scrolling.
35    ///
36    /// # Errors
37    /// Returns an error if raw mode cannot be enabled, the alternate screen
38    /// cannot be entered, or the terminal backend fails to initialize.
39    pub fn new() -> Result<Self> {
40        enable_raw_mode()?;
41        let mut stdout = io::stdout();
42        if let Err(error) = execute!(stdout, EnterAlternateScreen, EnableMouseCapture) {
43            let _ = Self::restore();
44            return Err(error.into());
45        }
46        match Terminal::new(CrosstermBackend::new(stdout)) {
47            Ok(terminal) => Ok(Self { terminal }),
48            Err(error) => {
49                let _ = Self::restore();
50                Err(error.into())
51            },
52        }
53    }
54
55    /// Copies `text` onto the system clipboard: a native clipboard tool when
56    /// one works (herdr's preference), falling back to OSC 52 through the
57    /// host terminal.
58    ///
59    /// # Errors
60    /// Returns an error when the escape sequence cannot be written to stdout.
61    pub fn copy_to_clipboard(&mut self, text: &str) -> io::Result<()> {
62        if clipboard::write_native(text) {
63            return Ok(());
64        }
65        execute!(io::stdout(), CopyToClipboard::to_clipboard_from(text))
66    }
67
68    /// Asks the host terminal to show `shape` as the mouse pointer (OSC 22).
69    ///
70    /// # Errors
71    /// Returns an error when the escape sequence cannot be written to stdout.
72    pub fn set_pointer_shape(&mut self, shape: PointerShape) -> io::Result<()> {
73        execute!(
74            io::stdout(),
75            Print(format!("{POINTER_SHAPE_PREFIX}{shape}{OSC_TERMINATOR}"))
76        )
77    }
78
79    /// Restores the terminal to its original cooked state. Safe to call more
80    /// than once; used by both `Drop` and the panic hook.
81    ///
82    /// # Errors
83    /// Returns an error if raw mode cannot be disabled or the alternate screen
84    /// cannot be left.
85    pub fn restore() -> io::Result<()> {
86        let raw = disable_raw_mode();
87        let mouse = execute!(io::stdout(), DisableMouseCapture);
88        let pointer = execute!(
89            io::stdout(),
90            Print(format!(
91                "{POINTER_SHAPE_PREFIX}{}{OSC_TERMINATOR}",
92                PointerShape::Default
93            ))
94        );
95        let screen = execute!(io::stdout(), LeaveAlternateScreen);
96        raw.and(mouse).and(pointer).and(screen)
97    }
98}
99
100impl Drop for TerminalGuard {
101    fn drop(&mut self) {
102        let _ = Self::restore();
103    }
104}