1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
//! 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);
}
}