tastty-driver 0.1.0

Terminal automation driver built on tastty
//! Typed input: bytes, text, keys, paste, and focus reports.

use super::Session;
use crate::{Error, InputSegment, Result};
use tastty::{IoError, IoErrorReceiver, KeyEvent};

impl Session {
    /// Returns a fatal terminal I/O-thread error if one has been observed.
    ///
    /// Each reader or writer thread emits at most one [`IoError`] before
    /// exiting. Reader EOF remains silent.
    #[must_use]
    pub fn try_recv_io_error(&self) -> Option<IoError> {
        self.terminal.try_recv_io_error()
    }

    /// Takes ownership of the terminal I/O-error receiver.
    ///
    /// Returns `None` if the receiver was already taken by a previous call.
    /// After this call, [`Session::try_recv_io_error`] will always return
    /// `None`.
    pub fn take_io_error_receiver(&self) -> Option<IoErrorReceiver> {
        self.terminal.take_io_error_receiver()
    }

    /// Send raw bytes to the child.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Send`] when the writer side is gone (child
    /// exited, session terminated) or the writer queue is full.
    pub fn send_bytes(&self, bytes: &[u8]) -> Result<()> {
        self.terminal.send(bytes).map_err(Error::Send)
    }

    /// Send UTF-8 text to the child.
    ///
    /// # Errors
    ///
    /// Same as [`Session::send_bytes`].
    pub fn send_text(&self, text: &str) -> Result<()> {
        self.send_bytes(text.as_bytes())
    }

    /// Encode and send a key event to the child.
    ///
    /// # Errors
    ///
    /// Returns [`Error::SendKey`] when the encoded bytes cannot be
    /// written (writer side gone or queue full).
    pub fn send_key(&self, key: KeyEvent) -> Result<()> {
        self.terminal.send_key(key).map_err(Error::SendKey)
    }

    /// Send a mixed input sequence.
    ///
    /// # Errors
    ///
    /// Returns the first error encountered while delivering segments:
    /// [`Error::Send`] for [`InputSegment::Text`] / [`InputSegment::Bytes`]
    /// or [`Error::SendKey`] for [`InputSegment::Key`]. Earlier segments
    /// in the slice are delivered before the error surfaces.
    pub fn send_input(&self, segments: &[InputSegment]) -> Result<()> {
        for segment in segments {
            match segment {
                InputSegment::Text(text) => self.send_text(text)?,
                InputSegment::Key(key) => self.send_key(*key)?,
                InputSegment::Bytes(bytes) => self.send_bytes(bytes)?,
            }
        }
        Ok(())
    }

    /// Parse `input` with [`parse_input`](crate::parse_input) and forward
    /// the resulting segments through [`Session::send_input`].
    ///
    /// Sugar for the common script-runner shape `parse_input(s)?` ->
    /// `send_input(&segments)`. The grammar is the angle-bracket
    /// syntax: literal text passes through verbatim, and `<...>` tags
    /// resolve to key events. Recognised tags include `<Enter>`,
    /// `<Tab>`, `<Esc>`, `<Backspace>`, `<Space>`, arrow and navigation
    /// keys (`<Up>`, `<PageDown>`, ...), function keys `<F1>`..`<F24>`,
    /// and any single character with optional modifiers
    /// (`<Ctrl-c>`, `<Alt-Shift-x>`, `<Shift-Tab>`).
    ///
    /// # Errors
    ///
    /// Returns [`Error::ParseInput`] if `input` contains an unclosed
    /// `<...>` tag or an unrecognised key name. Otherwise propagates
    /// the same errors as [`Session::send_input`].
    pub fn send_keys(&self, input: &str) -> Result<()> {
        let segments = crate::parse_input(input)?;
        self.send_input(&segments)
    }

    /// Send `text` as a paste, framing it with bracketed-paste markers
    /// when the child has enabled them and rewriting bare LF to CR
    /// otherwise so receiving shells commit each line.
    ///
    /// This is the paste-aware sibling of [`Session::send_text`]: that
    /// method writes the bytes verbatim (intended for short typed strings
    /// and pre-encoded escape sequences); `send_paste` is the right call
    /// for any payload the user thinks of as "pasted content", whether
    /// from a clipboard, a multi-line script, or a captured file.
    ///
    /// The byte assembly is delegated to `tastty_core::frame::bracketed_paste`,
    /// which strips embedded `ESC[200~` / `ESC[201~` markers and most C0
    /// control bytes from the payload before assembly.
    ///
    /// Blocks if the writer queue is full and rejects calls made from
    /// inside a Tokio runtime to avoid panicking the executor; callers
    /// driven by an async runtime should use
    /// [`Session::send_paste_async`] when the `async` feature is
    /// enabled.
    ///
    /// # Errors
    ///
    /// Returns [`Error::SendPaste`] wrapping
    /// [`tastty::Error::SendClosed`] when the writer side is gone, or
    /// [`tastty::Error::BlockingInsideAsync`] when called from inside a
    /// Tokio runtime.
    pub fn send_paste(&self, text: &str) -> Result<()> {
        self.terminal.send_paste(text).map_err(Error::SendPaste)
    }

    /// Asynchronous variant of [`Session::send_paste`].
    ///
    /// The bracketed-paste frame is built and the writer handle is
    /// cloned synchronously up-front, then the writer-queue insert is
    /// the only work awaited. This means concurrent [`Session`] callers
    /// are not blocked behind this paste's `.await`, and the future is
    /// a clean `tokio::select!` arm: backed by
    /// `tokio::sync::mpsc::Sender::send`, dropping the future neither
    /// queues nor drops the bytes.
    ///
    /// # Errors
    ///
    /// Returns [`Error::SendPaste`] wrapping [`tastty::Error::SendClosed`]
    /// when the writer side is gone before or during the await.
    #[cfg(feature = "async")]
    pub async fn send_paste_async(&self, text: &str) -> Result<()> {
        let writer = self
            .terminal
            .writer()
            .ok_or_else(|| Error::SendPaste(tastty::Error::SendClosed))?;
        let frame = self.terminal.paste_frame(text);
        writer.send(&frame).await.map_err(Error::SendPaste)
    }

    /// Deliver a focus-in report when the child has enabled focus
    /// reporting (DEC private mode 1004).
    ///
    /// Sends the `ESC [ I` byte sequence; a no-op when the child has
    /// not opted into focus reports. Inherits the blocking semantics
    /// of [`Session::send_bytes`]; for async callers see
    /// [`Session::send_focus_in_async`].
    ///
    /// # Errors
    ///
    /// Returns [`Error::Send`] wrapping
    /// [`tastty::Error::BlockingInsideAsync`] when invoked from inside
    /// a Tokio runtime, or [`tastty::Error::SendClosed`] when the
    /// writer thread is gone.
    pub fn send_focus_in(&self) -> Result<()> {
        self.terminal.send_focus(true).map_err(Error::Send)
    }

    /// Deliver a focus-out report when the child has enabled focus
    /// reporting (DEC private mode 1004).
    ///
    /// Sends the `ESC [ O` byte sequence; a no-op when the child has
    /// not opted into focus reports. Inherits the blocking semantics
    /// of [`Session::send_bytes`]; for async callers see
    /// [`Session::send_focus_out_async`].
    ///
    /// # Errors
    ///
    /// Returns [`Error::Send`] wrapping
    /// [`tastty::Error::BlockingInsideAsync`] when invoked from inside
    /// a Tokio runtime, or [`tastty::Error::SendClosed`] when the
    /// writer thread is gone.
    pub fn send_focus_out(&self) -> Result<()> {
        self.terminal.send_focus(false).map_err(Error::Send)
    }

    /// Asynchronous variant of [`Session::send_focus_in`].
    ///
    /// The focus-report frame is resolved and the writer handle is
    /// cloned synchronously up-front, then the writer-queue insert is
    /// the only work awaited. A no-op when the child has not enabled
    /// focus reporting.
    ///
    /// # Errors
    ///
    /// Returns [`Error::Send`] wrapping [`tastty::Error::SendClosed`]
    /// when the writer side is gone.
    #[cfg(feature = "async")]
    pub async fn send_focus_in_async(&self) -> Result<()> {
        self.send_focus_async_inner(true).await
    }

    /// Asynchronous variant of [`Session::send_focus_out`].
    ///
    /// Cancellation-safety mirrors [`Session::send_focus_in_async`].
    ///
    /// # Errors
    ///
    /// Same as [`Session::send_focus_in_async`].
    #[cfg(feature = "async")]
    pub async fn send_focus_out_async(&self) -> Result<()> {
        self.send_focus_async_inner(false).await
    }

    #[cfg(feature = "async")]
    async fn send_focus_async_inner(&self, gained: bool) -> Result<()> {
        let Some(bytes) = self.terminal.focus_frame(gained) else {
            return Ok(());
        };
        let writer = self
            .terminal
            .writer()
            .ok_or_else(|| Error::Send(tastty::Error::SendClosed))?;
        writer.send(bytes).await.map_err(Error::Send)
    }
}