Skip to main content

brush_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    pub fn acquire() -> Result<Self, error::Error> {
13        // Mask out SIGTTOU *first*. If `lead_new_process_group` succeeds in
14        // moving us into a new process group, the subsequent `tcsetpgrp` call
15        // in `move_self_to_foreground` is a "write to the controlling
16        // terminal from a background process," which the kernel signals with
17        // SIGTTOU. The default action for SIGTTOU is to stop the process,
18        // leaving brush (and any downstream reads from the terminal) hung.
19        // Installing the SIG_IGN handler before the tcsetpgrp makes that call
20        // succeed instead of stopping us.
21        sys::signal::mask_sigttou()?;
22
23        let prev_fg_pid = sys::terminal::get_foreground_pid();
24
25        // Break out into new process group.
26        // TODO(jobs): Investigate why this sometimes fails with EPERM.
27        let _ = sys::signal::lead_new_process_group();
28
29        // Take ownership.
30        sys::terminal::move_self_to_foreground()?;
31
32        Ok(Self { prev_fg_pid })
33    }
34
35    fn try_release(&mut self) {
36        // Restore the previous foreground process group.
37        if let Some(pid) = self.prev_fg_pid
38            && sys::terminal::move_to_foreground(pid).is_ok()
39        {
40            self.prev_fg_pid = None;
41        }
42    }
43}
44
45impl Drop for TerminalControl {
46    fn drop(&mut self) {
47        self.try_release();
48    }
49}
50
51/// Describes high-level terminal settings that can be requested.
52#[derive(Default, bon::Builder)]
53pub struct Settings {
54    /// Whether to enable input echoing.
55    pub echo_input: Option<bool>,
56    /// Whether to enable line input (sometimes known as canonical mode).
57    pub line_input: Option<bool>,
58    /// Whether to disable interrupt signals and instead yield the control characters.
59    pub interrupt_signals: Option<bool>,
60    /// Whether to output newline characters as CRLF pairs.
61    pub output_nl_as_nlcr: Option<bool>,
62}
63
64/// Guard that automatically restores terminal settings on drop.
65pub struct AutoModeGuard {
66    initial: sys::terminal::Config,
67    file: openfiles::OpenFile,
68}
69
70impl AutoModeGuard {
71    /// Creates a new `AutoModeGuard` for the given file.
72    ///
73    /// # Arguments
74    ///
75    /// * `file` - The file representing the terminal to control.
76    pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
77        let initial = sys::terminal::Config::from_term(&file)?;
78        Ok(Self { initial, file })
79    }
80
81    /// Applies the given terminal settings.
82    ///
83    /// # Arguments
84    ///
85    /// * `settings` - The terminal settings to apply.
86    pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
87        let mut config = sys::terminal::Config::from_term(&self.file)?;
88        config.update(settings);
89        config.apply_to_term(&self.file)?;
90
91        Ok(())
92    }
93}
94
95impl Drop for AutoModeGuard {
96    fn drop(&mut self) {
97        let _ = self.initial.apply_to_term(&self.file);
98    }
99}