@@ -5,7 +5,8 @@ use std::io;
55use std:: io:: { BufRead , BufReader } ;
66use std:: mem;
77use std:: os:: unix:: io:: AsRawFd ;
8- use std:: ptr;
8+ use std:: os:: unix:: io:: FromRawFd ;
9+ use std:: os:: unix:: io:: IntoRawFd ;
910use std:: str;
1011
1112use crate :: kb:: Key ;
@@ -360,3 +361,289 @@ pub fn wants_emoji() -> bool {
360361pub 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