summaryrefslogtreecommitdiffstats
path: root/src/sys/unix.rs
blob: 67bc1da0580c435a19cadd9bc74a55675769785a (plain)
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
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
use std::fs::File;
use std::io::{Read, Write};
use std::mem::ManuallyDrop;
use std::os::unix::io::{AsFd, AsRawFd, BorrowedFd, FromRawFd, RawFd};

/// Unix handle to an open terminal.
pub enum Terminal {
	/// Non-owning file for one of the standard I/O streams.
	Stdio(ManuallyDrop<File>),

	/// Owned file for `/dev/tty`.
	File(File),
}

#[derive(Copy, Clone)]
pub struct TerminalMode {
	termios: libc::termios,
}

impl Terminal {
	pub fn open() -> std::io::Result<Self> {
		if let Some(terminal) = open_fd_terminal(2) {
			Ok(terminal)
		} else if let Some(terminal) = open_fd_terminal(0) {
			Ok(terminal)
		} else if let Some(terminal) = open_fd_terminal(1) {
			Ok(terminal)
		} else {
			let file = std::fs::OpenOptions::new()
				.read(true)
				.write(true)
				.open("/dev/tty")?;
			if is_terminal(file.as_fd()) {
				Ok(Self::File(file))
			} else {
				Err(std::io::Error::from_raw_os_error(libc::ENOTTY))
			}
		}
	}

	pub fn get_terminal_mode(&self) -> std::io::Result<TerminalMode> {
		unsafe {
			let mut termios = std::mem::zeroed();
			check_ret(libc::tcgetattr(self.as_fd().as_raw_fd(), &mut termios))?;
			Ok(TerminalMode { termios })
		}
	}

	pub fn set_terminal_mode(&self, mode: &TerminalMode) -> std::io::Result<()> {
		unsafe {
			check_ret(libc::tcsetattr(
					self.as_fd().as_raw_fd(),
					libc::TCSANOW,
					&mode.termios,
			))?;
			Ok(())
		}
	}

	fn as_file(&self) -> &File {
		match self {
			Self::Stdio(io) => io,
			Self::File(io) => io,
		}
	}
}

fn open_fd_terminal(fd: RawFd) -> Option<Terminal> {
	let file = unsafe { ManuallyDrop::new(File::from_raw_fd(fd)) };
	if is_terminal(file.as_fd()) {
		Some(Terminal::Stdio(file))
	} else {
		None
	}
}

impl TerminalMode {
	pub fn enable_line_editing(&mut self) {
		self.termios.c_lflag |= libc::ICANON;
	}

	pub fn disable_echo(&mut self) {
		self.termios.c_lflag &= !libc::ECHO;
		self.termios.c_lflag &= !libc::ICANON;
	}

	pub fn enable_echo(&mut self) {
		self.termios.c_lflag |= libc::ECHO;
		self.termios.c_lflag |= !libc::ICANON;
	}

	pub fn is_echo_enabled(&self) -> bool {
		self.termios.c_lflag & libc::ECHO != 0
	}
}

fn is_terminal(fd: BorrowedFd) -> bool {
	unsafe {
		libc::isatty(fd.as_raw_fd()) == 1
	}
}

fn check_ret(input: i32) -> std::io::Result<()> {
	if input == 0 {
		Ok(())
	} else {
		Err(std::io::Error::last_os_error())
	}
}

impl AsFd for Terminal {
	fn as_fd(&self) -> std::os::fd::BorrowedFd<'_> {
		match self {
			Self::Stdio(stdin) => stdin.as_fd(),
			Self::File(file) => file.as_fd(),
		}
	}
}

impl Read for Terminal {
	fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
		self.as_file().read(buf)
	}

	fn read_vectored(&mut self, bufs: &mut [std::io::IoSliceMut<'_>]) -> std::io::Result<usize> {
		self.as_file().read_vectored(bufs)
	}
}

impl Write for Terminal {
	fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
		self.as_file().write(buf)
	}

	fn flush(&mut self) -> std::io::Result<()> {
		self.as_file().flush()
	}

	fn write_vectored(&mut self, bufs: &[std::io::IoSlice<'_>]) -> std::io::Result<usize> {
		self.as_file().write_vectored(bufs)
	}
}