aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 23:29:11 +0300
committerOgulcan Celik <ogulcancelik@gmail.com>2026-07-28 23:29:11 +0300
commite27a4ea427c9ed6248c0ee80570e94ed7b1928bd (patch)
tree0cae21a2b3939293be129aaaecb23641f68bab54 /src
parente16d7d8c07a20f5ee0b4111808680bbcfd7df9ac (diff)
perf: skip rendering hidden pty updates
Diffstat (limited to 'src')
-rw-r--r--src/app/actions.rs2
-rw-r--r--src/app/api.rs11
-rw-r--r--src/app/mod.rs17
-rw-r--r--src/app/popup.rs3
-rw-r--r--src/app/theme_sync.rs6
-rw-r--r--src/app/worktrees.rs9
-rw-r--r--src/main.rs1
-rw-r--r--src/pane.rs23
-rw-r--r--src/persist/restore.rs28
-rw-r--r--src/render_signal.rs91
-rw-r--r--src/server/headless.rs352
-rw-r--r--src/server/notifications.rs2
-rw-r--r--src/terminal/id.rs4
-rw-r--r--src/terminal/runtime.rs14
-rw-r--r--src/ui/dialogs.rs2
-rw-r--r--src/ui/mobile.rs2
-rw-r--r--src/ui/sidebar.rs2
-rw-r--r--src/workspace.rs23
-rw-r--r--src/workspace/tab.rs12
19 files changed, 509 insertions, 95 deletions
diff --git a/src/app/actions.rs b/src/app/actions.rs
index c92a91e5..9ef4af68 100644
--- a/src/app/actions.rs
+++ b/src/app/actions.rs
@@ -3708,7 +3708,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
diff --git a/src/app/api.rs b/src/app/api.rs
index c46be676..76087e2d 100644
--- a/src/app/api.rs
+++ b/src/app/api.rs
@@ -1,4 +1,3 @@
-use std::sync::atomic::Ordering;
use std::time::{Duration, Instant};
mod agent_view;
@@ -91,7 +90,7 @@ impl App {
.state
.apply_workspace_git_statuses(&self.terminal_runtimes, results);
if changed {
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
changed
@@ -194,7 +193,7 @@ impl App {
&& self.respawn_shell_for_launch_pane(*pane_id)
{
self.overlay_panes.remove(pane_id);
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
return;
}
@@ -293,7 +292,7 @@ impl App {
self.sync_full_lifecycle_authority_detection_pauses();
if terminal_cwd_reported {
self.request_git_identity_refresh(Instant::now());
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
for update in &pane_updates {
@@ -1717,7 +1716,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
@@ -1809,7 +1808,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
diff --git a/src/app/mod.rs b/src/app/mod.rs
index c8c8113a..1733e234 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -28,7 +28,6 @@ mod worktrees;
use std::collections::{HashMap, HashSet};
use std::future::pending;
use std::io::{self, Write};
-use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::time::{Duration, Instant};
@@ -140,7 +139,7 @@ pub struct App {
HashMap<(InputSourceId, crossterm::event::KeyCode), PressedTerminalKey>,
pub(crate) suppressed_repeat_keys: HashSet<(InputSourceId, crossterm::event::KeyCode)>,
pub render_notify: Arc<Notify>,
- pub render_dirty: Arc<AtomicBool>,
+ pub(crate) render_dirty: Arc<crate::render_signal::RenderSignal>,
pub(crate) full_redraw_pending: bool,
pub(crate) overlay_panes: HashMap<crate::layout::PaneId, OverlayPaneState>,
pub(crate) local_terminal_notifications: bool,
@@ -381,7 +380,7 @@ impl App {
crate::kitty_graphics::set_enabled(config.experimental.kitty_graphics);
let (event_tx, event_rx) = mpsc::channel::<AppEvent>(APP_EVENT_CHANNEL_CAPACITY);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(crate::render_signal::RenderSignal::new());
// Try to restore previous session
let mut restored_terminals = std::collections::HashMap::new();
@@ -919,7 +918,7 @@ impl App {
while !self.state.should_quit {
self.reap_finished_custom_commands();
- if self.render_dirty.load(Ordering::Acquire) {
+ if self.render_dirty.is_pending() {
needs_render = true;
}
let terminal_title_changed = self.sync_terminal_titles();
@@ -1044,7 +1043,7 @@ impl App {
self.sync_host_keyboard_report_all(&mut host_keyboard_report_all_active)?;
if needs_render && self.can_render_now(now) {
- self.render_dirty.swap(false, Ordering::AcqRel);
+ let _ = self.render_dirty.take();
let _sync_output = SyncOutputGuard::begin()?;
let kitty_graphics_enabled = self.state.kitty_graphics_enabled;
if self.full_redraw_pending {
@@ -1094,7 +1093,7 @@ impl App {
}
self.sync_pending_agent_resume_deadline(now);
if self.start_pending_agent_resumes(self.pending_agent_resume_due(now)) {
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
self.last_render_at = Some(now);
@@ -1144,7 +1143,7 @@ impl App {
self.input_rx = None;
}
LoopEvent::RenderRequested => {
- if self.render_dirty.load(Ordering::Acquire) {
+ if self.render_dirty.is_pending() {
needs_render = true;
}
}
@@ -2165,7 +2164,7 @@ mod tests {
fn git_status_event_marks_render_dirty_when_status_changes() {
let mut app = test_app();
app.state.workspaces.push(Workspace::test_new("one"));
- app.render_dirty.store(false, Ordering::Release);
+ let _ = app.render_dirty.take();
let workspace_id = app.state.workspaces[0].id.clone();
let resolved_identity_cwd = app.state.workspaces[0].resolved_identity_cwd().unwrap();
@@ -2183,7 +2182,7 @@ mod tests {
cache_updates: Vec::new(),
});
- assert!(app.render_dirty.load(Ordering::Acquire));
+ assert!(app.render_dirty.is_pending());
}
#[test]
diff --git a/src/app/popup.rs b/src/app/popup.rs
index 3aeef35c..3356ab14 100644
--- a/src/app/popup.rs
+++ b/src/app/popup.rs
@@ -34,8 +34,7 @@ impl App {
} else {
Mode::Navigate
};
- self.render_dirty
- .store(true, std::sync::atomic::Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
true
}
diff --git a/src/app/theme_sync.rs b/src/app/theme_sync.rs
index c41ca24c..e4284f69 100644
--- a/src/app/theme_sync.rs
+++ b/src/app/theme_sync.rs
@@ -1,5 +1,3 @@
-use std::sync::atomic::Ordering;
-
use super::App;
impl App {
@@ -92,7 +90,7 @@ impl App {
}
self.state.theme_name = theme_name;
self.state.palette = palette;
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
true
}
@@ -102,7 +100,7 @@ impl App {
runtime.apply_host_terminal_theme(self.state.host_terminal_theme);
}
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
}
diff --git a/src/app/worktrees.rs b/src/app/worktrees.rs
index c839c04a..23e17b4d 100644
--- a/src/app/worktrees.rs
+++ b/src/app/worktrees.rs
@@ -1,4 +1,3 @@
-use std::sync::atomic::Ordering;
use std::time::{SystemTime, UNIX_EPOCH};
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
@@ -864,14 +863,14 @@ impl App {
}
}
}
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
Err(message) => {
tracing::warn!(checkout_path = %create.checkout_path.display(), error = %message, "git worktree add failed");
create.creating = false;
create.error = Some(message);
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
}
@@ -942,7 +941,7 @@ impl App {
} else {
Mode::Navigate
};
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
Err(message) => {
@@ -956,7 +955,7 @@ impl App {
} else {
remove.error = Some(message);
}
- self.render_dirty.store(true, Ordering::Release);
+ self.render_dirty.request_generic();
self.render_notify.notify_one();
}
}
diff --git a/src/main.rs b/src/main.rs
index 65dc7c11..b4517e21 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -88,6 +88,7 @@ mod raw_input;
mod release_notes;
mod remote;
mod render_prof;
+mod render_signal;
mod selection;
mod server;
mod session;
diff --git a/src/pane.rs b/src/pane.rs
index a26dad6b..3bf94ec0 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -22,6 +22,7 @@ use crate::detect::{Agent, AgentState};
use crate::events::AppEvent;
use crate::layout::PaneId;
use crate::pty::actor::{PtyIoActor, PtyIoActorConfig, PtyIoActorHandle, PtyReadResult};
+use crate::render_signal::RenderSignal;
mod agent_detection;
mod cursor;
@@ -1603,7 +1604,7 @@ impl PaneRuntime {
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
Self::spawn_with_initial_history(
pane_id,
@@ -1635,7 +1636,7 @@ impl PaneRuntime {
initial_history_ansi: Option<&str>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
let windows_powershell_prompt_cwd_reporting =
uses_windows_powershell_pane_shell(shell_config);
@@ -1677,7 +1678,7 @@ impl PaneRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
let mut cmd = crate::platform::pane_custom_command_pty_builder(command);
cmd.cwd(cwd);
@@ -1713,7 +1714,7 @@ impl PaneRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
let Some((program, args)) = argv.split_first() else {
return Err(std::io::Error::new(
@@ -1751,7 +1752,7 @@ impl PaneRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
let crate::handoff_runtime::ImportedHandoffRuntime { master_fd, state } = import;
let crate::handoff_runtime::HandoffRuntimeState {
@@ -1819,7 +1820,7 @@ impl PaneRuntime {
let result =
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
observe_detection_content_change(bytes, &detection_content_seq);
- if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) {
+ if result.request_render && render_dirty.request_pty(pane_id) {
render_notify.notify_one();
}
if let Some(delay) = result.render_delay {
@@ -1827,7 +1828,7 @@ impl PaneRuntime {
let render_dirty = render_dirty.clone();
delay_rt.spawn(async move {
tokio::time::sleep(delay).await;
- if !render_dirty.swap(true, Ordering::AcqRel) {
+ if render_dirty.request_pty(pane_id) {
render_notify.notify_one();
}
});
@@ -1900,7 +1901,7 @@ impl PaneRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
cmd: CommandBuilder,
spawn_error_message: &'static str,
initial_state: SpawnInitialState<'_>,
@@ -1979,7 +1980,7 @@ impl PaneRuntime {
if agent_detection == AgentDetection::Enabled {
observe_detection_content_change(bytes, &detection_content_seq);
}
- if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) {
+ if result.request_render && render_dirty.request_pty(pane_id) {
render_notify.notify_one();
}
if let Some(delay) = result.render_delay {
@@ -1987,7 +1988,7 @@ impl PaneRuntime {
let render_dirty = render_dirty.clone();
rt.spawn(async move {
tokio::time::sleep(delay).await;
- if !render_dirty.swap(true, Ordering::AcqRel) {
+ if render_dirty.request_pty(pane_id) {
render_notify.notify_one();
}
});
@@ -2253,7 +2254,7 @@ impl PaneRuntime {
// Keep the terminal restore side effect separate from render notification state.
#[allow(clippy::collapsible_if)]
if pid > 0 && terminal.maybe_restore_host_terminal_theme(pane_id, pid) {
- if !render_dirty.swap(true, Ordering::AcqRel) {
+ if render_dirty.request_pty(pane_id) {
render_notify.notify_one();
}
}
diff --git a/src/persist/restore.rs b/src/persist/restore.rs
index a1b07710..99b9a64c 100644
--- a/src/persist/restore.rs
+++ b/src/persist/restore.rs
@@ -1,6 +1,5 @@
use std::collections::{HashMap, HashSet};
use std::path::PathBuf;
-use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ratatui::layout::Direction;
@@ -11,6 +10,7 @@ use crate::detect::AgentState;
use crate::events::AppEvent;
use crate::layout::{Node, PaneId, TileLayout};
use crate::pane::{PaneLaunchEnv, PaneState};
+use crate::render_signal::RenderSignal;
use crate::terminal::{TerminalId, TerminalRuntime, TerminalState};
use crate::workspace::Workspace;
@@ -40,7 +40,7 @@ struct RestoreRuntimeContext<'a> {
resume_agents_on_restore: bool,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
}
type RestoredSession = (
@@ -73,7 +73,7 @@ pub fn restore(
resume_agents_on_restore: bool,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> RestoredSession {
let mut imported_panes = HashMap::new();
restore_with_imports(
@@ -100,7 +100,7 @@ pub fn restore_handoff(
imports: &mut HashMap<u32, crate::handoff_runtime::ImportedHandoffRuntime>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<RestoredSession> {
restore_with_imports_strict(
snapshot,
@@ -196,7 +196,7 @@ fn restore_with_imports_strict(
imported_panes: &mut HashMap<u32, crate::handoff_runtime::ImportedHandoffRuntime>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<RestoredSession> {
let (restored, failed_imports) = restore_with_imports_and_failures(
snapshot,
@@ -236,7 +236,7 @@ fn restore_with_imports(
imported_panes: &mut HashMap<u32, crate::handoff_runtime::ImportedHandoffRuntime>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> RestoredSession {
restore_with_imports_and_failures(
snapshot,
@@ -265,7 +265,7 @@ fn restore_with_imports_and_failures(
imported_panes: &mut HashMap<u32, crate::handoff_runtime::ImportedHandoffRuntime>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> RestoreFailures<RestoredSession> {
let mut workspaces = Vec::new();
let mut terminals = HashMap::new();
@@ -1222,7 +1222,7 @@ mod tests {
false,
events,
Arc::new(Notify::new()),
- Arc::new(AtomicBool::new(false)),
+ Arc::new(RenderSignal::new()),
);
let terminal = terminals
@@ -1315,7 +1315,7 @@ mod tests {
false,
events,
Arc::new(Notify::new()),
- Arc::new(AtomicBool::new(false)),
+ Arc::new(RenderSignal::new()),
);
let workspace = workspaces.first().expect("workspace should restore");
@@ -1422,7 +1422,7 @@ mod tests {
false,
events,
Arc::new(Notify::new()),
- Arc::new(AtomicBool::new(false)),
+ Arc::new(RenderSignal::new()),
);
let workspace = workspaces.first().expect("workspace should restore");
@@ -1533,7 +1533,7 @@ mod tests {
true,
events,
Arc::new(Notify::new()),
- Arc::new(AtomicBool::new(false)),
+ Arc::new(RenderSignal::new()),
);
let terminal = terminals
@@ -1561,7 +1561,7 @@ mod tests {
&mut imports,
mpsc::channel(4).0,
Arc::new(Notify::new()),
- Arc::new(AtomicBool::new(false)),
+ Arc::new(RenderSignal::new()),
)
.expect("handoff restore should preserve pending native agent resume");
let handoff_terminal = handoff_terminals
@@ -1583,7 +1583,7 @@ mod tests {
let (snapshot, history) = snapshot_with_saved_pane_history();
let (events, _events_rx) = mpsc::channel(8);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(RenderSignal::new());
let (_workspaces, _terminals, runtimes) = restore(
&snapshot,
@@ -1621,7 +1621,7 @@ mod tests {
let (snapshot, _history) = snapshot_with_saved_pane_history();
let (events, _events_rx) = mpsc::channel(8);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(RenderSignal::new());
let (_workspaces, _terminals, runtimes) = restore(
&snapshot,
diff --git a/src/render_signal.rs b/src/render_signal.rs
new file mode 100644
index 00000000..d4544e5f
--- /dev/null
+++ b/src/render_signal.rs
@@ -0,0 +1,91 @@
+use std::collections::HashSet;
+use std::sync::atomic::{AtomicBool, Ordering};
+use std::sync::Mutex;
+
+use crate::layout::PaneId;
+
+#[derive(Debug, Default)]
+pub(crate) struct RenderRequest {
+ pub(crate) generic: bool,
+ pub(crate) pty_sources: HashSet<PaneId>,
+}
+
+/// Coalesces render requests while retaining enough origin information for the
+/// headless server to discard PTY-only updates hidden from every client.
+#[derive(Debug, Default)]
+pub(crate) struct RenderSignal {
+ pending: AtomicBool,
+ request: Mutex<RenderRequest>,
+}
+
+impl RenderSignal {
+ pub(crate) fn new() -> Self {
+ Self::default()
+ }
+
+ pub(crate) fn is_pending(&self) -> bool {
+ self.pending.load(Ordering::Acquire)
+ }
+
+ pub(crate) fn request_generic(&self) {
+ let mut request = self
+ .request
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ request.generic = true;
+ self.pending.store(true, Ordering::Release);
+ }
+
+ /// Returns true when this request transitions the signal from idle to pending.
+ pub(crate) fn request_pty(&self, pane_id: PaneId) -> bool {
+ let mut request = self
+ .request
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ request.pty_sources.insert(pane_id);
+ !self.pending.swap(true, Ordering::AcqRel)
+ }
+
+ pub(crate) fn take(&self) -> RenderRequest {
+ let mut request = self
+ .request
+ .lock()
+ .unwrap_or_else(std::sync::PoisonError::into_inner);
+ self.pending.store(false, Ordering::Release);
+ std::mem::take(&mut *request)
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn coalesces_pty_sources_until_taken() {
+ let signal = RenderSignal::new();
+ let first = PaneId::from_raw(10);
+ let second = PaneId::from_raw(20);
+
+ assert!(signal.request_pty(first));
+ assert!(!signal.request_pty(first));
+ assert!(!signal.request_pty(second));
+
+ let request = signal.take();
+ assert!(!request.generic);
+ assert_eq!(request.pty_sources, HashSet::from([first, second]));
+ assert!(!signal.is_pending());
+ }
+
+ #[test]
+ fn keeps_generic_and_pty_requests_distinct() {
+ let signal = RenderSignal::new();
+ let pane_id = PaneId::from_raw(10);
+
+ signal.request_generic();
+ assert!(!signal.request_pty(pane_id));
+
+ let request = signal.take();
+ assert!(request.generic);
+ assert_eq!(request.pty_sources, HashSet::from([pane_id]));
+ }
+}
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 7444f10d..899dbf9b 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -14,7 +14,7 @@
//! - Handles stale socket cleanup, explicit server stop, minimum terminal size,
//! and pane spawn failure during restore
-use std::collections::HashMap;
+use std::collections::{HashMap, HashSet};
use std::io;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicBool, Ordering};
@@ -145,6 +145,42 @@ impl RenderImpact {
}
}
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum PtyRenderState {
+ Clean,
+ Hidden,
+ Visible,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+struct RetainedRenderInput {
+ needs_full_render: bool,
+ needs_graphics_render: bool,
+ pty: PtyRenderState,
+}
+
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+enum RetainedRenderPlan {
+ Full,
+ Graphics,
+ Pty,
+ HiddenPty,
+}
+
+fn retained_render_plan(input: RetainedRenderInput) -> RetainedRenderPlan {
+ if input.needs_full_render {
+ RetainedRenderPlan::Full
+ } else if input.needs_graphics_render && input.pty != PtyRenderState::Visible {
+ RetainedRenderPlan::Graphics
+ } else {
+ match input.pty {
+ PtyRenderState::Visible => RetainedRenderPlan::Pty,
+ PtyRenderState::Hidden => RetainedRenderPlan::HiddenPty,
+ PtyRenderState::Clean => RetainedRenderPlan::Full,
+ }
+ }
+}
+
fn record_render_impact(source: &'static str, impact: RenderImpact) {
let event = match (source, impact) {
("api_requests", RenderImpact::Graphics) => "graphics_render_cause.api_requests",
@@ -511,10 +547,10 @@ impl HeadlessServer {
continue;
}
- // 1. Check render_dirty flag from PTY reader tasks.
- if self.app.render_dirty.load(Ordering::Acquire) {
+ // 1. Check the coalesced render signal from PTY readers and generic runtime work.
+ if self.app.render_dirty.is_pending() {
needs_render = true;
- crate::render_prof::event("render.request.pty_dirty");
+ crate::render_prof::event("render.request.signal");
}
let terminal_title_changed = self.app.sync_terminal_titles();
if terminal_title_changed && self.app.terminal_title_sidebar_configured() {
@@ -618,28 +654,54 @@ impl HeadlessServer {
// 7. Render virtually and stream frames.
if needs_render && self.app.can_render_now(now) {
crate::render_prof::event("render.attempt");
- let pty_dirty = self.app.render_dirty.swap(false, Ordering::AcqRel);
+ let render_request = self.app.render_dirty.take();
+ let pty_dirty = !render_request.pty_sources.is_empty();
if pty_dirty {
crate::render_prof::event("render.attempt.pty_dirty");
+ crate::render_prof::counter(
+ "render.attempt.pty_sources",
+ render_request.pty_sources.len() as u64,
+ );
+ }
+ if render_request.generic {
+ needs_full_render = true;
+ crate::render_prof::event("full_render_cause.generic_dirty");
}
if needs_full_render {
crate::render_prof::event("retained_gate.needs_full_render");
} else if !pty_dirty {
crate::render_prof::event("retained_gate.not_pty_dirty");
}
- let mut deferred_graphics = false;
- let rendered_retained = if needs_graphics_render && !needs_full_render && !pty_dirty
+ let pty = if !pty_dirty {
+ PtyRenderState::Clean
+ } else if self.pty_sources_visible_to_any_render_target(&render_request.pty_sources)
{
- match self.render_retained_graphics_update_and_stream() {
- RetainedGraphicsOutcome::Sent => true,
- RetainedGraphicsOutcome::Deferred => {
- deferred_graphics = true;
- false
+ PtyRenderState::Visible
+ } else {
+ PtyRenderState::Hidden
+ };
+ let mut deferred_graphics = false;
+ let rendered_retained = match retained_render_plan(RetainedRenderInput {
+ needs_full_render,
+ needs_graphics_render,
+ pty,
+ }) {
+ RetainedRenderPlan::Full => false,
+ RetainedRenderPlan::Graphics => {
+ match self.render_retained_graphics_update_and_stream() {
+ RetainedGraphicsOutcome::Sent => true,
+ RetainedGraphicsOutcome::Deferred => {
+ deferred_graphics = true;
+ false
+ }
+ RetainedGraphicsOutcome::Fallback => false,
}
- RetainedGraphicsOutcome::Fallback => false,
}
- } else {
- pty_dirty && !needs_full_render && self.render_retained_pty_update_and_stream()
+ RetainedRenderPlan::Pty => self.render_retained_pty_update_and_stream(),
+ RetainedRenderPlan::HiddenPty => {
+ crate::render_prof::event("render.skipped.hidden_sources");
+ true
+ }
};
if deferred_graphics {
needs_render = false;
@@ -737,7 +799,7 @@ impl HeadlessServer {
}
}
LoopEvent::RenderRequested => {
- if self.app.render_dirty.load(Ordering::Acquire) {
+ if self.app.render_dirty.is_pending() {
needs_render = true;
}
}
@@ -3396,6 +3458,85 @@ impl HeadlessServer {
}
}
+ fn pty_sources_visible_to_any_render_target(
+ &self,
+ sources: &HashSet<crate::layout::PaneId>,
+ ) -> bool {
+ let mut has_app_target = false;
+ let mut direct_terminal_targets = HashSet::new();
+ for client in self
+ .clients
+ .values()
+ .filter(|client| client.writer.is_some())
+ {
+ match &client.mode {
+ ClientConnectionMode::App if client.is_full_app_client() => {
+ has_app_target = true;
+ }
+ ClientConnectionMode::TerminalAttach { terminal_id }
+ | ClientConnectionMode::TerminalObserve { terminal_id } => {
+ direct_terminal_targets.insert(terminal_id.as_str());
+ }
+ ClientConnectionMode::App => {}
+ }
+ }
+ if !has_app_target && direct_terminal_targets.is_empty() {
+ return false;
+ }
+
+ sources.iter().any(|&pane_id| {
+ let terminal_id = self.terminal_id_for_pane(pane_id);
+ (has_app_target && (terminal_id.is_none() || self.app_surface_contains_pane(pane_id)))
+ || terminal_id
+ .is_none_or(|source| direct_terminal_targets.contains(source.as_str()))
+ })
+ }
+
+ fn terminal_id_for_pane(
+ &self,
+ pane_id: crate::layout::PaneId,
+ ) -> Option<&crate::terminal::TerminalId> {
+ if let Some(popup) = self
+ .app
+ .state
+ .popup_pane
+ .as_ref()
+ .filter(|popup| popup.pane_id == pane_id)
+ {
+ return Some(&popup.terminal_id);
+ }
+ self.app
+ .find_pane(pane_id)
+ .map(|(_, pane)| &pane.attached_terminal_id)
+ }
+
+ fn app_surface_contains_pane(&self, pane_id: crate::layout::PaneId) -> bool {
+ if self
+ .app
+ .state
+ .popup_pane
+ .as_ref()
+ .is_some_and(|popup| popup.pane_id == pane_id)
+ {
+ return true;
+ }
+ let Some(workspace) = self
+ .app
+ .state
+ .active
+ .and_then(|ws_idx| self.app.state.workspaces.get(ws_idx))
+ else {
+ return false;
+ };
+ let Some(tab) = workspace.active_tab() else {
+ return false;
+ };
+ if !tab.panes.contains_key(&pane_id) {
+ return false;
+ }
+ !tab.zoomed || tab.layout.focused() == pane_id
+ }
+
fn render_retained_pty_update_and_stream(&mut self) -> bool {
crate::render_prof::event("retained.attempt");
let retained_started = crate::render_prof::timer();
@@ -4478,6 +4619,42 @@ mod tests {
#[path = "pane_graphics.rs"]
mod pane_graphics_tests;
+ #[test]
+ fn retained_render_plan_covers_each_render_path() {
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: true,
+ needs_graphics_render: true,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::Full
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: true,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::Graphics
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty: PtyRenderState::Visible,
+ }),
+ RetainedRenderPlan::Pty
+ );
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty: PtyRenderState::Hidden,
+ }),
+ RetainedRenderPlan::HiddenPty
+ );
+ }
+
fn test_headless_server() -> HeadlessServer {
test_headless_server_with_event_hub(api::EventHub::default())
}
@@ -4827,6 +5004,38 @@ mod tests {
(server, client_rx, pane_id)
}
+ fn hidden_pty_visibility_test_server(
+ client_sizes: &[(u16, u16)],
+ ) -> (HeadlessServer, crate::layout::PaneId) {
+ 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 background_pane = workspace.tabs[background_tab].root_pane;
+ 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;
+
+ for (index, &terminal_size) in client_sizes.iter().enumerate() {
+ let client_id = index as u64 + 1;
+ let (client_tx, _client_control_rx, _client_rx) = test_client_writer();
+ server.clients.insert(
+ client_id,
+ ClientConnection::new(
+ terminal_size,
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ client_id,
+ RenderEncoding::SemanticFrame,
+ Some(client_tx),
+ ),
+ );
+ }
+
+ (server, background_pane)
+ }
+
fn assert_frame_data_eq(actual: &FrameData, expected: &FrameData) {
assert_eq!(
(actual.width, actual.height),
@@ -8076,6 +8285,117 @@ next_tab = ""
);
}
+ #[test]
+ fn inactive_tab_pty_source_is_hidden_until_tab_focus() {
+ let (server, background_pane) = hidden_pty_visibility_test_server(&[]);
+ let sources = HashSet::from([background_pane]);
+ assert!(!server.pty_sources_visible_to_any_render_target(&sources));
+
+ let (mut server, background_pane) =
+ hidden_pty_visibility_test_server(&[(120, 40), (44, 20)]);
+ let sources = HashSet::from([background_pane]);
+ assert!(!server.pty_sources_visible_to_any_render_target(&sources));
+
+ server.app.state.workspaces[0].switch_tab(1);
+ assert!(server.pty_sources_visible_to_any_render_target(&sources));
+ }
+
+ #[tokio::test]
+ async fn hidden_pty_output_appears_after_switching_to_its_tab() {
+ 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 background_pane = workspace.tabs[background_tab].root_pane;
+ workspace.insert_test_runtime(
+ background_pane,
+ crate::terminal::TerminalRuntime::test_with_screen_bytes(80, 24, b"before"),
+ );
+ 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;
+
+ let (client_tx, _client_control_rx, client_rx) = test_client_writer();
+ server.clients.insert(
+ 1,
+ ClientConnection::new(
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::SemanticFrame,
+ Some(client_tx),
+ ),
+ );
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+ server.resize_shared_runtime_to_effective_size();
+ server.render_and_stream();
+ let _initial_frame = client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("initial frame");
+
+ let runtime = server
+ .app
+ .state
+ .runtime_for_pane_in_workspace(&server.app.terminal_runtimes, 0, background_pane)
+ .expect("background runtime");
+ runtime.test_process_pty_bytes(b"\rhidden-update");
+ assert!(server.app.render_dirty.request_pty(background_pane));
+ let request = server.app.render_dirty.take();
+ let pty = if server.pty_sources_visible_to_any_render_target(&request.pty_sources) {
+ PtyRenderState::Visible
+ } else {
+ PtyRenderState::Hidden
+ };
+ assert_eq!(
+ retained_render_plan(RetainedRenderInput {
+ needs_full_render: false,
+ needs_graphics_render: false,
+ pty,
+ }),
+ RetainedRenderPlan::HiddenPty
+ );
+ assert!(client_rx.recv_timeout(Duration::from_millis(50)).is_err());
+
+ server.app.state.workspaces[0].switch_tab(background_tab);
+ server.render_and_stream();
+ let visible_frame = read_server_frame(
+ client_rx
+ .recv_timeout(Duration::from_millis(100))
+ .expect("frame after tab switch"),
+ );
+ assert!(frame_text(&visible_frame).contains("hidden-update"));
+ }
+
+ #[test]
+ fn direct_terminal_observer_keeps_hidden_pty_source_renderable() {
+ let (mut server, background_pane) = hidden_pty_visibility_test_server(&[]);
+ let terminal_id = server.app.state.workspaces[0]
+ .terminal_id(background_pane)
+ .expect("background terminal id")
+ .to_string();
+ let (client_tx, _client_control_rx, _client_rx) = test_client_writer();
+ server.clients.insert(
+ 1,
+ ClientConnection::new_with_mode(
+ ClientConnectionMode::TerminalObserve { terminal_id },
+ None,
+ (80, 24),
+ crate::kitty_graphics::HostCellSize::default(),
+ crate::terminal_theme::TerminalTheme::default(),
+ None,
+ 1,
+ RenderEncoding::SemanticFrame,
+ false,
+ Some(client_tx),
+ ),
+ );
+
+ assert!(server.pty_sources_visible_to_any_render_target(&HashSet::from([background_pane])));
+ }
+
#[tokio::test]
async fn retained_pty_update_streams_dirty_row_from_last_frame() {
let (mut server, client_rx, pane_id) = retained_test_server(b"aaaa");
diff --git a/src/server/notifications.rs b/src/server/notifications.rs
index 79ff63ca..b78cb6df 100644
--- a/src/server/notifications.rs
+++ b/src/server/notifications.rs
@@ -124,7 +124,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
diff --git a/src/terminal/id.rs b/src/terminal/id.rs
index 53ef6786..c7310753 100644
--- a/src/terminal/id.rs
+++ b/src/terminal/id.rs
@@ -20,6 +20,10 @@ impl TerminalId {
let counter = NEXT_TERMINAL_ID.fetch_add(1, Ordering::Relaxed);
Self(format!("term_{micros:x}{counter:x}"))
}
+
+ pub(crate) fn as_str(&self) -> &str {
+ &self.0
+ }
}
impl fmt::Display for TerminalId {
diff --git a/src/terminal/runtime.rs b/src/terminal/runtime.rs
index f9858ce0..d4b66f06 100644
--- a/src/terminal/runtime.rs
+++ b/src/terminal/runtime.rs
@@ -1,4 +1,6 @@
-use std::sync::{atomic::AtomicBool, Arc};
+use std::sync::Arc;
+
+use crate::render_signal::RenderSignal;
use bytes::Bytes;
use ratatui::{layout::Rect, Frame};
@@ -64,7 +66,7 @@ impl TerminalRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::from_handoff_fd(
import,
@@ -88,7 +90,7 @@ impl TerminalRuntime {
launch_env: &crate::pane::PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn(
pane_id,
@@ -120,7 +122,7 @@ impl TerminalRuntime {
initial_history_ansi: Option<&str>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_with_initial_history(
pane_id,
@@ -153,7 +155,7 @@ impl TerminalRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_shell_command(
pane_id,
@@ -186,7 +188,7 @@ impl TerminalRuntime {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<Self> {
crate::pane::PaneRuntime::spawn_argv_command(
pane_id,
diff --git a/src/ui/dialogs.rs b/src/ui/dialogs.rs
index 9575cfe9..84ce1d35 100644
--- a/src/ui/dialogs.rs
+++ b/src/ui/dialogs.rs
@@ -831,7 +831,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
let mut terminal_runtimes = crate::terminal::TerminalRuntimeRegistry::new();
diff --git a/src/ui/mobile.rs b/src/ui/mobile.rs
index 299a152b..b7738992 100644
--- a/src/ui/mobile.rs
+++ b/src/ui/mobile.rs
@@ -1477,7 +1477,7 @@ mod tests {
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
diff --git a/src/ui/sidebar.rs b/src/ui/sidebar.rs
index 69193158..0a2b64ce 100644
--- a/src/ui/sidebar.rs
+++ b/src/ui/sidebar.rs
@@ -2258,7 +2258,7 @@ rows = [[{ token = "git_status", fg = "#123456" }]]
&crate::pane::PaneLaunchEnv::default(),
events,
std::sync::Arc::new(tokio::sync::Notify::new()),
- std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)),
+ std::sync::Arc::new(crate::render_signal::RenderSignal::new()),
)
.unwrap();
diff --git a/src/workspace.rs b/src/workspace.rs
index bb079b96..23e35322 100644
--- a/src/workspace.rs
+++ b/src/workspace.rs
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::ops::{Deref, DerefMut};
use std::path::PathBuf;
-use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
+use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use ratatui::layout::Direction;
@@ -12,6 +12,7 @@ use crate::layout::PaneId;
#[cfg(test)]
use crate::layout::TileLayout;
use crate::pane::{PaneLaunchEnv, PaneState};
+use crate::render_signal::RenderSignal;
use crate::terminal::{TerminalId, TerminalRuntime, TerminalRuntimeRegistry, TerminalState};
mod aggregate;
@@ -234,7 +235,7 @@ impl Workspace {
moved: MovedPane,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> Self {
let id = generate_workspace_id();
let root_pane = moved.pane_id;
@@ -278,7 +279,7 @@ impl Workspace {
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_with_extra_env(
initial_cwd,
@@ -304,7 +305,7 @@ impl Workspace {
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
extra_env: Vec<(String, String)>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_with_tab(
@@ -333,7 +334,7 @@ impl Workspace {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_argv_command_with_extra_env(
initial_cwd,
@@ -359,7 +360,7 @@ impl Workspace {
host_terminal_theme: crate::terminal_theme::TerminalTheme,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
extra_env: Vec<(String, String)>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_with_tab(
@@ -387,7 +388,7 @@ impl Workspace {
shell_config: crate::pane::PaneShellConfig<'_>,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
argv: Option<&[String]>,
extra_env: Vec<(String, String)>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
@@ -1014,7 +1015,7 @@ impl Workspace {
label: Option<String>,
fallback_events: mpsc::Sender<AppEvent>,
fallback_render_notify: Arc<Notify>,
- fallback_render_dirty: Arc<AtomicBool>,
+ fallback_render_dirty: Arc<RenderSignal>,
) -> usize {
let number = self.next_public_tab_number;
self.next_public_tab_number += 1;
@@ -1246,7 +1247,7 @@ impl Workspace {
pub(crate) fn test_new(name: &str) -> Self {
let (events, _) = mpsc::channel(64);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(RenderSignal::new());
let identity_cwd = std::env::current_dir().unwrap_or_else(|_| "/".into());
let (layout, root_id) = TileLayout::new();
let terminal_id = TerminalId::alloc();
@@ -1304,7 +1305,7 @@ impl Workspace {
pub(crate) fn test_add_tab(&mut self, name: Option<&str>) -> usize {
let (events, _) = mpsc::channel(64);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(RenderSignal::new());
let (layout, root_id) = TileLayout::new();
let mut panes = HashMap::new();
panes.insert(root_id, PaneState::new(TerminalId::alloc()));
@@ -1653,7 +1654,7 @@ mod tests {
let argv = vec![command.to_string()];
let (events, _) = mpsc::channel(64);
let render_notify = Arc::new(Notify::new());
- let render_dirty = Arc::new(AtomicBool::new(false));
+ let render_dirty = Arc::new(RenderSignal::new());
let (workspace, _terminal, runtime) = Workspace::new_argv_command(
root.clone(),
diff --git a/src/workspace/tab.rs b/src/workspace/tab.rs
index e2f82741..55fa4c96 100644
--- a/src/workspace/tab.rs
+++ b/src/workspace/tab.rs
@@ -1,6 +1,5 @@
use std::collections::HashMap;
use std::path::PathBuf;
-use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use ratatui::layout::Direction;
@@ -9,6 +8,7 @@ use tokio::sync::{mpsc, Notify};
use crate::events::AppEvent;
use crate::layout::{Node, PaneId, TileLayout};
use crate::pane::{PaneLaunchEnv, PaneState};
+use crate::render_signal::RenderSignal;
use crate::terminal::{TerminalId, TerminalRuntime, TerminalRuntimeRegistry, TerminalState};
pub(crate) type DetachedPane = (PaneId, TerminalId);
@@ -48,7 +48,7 @@ pub struct Tab {
pub zoomed: bool,
pub events: mpsc::Sender<AppEvent>,
pub(crate) render_notify: Arc<Notify>,
- pub(crate) render_dirty: Arc<AtomicBool>,
+ pub(crate) render_dirty: Arc<RenderSignal>,
}
impl Tab {
@@ -63,7 +63,7 @@ impl Tab {
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_with_runtime(
number,
@@ -92,7 +92,7 @@ impl Tab {
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
Self::new_with_runtime(
number,
@@ -122,7 +122,7 @@ impl Tab {
launch_env: &PaneLaunchEnv,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
argv: Option<&[String]>,
) -> std::io::Result<(Self, TerminalState, TerminalRuntime)> {
let (layout, root_id) = TileLayout::new();
@@ -439,7 +439,7 @@ impl Tab {
moved: MovedPane,
events: mpsc::Sender<AppEvent>,
render_notify: Arc<Notify>,
- render_dirty: Arc<AtomicBool>,
+ render_dirty: Arc<RenderSignal>,
) -> Self {
let mut panes = HashMap::new();
let pane_id = moved.pane_id;