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    pub fn from_term(file: &openfiles::OpenFile) -> Result<Self, error::Error> {
23        let fd = file.try_borrow_as_fd()?;
24        let termios = nix::sys::termios::tcgetattr(fd)?;
25        Ok(Self { termios })
26    }
27
28    /// Applies the terminal settings to the terminal associated with the given
29    /// file descriptor.
30    ///
31    /// # Arguments
32    ///
33    /// * `file` - A reference to the open terminal.
34    pub fn apply_to_term(&self, file: &openfiles::OpenFile) -> Result<(), error::Error> {
35        let fd = file.try_borrow_as_fd()?;
36        nix::sys::termios::tcsetattr(fd, nix::sys::termios::SetArg::TCSANOW, &self.termios)?;
37        Ok(())
38    }
39
40    /// Applies the given high-level terminal settings to this configuration.
41    /// Does not modify any terminal itself.
42    ///
43    /// # Arguments
44    ///
45    /// * `settings` - The high-level terminal settings to apply to this
46    ///   configuration.
47    pub fn update(&mut self, settings: &terminal::Settings) {
48        if settings.raw_input {
49            nix::sys::termios::cfmakeraw(&mut self.termios);
50        }
51
52        if let Some(echo_input) = &settings.echo_input {
53            if *echo_input {
54                self.termios.local_flags |= nix::sys::termios::LocalFlags::ECHO;
55            } else {
56                self.termios.local_flags -= nix::sys::termios::LocalFlags::ECHO;
57            }
58        }
59
60        if let Some(line_input) = &settings.line_input {
61            if *line_input {
62                self.termios.local_flags |= nix::sys::termios::LocalFlags::ICANON;
63            } else {
64                self.termios.local_flags -= nix::sys::termios::LocalFlags::ICANON;
65            }
66        }
67
68        if let Some(interrupt_signals) = &settings.interrupt_signals {
69            if *interrupt_signals {
70                self.termios.local_flags |= nix::sys::termios::LocalFlags::ISIG;
71            } else {
72                self.termios.local_flags -= nix::sys::termios::LocalFlags::ISIG;
73            }
74        }
75
76        if let Some(output_nl_as_nlcr) = &settings.output_nl_as_nlcr {
77            if *output_nl_as_nlcr {
78                self.termios.output_flags |=
79                    nix::sys::termios::OutputFlags::OPOST | nix::sys::termios::OutputFlags::ONLCR;
80            } else {
81                self.termios.output_flags -= nix::sys::termios::OutputFlags::ONLCR;
82            }
83        }
84    }
85}
86
87/// Get the process ID of this process's parent.
88pub fn get_parent_process_id() -> Option<sys::process::ProcessId> {
89    Some(nix::unistd::getppid().as_raw())
90}
91
92/// Get the process group ID for this process's process group.
93pub fn get_process_group_id() -> Option<sys::process::ProcessId> {
94    Some(nix::unistd::getpgrp().as_raw())
95}
96
97/// Get the foreground process ID of the attached terminal.
98pub fn get_foreground_pid() -> Option<sys::process::ProcessId> {
99    nix::unistd::tcgetpgrp(std::io::stdin())
100        .ok()
101        .map(|pgid| pgid.as_raw())
102}
103
104/// Move the specified process to the foreground of the attached terminal.
105pub fn move_to_foreground(pid: sys::process::ProcessId) -> Result<(), error::Error> {
106    nix::unistd::tcsetpgrp(std::io::stdin(), nix::unistd::Pid::from_raw(pid))?;
107    Ok(())
108}
109
110/// Moves the current process to the foreground of the attached terminal.
111// This function needs to return `std::io::Error` so that the OS error code can
112// be recovered.
113pub fn move_self_to_foreground() -> Result<(), std::io::Error> {
114    if std::io::stdin().is_terminal() {
115        let pgid = nix::unistd::getpgid(None)?;
116
117        // TODO(jobs): This sometimes fails with ENOTTY even though we checked
118        // that stdin is a terminal. We should investigate why this is
119        // happening.
120        let _ = nix::unistd::tcsetpgrp(std::io::stdin(), pgid);
121    }
122
123    Ok(())
124}
125
126/// Tries to get the path of the terminal device associated with the attached
127/// terminal. Returns `None` if there is no terminal attached or the lookup
128/// failed.
129pub fn try_get_terminal_device_path() -> Option<PathBuf> {
130    nix::unistd::ttyname(std::io::stdin()).ok()
131}
132
133/// Returns the width, in columns, of the terminal associated with the given
134/// file. Returns `None` if it isn't a terminal or doesn't report a width.
135///
136/// # Arguments
137///
138/// * `file` - A reference to the open terminal.
139pub fn get_columns(file: &openfiles::OpenFile) -> Option<usize> {
140    let fd = file.try_borrow_as_fd().ok()?;
141    let (terminal_size::Width(columns), _) = terminal_size::terminal_size_of(fd)?;
142    Some(usize::from(columns))
143}