diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/pane.rs | 27 | ||||
| -rw-r--r-- | src/server/headless.rs | 48 | ||||
| -rw-r--r-- | src/ui.rs | 19 | ||||
| -rw-r--r-- | src/ui/panes.rs | 42 |
4 files changed, 123 insertions, 13 deletions
diff --git a/src/pane.rs b/src/pane.rs index 7d4ec729..bf669ecf 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -8,7 +8,7 @@ use std::sync::{ use bytes::Bytes; use portable_pty::{native_pty_system, CommandBuilder, PtySize}; use ratatui::{layout::Rect, Frame}; -use tokio::sync::{mpsc, Notify}; +use tokio::sync::{mpsc, watch, Notify}; use tracing::{debug, error, info, warn}; use crate::detect::{Agent, AgentState}; @@ -162,7 +162,7 @@ impl AgentDetectionPresence { pub struct PaneRuntime { terminal: Arc<PaneTerminal>, sender: mpsc::Sender<Bytes>, - resize_tx: mpsc::Sender<(u16, u16)>, + resize_tx: watch::Sender<(u16, u16)>, current_size: Cell<(u16, u16)>, child_pid: Arc<AtomicU32>, kitty_keyboard_flags: Arc<AtomicU16>, @@ -663,12 +663,18 @@ impl PaneRuntime { } // --- Resize task --- - let (resize_tx, mut resize_rx) = mpsc::channel::<(u16, u16)>(4); + let (resize_tx, mut resize_rx) = watch::channel::<(u16, u16)>((rows, cols)); { let master = pair.master; tokio::task::spawn_blocking(move || { let rt = tokio::runtime::Handle::current(); - while let Some((rows, cols)) = rt.block_on(resize_rx.recv()) { + let mut last_size = (rows, cols); + while rt.block_on(resize_rx.changed()).is_ok() { + let (rows, cols) = *resize_rx.borrow_and_update(); + if (rows, cols) == last_size { + continue; + } + last_size = (rows, cols); if let Err(e) = master.resize(PtySize { rows, cols, @@ -704,6 +710,11 @@ impl PaneRuntime { self.detect_reset_notify.notify_one(); } + #[cfg(test)] + pub(crate) fn current_size(&self) -> (u16, u16) { + self.current_size.get() + } + /// Resize if the dimensions actually changed. pub fn resize(&self, rows: u16, cols: u16) { let rows = rows.max(2); @@ -713,7 +724,7 @@ impl PaneRuntime { } self.current_size.set((rows, cols)); self.terminal.resize(rows, cols); - let _ = self.resize_tx.try_send((rows, cols)); + let _ = self.resize_tx.send((rows, cols)); } /// Scroll up by N lines (into scrollback history). @@ -937,7 +948,7 @@ impl PaneRuntime { bytes: &[u8], ) -> (Self, mpsc::Receiver<Bytes>) { let (tx, rx) = mpsc::channel(4); - let (resize_tx, _resize_rx) = mpsc::channel(1); + let (resize_tx, _resize_rx) = watch::channel((rows, cols)); let mut terminal = crate::ghostty::Terminal::new(cols, rows, scrollback_limit_bytes).unwrap(); terminal.write(bytes); @@ -1020,7 +1031,7 @@ mod tests { #[tokio::test] async fn focus_events_are_forwarded_when_enabled() { let (tx, mut rx) = mpsc::channel(4); - let (resize_tx, _resize_rx) = mpsc::channel(1); + let (resize_tx, _resize_rx) = watch::channel((80, 24)); let mut terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); terminal .mode_set(crate::ghostty::MODE_FOCUS_EVENT, true) @@ -1046,7 +1057,7 @@ mod tests { #[tokio::test] async fn focus_events_are_suppressed_when_disabled() { let (tx, mut rx) = mpsc::channel(4); - let (resize_tx, _resize_rx) = mpsc::channel(1); + let (resize_tx, _resize_rx) = watch::channel((80, 24)); let terminal = crate::ghostty::Terminal::new(80, 24, 0).unwrap(); let runtime = PaneRuntime { terminal: Arc::new(PaneTerminal::new( diff --git a/src/server/headless.rs b/src/server/headless.rs index bd1bf55e..337bbec7 100644 --- a/src/server/headless.rs +++ b/src/server/headless.rs @@ -2190,6 +2190,54 @@ mod tests { assert_eq!((phone_frame.width, phone_frame.height), (80, 24)); } + #[tokio::test] + async fn resize_shared_runtime_resizes_background_tabs() { + let mut server = test_headless_server(); + let mut workspace = crate::workspace::Workspace::test_new("test"); + let background_tab = workspace.test_add_tab(Some("background")); + let active_pane = workspace.tabs[0].root_pane; + let background_pane = workspace.tabs[background_tab].root_pane; + workspace.tabs[0].runtimes.insert( + active_pane, + crate::pane::PaneRuntime::test_with_screen_bytes(80, 24, b""), + ); + workspace.tabs[background_tab].runtimes.insert( + background_pane, + crate::pane::PaneRuntime::test_with_screen_bytes(80, 24, b""), + ); + server.app.state.workspaces = vec![workspace]; + server.app.state.active = Some(0); + server.app.state.selected = 0; + server.app.state.mode = crate::app::Mode::Terminal; + + server.clients.insert( + 1, + ClientConnection::new( + (120, 40), + crate::terminal_theme::TerminalTheme::default(), + None, + 1, + RenderEncoding::SemanticFrame, + None, + ), + ); + server.foreground_client_id = Some(1); + server.sync_foreground_client_state(); + server.resize_shared_runtime_to_effective_size(); + + let terminal_area = server.app.state.view.terminal_area; + let expected = (terminal_area.height, terminal_area.width.saturating_sub(1)); + assert_eq!( + server.app.state.workspaces[0].tabs[0].runtimes[&active_pane].current_size(), + expected + ); + assert_eq!( + server.app.state.workspaces[0].tabs[background_tab].runtimes[&background_pane] + .current_size(), + expected + ); + } + #[test] fn render_and_stream_sends_terminal_frame_for_terminal_ansi_client() { let mut server = test_headless_server(); @@ -32,7 +32,7 @@ use self::mobile::{ }; pub(crate) use self::onboarding::onboarding_welcome_continue_rect; use self::onboarding::render_onboarding_overlay; -use self::panes::{compute_pane_infos, render_panes}; +use self::panes::{compute_pane_infos, render_panes, resize_tab_panes}; use self::release_notes::render_release_notes_overlay; pub(crate) use self::release_notes::{ release_notes_close_button_rect, release_notes_display_lines, release_notes_sections, @@ -97,6 +97,17 @@ pub(crate) fn compute_view_without_resizing_panes(app: &mut AppState, area: Rect compute_view_internal(app, area, false); } +fn resize_background_tab_panes_to_terminal_area(app: &AppState, terminal_area: Rect) { + for (ws_idx, ws) in app.workspaces.iter().enumerate() { + for (tab_idx, tab) in ws.tabs.iter().enumerate() { + if app.active == Some(ws_idx) && tab_idx == ws.active_tab_index() { + continue; + } + resize_tab_panes(tab, terminal_area); + } + } +} + fn compute_view_internal(app: &mut AppState, area: Rect, resize_panes: bool) { if is_mobile_width(area) { compute_mobile_view(app, area, resize_panes); @@ -160,6 +171,9 @@ fn compute_view_internal(app: &mut AppState, area: Rect, resize_panes: bool) { .unwrap_or_default(); let pane_infos = compute_pane_infos(app, terminal_area, resize_panes); + if resize_panes { + resize_background_tab_panes_to_terminal_area(app, terminal_area); + } let toast_hit_area = app .toast @@ -208,6 +222,9 @@ fn compute_mobile_view(app: &mut AppState, area: Rect, resize_panes: bool) { .unwrap_or_default(); let pane_infos = compute_pane_infos(app, terminal_area, resize_panes); + if resize_panes { + resize_background_tab_panes_to_terminal_area(app, terminal_area); + } let header_hits = compute_mobile_header_hit_areas(app, header_rect); let toast_hit_area = app diff --git a/src/ui/panes.rs b/src/ui/panes.rs index c4ba0c54..20ac9165 100644 --- a/src/ui/panes.rs +++ b/src/ui/panes.rs @@ -13,17 +13,24 @@ use crate::app::{AppState, Mode}; use crate::layout::PaneInfo; use crate::pane::PaneRuntime; -fn stable_scrollbar_gutter(rt: &PaneRuntime, pane_inner: Rect) -> (Rect, Option<Rect>) { +fn stable_terminal_inner_rect(pane_inner: Rect) -> Rect { if pane_inner.width <= 4 { - return (pane_inner, None); + return pane_inner; } - let inner_rect = Rect::new( + Rect::new( pane_inner.x, pane_inner.y, pane_inner.width.saturating_sub(1), pane_inner.height, - ); + ) +} + +fn stable_scrollbar_gutter(rt: &PaneRuntime, pane_inner: Rect) -> (Rect, Option<Rect>) { + let inner_rect = stable_terminal_inner_rect(pane_inner); + if inner_rect == pane_inner { + return (inner_rect, None); + } let gutter = Rect::new( pane_inner.x + pane_inner.width.saturating_sub(1), pane_inner.y, @@ -38,6 +45,33 @@ fn stable_scrollbar_gutter(rt: &PaneRuntime, pane_inner: Rect) -> (Rect, Option< (inner_rect, scrollbar_rect) } +/// Resize every visible runtime in a tab to the geometry it would receive if the tab were selected. +pub(super) fn resize_tab_panes(tab: &crate::workspace::Tab, area: Rect) { + let multi_pane = tab.layout.pane_count() > 1; + + if tab.zoomed { + let focused_id = tab.layout.focused(); + if let Some(rt) = tab.runtimes.get(&focused_id) { + let inner_rect = stable_terminal_inner_rect(area); + rt.resize(inner_rect.height, inner_rect.width); + } + return; + } + + for info in tab.layout.panes(area) { + let pane_inner = if multi_pane { + Block::default().borders(Borders::ALL).inner(info.rect) + } else { + area + }; + + if let Some(rt) = tab.runtimes.get(&info.id) { + let inner_rect = stable_terminal_inner_rect(pane_inner); + rt.resize(inner_rect.height, inner_rect.width); + } + } +} + /// Compute pane layout info and optionally resize pane runtimes to match. pub(super) fn compute_pane_infos(app: &AppState, area: Rect, resize_panes: bool) -> Vec<PaneInfo> { let Some(ws_idx) = app.active else { |
