quvyta-framework 0.1.35

A Rust framework for building terminal applications
//! A terminal session whose program runs in another process, reached through a carrier: a desktop
//! whose programs outlive the connection to its screen keeps them in a process of their own, and a
//! screen that attaches draws them through one of these.

use std::io::{self, Read};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use super::terminal_session::{Backend, SCROLLBACK, SIZE, Shared, TerminalSession, lock, read_output};

/// What a [`TerminalSession`] whose program lives in another process sends to it: the keys and
/// pastes, the size of the screen and the request to end it. The program's output comes the other
/// way, as the reader given to [`TerminalSession::attach`].
///
/// The application implements it on whatever reaches the program, a socket to the process that
/// runs it, say, with the framing that process reads. The session calls it on the thread that
/// writes, resizes or ends; a call that waits holds that thread, so a carrier that cannot write at
/// once is better off queueing the bytes than blocking.
pub trait TerminalCarrier: Send + 'static {
    /// Sends `bytes` to the program as its input: the person's keys, a paste, a report the
    /// terminal makes on its own account.
    ///
    /// # Errors
    ///
    /// Whatever went wrong on the way; the session hands it back to the caller of
    /// [`TerminalSession::write`] or [`TerminalSession::paste`].
    fn write(&mut self, bytes: &[u8]) -> io::Result<()>;

    /// Tells the program its screen is now `columns` by `rows`, as a pseudo-terminal's resize
    /// does. Called by the session's watch, never while drawing.
    ///
    /// # Errors
    ///
    /// Whatever went wrong on the way; the session draws the new size either way.
    fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()>;

    /// Asks for the program to be ended: [`TerminalSession::kill`] and
    /// [`TerminalSession::terminate`] of an attached session come here. Letting the session go
    /// does not, since the program is meant to outlive the screen that showed it.
    fn end(&mut self);

    /// The program's exit code, asked once its output has ended: what the process that ran it
    /// says it ended with, or `None` when it cannot say, as when the connection to it closed with
    /// the program still running. [`TerminalWatch`](super::TerminalWatch) reports it as
    /// [`Exited`](super::TerminalChange::Exited). Default: `None`.
    fn exit_code(&mut self) -> Option<u32> {
        None
    }
}

/// How to attach a [`TerminalSession`] to a program in another process: the screen size, the
/// scrollback and how often output is reported. Made by [`TerminalSession::attach`].
///
/// The screen must start at the size the program's own session has, since what it is sent first,
/// that session's [`snapshot`](TerminalSession::snapshot), draws a screen of that size; a
/// [`Terminal`](super::Terminal) widget resizes it to its own size afterwards, as it does any
/// session.
#[must_use]
pub struct AttachedBuilder<R, C> {
    output: R,
    carrier: C,
    size: (u16, u16),
    scrollback: usize,
    coalesce: Duration,
}

impl<R: Read + Send + 'static, C: TerminalCarrier> AttachedBuilder<R, C> {
    /// The size of the screen the program draws on now, in columns and rows. Sizes below 2 count
    /// as 2. Default: 80 × 24.
    pub fn size(mut self, columns: u16, rows: u16) -> Self {
        self.size = (rows, columns);
        self
    }

    /// Lines kept above the screen for scrolling back; `0` keeps none. To keep all of the history
    /// a snapshot sends, keep at least as many as the program's own session does. Default: 5000.
    pub fn scrollback(mut self, lines: usize) -> Self {
        self.scrollback = lines;
        self
    }

    /// Reports output at most once per `interval`, as
    /// [`TerminalBuilder::coalesce`](super::TerminalBuilder::coalesce) does. Default: zero.
    pub fn coalesce(mut self, interval: Duration) -> Self {
        self.coalesce = interval;
        self
    }

    /// Starts reading the program's output and returns the session.
    pub fn start(self) -> TerminalSession {
        let size = (self.size.0.max(2), self.size.1.max(2));
        let shared = Shared::new(size, self.scrollback, self.coalesce);
        let backend = Arc::new(Mutex::new(Backend::Carried(Box::new(self.carrier))));
        read_output(Box::new(self.output), Arc::clone(&shared), Arc::downgrade(&backend), None, |backend| {
            let backend = backend.upgrade()?;
            match &mut *lock(&backend) {
                Backend::Carried(carrier) => carrier.exit_code(),
                Backend::Pty(_) => None,
            }
        });
        TerminalSession { shared, backend, pid: None }
    }
}

impl TerminalSession {
    /// Starts describing a session whose program runs in another process: `output` brings the
    /// bytes the program writes, as that process relays them, and `carrier` takes the input, the
    /// size and the request to end it. Finish with [`AttachedBuilder::start`].
    ///
    /// The session is drawn and watched exactly like one that runs its program here: a
    /// [`Terminal`](super::Terminal) widget shows it and sends keys through the carrier, and its
    /// [`TerminalWatch`](super::TerminalWatch) reports output, the title, the folder, the bell,
    /// notifications and copied text, and the end once `output` ends, with the code
    /// [`TerminalCarrier::exit_code`] gives.
    ///
    /// Two things differ. The program's questions to the terminal, such as where the cursor is or
    /// which colours it draws, are not answered here: the session in the program's own process
    /// answered them already, and a second answer would reach the program's input twice. And
    /// letting the last handle go leaves the program running, since it lives elsewhere and is
    /// meant to outlive the screen; [`kill`](Self::kill) and [`terminate`](Self::terminate) ask
    /// the carrier to [end](TerminalCarrier::end) it. There is no [`pid`](Self::pid) here.
    ///
    /// To draw what the program's own session draws, `output` starts with that session's
    /// [`snapshot`](Self::snapshot), on a screen of the same [`size`](AttachedBuilder::size), and
    /// goes on with the bytes the program writes after it, which the process that runs it can
    /// hear with [`TerminalBuilder::tee`](super::TerminalBuilder::tee).
    ///
    /// ```no_run
    /// use std::io;
    /// use std::os::unix::net::UnixStream;
    /// use qframe::widgets::{TerminalCarrier, TerminalSession};
    ///
    /// /// Input and sizes go to the process that runs the program, one message a line.
    /// struct Socket(UnixStream);
    ///
    /// impl TerminalCarrier for Socket {
    ///     fn write(&mut self, bytes: &[u8]) -> io::Result<()> {
    ///         use io::Write;
    ///         writeln!(self.0, "keys {}", bytes.len())?;
    ///         self.0.write_all(bytes)
    ///     }
    ///     fn resize(&mut self, columns: u16, rows: u16) -> io::Result<()> {
    ///         use io::Write;
    ///         writeln!(self.0, "size {columns} {rows}")
    ///     }
    ///     fn end(&mut self) {
    ///         use io::Write;
    ///         let _ = writeln!(self.0, "end");
    ///     }
    /// }
    ///
    /// let control = UnixStream::connect("desk-1.control")?;
    /// let output = UnixStream::connect("desk-1.output")?;
    /// let session = TerminalSession::attach(output, Socket(control)).size(120, 40).start();
    /// # Ok::<(), io::Error>(())
    /// ```
    pub fn attach<R: Read + Send + 'static, C: TerminalCarrier>(output: R, carrier: C) -> AttachedBuilder<R, C> {
        AttachedBuilder { output, carrier, size: SIZE, scrollback: SCROLLBACK, coalesce: Duration::ZERO }
    }

    /// The short form of [`TerminalSession::attach`] with every option at its default: an 80 × 24
    /// screen keeping 5000 lines of scrollback.
    pub fn attached(output: impl Read + Send + 'static, carrier: impl TerminalCarrier) -> TerminalSession {
        Self::attach(output, carrier).start()
    }
}

#[cfg(test)]
#[path = "terminal_attached_tests.rs"]
mod tests;