aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
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/pane.rs
parent94f6d9c0d9bb9cf9ffae99d8bbfb09e9bf2fc9e0 (diff)
perf: compress idle terminal scrollback (#3556)
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs405
1 files changed, 378 insertions, 27 deletions
diff --git a/src/pane.rs b/src/pane.rs
index c71665f5..262fbe51 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -3,7 +3,7 @@ use std::io;
use std::path::Path;
use std::sync::{
atomic::{AtomicBool, AtomicU16, AtomicU32, AtomicU64, Ordering},
- Arc, Mutex,
+ Arc, Mutex, OnceLock,
};
use bytes::Bytes;
@@ -44,8 +44,8 @@ use self::agent_detection::{
pub use self::terminal::InputState;
use self::terminal::{GhosttyPaneTerminal, PaneTerminal};
pub(crate) use self::terminal::{
- TerminalDirtyPatch, TerminalDirtyPatchOutcome, TerminalReadSnapshot, TerminalSearchDirection,
- TerminalSearchWindow, TerminalTextPoint, TerminalWordMotion,
+ TerminalCompressionStep, TerminalDirtyPatch, TerminalDirtyPatchOutcome, TerminalReadSnapshot,
+ TerminalSearchDirection, TerminalSearchWindow, TerminalTextPoint, TerminalWordMotion,
};
pub use self::{
state::PaneState,
@@ -53,9 +53,32 @@ pub use self::{
};
const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::from_secs(1);
+const TERMINAL_COMPRESSION_IDLE: std::time::Duration = std::time::Duration::from_millis(250);
+const TERMINAL_COMPRESSION_STEP: std::time::Duration = std::time::Duration::from_millis(1);
const PANE_TERM: &str = "xterm-256color";
const PANE_COLORTERM: &str = "truecolor";
+fn terminal_compression_permits() -> Arc<tokio::sync::Semaphore> {
+ static PERMITS: OnceLock<Arc<tokio::sync::Semaphore>> = OnceLock::new();
+ PERMITS
+ .get_or_init(|| Arc::new(tokio::sync::Semaphore::new(4)))
+ .clone()
+}
+
+fn spawn_blocking_with_compression_permit<T, F>(
+ permit: tokio::sync::OwnedSemaphorePermit,
+ operation: F,
+) -> tokio::task::JoinHandle<T>
+where
+ T: Send + 'static,
+ F: FnOnce() -> T + Send + 'static,
+{
+ tokio::task::spawn_blocking(move || {
+ let _permit = permit;
+ operation()
+ })
+}
+
fn apply_pane_terminal_env(cmd: &mut CommandBuilder) {
// Each pane is rendered by herdr's own terminal layer, not the outer terminal
// that launched the app. Advertising the inherited TERM leaks the host terminal
@@ -1026,8 +1049,190 @@ impl AgentDetectionPresence {
// PaneRuntime — PTY, parser, channels, background tasks
// ---------------------------------------------------------------------------
+#[derive(Clone)]
+struct TerminalCompressionWake {
+ notify: Arc<Notify>,
+ generation: Arc<AtomicU64>,
+}
+
+impl TerminalCompressionWake {
+ fn wake(&self) {
+ self.generation.fetch_add(1, Ordering::Release);
+ self.notify.notify_one();
+ }
+}
+
+/// Drives libghostty-vt's caller-owned compression after terminal activity settles.
+struct TerminalCompressionTask {
+ wake: TerminalCompressionWake,
+ #[cfg(test)]
+ completed_passes: Arc<AtomicU64>,
+ handle: tokio::task::AbortHandle,
+}
+
+impl Drop for TerminalCompressionTask {
+ fn drop(&mut self) {
+ self.handle.abort();
+ }
+}
+
+impl TerminalCompressionTask {
+ fn spawn(pane_id: PaneId, terminal: Arc<PaneTerminal>) -> Self {
+ let wake = TerminalCompressionWake {
+ notify: Arc::new(Notify::new()),
+ generation: Arc::new(AtomicU64::new(0)),
+ };
+ let task_notify = wake.notify.clone();
+ let task_generation = wake.generation.clone();
+ #[cfg(test)]
+ let completed_passes = Arc::new(AtomicU64::new(0));
+ #[cfg(test)]
+ let task_completed_passes = completed_passes.clone();
+ let handle = tokio::spawn(async move {
+ run_terminal_compression_task(
+ pane_id,
+ terminal,
+ task_notify,
+ task_generation,
+ #[cfg(test)]
+ task_completed_passes,
+ )
+ .await;
+ })
+ .abort_handle();
+ Self {
+ wake,
+ #[cfg(test)]
+ completed_passes,
+ handle,
+ }
+ }
+
+ fn wake(&self) {
+ self.wake.wake();
+ }
+
+ fn notifier(&self) -> TerminalCompressionWake {
+ self.wake.clone()
+ }
+
+ fn abort(&self) {
+ self.handle.abort();
+ }
+
+ #[cfg(test)]
+ fn completed_passes(&self) -> u64 {
+ self.completed_passes.load(Ordering::Acquire)
+ }
+}
+
+async fn run_terminal_compression_task(
+ pane_id: PaneId,
+ terminal: Arc<PaneTerminal>,
+ notify: Arc<Notify>,
+ generation: Arc<AtomicU64>,
+ #[cfg(test)] completed_passes: Arc<AtomicU64>,
+) {
+ let mut observed_generation = generation.load(Ordering::Acquire);
+ let mut activity = loop {
+ match terminal.try_compression_activity() {
+ Ok(Some(activity)) => break activity,
+ Ok(None) => tokio::time::sleep(TERMINAL_COMPRESSION_IDLE).await,
+ Err(err) => {
+ warn!(pane = pane_id.raw(), err = %err, "failed to read terminal compression activity");
+ return;
+ }
+ }
+ };
+
+ 'schedule: loop {
+ loop {
+ tokio::time::sleep(TERMINAL_COMPRESSION_IDLE).await;
+ let current = match terminal.try_compression_activity() {
+ Ok(Some(current)) => current,
+ Ok(None) => continue,
+ Err(err) => {
+ warn!(pane = pane_id.raw(), err = %err, "failed to read terminal compression activity");
+ return;
+ }
+ };
+ let current_generation = generation.load(Ordering::Acquire);
+ if activity == current && observed_generation == current_generation {
+ break;
+ }
+ activity = current;
+ observed_generation = current_generation;
+ }
+
+ loop {
+ let current_generation = generation.load(Ordering::Acquire);
+ if observed_generation != current_generation {
+ observed_generation = current_generation;
+ continue 'schedule;
+ }
+
+ let permit = match terminal_compression_permits().acquire_owned().await {
+ Ok(permit) => permit,
+ Err(_) => return,
+ };
+ let current_generation = generation.load(Ordering::Acquire);
+ if observed_generation != current_generation {
+ observed_generation = current_generation;
+ continue 'schedule;
+ }
+
+ let terminal_for_step = terminal.clone();
+ let step = spawn_blocking_with_compression_permit(permit, move || {
+ terminal_for_step.try_compress_incremental_if_activity(activity)
+ })
+ .await;
+ let step = match step {
+ Ok(Ok(step)) => step,
+ Ok(Err(err)) => {
+ warn!(pane = pane_id.raw(), err = %err, "failed to compress terminal scrollback");
+ return;
+ }
+ Err(err) => {
+ warn!(pane = pane_id.raw(), err = %err, "terminal compression worker failed");
+ return;
+ }
+ };
+
+ match step {
+ TerminalCompressionStep::Busy => continue 'schedule,
+ TerminalCompressionStep::ActivityChanged(current) => {
+ activity = current;
+ observed_generation = generation.load(Ordering::Acquire);
+ continue 'schedule;
+ }
+ TerminalCompressionStep::Compressed(
+ crate::ghostty::TerminalCompressionResult::Unsupported,
+ ) => return,
+ TerminalCompressionStep::Compressed(
+ crate::ghostty::TerminalCompressionResult::Pending,
+ ) => tokio::time::sleep(TERMINAL_COMPRESSION_STEP).await,
+ TerminalCompressionStep::Compressed(
+ crate::ghostty::TerminalCompressionResult::Complete,
+ ) => {
+ #[cfg(test)]
+ completed_passes.fetch_add(1, Ordering::Release);
+ loop {
+ notify.notified().await;
+ let current_generation = generation.load(Ordering::Acquire);
+ if observed_generation != current_generation {
+ observed_generation = current_generation;
+ continue 'schedule;
+ }
+ }
+ }
+ }
+ }
+ }
+}
+
/// PTY runtime for a pane. Owns the terminal, I/O channels, and background tasks.
-/// Dropping this shuts down all background tasks and closes the PTY.
+/// Dropping this aborts async tasks and closes the PTY. An already-running bounded
+/// compression step may finish before releasing its terminal reference.
pub struct PaneRuntime {
pane_id: PaneId,
terminal: Arc<PaneTerminal>,
@@ -1045,6 +1250,7 @@ pub struct PaneRuntime {
pending_release: Arc<Mutex<Option<PendingAgentRelease>>>,
preserve_processes_on_drop: bool,
// Task handles for deterministic shutdown
+ compression: TerminalCompressionTask,
detect_handle: Option<tokio::task::AbortHandle>,
}
@@ -1223,6 +1429,7 @@ impl Drop for PaneRuntime {
if let Some(handle) = &self.detect_handle {
handle.abort();
}
+ self.compression.abort();
self.io.shutdown();
if !self.preserve_processes_on_drop {
shutdown_pane_processes(
@@ -1589,6 +1796,7 @@ impl PaneRuntime {
if let Some(handle) = self.detect_handle.take() {
handle.abort();
}
+ self.compression.abort();
self.io.shutdown();
shutdown_pane_processes(
self.pane_id,
@@ -1615,6 +1823,7 @@ impl PaneRuntime {
if let Some(handle) = self.detect_handle.take() {
handle.abort();
}
+ self.compression.abort();
self.preserve_processes_on_drop = true;
}
@@ -1905,6 +2114,7 @@ impl PaneRuntime {
pane_terminal.seed_history_ansi(ansi);
}
let terminal = Arc::new(PaneTerminal::new(pane_terminal));
+ let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone());
let child_pid = Arc::new(AtomicU32::new(child_pid));
let reported_cwd = Arc::new(Mutex::new(None));
let kitty_keyboard_flags = Arc::new(AtomicU16::new(keyboard_protocol_flags));
@@ -1923,6 +2133,7 @@ impl PaneRuntime {
let child_pid = child_pid.clone();
let read_events = events.clone();
let reported_cwd = reported_cwd.clone();
+ let compression_wake = compression.notifier();
let rt = tokio::runtime::Handle::current();
let delay_rt = rt.clone();
let on_read = Box::new(move |bytes: &[u8]| {
@@ -1936,6 +2147,7 @@ impl PaneRuntime {
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
content_seq.fetch_add(1, Ordering::Release);
drop(_content_write_guard);
+ compression_wake.wake();
publish_terminal_bells(pane_id, result.terminal_bells, &read_events);
observe_detection_content_change(bytes, &detection_content_seq);
let title_requested =
@@ -2010,6 +2222,7 @@ impl PaneRuntime {
detect_reset_notify,
pending_release,
preserve_processes_on_drop: true,
+ compression,
detect_handle: Some(detect_handle),
})
}
@@ -2051,6 +2264,7 @@ impl PaneRuntime {
pane_terminal.seed_history_ansi(ansi);
}
let terminal = Arc::new(PaneTerminal::new(pane_terminal));
+ let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone());
let kitty_keyboard_flags = Arc::new(AtomicU16::new(0));
let content_write_lock = Arc::new(Mutex::new(()));
@@ -2101,6 +2315,7 @@ impl PaneRuntime {
let child_pid = child_pid.clone();
let events = events.clone();
let reported_cwd = reported_cwd.clone();
+ let compression_wake = compression.notifier();
let rt = tokio::runtime::Handle::current();
let on_read = Box::new(move |bytes: &[u8]| {
let _content_write_guard = match content_write_lock.lock() {
@@ -2113,6 +2328,7 @@ impl PaneRuntime {
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
content_seq.fetch_add(1, Ordering::Release);
drop(_content_write_guard);
+ compression_wake.wake();
publish_terminal_bells(pane_id, result.terminal_bells, &events);
if agent_detection == AgentDetection::Enabled {
observe_detection_content_change(bytes, &detection_content_seq);
@@ -2574,6 +2790,7 @@ impl PaneRuntime {
detect_reset_notify,
pending_release,
preserve_processes_on_drop: false,
+ compression,
detect_handle,
})
}
@@ -2634,6 +2851,7 @@ impl PaneRuntime {
.resize(rows, cols, cell_width_px, cell_height_px);
self.content_seq.fetch_add(1, Ordering::Release);
drop(_content_write_guard);
+ self.compression.wake();
mark_detection_content_changed(&self.detection_content_seq);
self.io.resize(
rows,
@@ -2654,21 +2872,25 @@ impl PaneRuntime {
/// Scroll up by N lines (into scrollback history).
pub fn scroll_up(&self, lines: usize) {
self.terminal.scroll_up(lines);
+ self.compression.wake();
}
/// Scroll down by N lines (toward live output).
pub fn scroll_down(&self, lines: usize) {
self.terminal.scroll_down(lines);
+ self.compression.wake();
}
/// Reset scroll to live view (offset = 0).
pub fn scroll_reset(&self) {
self.terminal.scroll_reset();
+ self.compression.wake();
}
/// Set scrollback offset measured from the live bottom of the terminal.
pub fn set_scroll_offset_from_bottom(&self, lines: usize) {
self.terminal.set_scroll_offset_from_bottom(lines);
+ self.compression.wake();
}
pub fn scroll_metrics(&self) -> Option<ScrollMetrics> {
@@ -2687,8 +2909,16 @@ impl PaneRuntime {
)>,
limit: usize,
) -> crate::pane::TerminalSearchWindow {
- self.terminal
- .search_text_window(query, case_sensitive, direction, cursor, previous, limit)
+ let result = self.terminal.search_text_window(
+ query,
+ case_sensitive,
+ direction,
+ cursor,
+ previous,
+ limit,
+ );
+ self.compression.wake();
+ result
}
pub(crate) fn word_motion_target(
@@ -2697,7 +2927,9 @@ impl PaneRuntime {
col: u16,
motion: crate::pane::TerminalWordMotion,
) -> Option<crate::pane::TerminalTextPoint> {
- self.terminal.word_motion_target(row, col, motion)
+ let result = self.terminal.word_motion_target(row, col, motion);
+ self.compression.wake();
+ result
}
pub(crate) fn terminal_dimensions(&self) -> Option<(u16, u16)> {
@@ -2709,7 +2941,9 @@ impl PaneRuntime {
row: u32,
direction: i8,
) -> Option<crate::pane::TerminalTextPoint> {
- self.terminal.paragraph_motion_target(row, direction)
+ let result = self.terminal.paragraph_motion_target(row, direction);
+ self.compression.wake();
+ result
}
#[cfg(any(unix, test))]
@@ -2786,23 +3020,33 @@ impl PaneRuntime {
}
pub(crate) fn recent_text_snapshot(&self, lines: usize) -> TerminalReadSnapshot {
- self.terminal.recent_text_snapshot(lines)
+ let result = self.terminal.recent_text_snapshot(lines);
+ self.compression.wake();
+ result
}
pub(crate) fn recent_ansi_snapshot(&self, lines: usize) -> TerminalReadSnapshot {
- self.terminal.recent_ansi_snapshot(lines)
+ let result = self.terminal.recent_ansi_snapshot(lines);
+ self.compression.wake();
+ result
}
pub(crate) fn recent_unwrapped_text_snapshot(&self, lines: usize) -> TerminalReadSnapshot {
- self.terminal.recent_unwrapped_text_snapshot(lines)
+ let result = self.terminal.recent_unwrapped_text_snapshot(lines);
+ self.compression.wake();
+ result
}
pub fn recent_unwrapped_ansi(&self, lines: usize) -> String {
- self.terminal.recent_unwrapped_ansi(lines)
+ let result = self.terminal.recent_unwrapped_ansi(lines);
+ self.compression.wake();
+ result
}
pub(crate) fn recent_unwrapped_ansi_snapshot(&self, lines: usize) -> TerminalReadSnapshot {
- self.terminal.recent_unwrapped_ansi_snapshot(lines)
+ let result = self.terminal.recent_unwrapped_ansi_snapshot(lines);
+ self.compression.wake();
+ result
}
pub fn snapshot_history(&self) -> Option<String> {
@@ -2811,7 +3055,9 @@ impl PaneRuntime {
}
pub fn extract_selection(&self, selection: &crate::selection::Selection) -> Option<String> {
- self.terminal.extract_selection(selection)
+ let result = self.terminal.extract_selection(selection);
+ self.compression.wake();
+ result
}
pub fn render(&self, frame: &mut Frame, area: Rect, show_cursor: bool) {
@@ -2909,7 +3155,9 @@ impl PaneRuntime {
u16,
Vec<crate::ghostty::ScreenTextRow>,
)> {
- self.terminal.screen_text_snapshot()
+ let result = self.terminal.screen_text_snapshot();
+ self.compression.wake();
+ result
}
pub fn encode_mouse_button(
@@ -3060,6 +3308,7 @@ impl PaneRuntime {
let (tx, _rx) = mpsc::channel(1);
let _ = self.terminal.process_pty_bytes(self.pane_id, 0, bytes, &tx);
self.content_seq.fetch_add(1, Ordering::Release);
+ self.compression.wake();
}
pub(crate) fn test_with_scrollback_bytes(
@@ -3083,13 +3332,16 @@ impl PaneRuntime {
let mut terminal =
crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes).unwrap();
terminal.write(bytes);
+ let pane_id = PaneId::from_raw(0);
+ let terminal = Arc::new(PaneTerminal::new(
+ GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
+ ));
+ let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone());
(
Self {
- pane_id: PaneId::from_raw(0),
- terminal: Arc::new(PaneTerminal::new(
- GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
- )),
+ pane_id,
+ terminal,
io: PaneRuntimeIo::TestChannel {
sender: tx,
resize_tx,
@@ -3106,6 +3358,7 @@ impl PaneRuntime {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
+ compression,
detect_handle: Some(tokio::spawn(async {}).abort_handle()),
},
rx,
@@ -3648,6 +3901,96 @@ mod tests {
}
#[tokio::test]
+ async fn compression_permit_survives_an_aborted_async_waiter() {
+ let semaphore = Arc::new(tokio::sync::Semaphore::new(1));
+ let permit = semaphore.clone().acquire_owned().await.unwrap();
+ let (started_tx, started_rx) = tokio::sync::oneshot::channel();
+ let (release_tx, release_rx) = std::sync::mpsc::channel();
+ let handle = spawn_blocking_with_compression_permit(permit, move || {
+ let _ = started_tx.send(());
+ let _ = release_rx.recv();
+ });
+ started_rx.await.unwrap();
+
+ handle.abort();
+ assert!(semaphore.clone().try_acquire_owned().is_err());
+
+ release_tx.send(()).unwrap();
+ handle.await.unwrap();
+ assert!(semaphore.try_acquire_owned().is_ok());
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test]
+ async fn compression_task_rechecks_history_after_a_read() {
+ let suffix = "x".repeat(66);
+ let history = (1..=2_000)
+ .map(|line| format!("{line:05} {suffix}\r\n"))
+ .collect::<String>();
+ let runtime =
+ PaneRuntime::test_with_scrollback_bytes(80, 24, 20_000_000, history.as_bytes());
+
+ tokio::time::timeout(std::time::Duration::from_secs(5), async {
+ while runtime.compression.completed_passes() == 0 {
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
+ }
+ })
+ .await
+ .unwrap();
+ let completed_before_read = runtime.compression.completed_passes();
+
+ let snapshot = runtime.recent_unwrapped_text_snapshot(usize::MAX);
+ assert!(snapshot.text.contains("00001 "));
+ assert!(snapshot.text.contains("02000 "));
+
+ tokio::time::timeout(std::time::Duration::from_secs(5), async {
+ while runtime.compression.completed_passes() == completed_before_read {
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
+ }
+ })
+ .await
+ .unwrap();
+ }
+
+ #[cfg(any(target_os = "linux", target_os = "macos"))]
+ #[tokio::test]
+ async fn compressed_scrollback_survives_shrink_and_grow_resize() {
+ let suffix = "x".repeat(66);
+ let history = (1..=2_000)
+ .map(|line| format!("{line:05} {suffix}\r\n"))
+ .collect::<String>();
+ let runtime =
+ PaneRuntime::test_with_scrollback_bytes(80, 45, 20_000_000, history.as_bytes());
+
+ tokio::time::timeout(std::time::Duration::from_secs(5), async {
+ while runtime.compression.completed_passes() == 0 {
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
+ }
+ })
+ .await
+ .unwrap();
+
+ runtime.resize(21, 80, 0, 0);
+ let completed_after_shrink = runtime.compression.completed_passes();
+ tokio::time::timeout(std::time::Duration::from_secs(5), async {
+ while runtime.compression.completed_passes() == completed_after_shrink {
+ tokio::time::sleep(std::time::Duration::from_millis(10)).await;
+ }
+ })
+ .await
+ .unwrap();
+
+ runtime.resize(45, 80, 0, 0);
+
+ assert_eq!(runtime.current_size(), (45, 80));
+ assert_eq!(runtime.terminal_dimensions(), Some((80, 45)));
+ assert_eq!(runtime.scroll_metrics().unwrap().viewport_rows, 45);
+ let snapshot = runtime.recent_unwrapped_text_snapshot(usize::MAX);
+ assert!(snapshot.text.contains("00001 "));
+ assert!(snapshot.text.contains("02000 "));
+ }
+
+ #[tokio::test]
async fn focus_events_are_forwarded_when_enabled() {
let (tx, mut rx) = mpsc::channel(4);
let (resize_tx, _resize_rx) = watch::channel((80, 24, 0, 0));
@@ -3655,11 +3998,14 @@ mod tests {
terminal
.mode_set(crate::ghostty::MODE_FOCUS_EVENT, true)
.unwrap();
+ let pane_id = PaneId::from_raw(0);
+ let terminal = Arc::new(PaneTerminal::new(
+ GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
+ ));
+ let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone());
let runtime = PaneRuntime {
- pane_id: PaneId::from_raw(0),
- terminal: Arc::new(PaneTerminal::new(
- GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
- )),
+ pane_id,
+ terminal,
io: PaneRuntimeIo::TestChannel {
sender: tx,
resize_tx,
@@ -3676,6 +4022,7 @@ mod tests {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
+ compression,
detect_handle: Some(tokio::spawn(async {}).abort_handle()),
};
@@ -3688,11 +4035,14 @@ mod tests {
let (tx, mut rx) = mpsc::channel(4);
let (resize_tx, _resize_rx) = watch::channel((80, 24, 0, 0));
let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap();
+ let pane_id = PaneId::from_raw(0);
+ let terminal = Arc::new(PaneTerminal::new(
+ GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
+ ));
+ let compression = TerminalCompressionTask::spawn(pane_id, terminal.clone());
let runtime = PaneRuntime {
- pane_id: PaneId::from_raw(0),
- terminal: Arc::new(PaneTerminal::new(
- GhosttyPaneTerminal::new(terminal, tx.clone()).unwrap(),
- )),
+ pane_id,
+ terminal,
io: PaneRuntimeIo::TestChannel {
sender: tx,
resize_tx,
@@ -3709,6 +4059,7 @@ mod tests {
detect_reset_notify: Arc::new(Notify::new()),
pending_release: Arc::new(Mutex::new(None)),
preserve_processes_on_drop: true,
+ compression,
detect_handle: Some(tokio::spawn(async {}).abort_handle()),
};