Files
IdeA/crates/app-tauri/src/pty.rs
Blomios c8fef2a76a refactor(backend): abstraire le sink de stream hors de tauri::ipc::Channel (#13)
Lot B2 du chantier server/client mode : le cœur backend émet désormais ses
flux via une abstraction de sink agnostique, sans dépendre directement de
tauri::ipc::Channel, préalable au futur serveur web + PTY WebSocket.

- crates/backend : abstraction de sink (stream.rs) câblée dans lib.rs.
- crates/app-tauri : implémentation Tauri du sink (stream.rs) et adaptation
  des surfaces lib.rs, pty.rs, chat.rs.
- Nettoyage clippy des 2 warnings B2.

Validé : cargo check --workspace vert, tests backend/app-tauri verts,
cœur agnostique Tauri, clippy B2 propre.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-15 12:50:29 +02:00

86 lines
3.3 KiB
Rust

//! Generic **PTY ↔ outbound stream sink** bridge infrastructure.
//!
//! ARCHITECTURE §2 decides that high-frequency PTY byte streams travel over a
//! per-session stream channel rather than global events, for throughput and
//! isolation. This module provides the desktop-facing plumbing that adapts the
//! shared backend sink abstraction to Tauri IPC.
//!
//! Design:
//! - The frontend opens a terminal and passes a [`tauri::ipc::Channel`] for that
//! session. The adapter wraps it as a sink and registers it in [`PtyBridge`].
//! - Whatever produces output calls [`PtyBridge::send_output`], which forwards
//! the bytes through the current sink. Bytes are sent as-is; the frontend
//! xterm wrapper consumes them.
//! - [`PtyBridge::unregister`] tears the channel down on terminal close.
use std::sync::Arc;
use backend::stream::OutputBridge;
use tauri::ipc::Channel;
use domain::ids::SessionId;
use crate::stream::TauriChannelSink;
/// 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<u8>;
/// Registry mapping live terminal sessions to their output sink.
///
/// Thread-safe; cloned `Arc<PtyBridge>` is held in [`crate::state::AppState`].
#[derive(Default)]
pub struct PtyBridge {
inner: OutputBridge<SessionId, PtyChunk>,
}
impl PtyBridge {
/// Creates an empty bridge.
#[must_use]
pub fn new() -> Self {
Self {
inner: OutputBridge::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<PtyChunk>) -> u64 {
self.inner
.register(session, Arc::new(TauriChannelSink::new(channel)))
}
/// Removes a session's channel unconditionally (terminal explicitly closed).
pub fn unregister(&self, session: &SessionId) {
self.inner.unregister(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) {
self.inner.unregister_if(session, gen);
}
/// 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 {
self.inner.send_output(session, chunk)
}
/// Number of currently-registered sessions (handy for tests/diagnostics).
#[must_use]
pub fn active_sessions(&self) -> usize {
self.inner.active_sessions()
}
}