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 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 shrub (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 put the terminal in raw mode, as `cfmakeraw` does: input
55 /// arrives a byte at a time, as typed, without being echoed, translated
56 /// (e.g., Enter to newline), or turned into signals, and output isn't
57 /// processed. The other settings then apply on top of it (e.g., to have
58 /// newlines output as CRLF pairs).
59 #[builder(default)]
60 pub raw_input: bool,
61 /// Whether to enable input echoing.
62 pub echo_input: Option<bool>,
63 /// Whether to enable line input (sometimes known as canonical mode).
64 pub line_input: Option<bool>,
65 /// Whether to disable interrupt signals and instead yield the control
66 /// characters.
67 pub interrupt_signals: Option<bool>,
68 /// Whether to output newline characters as CRLF pairs.
69 pub output_nl_as_nlcr: Option<bool>,
70}
71
72/// Guard that automatically restores terminal settings on drop.
73pub struct AutoModeGuard {
74 initial: sys::terminal::Config,
75 file: openfiles::OpenFile,
76}
77
78impl AutoModeGuard {
79 /// Creates a new `AutoModeGuard` for the given file.
80 ///
81 /// # Arguments
82 ///
83 /// * `file` - The file representing the terminal to control.
84 pub fn new(file: openfiles::OpenFile) -> Result<Self, error::Error> {
85 let initial = sys::terminal::Config::from_term(&file)?;
86 Ok(Self { initial, file })
87 }
88
89 /// Applies the given terminal settings.
90 ///
91 /// # Arguments
92 ///
93 /// * `settings` - The terminal settings to apply.
94 pub fn apply_settings(&self, settings: &Settings) -> Result<(), error::Error> {
95 let mut config = sys::terminal::Config::from_term(&self.file)?;
96 config.update(settings);
97 config.apply_to_term(&self.file)?;
98
99 Ok(())
100 }
101}
102
103impl Drop for AutoModeGuard {
104 fn drop(&mut self) {
105 let _ = self.initial.apply_to_term(&self.file);
106 }
107}