aboutsummaryrefslogtreecommitdiff
path: root/src/pane.rs
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-09-01 15:48:22 +0300
committerGitHub <noreply@github.com>2026-09-01 15:48:22 +0300
commit207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch)
treed7b037ecdf629224a048c841d11659efdc23507d /src/pane.rs
parent8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff)
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell * feat: add client-owned shell mouse controls * feat: complete client-owned shell chrome * feat: add client-owned shell settings and notifications * feat: run client shell custom commands * feat: render popup terminals in client shell * feat: add client-owned scrollback and copy mode * fix: preserve client worktree trust defaults * feat: render startup diagnostics in client shell * feat: render onboarding in client shell * feat: render product announcements in client shell * feat: render release notes in client shell * feat: render mobile switcher in client shell * feat: render kitty graphics in client shell * fix: preserve client shell lifecycle coherence * refactor: route client shell commands through endpoint connections * refactor: make the client-rendered shell the default * refactor: remove monolithic launch mode * refactor: complete client-rendered shell cutover * perf: retain client pane surface updates * perf: scale retained pane surface updates * perf: remove remote pane input latency * test: make client shell checks platform-independent * fix: prevent client timer starvation * test: fix client shell platform gates * test: accept retained surfaces in cross-area checks * test: make client mode assertions platform-neutral * fix: badge only outdated integrations
Diffstat (limited to 'src/pane.rs')
-rw-r--r--src/pane.rs118
1 files changed, 60 insertions, 58 deletions
diff --git a/src/pane.rs b/src/pane.rs
index 7340629c..48928034 100644
--- a/src/pane.rs
+++ b/src/pane.rs
@@ -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, TerminalTextMatch,
- TerminalTextPoint, TerminalWordMotion,
+ TerminalDirtyPatch, TerminalDirtyPatchOutcome, TerminalReadSnapshot, TerminalSearchDirection,
+ TerminalSearchWindow, TerminalTextPoint, TerminalWordMotion,
};
pub use self::{
state::PaneState,
@@ -56,21 +56,6 @@ const RELEASE_REACQUIRE_SUPPRESSION: std::time::Duration = std::time::Duration::
const PANE_TERM: &str = "xterm-256color";
const PANE_COLORTERM: &str = "truecolor";
-#[cfg(test)]
-thread_local! {
- static AGGREGATE_INPUT_STATE_READS: Cell<usize> = const { Cell::new(0) };
-}
-
-#[cfg(test)]
-pub(crate) fn reset_aggregate_input_state_reads() {
- AGGREGATE_INPUT_STATE_READS.set(0);
-}
-
-#[cfg(test)]
-pub(crate) fn aggregate_input_state_reads() -> usize {
- AGGREGATE_INPUT_STATE_READS.get()
-}
-
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
@@ -1053,6 +1038,7 @@ pub struct PaneRuntime {
child_wait_completed: Option<Arc<AtomicBool>>,
kitty_keyboard_flags: Arc<AtomicU16>,
content_seq: Arc<AtomicU64>,
+ content_write_lock: Arc<Mutex<()>>,
detection_content_seq: Arc<AtomicU64>,
full_lifecycle_authority_active: Arc<AtomicBool>,
detect_reset_notify: Arc<Notify>,
@@ -1175,14 +1161,6 @@ impl PaneRuntimeIo {
}
}
- async fn send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::SendError<Bytes>> {
- match self {
- PaneRuntimeIo::Actor(actor) => actor.write_user_input(bytes).await,
- #[cfg(test)]
- PaneRuntimeIo::TestChannel { sender, .. } => sender.send(bytes).await,
- }
- }
-
fn try_send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::TrySendError<Bytes>> {
match self {
PaneRuntimeIo::Actor(actor) => actor.try_write_user_input(bytes),
@@ -1926,6 +1904,7 @@ impl PaneRuntime {
let reported_cwd = Arc::new(Mutex::new(None));
let kitty_keyboard_flags = Arc::new(AtomicU16::new(keyboard_protocol_flags));
let content_seq = Arc::new(AtomicU64::new(0));
+ let content_write_lock = Arc::new(Mutex::new(()));
let detection_content_seq = Arc::new(AtomicU64::new(0));
let io = {
@@ -1934,6 +1913,7 @@ impl PaneRuntime {
let render_notify = render_notify.clone();
let render_dirty = render_dirty.clone();
let content_seq = content_seq.clone();
+ let content_write_lock = content_write_lock.clone();
let detection_content_seq = detection_content_seq.clone();
let child_pid = child_pid.clone();
let read_events = events.clone();
@@ -1941,11 +1921,16 @@ impl PaneRuntime {
let rt = tokio::runtime::Handle::current();
let delay_rt = rt.clone();
let on_read = Box::new(move |bytes: &[u8]| {
+ let _content_write_guard = match content_write_lock.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
content_seq.fetch_add(1, Ordering::AcqRel);
let shell_pid = child_pid.load(Ordering::Acquire);
let result =
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
content_seq.fetch_add(1, Ordering::Release);
+ drop(_content_write_guard);
publish_terminal_bells(pane_id, result.terminal_bells, &read_events);
observe_detection_content_change(bytes, &detection_content_seq);
let title_requested =
@@ -2014,6 +1999,7 @@ impl PaneRuntime {
child_wait_completed: None,
kitty_keyboard_flags,
content_seq,
+ content_write_lock,
detection_content_seq,
full_lifecycle_authority_active,
detect_reset_notify,
@@ -2061,6 +2047,7 @@ impl PaneRuntime {
}
let terminal = Arc::new(PaneTerminal::new(pane_terminal));
let kitty_keyboard_flags = Arc::new(AtomicU16::new(0));
+ let content_write_lock = Arc::new(Mutex::new(()));
let spawned = crate::pty::backend::spawn_with_portable_pty(rows, cols, cmd)
.inspect_err(|err| error!(pane = pane_id.raw(), err = %err, "{spawn_error_message}"))?;
@@ -2104,17 +2091,23 @@ impl PaneRuntime {
let render_notify = render_notify.clone();
let render_dirty = render_dirty.clone();
let content_seq = content_seq.clone();
+ let content_write_lock = content_write_lock.clone();
let detection_content_seq = detection_content_seq.clone();
let child_pid = child_pid.clone();
let events = events.clone();
let reported_cwd = reported_cwd.clone();
let rt = tokio::runtime::Handle::current();
let on_read = Box::new(move |bytes: &[u8]| {
+ let _content_write_guard = match content_write_lock.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
content_seq.fetch_add(1, Ordering::AcqRel);
let shell_pid = child_pid.load(Ordering::Acquire);
let result =
terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer);
content_seq.fetch_add(1, Ordering::Release);
+ drop(_content_write_guard);
publish_terminal_bells(pane_id, result.terminal_bells, &events);
if agent_detection == AgentDetection::Enabled {
observe_detection_content_change(bytes, &detection_content_seq);
@@ -2570,6 +2563,7 @@ impl PaneRuntime {
child_wait_completed: Some(child_wait_completed),
kitty_keyboard_flags,
content_seq,
+ content_write_lock,
detection_content_seq,
full_lifecycle_authority_active,
detect_reset_notify,
@@ -2598,11 +2592,6 @@ impl PaneRuntime {
self.detect_reset_notify.clone()
}
- #[cfg(test)]
- pub(crate) fn agent_detection_enabled_for_test(&self) -> bool {
- self.detect_handle.is_some()
- }
-
pub fn set_full_lifecycle_authority_active(&self, active: bool) {
let previous = self
.full_lifecycle_authority_active
@@ -2630,9 +2619,16 @@ impl PaneRuntime {
return;
}
self.current_size.set(size);
+ let _content_write_guard = match self.content_write_lock.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
+ self.content_seq.fetch_add(1, Ordering::AcqRel);
let terminal_responses = self
.terminal
.resize(rows, cols, cell_width_px, cell_height_px);
+ self.content_seq.fetch_add(1, Ordering::Release);
+ drop(_content_write_guard);
mark_detection_content_changed(&self.detection_content_seq);
self.io.resize(
rows,
@@ -2674,23 +2670,20 @@ impl PaneRuntime {
self.terminal.scroll_metrics()
}
- pub(crate) fn search_text_matches(
+ pub(crate) fn search_text_window(
&self,
query: &str,
case_sensitive: bool,
- ) -> Vec<crate::pane::TerminalTextMatch> {
- self.terminal.search_text_matches(query, case_sensitive)
- }
-
- pub(crate) fn text_match_is_current(&self, text_match: crate::pane::TerminalTextMatch) -> bool {
- self.terminal.text_match_is_current(text_match)
- }
-
- pub(crate) fn text_matches_are_current(
- &self,
- text_matches: &[crate::pane::TerminalTextMatch],
- ) -> Vec<bool> {
- self.terminal.text_matches_are_current(text_matches)
+ direction: crate::pane::TerminalSearchDirection,
+ cursor: crate::pane::TerminalTextPoint,
+ previous: Option<(
+ crate::pane::TerminalTextPoint,
+ crate::pane::TerminalTextPoint,
+ )>,
+ limit: usize,
+ ) -> crate::pane::TerminalSearchWindow {
+ self.terminal
+ .search_text_window(query, case_sensitive, direction, cursor, previous, limit)
}
pub(crate) fn word_motion_target(
@@ -2702,17 +2695,23 @@ impl PaneRuntime {
self.terminal.word_motion_target(row, col, motion)
}
+ pub(crate) fn terminal_dimensions(&self) -> Option<(u16, u16)> {
+ self.terminal.dimensions()
+ }
+
+ pub(crate) fn paragraph_motion_target(
+ &self,
+ row: u32,
+ direction: i8,
+ ) -> Option<crate::pane::TerminalTextPoint> {
+ self.terminal.paragraph_motion_target(row, direction)
+ }
+
#[cfg(any(unix, test))]
pub fn input_state(&self) -> Option<InputState> {
- #[cfg(test)]
- AGGREGATE_INPUT_STATE_READS.set(AGGREGATE_INPUT_STATE_READS.get() + 1);
self.terminal.input_state()
}
- pub fn keyboard_report_all_requested(&self) -> bool {
- self.terminal.keyboard_report_all_requested()
- }
-
pub fn bracketed_paste_enabled(&self) -> bool {
self.terminal.bracketed_paste_enabled()
}
@@ -2844,15 +2843,15 @@ impl PaneRuntime {
self.terminal.keyboard_protocol(fallback)
}
+ pub fn modify_other_keys_level(&self) -> u8 {
+ self.terminal.modify_other_keys_level()
+ }
+
pub fn encode_terminal_key(&self, key: crate::input::TerminalKey) -> Vec<u8> {
self.terminal
.encode_terminal_key(key, self.keyboard_protocol())
}
- pub async fn send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::SendError<Bytes>> {
- self.io.send_bytes(bytes).await
- }
-
pub fn try_send_bytes(&self, bytes: Bytes) -> Result<(), mpsc::error::TrySendError<Bytes>> {
self.io.try_send_bytes(bytes)
}
@@ -2861,10 +2860,6 @@ impl PaneRuntime {
self.io.send_bytes_after(bytes, delay);
}
- pub async fn send_paste(&self, text: String) -> Result<(), mpsc::error::SendError<Bytes>> {
- self.send_bytes(self.paste_payload(text)).await
- }
-
pub fn try_send_paste(&self, text: String) -> Result<(), mpsc::error::TrySendError<Bytes>> {
self.try_send_bytes(self.paste_payload(text))
}
@@ -3047,6 +3042,10 @@ impl PaneRuntime {
}
pub(crate) fn test_process_pty_bytes(&self, bytes: &[u8]) {
+ let _content_write_guard = match self.content_write_lock.lock() {
+ Ok(guard) => guard,
+ Err(poisoned) => poisoned.into_inner(),
+ };
self.content_seq.fetch_add(1, Ordering::AcqRel);
let (tx, _rx) = mpsc::channel(1);
let _ = self.terminal.process_pty_bytes(self.pane_id, 0, bytes, &tx);
@@ -3091,6 +3090,7 @@ impl PaneRuntime {
child_wait_completed: None,
kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
content_seq: Arc::new(AtomicU64::new(0)),
+ content_write_lock: Arc::new(Mutex::new(())),
detection_content_seq: Arc::new(AtomicU64::new(0)),
full_lifecycle_authority_active: Arc::new(AtomicBool::new(false)),
detect_reset_notify: Arc::new(Notify::new()),
@@ -3660,6 +3660,7 @@ mod tests {
child_wait_completed: None,
kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
content_seq: Arc::new(AtomicU64::new(0)),
+ content_write_lock: Arc::new(Mutex::new(())),
detection_content_seq: Arc::new(AtomicU64::new(0)),
full_lifecycle_authority_active: Arc::new(AtomicBool::new(false)),
detect_reset_notify: Arc::new(Notify::new()),
@@ -3692,6 +3693,7 @@ mod tests {
child_wait_completed: None,
kitty_keyboard_flags: Arc::new(AtomicU16::new(0)),
content_seq: Arc::new(AtomicU64::new(0)),
+ content_write_lock: Arc::new(Mutex::new(())),
detection_content_seq: Arc::new(AtomicU64::new(0)),
full_lifecycle_authority_active: Arc::new(AtomicBool::new(false)),
detect_reset_notify: Arc::new(Notify::new()),