use std::fs::File;
use std::io;
use std::os::fd::AsRawFd;
pub mod escape {
pub const HIDE_CURSOR: &str = "\x1b[?25l";
pub const SHOW_CURSOR: &str = "\x1b[?25h";
pub const CLEAR_TO_END: &str = "\x1b[J";
pub fn move_up(lines: u16) -> String {
format!("\x1b[{lines}F")
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Key {
Up,
Down,
Enter,
Escape,
Interrupt,
Backspace,
Char(char),
Other,
}
pub struct RawTerminal {
tty: File,
original: libc::termios,
}
impl RawTerminal {
pub fn acquire() -> io::Result<Option<Self>> {
let tty = match File::options().read(true).write(true).open("/dev/tty") {
Ok(tty) => tty,
Err(error)
if matches!(
error.kind(),
io::ErrorKind::NotFound | io::ErrorKind::PermissionDenied
) =>
{
return Ok(None);
}
Err(error) => return Err(error),
};
let fd = tty.as_raw_fd();
let original = unsafe {
let mut current: libc::termios = std::mem::zeroed();
if libc::tcgetattr(fd, &mut current) != 0 {
return Err(io::Error::last_os_error());
}
current
};
let mut raw = original;
unsafe {
libc::cfmakeraw(&mut raw);
raw.c_cc[libc::VMIN] = 1;
raw.c_cc[libc::VTIME] = 0;
if libc::tcsetattr(fd, libc::TCSANOW, &raw) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(Some(Self { tty, original }))
}
pub fn size(&self) -> Option<(usize, usize)> {
let size = unsafe {
let mut size: libc::winsize = std::mem::zeroed();
if libc::ioctl(self.tty.as_raw_fd(), libc::TIOCGWINSZ, &raw mut size) != 0 {
return None;
}
size
};
(size.ws_row > 0).then_some((usize::from(size.ws_row), usize::from(size.ws_col)))
}
fn read_byte(&self) -> io::Result<Option<u8>> {
let mut byte = 0u8;
loop {
let read = unsafe {
libc::read(
self.tty.as_raw_fd(),
std::ptr::from_mut(&mut byte).cast::<libc::c_void>(),
1,
)
};
return match read {
1 => Ok(Some(byte)),
0 => Ok(None),
_ => {
let error = io::Error::last_os_error();
if error.kind() == io::ErrorKind::Interrupted {
continue;
}
Err(error)
}
};
}
}
fn set_read_timing(&self, min: u8, time: u8) -> io::Result<()> {
let mut settings = self.original;
unsafe {
libc::cfmakeraw(&mut settings);
settings.c_cc[libc::VMIN] = min;
settings.c_cc[libc::VTIME] = time;
if libc::tcsetattr(self.tty.as_raw_fd(), libc::TCSANOW, &settings) != 0 {
return Err(io::Error::last_os_error());
}
}
Ok(())
}
fn timed_reads(&self) -> io::Result<TimedReads<'_>> {
self.set_read_timing(0, 1)?;
Ok(TimedReads { terminal: self })
}
pub fn read_key(&self) -> io::Result<Key> {
let Some(byte) = self.read_byte()? else {
return Ok(Key::Interrupt);
};
match byte {
0x03 => Ok(Key::Interrupt),
b'\r' | b'\n' => Ok(Key::Enter),
0x1b => self.read_escape(),
0x08 | 0x7f => Ok(Key::Backspace),
0x00..=0x1f => Ok(Key::Other),
_ => self.read_utf8(byte),
}
}
fn read_escape(&self) -> io::Result<Key> {
let timed = self.timed_reads()?;
let Some(second) = timed.read_byte()? else {
return Ok(Key::Escape);
};
if !matches!(second, b'[' | b'O') {
return Ok(Key::Other);
}
let Some(third) = timed.read_byte()? else {
return Ok(Key::Other);
};
match third {
b'A' => Ok(Key::Up),
b'B' => Ok(Key::Down),
b'0'..=b'9' | b';' => {
timed.skip_sequence_tail()?;
Ok(Key::Other)
}
_ => Ok(Key::Other),
}
}
fn read_utf8(&self, first: u8) -> io::Result<Key> {
let width = match first {
0x00..=0x7f => 1,
0xc0..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf7 => 4,
_ => return Ok(Key::Other),
};
let timed = self.timed_reads()?;
let mut bytes = vec![first];
for _ in 1..width {
match timed.read_byte()? {
Some(byte) => bytes.push(byte),
None => return Ok(Key::Other),
}
}
Ok(std::str::from_utf8(&bytes)
.ok()
.and_then(|text| text.chars().next())
.map_or(Key::Other, Key::Char))
}
}
impl Drop for RawTerminal {
fn drop(&mut self) {
unsafe {
libc::tcsetattr(self.tty.as_raw_fd(), libc::TCSANOW, &self.original);
}
}
}
struct TimedReads<'a> {
terminal: &'a RawTerminal,
}
impl TimedReads<'_> {
fn read_byte(&self) -> io::Result<Option<u8>> {
self.terminal.read_byte()
}
fn skip_sequence_tail(&self) -> io::Result<()> {
for _ in 0..16 {
match self.read_byte()? {
Some(0x40..=0x7e) | None => return Ok(()),
Some(_) => {}
}
}
Ok(())
}
}
impl Drop for TimedReads<'_> {
fn drop(&mut self) {
let _ = self.terminal.set_read_timing(1, 0);
}
}