Skip to main content

shrub_core/
terminal.rs

1//! Terminal control utilities.
2
3use crate::{error, openfiles, sys};
4
5/// Encapsulates the state of a controlled terminal.
6pub struct TerminalControl {
7    prev_fg_pid: Option<sys::process::ProcessId>,
8}
9
10impl TerminalControl {
11    /// Acquire the terminal for the shell.
12    ///
13    /// # Errors
14    ///
15    /// Returns [`error::ErrorKind::PlatformError`] if masking `SIGTTOU` or
16    /// moving the shell's process group to the terminal foreground fails,
17    /// for example when there is no controlling terminal.
18    pub fn acquire() -> Result<Self, error::Error> {
19        // Mask out SIGTTOU *first*. If `lead_new_process_group` succeeds in
20        // moving us into a new process group, the subsequent `tcsetpgrp` call
21        // in `move_self_to_foreground` is a "write to the controlling
22        // terminal from a background process," which the kernel signals with
23        // SIGTTOU. The default action for SIGTTOU is to stop the process,
24        // leaving shrub (and any downstream reads from the terminal) hung.
25        // Installing the SIG_IGN handler before the tcsetpgrp makes that call
26        // succeed instead of stopping us.
27        sys::signal::mask_sigttou()?;
28
29        let prev_fg_pid = sys::terminal::get_foreground_pid();
30
31        // Break out into new process group.
32        // TODO(jobs): Investigate why this sometimes fails with EPERM.
33        let _ = sys::signal::lead_new_process_group();
34
35        // Take ownership.
36        sys::terminal::move_self_to_foreground()?;
37
38        Ok(Self { prev_fg_pid })
39    }
40
41    fn try_release(&mut self) {
42        // Restore the previous foreground process group.
43        if let Some(pid) = self.prev_fg_pid
44            && sys::terminal::move_to_foreground(pid).is_ok()
45        {
46            self.prev_fg_pid = None;
47        }
48    }
49}
50
51impl Drop for TerminalControl {
52    fn drop(&mut self) {
53        self.try_release();
54    }
55}
56
57/// Describes high-level terminal settings that can be requested.
58#[derive(Default, bon::Builder)]
59pub struct Settings {
60    /// Whether to put the terminal in raw mode, as `cfmakeraw` does: input
61    /// arrives a byte at a time, as typed, without being echoed, translated
62    /// (e.g., Enter to newline), or turned into signals, and output isn't
63    /// processed. The other settings then apply on top of it (e.g., to have
64    /// newlines output as CRLF pairs).
65    #[builder(default)]
66    pub raw_input: bool,
67    /// Whether to enable input echoing.
68    pub echo_input: Option<bool>,
69    /// Whether to enable line input (sometimes known as canonical mode).
70    pub line_input: Option<bool>,
71    /// Whether to disable interrupt signals and instead yield the control
72    /// characters.
73    pub interrupt_signals: Option<bool>,
74    /// Whether to output newline characters as CRLF pairs.
75    pub output_nl_as_nlcr: Option<bool>,
76}
77
78/// Guard that automatically restores terminal settings on drop.
79pub struct AutoModeGuard {
80    initial: sys::terminal::Config,
81    file: openfiles::OpenFile,
82}
83
84impl AutoModeGuard {
85    /// Creates a new `AutoModeGuard` for the given file.
86    ///
87    /// # Arguments
88    ///
89    /// * `file` - The file representing the terminal to control.
90    ///
91    /// # Errors
92    ///
93    /// Returns an error if the current terminal settings of `file` cannot be
94    /// read, for example when `file` is not a terminal.
95    pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
96        let initial = sys::terminal::Config::from_term(&file)?;
97        Ok(Self { initial, file })
98    }
99
100    /// Applies the given terminal settings.
101    ///
102    /// # Arguments
103    ///
104    /// * `settings` - The terminal settings to apply.
105    ///
106    /// # Errors
107    ///
108    /// Returns an error if the terminal's current settings cannot be read or
109    /// the updated settings cannot be applied.
110    pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
111        let mut config = sys::terminal::Config::from_term(&self.file)?;
112        config.update(settings);
113        config.apply_to_term(&self.file)?;
114
115        Ok(())
116    }
117}
118
119impl Drop for AutoModeGuard {
120    fn drop(&mut self) {
121        let _ = self.initial.apply_to_term(&self.file);
122    }
123}