use std::collections::{BTreeMap, HashSet, VecDeque}; use std::sync::{Arc, RwLock}; use crate::thread_bus::ThreadSenders; use crate::{ os_input_output::ServerOsApi, panes::PaneId, plugins::PluginInstruction, pty::{ClientTabIndexOrPaneId, NewPanePlacement, PtyInstruction}, screen::ScreenInstruction, ServerInstruction, SessionMetaData, SessionState, }; use std::thread; use std::time::Duration; use uuid::Uuid; use zellij_utils::{ channels::SenderWithContext, data::{Direction, Event, InputMode, PluginCapabilities, ResizeStrategy}, errors::prelude::*, input::{ actions::{Action, SearchDirection, SearchOption}, command::TerminalAction, get_mode_info, keybinds::Keybinds, layout::Layout, }, ipc::{ ClientAttributes, ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg, }, }; use crate::ClientId; pub(crate) fn route_action( action: Action, client_id: ClientId, pane_id: Option, senders: ThreadSenders, capabilities: PluginCapabilities, client_attributes: ClientAttributes, default_shell: Option, default_layout: Box, mut seen_cli_pipes: Option<&mut HashSet>, client_keybinds: Keybinds, default_mode: InputMode, ) -> Result { let mut should_break = false; let err_context = || format!("failed to route action for client {client_id}"); 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)?; } match action { Action::ToggleTab => { senders .send_to_screen(ScreenInstruction::ToggleTab(client_id)) .with_context(err_context)?; }, Action::Write(key_with_modifier, 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, )) .with_context(err_context)?; }, Action::WriteChars(val) => { senders .send_to_screen(ScreenInstruction::ClearScroll(client_id)) .with_context(err_context)?; let val = val.into_bytes(); senders .send_to_screen(ScreenInstruction::WriteCharacter( None, val, false, client_id, )) .with_context(err_context)?; }, Action::SwitchToMode(mode) => { let attrs = &client_attributes; senders .send_to_server(ServerInstruction::ChangeMode(client_id, mode)) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::ChangeMode( get_mode_info( mode, attrs, capabilities, &client_keybinds, Some(default_mode), ), client_id, )) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::Render) .with_context(err_context)?; }, Action::Resize(resize, direction) => { let screen_instr = ScreenInstruction::Resize(client_id, ResizeStrategy::new(resize, direction)); senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::SwitchFocus => { senders .send_to_screen(ScreenInstruction::SwitchFocus(client_id)) .with_context(err_context)?; }, Action::FocusNextPane => { senders .send_to_screen(ScreenInstruction::FocusNextPane(client_id)) .with_context(err_context)?; }, Action::FocusPreviousPane => { senders .send_to_screen(ScreenInstruction::FocusPreviousPane(client_id)) .with_context(err_context)?; }, Action::MoveFocus(direction) => { let screen_instr = match direction { Direction::Left => ScreenInstruction::MoveFocusLeft(client_id), Direction::Right => ScreenInstruction::MoveFocusRight(client_id), Direction::Up => ScreenInstruction::MoveFocusUp(client_id), Direction::Down => ScreenInstruction::MoveFocusDown(client_id), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MoveFocusOrTab(direction) => { let screen_instr = match direction { Direction::Left => ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id), Direction::Right => ScreenInstruction::MoveFocusRightOrNextTab(client_id), Direction::Up => ScreenInstruction::SwitchTabNext(client_id), Direction::Down => ScreenInstruction::SwitchTabPrev(client_id), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MovePane(direction) => { let screen_instr = match direction { Some(Direction::Left) => ScreenInstruction::MovePaneLeft(client_id), Some(Direction::Right) => ScreenInstruction::MovePaneRight(client_id), Some(Direction::Up) => ScreenInstruction::MovePaneUp(client_id), Some(Direction::Down) => ScreenInstruction::MovePaneDown(client_id), None => ScreenInstruction::MovePane(client_id), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::MovePaneBackwards => { senders .send_to_screen(ScreenInstruction::MovePaneBackwards(client_id)) .with_context(err_context)?; }, Action::ClearScreen => { senders .send_to_screen(ScreenInstruction::ClearScreen(client_id)) .with_context(err_context)?; }, Action::DumpScreen(val, full) => { senders .send_to_screen(ScreenInstruction::DumpScreen(val, client_id, full)) .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, client_id)) .with_context(err_context)?; }, Action::EditScrollback => { senders .send_to_screen(ScreenInstruction::EditScrollback(client_id)) .with_context(err_context)?; }, Action::ScrollUp => { senders .send_to_screen(ScreenInstruction::ScrollUp(client_id)) .with_context(err_context)?; }, Action::ScrollUpAt(point) => { senders .send_to_screen(ScreenInstruction::ScrollUpAt(point, client_id)) .with_context(err_context)?; }, Action::ScrollDown => { senders .send_to_screen(ScreenInstruction::ScrollDown(client_id)) .with_context(err_context)?; }, Action::ScrollDownAt(point) => { senders .send_to_screen(ScreenInstruction::ScrollDownAt(point, client_id)) .with_context(err_context)?; }, Action::ScrollToBottom => { senders .send_to_screen(ScreenInstruction::ScrollToBottom(client_id)) .with_context(err_context)?; }, Action::ScrollToTop => { senders .send_to_screen(ScreenInstruction::ScrollToTop(client_id)) .with_context(err_context)?; }, Action::PageScrollUp => { senders .send_to_screen(ScreenInstruction::PageScrollUp(client_id)) .with_context(err_context)?; }, Action::PageScrollDown => { senders .send_to_screen(ScreenInstruction::PageScrollDown(client_id)) .with_context(err_context)?; }, Action::HalfPageScrollUp => { senders .send_to_screen(ScreenInstruction::HalfPageScrollUp(client_id)) .with_context(err_context)?; }, Action::HalfPageScrollDown => { senders .send_to_screen(ScreenInstruction::HalfPageScrollDown(client_id)) .with_context(err_context)?; }, Action::ToggleFocusFullscreen => { senders .send_to_screen(ScreenInstruction::ToggleActiveTerminalFullscreen(client_id)) .with_context(err_context)?; }, Action::TogglePaneFrames => { senders .send_to_screen(ScreenInstruction::TogglePaneFrames) .with_context(err_context)?; }, Action::NewPane(direction, name, start_suppressed) => { let shell = default_shell.clone(); let new_pane_placement = match direction { Some(direction) => NewPanePlacement::Tiled(Some(direction)), None => NewPanePlacement::NoPreference, }; let _ = senders.send_to_pty(PtyInstruction::SpawnTerminal( shell, name, new_pane_placement, start_suppressed, ClientTabIndexOrPaneId::ClientId(client_id), )); }, Action::EditFile( open_file_payload, split_direction, should_float, should_open_in_place, start_suppressed, floating_pane_coordinates, ) => { 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 { match pane_id { Some(pane_id) => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, ClientTabIndexOrPaneId::PaneId(pane_id), ), None => PtyInstruction::SpawnInPlaceTerminal( Some(open_file), Some(title), false, ClientTabIndexOrPaneId::ClientId(client_id), ), } } else { PtyInstruction::SpawnTerminal( Some(open_file), Some(title), if should_float { NewPanePlacement::Floating(floating_pane_coordinates) } else { NewPanePlacement::Tiled(split_direction) }, start_suppressed, ClientTabIndexOrPaneId::ClientId(client_id), ) }; senders.send_to_pty(pty_instr).with_context(err_context)?; }, Action::SwitchModeForAllClients(input_mode) => { let attrs = &client_attributes; senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::ModeUpdate(get_mode_info( input_mode, attrs, capabilities, &client_keybinds, Some(default_mode), )), )])) .with_context(err_context)?; senders .send_to_server(ServerInstruction::ChangeModeForAllClients(input_mode)) .with_context(err_context)?; senders .send_to_screen(ScreenInstruction::ChangeModeForAllClients(get_mode_info( input_mode, attrs, capabilities, &client_keybinds, Some(default_mode), ))) .with_context(err_context)?; }, Action::NewFloatingPane(run_command, name, floating_pane_coordinates) => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Floating(floating_pane_coordinates), false, ClientTabIndexOrPaneId::ClientId(client_id), )) .with_context(err_context)?; }, Action::NewInPlacePane(run_command, name) => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); match pane_id { Some(pane_id) => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, false, ClientTabIndexOrPaneId::PaneId(pane_id), )) .with_context(err_context)?; }, None => { senders .send_to_pty(PtyInstruction::SpawnInPlaceTerminal( run_cmd, name, false, ClientTabIndexOrPaneId::ClientId(client_id), )) .with_context(err_context)?; }, } }, Action::NewStackedPane(run_command, name) => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); match pane_id { Some(pane_id) => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Stacked(Some(pane_id)), false, ClientTabIndexOrPaneId::PaneId(pane_id), )) .with_context(err_context)?; }, None => { senders .send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Stacked(None), false, ClientTabIndexOrPaneId::ClientId(client_id), )) .with_context(err_context)?; }, } }, Action::NewTiledPane(direction, run_command, name) => { let run_cmd = run_command .map(|cmd| TerminalAction::RunCommand(cmd.into())) .or_else(|| default_shell.clone()); let _ = senders.send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, name, NewPanePlacement::Tiled(direction), false, ClientTabIndexOrPaneId::ClientId(client_id), )); }, Action::TogglePaneEmbedOrFloating => { senders .send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloating(client_id)) .with_context(err_context)?; }, Action::ToggleFloatingPanes => { senders .send_to_screen(ScreenInstruction::ToggleFloatingPanes( client_id, default_shell.clone(), )) .with_context(err_context)?; }, Action::PaneNameInput(c) => { senders .send_to_screen(ScreenInstruction::UpdatePaneName(c, client_id)) .with_context(err_context)?; }, Action::UndoRenamePane => { senders .send_to_screen(ScreenInstruction::UndoRenamePane(client_id)) .with_context(err_context)?; }, Action::Run(command) => { let run_cmd = Some(TerminalAction::RunCommand(command.clone().into())); let _ = senders.send_to_pty(PtyInstruction::SpawnTerminal( run_cmd, None, NewPanePlacement::Tiled(command.direction), false, ClientTabIndexOrPaneId::ClientId(client_id), )); }, Action::CloseFocus => { senders .send_to_screen(ScreenInstruction::CloseFocusedPane(client_id)) .with_context(err_context)?; }, Action::NewTab( tab_layout, floating_panes_layout, swap_tiled_layouts, swap_floating_layouts, tab_name, should_change_focus_to_new_tab, cwd, ) => { let shell = default_shell.clone(); let swap_tiled_layouts = swap_tiled_layouts.unwrap_or_else(|| default_layout.swap_tiled_layouts.clone()); let swap_floating_layouts = swap_floating_layouts .unwrap_or_else(|| default_layout.swap_floating_layouts.clone()); let is_web_client = false; // actions cannot be initiated directly from the web senders .send_to_screen(ScreenInstruction::NewTab( cwd, shell, tab_layout, floating_panes_layout, tab_name, (swap_tiled_layouts, swap_floating_layouts), should_change_focus_to_new_tab, (client_id, is_web_client), )) .with_context(err_context)?; }, Action::GoToNextTab => { senders .send_to_screen(ScreenInstruction::SwitchTabNext(client_id)) .with_context(err_context)?; }, Action::GoToPreviousTab => { senders .send_to_screen(ScreenInstruction::SwitchTabPrev(client_id)) .with_context(err_context)?; }, Action::ToggleActiveSyncTab => { senders .send_to_screen(ScreenInstruction::ToggleActiveSyncTab(client_id)) .with_context(err_context)?; }, Action::CloseTab => { senders .send_to_screen(ScreenInstruction::CloseTab(client_id)) .with_context(err_context)?; }, Action::GoToTab(i) => { senders .send_to_screen(ScreenInstruction::GoToTab(i, Some(client_id))) .with_context(err_context)?; }, Action::GoToTabName(name, create) => { let shell = default_shell.clone(); let swap_tiled_layouts = default_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = default_layout.swap_floating_layouts.clone(); senders .send_to_screen(ScreenInstruction::GoToTabName( name, (swap_tiled_layouts, swap_floating_layouts), shell, create, Some(client_id), )) .with_context(err_context)?; }, Action::TabNameInput(c) => { senders .send_to_screen(ScreenInstruction::UpdateTabName(c, client_id)) .with_context(err_context)?; }, Action::UndoRenameTab => { senders .send_to_screen(ScreenInstruction::UndoRenameTab(client_id)) .with_context(err_context)?; }, Action::MoveTab(direction) => { let screen_instr = match direction { Direction::Left => ScreenInstruction::MoveTabLeft(client_id), Direction::Right => ScreenInstruction::MoveTabRight(client_id), _ => return Ok(false), }; senders .send_to_screen(screen_instr) .with_context(err_context)?; }, Action::Quit => { senders .send_to_server(ServerInstruction::ClientExit(client_id)) .with_context(err_context)?; should_break = true; }, Action::Detach => { senders .send_to_server(ServerInstruction::DetachSession(vec![client_id])) .with_context(err_context)?; should_break = true; }, Action::MouseEvent(event) => { senders .send_to_screen(ScreenInstruction::MouseEvent(event, client_id)) .with_context(err_context)?; }, Action::Copy => { senders .send_to_screen(ScreenInstruction::Copy(client_id)) .with_context(err_context)?; }, Action::Confirm => { senders .send_to_screen(ScreenInstruction::ConfirmPrompt(client_id)) .with_context(err_context)?; }, Action::Deny => { senders .send_to_screen(ScreenInstruction::DenyPrompt(client_id)) .with_context(err_context)?; }, #[allow(clippy::single_match)] Action::SkipConfirm(action) => match *action { Action::Quit => { senders .send_to_server(ServerInstruction::ClientExit(client_id)) .with_context(err_context)?; should_break = true; }, _ => {}, }, Action::NoOp => {}, Action::SearchInput(c) => { senders .send_to_screen(ScreenInstruction::UpdateSearch(c, client_id)) .with_context(err_context)?; }, Action::Search(d) => { let instruction = match d { SearchDirection::Down => ScreenInstruction::SearchDown(client_id), SearchDirection::Up => ScreenInstruction::SearchUp(client_id), }; senders .send_to_screen(instruction) .with_context(err_context)?; }, Action::SearchToggleOption(o) => { let instruction = match o { SearchOption::CaseSensitivity => { ScreenInstruction::SearchToggleCaseSensitivity(client_id) }, SearchOption::WholeWord => ScreenInstruction::SearchToggleWholeWord(client_id), SearchOption::Wrap => ScreenInstruction::SearchToggleWrap(client_id), }; senders .send_to_screen(instruction) .with_context(err_context)?; }, Action::ToggleMouseMode => {}, // Handled client side Action::PreviousSwapLayout => { senders .send_to_screen(ScreenInstruction::PreviousSwapLayout(client_id)) .with_context(err_context)?; }, Action::NextSwapLayout => { senders .send_to_screen(ScreenInstruction::NextSwapLayout(client_id)) .with_context(err_context)?; }, Action::QueryTabNames => { senders .send_to_screen(ScreenInstruction::QueryTabNames(client_id)) .with_context(err_context)?; }, Action::NewTiledPluginPane(run_plugin, name, skip_cache, cwd) => { senders .send_to_screen(ScreenInstruction::NewTiledPluginPane( run_plugin, name, skip_cache, cwd, client_id, )) .with_context(err_context)?; }, Action::NewFloatingPluginPane( run_plugin, name, skip_cache, cwd, floating_pane_coordinates, ) => { senders .send_to_screen(ScreenInstruction::NewFloatingPluginPane( run_plugin, name, skip_cache, cwd, floating_pane_coordinates, client_id, )) .with_context(err_context)?; }, Action::NewInPlacePluginPane(run_plugin, name, skip_cache) => { if let Some(pane_id) = pane_id { senders .send_to_screen(ScreenInstruction::NewInPlacePluginPane( run_plugin, name, pane_id, skip_cache, client_id, )) .with_context(err_context)?; } else { log::error!("Must have pane_id in order to open in place pane"); } }, Action::StartOrReloadPlugin(run_plugin) => { senders .send_to_screen(ScreenInstruction::StartOrReloadPluginPane(run_plugin, None)) .with_context(err_context)?; }, Action::LaunchOrFocusPlugin( run_plugin, should_float, move_to_focused_tab, should_open_in_place, skip_cache, ) => { senders .send_to_screen(ScreenInstruction::LaunchOrFocusPlugin( run_plugin, should_float, move_to_focused_tab, should_open_in_place, pane_id, skip_cache, client_id, )) .with_context(err_context)?; }, Action::LaunchPlugin(run_plugin, should_float, should_open_in_place, skip_cache, cwd) => { senders .send_to_screen(ScreenInstruction::LaunchPlugin( run_plugin, should_float, should_open_in_place, pane_id, skip_cache, cwd, client_id, )) .with_context(err_context)?; }, Action::CloseTerminalPane(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 )) .with_context(err_context)?; }, Action::ClosePluginPane(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 )) .with_context(err_context)?; }, Action::FocusTerminalPaneWithId(pane_id, should_float_if_hidden) => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Terminal(pane_id), should_float_if_hidden, client_id, )) .with_context(err_context)?; }, Action::FocusPluginPaneWithId(pane_id, should_float_if_hidden) => { senders .send_to_screen(ScreenInstruction::FocusPaneWithId( PaneId::Plugin(pane_id), should_float_if_hidden, client_id, )) .with_context(err_context)?; }, Action::RenameTerminalPane(pane_id, name_bytes) => { senders .send_to_screen(ScreenInstruction::RenamePane( PaneId::Terminal(pane_id), name_bytes, )) .with_context(err_context)?; }, Action::RenamePluginPane(pane_id, name_bytes) => { senders .send_to_screen(ScreenInstruction::RenamePane( PaneId::Plugin(pane_id), name_bytes, )) .with_context(err_context)?; }, Action::RenameTab(tab_position, name_bytes) => { senders .send_to_screen(ScreenInstruction::RenameTab( tab_position as usize, name_bytes, )) .with_context(err_context)?; }, Action::BreakPane => { senders .send_to_screen(ScreenInstruction::BreakPane( default_layout.clone(), default_shell.clone(), client_id, )) .with_context(err_context)?; }, Action::BreakPaneRight => { senders .send_to_screen(ScreenInstruction::BreakPaneRight(client_id)) .with_context(err_context)?; }, Action::BreakPaneLeft => { senders .send_to_screen(ScreenInstruction::BreakPaneLeft(client_id)) .with_context(err_context)?; }, Action::RenameSession(name) => { senders .send_to_screen(ScreenInstruction::RenameSession(name, client_id)) .with_context(err_context)?; }, Action::CliPipe { pipe_id, mut name, payload, plugin, args, configuration, floating, in_place, skip_cache, cwd, pane_title, .. } => { 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, }) .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: 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)), }) .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, client_id, )) .with_context(err_context)?; }, Action::TogglePanePinned => { senders .send_to_screen(ScreenInstruction::TogglePanePinned(client_id)) .with_context(err_context)?; }, Action::StackPanes(pane_ids_to_stack) => { senders .send_to_screen(ScreenInstruction::StackPanes( pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(), client_id, )) .with_context(err_context)?; }, Action::ChangeFloatingPaneCoordinates(pane_id, coordinates) => { senders .send_to_screen(ScreenInstruction::ChangeFloatingPanesCoordinates(vec![( pane_id.into(), coordinates, )])) .with_context(err_context)?; }, Action::TogglePaneInGroup => { senders .send_to_screen(ScreenInstruction::TogglePaneInGroup(client_id)) .with_context(err_context)?; }, Action::ToggleGroupMarking => { senders .send_to_screen(ScreenInstruction::ToggleGroupMarking(client_id)) .with_context(err_context)?; }, } Ok(should_break) } // this should only be used for one-off startup instructions macro_rules! send_to_screen_or_retry_queue { ($rlocked_sessions:expr, $message:expr, $instruction: expr, $retry_queue:expr) => {{ match $rlocked_sessions.as_ref() { Some(session_metadata) => session_metadata.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(); 'route_loop: loop { match receiver.recv() { Some((instruction, err_ctx)) => { err_ctx.update_thread_ctx(); let mut handle_instruction = |instruction: ClientToServerMsg, mut retry_queue: Option< &mut VecDeque, >| -> Result { let mut should_break = false; let rlocked_sessions = session_data.read().to_anyhow().with_context(err_context)?; match instruction { ClientToServerMsg::Key(key, raw_bytes, is_kitty_keyboard_protocol) => { if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { match rlocked_sessions.get_client_keybinds_and_mode(&client_id) { Some((keybinds, input_mode, default_input_mode)) => { for action in keybinds .get_actions_for_key_in_mode_or_default_action( &input_mode, &key, raw_bytes, default_input_mode, is_kitty_keyboard_protocol, ) { if route_action( action, client_id, None, rlocked_sessions.senders.clone(), rlocked_sessions.capabilities.clone(), rlocked_sessions.client_attributes.clone(), rlocked_sessions.default_shell.clone(), rlocked_sessions.layout.clone(), Some(&mut seen_cli_pipes), keybinds.clone(), rlocked_sessions .session_configuration .get_client_configuration(&client_id) .options .default_mode .unwrap_or(InputMode::Normal) .clone(), )? { should_break = true; } } }, None => { log::error!("Failed to get keybindings for client"); }, } } }, ClientToServerMsg::Action(action, maybe_pane_id, maybe_client_id) => { let client_id = maybe_client_id.unwrap_or(client_id); if let Some(rlocked_sessions) = rlocked_sessions.as_ref() { if route_action( action, client_id, maybe_pane_id.map(|p| PaneId::Terminal(p)), rlocked_sessions.senders.clone(), rlocked_sessions.capabilities.clone(), rlocked_sessions.client_attributes.clone(), rlocked_sessions.default_shell.clone(), rlocked_sessions.layout.clone(), Some(&mut seen_cli_pipes), rlocked_sessions .session_configuration .get_client_keybinds(&client_id) .clone(), rlocked_sessions .session_configuration .get_client_configuration(&client_id) .options .default_mode .unwrap_or(InputMode::Normal) .clone(), )? { should_break = true; } } }, ClientToServerMsg::TerminalResize(new_size) => { session_state .write() .to_anyhow() .with_context(err_context)? .set_client_size(client_id, new_size); session_state .read() .to_anyhow() .and_then(|state| { state.min_client_terminal_size().ok_or(anyhow!( "failed to determine minimal client terminal size" )) }) .and_then(|min_size| { rlocked_sessions .as_ref() .context("couldn't get reference to read-locked session")? .senders .send_to_screen(ScreenInstruction::TerminalResize(min_size)) }) .with_context(err_context)?; }, ClientToServerMsg::TerminalPixelDimensions(pixel_dimensions) => { send_to_screen_or_retry_queue!( rlocked_sessions, ScreenInstruction::TerminalPixelDimensions(pixel_dimensions), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::BackgroundColor(ref background_color_instruction) => { send_to_screen_or_retry_queue!( rlocked_sessions, ScreenInstruction::TerminalBackgroundColor( background_color_instruction.clone() ), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::ForegroundColor(ref foreground_color_instruction) => { send_to_screen_or_retry_queue!( rlocked_sessions, ScreenInstruction::TerminalForegroundColor( foreground_color_instruction.clone() ), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::ColorRegisters(ref color_registers) => { send_to_screen_or_retry_queue!( rlocked_sessions, ScreenInstruction::TerminalColorRegisters(color_registers.clone()), instruction, retry_queue ) .with_context(err_context)?; }, ClientToServerMsg::NewClient( client_attributes, cli_args, config, runtime_config_options, layout, plugin_aliases, should_launch_setup_wizard, is_web_client, layout_is_welcome_screen, ) => { let new_client_instruction = ServerInstruction::NewClient( client_attributes, cli_args, config, runtime_config_options, layout, plugin_aliases, should_launch_setup_wizard, is_web_client, layout_is_welcome_screen, client_id, ); to_server .send(new_client_instruction) .with_context(err_context)?; }, ClientToServerMsg::AttachClient( client_attributes, config, runtime_config_options, tab_position_to_focus, pane_id_to_focus, is_web_client, ) => { let allow_web_connections = rlocked_sessions .as_ref() .map(|rlocked_sessions| { rlocked_sessions.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( client_attributes, config, runtime_config_options, tab_position_to_focus, pane_id_to_focus, 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, )); let _ = to_server .send(ServerInstruction::SendWebClientsForbidden(client_id)); } }, ClientToServerMsg::ClientExited => { // we don't unwrap this because we don't really care if there's an error here (eg. // if the main server thread exited before this router thread did) 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_id) => { let _ = to_server.send(ServerInstruction::DetachSession(client_id)); should_break = true; }, ClientToServerMsg::ConfigWrittenToDisk(config) => { let _ = to_server .send(ServerInstruction::ConfigWrittenToDisk(client_id, config)); }, ClientToServerMsg::FailedToWriteConfigToDisk(failed_path) => { let _ = to_server.send(ServerInstruction::FailedToWriteConfigToDisk( client_id, failed_path, )); }, ClientToServerMsg::WebServerStarted(base_url) => { let _ = to_server.send(ServerInstruction::WebServerStarted(base_url)); }, ClientToServerMsg::FailedToStartWebServer(error) => { let _ = to_server.send(ServerInstruction::FailedToStartWebServer(error)); }, } 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 => { log::error!("Received empty message from client, logging client out."); let _ = os_input.send_to_client( client_id, ServerToClientMsg::Exit(ExitReason::Error( "Received empty message".to_string(), )), ); let _ = to_server.send(ServerInstruction::RemoveClient(client_id)); break 'route_loop; }, } } Ok(()) }