use std::collections::{BTreeMap, HashSet, VecDeque}; use std::sync::{Arc, RwLock}; use tokio::sync::oneshot; use crate::global_async_runtime::get_tokio_runtime; use crate::thread_bus::ThreadSenders; use crate::{ os_input_output::ServerOsApi, panes::PaneId, plugins::PluginInstruction, pty::{ClientTabIndexOrPaneId, PtyInstruction}, screen::ScreenInstruction, ServerInstruction, SessionMetaData, SessionState, }; use std::thread; use std::time::Duration; use uuid::Uuid; use zellij_utils::{ channels::SenderWithContext, data::{ BareKey, ConnectToSession, Direction, Event, InputMode, KeyModifier, ListPanesResponse, ListTabsResponse, NewPanePlacement, PaneListEntry, ResizeStrategy, TabInfo, UnblockCondition, }, envs, errors::prelude::*, input::{ actions::{Action, SearchDirection, SearchOption}, command::TerminalAction, }, ipc::{ClientToServerMsg, ExitReason, IpcReceiverWithContext, ResizeCause, ServerToClientMsg}, }; use crate::ClientId; const ACTION_COMPLETION_TIMEOUT: Duration = Duration::from_secs(1); #[derive(Debug, Clone)] pub struct ActionCompletionResult { pub exit_status: Option, pub affected_pane_id: Option, pub affected_tab_id: Option, pub error_message: Option, pub stdout_message: Option, } pub fn wait_for_action_completion( receiver: oneshot::Receiver, action_name: &str, wait_forever: bool, ) -> ActionCompletionResult { let runtime = get_tokio_runtime(); if wait_forever { runtime.block_on(async { match receiver.await { Ok(result) => result, Err(e) => { log::error!("Failed to wait for action {}: {}", action_name, e); ActionCompletionResult { exit_status: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None, } }, } }) } else { match runtime .block_on(async { tokio::time::timeout(ACTION_COMPLETION_TIMEOUT, receiver).await }) { Ok(Ok(result)) => result, Err(_) | Ok(Err(_)) => { log::error!( "Action {} did not complete within {:?} timeout", action_name, ACTION_COMPLETION_TIMEOUT ); ActionCompletionResult { exit_status: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None, } }, } } } // This is used to wait for actions that span multiple threads until they logically end // dropping this struct sends a notification through the oneshot channel to the receiver, letting // it know the action is ended and thus releasing it // // Note: Cloning this struct DOES NOT clone that internal receiver, it only implements Clone so // that it can be included in various other larger structs - DO NOT RELY ON CLONING IT! #[derive(Debug)] pub struct NotificationEnd { channel: Option>, exit_status: Option, unblock_condition: Option, affected_pane_id: Option, // optional payload of the pane id affected by this action affected_tab_id: Option, // optional payload of the tab id affected by this action error_message: Option, stdout_message: Option, } impl Clone for NotificationEnd { fn clone(&self) -> Self { // Always clone as None - only the original holder should signal completion NotificationEnd { channel: None, exit_status: self.exit_status, unblock_condition: self.unblock_condition, affected_pane_id: self.affected_pane_id, affected_tab_id: self.affected_tab_id, error_message: self.error_message.clone(), stdout_message: self.stdout_message.clone(), } } } impl NotificationEnd { pub fn new(sender: oneshot::Sender) -> Self { NotificationEnd { channel: Some(sender), exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None, } } pub fn new_with_condition( sender: oneshot::Sender, unblock_condition: UnblockCondition, ) -> Self { NotificationEnd { channel: Some(sender), exit_status: None, unblock_condition: Some(unblock_condition), affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None, } } pub fn set_exit_status(&mut self, exit_status: i32) { self.exit_status = Some(exit_status); } pub fn set_affected_pane_id(&mut self, pane_id: PaneId) { self.affected_pane_id = Some(pane_id); } pub fn set_affected_tab_id(&mut self, tab_id: usize) { self.affected_tab_id = Some(tab_id); } pub fn set_error_message(&mut self, message: String) { self.error_message = Some(message); } pub fn set_stdout_message(&mut self, message: String) { self.stdout_message = Some(message); } pub fn unblock_condition(&self) -> Option { self.unblock_condition } } impl Drop for NotificationEnd { fn drop(&mut self) { if let Some(tx) = self.channel.take() { let result = ActionCompletionResult { exit_status: self.exit_status, affected_pane_id: self.affected_pane_id, affected_tab_id: self.affected_tab_id, error_message: self.error_message.take(), stdout_message: self.stdout_message.take(), }; let _ = tx.send(result); } } } // `route_action` must not borrow from the `session_data` read guard. // otherwise blocking-CLI actions // (`wait_forever=true`) park this function while still holding the guard, // deadlocking concurrent `session_data.write()`s. fn new_pane_routing( no_focus: bool, near_current_pane: bool, tab_id: Option, pane_id: Option, client_id: ClientId, ) -> ClientTabIndexOrPaneId { if let Some(tab_id) = tab_id { if no_focus { ClientTabIndexOrPaneId::TabIndexNoFocus(tab_id) } else { ClientTabIndexOrPaneId::TabIndex(tab_id) } } else if (no_focus || near_current_pane) && pane_id.is_some() { ClientTabIndexOrPaneId::PaneId(pane_id.unwrap()) } else if no_focus { ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) } else { ClientTabIndexOrPaneId::ClientId(client_id) } } pub(crate) fn route_action( action: Action, client_id: ClientId, cli_client_id: Option, pane_id: Option, senders: ThreadSenders, default_shell: Option, mut seen_cli_pipes: Option<&mut HashSet>, default_mode: InputMode, os_input: Option>, ) -> Result<(bool, Option)> { let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); let action_name = action.to_string(); if !action.is_mouse_action() { // mouse actions should only send InputReceived to plugins // if they do not result in text being marked, this is handled in Tab senders .send_to_plugin(PluginInstruction::Update(vec![( None, Some(client_id), Event::InputReceived, )])) .with_context(err_context)?; } // we use this oneshot channel to wait for an action to be "logically" // done, meaning that it traveled through all the threads it needed to travel through and the // app has confirmed that it is complete. Once this happens, we get a signal through the // wait_for_action_completion call below (or timeout after 1 second) and release this thread, // allowing the client to produce another action without risking races let (completion_tx, completion_rx) = oneshot::channel(); let mut wait_forever = false; match action { Action::ToggleTab => { senders .send_to_screen(ScreenInstruction::ToggleTab( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Write { key_with_modifier, bytes: raw_bytes, is_kitty_keyboard_protocol, } => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::WriteCharacter( key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::WriteChars { chars } => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; let chars = chars.into_bytes(); senders .send_to_screen(ScreenInstruction::WriteCharacter( None, chars, false, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::WriteToPaneId { bytes, pane_id } => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::WriteToPaneId( bytes, pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::WriteCharsToPaneId { chars, pane_id } => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; let bytes = chars.into_bytes(); senders .send_to_screen(ScreenInstruction::WriteToPaneId( bytes, pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Paste { chars, pane_id } => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; let bytes = chars.into_bytes(); senders .send_to_screen(ScreenInstruction::Paste( bytes, pane_id.map(|p| p.into()), client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::SetPaneColor { pane_id, fg, bg } => { senders .send_to_screen(ScreenInstruction::SetPaneColor( pane_id.into(), fg, bg, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::SwitchToMode { input_mode } => { senders .send_to_server(ServerInstruction::ChangeMode( client_id, input_mode, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Resize { resize, direction } => { let screen_instr = ScreenInstruction::Resize( client_id, ResizeStrategy::new(resize, direction), Some(NotificationEnd::new(completion_tx)), ); senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::SwitchFocus => { senders .send_to_screen(ScreenInstruction::SwitchFocus( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusNextPane => { senders .send_to_screen(ScreenInstruction::FocusNextPane( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusPreviousPane => { senders .send_to_screen(ScreenInstruction::FocusPreviousPane( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusPaneByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( pane_id.into(), true, // should_float_if_hidden false, // should_be_in_place_if_hidden client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusLastPane => { senders .send_to_screen(ScreenInstruction::FocusLastPane( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::MoveFocus { direction } => { let notification_end = Some(NotificationEnd::new(completion_tx)); let screen_instr = match direction { Direction::Left => ScreenInstruction::MoveFocusLeft(client_id, notification_end), Direction::Right => ScreenInstruction::MoveFocusRight(client_id, notification_end), Direction::Up => ScreenInstruction::MoveFocusUp(client_id, notification_end), Direction::Down => ScreenInstruction::MoveFocusDown(client_id, notification_end), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MoveFocusOrTab { direction } => { let notification_end = Some(NotificationEnd::new(completion_tx)); let screen_instr = match direction { Direction::Left => { ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id, notification_end) }, Direction::Right => { ScreenInstruction::MoveFocusRightOrNextTab(client_id, notification_end) }, Direction::Up => ScreenInstruction::SwitchTabNext(client_id, notification_end), Direction::Down => ScreenInstruction::SwitchTabPrev(client_id, notification_end), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MovePane { direction } => { let notification_end = Some(NotificationEnd::new(completion_tx)); let screen_instr = match direction { Some(Direction::Left) => { ScreenInstruction::MovePaneLeft(client_id, notification_end) }, Some(Direction::Right) => { ScreenInstruction::MovePaneRight(client_id, notification_end) }, Some(Direction::Up) => ScreenInstruction::MovePaneUp(client_id, notification_end), Some(Direction::Down) => { ScreenInstruction::MovePaneDown(client_id, notification_end) }, None => ScreenInstruction::MovePane(client_id, notification_end), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MovePaneBackwards => { senders .send_to_screen(ScreenInstruction::MovePaneBackwards( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ClearScreen => { senders .send_to_screen(ScreenInstruction::ClearScreen( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::DumpScreen { file_path, include_scrollback, pane_id, ansi, } => { senders .send_to_screen(ScreenInstruction::DumpScreen( file_path, client_id, include_scrollback, pane_id.map(|p| p.into()), Some(NotificationEnd::new(completion_tx)), cli_client_id, ansi, )) .with_context(err_context)?; }, Action::DumpLayout => { let default_shell = match default_shell { Some(TerminalAction::RunCommand(run_command)) => Some(run_command.command), _ => None, }; senders .send_to_screen(ScreenInstruction::DumpLayout( default_shell, cli_client_id.unwrap_or(client_id), // we prefer the cli client here because // this is a cli query and we want to print // it there Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::SaveSession => { senders .send_to_screen(ScreenInstruction::SaveSession( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::EditScrollback { ansi } => { senders .send_to_screen(ScreenInstruction::EditScrollback( client_id, ansi, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollUp => { senders .send_to_screen(ScreenInstruction::ScrollUp( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollUpAt { position } => { senders .send_to_screen(ScreenInstruction::ScrollUpAt( position, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollDown => { senders .send_to_screen(ScreenInstruction::ScrollDown( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollDownAt { position } => { senders .send_to_screen(ScreenInstruction::ScrollDownAt( position, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollToBottom => { senders .send_to_screen(ScreenInstruction::ScrollToBottom( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollToTop => { senders .send_to_screen(ScreenInstruction::ScrollToTop( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PageScrollUp => { senders .send_to_screen(ScreenInstruction::PageScrollUp( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PageScrollDown => { senders .send_to_screen(ScreenInstruction::PageScrollDown( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::HalfPageScrollUp => { senders .send_to_screen(ScreenInstruction::HalfPageScrollUp( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::HalfPageScrollDown => { senders .send_to_screen(ScreenInstruction::HalfPageScrollDown( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFocusFullscreen => { senders .send_to_screen(ScreenInstruction::ToggleActiveTerminalFullscreen( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFocusNoUiFullscreen => { senders .send_to_screen(ScreenInstruction::ToggleActiveTerminalNoUiFullscreen( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TogglePaneFrames => { senders .send_to_screen(ScreenInstruction::TogglePaneFrames(Some( NotificationEnd::new(completion_tx), ))) .with_context(err_context)?; }, Action::SetPaneFrameStyle(pane_frame_style) => { senders .send_to_screen(ScreenInstruction::SetPaneFrameStyle( pane_frame_style, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NewPane { direction, pane_name, start_suppressed, } => { let shell = default_shell.clone(); let new_pane_placement = match direction { Some(direction) => NewPanePlacement::Tiled { direction: Some(direction), borderless: None, }, None => NewPanePlacement::NoPreference { borderless: None }, }; senders .send_to_pty(PtyInstruction::SpawnTerminal( shell, pane_name, new_pane_placement, start_suppressed, ClientTabIndexOrPaneId::ClientId(client_id), Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) .with_context(err_context)?; }, Action::NewBlockingPane { placement, pane_name, command, unblock_condition, near_current_pane, no_focus, tab_id, } => { let command = command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let set_pane_blocking = true; let notification_end = if let Some(condition) = unblock_condition { Some(NotificationEnd::new_with_condition( completion_tx, condition, )) } else { Some(NotificationEnd::new(completion_tx)) }; // we prefer the pane id provided by the action explicitly over the one that originated // it (this might be a bit misleading with "near_current_pane", but it's still the // right behavior - in the latter case, if the originator does not wish for this // behavior, they should not provide pane // inside the placement, but rather have the current pane id be picked up instead) let pane_id = match placement { NewPanePlacement::Stacked { pane_id_to_stack_under, .. } => pane_id_to_stack_under.map(|p| p.into()).or(pane_id), NewPanePlacement::InPlace { pane_id_to_replace, .. } => pane_id_to_replace.map(|p| p.into()).or(pane_id), _ => pane_id, }; let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, tab_id, pane_id, client_id); senders .send_to_pty(PtyInstruction::SpawnTerminal( command, pane_name, placement, false, client_tab_index_or_paneid, notification_end, set_pane_blocking, )) .with_context(err_context)?; wait_forever = true; }, Action::EditFile { payload: open_file_payload, direction: split_direction, floating: should_float, in_place: should_open_in_place, close_replaced_pane, start_suppressed, coordinates: floating_pane_coordinates, near_current_pane, no_focus, tab_id, } => { let title = format!("Editing: {}", open_file_payload.path.display()); let open_file = TerminalAction::OpenFile(open_file_payload); let pty_instr = if should_open_in_place { let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, tab_id, pane_id, client_id); PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), close_replaced_pane, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), ) } else { let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, tab_id, pane_id, client_id); PtyInstruction::SpawnTerminal( Some(open_file), Some(title), if should_float { NewPanePlacement::Floating(floating_pane_coordinates) } else { NewPanePlacement::Tiled { direction: split_direction, borderless: None, } }, start_suppressed, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking ) }; senders.send_to_pty(pty_instr).with_context(err_context)?; }, Action::SwitchModeForAllClients { input_mode } => { // ModeUpdate broadcast is handled by the screen thread via // change_mode_for_all_clients() -> change_mode() -> update_input_modes() senders .send_to_server(ServerInstruction::ChangeModeForAllClients(input_mode)) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::ChangeModeForAllClients( input_mode, Some(default_mode), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NewFloatingPane { command: run_command, pane_name: name, coordinates: floating_pane_coordinates, near_current_pane, no_focus, tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, tab_id, pane_id, client_id); senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Floating(floating_pane_coordinates), false, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) .with_context(err_context)?; }, Action::NewInPlacePane { command: run_command, pane_name: name, near_current_pane, no_focus, pane_id_to_replace, close_replaced_pane, tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let explicit_pane_id_to_replace: Option = pane_id_to_replace .and_then(|pane_id_to_replace| pane_id_to_replace.try_into().ok()); let pane_id = explicit_pane_id_to_replace.or(pane_id); let client_tab_index_or_paneid = if let Some(tab_id) = tab_id { if no_focus { ClientTabIndexOrPaneId::TabIndexNoFocus(tab_id) } else { ClientTabIndexOrPaneId::TabIndex(tab_id) } } else if let Some(pane_id) = pane_id .filter(|_| explicit_pane_id_to_replace.is_some() || near_current_pane || no_focus) { ClientTabIndexOrPaneId::PaneId(pane_id) } else if no_focus { ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) } else { ClientTabIndexOrPaneId::ClientId(client_id) }; senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, close_replaced_pane, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NewStackedPane { command: run_command, pane_name: name, near_current_pane, no_focus, tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let (pane_placement, client_tab_index_or_paneid) = if let Some(tab_id) = tab_id { ( NewPanePlacement::Stacked { pane_id_to_stack_under: None, borderless: None, }, if no_focus { ClientTabIndexOrPaneId::TabIndexNoFocus(tab_id) } else { ClientTabIndexOrPaneId::TabIndex(tab_id) }, ) } else if (no_focus || near_current_pane) && pane_id.is_some() { let pane_id = pane_id.unwrap(); ( NewPanePlacement::Stacked { pane_id_to_stack_under: Some(pane_id.into()), borderless: None, }, ClientTabIndexOrPaneId::PaneId(pane_id), ) } else { ( NewPanePlacement::Stacked { pane_id_to_stack_under: None, borderless: None, }, if no_focus { ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) } else { ClientTabIndexOrPaneId::ClientId(client_id) }, ) }; senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, pane_placement, false, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) .with_context(err_context)?; }, Action::NewTiledPane { direction, command: run_command, pane_name: name, near_current_pane, no_focus, borderless, tab_id, } => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, tab_id, pane_id, client_id); senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Tiled { direction, borderless, }, false, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) .with_context(err_context)?; }, Action::TogglePaneEmbedOrFloating => { senders .send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloating( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFloatingPanes => { senders .send_to_screen(ScreenInstruction::ToggleFloatingPanes( client_id, default_shell.clone(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PaneNameInput { input } => { senders .send_to_screen(ScreenInstruction::UpdatePaneName( input, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::UndoRenamePane => { senders .send_to_screen(ScreenInstruction::UndoRenamePane( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Run { command, near_current_pane, no_focus, } => { let run_cmd = Some(TerminalAction::RunCommand(command.clone().into())); let client_tab_index_or_paneid = new_pane_routing(no_focus, near_current_pane, None, pane_id, client_id); senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, None, NewPanePlacement::Tiled { direction: command.direction, borderless: None, }, false, client_tab_index_or_paneid, Some(NotificationEnd::new(completion_tx)), false, // set_blocking )) .with_context(err_context)?; }, Action::CloseFocus => { senders .send_to_screen(ScreenInstruction::CloseFocusedPane( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NewTab { tiled_layout: tab_layout, floating_layouts: floating_panes_layout, swap_tiled_layouts, swap_floating_layouts, tab_name, should_change_focus_to_new_tab, cwd, initial_panes, first_pane_unblock_condition, } => { let shell = default_shell.clone(); let is_web_client = false; // actions cannot be initiated directly from the web // Construct completion_tx conditionally let (completion_tx, block_on_first_terminal) = if let Some(condition) = first_pane_unblock_condition { let notification = NotificationEnd::new_with_condition(completion_tx, condition); wait_forever = true; (notification, true) } else { (NotificationEnd::new(completion_tx), false) }; senders .send_to_screen(ScreenInstruction::NewTab( cwd, shell, tab_layout, floating_panes_layout, tab_name, (swap_tiled_layouts, swap_floating_layouts), initial_panes, block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), Some(completion_tx), )) .with_context(err_context)?; }, Action::GoToNextTab => { senders .send_to_screen(ScreenInstruction::SwitchTabNext( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::GoToPreviousTab => { senders .send_to_screen(ScreenInstruction::SwitchTabPrev( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleActiveSyncTab => { senders .send_to_screen(ScreenInstruction::ToggleActiveSyncTab( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::CloseTab => { senders .send_to_screen(ScreenInstruction::CloseTab( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::GoToTab { index } => { senders .send_to_screen(ScreenInstruction::GoToTab( index, Some(client_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::GoToTabName { name, create } => { let shell = default_shell.clone(); senders .send_to_screen(ScreenInstruction::GoToTabName( name, shell, create, Some(client_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TabNameInput { input } => { senders .send_to_screen(ScreenInstruction::UpdateTabName( input, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::UndoRenameTab => { senders .send_to_screen(ScreenInstruction::UndoRenameTab( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::GoToTabById { id } => { senders .send_to_screen(ScreenInstruction::GoToTabWithId( id as usize, Some(client_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::CloseTabById { id } => { senders .send_to_screen(ScreenInstruction::CloseTabWithId( id as usize, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenameTabById { id, name } => { senders .send_to_screen(ScreenInstruction::RenameTabWithId( id as usize, name.into_bytes(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::MoveTab { direction } => { let screen_instr = match direction { Direction::Left | Direction::Up => ScreenInstruction::MoveTabLeft( client_id, Some(NotificationEnd::new(completion_tx)), ), Direction::Right | Direction::Down => ScreenInstruction::MoveTabRight( client_id, Some(NotificationEnd::new(completion_tx)), ), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::Quit => { senders .send_to_server(ServerInstruction::ClientExit( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; should_break = true; }, Action::Detach => { senders .send_to_server(ServerInstruction::DetachSession( vec![client_id], Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; should_break = true; }, Action::SetDarkTheme => { senders .send_to_screen(ScreenInstruction::SetDarkTheme(Some(NotificationEnd::new( completion_tx, )))) .with_context(err_context)?; }, Action::SetLightTheme => { senders .send_to_screen(ScreenInstruction::SetLightTheme(Some( NotificationEnd::new(completion_tx), ))) .with_context(err_context)?; }, Action::ToggleTheme => { senders .send_to_screen(ScreenInstruction::ToggleTheme(Some(NotificationEnd::new( completion_tx, )))) .with_context(err_context)?; }, Action::SwitchSession { name, tab_position, pane_id, layout, cwd, } => { let current_session_name = envs::get_session_name().unwrap_or_else(|_| String::new()); if name != current_session_name { let connect_to_session = ConnectToSession { name: Some(name.clone()), tab_position: tab_position.clone(), pane_id: pane_id.clone(), layout: layout.clone(), cwd: cwd.clone(), }; senders .send_to_server(ServerInstruction::SwitchSession( connect_to_session, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; should_break = true; } else { drop(completion_tx); // no need to wait, this is a no-op } }, Action::MouseEvent { event } => { senders .send_to_screen(ScreenInstruction::MouseEvent( event, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Copy => { senders .send_to_screen(ScreenInstruction::Copy( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Confirm => { // no-op, these are deprecated and should be removed when we upgrade the server/client // contract }, Action::Deny => { // no-op, these are deprecated and should be removed when we upgrade the server/client // contract }, #[allow(clippy::single_match)] Action::SkipConfirm { action } => match *action { Action::Quit => { drop(completion_tx); senders .send_to_server(ServerInstruction::ClientExit(client_id, None)) .with_context(err_context)?; should_break = true; }, _ => {}, }, Action::NoOp => { drop(completion_tx); }, Action::SearchInput { input } => { senders .send_to_screen(ScreenInstruction::UpdateSearch( input, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::Search { direction } => { let notification_end = Some(NotificationEnd::new(completion_tx)); let instruction = match direction { SearchDirection::Down => ScreenInstruction::SearchDown(client_id, notification_end), SearchDirection::Up => ScreenInstruction::SearchUp(client_id, notification_end), }; senders .send_to_screen(instruction) .with_context(err_context)?; }, Action::SearchToggleOption { option } => { let notification_end = Some(NotificationEnd::new(completion_tx)); let instruction = match option { SearchOption::CaseSensitivity => { ScreenInstruction::SearchToggleCaseSensitivity(client_id, notification_end) }, SearchOption::WholeWord => { ScreenInstruction::SearchToggleWholeWord(client_id, notification_end) }, SearchOption::Wrap => { ScreenInstruction::SearchToggleWrap(client_id, notification_end) }, }; senders .send_to_screen(instruction) .with_context(err_context)?; }, Action::ToggleMouseMode => {}, // Handled client side Action::ToggleMobileMode => { senders .send_to_screen(ScreenInstruction::ToggleMobileMode( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PreviousSwapLayout => { senders .send_to_screen(ScreenInstruction::PreviousSwapLayout( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NextSwapLayout => { senders .send_to_screen(ScreenInstruction::NextSwapLayout( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::OverrideLayout { tabs, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_active_tab, } => { let cwd = None; let shell = default_shell.clone(); senders .send_to_screen(ScreenInstruction::OverrideLayout( cwd, shell, tabs, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_active_tab, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::QueryTabNames => { senders .send_to_screen(ScreenInstruction::QueryTabNames( cli_client_id.unwrap_or(client_id), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NewTiledPluginPane { plugin: run_plugin, pane_name: name, skip_cache, cwd, no_focus, tab_id, } => { senders .send_to_screen(ScreenInstruction::NewTiledPluginPane( run_plugin, name, skip_cache, cwd, client_id, Some(NotificationEnd::new(completion_tx)), tab_id, no_focus, )) .with_context(err_context)?; }, Action::NewFloatingPluginPane { plugin: run_plugin, pane_name: name, skip_cache, cwd, coordinates: floating_pane_coordinates, no_focus, tab_id, } => { senders .send_to_screen(ScreenInstruction::NewFloatingPluginPane( run_plugin, name, skip_cache, cwd, floating_pane_coordinates, client_id, Some(NotificationEnd::new(completion_tx)), tab_id, no_focus, )) .with_context(err_context)?; }, Action::NewInPlacePluginPane { plugin: run_plugin, pane_name: name, skip_cache, close_replaced_pane, no_focus, tab_id, } => { if let Some(pane_id) = pane_id { senders .send_to_screen(ScreenInstruction::NewInPlacePluginPane( run_plugin, name, pane_id, skip_cache, close_replaced_pane, client_id, Some(NotificationEnd::new(completion_tx)), tab_id, no_focus, )) .with_context(err_context)?; } else { log::error!("Must have pane_id in order to open in place pane"); } }, Action::StartOrReloadPlugin { plugin: run_plugin } => { senders .send_to_screen(ScreenInstruction::StartOrReloadPluginPane( run_plugin, None, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::LaunchOrFocusPlugin { plugin: run_plugin, should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, skip_cache, tab_id, } => { senders .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin( run_plugin, should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, pane_id, skip_cache, client_id, Some(NotificationEnd::new(completion_tx)), tab_id, )) .with_context(err_context)?; }, Action::LaunchPlugin { plugin: run_plugin, should_float, should_open_in_place, close_replaced_pane, skip_cache, cwd, no_focus, tab_id, } => { senders .send_to_screen(ScreenInstruction::LaunchPlugin( run_plugin, should_float, should_open_in_place, close_replaced_pane, pane_id, skip_cache, cwd, client_id, Some(NotificationEnd::new(completion_tx)), tab_id, no_focus, )) .with_context(err_context)?; }, Action::CloseTerminalPane { pane_id: terminal_pane_id, } => { senders .send_to_screen(ScreenInstruction::ClosePane( PaneId::Terminal(terminal_pane_id), None, // we send None here so that the terminal pane would be closed anywhere // in the app, not just in the client's tab Some(NotificationEnd::new(completion_tx)), None, )) .with_context(err_context)?; }, Action::ClosePluginPane { pane_id: plugin_pane_id, } => { senders .send_to_screen(ScreenInstruction::ClosePane( PaneId::Plugin(plugin_pane_id), None, // we send None here so that the terminal pane would be closed anywhere // in the app, not just in the client's tab Some(NotificationEnd::new(completion_tx)), None, )) .with_context(err_context)?; }, Action::FocusTerminalPaneWithId { pane_id, should_float_if_hidden, should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Terminal(pane_id), should_float_if_hidden, should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusPluginPaneWithId { pane_id, should_float_if_hidden, should_be_in_place_if_hidden, } => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Plugin(pane_id), should_float_if_hidden, should_be_in_place_if_hidden, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenameTerminalPane { pane_id, name: name_bytes, } => { senders .send_to_screen(ScreenInstruction::RenamePane( PaneId::Terminal(pane_id), name_bytes, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenamePluginPane { pane_id, name: name_bytes, } => { senders .send_to_screen(ScreenInstruction::RenamePane( PaneId::Plugin(pane_id), name_bytes, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenameTab { tab_index: tab_position, name: name_bytes, } => { senders .send_to_screen(ScreenInstruction::RenameTab( tab_position as usize, name_bytes, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::BreakPane => { senders .send_to_screen(ScreenInstruction::BreakPane( default_shell.clone(), client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::BreakPaneRight => { senders .send_to_screen(ScreenInstruction::BreakPaneRight( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::BreakPaneLeft => { senders .send_to_screen(ScreenInstruction::BreakPaneLeft( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusHostSession => { senders .send_to_screen(ScreenInstruction::FocusHostSession( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::FocusGuestSession => { senders .send_to_screen(ScreenInstruction::FocusGuestSession( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleHostFullscreen => { senders .send_to_screen(ScreenInstruction::ToggleHostFullscreen( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenameSession { name } => { senders .send_to_screen(ScreenInstruction::RenameSession( name, client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::CliPipe { pipe_id, mut name, payload, plugin, args, configuration, floating, in_place, skip_cache, cwd, pane_title, .. } => { drop(completion_tx); // releasing pipes is handled by the plugins, so we don't want // this to block additionallu if let Some(seen_cli_pipes) = seen_cli_pipes.as_mut() { if !seen_cli_pipes.contains(&pipe_id) { seen_cli_pipes.insert(pipe_id.clone()); senders .send_to_server(ServerInstruction::AssociatePipeWithClient { pipe_id: pipe_id.clone(), client_id: cli_client_id.unwrap_or(client_id), }) .with_context(err_context)?; } } if let Some(name) = name.take() { let should_open_in_place = in_place.unwrap_or(false); if should_open_in_place && pane_id.is_none() { log::error!("Was asked to open a new plugin in-place, but cannot identify the pane id... is the ZELLIJ_PANE_ID variable set?"); } let pane_id_to_replace = if should_open_in_place { pane_id } else { None }; senders .send_to_plugin(PluginInstruction::CliPipe { pipe_id, name, payload, plugin, args, configuration, floating, pane_id_to_replace, cwd, pane_title, skip_cache, cli_client_id: cli_client_id.unwrap_or(client_id), }) .with_context(err_context)?; } else { log::error!("Message must have a name"); } }, Action::KeybindPipe { mut name, payload, plugin, args, mut configuration, floating, in_place, skip_cache, cwd, pane_title, launch_new, plugin_id, .. } => { if let Some(name) = name.take() { let should_open_in_place = in_place.unwrap_or(false); let pane_id_to_replace = if should_open_in_place { pane_id } else { None }; if launch_new && plugin_id.is_none() { // we do this to make sure the plugin is unique (has a unique configuration parameter) configuration .get_or_insert_with(BTreeMap::new) .insert("_zellij_id".to_owned(), Uuid::new_v4().to_string()); } senders .send_to_plugin(PluginInstruction::KeybindPipe { name, payload, plugin, args, configuration, floating, pane_id_to_replace, cwd, pane_title, skip_cache, cli_client_id: client_id, plugin_and_client_id: plugin_id.map(|plugin_id| (plugin_id, client_id)), notification_end: Some(NotificationEnd::new(completion_tx)), }) .with_context(err_context)?; } else { log::error!("Message must have a name"); } }, Action::ListClients => { let default_shell = match default_shell { Some(TerminalAction::RunCommand(run_command)) => Some(run_command.command), _ => None, }; senders .send_to_screen(ScreenInstruction::ListClientsMetadata( default_shell, cli_client_id.unwrap_or(client_id), // we prefer the cli client here because // this is a cli query and we want to print // it there Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ListPanes { show_tab, show_command, show_state, show_geometry, show_all, output_json, } => { let maybe_panes = request_panes_from_screen(&senders, show_all).with_context(err_context)?; if let Some(mut pane_entries) = maybe_panes { if show_command || show_all || output_json { enrich_panes_with_pty_data(&mut pane_entries, &senders) .with_context(err_context)?; } let output_lines = if output_json { format_panes_as_json(&pane_entries) } else { format_panes_table( &pane_entries, show_tab || show_all, show_command || show_all, show_state || show_all, show_geometry || show_all, ) }; send_output_to_client(cli_client_id, os_input.as_ref(), output_lines); } else { send_error_to_client(cli_client_id, os_input.as_ref(), "Timeout listing panes"); } drop(NotificationEnd::new(completion_tx)); }, Action::ListTabs { show_state, show_dimensions, show_panes, show_layout, show_all, output_json, } => { let maybe_tabs = request_tabs_from_screen(&senders, client_id).with_context(err_context)?; if let Some(tab_infos) = maybe_tabs { let output_lines = if output_json { format_tabs_as_json(&tab_infos) } else { format_tabs_table( &tab_infos, show_state || show_all, show_dimensions || show_all, show_panes || show_all, show_layout || show_all, ) }; send_output_to_client(cli_client_id, os_input.as_ref(), output_lines); } else { send_error_to_client(cli_client_id, os_input.as_ref(), "Timeout listing tabs"); } drop(NotificationEnd::new(completion_tx)); }, Action::CurrentTabInfo { output_json } => { let maybe_tab_info = request_current_tab_info_from_screen(&senders, client_id) .with_context(err_context)?; match maybe_tab_info { Some(tab_info) => { let output_lines = if output_json { format_current_tab_info_as_json(&tab_info) } else { format_current_tab_info_plain(&tab_info) }; send_output_to_client(cli_client_id, os_input.as_ref(), output_lines); }, None => { send_error_to_client( cli_client_id, os_input.as_ref(), "No active tab found for current client", ); }, } drop(NotificationEnd::new(completion_tx)); }, Action::TogglePanePinned => { senders .send_to_screen(ScreenInstruction::TogglePanePinned( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::StackPanes { pane_ids: pane_ids_to_stack, } => { senders .send_to_screen(ScreenInstruction::StackPanes( pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(), client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ChangeFloatingPaneCoordinates { pane_id, coordinates, } => { senders .send_to_screen(ScreenInstruction::ChangeFloatingPanesCoordinates( vec![(pane_id.into(), coordinates)], Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TogglePaneBorderless { pane_id } => { senders .send_to_screen(ScreenInstruction::TogglePaneBorderless( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::SetPaneBorderless { pane_id, borderless, } => { senders .send_to_screen(ScreenInstruction::SetPaneBorderless( pane_id.into(), borderless, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TogglePaneInGroup => { senders .send_to_screen(ScreenInstruction::TogglePaneInGroup( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleGroupMarking => { senders .send_to_screen(ScreenInstruction::ToggleGroupMarking( client_id, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ShowFloatingPanes { tab_id } => { senders .send_to_screen(ScreenInstruction::ShowFloatingPanes { client_id, tab_id, completion: Some(NotificationEnd::new(completion_tx)), }) .with_context(err_context)?; }, Action::HideFloatingPanes { tab_id } => { senders .send_to_screen(ScreenInstruction::HideFloatingPanes { client_id, tab_id, completion: Some(NotificationEnd::new(completion_tx)), }) .with_context(err_context)?; }, Action::AreFloatingPanesVisible { tab_id } => { senders .send_to_screen(ScreenInstruction::AreFloatingPanesVisible { client_id, tab_id, completion: Some(NotificationEnd::new(completion_tx)), }) .with_context(err_context)?; }, // Pane-targeting CLI-only variants Action::ScrollUpByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ScrollUpWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollDownByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ScrollDownWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollToTopByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ScrollToTopWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ScrollToBottomByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ScrollToBottomWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PageScrollUpByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::PageScrollUpWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PageScrollDownByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::PageScrollDownWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::HalfPageScrollUpByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::HalfPageScrollUpWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::HalfPageScrollDownByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::HalfPageScrollDownWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ResizeByPaneId { pane_id, resize, direction, } => { let resize_strategy = ResizeStrategy::new(resize, direction); senders .send_to_screen(ScreenInstruction::ResizeWithPaneId( pane_id.into(), resize_strategy, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::MovePaneByPaneId { pane_id, direction } => { senders .send_to_screen(ScreenInstruction::MovePaneWithPaneIdCli( pane_id.into(), direction, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::MovePaneBackwardsByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::MovePaneBackwardsWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ClearScreenByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ClearScreenWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::EditScrollbackByPaneId { pane_id, ansi } => { senders .send_to_screen(ScreenInstruction::EditScrollbackWithPaneId( pane_id.into(), ansi, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFocusFullscreenByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ToggleFullscreenWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFocusNoUiFullscreenByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::ToggleNoUiFullscreenWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TogglePaneEmbedOrFloatingByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloatingWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::CloseFocusByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::CloseFocusWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::RenamePaneByPaneId { pane_id, name } => { let instruction = match pane_id { Some(pane_id) => ScreenInstruction::RenamePaneWithPaneId( pane_id.into(), name, Some(NotificationEnd::new(completion_tx)), ), None => ScreenInstruction::RenameActivePane( name, client_id, Some(NotificationEnd::new(completion_tx)), ), }; senders .send_to_screen(instruction) .with_context(err_context)?; }, Action::UndoRenamePaneByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::UndoRenamePaneWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::TogglePanePinnedByPaneId { pane_id } => { senders .send_to_screen(ScreenInstruction::TogglePanePinnedWithPaneId( pane_id.into(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, // Tab-targeting CLI-only variants Action::UndoRenameTabByTabId { id } => { senders .send_to_screen(ScreenInstruction::UndoRenameTabWithTabId( id as usize, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleActiveSyncTabByTabId { id } => { senders .send_to_screen(ScreenInstruction::ToggleActiveSyncTabWithTabId( id as usize, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::ToggleFloatingPanesByTabId { id } => { senders .send_to_screen(ScreenInstruction::ToggleFloatingPanesWithTabId( id as usize, default_shell.clone(), Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::PreviousSwapLayoutByTabId { id } => { senders .send_to_screen(ScreenInstruction::PreviousSwapLayoutWithTabId( id as usize, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::NextSwapLayoutByTabId { id } => { senders .send_to_screen(ScreenInstruction::NextSwapLayoutWithTabId( id as usize, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, Action::MoveTabByTabId { id, direction } => { senders .send_to_screen(ScreenInstruction::MoveTabWithTabId( id as usize, direction, Some(NotificationEnd::new(completion_tx)), )) .with_context(err_context)?; }, } let result = wait_for_action_completion(completion_rx, &action_name, wait_forever); if let Some(error_message) = &result.error_message { if let Some(cli_client_id) = cli_client_id { if let Some(ref os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::LogError { lines: vec![error_message.clone()], }, ); } } } else if let Some(stdout_message) = &result.stdout_message { if let Some(cli_client_id) = cli_client_id { if let Some(ref os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::Log { lines: vec![stdout_message.clone()], }, ); } } } else if let Some(exit_status) = result.exit_status { if let Some(cli_client_id) = cli_client_id { if let Some(ref os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::Exit { exit_reason: ExitReason::CustomExitStatus(exit_status), }, ); } } } // Return tab ID to CLI clients as plain text if let Some(tab_id) = result.affected_tab_id { if let Some(cli_client_id) = cli_client_id { if let Some(ref os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::Log { lines: vec![tab_id.to_string()], }, ); } } } // Return pane ID to CLI clients as plain text if let Some(pane_id) = result.affected_pane_id { if let Some(cli_client_id) = cli_client_id { if let Some(ref os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::Log { lines: vec![pane_id.to_string()], }, ); } } } Ok((should_break, Some(result))) } // this should only be used for one-off startup instructions macro_rules! send_to_screen_or_retry_queue { ($senders:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ match $senders.as_ref() { Some(senders) => senders.send_to_screen($message), None => { log::warn!("Server not ready, trying to place instruction in retry queue..."); if let Some(retry_queue) = $retry_queue.as_mut() { retry_queue.push_back($instruction); } Ok(()) }, } }}; } pub(crate) fn route_thread_main( session_data: Arc>>, session_state: Arc>, os_input: Box, to_server: SenderWithContext, mut receiver: IpcReceiverWithContext, client_id: ClientId, ) -> Result<()> { let mut retry_queue = VecDeque::new(); let err_context = || format!("failed to handle instruction for client {client_id}"); let mut seen_cli_pipes = HashSet::new(); let mut consecutive_unknown_messages_received = 0; 'route_loop: loop { match receiver.recv_client_msg() { Some((instruction, err_ctx)) => { consecutive_unknown_messages_received = 0; err_ctx.update_thread_ctx(); let mut handle_instruction = |instruction: ClientToServerMsg, mut retry_queue: Option< &mut VecDeque, >| -> Result { let mut should_break = false; let senders = session_data .read() .to_anyhow() .ok() .and_then(|r| r.as_ref().map(|r| r.senders.clone())); // Check if this is a watcher client and ignore input messages let is_watcher = session_state.read().unwrap().is_watcher(&client_id); if is_watcher { match &instruction { ClientToServerMsg::Key { key, .. } => { if (key.bare_key == BareKey::Char('q') && key.key_modifiers.contains(&KeyModifier::Ctrl)) || key.bare_key == BareKey::Esc || (key.bare_key == BareKey::Char('c') && key.key_modifiers.contains(&KeyModifier::Ctrl)) { let _ = os_input.send_to_client( client_id, ServerToClientMsg::Exit { exit_reason: ExitReason::Normal, }, ); let _ = senders.as_ref().map(|s| { s.send_to_screen(ScreenInstruction::RemoveWatcherClient( client_id, )) }); should_break = true; } }, ClientToServerMsg::TerminalResize { new_size, .. } => { // For watchers: send size to Screen for rendering adjustments, but // this does not affect the screen size send_to_screen_or_retry_queue!( senders, ScreenInstruction::WatcherTerminalResize(client_id, *new_size), instruction.clone(), retry_queue ) .with_context(err_context)?; }, _ => { // Ignore all input from watcher clients }, } // don't do anything else for watchers return Ok(should_break); } match instruction { ClientToServerMsg::Key { key, raw_bytes, is_kitty_keyboard_protocol, } => { // Track this as the last active client session_state .write() .unwrap() .set_last_active_client(client_id); // The read guard ends as a temporary in this expression so // `route_action` runs without holding `session_data.read()` — // see the doc comment on `route_action` for why this matters. let dispatch_inputs = session_data.read().unwrap().as_ref().and_then(|s| { let in_passthrough = s.key_passthrough_clients.contains_key(&client_id); if in_passthrough { return Some(( s.senders.clone(), s.default_shell.clone(), s.session_configuration .get_client_default_input_mode(&client_id), vec![Action::Write { key_with_modifier: Some(key), bytes: raw_bytes, is_kitty_keyboard_protocol, }], )); } let (kb, im, dim) = s.get_client_keybinds_and_mode(&client_id)?; let actions: Vec = kb .get_actions_for_key_in_mode_or_default_action( im, &key, raw_bytes, dim, is_kitty_keyboard_protocol, ); Some(( s.senders.clone(), s.default_shell.clone(), s.session_configuration .get_client_default_input_mode(&client_id), actions, )) }); if let Some((senders, default_shell, client_input_mode, actions)) = dispatch_inputs { for action in actions { // Send user input to plugin thread for logging let _ = senders.send_to_plugin(PluginInstruction::UserInput { client_id, action: action.clone(), terminal_id: None, cli_client_id: None, }); match route_action( action, client_id, None, None, senders.clone(), default_shell.clone(), Some(&mut seen_cli_pipes), client_input_mode, Some(os_input.clone()), ) { Ok(route_action_should_break) => { if route_action_should_break.0 { should_break = true; } }, Err(e) => { log::error!("{}", e); }, } } } }, ClientToServerMsg::Action { action, terminal_id: maybe_pane_id, client_id: maybe_client_id, is_cli_client, } => { let cli_client_id = client_id; let client_id = if is_cli_client { // for cli clients, we want to default to the last active client // (i.e. the last client to have issued a keystroke) this is to // interpret actions that require a client_id (such as move focus, // detach, etc.) for which using the cli client id will not be // doing the right thing - using the last_active_client is almost // certainly correct in almost all cases session_state .read() .unwrap() .get_last_active_client() .or(maybe_client_id) .unwrap_or(client_id) } else { maybe_client_id.unwrap_or(client_id) }; // Send user input to plugin thread for logging if let Some(ref senders) = senders { let _ = senders.send_to_plugin(PluginInstruction::UserInput { client_id, action: action.clone(), terminal_id: maybe_pane_id, cli_client_id: if is_cli_client { Some(cli_client_id) } else { None }, }); } // The read guard ends as a temporary in this expression so // `route_action` runs without holding `session_data.read()` — // see the doc comment on `route_action` for why this matters. let session_data_assets = session_data.read().unwrap().as_ref().map(|s| { ( s.senders.clone(), s.default_shell.clone(), s.session_configuration .get_client_default_input_mode(&client_id), ) }); if let Some((senders, default_shell, client_input_mode)) = session_data_assets { match route_action( action, client_id, Some(cli_client_id), maybe_pane_id.map(|p| PaneId::Terminal(p)), senders, default_shell, Some(&mut seen_cli_pipes), client_input_mode, Some(os_input.clone()), ) { Ok(route_action_should_break) => { if route_action_should_break.0 { should_break = true; } }, Err(e) => { log::error!("{}", e); }, } } }, ClientToServerMsg::TerminalResize { new_size, cause } => { // Check if this is a watcher or regular client if is_watcher { // For watchers: send size to Screen for tracking, don't affect screen size send_to_screen_or_retry_queue!( senders.clone(), ScreenInstruction::WatcherTerminalResize(client_id, new_size), instruction, retry_queue ) .with_context(err_context)?; } else { session_state .write() .to_anyhow() .with_context(err_context)? .set_client_size(client_id, new_size); // Per-tab sizing: Screen's RecomputeTabSize // handler is a no-op for clients without an // active tab yet (i.e. resizes arriving // before AddClient is processed), so no // session-level gating is needed. let _ = senders.as_ref().map(|s| { s.send_to_screen(ScreenInstruction::RecomputeTabSize( client_id, new_size, )) }); if matches!(cause, ResizeCause::Viewport) { let mobile_options = session_data.read().ok().and_then(|guard| { guard.as_ref().map(|s| { let config = s .session_configuration .get_client_configuration(&client_id); ( config .options .mobile_layout .unwrap_or_default(), config .options .mobile_threshold_cols .unwrap_or(60), config .options .mobile_threshold_rows .unwrap_or(30), ) }) }); if let Some((mobile_layout, threshold_cols, threshold_rows)) = mobile_options { let _ = senders.as_ref().map(|s| { s.send_to_screen( ScreenInstruction::ReevaluateMobileMode { client_id, new_size, mobile_layout, threshold_cols, threshold_rows, }, ) }); } } else if matches!(cause, ResizeCause::SizeSettled) { let _ = senders.as_ref().map(|s| { s.send_to_screen(ScreenInstruction::MobileSizeSettled( client_id, )) }); } } }, ClientToServerMsg::TerminalPixelDimensions { pixel_dimensions } => { send_to_screen_or_retry_queue!( senders, ScreenInstruction::TerminalPixelDimensions(pixel_dimensions), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::KittyGraphicsSupport { supported } => { send_to_screen_or_retry_queue!( senders, ScreenInstruction::SetKittyGraphicsSupport { client_id, supported }, instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::BackgroundColor { color: ref background_color_instruction, } => { send_to_screen_or_retry_queue!( senders, ScreenInstruction::TerminalBackgroundColor( background_color_instruction.clone() ), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::ForegroundColor { color: ref foreground_color_instruction, } => { send_to_screen_or_retry_queue!( senders, ScreenInstruction::TerminalForegroundColor( foreground_color_instruction.clone() ), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::ColorRegisters { ref color_registers, } => { send_to_screen_or_retry_queue!( senders, ScreenInstruction::TerminalColorRegisters( color_registers .iter() .map(|c| (c.index, c.color.clone())) .collect() ), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::FirstClientConnected { cli_assets, is_web_client, } => { let new_client_instruction = ServerInstruction::FirstClientConnected( cli_assets, is_web_client, client_id, ); to_server .send(new_client_instruction) .with_context(err_context)?; }, ClientToServerMsg::AttachClient { cli_assets, tab_position_to_focus, pane_to_focus: pane_id_to_focus, is_web_client, } => { let allow_web_connections = session_data .read() .ok() .and_then(|s| { s.as_ref().map(|s| s.web_sharing.web_clients_allowed()) }) .unwrap_or(false); let should_allow_connection = !is_web_client || allow_web_connections; if should_allow_connection { let attach_client_instruction = ServerInstruction::AttachClient( cli_assets, tab_position_to_focus, pane_id_to_focus.map(|p| (p.pane_id, p.is_plugin)), is_web_client, client_id, ); to_server .send(attach_client_instruction) .with_context(err_context)?; } else { let error = "This session does not allow web connections."; let _ = to_server.send(ServerInstruction::LogError( vec![error.to_owned()], client_id, None, )); let _ = to_server .send(ServerInstruction::SendWebClientsForbidden(client_id)); } }, ClientToServerMsg::AttachWatcherClient { terminal_size, is_web_client, } => { let allow_web_connections = session_data .read() .ok() .and_then(|s| { s.as_ref().map(|s| s.web_sharing.web_clients_allowed()) }) .unwrap_or(false); let should_allow_connection = !is_web_client || allow_web_connections; if should_allow_connection { let attach_watcher_instruction = ServerInstruction::AttachWatcherClient( client_id, terminal_size, is_web_client, ); to_server .send(attach_watcher_instruction) .with_context(err_context)?; } else { let error = "This session does not allow web connections."; let _ = to_server.send(ServerInstruction::LogError( vec![error.to_owned()], client_id, None, )); let _ = to_server .send(ServerInstruction::SendWebClientsForbidden(client_id)); } }, ClientToServerMsg::ClientExited => { let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); return Ok(true); }, ClientToServerMsg::KillSession => { to_server .send(ServerInstruction::KillSession) .with_context(err_context)?; }, ClientToServerMsg::ConnStatus => { let _ = to_server.send(ServerInstruction::ConnStatus(client_id)); should_break = true; }, ClientToServerMsg::DetachSession { client_ids } => { let _ = to_server.send(ServerInstruction::DetachSession(client_ids, None)); should_break = true; }, ClientToServerMsg::WebServerStarted { base_url } => { let _ = to_server.send(ServerInstruction::WebServerStarted(base_url)); }, ClientToServerMsg::FailedToStartWebServer { error } => { let _ = to_server.send(ServerInstruction::FailedToStartWebServer(error)); }, ClientToServerMsg::DesktopNotificationResponse { ref raw_bytes } => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::DesktopNotificationResponse( raw_bytes.clone(), client_id, ), instruction, retry_queue ); }, ClientToServerMsg::ForwardedReplyFromHost { token, ref reply_bytes, } => { // The client that owns this forward // answered — drop the in-flight entry // first so a later disconnect of that // client can't synthesize a spurious // empty reply for the same token. session_state .write() .unwrap() .clear_forward_in_flight(token); let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::ForwardedReplyFromHost { token, reply_bytes: reply_bytes.clone(), }, instruction, retry_queue ); }, ClientToServerMsg::HostTerminalThemeChanged { mode } => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::HostTerminalThemeChanged(mode), instruction, retry_queue ); }, ClientToServerMsg::SubscribeToPaneRenders { ref pane_ids, ref scrollback, ansi, } => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::SubscribeToPaneRenders { client_id, pane_ids: pane_ids.clone(), scrollback: *scrollback, ansi, }, instruction, retry_queue ); }, ClientToServerMsg::SoftKeyboardVisibilityChanged { visible } => { if let Some(senders) = senders.as_ref() { let _ = senders.send_to_plugin(PluginInstruction::Update(vec![( None, Some(client_id), Event::SoftKeyboardVisibilityChanged(visible), )])); } }, ClientToServerMsg::NestedSessionFrameFromHost { ref payload_bytes } => { match zellij_utils::nested_session::decode_payload(payload_bytes) { Some(message @ zellij_utils::nested_session::NestedSessionMessage::AnnounceAck { .. }) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message @ zellij_utils::nested_session::NestedSessionMessage::FocusGained { .. }) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message @ zellij_utils::nested_session::NestedSessionMessage::FocusLost) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message @ zellij_utils::nested_session::NestedSessionMessage::ShortcutUpdate { .. }) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message @ zellij_utils::nested_session::NestedSessionMessage::FullscreenState { .. }) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message @ zellij_utils::nested_session::NestedSessionMessage::AncestryUpdate { .. }) => { let _ = send_to_screen_or_retry_queue!( senders, ScreenInstruction::NestedSessionMessageFromHost { client_id, message }, instruction, retry_queue ); }, Some(message) => { log::debug!( "dropping unsupported nested session frame relayed from host: {:?}", message ); }, None => { log::debug!("dropping undecodable nested session frame relayed from host"); }, } }, } Ok(should_break) }; let mut repeat_retries = VecDeque::new(); while let Some(instruction_to_retry) = retry_queue.pop_front() { log::warn!("Server ready, retrying sending instruction."); thread::sleep(Duration::from_millis(5)); let should_break = handle_instruction(instruction_to_retry, Some(&mut repeat_retries))?; if should_break { break 'route_loop; } } // retry on loop around retry_queue.append(&mut repeat_retries); let should_break = handle_instruction(instruction, Some(&mut retry_queue))?; if should_break { break 'route_loop; } }, None => { consecutive_unknown_messages_received += 1; if consecutive_unknown_messages_received == 1 { log::error!("Received unknown message from client."); } if consecutive_unknown_messages_received >= 1000 { log::error!("Client sent over 1000 consecutive unknown messages, this is probably an infinite loop, logging client out"); let _ = os_input.send_to_client( client_id, ServerToClientMsg::Exit { exit_reason: ExitReason::Error("Received empty message".to_string()), }, ); let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); break 'route_loop; } }, } // signal to the client that the action has finished processing and it can either exit (if // it's a cli client) or allow the user to perform another action (if it's an actively // connected user) let _ = os_input.send_to_client(client_id, ServerToClientMsg::UnblockInputThread); } // route thread exited, make sure we clean up let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); Ok(()) } fn request_panes_from_screen( senders: &ThreadSenders, show_all: bool, ) -> Result> { use crossbeam::channel::{unbounded, RecvTimeoutError}; use std::time::Duration; let (response_sender, response_receiver) = unbounded(); senders.send_to_screen(ScreenInstruction::ListPanes { show_all, response_channel: response_sender, })?; match response_receiver.recv_timeout(Duration::from_secs(1)) { Ok(entries) => Ok(Some(entries)), Err(RecvTimeoutError::Timeout) => { log::error!("ListPanes timed out waiting for Screen response"); Ok(None) }, Err(RecvTimeoutError::Disconnected) => { log::error!("ListPanes channel disconnected"); Ok(None) }, } } fn request_tabs_from_screen( senders: &ThreadSenders, client_id: ClientId, ) -> Result> { use crossbeam::channel::{unbounded, RecvTimeoutError}; use std::time::Duration; let (response_sender, response_receiver) = unbounded(); senders.send_to_screen(ScreenInstruction::ListTabs { client_id, response_channel: response_sender, })?; match response_receiver.recv_timeout(Duration::from_secs(1)) { Ok(entries) => Ok(Some(entries)), Err(RecvTimeoutError::Timeout) => { log::error!("ListTabs timed out waiting for Screen response"); Ok(None) }, Err(RecvTimeoutError::Disconnected) => { log::error!("ListTabs channel disconnected"); Ok(None) }, } } fn request_current_tab_info_from_screen( senders: &ThreadSenders, client_id: ClientId, ) -> Result> { use crossbeam::channel::{unbounded, RecvTimeoutError}; use std::time::Duration; let (response_sender, response_receiver) = unbounded(); senders.send_to_screen(ScreenInstruction::GetCurrentTabInfo { client_id, response_channel: response_sender, })?; match response_receiver.recv_timeout(Duration::from_secs(1)) { Ok(tab_info_opt) => Ok(tab_info_opt), Err(RecvTimeoutError::Timeout) => { log::error!("GetCurrentTabInfo timed out waiting for Screen response"); Ok(None) }, Err(RecvTimeoutError::Disconnected) => { log::error!("GetCurrentTabInfo channel disconnected"); Ok(None) }, } } fn enrich_panes_with_pty_data( pane_entries: &mut [PaneListEntry], senders: &ThreadSenders, ) -> Result<()> { for entry in pane_entries.iter_mut() { if !entry.pane_info.is_plugin { let pane_id = PaneId::Terminal(entry.pane_info.id); enrich_pane_with_running_command(entry, pane_id, senders)?; enrich_pane_with_cwd(entry, pane_id, senders)?; } } Ok(()) } fn enrich_pane_with_running_command( entry: &mut PaneListEntry, pane_id: PaneId, senders: &ThreadSenders, ) -> Result<()> { use crossbeam::channel::unbounded; use std::time::Duration; use zellij_utils::data::GetPaneRunningCommandResponse; let (cmd_sender, cmd_receiver) = unbounded(); senders.send_to_pty(PtyInstruction::GetPaneRunningCommand { pane_id, response_channel: cmd_sender, })?; if let Ok(GetPaneRunningCommandResponse::Ok(command_vec)) = cmd_receiver.recv_timeout(Duration::from_millis(100)) { entry.pane_command = Some(command_vec.join(" ")); } Ok(()) } fn enrich_pane_with_cwd( entry: &mut PaneListEntry, pane_id: PaneId, senders: &ThreadSenders, ) -> Result<()> { use crossbeam::channel::unbounded; use std::time::Duration; use zellij_utils::data::GetPaneCwdResponse; let (cwd_sender, cwd_receiver) = unbounded(); senders.send_to_pty(PtyInstruction::GetPaneCwd { pane_id, response_channel: cwd_sender, })?; if let Ok(GetPaneCwdResponse::Ok(cwd)) = cwd_receiver.recv_timeout(Duration::from_millis(100)) { entry.pane_cwd = Some(cwd.to_string_lossy().to_string()); } Ok(()) } fn format_panes_as_json(pane_entries: &[PaneListEntry]) -> Vec { vec![serde_json::to_string_pretty(pane_entries).unwrap_or_else(|_| "[]".to_string())] } fn format_panes_table( entries: &[PaneListEntry], show_tab: bool, show_command: bool, show_state: bool, show_geometry: bool, ) -> Vec { let mut lines = Vec::new(); lines.push(build_table_header( show_tab, show_command, show_state, show_geometry, )); for entry in entries { lines.push(build_table_row( entry, show_tab, show_command, show_state, show_geometry, )); } lines } fn build_table_header( show_tab: bool, show_command: bool, show_state: bool, show_geometry: bool, ) -> String { let mut header = Vec::new(); if show_tab { header.push("TAB_ID"); header.push("TAB_POS"); header.push("TAB_NAME"); } header.push("PANE_ID"); header.push("TYPE"); header.push("TITLE"); if show_command { header.push("COMMAND"); header.push("CWD"); } if show_state { header.push("FOCUSED"); header.push("FLOATING"); header.push("EXITED"); } if show_geometry { header.push("X"); header.push("Y"); header.push("ROWS"); header.push("COLS"); } header.join(" ") } fn build_table_row( entry: &PaneListEntry, show_tab: bool, show_command: bool, show_state: bool, show_geometry: bool, ) -> String { let mut row = Vec::new(); if show_tab { row.push(entry.tab_id.to_string()); row.push(entry.tab_position.to_string()); row.push(entry.tab_name.clone()); } row.push(format_pane_id(&entry.pane_info)); row.push(format_pane_type(&entry.pane_info)); row.push(entry.pane_info.title.clone()); if show_command { row.push(extract_command(entry)); row.push(extract_cwd(entry)); } if show_state { row.push(entry.pane_info.is_focused.to_string()); row.push(entry.pane_info.is_floating.to_string()); row.push(entry.pane_info.exited.to_string()); } if show_geometry { row.push(entry.pane_info.pane_x.to_string()); row.push(entry.pane_info.pane_y.to_string()); row.push(entry.pane_info.pane_rows.to_string()); row.push(entry.pane_info.pane_columns.to_string()); } row.join(" ") } fn format_pane_id(pane_info: &zellij_utils::data::PaneInfo) -> String { if pane_info.is_plugin { format!("plugin_{}", pane_info.id) } else { format!("terminal_{}", pane_info.id) } } fn format_pane_type(pane_info: &zellij_utils::data::PaneInfo) -> String { if pane_info.is_plugin { "plugin".to_string() } else { "terminal".to_string() } } fn extract_command(entry: &PaneListEntry) -> String { entry .pane_command .as_ref() .or(entry.pane_info.terminal_command.as_ref()) .or(entry.pane_info.plugin_url.as_ref()) .map(|s| s.as_str()) .unwrap_or("-") .to_string() } fn extract_cwd(entry: &PaneListEntry) -> String { entry .pane_cwd .as_ref() .map(|s| s.as_str()) .unwrap_or("-") .to_string() } fn format_tabs_as_json(tab_infos: &[TabInfo]) -> Vec { vec![serde_json::to_string_pretty(tab_infos).unwrap_or_else(|_| "[]".to_string())] } fn format_tabs_table( tabs: &[TabInfo], show_state: bool, show_dimensions: bool, show_panes: bool, show_layout: bool, ) -> Vec { let mut lines = Vec::new(); lines.push(build_tabs_table_header( show_state, show_dimensions, show_panes, show_layout, )); for tab_info in tabs { lines.push(build_tabs_table_row( tab_info, show_state, show_dimensions, show_panes, show_layout, )); } lines } fn build_tabs_table_header( show_state: bool, show_dimensions: bool, show_panes: bool, show_layout: bool, ) -> String { let mut header = Vec::new(); // Core fields (always shown) header.push("TAB_ID"); header.push("POSITION"); header.push("NAME"); if show_state { header.push("ACTIVE"); header.push("FULLSCREEN"); header.push("SYNC_PANES"); header.push("FLOATING_VIS"); } if show_dimensions { header.push("VP_ROWS"); header.push("VP_COLS"); header.push("DA_ROWS"); header.push("DA_COLS"); } if show_panes { header.push("TILED_PANES"); header.push("FLOAT_PANES"); header.push("HIDDEN_PANES"); } if show_layout { header.push("SWAP_LAYOUT"); header.push("LAYOUT_DIRTY"); } header.join(" ") } fn build_tabs_table_row( tab_info: &TabInfo, show_state: bool, show_dimensions: bool, show_panes: bool, show_layout: bool, ) -> String { let mut row = Vec::new(); // Core fields row.push(tab_info.tab_id.to_string()); row.push(tab_info.position.to_string()); row.push(tab_info.name.clone()); if show_state { row.push(tab_info.active.to_string()); row.push(tab_info.is_fullscreen_active.to_string()); row.push(tab_info.is_sync_panes_active.to_string()); row.push(tab_info.are_floating_panes_visible.to_string()); } if show_dimensions { row.push(tab_info.viewport_rows.to_string()); row.push(tab_info.viewport_columns.to_string()); row.push(tab_info.display_area_rows.to_string()); row.push(tab_info.display_area_columns.to_string()); } if show_panes { row.push(tab_info.selectable_tiled_panes_count.to_string()); row.push(tab_info.selectable_floating_panes_count.to_string()); row.push(tab_info.panes_to_hide.to_string()); } if show_layout { row.push( tab_info .active_swap_layout_name .as_deref() .unwrap_or("-") .to_string(), ); row.push(tab_info.is_swap_layout_dirty.to_string()); } row.join(" ") } fn format_current_tab_info_as_json(tab_info: &TabInfo) -> Vec { vec![serde_json::to_string_pretty(tab_info).unwrap_or_else(|_| "{}".to_string())] } fn format_current_tab_info_plain(tab_info: &TabInfo) -> Vec { vec![ format!("name: {}", tab_info.name), format!("id: {}", tab_info.tab_id), format!("position: {}", tab_info.position), ] } fn send_error_to_client( cli_client_id: Option, os_input: Option<&Box>, error_message: &str, ) { if let Some(cli_client_id) = cli_client_id { if let Some(os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::LogError { lines: vec![error_message.to_string()], }, ); } } } fn send_output_to_client( cli_client_id: Option, os_input: Option<&Box>, output_lines: Vec, ) { if let Some(cli_client_id) = cli_client_id { if let Some(os_input) = os_input { let _ = os_input.send_to_client( cli_client_id, ServerToClientMsg::Log { lines: output_lines, }, ); } } } #[cfg(test)] mod tests { use super::*; #[test] fn test_notification_end_sets_affected_tab_id() { let (tx, rx) = oneshot::channel(); let mut notification_end = NotificationEnd::new(tx); notification_end.set_affected_tab_id(42); drop(notification_end); let result = rx.blocking_recv().unwrap(); assert_eq!(result.affected_tab_id, Some(42)); } #[test] fn test_notification_end_default_affected_tab_id_none() { let (tx, rx) = oneshot::channel(); let notification_end = NotificationEnd::new(tx); drop(notification_end); let result = rx.blocking_recv().unwrap(); assert_eq!(result.affected_tab_id, None); } #[test] fn test_action_completion_result_includes_tab_id() { let result = ActionCompletionResult { exit_status: None, affected_pane_id: None, affected_tab_id: Some(123), error_message: None, stdout_message: None, }; assert_eq!(result.affected_tab_id, Some(123)); } #[test] fn test_notification_end_with_pane_and_tab_ids() { let (tx, rx) = oneshot::channel(); let mut notification_end = NotificationEnd::new(tx); notification_end.set_affected_pane_id(PaneId::Terminal(10)); notification_end.set_affected_tab_id(5); drop(notification_end); let result = rx.blocking_recv().unwrap(); assert_eq!(result.affected_pane_id, Some(PaneId::Terminal(10))); assert_eq!(result.affected_tab_id, Some(5)); } #[test] fn test_notification_end_clone_does_not_copy_channel() { let (tx, _rx) = oneshot::channel(); let mut notification_end = NotificationEnd::new(tx); notification_end.set_affected_tab_id(99); let cloned = notification_end.clone(); // Verify the clone has the same data assert_eq!(cloned.affected_tab_id, Some(99)); // But channel should be None (as per the Clone implementation comment) assert!(cloned.channel.is_none()); } }