Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions engine/src/commands/task/status.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,15 @@ pub fn cmd_status(

// Flicker-free watch: hide cursor, use cursor repositioning
ui::hide_cursor();
// Initial clear
print!("\x1b[2J\x1b[H");
// Initial clear — \x1b[3J clears the scrollback buffer in addition to the
// visible screen (\x1b[2J), preventing old scrollback content from being
// reflowed into the visible area on terminal resize.
print!("\x1b[3J\x1b[2J\x1b[H");
std::io::Write::flush(&mut std::io::stdout()).ok();

let mut prev_lines = 0;
// Track terminal width across ticks to detect resize events.
let mut prev_term_width: usize = 0;

// Ensure cursor is restored on exit (Ctrl+C, normal return, or panic)
struct CursorGuard;
Expand Down Expand Up @@ -127,6 +131,17 @@ pub fn cmd_status(
.map(|(w, _)| w.0 as usize)
.unwrap_or(80);

// On terminal resize: full-clear including scrollback buffer.
// Without this, the terminal reflows old scrollback content into the visible
// area, causing the header to appear twice and leaving residual lines below
// the new render.
if prev_term_width != 0 && current_term_width != prev_term_width {
print!("\x1b[3J\x1b[2J\x1b[H");
std::io::Write::flush(&mut std::io::stdout()).ok();
prev_lines = 0;
}
prev_term_width = current_term_width;

if tick_count >= ticks_per_refresh {
running = db.list_tasks(Some(Status::Running))?;
pending = db.list_tasks(Some(Status::Pending))?;
Expand Down
18 changes: 10 additions & 8 deletions engine/src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,12 @@ pub fn ansi_truncate(s: &str, max_width: usize) -> String {
/// Each line is truncated to `term_width` visible columns before writing so that
/// long lines never wrap in the terminal — wrapping breaks the cursor-home
/// refresh strategy and makes the layout explode on narrow panels.
pub fn refresh_screen(content: &str, prev_lines: usize, term_width: usize) {
///
/// After writing all content lines, `\x1b[J` (erase from cursor to bottom) clears
/// any residual lines from previous renders. This is more robust than the old
/// prev_lines counting approach, which could not account for wrapped lines
/// introduced by a terminal resize.
pub fn refresh_screen(content: &str, _prev_lines: usize, term_width: usize) {
let mut stdout = std::io::stdout();
// Move cursor to home position (top-left)
let _ = write!(stdout, "\x1b[H");
Expand All @@ -252,13 +257,10 @@ pub fn refresh_screen(content: &str, prev_lines: usize, term_width: usize) {
// Write line + clear to end of line + newline
let _ = writeln!(stdout, "{safe}\x1b[K");
}
// Clear any remaining lines from previous render
let current_lines = content.lines().count();
if current_lines < prev_lines {
for _ in 0..(prev_lines - current_lines) {
let _ = writeln!(stdout, "\x1b[K");
}
}
// Erase from cursor to end of screen.
// Clears residual lines from any previous render that was taller than the
// current one, including phantom lines left by terminal resize reflow.
let _ = write!(stdout, "\x1b[J");
let _ = stdout.flush();
}

Expand Down
Loading