Skip to main content

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}