use std::collections::VecDeque;
use std::io::Write;
use std::pin::Pin;
use std::time::Duration;
use crate::{
error::ResultLogger,
term::{
input_parser::InputParser,
internal::{InternalTermEvent, KeyboardMode},
TermEvent,
},
vt100::Size,
};
pub struct TermDriver {
#[cfg(unix)]
stdin_fd: rustix::fd::BorrowedFd<'static>,
#[cfg(unix)]
orig_termios: rustix::termios::Termios,
#[cfg(unix)]
stdin_thread: Option<std::thread::JoinHandle<()>>,
#[cfg(unix)]
stdin_wakeup: std::os::fd::OwnedFd,
#[cfg(unix)]
sigwinch: tokio::signal::unix::Signal,
#[cfg(windows)]
win_vt: super::windows::WinVt,
events:
tokio::sync::mpsc::UnboundedReceiver<std::io::Result<InternalTermEvent>>,
stdout: std::io::Stdout,
pending: VecDeque<InternalTermEvent>,
init_timeout: Option<Pin<Box<tokio::time::Sleep>>>,
keyboard: KeyboardMode,
}
impl TermDriver {
pub fn create() -> anyhow::Result<Self> {
#[cfg(unix)]
let stdin_fd = rustix::stdio::stdin();
#[cfg(unix)]
if !rustix::termios::isatty(stdin_fd) {
anyhow::bail!("Stdin is not a tty.");
}
#[cfg(windows)]
let win_vt = super::windows::WinVt::enable()?;
let mut stdout = std::io::stdout();
#[cfg(unix)]
let orig_termios = rustix::termios::tcgetattr(stdin_fd)?;
#[cfg(unix)]
let mut termios = orig_termios.clone();
#[cfg(unix)]
termios.make_raw();
#[cfg(unix)]
rustix::termios::tcsetattr(
stdin_fd,
rustix::termios::OptionalActions::Now,
&termios,
)?;
stdout.write_all(b"\x1b7")?;
stdout.write_all(b"\x1B[?1049h")?;
stdout.write_all(b"\x1B[2J")?;
{
stdout.write_all(b"\x1B[?1000h")?;
stdout.write_all(b"\x1B[?1002h")?;
stdout.write_all(b"\x1B[?1003h")?;
stdout.write_all(b"\x1B[?1015h")?;
stdout.write_all(b"\x1B[?1006h")?;
}
stdout.write_all(b"\x1B[?u")?;
stdout.write_all(b"\x1B[c")?;
#[cfg(unix)]
let sigwinch = tokio::signal::unix::signal(
tokio::signal::unix::SignalKind::window_change(),
)
.expect("Failed to register SIGWINCH handler");
#[cfg(unix)]
let (events, stdin_thread, stdin_wakeup) = {
use std::os::fd::{AsRawFd, FromRawFd, OwnedFd};
let (sender, events) = tokio::sync::mpsc::unbounded_channel();
let mut pipe_fds = [0; 2];
unsafe {
if libc::pipe(pipe_fds.as_mut_ptr()) < 0 {
return Err(std::io::Error::last_os_error().into());
}
}
let wake_read = unsafe { OwnedFd::from_raw_fd(pipe_fds[0]) };
let wake_write = unsafe { OwnedFd::from_raw_fd(pipe_fds[1]) };
let stdin_raw = stdin_fd.as_raw_fd();
let stdin_thread = std::thread::spawn(move || {
let mut input_parser = InputParser::new();
let mut read_buf = [0u8; 1024];
loop {
let mut poll_fds = [
libc::pollfd {
fd: stdin_raw,
events: libc::POLLIN,
revents: 0,
},
libc::pollfd {
fd: wake_read.as_raw_fd(),
events: libc::POLLIN,
revents: 0,
},
];
let poll_result = unsafe { libc::poll(poll_fds.as_mut_ptr(), 2, -1) };
if poll_result < 0 {
let err = std::io::Error::last_os_error();
if err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
let _ = sender.send(Err(err));
break;
}
if (poll_fds[1].revents & libc::POLLIN) != 0 {
break;
}
if (poll_fds[0].revents & (libc::POLLIN | libc::POLLHUP)) != 0 {
let n = match rustix::io::read(stdin_fd, &mut read_buf) {
Ok(n) => n,
Err(err) => {
let io_err = std::io::Error::from(err);
if io_err.kind() == std::io::ErrorKind::Interrupted {
continue;
}
let _ = sender.send(Err(io_err));
break;
}
};
if n == 0 {
break;
}
input_parser.parse_input(&read_buf[..n], true, false, |event| {
let _ = sender.send(Ok(event));
});
}
}
});
(events, Some(stdin_thread), wake_write)
};
#[cfg(windows)]
let events = {
let (sender, events) = tokio::sync::mpsc::unbounded_channel();
unsafe {
std::thread::spawn(move || {
let stdin = match windows::Win32::System::Console::GetStdHandle(
windows::Win32::System::Console::STD_INPUT_HANDLE,
) {
Ok(stdin) => stdin,
Err(err) => {
log::error!("GetStdHandle error: {}", err);
return;
}
};
let mut input_parser = InputParser::new();
let mut buf =
[windows::Win32::System::Console::INPUT_RECORD::default(); 128];
loop {
let mut count = 0;
match windows::Win32::System::Console::ReadConsoleInputA(
stdin, &mut buf, &mut count,
) {
Ok(()) => (),
Err(err) => {
log::error!("ReadConsoleInputA error: {}", err);
break;
}
};
super::windows::decode_input_records(
&mut input_parser,
&buf[..count as usize],
&mut |event| {
let _ = sender.send(Ok(event));
},
);
}
});
};
events
};
Ok(Self {
#[cfg(unix)]
stdin_fd,
#[cfg(unix)]
orig_termios,
#[cfg(unix)]
stdin_thread,
#[cfg(unix)]
stdin_wakeup,
#[cfg(unix)]
sigwinch,
#[cfg(windows)]
win_vt,
events,
stdout,
pending: VecDeque::new(),
init_timeout: Some(Box::pin(tokio::time::sleep(Duration::from_millis(
200,
)))),
keyboard: KeyboardMode::Unknown,
})
}
fn handle_internal(
&mut self,
event: InternalTermEvent,
) -> std::io::Result<Option<TermEvent>> {
match event {
InternalTermEvent::Key(key_event) => {
return Ok(Some(TermEvent::Key(key_event)));
}
InternalTermEvent::Mouse(mouse_event) => {
return Ok(Some(TermEvent::Mouse(mouse_event)));
}
InternalTermEvent::Resize(cols, rows) => {
return Ok(Some(TermEvent::Resize(cols, rows)));
}
InternalTermEvent::FocusGained => {
return Ok(Some(TermEvent::FocusGained));
}
InternalTermEvent::FocusLost => return Ok(Some(TermEvent::FocusLost)),
InternalTermEvent::CursorPos(_x, _y) => (),
InternalTermEvent::PrimaryDeviceAttributes => {
self.init_timeout = None;
self.activate_keyboard_fallback()?;
}
InternalTermEvent::ReplyKittyKeyboard(_flags) => {
self.keyboard = KeyboardMode::Kitty;
self.stdout.write_all(b"\x1b[>15u")?;
}
};
Ok(None)
}
fn activate_keyboard_fallback(&mut self) -> std::io::Result<()> {
if matches!(self.keyboard, KeyboardMode::Unknown) {
#[cfg(unix)]
{
self.keyboard = KeyboardMode::ModifyOtherKeys;
self.stdout.write_all(b"\x1b[>4;2m")?;
}
#[cfg(windows)]
{
self.keyboard = KeyboardMode::Win32;
self.stdout.write_all(b"\x1b[?9001h")?;
}
}
Ok(())
}
#[cfg(unix)]
pub async fn input(&mut self) -> std::io::Result<Option<TermEvent>> {
loop {
while let Some(event) = self.pending.pop_front() {
if let Some(term_event) = self.handle_internal(event)? {
return Ok(Some(term_event));
}
}
tokio::select! {
event = self.events.recv() => {
match event {
Some(Ok(event)) => {
if let Some(term_event) = self.handle_internal(event)? {
return Ok(Some(term_event));
}
}
Some(Err(err)) => return Err(err),
None => return Ok(None),
}
}
_ = self.sigwinch.recv() => {
let winsize = rustix::termios::tcgetwinsize(self.stdin_fd)?;
return Ok(Some(TermEvent::Resize(winsize.ws_col, winsize.ws_row)));
}
_ = async {
match &mut self.init_timeout {
Some(sleep) => sleep.await,
None => std::future::pending().await,
}
} => {
self.init_timeout = None;
self.activate_keyboard_fallback()?;
}
}
}
}
#[cfg(windows)]
pub async fn input(&mut self) -> std::io::Result<Option<TermEvent>> {
loop {
while let Some(event) = self.pending.pop_front() {
if let Some(term_event) = self.handle_internal(event)? {
return Ok(Some(term_event));
}
}
tokio::select! {
event = self.events.recv() => {
match event {
Some(Ok(event)) => {
if let Some(term_event) = self.handle_internal(event)? {
return Ok(Some(term_event));
}
}
Some(Err(err)) => return Err(err),
None => return Ok(None),
}
}
_ = async {
match &mut self.init_timeout {
Some(sleep) => sleep.await,
None => std::future::pending().await,
}
} => {
self.init_timeout = None;
self.activate_keyboard_fallback()?;
}
}
}
}
#[cfg(unix)]
pub fn size(&self) -> std::io::Result<Size> {
let winsize = rustix::termios::tcgetwinsize(self.stdin_fd)?;
Ok(Size {
height: winsize.ws_row,
width: winsize.ws_col,
})
}
#[cfg(windows)]
pub fn size(&self) -> std::io::Result<Size> {
unsafe {
use std::os::windows::io::AsRawHandle;
use windows::Win32::{
Foundation::HANDLE,
System::Console::{
GetConsoleScreenBufferInfo, CONSOLE_SCREEN_BUFFER_INFO,
},
};
let mut csbi: CONSOLE_SCREEN_BUFFER_INFO = Default::default();
GetConsoleScreenBufferInfo(
HANDLE(self.stdout.as_raw_handle()),
&mut csbi,
)?;
Ok(Size {
height: (csbi.srWindow.Bottom - csbi.srWindow.Top + 1) as u16,
width: (csbi.srWindow.Right - csbi.srWindow.Left + 1) as u16,
})
}
}
}
impl Drop for TermDriver {
fn drop(&mut self) {
match self.keyboard {
KeyboardMode::Unknown => (),
KeyboardMode::ModifyOtherKeys => {
self.stdout.write_all(b"\x1b[>4;0m").log_ignore();
}
KeyboardMode::Kitty => {
self.stdout.write_all(b"\x1b[<1u").log_ignore();
}
KeyboardMode::Win32 => {
self.stdout.write_all(b"\x1b[?9001l").log_ignore();
}
}
{
self.stdout.write_all(b"\x1B[?1006l").log_ignore();
self.stdout.write_all(b"\x1B[?1015l").log_ignore();
self.stdout.write_all(b"\x1B[?1003l").log_ignore();
self.stdout.write_all(b"\x1B[?1002l").log_ignore();
self.stdout.write_all(b"\x1B[?1000l").log_ignore();
}
self.stdout.write_all(b"\x1B[?1049l").log_ignore();
self.stdout.write_all(b"\x1b[?25h").log_ignore();
self.stdout.write_all(b"\x1b8").log_ignore();
self.stdout.flush().log_ignore();
#[cfg(unix)]
rustix::io::write(&self.stdin_wakeup, &[0]).log_ignore();
#[cfg(unix)]
if let Some(stdin_thread) = self.stdin_thread.take() {
stdin_thread.join().ok();
}
#[cfg(unix)]
rustix::termios::tcsetattr(
self.stdin_fd,
rustix::termios::OptionalActions::Now,
&self.orig_termios,
)
.log_ignore();
#[cfg(windows)]
self.win_vt.disable();
}
}