diff options
| author | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 03:58:07 +0300 |
|---|---|---|
| committer | Ogulcan Celik <ogulcancelik@gmail.com> | 2026-07-15 04:02:14 +0300 |
| commit | 2c7c8beb079c5fd58a2e53efd2058df050ad9f55 (patch) | |
| tree | 6756f671dc728d915b1d6ebececa8cf7f70b66fb /src/pane.rs | |
| parent | 88370e1516182ab519d22a12fa82d5780a40fee6 (diff) | |
feat: add floating popup panes
refs #1125
Diffstat (limited to 'src/pane.rs')
| -rw-r--r-- | src/pane.rs | 118 |
1 files changed, 90 insertions, 28 deletions
diff --git a/src/pane.rs b/src/pane.rs index 8f71031f..5bb6fe4e 100644 --- a/src/pane.rs +++ b/src/pane.rs @@ -65,21 +65,26 @@ fn apply_pane_terminal_env(cmd: &mut CommandBuilder) { #[derive(Debug, Clone, Default, PartialEq, Eq)] pub(crate) struct PaneLaunchEnv { extra: Vec<(String, String)>, - identity: Option<PaneLaunchIdentity>, + identity: PaneLaunchIdentity, } -#[derive(Debug, Clone, PartialEq, Eq)] -struct PaneLaunchIdentity { - workspace_id: String, - tab_id: String, - pane_id: String, +#[derive(Debug, Clone, Default, PartialEq, Eq)] +enum PaneLaunchIdentity { + #[default] + Inherit, + Managed { + workspace_id: String, + tab_id: String, + pane_id: String, + }, + OmitPane, } impl PaneLaunchEnv { pub(crate) fn from_extra(extra: Vec<(String, String)>) -> Self { Self { extra, - identity: None, + identity: PaneLaunchIdentity::Inherit, } } @@ -89,11 +94,16 @@ impl PaneLaunchEnv { tab_id: String, pane_id: String, ) -> Self { - self.identity = Some(PaneLaunchIdentity { + self.identity = PaneLaunchIdentity::Managed { workspace_id, tab_id, pane_id, - }); + }; + self + } + + pub(crate) fn without_pane_identity(mut self) -> Self { + self.identity = PaneLaunchIdentity::OmitPane; self } } @@ -104,13 +114,20 @@ fn apply_pane_launch_env(cmd: &mut CommandBuilder, launch_env: &PaneLaunchEnv) { } cmd.env(crate::HERDR_ENV_VAR, crate::HERDR_ENV_VALUE); crate::integration::apply_pane_base_env(cmd); - if let Some(identity) = &launch_env.identity { - cmd.env( - crate::integration::HERDR_WORKSPACE_ID_ENV_VAR, - &identity.workspace_id, - ); - cmd.env(crate::integration::HERDR_TAB_ID_ENV_VAR, &identity.tab_id); - cmd.env(crate::integration::HERDR_PANE_ID_ENV_VAR, &identity.pane_id); + match &launch_env.identity { + PaneLaunchIdentity::Inherit => {} + PaneLaunchIdentity::Managed { + workspace_id, + tab_id, + pane_id, + } => { + cmd.env(crate::integration::HERDR_WORKSPACE_ID_ENV_VAR, workspace_id); + cmd.env(crate::integration::HERDR_TAB_ID_ENV_VAR, tab_id); + cmd.env(crate::integration::HERDR_PANE_ID_ENV_VAR, pane_id); + } + PaneLaunchIdentity::OmitPane => { + cmd.env_remove(crate::integration::HERDR_PANE_ID_ENV_VAR); + } } } @@ -127,6 +144,12 @@ struct SpawnInitialState<'a> { windows_powershell_prompt_cwd_reporting: bool, } +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum AgentDetection { + Enabled, + Disabled, +} + fn active_pending_release( pending_release: &Mutex<Option<PendingAgentRelease>>, now: std::time::Instant, @@ -917,7 +940,7 @@ pub struct PaneRuntime { pending_release: Arc<Mutex<Option<PendingAgentRelease>>>, preserve_processes_on_drop: bool, // Task handles for deterministic shutdown - detect_handle: tokio::task::AbortHandle, + detect_handle: Option<tokio::task::AbortHandle>, } enum PaneRuntimeIo { @@ -1061,7 +1084,9 @@ impl Drop for PaneRuntime { fn drop(&mut self) { // Abort detection task immediately and terminate the owned session. // The PTY actor shuts down before the process/session policy runs. - self.detect_handle.abort(); + if let Some(handle) = &self.detect_handle { + handle.abort(); + } self.io.shutdown(); if !self.preserve_processes_on_drop { shutdown_pane_processes( @@ -1411,7 +1436,9 @@ fn publish_reported_cwd( impl PaneRuntime { pub fn shutdown(mut self) { - self.detect_handle.abort(); + if let Some(handle) = self.detect_handle.take() { + handle.abort(); + } self.io.shutdown(); shutdown_pane_processes( self.pane_id, @@ -1435,7 +1462,9 @@ impl PaneRuntime { "failed to release PTY actor after handoff commit; dropping runtime will still close the actor handle" ); } - self.detect_handle.abort(); + if let Some(handle) = self.detect_handle.take() { + handle.abort(); + } self.preserve_processes_on_drop = true; } @@ -1571,6 +1600,7 @@ impl PaneRuntime { history_ansi: initial_history_ansi, windows_powershell_prompt_cwd_reporting, }, + AgentDetection::Enabled, ) } @@ -1583,6 +1613,7 @@ impl PaneRuntime { cwd: std::path::PathBuf, command: &str, launch_env: &PaneLaunchEnv, + agent_detection: AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, events: mpsc::Sender<AppEvent>, @@ -1605,9 +1636,12 @@ impl PaneRuntime { cmd, "failed to spawn command pane", SpawnInitialState::default(), + agent_detection, ) } + // Runtime construction needs to thread PTY size, environment, theme, render hooks, and detection policy together. + #[allow(clippy::too_many_arguments)] pub fn spawn_argv_command( pane_id: PaneId, rows: u16, @@ -1615,6 +1649,7 @@ impl PaneRuntime { cwd: std::path::PathBuf, argv: &[String], launch_env: &PaneLaunchEnv, + agent_detection: AgentDetection, scrollback_limit_bytes: usize, host_terminal_theme: crate::terminal_theme::TerminalTheme, events: mpsc::Sender<AppEvent>, @@ -1646,6 +1681,7 @@ impl PaneRuntime { cmd, "failed to spawn argv command pane", SpawnInitialState::default(), + agent_detection, ) } @@ -1791,10 +1827,12 @@ impl PaneRuntime { detect_reset_notify, pending_release, preserve_processes_on_drop: true, - detect_handle, + detect_handle: Some(detect_handle), }) } + // Runtime construction needs to thread PTY size, environment, theme, render hooks, and detection policy together. + #[allow(clippy::too_many_arguments)] fn spawn_command_builder( pane_id: PaneId, rows: u16, @@ -1807,6 +1845,7 @@ impl PaneRuntime { cmd: CommandBuilder, spawn_error_message: &'static str, initial_state: SpawnInitialState<'_>, + agent_detection: AgentDetection, ) -> std::io::Result<Self> { crate::logging::pane_spawn_started(pane_id.raw(), rows, cols, scrollback_limit_bytes); @@ -1881,7 +1920,9 @@ impl PaneRuntime { let shell_pid = child_pid.load(Ordering::Acquire); let result = terminal.process_pty_bytes(pane_id, shell_pid, bytes, &response_writer); - observe_detection_content_change(bytes, &detection_content_seq); + if agent_detection == AgentDetection::Enabled { + observe_detection_content_change(bytes, &detection_content_seq); + } if result.request_render && !render_dirty.swap(true, Ordering::AcqRel) { render_notify.notify_one(); } @@ -1924,7 +1965,9 @@ impl PaneRuntime { }; // --- Detection task --- - let (detect_handle, detect_reset_notify, pending_release) = { + let (detect_handle, detect_reset_notify, pending_release) = if agent_detection + == AgentDetection::Enabled + { use crate::detect; use std::time::{Duration, Instant}; @@ -2282,7 +2325,13 @@ impl PaneRuntime { } } }); - (handle.abort_handle(), detect_reset_notify, pending_release) + ( + Some(handle.abort_handle()), + detect_reset_notify, + pending_release, + ) + } else { + (None, Arc::new(Notify::new()), Arc::new(Mutex::new(None))) }; Ok(Self { @@ -2322,6 +2371,11 @@ 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 @@ -2538,6 +2592,14 @@ impl PaneRuntime { } 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)) + } + + fn paste_payload(&self, text: String) -> Bytes { let bracketed = self .input_state() .map(|state| state.bracketed_paste) @@ -2547,7 +2609,7 @@ impl PaneRuntime { } else { text }; - self.send_bytes(Bytes::from(payload)).await + Bytes::from(payload) } pub fn try_send_focus_event(&self, event: crate::ghostty::FocusEvent) -> bool { @@ -2743,7 +2805,7 @@ impl PaneRuntime { detect_reset_notify: Arc::new(Notify::new()), pending_release: Arc::new(Mutex::new(None)), preserve_processes_on_drop: true, - detect_handle: tokio::spawn(async {}).abort_handle(), + detect_handle: Some(tokio::spawn(async {}).abort_handle()), }, rx, ) @@ -3205,7 +3267,7 @@ mod tests { detect_reset_notify: Arc::new(Notify::new()), pending_release: Arc::new(Mutex::new(None)), preserve_processes_on_drop: true, - detect_handle: tokio::spawn(async {}).abort_handle(), + detect_handle: Some(tokio::spawn(async {}).abort_handle()), }; assert!(runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained)); @@ -3236,7 +3298,7 @@ mod tests { detect_reset_notify: Arc::new(Notify::new()), pending_release: Arc::new(Mutex::new(None)), preserve_processes_on_drop: true, - detect_handle: tokio::spawn(async {}).abort_handle(), + detect_handle: Some(tokio::spawn(async {}).abort_handle()), }; assert!(!runtime.try_send_focus_event(crate::ghostty::FocusEvent::Gained)); |
