freya_terminal/
handle.rs

1use std::{
2    io::Write,
3    sync::{
4        Arc,
5        Mutex,
6    },
7};
8
9use freya_core::{
10    notify::ArcNotify,
11    prelude::{
12        TaskHandle,
13        UseId,
14    },
15};
16use futures_channel::mpsc::UnboundedSender;
17
18use crate::{
19    buffer::TerminalBuffer,
20    pty::spawn_pty,
21};
22
23type ResizeSender = Arc<UnboundedSender<(u16, u16)>>;
24
25/// Unique identifier for a terminal instance
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
27pub struct TerminalId(pub usize);
28
29impl TerminalId {
30    pub fn new() -> Self {
31        Self(UseId::<TerminalId>::get_in_hook())
32    }
33}
34
35impl Default for TerminalId {
36    fn default() -> Self {
37        Self::new()
38    }
39}
40
41/// Error type for terminal operations
42#[derive(Debug, thiserror::Error)]
43pub enum TerminalError {
44    #[error("PTY error: {0}")]
45    PtyError(String),
46
47    #[error("Write error: {0}")]
48    WriteError(String),
49
50    #[error("Terminal not initialized")]
51    NotInitialized,
52}
53
54/// Internal cleanup handler for terminal resources.
55pub(crate) struct TerminalCleaner {
56    /// Writer handle for the PTY.
57    pub(crate) writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
58    /// Task handle for the terminal reader task.
59    pub(crate) task: TaskHandle,
60    /// Notifier that signals when the terminal should close.
61    pub(crate) closer_notifier: ArcNotify,
62}
63
64impl Drop for TerminalCleaner {
65    fn drop(&mut self) {
66        *self.writer.lock().unwrap() = None;
67        self.task.try_cancel();
68        self.closer_notifier.notify();
69    }
70}
71
72/// Handle to a running terminal instance.
73///
74/// The handle allows you to write input to the terminal and resize it.
75/// Multiple Terminal components can share the same handle.
76///
77/// The PTY is automatically closed when the handle is dropped.
78#[derive(Clone)]
79#[allow(dead_code)]
80pub struct TerminalHandle {
81    /// Unique identifier for this terminal instance.
82    pub(crate) id: TerminalId,
83    /// Terminal buffer containing the current screen state.
84    pub(crate) buffer: Arc<Mutex<TerminalBuffer>>,
85    /// Writer for sending input to the PTY process.
86    pub(crate) writer: Arc<Mutex<Option<Box<dyn Write + Send>>>>,
87    /// Channel for sending resize events to the PTY.
88    pub(crate) resize_sender: ResizeSender,
89    /// Notifier that signals when the terminal/PTY closes.
90    pub(crate) closer_notifier: ArcNotify,
91    /// Handles cleanup when the terminal is dropped.
92    pub(crate) cleaner: Arc<TerminalCleaner>,
93}
94
95impl PartialEq for TerminalHandle {
96    fn eq(&self, other: &Self) -> bool {
97        self.id == other.id
98    }
99}
100
101impl TerminalHandle {
102    /// Create a new terminal with the specified command.
103    ///
104    /// # Example
105    ///
106    /// ```rust,no_run
107    /// use freya_terminal::prelude::*;
108    /// use portable_pty::CommandBuilder;
109    ///
110    /// let mut cmd = CommandBuilder::new("bash");
111    /// cmd.env("TERM", "xterm-256color");
112    ///
113    /// let handle = TerminalHandle::new(cmd).unwrap();
114    /// ```
115    pub fn new(command: portable_pty::CommandBuilder) -> Result<Self, TerminalError> {
116        spawn_pty(command)
117    }
118
119    /// Write data to the terminal.
120    ///
121    /// # Example
122    ///
123    /// ```rust,no_run
124    /// # use freya_terminal::prelude::*;
125    /// # let handle: TerminalHandle = unimplemented!();
126    /// handle.write(b"ls -la\n").unwrap();
127    /// ```
128    pub fn write(&self, data: &[u8]) -> Result<(), TerminalError> {
129        match self.writer.lock() {
130            Ok(mut guard) => match guard.as_mut() {
131                Some(w) => {
132                    w.write_all(data)
133                        .map_err(|e| TerminalError::WriteError(e.to_string()))?;
134                    w.flush()
135                        .map_err(|e| TerminalError::WriteError(e.to_string()))?;
136                    Ok(())
137                }
138                None => Err(TerminalError::NotInitialized),
139            },
140            Err(_) => Err(TerminalError::WriteError("Lock poisoned".to_string())),
141        }
142    }
143
144    /// Resize the terminal to the specified rows and columns.
145    ///
146    /// # Example
147    ///
148    /// ```rust,no_run
149    /// # use freya_terminal::prelude::*;
150    /// # let handle: TerminalHandle = unimplemented!();
151    /// handle.resize(24, 80);
152    /// ```
153    pub fn resize(&self, rows: u16, cols: u16) {
154        let _ = self.resize_sender.unbounded_send((rows, cols));
155    }
156
157    /// Read the current terminal buffer.
158    pub fn read_buffer(&self) -> TerminalBuffer {
159        self.buffer.lock().unwrap().clone()
160    }
161
162    /// Returns a future that completes when the terminal/PTY closes.
163    ///
164    /// This can be used to detect when the shell process exits and update the UI accordingly.
165    ///
166    /// # Example
167    ///
168    /// ```rust,ignore
169    /// use_future(move || async move {
170    ///     terminal_handle.closed().await;
171    ///     // Terminal has exited, update UI state
172    /// });
173    /// ```
174    pub fn closed(&self) -> impl std::future::Future<Output = ()> + '_ {
175        self.closer_notifier.notified()
176    }
177
178    /// Returns the unique identifier for this terminal instance.
179    pub fn id(&self) -> TerminalId {
180        self.id
181    }
182}