Skip to content

Commit

Permalink
synchronized output
Browse files Browse the repository at this point in the history
  • Loading branch information
Funami580 committed Dec 30, 2023
1 parent 9b533cc commit 545a454
Show file tree
Hide file tree
Showing 6 changed files with 299 additions and 0 deletions.
4 changes: 4 additions & 0 deletions Cargo.toml
Expand Up @@ -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"

Expand Down
11 changes: 11 additions & 0 deletions src/draw_target.rs
Expand Up @@ -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
///
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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(())
Expand Down
8 changes: 8 additions & 0 deletions src/in_memory.rs
Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions src/lib.rs
Expand Up @@ -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;
Expand Down
262 changes: 262 additions & 0 deletions 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<u8> {
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<R>(
f: impl FnOnce(&mut std::io::StdinLock, &mut std::io::StdoutLock, &mut std::io::StderrLock) -> R,
) -> std::io::Result<R> {
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())
}
13 changes: 13 additions & 0 deletions src/term_like.rs
Expand Up @@ -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 {
Expand Down Expand Up @@ -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")
}
}

0 comments on commit 545a454

Please sign in to comment.