use std::io;
use std::sync::Mutex;
use std::time::Duration;
#[cfg(unix)]
use std::time::Instant;
use rich::ColorSystem;
use termwiz::caps::{Capabilities, ColorLevel, ProbeHints};
use termwiz::input::InputEvent;
#[cfg(unix)]
use termwiz::input::InputParser;
use termwiz::terminal::{SystemTerminal, Terminal};
use super::{Output, WakeFd};
static TERMINAL: Mutex<Option<SystemTerminal>> = Mutex::new(None);
pub(super) struct Probed {
pub(super) mouse: bool,
pub(super) paste: bool,
pub(super) color: Option<ColorSystem>,
#[cfg(unix)]
reader: Box<Reader>,
}
#[cfg(unix)]
const SEQUENCE_WAIT: Duration = Duration::from_millis(100);
#[cfg(unix)]
const CSI_WAIT: Duration = Duration::from_millis(500);
#[cfg(unix)]
struct Reader {
tty: std::fs::File,
parser: InputParser,
held: Vec<u8>,
events: std::collections::VecDeque<InputEvent>,
last_read: Option<Instant>,
resized: std::os::unix::net::UnixStream,
resize_signal: signal_hook::SigId,
}
#[cfg(unix)]
impl Reader {
fn open(tty: std::fs::File) -> io::Result<Reader> {
let (resized, notify) = std::os::unix::net::UnixStream::pair()?;
resized.set_nonblocking(true)?;
notify.set_nonblocking(true)?;
let resize_signal =
signal_hook::low_level::pipe::register(signal_hook::consts::SIGWINCH, notify)?;
Ok(Reader {
tty,
parser: InputParser::new(),
held: Vec::new(),
events: Default::default(),
last_read: None,
resized,
resize_signal,
})
}
fn parse(&mut self, bytes: &[u8]) {
self.held.extend_from_slice(bytes);
let cut = match self.held.iter().rposition(|&b| b == 0x1b) {
Some(at)
if at + 1 == self.held.len()
|| (self.held[at + 1] == b'['
&& self.held[at + 2..]
.iter()
.all(|b| (0x20..=0x3f).contains(b))) =>
{
at
}
_ => self.held.len(),
};
let events = &mut self.events;
self.parser
.parse(&self.held[..cut], |event| events.push_back(event), true);
self.held.drain(..cut);
self.last_read = Some(Instant::now());
}
fn fill(&mut self) -> io::Result<()> {
use std::io::Read;
let mut buffer = [0u8; 1024];
match self.tty.read(&mut buffer) {
Ok(0) => Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"the terminal hung up",
)),
Ok(read) => {
self.parse(&buffer[..read]);
Ok(())
}
Err(e) if e.kind() == io::ErrorKind::Interrupted => Ok(()),
Err(e) => Err(e),
}
}
fn flush(&mut self) {
let events = &mut self.events;
let held = std::mem::take(&mut self.held);
self.parser
.parse(&held, |event| events.push_back(event), false);
self.last_read = None;
}
fn sequence_wait(&self) -> Duration {
if self.held.len() > 1 {
CSI_WAIT
} else {
SEQUENCE_WAIT
}
}
fn readable(&self, until: Instant) -> io::Result<bool> {
use filedescriptor::{POLLIN, poll, pollfd};
use std::os::unix::io::AsRawFd;
loop {
let mut ready = [pollfd {
fd: self.tty.as_raw_fd(),
events: POLLIN,
revents: 0,
}];
let wait = until.saturating_duration_since(Instant::now());
match poll(&mut ready, Some(wait)) {
Ok(_) => return Ok(ready[0].revents != 0),
Err(filedescriptor::Error::Poll(e))
if e.kind() == io::ErrorKind::Interrupted && Instant::now() < until => {}
Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => {
return Ok(false);
}
Err(e) => return Err(other(e)),
}
}
}
fn next(&mut self, wait: Duration, wake: Option<WakeFd>) -> io::Result<Option<InputEvent>> {
use filedescriptor::{POLLIN, poll, pollfd};
use std::io::Read;
use std::os::unix::io::AsRawFd;
if let Some(event) = self.events.pop_front() {
return Ok(Some(event));
}
let wait = match self.last_read {
Some(at) => {
wait.min((at + self.sequence_wait()).saturating_duration_since(Instant::now()))
}
None => wait,
};
let mut ready = vec![
pollfd {
fd: self.tty.as_raw_fd(),
events: POLLIN,
revents: 0,
},
pollfd {
fd: self.resized.as_raw_fd(),
events: POLLIN,
revents: 0,
},
];
ready.extend(wake.map(|fd| pollfd {
fd,
events: POLLIN,
revents: 0,
}));
match poll(&mut ready, Some(wait)) {
Ok(_) => {}
Err(filedescriptor::Error::Poll(e)) if e.kind() == io::ErrorKind::Interrupted => {
return Ok(None);
}
Err(e) => return Err(other(e)),
}
if ready[1].revents != 0 {
let mut drained = [0u8; 64];
while matches!(self.resized.read(&mut drained), Ok(n) if n > 0) {}
let (cols, rows) = size()?;
return Ok(Some(InputEvent::Resized {
cols: cols.into(),
rows: rows.into(),
}));
}
if ready[0].revents != 0 {
self.fill()?;
while !self.held.is_empty() {
let since = self.last_read.unwrap_or_else(Instant::now);
if !self.readable(since + self.sequence_wait())? {
break;
}
self.fill()?;
}
if !self.held.is_empty() {
self.flush();
}
} else if self
.last_read
.is_some_and(|at| at.elapsed() >= self.sequence_wait())
{
self.flush();
}
Ok(self.events.pop_front())
}
}
#[cfg(unix)]
impl Drop for Reader {
fn drop(&mut self) {
signal_hook::low_level::unregister(self.resize_signal);
}
}
fn other(error: impl std::fmt::Display) -> io::Error {
io::Error::other(error.to_string())
}
pub(super) fn open(output: Output) -> io::Result<Probed> {
let probed = Capabilities::new_from_env().map_err(other)?;
let caps = Capabilities::new_with_hints(
ProbeHints::new_from_env()
.mouse_reporting(Some(false))
.bracketed_paste(Some(false)),
)
.map_err(other)?;
#[cfg(unix)]
let tty = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open("/dev/tty")?;
#[cfg(unix)]
let terminal = match output {
Output::Stdout => SystemTerminal::new_with(caps, &tty, &io::stdout()),
Output::Stderr => SystemTerminal::new_with(caps, &tty, &io::stderr()),
};
#[cfg(windows)]
let terminal = {
let _ = output;
SystemTerminal::new(caps)
};
*super::lock(&TERMINAL, true)? = Some(terminal.map_err(other)?);
Ok(Probed {
mouse: probed.mouse_reporting(),
paste: probed.bracketed_paste(),
color: match probed.color_level() {
ColorLevel::MonoChrome => None,
ColorLevel::Sixteen => Some(ColorSystem::Standard),
ColorLevel::TwoFiftySix => Some(ColorSystem::EightBit),
ColorLevel::TrueColor => Some(ColorSystem::Truecolor),
},
#[cfg(unix)]
reader: Box::new(Reader::open(tty)?),
})
}
fn with<R>(
wait: bool,
f: impl FnOnce(&mut SystemTerminal) -> termwiz::Result<R>,
) -> io::Result<Option<R>> {
let mut terminal = super::lock(&TERMINAL, wait)?;
match terminal.as_mut() {
Some(terminal) => f(terminal).map(Some).map_err(other),
None => Ok(None),
}
}
pub(super) fn raw_mode(on: bool, wait: bool) -> io::Result<()> {
with(wait, |terminal| {
if on {
terminal.set_raw_mode()?;
} else {
terminal.set_cooked_mode()?;
}
terminal.flush()
})
.map(drop)
}
pub(super) fn alternate_screen(on: bool, wait: bool) -> io::Result<()> {
with(wait, |terminal| {
if on {
terminal.enter_alternate_screen()?;
} else {
terminal.exit_alternate_screen()?;
}
terminal.flush()
})
.map(drop)
}
pub(super) fn size() -> io::Result<(u16, u16)> {
let size = with(true, |terminal| terminal.get_screen_size())?
.ok_or_else(|| other("the termwiz terminal is closed"))?;
let clamp = |n: usize| u16::try_from(n).unwrap_or(u16::MAX);
Ok((clamp(size.cols), clamp(size.rows)))
}
pub(super) fn poll(
probed: &mut Probed,
wait: Duration,
wake: Option<WakeFd>,
) -> io::Result<Option<InputEvent>> {
#[cfg(unix)]
{
probed.reader.next(wait, wake)
}
#[cfg(not(unix))]
{
let _ = (probed, wake);
Ok(with(true, |terminal| terminal.poll_input(Some(wait)))?.flatten())
}
}
#[cfg(unix)]
pub(super) fn unread(probed: &mut Probed, bytes: &[u8]) {
probed.reader.parse(bytes);
}
pub(super) fn release() {
let terminal = super::lock(&TERMINAL, true)
.ok()
.and_then(|mut terminal| terminal.take());
drop(terminal);
}
#[cfg(all(test, unix))]
mod tests {
use super::*;
fn events(chunks: &[&[u8]]) -> Vec<String> {
let tty = std::fs::File::open("/dev/null").expect("/dev/null opens");
let mut reader = Reader::open(tty).expect("a reader");
for chunk in chunks {
reader.parse(chunk);
}
reader.flush();
reader
.events
.iter()
.map(|event| format!("{event:?}"))
.collect()
}
#[test]
fn an_escape_a_read_ends_with_waits_for_its_rest() {
let whole = events(&[b"\x1b[<0;5;3M"]);
assert_eq!(whole.len(), 1, "{whole:?}");
assert!(whole[0].starts_with("Mouse"), "{whole:?}");
for split in [
&[&b"\x1b"[..], b"[<0;5;", b"3M"][..],
&[b"\x1b", b"[<0;5;3M"],
&[b"\x1b[<0;5;", b"3M"],
] {
assert_eq!(events(split), whole, "{split:?}");
}
let escape = events(&[b"\x1b"]);
assert_eq!(escape.len(), 1, "{escape:?}");
assert!(escape[0].contains("Escape"), "{escape:?}");
}
}