fix: image appearing over alternate screen

This commit is contained in:
2026-07-04 21:18:12 +02:00
parent 96b85f0159
commit 04b6776ebf
7 changed files with 344 additions and 162 deletions
+76 -48
View File
@@ -287,8 +287,7 @@ struct SavedCursor {
}
/// Alternate screen buffer state.
#[derive(Clone)]
struct AlternateScreen {
pub struct AlternateScreen {
grid: Vec<Vec<Cell>>,
line_map: Vec<usize>,
cursor_col: usize,
@@ -296,6 +295,7 @@ struct AlternateScreen {
saved_cursor: SavedCursor,
scroll_top: usize,
scroll_bottom: usize,
pub image_storage: ImageStorage,
}
/// Kitty-style ring buffer for scrollback history.
@@ -472,7 +472,7 @@ pub struct Terminal {
/// Saved cursor state (DECSC/DECRC).
saved_cursor: SavedCursor,
/// Alternate screen buffer (for fullscreen apps like vim, less).
alternate_screen: Option<AlternateScreen>,
pub alternate_screen: Option<AlternateScreen>,
/// Whether we're currently using the alternate screen.
pub using_alternate_screen: bool,
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
@@ -775,16 +775,19 @@ impl Terminal {
return; // Already in alternate screen
}
// Save main screen state
self.alternate_screen = Some(AlternateScreen {
grid: self.grid.clone(),
line_map: self.line_map.clone(),
cursor_col: self.cursor_col,
cursor_row: self.cursor_row,
saved_cursor: self.saved_cursor.clone(),
scroll_top: self.scroll_top,
scroll_bottom: self.scroll_bottom,
});
// Create alternate screen if it doesn't exist, otherwise reuse it
if self.alternate_screen.is_none() {
self.alternate_screen = Some(AlternateScreen {
grid: self.grid.clone(),
line_map: self.line_map.clone(),
cursor_col: self.cursor_col,
cursor_row: self.cursor_row,
saved_cursor: self.saved_cursor.clone(),
scroll_top: self.scroll_top,
scroll_bottom: self.scroll_bottom,
image_storage: ImageStorage::new(),
});
}
// Clear the screen for alternate buffer
self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
@@ -812,10 +815,10 @@ impl Terminal {
return; // Not in alternate screen
}
if let Some(saved) = self.alternate_screen.take() {
self.grid = saved.grid;
self.line_map = saved.line_map;
self.saved_cursor = saved.saved_cursor;
if let Some(saved) = self.alternate_screen.as_ref() {
self.grid = saved.grid.clone();
self.line_map = saved.line_map.clone();
self.saved_cursor = saved.saved_cursor.clone();
self.scroll_top = saved.scroll_top;
self.scroll_bottom = saved.scroll_bottom;
// Clamp cursor positions to current grid dimensions (defensive)
@@ -2721,51 +2724,76 @@ impl Terminal {
// Convert cursor_row to absolute row (accounting for scrollback)
// This allows images to scroll with terminal content
let absolute_row = self.scrollback.len() + self.cursor_row;
let absolute_row = if self.using_alternate_screen {
self.cursor_row
} else {
self.scrollback.len() + self.cursor_row
};
// Process the command
let (response, placement_result) =
log::debug!(
"Routing image command to {}: cursor_col={}, absolute_row={}, using_alt={}",
if self.using_alternate_screen { "alternate" } else { "main" },
self.cursor_col,
absolute_row,
self.using_alternate_screen
);
let (response, placement_result) = if self.using_alternate_screen {
if let Some(ref mut alt) = self.alternate_screen {
alt.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
)
} else {
// This should not happen if using_alternate_screen is true
(None, None)
}
} else {
self.image_storage.process_command(
cmd,
self.cursor_col,
absolute_row,
self.cell_width,
self.cell_height,
);
)
};
// Queue the response to send back to the application
if let Some(resp) = response {
self.response_queue.extend_from_slice(resp.as_bytes());
}
// Move cursor after image placement per Kitty protocol spec:
// "After placing an image on the screen the cursor must be moved to the
// right by the number of cols in the image placement rectangle and down
// by the number of rows in the image placement rectangle."
// However, if C=1 was specified, don't move the cursor.
if let Some(placement) = placement_result {
if !placement.suppress_cursor_move
&& !placement.virtual_placement
{
// Move cursor to the right and down by the image dimensions
self.cursor_col += placement.cols;
let new_row = self.cursor_row + placement.rows;
if new_row >= self.rows {
// Need to scroll
let scroll_amount = new_row - self.rows + 1;
self.scroll_up(scroll_amount);
self.cursor_row = self.rows - 1;
} else {
self.cursor_row = new_row;
// Move cursor after image placement per Kitty protocol spec:
// "After placing an image on the screen the cursor must be moved to the
// right by the number of cols in the image placement rectangle and down
// by the number of rows in the image placement rectangle."
// However, if C=1 was specified, don't move the cursor.
if let Some(placement) = placement_result {
self.dirty = true;
if !placement.suppress_cursor_move
&& !placement.virtual_placement
{
// Move cursor to the right and and down by the image dimensions
self.cursor_col += placement.cols;
let new_row = self.cursor_row + placement.rows;
if new_row >= self.rows {
// Need to scroll
let scroll_amount = new_row - self.rows + 1;
self.scroll_up(scroll_amount);
self.cursor_row = self.rows - 1;
} else {
self.cursor_row = new_row;
}
// If cursor is now beyond the right edge, it will be handled by the normal
// cursor movement logic (wrapping/scrolling) if applicable.
log::debug!(
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
self.cursor_col, self.cursor_row, placement.cols, placement.rows
);
}
}
// If cursor is now beyond the right edge, it will be handled by the normal
// cursor movement logic (wrapping/scrolling) if applicable.
log::debug!(
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
self.cursor_col, self.cursor_row, placement.cols, placement.rows
);
}
}
}
}
}