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
17 changes: 17 additions & 0 deletions src/app_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3917,6 +3917,23 @@ fn unchanged_frame_emits_nothing() {
assert!(second.is_empty(), "an identical frame is a no-op");
}

#[test]
fn peek_shows_short_output_instead_of_the_blank_grid_tail() {
let mut app = App::attached_with_lines(&["alpha", "beta", "gamma"]);
assert_eq!(
app.selected_id, app.focused_id,
"the peek reads the selected task's screen"
);
app.mode = Mode::Peek;
app.last_frame.clear();
let mut out = Vec::new();
crate::ui::render(&mut out, &mut app).unwrap();
let frame = String::from_utf8_lossy(&out);
for word in ["alpha", "beta", "gamma"] {
assert!(frame.contains(word), "peek frame must show {word:?}");
}
}

// Repaint timing.

/// Core-driven repaints wait for the remainder of `PAINT_MIN`.
Expand Down
88 changes: 84 additions & 4 deletions src/ui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -448,10 +448,10 @@ fn render_peek(out: &mut impl Write, app: &App) -> io::Result<()> {
let bh = rows.saturating_sub(6).clamp(5, 16);

// Use an empty body until the selected task's screen arrives.
let lines: &[String] = app.screen_for(v.id).map_or(&[], |s| &s.lines);
// Show the newest lines that fit inside the overlay.
let start = lines.len().saturating_sub(bh.saturating_sub(2));
let tail = &lines[start..];
let (lines, alt_screen): (&[String], bool) = app
.screen_for(v.id)
.map_or((&[], false), |s| (&s.lines, s.alt_screen));
let tail = peek_window(lines, bh.saturating_sub(2), alt_screen);

// The peek footer identifies the preview source and in-process matcher.
let footer = format!(
Expand All @@ -472,6 +472,22 @@ fn render_peek(out: &mut impl Write, app: &App) -> io::Result<()> {
)
}

/// The peek body is the last `height` lines of the selected task's screen, or all lines
/// when the task is in alternate-screen mode. When the screen is shorter than `height`,
/// all lines are returned.
fn peek_window(lines: &[String], height: usize, alt_screen: bool) -> &[String] {
// `contents()` trims trailing padding, so a blank row is exactly empty.
let end = if alt_screen {
lines.len()
} else {
lines
.iter()
.rposition(|l| !l.is_empty())
.map_or(0, |i| i + 1)
};
&lines[end.saturating_sub(height)..end]
}

/// The peek footer's provenance label: source, then the matcher rule when
/// one produced it, then the frozen flag. Examples: `title`, `floor (frozen)`.
fn preview_provenance(p: &Preview) -> String {
Expand Down Expand Up @@ -1009,6 +1025,70 @@ mod tests {
assert!(selection_overlay(Some(&sel), &[]).is_empty());
}

fn rows(spec: &[&str]) -> Vec<String> {
spec.iter().map(|s| s.to_string()).collect()
}

/// A pane-sized grid whose first rows carry `content`, the rest blank.
fn grid(content: &[&str], height: usize) -> Vec<String> {
let mut g = rows(content);
g.resize(height, String::new());
g
}

#[test]
fn peek_window_shows_short_output_from_its_first_row() {
// Three rows in a 39-row grid: the window is those rows, not the
// grid's blank tail.
let g = grid(&["alpha", "beta", "gamma"], 39);
assert_eq!(peek_window(&g, 14, false), &g[..3]);
}

#[test]
fn peek_window_ends_at_the_last_non_blank_row() {
// Twenty content rows, height 14: rows 6..20, trailing blanks skipped.
let content: Vec<String> = (0..20).map(|i| format!("row{i}")).collect();
let mut g = content.clone();
g.resize(39, String::new());
assert_eq!(peek_window(&g, 14, false), &content[6..20]);
}

#[test]
fn peek_window_on_a_full_grid_is_the_bottom_slice() {
// A non-blank last row makes the content window the grid bottom:
// the scrolled-output case keeps its old crop exactly.
let g: Vec<String> = (0..39).map(|i| format!("row{i}")).collect();
assert_eq!(peek_window(&g, 14, false), &g[25..]);
}

#[test]
fn peek_window_keeps_interior_blank_rows() {
let g = grid(&["para one", "", "para two"], 39);
assert_eq!(peek_window(&g, 14, false), &g[..3]);
// A window shorter than the content still ends at the last row.
assert_eq!(peek_window(&g, 2, false), &g[1..3]);
}

#[test]
fn peek_window_of_a_blank_grid_is_empty() {
let g = grid(&[], 39);
assert!(peek_window(&g, 14, false).is_empty());
assert!(peek_window(&[], 14, false).is_empty());
assert!(peek_window(&[], 14, true).is_empty());
}

#[test]
fn peek_window_pins_the_alternate_screen_to_the_grid_bottom() {
// A canvas with a blank tail keeps the bottom crop: a partial repaint
// must not shift the window.
let g = grid(&["dialog"], 39);
assert_eq!(peek_window(&g, 14, true), &g[25..]);
assert!(peek_window(&g, 14, true).iter().all(String::is_empty));
// Height beyond the grid saturates to the whole grid.
assert_eq!(peek_window(&g, 50, true), &g[..]);
assert_eq!(peek_window(&g, 50, false), &g[..1]);
}

#[test]
fn scroll_window_keeps_selection_visible() {
assert_eq!(scroll_window(0, 5, 8), (0, 5)); // fits, no scroll
Expand Down