use crate::background_jobs::write_session_state_to_disk; use crate::background_jobs::BackgroundJob; use crate::global_async_runtime::get_tokio_runtime as async_runtime; use crate::os_input_output::{AsyncReader, NullAsyncReader}; use crate::route::NotificationEnd; use crate::terminal_bytes::TerminalBytes; use crate::{ panes::PaneId, plugins::{DumpSessionLayoutResponse, PluginId, PluginInstruction}, screen::{ScreenInstruction, TabOverrideResult}, session_layout_metadata::SessionLayoutMetadata, thread_bus::{Bus, ThreadSenders}, ClientId, ServerInstruction, }; use std::sync::Arc; use std::{collections::HashMap, path::PathBuf}; use tokio::task::JoinHandle; use zellij_utils::{ data::{ CommandOrPlugin, Event, FloatingPaneCoordinates, GetPaneCwdResponse, GetPanePidResponse, GetPaneRunningCommandResponse, NewPanePlacement, OriginatingPlugin, SessionInfo, }, errors::prelude::*, errors::{ContextType, PtyContext}, input::{ command::{OpenFilePayload, RunCommand, TerminalAction}, layout::{ FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TabLayoutInfo, TiledPaneLayout, }, }, pane_size::Size, session_serialization, }; pub type VteBytes = Vec; pub type TabIndex = u32; #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum ClientTabIndexOrPaneId { ClientId(ClientId), ClientIdNoFocus(ClientId), TabIndex(usize), TabIndexNoFocus(usize), PaneId(PaneId), } /// Instructions related to PTYs (pseudoterminals). #[derive(Clone, Debug)] pub enum PtyInstruction { SpawnTerminal( Option, Option, NewPanePlacement, bool, // start suppressed ClientTabIndexOrPaneId, Option, // completion signal bool, // set_blocking Option, // agent-facing purpose (see workspace hints) ), // bool (if Some) is // should_float, String is an optional pane name OpenInPlaceEditor( PathBuf, Option, ClientTabIndexOrPaneId, Option, ), // Option is the optional line number UpdateActivePane(Option, ClientId), GoToTab(TabIndex, ClientId), NewTab( Option, Option, Option, Vec, usize, // tab_index HashMap>, // plugin_ids Option>, // initial_panes bool, // block_on_first_terminal bool, // should change focus to new tab (ClientId, bool), // bool -> is_web_client Option, // completion signal ), // the String is the tab name OverrideLayout( Option, // CWD Option, // Default Shell Vec<(TabLayoutInfo, HashMap>)>, // (layout, plugin_ids) per tab bool, // retain_existing_terminal_panes bool, // retain_existing_plugin_panes ClientId, Option, ), ClosePane(PaneId, Option), CloseTab(Vec), ReRunCommandInPane(PaneId, RunCommand, Option), DropToShellInPane { pane_id: PaneId, shell: Option, working_dir: Option, completion_tx: Option, }, SpawnInPlaceTerminal( Option, Option, bool, // close replaced pane ClientTabIndexOrPaneId, Option, // completion signal ), // String is an optional pane name DumpLayout(SessionLayoutMetadata, ClientId, Option), DumpLayoutToPlugin { session_layout_metadata: SessionLayoutMetadata, plugin_id: PluginId, response_channel: crossbeam::channel::Sender, }, LogLayoutToHd(SessionLayoutMetadata), SaveSessionToDisk { session_name: String, session_info: SessionInfo, session_layout_metadata: SessionLayoutMetadata, completion_tx: Option, }, FillPluginCwd( Option, // should float bool, // should be opened in place bool, // close_replaced_pane Option, // pane title RunPluginOrAlias, usize, // tab index Option, // pane id to replace if this is to be opened "in-place" ClientId, Size, bool, // skip cache Option, // if Some, will not fill cwd but just forward the message Option, // should focus plugin Option, Option, ), ListClientsMetadata(SessionLayoutMetadata, ClientId, Option), Reconfigure { client_id: ClientId, default_editor: Option, post_command_discovery_hook: Option, }, ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId), ReportPluginCwd(PluginId, PathBuf), SendSigintToPaneId(PaneId), SendSigkillToPaneId(PaneId), GetPanePid { pane_id: PaneId, response_channel: crossbeam::channel::Sender, }, GetPaneRunningCommand { pane_id: PaneId, response_channel: crossbeam::channel::Sender, }, GetPaneCwd { pane_id: PaneId, response_channel: crossbeam::channel::Sender, }, UpdateAndReportCwds, NotifyCwdFromOsc7(u32, PathBuf), Exit, } impl From<&PtyInstruction> for PtyContext { fn from(pty_instruction: &PtyInstruction) -> Self { match *pty_instruction { PtyInstruction::SpawnTerminal(..) => PtyContext::SpawnTerminal, PtyInstruction::OpenInPlaceEditor(..) => PtyContext::OpenInPlaceEditor, PtyInstruction::UpdateActivePane(..) => PtyContext::UpdateActivePane, PtyInstruction::GoToTab(..) => PtyContext::GoToTab, PtyInstruction::ClosePane(..) => PtyContext::ClosePane, PtyInstruction::CloseTab(_) => PtyContext::CloseTab, PtyInstruction::NewTab(..) => PtyContext::NewTab, PtyInstruction::OverrideLayout(..) => PtyContext::OverrideLayout, PtyInstruction::ReRunCommandInPane(..) => PtyContext::ReRunCommandInPane, PtyInstruction::DropToShellInPane { .. } => PtyContext::DropToShellInPane, PtyInstruction::SpawnInPlaceTerminal(..) => PtyContext::SpawnInPlaceTerminal, PtyInstruction::DumpLayout(..) => PtyContext::DumpLayout, PtyInstruction::DumpLayoutToPlugin { .. } => PtyContext::DumpLayoutToPlugin, PtyInstruction::LogLayoutToHd(..) => PtyContext::LogLayoutToHd, PtyInstruction::SaveSessionToDisk { .. } => PtyContext::SaveSessionToDisk, PtyInstruction::FillPluginCwd(..) => PtyContext::FillPluginCwd, PtyInstruction::ListClientsMetadata(..) => PtyContext::ListClientsMetadata, PtyInstruction::Reconfigure { .. } => PtyContext::Reconfigure, PtyInstruction::ListClientsToPlugin(..) => PtyContext::ListClientsToPlugin, PtyInstruction::ReportPluginCwd(..) => PtyContext::ReportPluginCwd, PtyInstruction::SendSigintToPaneId(..) => PtyContext::SendSigintToPaneId, PtyInstruction::SendSigkillToPaneId(..) => PtyContext::SendSigkillToPaneId, PtyInstruction::GetPanePid { .. } => PtyContext::GetPanePid, PtyInstruction::GetPaneRunningCommand { .. } => PtyContext::GetPaneRunningCommand, PtyInstruction::GetPaneCwd { .. } => PtyContext::GetPaneCwd, PtyInstruction::UpdateAndReportCwds => PtyContext::UpdateAndReportCwds, PtyInstruction::NotifyCwdFromOsc7(..) => PtyContext::NotifyCwdFromOsc7, PtyInstruction::Exit => PtyContext::Exit, } } } pub(crate) struct Pty { pub active_panes: HashMap, pub bus: Bus, pub id_to_child_pid: HashMap, // terminal_id => child pid originating_plugins: HashMap, debug_to_file: bool, task_handles: HashMap>, // terminal_id to join-handle default_editor: Option, post_command_discovery_hook: Option, plugin_cwds: HashMap, // plugin_id -> cwd terminal_cwds: HashMap, // terminal_id -> cwd pane_activity_flags: HashMap>, terminal_cmds: HashMap>, terminal_foreground_cmds: HashMap>, } pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box) -> Result<()> { loop { let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Pty((&event).into())); match event { PtyInstruction::SpawnTerminal( terminal_action, name, new_pane_placement, start_suppressed, client_or_tab_index, completion_tx, set_blocking, purpose, ) => { let err_context = || format!("failed to spawn terminal for {:?}", client_or_tab_index); let (hold_on_close, run_command, pane_title, open_file_payload) = match &terminal_action { Some(TerminalAction::RunCommand(run_command)) => ( run_command.hold_on_close, Some(run_command.clone()), if name.is_some() { // User explicitly provided a name — use it regardless // of use_terminal_title name } else if run_command.use_terminal_title { None } else { Some(run_command.to_string()) }, None, ), Some(TerminalAction::OpenFile(open_file_payload)) => { (false, None, name, Some(open_file_payload.clone())) }, _ => (false, None, name, None), }; let invoked_with = match &terminal_action { Some(TerminalAction::RunCommand(run_command)) => { Some(Run::Command(run_command.clone())) }, Some(TerminalAction::OpenFile(payload)) => Some(Run::EditFile( payload.path.clone(), payload.line_number.clone(), payload.cwd.clone(), )), _ => None, }; match pty .spawn_terminal(terminal_action, client_or_tab_index) .with_context(err_context) { Ok((pid, starts_held)) => { let hold_for_command = if starts_held { run_command.clone() } else { None }; // if this command originated in a plugin, we send the plugin back an event // to let it know the command started and which pane_id it has if let Some(originating_plugin) = run_command.and_then(|r| r.originating_plugin) { pty.originating_plugins .insert(pid, originating_plugin.clone()); let update_event = Event::CommandPaneOpened(pid, originating_plugin.context.clone()); pty.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), update_event, )])) .with_context(err_context)?; } if let Some(originating_plugin) = open_file_payload.and_then(|o| o.originating_plugin) { let update_event = Event::EditPaneOpened(pid, originating_plugin.context.clone()); pty.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), update_event, )])) .with_context(err_context)?; } pty.bus .senders .send_to_screen(ScreenInstruction::NewPane( PaneId::Terminal(pid), pane_title, hold_for_command, invoked_with, new_pane_placement, start_suppressed, client_or_tab_index, completion_tx, set_blocking, purpose.clone(), )) .with_context(err_context)?; }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { if hold_on_close { let hold_for_command = None; // we do not hold an "error" pane pty.bus .senders .send_to_screen(ScreenInstruction::NewPane( PaneId::Terminal(*terminal_id), pane_title, hold_for_command, invoked_with, new_pane_placement, start_suppressed, client_or_tab_index, completion_tx, set_blocking, purpose.clone(), )) .with_context(err_context)?; if let Some(run_command) = run_command { send_command_not_found_to_screen( pty.bus.senders.clone(), *terminal_id, run_command.clone(), ) .with_context(err_context)?; } } else { log::error!("Failed to spawn terminal: {:?}", err); pty.close_pane(PaneId::Terminal(*terminal_id)) .with_context(err_context)?; } }, _ => Err::<(), _>(err).non_fatal(), }, } }, PtyInstruction::SpawnInPlaceTerminal( terminal_action, name, close_replaced_pane, client_id_tab_index_or_pane_id, completion_tx, ) => { let err_context = || { format!( "failed to spawn terminal for {:?}", client_id_tab_index_or_pane_id ) }; let (hold_on_close, run_command, pane_title) = match &terminal_action { Some(TerminalAction::RunCommand(run_command)) => ( run_command.hold_on_close, Some(run_command.clone()), if run_command.use_terminal_title { None } else { Some(name.unwrap_or_else(|| run_command.to_string())) }, ), _ => (false, None, name), }; let invoked_with = match &terminal_action { Some(TerminalAction::RunCommand(run_command)) => { Some(Run::Command(run_command.clone())) }, Some(TerminalAction::OpenFile(payload)) => Some(Run::EditFile( payload.path.clone(), payload.line_number.clone(), payload.cwd.clone(), )), _ => None, }; match pty .spawn_terminal(terminal_action, client_id_tab_index_or_pane_id) .with_context(err_context) { Ok((pid, starts_held)) => { let hold_for_command = if starts_held { run_command } else { None }; pty.bus .senders .send_to_screen(ScreenInstruction::ReplacePane( PaneId::Terminal(pid), hold_for_command, pane_title, invoked_with, close_replaced_pane, client_id_tab_index_or_pane_id, completion_tx, )) .with_context(err_context)?; }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { if hold_on_close { let hold_for_command = None; // we do not hold an "error" pane pty.bus .senders .send_to_screen(ScreenInstruction::ReplacePane( PaneId::Terminal(*terminal_id), hold_for_command, pane_title, invoked_with, close_replaced_pane, client_id_tab_index_or_pane_id, completion_tx, )) .with_context(err_context)?; if let Some(run_command) = run_command { send_command_not_found_to_screen( pty.bus.senders.clone(), *terminal_id, run_command.clone(), ) .with_context(err_context)?; } } else { log::error!("Failed to spawn terminal: {:?}", err); pty.close_pane(PaneId::Terminal(*terminal_id)) .with_context(err_context)?; } }, _ => Err::<(), _>(err).non_fatal(), }, } }, PtyInstruction::OpenInPlaceEditor( temp_file, line_number, client_tab_index_or_pane_id, _completion_tx, ) => { let err_context = || format!("failed to open in-place editor for client"); match pty.spawn_terminal( Some(TerminalAction::OpenFile(OpenFilePayload::new( temp_file, line_number, None, ))), client_tab_index_or_pane_id, ) { Ok((pid, _starts_held)) => { pty.bus .senders .send_to_screen(ScreenInstruction::OpenInPlaceEditor( PaneId::Terminal(pid), client_tab_index_or_pane_id, )) .with_context(err_context)?; }, Err(e) => { Err::<(), _>(e).with_context(err_context).non_fatal(); }, } }, PtyInstruction::UpdateActivePane(pane_id, client_id) => { pty.set_active_pane(pane_id, client_id); }, PtyInstruction::GoToTab(tab_index, client_id) => { pty.bus .senders .send_to_screen(ScreenInstruction::GoToTab(tab_index, Some(client_id), None)) .with_context(|| { format!("failed to move client {} to tab {}", client_id, tab_index) })?; }, PtyInstruction::NewTab( cwd, terminal_action, tab_layout, floating_panes_layout, tab_index, plugin_ids, initial_panes, block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, ) => { let err_context = || "failed to open new tab"; log::info!( "PtyInstruction::NewTab: spawning terminals for tab {}", tab_index ); let floating_panes_layout = if floating_panes_layout.is_empty() { layout.new_tab().1 } else { floating_panes_layout }; pty.spawn_terminals_for_layout( cwd, tab_layout.unwrap_or_else(|| layout.new_tab().0), floating_panes_layout, terminal_action.clone(), plugin_ids, initial_panes, tab_index, block_on_first_terminal, should_change_focus_to_new_tab, client_id_and_is_web_client, completion_tx, ) .with_context(err_context)?; }, PtyInstruction::OverrideLayout( cwd, default_shell, tab_layouts_with_plugin_ids, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, completion_tx, ) => { let err_context = || "failed to override layout"; let mut all_tab_results = Vec::new(); // Process each tab for (tab_layout_info, plugin_ids) in tab_layouts_with_plugin_ids { match pty.spawn_terminals_for_layout_override( cwd.clone(), tab_layout_info.tiled_layout, tab_layout_info.floating_layouts, default_shell.clone(), plugin_ids, tab_layout_info.tab_index, tab_layout_info.tab_name, client_id, tab_layout_info.swap_tiled_layouts, tab_layout_info.swap_floating_layouts, retain_existing_terminal_panes, retain_existing_plugin_panes, ) { Ok(tab_result) => all_tab_results.push(tab_result), Err(e) => { log::error!( "Failed to spawn terminals for tab {}: {:?}", tab_layout_info.tab_index, e ); // Continue with other tabs (best-effort approach) }, } } // Send all results back to screen in one message pty.bus .senders .send_to_screen(ScreenInstruction::OverrideLayoutComplete( all_tab_results, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, completion_tx, )) .with_context(err_context)?; }, PtyInstruction::ClosePane(id, _completion_tx) => { pty.close_pane(id) .and_then(|_| { pty.bus .senders .send_to_server(ServerInstruction::UnblockInputThread) }) .with_context(|| format!("failed to close pane {:?}", id))?; }, PtyInstruction::CloseTab(ids) => { pty.close_tab(ids) .and_then(|_| { pty.bus .senders .send_to_server(ServerInstruction::UnblockInputThread) }) .context("failed to close tabs")?; }, PtyInstruction::ReRunCommandInPane(pane_id, run_command, _completion_tx) => { let err_context = || format!("failed to rerun command in pane {:?}", pane_id); match pty .rerun_command_in_pane(pane_id, run_command.clone()) .with_context(err_context) { Ok(..) => {}, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { if run_command.hold_on_close { pty.bus .senders .send_to_screen(ScreenInstruction::PtyBytes( *terminal_id, format!( "Command not found: {}", run_command.command.display() ) .as_bytes() .to_vec(), )) .with_context(err_context)?; pty.bus .senders .send_to_screen(ScreenInstruction::HoldPane( PaneId::Terminal(*terminal_id), Some(2), // exit status run_command, )) .with_context(err_context)?; } }, _ => Err::<(), _>(err).non_fatal(), }, } }, PtyInstruction::DropToShellInPane { pane_id, shell, working_dir, completion_tx: _completion_tx, } => { let err_context = || format!("failed to rerun command in pane {:?}", pane_id); // TODO: get configured default_shell from screen/tab as an option and default to // this otherwise (also look for a place that turns get_default_shell into a // RunCommand, we might have done this before) let run_command = RunCommand { command: shell.unwrap_or_else(|| get_default_shell()), hold_on_close: false, hold_on_start: false, cwd: working_dir, ..Default::default() }; match pty .rerun_command_in_pane(pane_id, run_command.clone()) .with_context(err_context) { Ok(..) => {}, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { if run_command.hold_on_close { pty.bus .senders .send_to_screen(ScreenInstruction::PtyBytes( *terminal_id, format!( "Command not found: {}", run_command.command.display() ) .as_bytes() .to_vec(), )) .with_context(err_context)?; pty.bus .senders .send_to_screen(ScreenInstruction::HoldPane( PaneId::Terminal(*terminal_id), Some(2), // exit status run_command, )) .with_context(err_context)?; } }, _ => Err::<(), _>(err).non_fatal(), }, } }, PtyInstruction::DumpLayout(mut session_layout_metadata, client_id, completion_tx) => { let err_context = || format!("Failed to dump layout"); pty.populate_session_layout_metadata(&mut session_layout_metadata); match session_serialization::serialize_session_layout( session_layout_metadata.into(), ) { Ok((kdl_layout, _pane_contents)) => { pty.bus .senders .send_to_server(ServerInstruction::Log( vec![kdl_layout], client_id, completion_tx, )) .with_context(err_context) .non_fatal(); }, Err(e) => { pty.bus .senders .send_to_server(ServerInstruction::Log( vec![e.to_owned()], client_id, completion_tx, )) .with_context(err_context) .non_fatal(); }, } }, PtyInstruction::ListClientsMetadata( mut session_layout_metadata, client_id, completion_tx, ) => { let err_context = || format!("Failed to dump layout"); pty.populate_session_layout_metadata(&mut session_layout_metadata); pty.bus .senders .send_to_server(ServerInstruction::Log( vec![format!( "{}", session_layout_metadata.list_clients_metadata(), )], client_id, completion_tx, )) .with_context(err_context) .non_fatal(); }, PtyInstruction::DumpLayoutToPlugin { mut session_layout_metadata, plugin_id, response_channel, } => { let err_context = || format!("Failed to dump layout"); pty.populate_session_layout_metadata(&mut session_layout_metadata); pty.bus .senders .send_to_plugin(PluginInstruction::DumpLayoutToPlugin { session_layout_metadata, plugin_id, response_channel, }) .with_context(err_context) .non_fatal(); }, PtyInstruction::ListClientsToPlugin( mut session_layout_metadata, plugin_id, client_id, ) => { let err_context = || format!("Failed to dump layout"); pty.populate_session_layout_metadata(&mut session_layout_metadata); pty.bus .senders .send_to_plugin(PluginInstruction::ListClientsToPlugin( session_layout_metadata, plugin_id, client_id, )) .with_context(err_context) .non_fatal(); }, PtyInstruction::ReportPluginCwd(plugin_id, cwd) => { pty.plugin_cwds.insert(plugin_id, cwd); }, PtyInstruction::LogLayoutToHd(mut session_layout_metadata) => { let err_context = || format!("Failed to dump layout"); pty.populate_session_layout_metadata(&mut session_layout_metadata); if session_layout_metadata.is_dirty() { match session_serialization::serialize_session_layout( session_layout_metadata.into(), ) { Ok(kdl_layout_and_pane_contents) => { pty.bus .senders .send_to_background_jobs(BackgroundJob::ReportLayoutInfo( kdl_layout_and_pane_contents, )) .with_context(err_context)?; }, Err(e) => { log::error!("Failed to log layout to HD: {}", e); }, } } }, PtyInstruction::SaveSessionToDisk { session_name, session_info, mut session_layout_metadata, completion_tx: _completion_tx, // Dropped at end to signal completion } => { pty.populate_session_layout_metadata(&mut session_layout_metadata); match session_serialization::serialize_session_layout( session_layout_metadata.into(), ) { Ok(kdl_and_files) => { write_session_state_to_disk( session_name, session_info, kdl_and_files.clone(), ); // Update session save time for plugin query let timestamp_millis = std::time::SystemTime::now() .duration_since(std::time::UNIX_EPOCH) .unwrap_or_default() .as_millis() as u64; let _ = pty.bus.senders.send_to_plugin( PluginInstruction::UpdateSessionSaveTime(timestamp_millis), ); let _ = pty.bus.senders.send_to_background_jobs( BackgroundJob::ReportLayoutInfo(kdl_and_files), ); }, Err(e) => { log::error!("Failed to serialize layout: {}", e); }, }; }, PtyInstruction::FillPluginCwd( should_float, should_be_open_in_place, close_replaced_pane, pane_title, run, tab_index, pane_id_to_replace, client_id, size, skip_cache, cwd, should_focus_plugin, floating_pane_coordinates, completion_tx, ) => { pty.fill_plugin_cwd( should_float, should_be_open_in_place, close_replaced_pane, pane_title, run, tab_index, pane_id_to_replace, client_id, size, skip_cache, cwd, should_focus_plugin, floating_pane_coordinates, completion_tx, )?; }, PtyInstruction::Reconfigure { default_editor, post_command_discovery_hook, client_id: _, } => { pty.reconfigure(default_editor, post_command_discovery_hook); }, PtyInstruction::SendSigintToPaneId(pane_id) => { pty.send_sigint_to_pane(pane_id); }, PtyInstruction::SendSigkillToPaneId(pane_id) => { pty.send_sigkill_to_pane(pane_id); }, PtyInstruction::GetPanePid { pane_id, response_channel, } => { let response = pty.get_pane_pid(pane_id); let _ = response_channel.send(response); }, PtyInstruction::GetPaneRunningCommand { pane_id, response_channel, } => { let response = pty.get_pane_running_command(pane_id); let _ = response_channel.send(response); }, PtyInstruction::GetPaneCwd { pane_id, response_channel, } => { let response = pty.get_pane_cwd(pane_id); let _ = response_channel.send(response); }, PtyInstruction::UpdateAndReportCwds => { pty.update_and_report_cwds(); }, PtyInstruction::NotifyCwdFromOsc7(terminal_id, path) => { pty.notify_cwd_from_osc7(terminal_id, path); }, PtyInstruction::Exit => break, } } Ok(()) } impl Pty { pub fn new( bus: Bus, debug_to_file: bool, default_editor: Option, post_command_discovery_hook: Option, ) -> Self { Pty { active_panes: HashMap::new(), bus, id_to_child_pid: HashMap::new(), debug_to_file, task_handles: HashMap::new(), default_editor, originating_plugins: HashMap::new(), post_command_discovery_hook, plugin_cwds: HashMap::new(), terminal_cwds: HashMap::new(), pane_activity_flags: HashMap::new(), terminal_cmds: HashMap::new(), terminal_foreground_cmds: HashMap::new(), } } pub fn get_default_terminal( &self, cwd: Option, default_shell: Option, ) -> TerminalAction { match default_shell { Some(mut default_shell) => { if let Some(cwd) = cwd { match default_shell { TerminalAction::RunCommand(ref mut command) => { command.cwd = Some(cwd); }, TerminalAction::OpenFile(ref mut payload) => { match payload.cwd.as_mut() { Some(edit_cwd) => { *edit_cwd = cwd.join(&edit_cwd); }, None => { let _ = payload.cwd.insert(cwd.clone()); }, }; }, } } default_shell }, None => { let shell = get_default_shell(); TerminalAction::RunCommand(RunCommand { args: vec![], command: shell, cwd, // note: this might also be filled by the calling function, eg. spawn_terminal hold_on_close: false, hold_on_start: false, ..Default::default() }) }, } } fn fill_cwd(&self, terminal_action: &mut TerminalAction, client_id: ClientId) { let cwd = match terminal_action { TerminalAction::RunCommand(run_command) => &mut run_command.cwd, TerminalAction::OpenFile(payload) => &mut payload.cwd, }; if cwd.is_none() { *cwd = self .active_panes .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), PaneId::Terminal(id) => { // Try to get CWD from OS, fall back to cached value self.id_to_child_pid .get(id) .and_then(|&pid| { self.bus .os_input .as_ref() .and_then(|input| input.get_cwd(pid)) }) .or_else(|| self.terminal_cwds.get(id).cloned()) }, }) }; } fn fill_cwd_from_pane_id(&self, terminal_action: &mut TerminalAction, pane_id: &PaneId) { let cwd = match terminal_action { TerminalAction::RunCommand(run_command) => &mut run_command.cwd, TerminalAction::OpenFile(payload) => &mut payload.cwd, }; if cwd.is_none() { *cwd = match pane_id { PaneId::Terminal(terminal_pane_id) => { // Try to get CWD from OS, fall back to cached value self.id_to_child_pid .get(terminal_pane_id) .and_then(|&pid| { self.bus .os_input .as_ref() .and_then(|input| input.get_cwd(pid)) }) .or_else(|| self.terminal_cwds.get(terminal_pane_id).cloned()) }, PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), }; }; } pub fn spawn_terminal( &mut self, terminal_action: Option, client_or_tab_index: ClientTabIndexOrPaneId, ) -> Result<(u32, bool)> { // bool is starts_held let err_context = || format!("failed to spawn terminal for {:?}", client_or_tab_index); // returns the terminal id let terminal_action = match client_or_tab_index { ClientTabIndexOrPaneId::ClientId(client_id) | ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) => { let mut terminal_action = terminal_action.unwrap_or_else(|| self.get_default_terminal(None, None)); self.fill_cwd(&mut terminal_action, client_id); terminal_action }, ClientTabIndexOrPaneId::TabIndex(_) | ClientTabIndexOrPaneId::TabIndexNoFocus(_) => { terminal_action.unwrap_or_else(|| self.get_default_terminal(None, None)) }, ClientTabIndexOrPaneId::PaneId(pane_id) => { let mut terminal_action = terminal_action.unwrap_or_else(|| self.get_default_terminal(None, None)); self.fill_cwd_from_pane_id(&mut terminal_action, &pane_id); terminal_action }, }; let (hold_on_start, hold_on_close, originating_command_plugin, originating_edit_plugin) = match &terminal_action { TerminalAction::RunCommand(run_command) => ( run_command.hold_on_start, run_command.hold_on_close, run_command.originating_plugin.clone(), None, ), TerminalAction::OpenFile(open_file_payload) => ( false, false, None, open_file_payload.originating_plugin.clone(), ), }; if hold_on_start { // we don't actually open a terminal in this case, just wait for the user to run it let starts_held = hold_on_start; let terminal_id = self .bus .os_input .as_mut() .context("couldn't get mutable reference to OS interface") .and_then(|os_input| os_input.reserve_terminal_id()) .with_context(err_context)?; return Ok((terminal_id, starts_held)); } let originating_command_plugin = Arc::new(originating_command_plugin.clone()); let originating_edit_plugin = Arc::new(originating_edit_plugin.clone()); let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, command| { // if this command originated in a plugin, we send the plugin an event letting it // know the command exited and some other useful information if let PaneId::Terminal(pane_id) = pane_id { if let Some(originating_command_plugin) = originating_command_plugin.as_ref() { let update_event = Event::CommandPaneExited( pane_id, exit_status, originating_command_plugin.context.clone(), ); let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( Some(originating_command_plugin.plugin_id), Some(originating_command_plugin.client_id), update_event, )])); } if let Some(originating_edit_plugin) = originating_edit_plugin.as_ref() { let update_event = Event::EditPaneExited( pane_id, exit_status, originating_edit_plugin.context.clone(), ); let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( Some(originating_edit_plugin.plugin_id), Some(originating_edit_plugin.client_id), update_event, )])); } } if hold_on_close { let _ = senders.send_to_screen(ScreenInstruction::HoldPane( pane_id, exit_status, command, )); } else { let _ = senders.send_to_screen(ScreenInstruction::ClosePane( pane_id, None, None, exit_status, )); } } }); let (terminal_id, reader, child_pid): (u32, Box, Option) = self .bus .os_input .as_mut() .context("no OS I/O interface found") .and_then(|os_input| { os_input.spawn_terminal(terminal_action, quit_cb, self.default_editor.clone()) }) .with_context(err_context)?; let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let terminal_bytes = async_runtime().spawn({ let err_context = |terminal_id: u32| format!("failed to run async task for terminal {terminal_id}"); let senders = self.bus.senders.clone(); let debug_to_file = self.debug_to_file; let activity_flag = activity_flag.clone(); async move { TerminalBytes::new(terminal_id, reader, senders, debug_to_file, activity_flag) .listen() .await .with_context(|| err_context(terminal_id)) .fatal(); } }); self.task_handles.insert(terminal_id, terminal_bytes); self.pane_activity_flags.insert(terminal_id, activity_flag); if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(terminal_id, child_pid); self.capture_initial_cwd(terminal_id, child_pid); } let starts_held = false; Ok((terminal_id, starts_held)) } pub fn spawn_terminals_for_layout( &mut self, cwd: Option, layout: TiledPaneLayout, floating_panes_layout: Vec, default_shell: Option, plugin_ids: HashMap>, initial_panes: Option>, tab_index: usize, block_on_first_terminal: bool, should_change_focus_to_new_tab: bool, client_id_and_is_web_client: (ClientId, bool), completion_tx: Option, ) -> Result<()> { let err_context = || format!("failed to spawn terminals for layout for"); let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None)); let (client_id, is_web_client) = client_id_and_is_web_client; self.fill_cwd(&mut default_shell, client_id); // Match initial_panes commands to empty slots in the layout let mut layout = layout; if let Some(ref initial_panes_vec) = initial_panes { for initial_pane in initial_panes_vec.iter() { if let CommandOrPlugin::Command(run_command_action) = initial_pane { let run_command: RunCommand = run_command_action.clone().into(); if !layout.replace_next_empty_slot_with_run(Run::Command(run_command)) { log::warn!("More initial_panes provided than empty slots available"); break; } } else if let CommandOrPlugin::File(file_to_open) = initial_pane { if !layout.replace_next_empty_slot_with_run(Run::EditFile( file_to_open.path.clone(), file_to_open.line_number, file_to_open.cwd.clone(), )) { log::warn!("More initial_panes provided than empty slots available"); break; } } // Skip CommandOrPlugin::Plugin entries (already handled by plugin thread) } } let extracted_run_instructions = layout.extract_run_instructions(); let extracted_floating_run_instructions = floating_panes_layout .iter() .filter(|f| !f.already_running) .map(|f| f.run.clone()); let mut new_pane_pids: Vec<(u32, bool, Option, Result>)> = vec![]; // (terminal_id, // starts_held, // run_command, // file_descriptor) let mut new_floating_panes_pids: Vec<( u32, bool, Option, Result>, )> = vec![]; // same // as // new_pane_pids let mut originating_plugins_to_inform = vec![]; for run_instruction in extracted_run_instructions { let originating_plugin = run_instruction.as_ref().and_then(|r| { if let Run::Command(run_command) = r { run_command.originating_plugin.clone() } else { None } }); let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { terminal_id = Some(new_pane_data.0); new_pane_pids.push(new_pane_data); } if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) { originating_plugins_to_inform.push((terminal_id, originating_plugin)); } } for run_instruction in extracted_floating_run_instructions { let originating_plugin = run_instruction.as_ref().and_then(|r| { if let Run::Command(run_command) = r { run_command.originating_plugin.clone() } else { None } }); let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { terminal_id = Some(new_pane_data.0); new_floating_panes_pids.push(new_pane_data); } if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) { originating_plugins_to_inform.push((terminal_id, originating_plugin)); } } // Option should only be Some if the pane starts held let new_tab_pane_ids: Vec<(u32, Option)> = new_pane_pids .iter() .map(|(terminal_id, starts_held, run_command, _)| { if *starts_held { (*terminal_id, run_command.clone()) } else { (*terminal_id, None) } }) .collect(); let new_tab_floating_pane_ids: Vec<(u32, Option)> = new_floating_panes_pids .iter() .map(|(terminal_id, starts_held, run_command, _)| { if *starts_held { (*terminal_id, run_command.clone()) } else { (*terminal_id, None) } }) .collect(); // Track the first terminal_id if blocking is requested let first_initial_pane_terminal_id = if block_on_first_terminal && !new_pane_pids.is_empty() { Some(new_pane_pids[0].0) } else { None }; // Prepare blocking_terminal for ApplyLayout let (direct_completion_tx, blocking_terminal) = if let Some(terminal_id) = first_initial_pane_terminal_id { (None, completion_tx.map(|tx| (terminal_id, tx))) } else { (completion_tx, None) }; log::info!( "spawn_terminals_for_layout: {} tiled + {} floating panes created, sending ApplyLayout", new_tab_pane_ids.len(), new_tab_floating_pane_ids.len() ); self.bus .senders .send_to_screen(ScreenInstruction::ApplyLayout( layout, floating_panes_layout, new_tab_pane_ids.clone(), new_tab_floating_pane_ids.clone(), plugin_ids, tab_index, should_change_focus_to_new_tab, (client_id, is_web_client), direct_completion_tx, blocking_terminal, )) .with_context(err_context)?; let mut terminals_to_start = vec![]; terminals_to_start.append(&mut new_pane_pids); terminals_to_start.append(&mut new_floating_panes_pids); for (terminal_id, starts_held, run_command, reader_result) in terminals_to_start { if starts_held { // we do not run a command or start listening for bytes on held panes continue; } match reader_result { Ok(reader) => { let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let terminal_bytes = async_runtime().spawn({ let senders = self.bus.senders.clone(); let debug_to_file = self.debug_to_file; let activity_flag = activity_flag.clone(); async move { TerminalBytes::new( terminal_id, reader, senders, debug_to_file, activity_flag, ) .listen() .await .context("failed to spawn terminals for layout") .fatal(); } }); self.task_handles.insert(terminal_id, terminal_bytes); self.pane_activity_flags.insert(terminal_id, activity_flag); }, _ => match run_command { Some(run_command) => { if run_command.hold_on_close { send_command_not_found_to_screen( self.bus.senders.clone(), terminal_id, run_command.clone(), ) .with_context(err_context)?; } else { self.close_pane(PaneId::Terminal(terminal_id)) .with_context(err_context)?; } }, None => { self.close_pane(PaneId::Terminal(terminal_id)) .with_context(err_context)?; }, }, } } for (terminal_id, originating_plugin) in originating_plugins_to_inform { self.inform_originating_plugin_of_open(terminal_id, originating_plugin); } Ok(()) } pub fn spawn_terminals_for_layout_override( &mut self, cwd: Option, layout: TiledPaneLayout, floating_panes_layout: Vec, default_shell: Option, plugin_ids: HashMap>, tab_index: usize, tab_name: Option, client_id: ClientId, swap_tiled_layouts: Option>, swap_floating_layouts: Option>, _retain_existing_terminal_panes: bool, _retain_existing_plugin_panes: bool, ) -> Result { let err_context = || format!("failed to spawn terminals for layout for"); let mut default_shell = default_shell.unwrap_or_else(|| self.get_default_terminal(cwd, None)); self.fill_cwd(&mut default_shell, client_id); let extracted_run_instructions = layout.extract_run_instructions(); let extracted_floating_run_instructions = floating_panes_layout .iter() .filter(|f| !f.already_running) .map(|f| f.run.clone()); let mut new_pane_pids: Vec<(u32, bool, Option, Result>)> = vec![]; // (terminal_id, // starts_held, // run_command, // file_descriptor) let mut new_floating_panes_pids: Vec<( u32, bool, Option, Result>, )> = vec![]; // same // as // new_pane_pids let mut originating_plugins_to_inform = vec![]; for run_instruction in extracted_run_instructions { let originating_plugin = run_instruction.as_ref().and_then(|r| { if let Run::Command(run_command) = r { run_command.originating_plugin.clone() } else { None } }); let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { terminal_id = Some(new_pane_data.0); new_pane_pids.push(new_pane_data); } if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) { originating_plugins_to_inform.push((terminal_id, originating_plugin)); } } for run_instruction in extracted_floating_run_instructions { let originating_plugin = run_instruction.as_ref().and_then(|r| { if let Run::Command(run_command) = r { run_command.originating_plugin.clone() } else { None } }); let mut terminal_id = None; if let Some(new_pane_data) = self.apply_run_instruction(run_instruction, default_shell.clone())? { terminal_id = Some(new_pane_data.0); new_floating_panes_pids.push(new_pane_data); } if let (Some(originating_plugin), Some(terminal_id)) = (originating_plugin, terminal_id) { originating_plugins_to_inform.push((terminal_id, originating_plugin)); } } // Option should only be Some if the pane starts held let new_tab_pane_ids: Vec<(u32, Option)> = new_pane_pids .iter() .map(|(terminal_id, starts_held, run_command, _)| { if *starts_held { (*terminal_id, run_command.clone()) } else { (*terminal_id, None) } }) .collect(); let new_tab_floating_pane_ids: Vec<(u32, Option)> = new_floating_panes_pids .iter() .map(|(terminal_id, starts_held, run_command, _)| { if *starts_held { (*terminal_id, run_command.clone()) } else { (*terminal_id, None) } }) .collect(); let tab_result = TabOverrideResult { tab_index, tab_name, tiled_layout: layout, floating_layouts: floating_panes_layout, swap_tiled_layouts, swap_floating_layouts, new_terminal_pids: new_tab_pane_ids, new_floating_pane_pids: new_tab_floating_pane_ids, plugin_ids, }; let mut terminals_to_start = vec![]; terminals_to_start.append(&mut new_pane_pids); terminals_to_start.append(&mut new_floating_panes_pids); for (terminal_id, starts_held, run_command, reader_result) in terminals_to_start { if starts_held { // we do not run a command or start listening for bytes on held panes continue; } match reader_result { Ok(reader) => { let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let terminal_bytes = async_runtime().spawn({ let senders = self.bus.senders.clone(); let debug_to_file = self.debug_to_file; let activity_flag = activity_flag.clone(); async move { TerminalBytes::new( terminal_id, reader, senders, debug_to_file, activity_flag, ) .listen() .await .context("failed to spawn terminals for layout") .fatal(); } }); self.task_handles.insert(terminal_id, terminal_bytes); self.pane_activity_flags.insert(terminal_id, activity_flag); }, _ => match run_command { Some(run_command) => { if run_command.hold_on_close { send_command_not_found_to_screen( self.bus.senders.clone(), terminal_id, run_command.clone(), ) .with_context(err_context)?; } else { self.close_pane(PaneId::Terminal(terminal_id)) .with_context(err_context)?; } }, None => { self.close_pane(PaneId::Terminal(terminal_id)) .with_context(err_context)?; }, }, } } for (terminal_id, originating_plugin) in originating_plugins_to_inform { self.inform_originating_plugin_of_open(terminal_id, originating_plugin); } Ok(tab_result) } fn inform_originating_plugin_of_open( &mut self, terminal_id: u32, originating_plugin: OriginatingPlugin, ) { self.originating_plugins .insert(terminal_id, originating_plugin.clone()); let update_event = Event::CommandPaneOpened(terminal_id, originating_plugin.context); let _ = self .bus .senders .send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), update_event, )])); } fn apply_run_instruction( &mut self, run_instruction: Option, default_shell: TerminalAction, ) -> Result, Result>)>> { // terminal_id, // starts_held, // command // successfully opened let err_context = || format!("failed to apply run instruction"); let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, _command| { let _ = senders.send_to_screen(ScreenInstruction::ClosePane( pane_id, None, None, exit_status, )); } }); let originating_plugin = run_instruction.as_ref().and_then(|r| { if let Run::Command(run_command) = r { run_command.originating_plugin.clone() } else { None } }); match run_instruction { Some(Run::Command(mut command)) => { let starts_held = command.hold_on_start; let hold_on_close = command.hold_on_close; let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, command| { if let PaneId::Terminal(terminal_pane_id) = pane_id { if let Some(originating_plugin) = originating_plugin.as_ref() { let update_event = Event::CommandPaneExited( terminal_pane_id, exit_status, originating_plugin.context.clone(), ); let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), update_event, )])); } } if hold_on_close { let _ = senders.send_to_screen(ScreenInstruction::HoldPane( pane_id, exit_status, command, )); } else { let _ = senders.send_to_screen(ScreenInstruction::ClosePane( pane_id, None, None, exit_status, )); } } }); if command.cwd.is_none() { if let TerminalAction::RunCommand(cmd) = default_shell { command.cwd = cmd.cwd; } } let cmd = TerminalAction::RunCommand(command.clone()); if starts_held { // we don't actually open a terminal in this case, just wait for the user to run it match self .bus .os_input .as_mut() .context("no OS I/O interface found") .with_context(err_context)? .reserve_terminal_id() { Ok(terminal_id) => { Ok(Some(( terminal_id, starts_held, Some(command.clone()), Ok(Box::new(NullAsyncReader) as Box), // placeholder, never used for held panes ))) }, Err(e) => Err(e), } } else { match self .bus .os_input .as_mut() .context("no OS I/O interface found") .with_context(err_context)? .spawn_terminal(cmd, quit_cb, self.default_editor.clone()) .with_context(err_context) { Ok((terminal_id, reader, child_pid)) => { if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(terminal_id, child_pid); self.capture_initial_cwd(terminal_id, child_pid); } Ok(Some(( terminal_id, starts_held, Some(command.clone()), Ok(reader), ))) }, Err(err) => { match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => Ok(Some( (*terminal_id, starts_held, Some(command.clone()), Err(err)), )), _ => Err(err), } }, } } }, Some(Run::Cwd(cwd)) => { let starts_held = false; // we do not hold Cwd panes let shell = self.get_default_terminal(Some(cwd), Some(default_shell.clone())); match self .bus .os_input .as_mut() .context("no OS I/O interface found") .with_context(err_context)? .spawn_terminal(shell, quit_cb, self.default_editor.clone()) .with_context(err_context) { Ok((terminal_id, reader, child_pid)) => { if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(terminal_id, child_pid); self.capture_initial_cwd(terminal_id, child_pid); } Ok(Some((terminal_id, starts_held, None, Ok(reader)))) }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { Ok(Some((*terminal_id, starts_held, None, Err(err)))) }, _ => Err(err), }, } }, Some(Run::EditFile(path_to_file, line_number, cwd)) => { let starts_held = false; // we do not hold edit panes (for now?) match self .bus .os_input .as_mut() .context("no OS I/O interface found") .with_context(err_context)? .spawn_terminal( TerminalAction::OpenFile(OpenFilePayload::new( path_to_file, line_number, cwd, )), quit_cb, self.default_editor.clone(), ) .with_context(err_context) { Ok((terminal_id, reader, child_pid)) => { if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(terminal_id, child_pid); self.capture_initial_cwd(terminal_id, child_pid); } Ok(Some((terminal_id, starts_held, None, Ok(reader)))) }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { Ok(Some((*terminal_id, starts_held, None, Err(err)))) }, _ => Err(err), }, } }, None => { let starts_held = false; match self .bus .os_input .as_mut() .context("no OS I/O interface found") .with_context(err_context)? .spawn_terminal(default_shell.clone(), quit_cb, self.default_editor.clone()) .with_context(err_context) { Ok((terminal_id, reader, child_pid)) => { if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(terminal_id, child_pid); self.capture_initial_cwd(terminal_id, child_pid); } Ok(Some((terminal_id, starts_held, None, Ok(reader)))) }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CommandNotFound { terminal_id, .. }) => { Ok(Some((*terminal_id, starts_held, None, Err(err)))) }, _ => Err(err), }, } }, // Investigate moving plugin loading to here. Some(Run::Plugin(_)) => Ok(None), } } pub fn close_pane(&mut self, id: PaneId) -> Result<()> { let err_context = || format!("failed to close for pane {id:?}"); match id { PaneId::Terminal(id) => { if let Some(handle) = self.task_handles.remove(&id) { handle.abort(); } if let Some(child_pid) = self.id_to_child_pid.remove(&id) { let err_context = || format!("failed to kill child processes for pane {id}"); self.bus .os_input .as_mut() .with_context(err_context) .fatal() .kill(child_pid) .with_context(err_context) .non_fatal(); } self.pane_activity_flags.remove(&id); self.terminal_cwds.remove(&id); self.terminal_cmds.remove(&id); self.terminal_foreground_cmds.remove(&id); self.bus .os_input .as_ref() .context("no OS I/O interface found") .and_then(|os_input| os_input.clear_terminal_id(id)) .with_context(err_context)?; }, PaneId::Plugin(pid) => drop( self.bus .senders .send_to_plugin(PluginInstruction::Unload(pid)), ), } Ok(()) } pub fn close_tab(&mut self, ids: Vec) -> Result<()> { for id in ids { self.close_pane(id) .with_context(|| format!("failed to close tab for pane {id:?}"))?; } Ok(()) } pub fn set_active_pane(&mut self, pane_id: Option, client_id: ClientId) { if let Some(pane_id) = pane_id { self.active_panes.insert(client_id, pane_id); } } pub fn rerun_command_in_pane( &mut self, pane_id: PaneId, mut run_command: RunCommand, ) -> Result<()> { let err_context = || format!("failed to rerun command in pane {:?}", pane_id); match pane_id { PaneId::Terminal(id) => { if let Some(originating_plugins) = self.originating_plugins.get(&id) { run_command.originating_plugin = Some(originating_plugins.clone()); } let _ = self.task_handles.remove(&id); // if all is well, this shouldn't be here let _ = self.id_to_child_pid.remove(&id); // if all is wlel, this shouldn't be here let hold_on_close = run_command.hold_on_close; let originating_plugin = Arc::new(run_command.originating_plugin.clone()); let quit_cb = Box::new({ let senders = self.bus.senders.clone(); move |pane_id, exit_status, command| { if let PaneId::Terminal(pane_id) = pane_id { if let Some(originating_plugin) = originating_plugin.as_ref() { let update_event = Event::CommandPaneExited( pane_id, exit_status, originating_plugin.context.clone(), ); let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), update_event, )])); } } if hold_on_close { let _ = senders.send_to_screen(ScreenInstruction::HoldPane( pane_id, exit_status, command, )); } else { let _ = senders.send_to_screen(ScreenInstruction::ClosePane( pane_id, None, None, exit_status, )); } } }); let (reader, child_pid): (Box, Option) = self .bus .os_input .as_mut() .context("no OS I/O interface found") .and_then(|os_input| { os_input.re_run_command_in_terminal(id, run_command, quit_cb) }) .with_context(err_context)?; let activity_flag = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)); let terminal_bytes = async_runtime().spawn({ let err_context = |pane_id| format!("failed to run async task for pane {pane_id:?}"); let senders = self.bus.senders.clone(); let debug_to_file = self.debug_to_file; let activity_flag = activity_flag.clone(); async move { TerminalBytes::new(id, reader, senders, debug_to_file, activity_flag) .listen() .await .with_context(|| err_context(pane_id)) .fatal(); } }); self.task_handles.insert(id, terminal_bytes); self.pane_activity_flags.insert(id, activity_flag); if let Some(child_pid) = child_pid { self.id_to_child_pid.insert(id, child_pid); self.capture_initial_cwd(id, child_pid); } if let Some(originating_plugin) = self.originating_plugins.get(&id) { self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( Some(originating_plugin.plugin_id), Some(originating_plugin.client_id), Event::CommandPaneReRun(id, originating_plugin.context.clone()), )])) .with_context(err_context)?; } Ok(()) }, _ => Err(anyhow!("cannot respawn plugin panes")).with_context(err_context), } } pub fn populate_session_layout_metadata( &mut self, session_layout_metadata: &mut SessionLayoutMetadata, ) { let terminal_ids = session_layout_metadata.all_terminal_ids(); let mut terminal_ids_to_commands: HashMap> = HashMap::new(); let mut terminal_ids_to_cwds: HashMap = HashMap::new(); let pids: Vec<_> = terminal_ids .iter() .filter_map(|id| self.id_to_child_pid.get(&id)) .copied() .collect(); let (pids_to_cwds, pids_to_cmds) = self .bus .os_input .as_ref() .map(|os_input| os_input.get_cwds(pids)) .unwrap_or_default(); let panes: Vec<(u32, u32)> = terminal_ids .iter() .filter_map(|id| self.id_to_child_pid.get(id).map(|pid| (*id, *pid))) .collect(); let foreground_cmds = self .bus .os_input .as_ref() .map(|os_input| os_input.get_foreground_cmds(&panes, &self.post_command_discovery_hook)) .unwrap_or_default(); for terminal_id in terminal_ids { let process_id = self.id_to_child_pid.get(&terminal_id); let cwd = process_id.and_then(|pid| pids_to_cwds.get(pid)); let cmd_sysinfo = process_id.and_then(|pid| pids_to_cmds.get(pid)); let cmd_foreground = foreground_cmds.get(&terminal_id); if let Some(cmd) = cmd_foreground { terminal_ids_to_commands.insert(terminal_id, cmd.clone()); } else if let Some(cmd) = cmd_sysinfo { terminal_ids_to_commands.insert(terminal_id, cmd.clone()); } if let Some(cwd) = cwd { terminal_ids_to_cwds.insert(terminal_id, cwd.clone()); } } session_layout_metadata.update_default_shell(get_default_shell()); session_layout_metadata.update_terminal_commands(terminal_ids_to_commands); session_layout_metadata.update_terminal_cwds(terminal_ids_to_cwds); session_layout_metadata.update_default_editor(&self.default_editor); session_layout_metadata.detect_editor_panes(); } pub fn fill_plugin_cwd( &self, should_float: Option, should_open_in_place: bool, // should be opened in place close_replaced_pane: bool, // close_replaced_pane pane_title: Option, // pane title mut run: RunPluginOrAlias, tab_index: usize, // tab index pane_id_to_replace: Option, // pane id to replace if this is to be opened "in-place" client_id: ClientId, size: Size, skip_cache: bool, cwd: Option, should_focus_plugin: Option, floating_pane_coordinates: Option, completion_tx: Option, ) -> Result<()> { let get_focused_cwd = || { self.active_panes .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => self.plugin_cwds.get(plugin_id).cloned(), PaneId::Terminal(id) => { // Try to get CWD from OS, fall back to cached value self.id_to_child_pid .get(id) .and_then(|&pid| { self.bus .os_input .as_ref() .and_then(|input| input.get_cwd(pid)) }) .or_else(|| self.terminal_cwds.get(id).cloned()) }, }) }; let cwd = cwd.or_else(get_focused_cwd); let focused_plugin_id = self .active_panes .get(&client_id) .and_then(|pane| match pane { PaneId::Plugin(plugin_id) => Some(*plugin_id), _ => None, }); if let RunPluginOrAlias::Alias(alias) = &mut run { let cwd = get_focused_cwd(); alias.set_caller_cwd_if_not_set(cwd); } self.bus.senders.send_to_plugin(PluginInstruction::Load( should_float, should_open_in_place, close_replaced_pane, pane_title, run, Some(tab_index), pane_id_to_replace, client_id, size, cwd, focused_plugin_id, skip_cache, should_focus_plugin, floating_pane_coordinates, completion_tx, ))?; Ok(()) } fn capture_initial_cwd(&mut self, terminal_id: u32, child_pid: u32) { if let Some(os_input) = self.bus.os_input.as_ref() { if let Some(cwd) = os_input.get_cwd(child_pid) { self.terminal_cwds.insert(terminal_id, cwd); } } } pub fn update_and_report_cwds(&mut self) { use std::sync::atomic::Ordering; let active_terminal_ids: Vec = self .id_to_child_pid .keys() .copied() .filter(|id| { self.pane_activity_flags .get(id) .map(|f| f.swap(false, Ordering::Relaxed)) .unwrap_or(false) }) .collect(); if active_terminal_ids.is_empty() { return; } let pids: Vec<_> = active_terminal_ids .iter() .filter_map(|id| self.id_to_child_pid.get(id)) .copied() .collect(); let (pids_to_cwds, pids_to_cmds) = self .bus .os_input .as_ref() .map(|os_input| os_input.get_cwds(pids)) .unwrap_or_default(); for terminal_id in &active_terminal_ids { let process_id = self.id_to_child_pid.get(terminal_id); let cwd = process_id.and_then(|pid| pids_to_cwds.get(pid)); if let Some(cwd) = cwd { if self.terminal_cwds.get(terminal_id) != Some(cwd) { let pane_id = PaneId::Terminal(*terminal_id); let focused_client_ids: Vec = self .active_panes .iter() .filter(|(_, active_pane)| *active_pane == &pane_id) .map(|(client_id, _)| *client_id) .collect(); let _ = self .bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::CwdChanged(pane_id.into(), cwd.clone(), focused_client_ids), )])); } self.terminal_cwds.insert(*terminal_id, cwd.clone()); } let cmd = process_id.and_then(|pid| pids_to_cmds.get(pid)); if let Some(cmd) = cmd { self.terminal_cmds.insert(*terminal_id, cmd.clone()); } } let panes: Vec<(u32, u32)> = active_terminal_ids .iter() .filter_map(|id| self.id_to_child_pid.get(id).map(|pid| (*id, *pid))) .collect(); let foreground_cmds = self .bus .os_input .as_ref() .map(|os_input| os_input.get_foreground_cmds(&panes, &self.post_command_discovery_hook)) .unwrap_or_default(); for terminal_id in &active_terminal_ids { let foreground_cmd: Vec = foreground_cmds .get(terminal_id) .cloned() .unwrap_or_default(); if self.terminal_foreground_cmds.get(terminal_id) != Some(&foreground_cmd) { let pane_id = PaneId::Terminal(*terminal_id); let focused_client_ids: Vec = self .active_panes .iter() .filter(|(_, active_pane)| *active_pane == &pane_id) .map(|(client_id, _)| *client_id) .collect(); let (command, is_foreground) = if foreground_cmd.is_empty() { let shell_cmd = self .terminal_cmds .get(terminal_id) .cloned() .unwrap_or_default(); (shell_cmd, false) } else { (foreground_cmd.clone(), true) }; let _ = self .bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::CommandChanged( pane_id.into(), command, is_foreground, focused_client_ids, ), )])); self.terminal_foreground_cmds .insert(*terminal_id, foreground_cmd); } } } pub fn reconfigure( &mut self, default_editor: Option, post_command_discovery_hook: Option, ) { self.default_editor = default_editor; self.post_command_discovery_hook = post_command_discovery_hook; } pub fn notify_cwd_from_osc7(&mut self, terminal_id: u32, path: PathBuf) { use std::sync::atomic::Ordering; if self.terminal_cwds.get(&terminal_id) != Some(&path) { let pane_id = PaneId::Terminal(terminal_id); let focused_client_ids: Vec = self .active_panes .iter() .filter(|(_, active_pane)| *active_pane == &pane_id) .map(|(client_id, _)| *client_id) .collect(); let _ = self .bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::CwdChanged(pane_id.into(), path.clone(), focused_client_ids), )])); self.terminal_cwds.insert(terminal_id, path); } if let Some(flag) = self.pane_activity_flags.get(&terminal_id) { flag.store(false, Ordering::Relaxed); } } pub fn send_sigint_to_pane(&self, pane_id: PaneId) { let err_context = || format!("failed to send SIGINT to pane {:?}", pane_id); match pane_id { PaneId::Terminal(terminal_id) => { if let Some(&child_pid) = self.id_to_child_pid.get(&terminal_id) { self.bus .os_input .as_ref() .context("no OS I/O interface found") .and_then(|os_input| os_input.send_sigint(child_pid)) .with_context(err_context) .non_fatal(); } else { log::warn!("Terminal pane {} not found or not running", terminal_id); } }, PaneId::Plugin(plugin_id) => { log::warn!("Cannot send SIGINT to plugin pane {}", plugin_id); }, } } pub fn send_sigkill_to_pane(&self, pane_id: PaneId) { let err_context = || format!("failed to send SIGKILL to pane {:?}", pane_id); match pane_id { PaneId::Terminal(terminal_id) => { if let Some(&child_pid) = self.id_to_child_pid.get(&terminal_id) { self.bus .os_input .as_ref() .context("no OS I/O interface found") .and_then(|os_input| os_input.force_kill(child_pid)) .with_context(err_context) .non_fatal(); } else { log::warn!("Terminal pane {} not found or not running", terminal_id); } }, PaneId::Plugin(plugin_id) => { log::warn!("Cannot send SIGKILL to plugin pane {}", plugin_id); }, } } pub fn get_pane_pid(&self, pane_id: PaneId) -> GetPanePidResponse { match pane_id { PaneId::Terminal(terminal_id) => { if let Some(&child_pid) = self.id_to_child_pid.get(&terminal_id) { GetPanePidResponse::Ok(child_pid as i32) } else { GetPanePidResponse::Err(format!( "Terminal pane {} not found or not running", terminal_id )) } }, PaneId::Plugin(plugin_id) => { GetPanePidResponse::Err(format!("Cannot get PID for plugin pane {}", plugin_id)) }, } } pub fn get_pane_running_command(&self, pane_id: PaneId) -> GetPaneRunningCommandResponse { match pane_id { PaneId::Terminal(terminal_id) => { if let Some(&child_pid) = self.id_to_child_pid.get(&terminal_id) { // Query OS for current running command if let Some(os_input) = self.bus.os_input.as_ref() { // First, try to get the foreground command (e.g., nvim running in bash) let foreground_cmds = os_input.get_foreground_cmds( &[(terminal_id, child_pid)], &self.post_command_discovery_hook, ); let cmd_foreground = foreground_cmds.get(&terminal_id); // If no foreground command, fall back to the shell itself let (_cwds, cmds) = os_input.get_cwds(vec![child_pid]); let cmd_sysinfo = cmds.get(&child_pid); if let Some(command_args) = cmd_foreground { GetPaneRunningCommandResponse::Ok(command_args.clone()) } else if let Some(command_args) = cmd_sysinfo { GetPaneRunningCommandResponse::Ok(command_args.clone()) } else { GetPaneRunningCommandResponse::Err(format!( "Could not retrieve running command for terminal pane {}", terminal_id )) } } else { GetPaneRunningCommandResponse::Err("OS input not available".to_string()) } } else { GetPaneRunningCommandResponse::Err(format!( "Terminal pane {} not found or not running", terminal_id )) } }, PaneId::Plugin(plugin_id) => GetPaneRunningCommandResponse::Err(format!( "Cannot get running command for plugin pane {}", plugin_id )), } } pub fn get_pane_cwd(&self, pane_id: PaneId) -> GetPaneCwdResponse { match pane_id { PaneId::Terminal(terminal_id) => { if let Some(&child_pid) = self.id_to_child_pid.get(&terminal_id) { // Query OS for current working directory if let Some(os_input) = self.bus.os_input.as_ref() { let (cwds, _cmds) = os_input.get_cwds(vec![child_pid]); if let Some(cwd) = cwds.get(&child_pid) { GetPaneCwdResponse::Ok(cwd.clone()) } else { GetPaneCwdResponse::Err(format!( "Could not retrieve CWD for terminal pane {}", terminal_id )) } } else { GetPaneCwdResponse::Err("OS input not available".to_string()) } } else { GetPaneCwdResponse::Err(format!( "Terminal pane {} not found or not running", terminal_id )) } }, PaneId::Plugin(plugin_id) => { GetPaneCwdResponse::Err(format!("Cannot get CWD for plugin pane {}", plugin_id)) }, } } } impl Drop for Pty { fn drop(&mut self) { let child_ids: Vec = self.id_to_child_pid.keys().copied().collect(); for id in child_ids { self.close_pane(PaneId::Terminal(id)) .with_context(|| format!("failed to close pane for pid {id}")) .fatal(); } } } fn send_command_not_found_to_screen( senders: ThreadSenders, terminal_id: u32, run_command: RunCommand, ) -> Result<()> { let err_context = || format!("failed to send command_not_fount for terminal {terminal_id}"); senders .send_to_screen(ScreenInstruction::PtyBytes( terminal_id, format!("Command not found: {}\n\rIf you were including arguments as part of the command, try including them as 'args' instead.", run_command.command.display()) .as_bytes() .to_vec(), )) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::HoldPane( PaneId::Terminal(terminal_id), Some(2), run_command.clone(), )) .with_context(err_context)?; Ok(()) } #[cfg(not(windows))] pub fn get_default_shell() -> PathBuf { PathBuf::from(std::env::var("SHELL").unwrap_or_else(|_| { log::warn!("Cannot read SHELL env, falling back to use /bin/sh"); "/bin/sh".to_string() })) } #[cfg(windows)] pub fn get_default_shell() -> PathBuf { if let Ok(shell) = std::env::var("SHELL") { return PathBuf::from(shell); } PathBuf::from(std::env::var("COMSPEC").unwrap_or_else(|_| { log::warn!("Cannot read SHELL or COMSPEC env, falling back to use cmd.exe"); "cmd.exe".to_string() })) } #[cfg(test)] #[path = "./unit/pty_tests.rs"] mod pty_tests;