@@ -5,6 +5,9 @@ use std::io;
55use std:: io:: { BufRead , BufReader } ;
66use std:: mem;
77use std:: os:: unix:: io:: AsRawFd ;
8+ use std:: ptr;
9+ use std:: os:: unix:: io:: FromRawFd ;
10+ use std:: os:: unix:: io:: IntoRawFd ;
811use std:: str;
912
1013use crate :: kb:: Key ;
@@ -363,3 +366,289 @@ pub fn wants_emoji() -> bool {
363366pub 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