pub mod buffer;
pub mod ipc_trait;
pub mod protocol;
pub mod pty_manager;
pub mod state;
#[cfg(test)]
mod tests;
pub use buffer::{ScrollbackBuffer, ScrollbackLine};
pub use ipc_trait::{DirectChannel, IpcChannel};
pub use protocol::{ControlMessage, TerminalInput, TerminalMetadata, TerminalOutput};
pub use pty_manager::{PtyHandle, PtyManager};
pub use state::TerminalState;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::sync::RwLock;
#[allow(async_fn_in_trait)]
pub trait PtyHandleExt {
async fn write(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>>;
async fn kill(&self) -> Result<(), Box<dyn std::error::Error>>;
}
impl PtyHandleExt for PtyHandle {
async fn write(&self, data: &[u8]) -> Result<(), Box<dyn std::error::Error>> {
self.send_input(bytes::Bytes::from(data.to_vec()))
.await
.map_err(|e| e.into())
}
async fn kill(&self) -> Result<(), Box<dyn std::error::Error>> {
self.shutdown().await.map_err(|e| e.into())
}
}
pub struct TerminalStreamManager {
pty_manager: Arc<PtyManager>,
ipc_channel: Arc<dyn IpcChannel>,
terminals: Arc<RwLock<HashMap<String, TerminalState>>>,
pty_handles: Arc<RwLock<HashMap<String, PtyHandle>>>,
scrollback_buffers: Arc<RwLock<HashMap<String, ScrollbackBuffer>>>,
}
impl TerminalStreamManager {
pub fn with_ipc_channel(ipc_channel: Arc<dyn IpcChannel>) -> Self {
let pty_manager = Arc::new(PtyManager::new());
Self {
pty_manager,
ipc_channel,
terminals: Arc::new(RwLock::new(HashMap::new())),
pty_handles: Arc::new(RwLock::new(HashMap::new())),
scrollback_buffers: Arc::new(RwLock::new(HashMap::new())),
}
}
pub async fn create_terminal(
&self,
terminal_id: String,
shell: Option<String>,
rows: u16,
cols: u16,
) -> Result<PtyHandle, Box<dyn std::error::Error>> {
let pty_handle = self
.pty_manager
.create_pty(terminal_id.clone(), shell, rows, cols)
.await?;
let terminal_state = TerminalState::new(terminal_id.clone(), rows, cols);
self.terminals
.write()
.await
.insert(terminal_id.clone(), terminal_state);
self.pty_handles
.write()
.await
.insert(terminal_id.clone(), pty_handle.clone());
let scrollback_buffer = ScrollbackBuffer::with_default_size();
self.scrollback_buffers
.write()
.await
.insert(terminal_id.clone(), scrollback_buffer);
self.ipc_channel
.start_streaming(terminal_id, pty_handle.clone())
.await?;
Ok(pty_handle)
}
pub async fn send_input(
&self,
terminal_id: &str,
input: TerminalInput,
) -> Result<(), Box<dyn std::error::Error>> {
let handles = self.pty_handles.read().await;
let pty_handle = handles
.get(terminal_id)
.ok_or_else(|| format!("Terminal {terminal_id} not found"))?;
match input {
TerminalInput::Text(text) => {
pty_handle.write(text.as_bytes()).await?;
}
TerminalInput::Binary(data) => {
pty_handle.write(&data).await?;
}
TerminalInput::SpecialKey(key) => {
pty_handle.write(key.as_bytes()).await?;
}
}
Ok(())
}
pub async fn send_control(
&self,
terminal_id: &str,
control: ControlMessage,
) -> Result<(), Box<dyn std::error::Error>> {
match control {
ControlMessage::Resize { rows, cols } => {
let handles = self.pty_handles.read().await;
if let Some(pty_handle) = handles.get(terminal_id) {
pty_handle.resize(rows, cols).await?;
let mut terminals = self.terminals.write().await;
if let Some(state) = terminals.get_mut(terminal_id) {
state.resize(rows, cols);
}
}
}
_ => {
}
}
Ok(())
}
pub async fn kill_terminal(&self, terminal_id: &str) -> Result<(), Box<dyn std::error::Error>> {
self.ipc_channel.stop_streaming(terminal_id).await?;
if let Some(handle) = self.pty_handles.write().await.remove(terminal_id) {
handle.kill().await?;
}
self.terminals.write().await.remove(terminal_id);
self.scrollback_buffers.write().await.remove(terminal_id);
Ok(())
}
pub async fn list_terminals(&self) -> Vec<String> {
self.terminals.read().await.keys().cloned().collect()
}
}