shadow_terminal/
active_terminal.rs

1//! A convenience module wrapping [`ShadowTerminal`] for running an active shadow terminal
2//! running in a Tokio task.
3//!
4//! The underlying [`Wezterm`] terminal cannnot be interacted with directly. Instead input
5//! and output must be sent and read over channels. This module is more likely useful for
6//! real-world usecases, such as terminal multiplexing for example.
7
8use tracing::Instrument as _;
9
10/// An active terminal is running in a Tokio task, so we don't have direct access to the
11/// underlying `wezterm_term::Terminal`. Instead we interact with it and the PTY through Tokio
12/// channels.
13#[non_exhaustive]
14pub struct ActiveTerminal {
15    /// The task handle to the actively running [`crate::shadow_tty::ShadowTerminal`]
16    pub task_handle: tokio::task::JoinHandle<()>,
17    /// A Tokio channel that receives [`termwiz::surface::Surface`] updates of the underlying
18    /// terminal.
19    pub surface_output_rx: tokio::sync::mpsc::Receiver<crate::output::native::Output>,
20    /// A Tokio channel that forwards bytes to the underlying PTY's STDIN.
21    pub pty_input_tx: tokio::sync::mpsc::Sender<crate::pty::BytesFromSTDIN>,
22    /// A Tokio broadcast sender to send protocol messages that control the shadow terminal and
23    /// PTY. For example; resizing and shutting down.
24    pub control_tx: tokio::sync::broadcast::Sender<crate::Protocol>,
25}
26
27impl ActiveTerminal {
28    /// Start a [`crate::shadow_tty::ShadowTerminal`] running in a Tokio task.
29    #[inline]
30    #[must_use]
31    pub fn start(config: crate::shadow_terminal::Config) -> Self {
32        tracing::debug!("Starting shadow terminal...");
33        let (pty_input_tx, pty_input_rx) = tokio::sync::mpsc::channel(1);
34        let (surface_output_tx, surface_output_rx) = tokio::sync::mpsc::channel(1);
35
36        let mut shadow_terminal =
37            crate::shadow_terminal::ShadowTerminal::new(config, surface_output_tx);
38        let control_tx = shadow_terminal.channels.control_tx.clone();
39
40        let current_span = tracing::Span::current();
41        let task_handle = tokio::spawn(async move {
42            shadow_terminal
43                .run(pty_input_rx)
44                .instrument(current_span)
45                .await;
46        });
47        tracing::debug!("Shadow terminal started.");
48
49        Self {
50            task_handle,
51            surface_output_rx,
52            pty_input_tx,
53            control_tx,
54        }
55    }
56
57    /// Send input directly into the underlying PTY process. This doesn't go through the shadow
58    /// terminal's "frontend".
59    ///
60    /// # Errors
61    /// If sending the bytes fails
62    #[inline]
63    pub async fn send_input(
64        &self,
65        bytes: crate::pty::BytesFromSTDIN,
66    ) -> Result<(), tokio::sync::mpsc::error::SendError<crate::pty::BytesFromSTDIN>> {
67        self.pty_input_tx.send(bytes).await
68    }
69
70    /// End all loops and send OS kill signals to the underlying PTY.
71    ///
72    /// # Errors
73    /// If sending message over channel fails.
74    #[inline]
75    pub fn kill(&self) -> Result<usize, tokio::sync::broadcast::error::SendError<crate::Protocol>> {
76        tracing::debug!("`kill()` called on `ActiveTerminal`");
77        self.control_tx.send(crate::Protocol::End)
78    }
79
80    /// Resize the shadow terminal "frontend". The PTY is agnostic about size.
81    ///
82    /// # Errors
83    /// If sending message over channel fails.
84    #[inline]
85    pub fn resize(
86        &self,
87        width: u16,
88        height: u16,
89    ) -> Result<usize, tokio::sync::broadcast::error::SendError<crate::Protocol>> {
90        self.control_tx
91            .send(crate::Protocol::Resize { width, height })
92    }
93
94    /// Scroll the shadow Wezterm terminal up.
95    ///
96    /// # Errors
97    /// If sending message over channel fails.
98    #[inline]
99    pub fn scroll_up(
100        &self,
101    ) -> Result<usize, tokio::sync::broadcast::error::SendError<crate::Protocol>> {
102        self.control_tx
103            .send(crate::Protocol::Scroll(crate::Scroll::Up))
104    }
105
106    /// Scroll the shadow Wezterm terminal down.
107    ///
108    /// # Errors
109    /// If sending message over channel fails.
110    #[inline]
111    pub fn scroll_down(
112        &self,
113    ) -> Result<usize, tokio::sync::broadcast::error::SendError<crate::Protocol>> {
114        self.control_tx
115            .send(crate::Protocol::Scroll(crate::Scroll::Down))
116    }
117
118    /// Cancel scrolling, and return the scroll to normal.
119    ///
120    /// # Errors
121    /// If sending message over channel fails.
122    #[inline]
123    pub fn scroll_cancel(
124        &self,
125    ) -> Result<usize, tokio::sync::broadcast::error::SendError<crate::Protocol>> {
126        self.control_tx
127            .send(crate::Protocol::Scroll(crate::Scroll::Cancel))
128    }
129}
130
131impl Drop for ActiveTerminal {
132    #[inline]
133    fn drop(&mut self) {
134        let result = self.kill();
135        if let Err(error) = result {
136            tracing::debug!("`ActiveTerminal.drop()`: {error:?}");
137        }
138    }
139}