Skip to content

Commit 3915728

Browse files
Funami580chris-laplante
authored andcommitted
support for synchronized output
1 parent de2f15a commit 3915728

File tree

1 file changed

+289
-0
lines changed

1 file changed

+289
-0
lines changed

src/unix_term.rs

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ use std::io;
55
use std::io::{BufRead, BufReader};
66
use std::mem;
77
use std::os::unix::io::AsRawFd;
8+
use std::ptr;
9+
use std::os::unix::io::FromRawFd;
10+
use std::os::unix::io::IntoRawFd;
811
use std::str;
912

1013
use crate::kb::Key;
@@ -363,3 +366,289 @@ pub fn wants_emoji() -> bool {
363366
pub fn set_title<T: Display>(title: T) {
364367
print!("\x1b]0;{}\x07", title);
365368
}
369+
370+
fn with_raw_terminal<R>(f: impl FnOnce(&mut fs::File) -> R) -> io::Result<R> {
371+
// We need a custom drop implementation for File,
372+
// so that the fd for stdin does not get closed
373+
enum CustomDropFile {
374+
CloseFd(Option<fs::File>),
375+
NotCloseFd(Option<fs::File>),
376+
}
377+
378+
impl Drop for CustomDropFile {
379+
fn drop(&mut self) {
380+
match self {
381+
CustomDropFile::CloseFd(_) => {}
382+
CustomDropFile::NotCloseFd(inner) => {
383+
if let Some(file) = inner.take() {
384+
file.into_raw_fd();
385+
}
386+
}
387+
}
388+
}
389+
}
390+
391+
let (mut tty_handle, tty_fd) = if unsafe { libc::isatty(libc::STDIN_FILENO) } == 1 {
392+
(
393+
CustomDropFile::NotCloseFd(Some(unsafe { fs::File::from_raw_fd(libc::STDIN_FILENO) })),
394+
libc::STDIN_FILENO,
395+
)
396+
} else {
397+
let handle = fs::OpenOptions::new()
398+
.read(true)
399+
.write(true)
400+
.open("/dev/tty")?;
401+
let fd = handle.as_raw_fd();
402+
(CustomDropFile::CloseFd(Some(handle)), fd)
403+
};
404+
405+
// Get current mode
406+
let mut termios = mem::MaybeUninit::uninit();
407+
c_result(|| unsafe { libc::tcgetattr(tty_fd, termios.as_mut_ptr()) })?;
408+
409+
let mut termios = unsafe { termios.assume_init() };
410+
let old_iflag = termios.c_iflag;
411+
let old_oflag = termios.c_oflag;
412+
let old_cflag = termios.c_cflag;
413+
let old_lflag = termios.c_lflag;
414+
415+
// Go into raw mode
416+
unsafe { libc::cfmakeraw(&mut termios) };
417+
if old_lflag & libc::ISIG != 0 {
418+
// Re-enable INTR, QUIT, SUSP, DSUSP, if it was activated before
419+
termios.c_lflag |= libc::ISIG;
420+
}
421+
c_result(|| unsafe { libc::tcsetattr(tty_fd, libc::TCSADRAIN, &termios) })?;
422+
423+
let result = match &mut tty_handle {
424+
CustomDropFile::CloseFd(Some(handle)) => f(handle),
425+
CustomDropFile::NotCloseFd(Some(handle)) => f(handle),
426+
_ => unreachable!(),
427+
};
428+
429+
// Reset to previous mode
430+
termios.c_iflag = old_iflag;
431+
termios.c_oflag = old_oflag;
432+
termios.c_cflag = old_cflag;
433+
termios.c_lflag = old_lflag;
434+
c_result(|| unsafe { libc::tcsetattr(tty_fd, libc::TCSADRAIN, &termios) })?;
435+
436+
Ok(result)
437+
}
438+
439+
pub fn supports_synchronized_output() -> bool {
440+
*sync_output::SUPPORTS_SYNCHRONIZED_OUTPUT
441+
}
442+
443+
/// Specification: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036
444+
mod sync_output {
445+
use std::convert::TryInto as _;
446+
use std::io::Read as _;
447+
use std::io::Write as _;
448+
use std::os::unix::io::AsRawFd as _;
449+
use std::time;
450+
451+
use lazy_static::lazy_static;
452+
453+
use super::select_or_poll_term_fd;
454+
use super::with_raw_terminal;
455+
456+
const RESPONSE_TIMEOUT: time::Duration = time::Duration::from_millis(10);
457+
458+
lazy_static! {
459+
pub(crate) static ref SUPPORTS_SYNCHRONIZED_OUTPUT: bool =
460+
supports_synchronized_output_uncached();
461+
}
462+
463+
struct ResponseParser {
464+
state: ResponseParserState,
465+
response: u8,
466+
}
467+
468+
#[derive(PartialEq)]
469+
enum ResponseParserState {
470+
None,
471+
CsiOne,
472+
CsiTwo,
473+
QuestionMark,
474+
ModeDigit1,
475+
ModeDigit2,
476+
ModeDigit3,
477+
ModeDigit4,
478+
Semicolon,
479+
Response,
480+
DollarSign,
481+
Ypsilon,
482+
}
483+
484+
impl ResponseParser {
485+
const fn new() -> Self {
486+
Self {
487+
state: ResponseParserState::None,
488+
response: u8::MAX,
489+
}
490+
}
491+
492+
fn process_byte(&mut self, byte: u8) {
493+
match byte {
494+
b'\x1b' => {
495+
self.state = ResponseParserState::CsiOne;
496+
}
497+
b'[' => {
498+
self.state = if self.state == ResponseParserState::CsiOne {
499+
ResponseParserState::CsiTwo
500+
} else {
501+
ResponseParserState::None
502+
};
503+
}
504+
b'?' => {
505+
self.state = if self.state == ResponseParserState::CsiTwo {
506+
ResponseParserState::QuestionMark
507+
} else {
508+
ResponseParserState::None
509+
};
510+
}
511+
byte @ b'0' => {
512+
self.state = if self.state == ResponseParserState::Semicolon {
513+
self.response = byte;
514+
ResponseParserState::Response
515+
} else if self.state == ResponseParserState::ModeDigit1 {
516+
ResponseParserState::ModeDigit2
517+
} else {
518+
ResponseParserState::None
519+
};
520+
}
521+
byte @ b'2' => {
522+
self.state = if self.state == ResponseParserState::Semicolon {
523+
self.response = byte;
524+
ResponseParserState::Response
525+
} else if self.state == ResponseParserState::QuestionMark {
526+
ResponseParserState::ModeDigit1
527+
} else if self.state == ResponseParserState::ModeDigit2 {
528+
ResponseParserState::ModeDigit3
529+
} else {
530+
ResponseParserState::None
531+
};
532+
}
533+
byte @ b'1' | byte @ b'3' | byte @ b'4' => {
534+
self.state = if self.state == ResponseParserState::Semicolon {
535+
self.response = byte;
536+
ResponseParserState::Response
537+
} else {
538+
ResponseParserState::None
539+
};
540+
}
541+
b'6' => {
542+
self.state = if self.state == ResponseParserState::ModeDigit3 {
543+
ResponseParserState::ModeDigit4
544+
} else {
545+
ResponseParserState::None
546+
};
547+
}
548+
b';' => {
549+
self.state = if self.state == ResponseParserState::ModeDigit4 {
550+
ResponseParserState::Semicolon
551+
} else {
552+
ResponseParserState::None
553+
};
554+
}
555+
b'$' => {
556+
self.state = if self.state == ResponseParserState::Response {
557+
ResponseParserState::DollarSign
558+
} else {
559+
ResponseParserState::None
560+
};
561+
}
562+
b'y' => {
563+
self.state = if self.state == ResponseParserState::DollarSign {
564+
ResponseParserState::Ypsilon
565+
} else {
566+
ResponseParserState::None
567+
};
568+
}
569+
_ => {
570+
self.state = ResponseParserState::None;
571+
}
572+
}
573+
}
574+
575+
fn get_response(&self) -> Option<u8> {
576+
if self.state == ResponseParserState::Ypsilon {
577+
Some(self.response - b'0')
578+
} else {
579+
None
580+
}
581+
}
582+
}
583+
584+
fn supports_synchronized_output_uncached() -> bool {
585+
with_raw_terminal(|term_handle| {
586+
// Query the state of the (DEC) mode 2026 (Synchronized Output)
587+
write!(term_handle, "\x1b[?2026$p").ok()?;
588+
term_handle.flush().ok()?;
589+
590+
// Wait for response or timeout
591+
let term_fd = term_handle.as_raw_fd();
592+
let mut parser = ResponseParser::new();
593+
let mut buf = [0u8; 256];
594+
let deadline = time::Instant::now() + RESPONSE_TIMEOUT;
595+
596+
loop {
597+
let remaining_time = deadline
598+
.saturating_duration_since(time::Instant::now())
599+
.as_millis()
600+
.try_into()
601+
.ok()?;
602+
603+
if remaining_time == 0 {
604+
// Timeout
605+
return Some(false);
606+
}
607+
608+
match select_or_poll_term_fd(term_fd, remaining_time) {
609+
Ok(false) => {
610+
// Timeout
611+
return Some(false);
612+
}
613+
Ok(true) => {
614+
'read: loop {
615+
match term_handle.read(&mut buf) {
616+
Ok(0) => {
617+
// Reached EOF
618+
return Some(false);
619+
}
620+
Ok(size) => {
621+
for byte in &buf[..size] {
622+
parser.process_byte(*byte);
623+
624+
match parser.get_response() {
625+
Some(1) | Some(2) => return Some(true),
626+
Some(_) => return Some(false),
627+
None => {}
628+
}
629+
}
630+
631+
break 'read;
632+
}
633+
Err(err) if err.kind() == std::io::ErrorKind::Interrupted => {
634+
// Got interrupted, retry read
635+
continue 'read;
636+
}
637+
Err(_) => {
638+
return Some(false);
639+
}
640+
}
641+
}
642+
}
643+
Err(_) => {
644+
// Error
645+
return Some(false);
646+
}
647+
}
648+
}
649+
})
650+
.ok()
651+
.flatten()
652+
.unwrap_or(false)
653+
}
654+
}

0 commit comments

Comments
 (0)