Skip to content

Commit f978a21

Browse files
committed
support for synchronized output
1 parent 0567bdc commit f978a21

File tree

1 file changed

+288
-1
lines changed

1 file changed

+288
-1
lines changed

src/unix_term.rs

Lines changed: 288 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ use std::io;
55
use std::io::{BufRead, BufReader};
66
use std::mem;
77
use std::os::unix::io::AsRawFd;
8-
use std::ptr;
8+
use std::os::unix::io::FromRawFd;
9+
use std::os::unix::io::IntoRawFd;
910
use std::str;
1011

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

0 commit comments

Comments
 (0)