rmux_client/attach/
terminal.rs1use std::error::Error as StdError;
2use std::fmt;
3use std::fs::File;
4use std::io::{self, Write};
5use std::os::fd::{AsFd, OwnedFd};
6use std::process::{Command, Stdio};
7
8use rmux_core::{alternate_screen_enter_sequence, alternate_screen_exit_sequence};
9use rustix::process::{kill_process, Pid, Signal};
10use rustix::termios::{
11 tcflush, tcgetattr, tcsetattr, OptionalActions, QueueSelector, SpecialCodeIndex, Termios,
12};
13
14use super::terminal_cleanup::fallback_attach_stop_sequence;
15
16pub(super) fn current_process_pid() -> io::Result<Pid> {
17 let raw = i32::try_from(std::process::id())
18 .map_err(|_| io::Error::other("process id does not fit in i32"))?;
19 Pid::from_raw(raw).ok_or_else(|| io::Error::other("process id must be positive"))
20}
21
22pub type Result<T> = std::result::Result<T, AttachError>;
24
25#[derive(Debug)]
27pub enum AttachError {
28 Io(io::Error),
30 Termios(rustix::io::Errno),
32}
33
34impl fmt::Display for AttachError {
35 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
36 match self {
37 Self::Io(error) => write!(formatter, "terminal descriptor operation failed: {error}"),
38 Self::Termios(errno) => write!(formatter, "terminal mode operation failed: {errno}"),
39 }
40 }
41}
42
43impl StdError for AttachError {
44 fn source(&self) -> Option<&(dyn StdError + 'static)> {
45 match self {
46 Self::Io(error) => Some(error),
47 Self::Termios(errno) => Some(errno),
48 }
49 }
50}
51
52impl From<io::Error> for AttachError {
53 fn from(error: io::Error) -> Self {
54 Self::Io(error)
55 }
56}
57
58impl From<rustix::io::Errno> for AttachError {
59 fn from(errno: rustix::io::Errno) -> Self {
60 Self::Termios(errno)
61 }
62}
63
64#[derive(Debug)]
70#[must_use = "keep the guard alive for as long as raw terminal mode is required"]
71pub struct RawTerminal {
72 fd: OwnedFd,
73 original_termios: Termios,
74}
75
76impl RawTerminal {
77 pub fn enter() -> Result<Self> {
79 Self::from_fd(&io::stdin())
80 }
81
82 pub fn from_fd<Fd>(fd: &Fd) -> Result<Self>
88 where
89 Fd: AsFd,
90 {
91 let owned_fd = fd.as_fd().try_clone_to_owned()?;
92 let original_termios = tcgetattr(&owned_fd)?;
93 let mut raw_termios = original_termios.clone();
94 configure_raw_mode(&mut raw_termios);
95 tcsetattr(&owned_fd, OptionalActions::Now, &raw_termios)?;
96
97 Ok(Self {
98 fd: owned_fd,
99 original_termios,
100 })
101 }
102
103 pub fn restore(&self) -> Result<()> {
108 tcsetattr(&self.fd, OptionalActions::Now, &self.original_termios)?;
109 Ok(())
110 }
111
112 fn reapply_raw_mode(&self) -> Result<()> {
113 let mut raw_termios = self.original_termios.clone();
114 configure_raw_mode(&mut raw_termios);
115 tcsetattr(&self.fd, OptionalActions::Now, &raw_termios)?;
116 Ok(())
117 }
118
119 pub(super) fn run_lock_command(&self, command: &str) -> Result<()> {
120 self.restore()?;
121 let result = run_lock_command_with_terminal(&self.fd, command);
122 let reapply_result = self.reapply_raw_mode();
123 if let Err(error) = result {
124 reapply_result?;
125 return Err(error);
126 }
127 reapply_result?;
128 Ok(())
129 }
130
131 pub(super) fn suspend_self(&self) -> Result<()> {
132 self.restore()?;
133 kill_process(current_process_pid()?, Signal::TSTP)?;
134 self.reapply_raw_mode()?;
135 Ok(())
136 }
137
138 pub(super) fn run_detach_exec_command(&self, command: &str) -> Result<()> {
139 self.restore()?;
140 run_lock_command_with_terminal(&self.fd, command)
141 }
142
143 pub(super) fn restore_attach_terminal_state(&self) -> Result<()> {
144 let mut terminal = File::from(self.fd.as_fd().try_clone_to_owned()?);
145 let term = std::env::var("TERM").unwrap_or_default();
146 terminal.write_all(&fallback_attach_stop_sequence(&term))?;
147 terminal.flush()?;
148 Ok(())
149 }
150
151 pub(super) fn flush_pending_input(&self) -> Result<()> {
152 tcflush(&self.fd, QueueSelector::IFlush)?;
153 Ok(())
154 }
155}
156
157impl Drop for RawTerminal {
158 fn drop(&mut self) {
159 let _ = self.restore();
160 }
161}
162
163fn configure_raw_mode(termios: &mut Termios) {
164 termios.make_raw();
165 termios.special_codes[SpecialCodeIndex::VMIN] = 1;
166 termios.special_codes[SpecialCodeIndex::VTIME] = 0;
167}
168
169fn run_lock_command_with_terminal(fd: &OwnedFd, command: &str) -> Result<()> {
170 let stdin = File::from(fd.as_fd().try_clone_to_owned()?);
171 let stdout = File::from(fd.as_fd().try_clone_to_owned()?);
172 let stderr = File::from(fd.as_fd().try_clone_to_owned()?);
173 let mut terminal = File::from(fd.as_fd().try_clone_to_owned()?);
174 let term = std::env::var("TERM").unwrap_or_default();
175
176 terminal.write_all(alternate_screen_enter_sequence(&term))?;
177 terminal.flush()?;
178
179 let status_result = Command::new("sh")
180 .arg("-c")
181 .arg(command)
182 .stdin(Stdio::from(stdin))
183 .stdout(Stdio::from(stdout))
184 .stderr(Stdio::from(stderr))
185 .status();
186
187 let restore_result = terminal
188 .write_all(alternate_screen_exit_sequence(&term))
189 .and_then(|()| terminal.flush())
190 .map_err(AttachError::Io);
191
192 restore_result?;
193 status_result.map_err(AttachError::Io)?;
194 Ok(())
195}