@@ -5,6 +5,9 @@ use std::io;
5
5
use std:: io:: { BufRead , BufReader } ;
6
6
use std:: mem;
7
7
use std:: os:: unix:: io:: AsRawFd ;
8
+ use std:: ptr;
9
+ use std:: os:: unix:: io:: FromRawFd ;
10
+ use std:: os:: unix:: io:: IntoRawFd ;
8
11
use std:: str;
9
12
10
13
use crate :: kb:: Key ;
@@ -363,3 +366,289 @@ pub fn wants_emoji() -> bool {
363
366
pub fn set_title < T : Display > ( title : T ) {
364
367
print ! ( "\x1b ]0;{}\x07 " , title) ;
365
368
}
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