//! Generic **PTY ↔ Tauri Channel** bridge infrastructure. //! //! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over //! per-session [`tauri::ipc::Channel`]s rather than global events, for //! throughput and isolation. This module provides the transport-side plumbing //! that L3 will plug a real `PtyPort` into; here there is **no real PTY** yet — //! only the registry + the abstraction that routes byte chunks to the right //! frontend channel. //! //! Design: //! - The frontend opens a terminal and passes a [`tauri::ipc::Channel`] for that //! session. The backend registers it in [`PtyBridge`] keyed by `SessionId`. //! - Whatever produces output (the PTY adapter in L3) calls //! [`PtyBridge::send_output`], which forwards the bytes on the matching //! channel. Bytes are sent as-is; the frontend xterm wrapper consumes them. //! - [`PtyBridge::unregister`] tears the channel down on terminal close. use std::collections::HashMap; use std::sync::Mutex; use tauri::ipc::Channel; use domain::ids::SessionId; /// A chunk of PTY output bytes destined for a specific session's channel. /// /// Sent as a raw byte vector; serde encodes it for the IPC channel. Kept as a /// distinct type so the wire shape can evolve (e.g. add a sequence number for /// backpressure handling) without touching call sites. pub type PtyChunk = Vec; /// Registry mapping live terminal sessions to their output [`Channel`]. /// /// Thread-safe; cloned `Arc` is held in [`crate::state::AppState`]. #[derive(Default)] pub struct PtyBridge { /// Per session: a monotonically-increasing **generation** plus the current /// output channel. The generation distinguishes successive (re-)attaches so a /// superseded pump thread can't tear down the channel of the attach that /// replaced it (see [`PtyBridge::unregister_if`]). channels: Mutex)>>, } impl PtyBridge { /// Creates an empty bridge. #[must_use] pub fn new() -> Self { Self { channels: Mutex::new(HashMap::new()), } } /// Registers (or replaces) the output channel for a session and returns the /// **generation** of this registration. Each call for a session bumps the /// generation, so the caller's pump thread can later tear down *only its own* /// registration via [`unregister_if`](Self::unregister_if). pub fn register(&self, session: SessionId, channel: Channel) -> u64 { if let Ok(mut map) = self.channels.lock() { let gen = map.get(&session).map_or(0, |(g, _)| g.wrapping_add(1)); map.insert(session, (gen, channel)); gen } else { 0 } } /// Removes a session's channel unconditionally (terminal explicitly closed). pub fn unregister(&self, session: &SessionId) { if let Ok(mut map) = self.channels.lock() { map.remove(session); } } /// Removes a session's channel **only if** `gen` is still the current /// generation. A pump thread calls this when its output stream ends: if the /// session has since been re-attached (newer generation), this is a no-op, so /// the dying thread never unregisters the live channel that superseded it. pub fn unregister_if(&self, session: &SessionId, gen: u64) { if let Ok(mut map) = self.channels.lock() { if matches!(map.get(session), Some((g, _)) if *g == gen) { map.remove(session); } } } /// Forwards a chunk of output bytes to a session's channel. /// /// Returns `true` if the chunk was delivered, `false` if no channel is /// registered for the session (e.g. already closed). In L3 the PTY adapter's /// output stream drives this. pub fn send_output(&self, session: &SessionId, chunk: PtyChunk) -> bool { let Ok(map) = self.channels.lock() else { return false; }; match map.get(session) { Some((_, channel)) => channel.send(chunk).is_ok(), None => false, } } /// Number of currently-registered sessions (handy for tests/diagnostics). #[must_use] pub fn active_sessions(&self) -> usize { self.channels.lock().map(|m| m.len()).unwrap_or(0) } }