aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/pane.rs27
-rw-r--r--src/server/headless.rs48
-rw-r--r--src/ui.rs19
-rw-r--r--src/ui/panes.rs42
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();
diff --git a/src/ui.rs b/src/ui.rs
index 874cc65e..83a9d987 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -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 {