diff --git a/Cargo.toml b/Cargo.toml index 211ed5bd..6aff84d6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,10 @@ tokio = { version = "1", features = ["fs", "time", "rt"] } futures = "0.3" # so the doctest for wrap_stream is nice pretty_assertions = "1.4.0" +[target.'cfg(unix)'.dependencies] +libc = "0.2" +lazy_static = "1.4.0" + [target.'cfg(target_arch = "wasm32")'.dependencies] instant = "0.1" diff --git a/src/draw_target.rs b/src/draw_target.rs index 003dfd5c..64f6ec66 100644 --- a/src/draw_target.rs +++ b/src/draw_target.rs @@ -11,6 +11,7 @@ use instant::Instant; use crate::multi::{MultiProgressAlignment, MultiState}; use crate::TermLike; +use crate::sync_output::supports_synchronized_output; /// Target for draw operations /// @@ -470,6 +471,11 @@ impl DrawState { return Ok(()); } + // Begin synchronized update + if supports_synchronized_output() { + term.begin_sync_update()?; + } + if !self.lines.is_empty() && self.move_cursor { term.move_cursor_up(*last_line_count)?; } else { @@ -542,6 +548,11 @@ impl DrawState { } term.write_str(&" ".repeat(last_line_filler))?; + // End synchronized update + if supports_synchronized_output() { + term.end_sync_update()?; + } + term.flush()?; *last_line_count = real_len - orphan_visual_line_count + shift; Ok(()) diff --git a/src/in_memory.rs b/src/in_memory.rs index 046ae14a..4c9c5a31 100644 --- a/src/in_memory.rs +++ b/src/in_memory.rs @@ -190,6 +190,14 @@ impl TermLike for InMemoryTerm { state.history.push(Move::Flush); state.parser.flush() } + + fn begin_sync_update(&self) -> std::io::Result<()> { + Ok(()) + } + + fn end_sync_update(&self) -> std::io::Result<()> { + Ok(()) + } } struct InMemoryTermState { diff --git a/src/lib.rs b/src/lib.rs index 2f7c8e9d..3a7140aa 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -243,6 +243,7 @@ mod progress_bar; mod rayon; mod state; pub mod style; +mod sync_output; mod term_like; pub use crate::draw_target::ProgressDrawTarget; diff --git a/src/sync_output.rs b/src/sync_output.rs new file mode 100644 index 00000000..31993a52 --- /dev/null +++ b/src/sync_output.rs @@ -0,0 +1,262 @@ +use lazy_static::lazy_static; + +#[cfg(unix)] +lazy_static! { + static ref SUPPORTS_SYNCHRONIZED_OUTPUT: bool = supports_synchronized_output_uncached(); +} + +#[cfg(not(unix))] +pub(crate) fn supports_synchronized_output() -> bool { + false +} + +#[cfg(unix)] +pub(crate) fn supports_synchronized_output() -> bool { + *SUPPORTS_SYNCHRONIZED_OUTPUT +} + +/// Specification: https://gist.github.com/christianparpart/d8a62cc1ab659194337d73e399004036 +#[cfg(unix)] +fn supports_synchronized_output_uncached() -> bool { + use std::io::{Read as _, Write as _}; + use std::os::fd::AsRawFd as _; + use std::time::Duration; + + const TIMEOUT_DURATION: Duration = Duration::from_millis(10); + + #[derive(PartialEq)] + enum ParserState { + None, + CsiOne, + CsiTwo, + QuestionMark, + ModeDigit1, + ModeDigit2, + ModeDigit3, + ModeDigit4, + Semicolon, + Response, + DollarSign, + Ypsilon, + } + + struct Parser { + state: ParserState, + response: u8, + } + + impl Parser { + fn process_byte(&mut self, byte: u8) { + match byte { + b'\x1b' => { + self.state = ParserState::CsiOne; + } + b'[' => { + self.state = if self.state == ParserState::CsiOne { + ParserState::CsiTwo + } else { + ParserState::None + }; + } + b'?' => { + self.state = if self.state == ParserState::CsiTwo { + ParserState::QuestionMark + } else { + ParserState::None + }; + } + byte @ b'0' => { + self.state = if self.state == ParserState::Semicolon { + self.response = byte; + ParserState::Response + } else if self.state == ParserState::ModeDigit1 { + ParserState::ModeDigit2 + } else { + ParserState::None + }; + } + byte @ b'2' => { + self.state = if self.state == ParserState::Semicolon { + self.response = byte; + ParserState::Response + } else if self.state == ParserState::QuestionMark { + ParserState::ModeDigit1 + } else if self.state == ParserState::ModeDigit2 { + ParserState::ModeDigit3 + } else { + ParserState::None + }; + } + byte @ b'1' | byte @ b'3' | byte @ b'4' => { + self.state = if self.state == ParserState::Semicolon { + self.response = byte; + ParserState::Response + } else { + ParserState::None + }; + } + b'6' => { + self.state = if self.state == ParserState::ModeDigit3 { + ParserState::ModeDigit4 + } else { + ParserState::None + }; + } + b';' => { + self.state = if self.state == ParserState::ModeDigit4 { + ParserState::Semicolon + } else { + ParserState::None + }; + } + b'$' => { + self.state = if self.state == ParserState::Response { + ParserState::DollarSign + } else { + ParserState::None + }; + } + b'y' => { + self.state = if self.state == ParserState::DollarSign { + ParserState::Ypsilon + } else { + ParserState::None + }; + } + _ => { + self.state = ParserState::None; + } + } + } + + fn get_response(&self) -> Option { + if self.state == ParserState::Ypsilon { + Some(self.response - b'0') + } else { + None + } + } + } + + with_raw_terminal(|stdin_lock, stdout_lock, _| { + write!(stdout_lock, "\x1b[?2026$p").ok()?; + stdout_lock.flush().ok()?; + + let stdin_fd = libc::pollfd { + fd: stdin_lock.as_raw_fd(), + events: libc::POLLIN, + revents: 0, + }; + let mut fds = [stdin_fd]; + let mut buf = [0u8; 256]; + let mut parser = Parser { + state: ParserState::None, + response: u8::MAX, + }; + let deadline = std::time::Instant::now() + TIMEOUT_DURATION; + + loop { + let remaining_time = deadline + .saturating_duration_since(std::time::Instant::now()) + .as_millis() + .try_into() + .ok()?; + + if remaining_time == 0 { + // Timeout + return Some(false); + } + + match unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as _, remaining_time) } { + 0 => { + // Timeout + return Some(false); + } + 1.. => { + 'read: loop { + match stdin_lock.read(&mut buf) { + Ok(0) => { + // Reached EOF + return Some(false); + } + Ok(size) => { + for byte in &buf[..size] { + parser.process_byte(*byte); + + match parser.get_response() { + Some(1 | 2) => return Some(true), + Some(_) => return Some(false), + None => {} + } + } + + break 'read; + } + Err(err) if err.kind() == std::io::ErrorKind::Interrupted => { + // Got interrupted, retry read + continue 'read; + } + Err(_) => { + return Some(false); + } + } + } + + // Reuse the pollfd for the next poll call + fds[0].revents = 0; + } + _ => { + // Error + return Some(false); + } + } + } + }) + .ok() + .flatten() + .unwrap_or(false) +} + +#[cfg(unix)] +fn with_raw_terminal( + f: impl FnOnce(&mut std::io::StdinLock, &mut std::io::StdoutLock, &mut std::io::StderrLock) -> R, +) -> std::io::Result { + use std::os::fd::AsRawFd as _; + + unsafe { + let fd = std::io::stdin().as_raw_fd(); + let mut ptr = std::mem::MaybeUninit::uninit(); + + if libc::tcgetattr(fd, ptr.as_mut_ptr()) == 0 { + let mut termios = ptr.assume_init(); + let old_iflag = termios.c_iflag; + let old_oflag = termios.c_oflag; + let old_cflag = termios.c_cflag; + let old_lflag = termios.c_lflag; + + libc::cfmakeraw(&mut termios); + + // Lock the standard streams, so no output gets lost while in raw mode + let mut stdin_lock = std::io::stdin().lock(); + let mut stdout_lock = std::io::stdout().lock(); + let mut stderr_lock = std::io::stderr().lock(); + + // Go into raw mode + if libc::tcsetattr(fd, libc::TCSADRAIN, &termios) == 0 { + let result = f(&mut stdin_lock, &mut stdout_lock, &mut stderr_lock); + + // Reset to previous mode + termios.c_iflag = old_iflag; + termios.c_oflag = old_oflag; + termios.c_cflag = old_cflag; + termios.c_lflag = old_lflag; + + if libc::tcsetattr(fd, libc::TCSADRAIN, &termios) == 0 { + return Ok(result); + } + } + } + } + + Err(std::io::Error::last_os_error()) +} diff --git a/src/term_like.rs b/src/term_like.rs index b489b655..144d520a 100644 --- a/src/term_like.rs +++ b/src/term_like.rs @@ -34,6 +34,11 @@ pub trait TermLike: Debug + Send + Sync { fn clear_line(&self) -> io::Result<()>; fn flush(&self) -> io::Result<()>; + + /// Begin synchronized update + fn begin_sync_update(&self) -> io::Result<()>; + /// End synchronized update + fn end_sync_update(&self) -> io::Result<()>; } impl TermLike for Term { @@ -76,4 +81,12 @@ impl TermLike for Term { fn flush(&self) -> io::Result<()> { self.flush() } + + fn begin_sync_update(&self) -> io::Result<()> { + self.write_str("\x1b[?2026h") + } + + fn end_sync_update(&self) -> io::Result<()> { + self.write_str("\x1b[?2026l") + } }