tastty 0.1.0

Embeddable pseudoterminal sessions for Rust applications
//! Errors produced by PTY sessions, input delivery, resizing, and shutdown.

/// Errors produced by a [`Terminal`](crate::Terminal) and its
/// associated I/O machinery.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
    /// Opening the PTY failed.
    #[error("failed to open PTY: {source}")]
    OpenPtyFailed {
        /// The underlying PTY-open error.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
    /// Disabling PTY echo failed on Unix.
    #[error("failed to disable PTY echo: {source}")]
    DisableEchoFailed {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// [`Builder`](crate::Builder) reached spawn without a program set.
    #[error("Builder has no command set")]
    MissingCommand,
    /// Spawning the child process inside the PTY failed.
    #[error("failed to spawn command in PTY: {source}")]
    SpawnCommandFailed {
        /// The underlying spawn error.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
    /// Cloning the PTY reader failed.
    #[error("failed to clone PTY reader: {source}")]
    CloneReaderFailed {
        /// The underlying PTY error.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
    /// Taking ownership of the PTY writer failed.
    #[error("failed to take PTY writer: {source}")]
    TakeWriterFailed {
        /// The underlying PTY error.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
    /// Spawning a background thread failed.
    #[error("failed to spawn thread '{name}': {source}")]
    ThreadSpawn {
        /// Name assigned to the thread.
        name: &'static str,
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// Sending input failed because the child stdin is closed.
    #[error("send failed: child stdin closed")]
    SendClosed,
    /// Sending input failed because the writer queue is full.
    #[error("send failed: writer queue is full")]
    SendQueueFull,
    /// A sync blocking send was attempted from within a tokio runtime.
    /// `tokio::mpsc::Sender::blocking_send` would panic in that context,
    /// so the call is refused and the async variant must be used instead.
    #[error("blocking send from inside a tokio runtime; use the async variant")]
    BlockingInsideAsync,
    /// The requested resize dimensions are invalid.
    #[error("invalid resize dimensions: rows={rows}, cols={cols}")]
    InvalidResize {
        /// Requested row count.
        rows: u16,
        /// Requested column count.
        cols: u16,
    },
    /// Resizing the PTY failed.
    #[error("failed to resize PTY to {rows}x{cols}: {source}")]
    ResizeFailed {
        /// Requested row count.
        rows: u16,
        /// Requested column count.
        cols: u16,
        /// The underlying resize error.
        #[source]
        source: Box<dyn std::error::Error + Send + Sync>,
    },
    /// The child exit status is unavailable.
    #[error("failed to receive child exit status")]
    ExitStatusUnavailable,
    /// Sending a termination signal to the process group failed.
    #[error("failed to terminate process group: {source}")]
    TerminateFailed {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// Sending a kill signal to the process group failed.
    #[error("failed to force-kill process group: {source}")]
    ForceKillFailed {
        /// The underlying I/O error.
        #[source]
        source: std::io::Error,
    },
    /// A parser, screen, or encoder failure.
    #[error(transparent)]
    Core(#[from] tastty_core::Error),
}

/// Convenient result alias for tastty operations.
pub type Result<T> = std::result::Result<T, Error>;

/// A fatal error from the PTY reader thread.
///
/// Surfaced when `read()` on the PTY master returns an error that is
/// neither [`io::ErrorKind::Interrupted`] nor [`io::ErrorKind::WouldBlock`].
/// The reader thread has exited by the time an embedder observes this
/// value: no further screen updates will arrive, and subsequent
/// [`Terminal::send`](crate::Terminal::send) calls may fail with
/// [`Error::SendClosed`] once the writer side notices the child has
/// gone.
///
/// EOF (a clean child exit on the slave side, which `portable_pty`
/// surfaces as `Ok(0)` after translating EIO) is not delivered through
/// this channel: it stays silent so embedders that already wait on
/// child exit are not woken twice.
///
/// [`io::ErrorKind::Interrupted`]: std::io::ErrorKind::Interrupted
/// [`io::ErrorKind::WouldBlock`]: std::io::ErrorKind::WouldBlock
#[derive(Debug, thiserror::Error)]
#[error("PTY reader thread failed: {source}")]
#[non_exhaustive]
pub struct ReaderError {
    /// `io::ErrorKind` of the failing read, broken out of `source` so
    /// embedders can pattern-match without dereferencing the chain.
    pub kind: std::io::ErrorKind,
    /// The underlying I/O error preserved for `#[source]` chaining.
    #[source]
    pub source: std::io::Error,
}

/// Operation being performed by the PTY writer thread when it failed.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WriterOperation {
    /// Writing queued bytes to the child failed.
    Write,
    /// Flushing bytes to the child failed.
    Flush,
}

impl std::fmt::Display for WriterOperation {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::Write => f.write_str("write"),
            Self::Flush => f.write_str("flush"),
        }
    }
}

/// A fatal error from the PTY writer thread.
///
/// Surfaced when either writing queued bytes or flushing them to the PTY
/// master fails. The writer thread exits immediately after emitting this
/// value, so future sends may still succeed briefly if they only reach the
/// bounded queue before the sender observes the closed writer side.
#[derive(Debug, thiserror::Error)]
#[error("PTY writer thread failed during {operation}: {source}")]
#[non_exhaustive]
pub struct WriterError {
    /// Distinguishes data-write failures from flush failures.
    pub operation: WriterOperation,
    /// `io::ErrorKind` of the failing write or flush, broken out of
    /// `source` so embedders can pattern-match without dereferencing the
    /// chain.
    pub kind: std::io::ErrorKind,
    /// Original writer error, including OS error code when available.
    #[source]
    pub source: std::io::Error,
}

/// Fatal I/O-thread errors surfaced by a [`Terminal`](crate::Terminal).
///
/// Reader and writer failures share this stream so embedders have one
/// observable path for terminal I/O health. Clean reader EOF remains silent.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum IoError {
    /// Fatal reader-thread failure.
    #[error(transparent)]
    Reader(#[from] ReaderError),
    /// Fatal writer-thread failure.
    #[error(transparent)]
    Writer(#[from] WriterError),
}

/// Receiver for fatal terminal I/O-thread errors.
pub type IoErrorReceiver = tokio::sync::mpsc::UnboundedReceiver<IoError>;