Skip to main content

tui/runtime/
terminal_runtime.rs

1use crossterm::event::Event as CrosstermEvent;
2use std::io::{self, Write};
3use std::process::{Command, ExitStatus};
4
5use crate::rendering::frame::Frame;
6use crate::rendering::render_context::ViewContext;
7use crate::rendering::renderer::{Renderer, RendererCommand};
8use crate::theme::Theme;
9
10use super::terminal::{MouseCapture, TerminalSession};
11use super::{EventTaskHandle, spawn_terminal_event_task};
12
13pub struct TerminalConfig {
14    pub bracketed_paste: bool,
15    pub mouse_capture: MouseCapture,
16}
17
18pub struct TerminalRuntime<T: Write> {
19    renderer: Renderer<T>,
20    session: Option<TerminalSession>,
21    event_task: Option<EventTaskHandle>,
22    config: TerminalConfig,
23}
24
25impl<T: Write> TerminalRuntime<T> {
26    pub fn new(writer: T, theme: Theme, size: (u16, u16), config: TerminalConfig) -> io::Result<Self> {
27        let renderer = Renderer::new(writer, theme, size);
28        let session = Some(TerminalSession::new(config.bracketed_paste, config.mouse_capture)?);
29        let event_task = Some(spawn_terminal_event_task());
30        Ok(Self { renderer, session, event_task, config })
31    }
32
33    pub fn headless(writer: T, size: (u16, u16)) -> Self {
34        let config = TerminalConfig { bracketed_paste: false, mouse_capture: MouseCapture::Disabled };
35        Self { renderer: Renderer::new(writer, Theme::default(), size), session: None, event_task: None, config }
36    }
37
38    pub async fn next_event(&mut self) -> Option<CrosstermEvent> {
39        let task = self.event_task.as_mut()?;
40        task.rx().recv().await
41    }
42
43    pub fn try_next_event(&mut self) -> Option<CrosstermEvent> {
44        self.event_task.as_mut()?.rx().try_recv().ok()
45    }
46
47    pub fn render_frame(&mut self, f: impl FnOnce(&ViewContext) -> Frame) -> io::Result<()> {
48        self.renderer.render_frame(f)
49    }
50
51    pub fn clear_screen(&mut self) -> io::Result<()> {
52        self.renderer.clear_screen()
53    }
54
55    pub fn on_resize(&mut self, size: (u16, u16)) {
56        self.renderer.on_resize(size);
57    }
58
59    pub fn apply_commands(&mut self, commands: Vec<RendererCommand>) -> io::Result<()> {
60        self.renderer.apply_commands(commands)
61    }
62
63    pub async fn suspend(&mut self) -> io::Result<SuspendedTerminal<'_, T>> {
64        let was_headless = self.session.is_none();
65        if let Some(handle) = self.event_task.take() {
66            handle.stop().await;
67        }
68        drop(self.session.take());
69        Ok(SuspendedTerminal { runtime: self, resumed: false, was_headless })
70    }
71
72    pub async fn run_external(&mut self, mut command: Command) -> io::Result<ExitStatus> {
73        let mut suspended = self.suspend().await?;
74        let status = command.status()?;
75        suspended.resume()?;
76        Ok(status)
77    }
78}
79
80impl<T: Write> Drop for TerminalRuntime<T> {
81    fn drop(&mut self) {
82        if let Some(ref handle) = self.event_task {
83            handle.cancel();
84        }
85        let _ = self.renderer.cleanup();
86    }
87}
88
89pub struct SuspendedTerminal<'a, T: Write> {
90    runtime: &'a mut TerminalRuntime<T>,
91    resumed: bool,
92    was_headless: bool,
93}
94
95impl<T: Write> SuspendedTerminal<'_, T> {
96    pub fn resume(&mut self) -> io::Result<()> {
97        if self.resumed {
98            return Ok(());
99        }
100
101        if self.was_headless {
102            self.resumed = true;
103            return Ok(());
104        }
105
106        self.runtime.session =
107            Some(TerminalSession::new(self.runtime.config.bracketed_paste, self.runtime.config.mouse_capture)?);
108        self.runtime.event_task = Some(spawn_terminal_event_task());
109        self.runtime.renderer.clear_screen()?;
110        self.resumed = true;
111        Ok(())
112    }
113}
114
115impl<T: Write> Drop for SuspendedTerminal<'_, T> {
116    fn drop(&mut self) {
117        if self.resumed {
118            return;
119        }
120
121        if self.was_headless {
122            self.resumed = true;
123            return;
124        }
125
126        if self.runtime.session.is_none()
127            && let Ok(session) =
128                TerminalSession::new(self.runtime.config.bracketed_paste, self.runtime.config.mouse_capture)
129        {
130            self.runtime.session = Some(session);
131        }
132
133        if self.runtime.session.is_some() && self.runtime.event_task.is_none() {
134            self.runtime.event_task = Some(spawn_terminal_event_task());
135        }
136
137        let _ = self.runtime.renderer.clear_screen();
138        self.resumed = true;
139    }
140}