aboutsummaryrefslogtreecommitdiff
path: root/src/ghostty
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-03 14:24:33 +0300
committerGitHub <noreply@github.com>2026-09-03 14:24:33 +0300
commit9a2a7af5402f2bc67ab24c8b4c14c6dd20a43bb2 (patch)
tree902f701de84d6fb7e08d16baaa85f99367cb9912 /src/ghostty
parent94f6d9c0d9bb9cf9ffae99d8bbfb09e9bf2fc9e0 (diff)
perf: compress idle terminal scrollback (#3556)
Diffstat (limited to 'src/ghostty')
-rw-r--r--src/ghostty/mod.rs74
1 files changed, 74 insertions, 0 deletions
diff --git a/src/ghostty/mod.rs b/src/ghostty/mod.rs
index 261638af..691160e6 100644
--- a/src/ghostty/mod.rs
+++ b/src/ghostty/mod.rs
@@ -74,6 +74,13 @@ pub enum Dirty {
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub(crate) enum TerminalCompressionResult {
+ Unsupported,
+ Pending,
+ Complete,
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RowSelection {
pub start_x: u16,
pub end_x: u16,
@@ -871,6 +878,41 @@ impl Terminal {
}
}
+ pub(crate) fn compression_activity(&self) -> Result<u64, Error> {
+ let mut activity = 0;
+ // SAFETY: self.raw is a live terminal handle and activity is a valid out pointer.
+ unsafe {
+ ffi::ghostty_terminal_compression_activity(self.raw, &mut activity).into_result()?;
+ }
+ Ok(activity)
+ }
+
+ pub(crate) fn compress_incremental(&mut self) -> Result<TerminalCompressionResult, Error> {
+ let mut result =
+ ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED;
+ // SAFETY: self.raw is a live terminal handle and result is a valid out pointer.
+ unsafe {
+ ffi::ghostty_terminal_compress(
+ self.raw,
+ ffi::GhosttyTerminalCompressionMode_GHOSTTY_TERMINAL_COMPRESSION_MODE_INCREMENTAL,
+ &mut result,
+ )
+ .into_result()?;
+ }
+ match result {
+ ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_UNSUPPORTED => {
+ Ok(TerminalCompressionResult::Unsupported)
+ }
+ ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_PENDING => {
+ Ok(TerminalCompressionResult::Pending)
+ }
+ ffi::GhosttyTerminalCompressionResult_GHOSTTY_TERMINAL_COMPRESSION_RESULT_COMPLETE => {
+ Ok(TerminalCompressionResult::Complete)
+ }
+ _ => Err(Error(ffi::GhosttyResult_GHOSTTY_INVALID_VALUE)),
+ }
+ }
+
pub fn set_default_palette(&mut self, palette: &[RgbColor; 256]) -> Result<(), Error> {
let palette = palette.map(|color| ffi::GhosttyColorRgb {
r: color.r,
@@ -3304,6 +3346,38 @@ mod tests {
}
#[test]
+ fn incremental_compression_preserves_cold_scrollback() {
+ let mut terminal = Terminal::new(80, 24, 20_000_000).unwrap();
+ let initial_activity = terminal.compression_activity().unwrap();
+ let suffix = "x".repeat(66);
+ for line in 1..=10_000 {
+ terminal.write(format!("{line:05} {suffix}\r\n").as_bytes());
+ }
+ assert_ne!(terminal.compression_activity().unwrap(), initial_activity);
+
+ let mut complete = false;
+ for _ in 0..10_000 {
+ match terminal.compress_incremental().unwrap() {
+ TerminalCompressionResult::Unsupported => return,
+ TerminalCompressionResult::Pending => {}
+ TerminalCompressionResult::Complete => {
+ complete = true;
+ break;
+ }
+ }
+ }
+ assert!(complete, "incremental compression did not converge");
+
+ let oldest = terminal.read_text_screen((0, 0), (79, 0), false).unwrap();
+ assert!(oldest.starts_with("00001 "));
+ let last_row = terminal.total_rows().unwrap() as u32 - 1;
+ let newest = terminal
+ .read_text_screen((0, last_row - 1), (79, last_row), false)
+ .unwrap();
+ assert!(newest.contains("10000"));
+ }
+
+ #[test]
fn kitty_image_fingerprint_covers_full_payload() {
let mut data = vec![1u8; 4096 * 4];
let original =