fix: stream preview command instead of waiting for completion

closes #1174
This commit is contained in:
Loric ANDRE 2026-09-08 13:07:33 +02:00
parent a177153700
commit 0f2322970b
2 changed files with 65 additions and 1 deletions

View file

@ -35,17 +35,35 @@ const VT_SCROLLBACK: usize = 100_000;
type PlainChild = Arc<Mutex<Option<Child>>>;
fn read_bounded(mut reader: impl Read) -> Vec<u8> {
read_bounded_with_updates(&mut reader, |_| {})
}
fn read_bounded_with_updates(mut reader: impl Read, mut update: impl FnMut(&[u8])) -> Vec<u8> {
const UPDATE_INTERVAL: Duration = Duration::from_millis(16);
let mut output = Vec::with_capacity(PREVIEW_MAX_BYTES);
let mut buffer = [0; 8192];
let mut last_update = None;
let mut published_len = 0;
loop {
match reader.read(&mut buffer) {
Ok(0) | Err(_) => break,
Ok(read) => {
let retained = PREVIEW_MAX_BYTES.saturating_sub(output.len()).min(read);
output.extend_from_slice(&buffer[..retained]);
let update_due = last_update.is_none_or(|last: Instant| last.elapsed() >= UPDATE_INTERVAL);
if retained > 0 && (update_due || output.len() == PREVIEW_MAX_BYTES) {
update(&output);
published_len = output.len();
last_update = Some(Instant::now());
}
}
}
}
if output.len() != published_len {
update(&output);
}
output
}
@ -570,7 +588,17 @@ impl Preview {
self.plain_child = Some(child.clone());
self.thread_handle = Some(std::thread::spawn(move || {
let stdout_reader = std::thread::spawn(move || read_bounded(stdout));
let streaming_content = content.clone();
let stdout_reader = std::thread::spawn(move || {
read_bounded_with_updates(stdout, |output| {
let Ok(text) = output.to_vec().into_text() else {
return;
};
if let Ok(mut content) = streaming_content.write() {
*content = PreviewContent::Text(text);
}
})
});
let stderr_reader = std::thread::spawn(move || read_bounded(stderr));
let status = loop {

View file

@ -86,6 +86,42 @@ fn bounded_reader_discards_output_after_limit() {
assert_eq!(output.len(), PREVIEW_MAX_BYTES);
}
#[cfg(unix)]
#[test]
fn plain_preview_streams_before_command_exits() {
use ratatui::backend::TestBackend;
use std::time::{Duration, Instant};
let mut preview = Preview::default();
preview.pty = None;
let mut tui =
super::super::Tui::new_with_height_and_backend(TestBackend::new(20, 5), super::super::Size::Percent(100))
.unwrap();
preview.spawn(&mut tui, "printf streamed; sleep 30").unwrap();
let started = Instant::now();
loop {
let has_streamed_output = preview.content.read().is_ok_and(|content| match &*content {
PreviewContent::Text(text) => text
.lines
.iter()
.any(|line| line.spans.iter().any(|span| span.content.as_ref().contains("streamed"))),
_ => false,
});
if has_streamed_output {
break;
}
assert!(
started.elapsed() < Duration::from_secs(2),
"preview output did not stream"
);
std::thread::sleep(Duration::from_millis(10));
}
preview.kill();
preview.thread_handle.take().unwrap().join().unwrap();
}
#[cfg(unix)]
#[test]
fn plain_preview_can_be_cancelled() {