Files
zellij/zellij-server/src/pty.rs
T
Aram Drevekenin 91e7297b63 Warning and duplication cleanups (#4857)
* cleanup compilation warnings

* consolidate EditScrollbackRaw into EditScrollback
2026-03-16 10:37:45 +01:00

2260 lines
95 KiB
Rust

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<u8>;
pub type TabIndex = u32;
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum ClientTabIndexOrPaneId {
ClientId(ClientId),
TabIndex(usize),
PaneId(PaneId),
}
/// Instructions related to PTYs (pseudoterminals).
#[derive(Clone, Debug)]
pub enum PtyInstruction {
SpawnTerminal(
Option<TerminalAction>,
Option<String>,
NewPanePlacement,
bool, // start suppressed
ClientTabIndexOrPaneId,
Option<NotificationEnd>, // completion signal
bool, // set_blocking
), // bool (if Some) is
// should_float, String is an optional pane name
OpenInPlaceEditor(
PathBuf,
Option<usize>,
ClientTabIndexOrPaneId,
Option<NotificationEnd>,
), // Option<usize> is the optional line number
UpdateActivePane(Option<PaneId>, ClientId),
GoToTab(TabIndex, ClientId),
NewTab(
Option<PathBuf>,
Option<TerminalAction>,
Option<TiledPaneLayout>,
Vec<FloatingPaneLayout>,
usize, // tab_index
HashMap<RunPluginOrAlias, Vec<u32>>, // plugin_ids
Option<Vec<CommandOrPlugin>>, // initial_panes
bool, // block_on_first_terminal
bool, // should change focus to new tab
(ClientId, bool), // bool -> is_web_client
Option<NotificationEnd>, // completion signal
), // the String is the tab name
OverrideLayout(
Option<PathBuf>, // CWD
Option<TerminalAction>, // Default Shell
Vec<(TabLayoutInfo, HashMap<RunPluginOrAlias, Vec<u32>>)>, // (layout, plugin_ids) per tab
bool, // retain_existing_terminal_panes
bool, // retain_existing_plugin_panes
ClientId,
Option<NotificationEnd>,
),
ClosePane(PaneId, Option<NotificationEnd>),
CloseTab(Vec<PaneId>),
ReRunCommandInPane(PaneId, RunCommand, Option<NotificationEnd>),
DropToShellInPane {
pane_id: PaneId,
shell: Option<PathBuf>,
working_dir: Option<PathBuf>,
completion_tx: Option<NotificationEnd>,
},
SpawnInPlaceTerminal(
Option<TerminalAction>,
Option<String>,
bool, // close replaced pane
ClientTabIndexOrPaneId,
Option<NotificationEnd>, // completion signal
), // String is an optional pane name
DumpLayout(SessionLayoutMetadata, ClientId, Option<NotificationEnd>),
DumpLayoutToPlugin {
session_layout_metadata: SessionLayoutMetadata,
plugin_id: PluginId,
response_channel: crossbeam::channel::Sender<DumpSessionLayoutResponse>,
},
LogLayoutToHd(SessionLayoutMetadata),
SaveSessionToDisk {
session_name: String,
session_info: SessionInfo,
session_layout_metadata: SessionLayoutMetadata,
completion_tx: Option<NotificationEnd>,
},
FillPluginCwd(
Option<bool>, // should float
bool, // should be opened in place
bool, // close_replaced_pane
Option<String>, // pane title
RunPluginOrAlias,
usize, // tab index
Option<PaneId>, // pane id to replace if this is to be opened "in-place"
ClientId,
Size,
bool, // skip cache
Option<PathBuf>, // if Some, will not fill cwd but just forward the message
Option<bool>, // should focus plugin
Option<FloatingPaneCoordinates>,
Option<NotificationEnd>,
),
ListClientsMetadata(SessionLayoutMetadata, ClientId, Option<NotificationEnd>),
Reconfigure {
client_id: ClientId,
default_editor: Option<PathBuf>,
post_command_discovery_hook: Option<String>,
},
ListClientsToPlugin(SessionLayoutMetadata, PluginId, ClientId),
ReportPluginCwd(PluginId, PathBuf),
SendSigintToPaneId(PaneId),
SendSigkillToPaneId(PaneId),
GetPanePid {
pane_id: PaneId,
response_channel: crossbeam::channel::Sender<GetPanePidResponse>,
},
GetPaneRunningCommand {
pane_id: PaneId,
response_channel: crossbeam::channel::Sender<GetPaneRunningCommandResponse>,
},
GetPaneCwd {
pane_id: PaneId,
response_channel: crossbeam::channel::Sender<GetPaneCwdResponse>,
},
UpdateAndReportCwds,
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::Exit => PtyContext::Exit,
}
}
}
pub(crate) struct Pty {
pub active_panes: HashMap<ClientId, PaneId>,
pub bus: Bus<PtyInstruction>,
pub id_to_child_pid: HashMap<u32, u32>, // terminal_id => child pid
originating_plugins: HashMap<u32, OriginatingPlugin>,
debug_to_file: bool,
task_handles: HashMap<u32, JoinHandle<()>>, // terminal_id to join-handle
default_editor: Option<PathBuf>,
post_command_discovery_hook: Option<String>,
plugin_cwds: HashMap<u32, PathBuf>, // plugin_id -> cwd
terminal_cwds: HashMap<u32, PathBuf>, // terminal_id -> cwd
}
pub(crate) fn pty_thread_main(mut pty: Pty, layout: Box<Layout>) -> 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,
) => {
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 run_command.use_terminal_title {
None
} else {
Some(name.unwrap_or_else(|| 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,
))
.with_context(err_context)?;
},
Err(err) => match err.downcast_ref::<ZellijError>() {
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,
))
.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::<ZellijError>() {
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::<ZellijError>() {
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::<ZellijError>() {
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::Exit => break,
}
}
Ok(())
}
impl Pty {
pub fn new(
bus: Bus<PtyInstruction>,
debug_to_file: bool,
default_editor: Option<PathBuf>,
post_command_discovery_hook: Option<String>,
) -> 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(),
}
}
pub fn get_default_terminal(
&self,
cwd: Option<PathBuf>,
default_shell: Option<TerminalAction>,
) -> 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<TerminalAction>,
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) => {
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(_) => {
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<dyn AsyncReader>, Option<u32>) = 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 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;
async move {
TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
.listen()
.await
.with_context(|| err_context(terminal_id))
.fatal();
}
});
self.task_handles.insert(terminal_id, terminal_bytes);
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<PathBuf>,
layout: TiledPaneLayout,
floating_panes_layout: Vec<FloatingPaneLayout>,
default_shell: Option<TerminalAction>,
plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
initial_panes: Option<Vec<CommandOrPlugin>>,
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<NotificationEnd>,
) -> 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<RunCommand>, Result<Box<dyn AsyncReader>>)> =
vec![]; // (terminal_id,
// starts_held,
// run_command,
// file_descriptor)
let mut new_floating_panes_pids: Vec<(
u32,
bool,
Option<RunCommand>,
Result<Box<dyn AsyncReader>>,
)> = 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<RunCommand> should only be Some if the pane starts held
let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = 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<RunCommand>)> = 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 terminal_bytes = async_runtime().spawn({
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
async move {
TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
.listen()
.await
.context("failed to spawn terminals for layout")
.fatal();
}
});
self.task_handles.insert(terminal_id, terminal_bytes);
},
_ => 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<PathBuf>,
layout: TiledPaneLayout,
floating_panes_layout: Vec<FloatingPaneLayout>,
default_shell: Option<TerminalAction>,
plugin_ids: HashMap<RunPluginOrAlias, Vec<u32>>,
tab_index: usize,
tab_name: Option<String>,
client_id: ClientId,
swap_tiled_layouts: Option<Vec<SwapTiledLayout>>,
swap_floating_layouts: Option<Vec<SwapFloatingLayout>>,
_retain_existing_terminal_panes: bool,
_retain_existing_plugin_panes: bool,
) -> Result<TabOverrideResult> {
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<RunCommand>, Result<Box<dyn AsyncReader>>)> =
vec![]; // (terminal_id,
// starts_held,
// run_command,
// file_descriptor)
let mut new_floating_panes_pids: Vec<(
u32,
bool,
Option<RunCommand>,
Result<Box<dyn AsyncReader>>,
)> = 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<RunCommand> should only be Some if the pane starts held
let new_tab_pane_ids: Vec<(u32, Option<RunCommand>)> = 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<RunCommand>)> = 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 terminal_bytes = async_runtime().spawn({
let senders = self.bus.senders.clone();
let debug_to_file = self.debug_to_file;
async move {
TerminalBytes::new(terminal_id, reader, senders, debug_to_file)
.listen()
.await
.context("failed to spawn terminals for layout")
.fatal();
}
});
self.task_handles.insert(terminal_id, terminal_bytes);
},
_ => 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<Run>,
default_shell: TerminalAction,
) -> Result<Option<(u32, bool, Option<RunCommand>, Result<Box<dyn AsyncReader>>)>> {
// 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<dyn AsyncReader>), // 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::<ZellijError>() {
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::<ZellijError>() {
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::<ZellijError>() {
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::<ZellijError>() {
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) => {
self.task_handles.remove(&id);
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.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<PaneId>) -> 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<PaneId>, 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<dyn AsyncReader>, Option<u32>) = 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 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;
async move {
TerminalBytes::new(id, reader, senders, debug_to_file)
.listen()
.await
.with_context(|| err_context(pane_id))
.fatal();
}
});
self.task_handles.insert(id, terminal_bytes);
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<u32, Vec<String>> = HashMap::new();
let mut terminal_ids_to_cwds: HashMap<u32, PathBuf> = 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 ppids_to_cmds = self
.bus
.os_input
.as_ref()
.map(|os_input| os_input.get_all_cmds_by_ppid(&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_ps = process_id.and_then(|pid| ppids_to_cmds.get(&format!("{}", pid)));
if let Some(cmd) = cmd_ps {
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<bool>,
should_open_in_place: bool, // should be opened in place
close_replaced_pane: bool, // close_replaced_pane
pane_title: Option<String>, // pane title
mut run: RunPluginOrAlias,
tab_index: usize, // tab index
pane_id_to_replace: Option<PaneId>, // pane id to replace if this is to be opened "in-place"
client_id: ClientId,
size: Size,
skip_cache: bool,
cwd: Option<PathBuf>,
should_focus_plugin: Option<bool>,
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
completion_tx: Option<NotificationEnd>,
) -> 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) {
let terminal_ids: Vec<u32> = self.id_to_child_pid.keys().copied().collect();
let pids: Vec<_> = terminal_ids
.iter()
.filter_map(|id| self.id_to_child_pid.get(&id))
.copied()
.collect();
let (pids_to_cwds, _) = self
.bus
.os_input
.as_ref()
.map(|os_input| os_input.get_cwds(pids))
.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));
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<ClientId> = 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());
}
}
}
pub fn reconfigure(
&mut self,
default_editor: Option<PathBuf>,
post_command_discovery_hook: Option<String>,
) {
self.default_editor = default_editor;
self.post_command_discovery_hook = post_command_discovery_hook;
}
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 child process command (e.g., nvim running in bash)
let ppids_to_cmds =
os_input.get_all_cmds_by_ppid(&self.post_command_discovery_hook);
let cmd_ps = ppids_to_cmds.get(&format!("{}", child_pid));
// If no child process, fall back to parent process (e.g., 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_ps {
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<u32> = 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()
}))
}