shrub-core 0.0.3

Reusable core of a POSIX/bash shell (used by shrub-shell)
Documentation
//! Terminal control utilities.

use crate::{error, openfiles, sys};

/// Encapsulates the state of a controlled terminal.
pub struct TerminalControl {
    prev_fg_pid: Option<sys::process::ProcessId>,
}

impl TerminalControl {
    /// Acquire the terminal for the shell.
    ///
    /// # Errors
    ///
    /// Returns [`error::ErrorKind::PlatformError`] if masking `SIGTTOU` or
    /// moving the shell's process group to the terminal foreground fails,
    /// for example when there is no controlling terminal.
    pub fn acquire() -> Result<Self, error::Error> {
        // Mask out SIGTTOU *first*. If `lead_new_process_group` succeeds in
        // moving us into a new process group, the subsequent `tcsetpgrp` call
        // in `move_self_to_foreground` is a "write to the controlling
        // terminal from a background process," which the kernel signals with
        // SIGTTOU. The default action for SIGTTOU is to stop the process,
        // leaving shrub (and any downstream reads from the terminal) hung.
        // Installing the SIG_IGN handler before the tcsetpgrp makes that call
        // succeed instead of stopping us.
        sys::signal::mask_sigttou()?;

        let prev_fg_pid = sys::terminal::get_foreground_pid();

        // Break out into new process group.
        // TODO(jobs): Investigate why this sometimes fails with EPERM.
        let _ = sys::signal::lead_new_process_group();

        // Take ownership.
        sys::terminal::move_self_to_foreground()?;

        Ok(Self { prev_fg_pid })
    }

    fn try_release(&mut self) {
        // Restore the previous foreground process group.
        if let Some(pid) = self.prev_fg_pid
            && sys::terminal::move_to_foreground(pid).is_ok()
        {
            self.prev_fg_pid = None;
        }
    }
}

impl Drop for TerminalControl {
    fn drop(&mut self) {
        self.try_release();
    }
}

/// Describes high-level terminal settings that can be requested.
#[derive(Default, bon::Builder)]
pub struct Settings {
    /// Whether to put the terminal in raw mode, as `cfmakeraw` does: input
    /// arrives a byte at a time, as typed, without being echoed, translated
    /// (e.g., Enter to newline), or turned into signals, and output isn't
    /// processed. The other settings then apply on top of it (e.g., to have
    /// newlines output as CRLF pairs).
    #[builder(default)]
    pub raw_input: bool,
    /// Whether to enable input echoing.
    pub echo_input: Option<bool>,
    /// Whether to enable line input (sometimes known as canonical mode).
    pub line_input: Option<bool>,
    /// Whether to disable interrupt signals and instead yield the control
    /// characters.
    pub interrupt_signals: Option<bool>,
    /// Whether to output newline characters as CRLF pairs.
    pub output_nl_as_nlcr: Option<bool>,
}

/// Guard that automatically restores terminal settings on drop.
pub struct AutoModeGuard {
    initial: sys::terminal::Config,
    file: openfiles::OpenFile,
}

impl AutoModeGuard {
    /// Creates a new `AutoModeGuard` for the given file.
    ///
    /// # Arguments
    ///
    /// * `file` - The file representing the terminal to control.
    ///
    /// # Errors
    ///
    /// Returns an error if the current terminal settings of `file` cannot be
    /// read, for example when `file` is not a terminal.
    pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
        let initial = sys::terminal::Config::from_term(&file)?;
        Ok(Self { initial, file })
    }

    /// Applies the given terminal settings.
    ///
    /// # Arguments
    ///
    /// * `settings` - The terminal settings to apply.
    ///
    /// # Errors
    ///
    /// Returns an error if the terminal's current settings cannot be read or
    /// the updated settings cannot be applied.
    pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
        let mut config = sys::terminal::Config::from_term(&self.file)?;
        config.update(settings);
        config.apply_to_term(&self.file)?;

        Ok(())
    }
}

impl Drop for AutoModeGuard {
    fn drop(&mut self) {
        let _ = self.initial.apply_to_term(&self.file);
    }
}