//! Runtime command launching and overlay pane lifecycle helpers for [`App`]. use std::process::{Command, Stdio}; use ratatui::layout::Direction; use super::{App, Mode}; #[derive(Debug, Clone, Copy)] pub(super) struct OverlayPaneState { ws_idx: usize, tab_idx: usize, previous_focus: crate::layout::PaneId, previous_zoomed: bool, } impl App { pub(super) fn launch_custom_command(&mut self, binding: crate::config::CustomCommandKeybind) { let previous_toast = self.state.toast.clone(); let result = match binding.action { crate::config::CustomCommandAction::Shell => self.spawn_custom_command(&binding), crate::config::CustomCommandAction::Pane => self.spawn_pane_command(&binding.command), }; match result { Ok(()) => { if self.state.active.is_some() { self.state.mode = Mode::Terminal; } } Err(err) => { self.state.toast = Some(crate::app::state::ToastNotification { kind: crate::app::state::ToastKind::NeedsAttention, title: "custom command failed".to_string(), context: err.to_string(), }); self.sync_toast_deadline(previous_toast); } } } fn custom_command_env(&self) -> (Vec<(String, String)>, Option) { let mut env = vec![( crate::api::SOCKET_PATH_ENV_VAR.to_string(), crate::api::socket_path().display().to_string(), )]; if let Ok(current_exe) = std::env::current_exe() { env.push(( "HERDR_BIN_PATH".to_string(), current_exe.display().to_string(), )); } let mut cwd = None; if let Some(ws_idx) = self.state.active { env.push(( "HERDR_ACTIVE_WORKSPACE_ID".to_string(), self.public_workspace_id(ws_idx), )); if let Some(workspace) = self.state.workspaces.get(ws_idx) { let tab_idx = workspace.active_tab_index(); if let Some(tab_id) = self.public_tab_id(ws_idx, tab_idx) { env.push(("HERDR_ACTIVE_TAB_ID".to_string(), tab_id)); } if let Some(pane_id) = workspace.focused_pane_id() { if let Some(public_pane_id) = self.public_pane_id(ws_idx, pane_id) { env.push(("HERDR_ACTIVE_PANE_ID".to_string(), public_pane_id)); } if let Some(pane_cwd) = workspace .active_tab() .and_then(|tab| tab.cwd_for_pane(pane_id)) { env.push(( "HERDR_ACTIVE_PANE_CWD".to_string(), pane_cwd.display().to_string(), )); if pane_cwd.is_dir() { cwd = Some(pane_cwd); } } } } } (env, cwd) } fn spawn_custom_command( &self, binding: &crate::config::CustomCommandKeybind, ) -> std::io::Result<()> { let mut command = Command::new("/bin/sh"); command .arg("-lc") .arg(&binding.command) .stdin(Stdio::null()) .stdout(Stdio::null()) .stderr(Stdio::null()); let (env, cwd) = self.custom_command_env(); command.envs(env); if let Some(cwd) = cwd { command.current_dir(cwd); } command.spawn()?; Ok(()) } fn spawn_pane_command(&mut self, command: &str) -> std::io::Result<()> { let Some(ws_idx) = self.state.active else { return Err(std::io::Error::other("no active workspace")); }; let (rows, cols) = self.state.estimate_pane_size(); let new_rows = rows.max(4); let new_cols = cols.max(10); let (env, _) = self.custom_command_env(); let ws = self .state .workspaces .get_mut(ws_idx) .ok_or_else(|| std::io::Error::other("active workspace disappeared"))?; let tab_idx = ws.active_tab_index(); let previous_focus = ws .focused_pane_id() .ok_or_else(|| std::io::Error::other("no focused pane"))?; let previous_zoomed = ws.active_tab().map(|tab| tab.zoomed).unwrap_or(false); let cwd = ws .active_tab() .and_then(|tab| tab.cwd_for_pane(previous_focus)); let new_pane_id = ws.split_focused_command( Direction::Horizontal, new_rows, new_cols, cwd, command, &env, self.state.pane_scrollback_limit_bytes, self.state.host_terminal_theme, )?; ws.active_tab_mut() .expect("workspace must have an active tab") .layout .focus_pane(new_pane_id); ws.active_tab_mut() .expect("workspace must have an active tab") .zoomed = true; self.overlay_panes.insert( new_pane_id, OverlayPaneState { ws_idx, tab_idx, previous_focus, previous_zoomed, }, ); self.state.mode = Mode::Terminal; Ok(()) } pub(super) fn restore_overlay_after_exit(&mut self, overlay: OverlayPaneState) { let Some(ws) = self.state.workspaces.get_mut(overlay.ws_idx) else { return; }; if overlay.tab_idx >= ws.tabs.len() { return; } ws.active_tab = overlay.tab_idx; let tab = &mut ws.tabs[overlay.tab_idx]; if tab.panes.contains_key(&overlay.previous_focus) { tab.layout.focus_pane(overlay.previous_focus); } tab.zoomed = overlay.previous_zoomed; if self.state.active == Some(overlay.ws_idx) { self.state.mode = Mode::Terminal; } } }