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}