diff --git a/crates/vp_shared/src/progress.rs b/crates/vp_shared/src/progress.rs index fc880b6a27..8a32dcbb35 100644 --- a/crates/vp_shared/src/progress.rs +++ b/crates/vp_shared/src/progress.rs @@ -69,8 +69,10 @@ impl Progress { /// Show a new row, blocking keyboard input and hiding the cursor until it is cleared. fn start(bar: ProgressBar, style: ProgressStyle) -> Self { + // Write cursor controls before the redraw thread can emit a progress frame. + let input = input::block(); let mut progress = Self::new(bar, style, None); - progress._input = input::block(); + progress._input = input; progress } diff --git a/crates/vp_shared/src/progress/input.rs b/crates/vp_shared/src/progress/input.rs index d60b0eed7f..af0435d30b 100644 --- a/crates/vp_shared/src/progress/input.rs +++ b/crates/vp_shared/src/progress/input.rs @@ -366,9 +366,7 @@ mod tests { sys::save(fd).is_some_and(|raw| sys::apply(fd, &raw)) } - /// The cursor changes written to the pty since the last call, `l` for each - /// hide and `h` for each show. - fn cursor_changes(master: &OwnedFd) -> String { + fn terminal_output(master: &OwnedFd) -> Vec { let mut output = Vec::new(); let mut buf = [0; 1024]; loop { @@ -382,6 +380,12 @@ mod tests { } } output + } + + /// The cursor changes written to the pty since the last call, `l` for each + /// hide and `h` for each show. + fn cursor_changes(master: &OwnedFd) -> String { + terminal_output(master) .windows(6) .filter_map(|sequence| match sequence { b"\x1b[?25l" => Some('l'), @@ -391,6 +395,41 @@ mod tests { .collect() } + const PROGRESS_CHILD: &str = "VP_SHARED_INPUT_PROGRESS_CHILD"; + + #[test] + fn progress_hides_cursor_before_first_frame() { + use std::process::Command; + + if std::env::var_os(PROGRESS_CHILD).is_some() { + crate::EnvConfig::with_vars([("CI", None::<&str>)], |_| { + let _progress = crate::progress::Progress::counter("Preparing fixture", 1) + .expect("stderr is a terminal"); + }); + return; + } + + let size = nix::pty::Winsize { ws_row: 24, ws_col: 80, ws_xpixel: 0, ws_ypixel: 0 }; + let pty = openpty(Some(&size), None).unwrap(); + let status = Command::new(std::env::current_exe().unwrap()) + .args(["--exact", "progress::input::tests::progress_hides_cursor_before_first_frame"]) + .env("TERM", "xterm-256color") + .env(PROGRESS_CHILD, "1") + .stdin(pty.slave.try_clone().unwrap()) + .stdout(std::process::Stdio::null()) + .stderr(pty.slave.try_clone().unwrap()) + .status() + .unwrap(); + + assert!(status.success(), "{status}"); + let output = terminal_output(&pty.master); + assert!(output.starts_with(b"\x1b[?25l"), "{output:?}"); + assert!( + output.windows(b"Preparing fixture".len()).any(|part| part == b"Preparing fixture"), + "{output:?}" + ); + } + #[test] fn raw_input_keeps_signals_and_restores_the_terminal() { let pty = openpty(None, None).unwrap();