aboutsummaryrefslogtreecommitdiff
path: root/src/pane
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:23 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-06-01 23:05:51 +0300
commit5bd21b7ed468b5ff4d1a5b2ed7e211f401eb138a (patch)
tree672fa4625cca0ecb3e9376fc7f8e33d7b0e9deef /src/pane
parent32abb3776668ee768200019c170956f67fe33a86 (diff)
chore: update libghostty vendor
refs #276
Diffstat (limited to 'src/pane')
-rw-r--r--src/pane/terminal.rs162
1 files changed, 134 insertions, 28 deletions
diff --git a/src/pane/terminal.rs b/src/pane/terminal.rs
index db05d1fa..8841bad0 100644
--- a/src/pane/terminal.rs
+++ b/src/pane/terminal.rs
@@ -562,9 +562,58 @@ impl GhosttyPaneTerminal {
pub fn resize(&self, rows: u16, cols: u16, cell_width_px: u32, cell_height_px: u32) {
if let Ok(mut core) = self.core.lock() {
+ let offset_from_bottom = core
+ .terminal
+ .scrollbar()
+ .ok()
+ .map(|scrollbar| {
+ scrollbar
+ .total
+ .saturating_sub(scrollbar.offset + scrollbar.len)
+ })
+ .unwrap_or(0);
+ let bottom_before_resize = ghostty_detection_text(&core)
+ .map(|text| !text.trim().is_empty())
+ .unwrap_or(false);
+ let resize_recovery_probe_lines = usize::from(rows)
+ .saturating_mul(8)
+ .max(DEFAULT_DETECTION_ROWS);
+ let replay_ansi = if core.terminal.active_screen().ok()
+ == Some(crate::ghostty::ActiveScreen::Primary)
+ && bottom_before_resize
+ {
+ ghostty_recent_ansi(&core, resize_recovery_probe_lines, true)
+ .ok()
+ .filter(|ansi| !ansi.trim().is_empty())
+ } else {
+ None
+ };
+
let _ = core
.terminal
.resize(cols, rows, cell_width_px, cell_height_px);
+
+ let bottom_is_blank = ghostty_detection_text(&core)
+ .map(|text| text.trim().is_empty())
+ .unwrap_or(false);
+ if bottom_is_blank {
+ if let Some(ansi) = replay_ansi.as_deref() {
+ core.terminal.scroll_viewport_bottom();
+ core.terminal.write(ansi.as_bytes());
+ }
+ }
+ ghostty_restore_scroll_offset_from_bottom(&mut core.terminal, offset_from_bottom);
+ if offset_from_bottom > 0 {
+ let mut remaining = offset_from_bottom.min(resize_recovery_probe_lines);
+ while remaining > 0
+ && ghostty_visible_text(&mut core)
+ .map(|text| text.trim().is_empty())
+ .unwrap_or(false)
+ {
+ core.terminal.scroll_viewport_delta(1);
+ remaining -= 1;
+ }
+ }
}
}
@@ -889,7 +938,7 @@ impl GhosttyPaneTerminal {
Ok(rows) => rows,
Err(_) => return,
};
- let mut grapheme_scratch = Vec::new();
+ let mut grapheme_bytes = Vec::new();
let mut symbol_scratch = String::new();
let mut y = 0u16;
while y < area.height && rows.next() {
@@ -911,7 +960,7 @@ impl GhosttyPaneTerminal {
&cells,
wide,
hide_kitty_placeholders,
- &mut grapheme_scratch,
+ &mut grapheme_bytes,
&mut symbol_scratch,
) {
Ok(symbol) => symbol,
@@ -1034,6 +1083,9 @@ fn ghostty_recent_text(
) -> Result<String, crate::ghostty::Error> {
let total_rows = core.terminal.total_rows()?;
let cols = core.terminal.cols()?;
+ if total_rows == 0 || cols == 0 {
+ return Ok(String::new());
+ }
let start = total_rows.saturating_sub(lines);
let mut rows = Vec::with_capacity(total_rows.saturating_sub(start));
for y in start..total_rows {
@@ -1053,7 +1105,7 @@ fn ghostty_recent_text_unwrapped(
return Ok(String::new());
}
let start = total_rows.saturating_sub(lines) as u32;
- let end = (total_rows.saturating_sub(1)) as u32;
+ let end = total_rows.saturating_sub(1) as u32;
core.terminal
.read_text_screen((0, start), (cols.saturating_sub(1), end), false)
}
@@ -1069,11 +1121,29 @@ fn ghostty_recent_ansi(
return Ok(String::new());
}
let start = total_rows.saturating_sub(lines) as u32;
- let end = (total_rows.saturating_sub(1)) as u32;
+ let end = total_rows.saturating_sub(1) as u32;
core.terminal
.read_ansi_screen((0, start), (cols.saturating_sub(1), end), false, unwrap)
}
+fn ghostty_restore_scroll_offset_from_bottom(
+ terminal: &mut crate::ghostty::Terminal,
+ offset_from_bottom: usize,
+) {
+ terminal.scroll_viewport_bottom();
+ if offset_from_bottom == 0 {
+ return;
+ }
+ let Ok(scrollbar) = terminal.scrollbar() else {
+ return;
+ };
+ let max_offset = scrollbar.total.saturating_sub(scrollbar.len);
+ let offset = offset_from_bottom.min(max_offset).min(isize::MAX as usize) as isize;
+ if offset > 0 {
+ terminal.scroll_viewport_delta(-offset);
+ }
+}
+
fn ghostty_extract_selection(
core: &mut GhosttyPaneCore,
selection: &crate::selection::Selection,
@@ -1091,7 +1161,9 @@ fn ghostty_screen_row(
let mut line = String::new();
for x in 0..cols {
let graphemes = core.terminal.screen_graphemes(x, y)?;
- if graphemes.is_empty() {
+ if graphemes.is_empty()
+ || graphemes.first().copied() == Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER)
+ {
line.push(' ');
} else {
for codepoint in graphemes {
@@ -1117,18 +1189,12 @@ fn ghostty_line_from_cells(
fn ghostty_cell_symbol(
cells: &crate::ghostty::RowCellIter<'_>,
) -> Result<String, crate::ghostty::Error> {
- let graphemes = cells.graphemes()?;
- if graphemes.is_empty() {
+ let text = cells.grapheme_text()?;
+ if text.chars().next().map(u32::from) == Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER) {
return Ok(" ".to_string());
}
- let mut text = String::new();
- for codepoint in graphemes {
- if let Some(ch) = char::from_u32(codepoint) {
- text.push(ch);
- }
- }
if text.is_empty() {
- text.push(' ');
+ return Ok(" ".to_string());
}
Ok(text)
}
@@ -1167,7 +1233,7 @@ fn ghostty_buffer_symbol_into<'a>(
cells: &crate::ghostty::RowCellIter<'_>,
wide: crate::ghostty::CellWide,
hide_kitty_placeholders: bool,
- grapheme_scratch: &mut Vec<u32>,
+ grapheme_bytes: &mut Vec<u8>,
symbol_scratch: &'a mut String,
) -> Result<&'a str, crate::ghostty::Error> {
symbol_scratch.clear();
@@ -1175,21 +1241,13 @@ fn ghostty_buffer_symbol_into<'a>(
crate::ghostty::CellWide::SpacerTail => {}
crate::ghostty::CellWide::SpacerHead => symbol_scratch.push(' '),
crate::ghostty::CellWide::Narrow | crate::ghostty::CellWide::Wide => {
- cells.graphemes_into(grapheme_scratch)?;
+ cells.grapheme_text_into(grapheme_bytes, symbol_scratch)?;
let hidden_kitty_placeholder = hide_kitty_placeholders
- && grapheme_scratch.first().copied()
+ && symbol_scratch.chars().next().map(u32::from)
== Some(crate::ghostty::KITTY_UNICODE_PLACEHOLDER);
- if hidden_kitty_placeholder || grapheme_scratch.is_empty() {
+ if hidden_kitty_placeholder || symbol_scratch.is_empty() {
+ symbol_scratch.clear();
symbol_scratch.push(' ');
- } else {
- for &codepoint in grapheme_scratch.iter() {
- if let Some(ch) = char::from_u32(codepoint) {
- symbol_scratch.push(ch);
- }
- }
- if symbol_scratch.is_empty() {
- symbol_scratch.push(' ');
- }
}
}
}
@@ -2106,8 +2164,18 @@ mod tests {
let metrics = pane.scroll_metrics().expect("scroll metrics after resize");
assert_eq!(metrics.viewport_rows, rows as usize);
assert!(metrics.offset_from_bottom <= metrics.max_offset_from_bottom);
+ assert!(
+ metrics.offset_from_bottom > 0,
+ "resize should preserve a scrolled viewport instead of jumping to bottom"
+ );
assert!(metrics.max_offset_from_bottom > 0);
- assert!(!pane.visible_text().trim().is_empty());
+ let visible = pane.visible_text();
+ assert!(
+ !visible.trim().is_empty(),
+ "visible text should not be empty after resize to {rows}x{cols}; metrics={metrics:?}; detection={:?}; recent={:?}",
+ pane.detection_text(),
+ pane.recent_text(6)
+ );
assert!(
pane.detection_text().contains("END"),
"bottom detection should remain independent from the scrolled viewport after resize"
@@ -2116,6 +2184,42 @@ mod tests {
}
#[test]
+ fn resize_recovery_does_not_replay_history_when_visible_screen_was_blank() {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(20, 3, 10_000).unwrap();
+ terminal.write(b"old history\r\n\x1b[2J\x1b[H");
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ assert!(pane.visible_text().trim().is_empty());
+ assert!(pane.detection_text().trim().is_empty());
+
+ pane.resize(3, 20, 0, 0);
+
+ assert!(pane.visible_text().trim().is_empty());
+ assert!(pane.detection_text().trim().is_empty());
+ assert!(pane.recent_text(3).trim().is_empty());
+ }
+
+ #[test]
+ fn resize_recovery_does_not_replay_scrolled_history_over_blank_bottom() {
+ let (tx, _rx) = mpsc::channel(4);
+ let mut terminal = crate::ghostty::Terminal::new(20, 3, 10_000).unwrap();
+ write_numbered_lines(&mut terminal, 20);
+ terminal.write(b"\x1b[2J\x1b[H");
+ let pane = GhosttyPaneTerminal::new(terminal, tx).unwrap();
+
+ assert!(pane.detection_text().trim().is_empty());
+ let metrics = pane.scroll_metrics().expect("scroll metrics");
+ pane.set_scroll_offset_from_bottom(metrics.max_offset_from_bottom);
+ assert!(!pane.visible_text().trim().is_empty());
+
+ pane.resize(3, 20, 0, 0);
+
+ assert!(pane.detection_text().trim().is_empty());
+ assert!(pane.recent_text(3).trim().is_empty());
+ }
+
+ #[test]
fn synchronized_output_suppresses_intermediate_render_requests_until_batch_ends() {
let (tx, _rx) = mpsc::channel(4);
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
@@ -2198,6 +2302,8 @@ mod tests {
assert_eq!(buffer[(0, 0)].symbol(), "b");
assert_eq!(buffer[(6, 0)].symbol(), " ");
assert_eq!(buffer[(7, 0)].symbol(), "a");
+ assert_eq!(pane.visible_text().lines().next(), Some("before after"));
+ assert_eq!(pane.recent_text(5), "before after\n");
}
#[test]