synd_term/terminal/
mod.rs

1use crossterm::{
2    event::{EnableFocusChange, EventStream},
3    terminal::{self, EnterAlternateScreen, LeaveAlternateScreen},
4    ExecutableCommand,
5};
6use futures_util::{future::Either, stream, Stream};
7use ratatui::Frame;
8use std::io::{self, IsTerminal};
9
10#[cfg(not(feature = "integration"))]
11mod backend;
12#[cfg(not(feature = "integration"))]
13pub use backend::{new_backend, TerminalBackend};
14
15#[cfg(feature = "integration")]
16mod integration_backend;
17#[cfg(feature = "integration")]
18pub use integration_backend::{new_backend, Buffer, TerminalBackend};
19
20/// Provide terminal manipulation operations.
21pub struct Terminal {
22    backend: ratatui::Terminal<TerminalBackend>,
23}
24
25impl Terminal {
26    /// Construct Terminal with default backend
27    pub fn new() -> anyhow::Result<Self> {
28        let backend = new_backend();
29        Ok(Terminal::with(ratatui::Terminal::new(backend)?))
30    }
31
32    pub fn with(backend: ratatui::Terminal<TerminalBackend>) -> Self {
33        Self { backend }
34    }
35
36    /// Initialize terminal
37    pub fn init(&mut self) -> io::Result<()> {
38        terminal::enable_raw_mode()?;
39        crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableFocusChange)?;
40
41        let panic_hook = std::panic::take_hook();
42        std::panic::set_hook(Box::new(move |panic| {
43            Self::restore_backend().expect("Failed to reset terminal");
44            panic_hook(panic);
45        }));
46
47        self.backend.hide_cursor()?;
48        self.backend.clear()?;
49
50        Ok(())
51    }
52
53    /// Reset terminal
54    pub fn restore(&mut self) -> io::Result<()> {
55        Self::restore_backend()?;
56        self.backend.show_cursor()?;
57        Ok(())
58    }
59
60    fn restore_backend() -> io::Result<()> {
61        terminal::disable_raw_mode()?;
62        io::stdout().execute(LeaveAlternateScreen)?;
63        Ok(())
64    }
65
66    pub fn render<F>(&mut self, f: F) -> anyhow::Result<()>
67    where
68        F: FnOnce(&mut Frame),
69    {
70        self.backend.draw(f)?;
71        Ok(())
72    }
73
74    pub fn force_redraw(&mut self) {
75        self.backend.clear().unwrap();
76    }
77
78    #[cfg(feature = "integration")]
79    pub fn buffer(&self) -> &Buffer {
80        self.backend.backend().buffer()
81    }
82}
83
84pub fn event_stream() -> impl Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin {
85    // When tests are run with nix(crane), /dev/tty is not available
86    // In such cases, executing `EventStream::new()` will cause a panic.
87    // Currently, this issue only arises during testing with nix, so an empty stream that does not panic is returned
88    // https://github.com/crossterm-rs/crossterm/blob/fce58c879a748f3159216f68833100aa16141ab0/src/terminal/sys/file_descriptor.rs#L74
89    // https://github.com/crossterm-rs/crossterm/blob/fce58c879a748f3159216f68833100aa16141ab0/src/event/read.rs#L39
90    let is_terminal = std::io::stdout().is_terminal();
91
92    if is_terminal {
93        Either::Left(EventStream::new())
94    } else {
95        Either::Right(stream::empty())
96    }
97}