tui/runtime/
terminal_runtime.rs1use 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 drop(self.event_task.take());
83 let _ = self.renderer.cleanup();
84 }
85}
86
87pub struct SuspendedTerminal<'a, T: Write> {
88 runtime: &'a mut TerminalRuntime<T>,
89 resumed: bool,
90 was_headless: bool,
91}
92
93impl<T: Write> SuspendedTerminal<'_, T> {
94 pub fn resume(&mut self) -> io::Result<()> {
95 if self.resumed {
96 return Ok(());
97 }
98
99 if self.was_headless {
100 self.resumed = true;
101 return Ok(());
102 }
103
104 self.runtime.session =
105 Some(TerminalSession::new(self.runtime.config.bracketed_paste, self.runtime.config.mouse_capture)?);
106 self.runtime.event_task = Some(spawn_terminal_event_task());
107 self.runtime.renderer.clear_screen()?;
108 self.resumed = true;
109 Ok(())
110 }
111}
112
113impl<T: Write> Drop for SuspendedTerminal<'_, T> {
114 fn drop(&mut self) {
115 if self.resumed {
116 return;
117 }
118
119 if self.was_headless {
120 self.resumed = true;
121 return;
122 }
123
124 if self.runtime.session.is_none()
125 && let Ok(session) =
126 TerminalSession::new(self.runtime.config.bracketed_paste, self.runtime.config.mouse_capture)
127 {
128 self.runtime.session = Some(session);
129 }
130
131 if self.runtime.session.is_some() && self.runtime.event_task.is_none() {
132 self.runtime.event_task = Some(spawn_terminal_event_task());
133 }
134
135 let _ = self.runtime.renderer.clear_screen();
136 self.resumed = true;
137 }
138}