shrub_core/sys/unix/terminal.rs
1//! Terminal utilities.
2
3use std::io::IsTerminal;
4use std::os::fd::AsFd;
5use std::path::PathBuf;
6
7use crate::{error, openfiles, sys, terminal};
8
9/// Terminal configuration.
10#[derive(Clone, Debug)]
11pub struct Config {
12 termios: nix::sys::termios::Termios,
13}
14
15impl Config {
16 /// Creates a new `Config` from the actual terminal attributes of the
17 /// terminal associated with the given file descriptor.
18 ///
19 /// # Arguments
20 ///
21 /// * `file` - A reference to the open terminal.
22 ///
23 /// # Errors
24 ///
25 /// Returns an error if the file's descriptor cannot be borrowed, or
26 /// [`crate::error::ErrorKind::PlatformError`] wrapping the OS error if
27 /// `tcgetattr` fails, for example because the file is not a terminal.
28 pub fn from_term(file: &openfiles::OpenFile) -> Result<Self, error::Error> {
29 let fd = file.try_borrow_as_fd()?;
30 let termios = nix::sys::termios::tcgetattr(fd)?;
31 Ok(Self { termios })
32 }
33
34 /// Applies the terminal settings to the terminal associated with the given
35 /// file descriptor.
36 ///
37 /// # Arguments
38 ///
39 /// * `file` - A reference to the open terminal.
40 ///
41 /// # Errors
42 ///
43 /// Returns an error if the file's descriptor cannot be borrowed, or
44 /// [`crate::error::ErrorKind::PlatformError`] wrapping the OS error if
45 /// `tcsetattr` fails.
46 pub fn apply_to_term(&self, file: &openfiles::OpenFile) -> Result<(), error::Error> {
47 let fd = file.try_borrow_as_fd()?;
48 nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.termios)?;
49 Ok(())
50 }
51
52 /// Applies the given high-level terminal settings to this configuration.
53 /// Does not modify any terminal itself.
54 ///
55 /// # Arguments
56 ///
57 /// * `settings` - The high-level terminal settings to apply to this
58 /// configuration.
59 pub fn update(&mut self, settings: &terminal::Settings) {
60 if settings.raw_input {
61 nix::sys::termios::cfmakeraw(&mut self.termios);
62 }
63
64 if let Some(echo_input) = &settings.echo_input {
65 if *echo_input {
66 self.termios.local_flags |= nix::sys::termios::LocalFlags::ECHO;
67 } else {
68 self.termios.local_flags -= nix::sys::termios::LocalFlags::ECHO;
69 }
70 }
71
72 if let Some(line_input) = &settings.line_input {
73 if *line_input {
74 self.termios.local_flags |= nix::sys::termios::LocalFlags::ICANON;
75 } else {
76 self.termios.local_flags -= nix::sys::termios::LocalFlags::ICANON;
77 }
78 }
79
80 if let Some(interrupt_signals) = &settings.interrupt_signals {
81 if *interrupt_signals {
82 self.termios.local_flags |= nix::sys::termios::LocalFlags::ISIG;
83 } else {
84 self.termios.local_flags -= nix::sys::termios::LocalFlags::ISIG;
85 }
86 }
87
88 if let Some(output_nl_as_nlcr) = &settings.output_nl_as_nlcr {
89 if *output_nl_as_nlcr {
90 self.termios.output_flags |=
91 nix::sys::termios::OutputFlags::OPOST | nix::sys::termios::OutputFlags::ONLCR;
92 } else {
93 self.termios.output_flags -= nix::sys::termios::OutputFlags::ONLCR;
94 }
95 }
96 }
97}
98
99/// Get the process ID of this process's parent.
100pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
101 Some(nix::unistd::getppid().as_raw())
102}
103
104/// Get the process group ID for this process's process group.
105pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
106 Some(nix::unistd::getpgrp().as_raw())
107}
108
109/// Get the foreground process ID of the attached terminal.
110pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
111 nix::unistd::tcgetpgrp(std::io::stdin())
112 .ok()
113 .map(|pgid| pgid.as_raw())
114}
115
116/// Move the specified process to the foreground of the attached terminal.
117///
118/// # Errors
119///
120/// Returns [`crate::error::ErrorKind::PlatformError`] wrapping the OS error if
121/// `tcsetpgrp` fails, for example when standard input is not a terminal.
122pub fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
123 nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
124 Ok(())
125}
126
127/// Moves the current process to the foreground of the attached terminal.
128///
129/// # Errors
130///
131/// Returns the OS error if the current process group cannot be determined. A
132/// failure to move the group to the foreground is ignored.
133// This function needs to return `std::io::Error` so that the OS error code can
134// be recovered.
135pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
136 if std::io::stdin().is_terminal() {
137 let pgid = nix::unistd::getpgid(None)?;
138
139 // TODO(jobs): This sometimes fails with ENOTTY even though we checked
140 // that stdin is a terminal. We should investigate why this is
141 // happening.
142 let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid);
143 }
144
145 Ok(())
146}
147
148/// Tries to get the path of the terminal device associated with the attached
149/// terminal. Returns `None` if there is no terminal attached or the lookup
150/// failed.
151pub fn try_get_terminal_device_path() -> Option<PathBuf> {
152 nix::unistd::ttyname(std::io::stdin()).ok()
153}
154
155/// Returns the width, in columns, of the terminal associated with the given
156/// file. Returns `None` if it isn't a terminal or doesn't report a width.
157///
158/// # Arguments
159///
160/// * `file` - A reference to the open terminal.
161pub fn get_columns(file: &openfiles::OpenFile) -> Option<usize> {
162 let fd = file.try_borrow_as_fd().ok()?;
163 let (terminal_size::Width(columns), _) = terminal_size::terminal_size_of(fd)?;
164 Some(usize::from(columns))
165}