//! Things related to [`Screen`]s. //! //! # Tab Identification //! //! Tabs have two distinct identifiers: //! //! - **ID** (`tab.id`): Stable, unique identifier that never changes after creation. //! Used as BTreeMap key and for internal tracking. Monotonically increasing. //! //! - **Position** (`tab.position`): Current display order (0-based index in tab bar). //! Changes when tabs are moved or closed. Used for user-facing operations. //! //! # Terminology Convention //! //! - **"id"**: Always means stable identifier //! - **"position"**: Always means 0-based display order //! - **"index"**: Synonym for "position" (used in public/plugin APIs) //! //! Examples: //! - `close_tab_by_id(5)` - Closes tab with stable ID 5 //! - `CloseTabWithIndex(2)` - Closes tab at position 2 (3rd tab visually) //! - `get_tab_by_position(0)` - Gets first tab in display order //! - `PluginInstruction::NewTab(tab_id, ...)` - Uses ID for async communication //! //! # Key Data Structures //! //! - `tabs: BTreeMap`: Keyed by tab.id (stable identifier) //! - `active_tab_ids: BTreeMap`: Maps clients to active tab ID //! - `tab_history: BTreeMap>`: History of tab IDs per client use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet, VecDeque}; use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; use std::rc::Rc; use std::str; use std::time::{Duration, Instant}; use crate::route::NotificationEnd; use log::{debug, warn}; use zellij_utils::data::{ CommandOrPlugin, Direction, EventType, FloatingPaneCoordinates, GetFocusedPaneInfoResponse, HostTerminalThemeMode, KeyWithModifier, LayoutInfo, LayoutWithError, ListPanesResponse, ListTabsResponse, NewPanePlacement, PaneContents, PaneInfo, PaneListEntry, PaneManifest, PaneRenderReport, PanePadding, PaneScrollbackResponse, PaneTitleAlignment, PluginPermission, RegexHighlight, Resize, ResizeStrategy, SessionInfo, Styling, TabInfo, ThemeHue, WebSharing, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::actions::Action; use zellij_utils::input::command::RunCommand; use zellij_utils::input::config::Config; use zellij_utils::input::keybinds::{shortcut_for_action, Keybinds}; use zellij_utils::input::mouse::{MouseEvent, MouseEventType}; use zellij_utils::input::options::{ Clipboard, MobileLayoutConfiguration, NestedSessionHandling, PaneFrameStyle, }; use zellij_utils::ipc::{ExitReason, ServerToClientMsg}; use zellij_utils::pane_size::{PaneGeom, Size, SizeInPixels}; use zellij_utils::shared::{clean_string_from_control_and_linebreak, detect_theme_hue}; use zellij_utils::{ consts::{session_info_folder_for_session, ZELLIJ_SOCK_DIR}, envs::set_session_name, input::command::TerminalAction, input::layout::{ FloatingPaneLayout, Layout, PercentOrFixed, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TabLayoutInfo, TiledPaneLayout, }, position::Position, }; use crate::background_jobs::BackgroundJob; use crate::os_input_output::ResizeCache; use crate::pane_groups::PaneGroups; use crate::panes::alacritty_functions::xparse_color; use crate::panes::nested_session_modal::GuestModalShortcuts; use crate::panes::terminal_character::AnsiCode; use crate::panes::terminal_pane::{BRACKETED_PASTE_BEGIN, BRACKETED_PASTE_END}; use crate::session_layout_metadata::{PaneLayoutMetadata, SessionLayoutMetadata}; use crate::{ nested_guest::NestedGuestTracker, output::{HostKittyState, Output}, panes::kitty_graphics::{KittyHostSupport, KittyImageStore}, panes::sixel::SixelImageStore, panes::PaneId, plugins::{DumpSessionLayoutResponse, PluginId, PluginInstruction, PluginRenderAsset}, pty::{get_default_shell, ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, pty_writer::PtyWriteInstruction, tab::{GuestChoiceIndicator, SuppressedPanes, Tab}, thread_bus::Bus, ui::loading_indication::LoadingIndication, ClientId, ServerInstruction, }; use zellij_utils::{ data::{Event, InputMode, ModeInfo, Palette, PaletteColor, PluginCapabilities, Style}, errors::{ContextType, ScreenContext}, input::get_mode_info, ipc::{ClientAttributes, PixelDimensions}, nested_session::{self, NestedSessionCapability, NestedSessionMessage}, }; use crate::mobile_mode::{MobileRenderGate, MobileState, ShadowFocusOutcome, FIT_RESIZE_MAX_ITERS}; /// Parses a namespaced OSC 99 response and extracts the original pane ID /// and un-namespaced response bytes. /// /// Input bytes (the termwiz OperatingSystemCommand payload after stripping "99;"): /// e.g. b"i=p42.mynotif" or b"i=p42.mynotif:p=close;some_data" /// /// Returns Some((terminal_id, full_osc_bytes)) where full_osc_bytes is /// the complete reconstructed OSC 99 sequence with original identifier, /// ready to write to the pane's PTY. /// Denormalizes a namespaced OSC 99 response. /// /// Parses the namespaced `i=p[r][q].` format and returns: /// - `pane_id`: the terminal pane that originated the notification /// - `app_wants_report`: `r` flag — app originally requested `a=report` /// - `is_query`: `q` flag — this was a capability query (`p=?`) /// - `restored_response_bytes`: the response with the original `i=` value restored pub(crate) fn denormalize_notification_response( payload: &[u8], ) -> Option<(u32, bool, bool, Vec)> { let payload_str = str::from_utf8(payload).ok()?; // Split into metadata and response payload on first ';' let (metadata, response_payload) = match payload_str.find(';') { Some(idx) => ( payload_str.get(..idx).unwrap_or_default(), payload_str.get(idx..).unwrap_or_default(), ), None => (payload_str, ""), }; // Find the i= key in colon-separated metadata let mut terminal_id = None; let mut app_wants_report = false; let mut is_query = false; let mut restored_parts = Vec::new(); for kv in metadata.split(':') { if let Some(namespaced_value) = kv.strip_prefix("i=p") { // Parse "p[r][q]." if let Some(dot_pos) = namespaced_value.find('.') { let flags_part = namespaced_value.get(..dot_pos).unwrap_or_default(); let original_id = namespaced_value.get(dot_pos + 1..).unwrap_or_default(); let pane_id_str = flags_part.trim_end_matches(|c| c == 'r' || c == 'q'); let flag_chars = flags_part.get(pane_id_str.len()..).unwrap_or_default(); if let Ok(pid) = pane_id_str.parse::() { terminal_id = Some(pid); app_wants_report = flag_chars.contains('r'); is_query = flag_chars.contains('q'); // Empty original_id means the app never sent an i= key; // don't inject one into the response if !original_id.is_empty() { restored_parts.push(format!("i={}", original_id)); } continue; } } } restored_parts.push(kv.to_string()); } let terminal_id = terminal_id?; let restored_metadata = restored_parts.join(":"); let full_response = format!("\x1b]99;{}{}\x1b\\", restored_metadata, response_payload); Some(( terminal_id, app_wants_report, is_query, full_response.into_bytes(), )) } /// Get the active tab and call a closure on it /// /// If no active tab can be found, an error is logged instead. /// /// # Parameters /// /// - screen: An instance of `Screen` to operate on /// - client_id: The client_id, usually taken from the `ScreenInstruction` that's being processed /// - closure: A closure satisfying `|tab: &mut Tab| -> ()` OR `|tab: &mut Tab| -> Result` (see /// '?' below) /// - ?: A literal "?", to append a `?` to the closure when it returns a `Result` type. This /// argument is optional and not needed when the closure returns `()` macro_rules! active_tab { ($screen:ident, $client_id:ident, $closure:expr) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { // This could be made more ergonomic by declaring the type of 'active_tab' in the // closure, known as "Type Ascription". Then we could hint the type here and forego the // "&mut Tab" in all the closures below... // See: https://github.com/rust-lang/rust/issues/23416 $closure(active_tab); }, Err(err) => Err::<(), _>(err).non_fatal(), }; }; // Same as above, but with an added `?` for when the close returns a `Result` type. ($screen:ident, $client_id:ident, $closure:expr, ?) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { $closure(active_tab)?; }, Err(err) => Err::<(), _>(err).non_fatal(), }; }; } macro_rules! active_tab_and_connected_client_id { ($screen:ident, $client_id:ident, $closure:expr) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { $closure(active_tab, $client_id); }, Err(_) => { if let Some(client_id) = $screen.get_first_client_id() { match $screen.get_active_tab_mut(client_id) { Ok(active_tab) => { $closure(active_tab, client_id); }, Err(err) => Err::<(), _>(err).non_fatal(), } } else { log::error!("No client ids in screen found"); }; }, } }; // Same as above, but with an added `?` for when the closure returns a `Result` type. ($screen:ident, $client_id:ident, $closure:expr, ?) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { $closure(active_tab, $client_id).non_fatal(); }, Err(_) => { if let Some(client_id) = $screen.get_first_client_id() { match $screen.get_active_tab_mut(client_id) { Ok(active_tab) => { $closure(active_tab, client_id)?; }, Err(err) => Err::<(), _>(err).non_fatal(), } } else { log::error!("No client ids in screen found"); }; }, } }; } macro_rules! active_tab_and_connected_client_id_with_first_tab_fallback { ($screen:ident, $client_id:ident, $closure:expr) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { $closure(active_tab, Some($client_id)); }, Err(_) => { if let Some(client_id) = $screen.get_first_client_id() { match $screen.get_active_tab_mut(client_id) { Ok(active_tab) => { $closure(active_tab, Some(client_id)); }, Err(err) => Err::<(), _>(err).non_fatal(), } } else { match $screen.get_indexed_tab_mut(0) { Some(first_tab) => { $closure(first_tab, None); }, None => { log::error!("Not tabs found!"); }, } }; }, } }; // Same as above, but with an added `?` for when the closure returns a `Result` type. ($screen:ident, $client_id:ident, $closure:expr, ?) => { match $screen.get_active_tab_mut($client_id) { Ok(active_tab) => { $closure(active_tab, Some($client_id)).non_fatal(); }, Err(_) => { if let Some(client_id) = $screen.get_first_client_id() { match $screen.get_active_tab_mut(client_id) { Ok(active_tab) => { $closure(active_tab, Some(client_id))?; }, Err(err) => Err::<(), _>(err).non_fatal(), } } else { match $screen.get_indexed_tab_mut(0) { Some(first_tab) => { $closure(first_tab, None)?; }, None => { log::error!("Not tabs found!"); }, } }; }, } }; } type InitialTitle = String; type HoldForCommand = Option; /// Result of overriding a single tab's layout #[derive(Debug, Clone)] pub struct TabOverrideResult { pub tab_index: usize, pub tab_name: Option, pub tiled_layout: TiledPaneLayout, pub floating_layouts: Vec, pub swap_tiled_layouts: Option>, pub swap_floating_layouts: Option>, pub new_terminal_pids: Vec<(u32, HoldForCommand)>, pub new_floating_pane_pids: Vec<(u32, HoldForCommand)>, pub plugin_ids: HashMap>, } /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] pub enum ScreenInstruction { PtyBytes(u32, VteBytes), PluginBytes(Vec), Render, RenderToClients, NewPane( PaneId, Option, HoldForCommand, Option, // invoked with NewPanePlacement, bool, // start suppressed ClientTabIndexOrPaneId, Option, // completion signal bool, // set_blocking ), OpenInPlaceEditor(PaneId, ClientTabIndexOrPaneId), TogglePaneEmbedOrFloating(ClientId, Option), ToggleFloatingPanes(ClientId, Option, Option), ShowFloatingPanes { client_id: ClientId, tab_id: Option, completion: Option, }, HideFloatingPanes { client_id: ClientId, tab_id: Option, completion: Option, }, AreFloatingPanesVisible { client_id: ClientId, tab_id: Option, completion: Option, }, WriteCharacter( Option, Vec, bool, ClientId, Option, ), // bool -> // is_kitty_keyboard_protocol Resize(ClientId, ResizeStrategy, Option), SwitchFocus(ClientId, Option), FocusNextPane(ClientId, Option), FocusPreviousPane(ClientId, Option), FocusLastPane(ClientId, Option), MoveFocusLeft(ClientId, Option), MoveFocusLeftOrPreviousTab(ClientId, Option), MoveFocusDown(ClientId, Option), MoveFocusUp(ClientId, Option), MoveFocusRight(ClientId, Option), MoveFocusRightOrNextTab(ClientId, Option), MovePane(ClientId, Option), MovePaneBackwards(ClientId, Option), MovePaneUp(ClientId, Option), MovePaneDown(ClientId, Option), MovePaneRight(ClientId, Option), MovePaneLeft(ClientId, Option), Exit, ClearScreen(ClientId, Option), DumpScreen( Option, ClientId, bool, Option, Option, Option, // cli_client_id - used to send output to the CLI client's STDOUT bool, // ansi - preserve ANSI styling in the dump output ), DumpLayout(Option, ClientId, Option), // PathBuf is the default configured // shell SaveSession(ClientId, Option), DumpLayoutToPlugin { plugin_id: PluginId, tab_index: Option, response_channel: crossbeam::channel::Sender, }, GetFocusedPaneInfo { client_id: ClientId, response_channel: crossbeam::channel::Sender, }, GetPaneInfo { pane_id: PaneId, response_channel: crossbeam::channel::Sender>, }, GetTabInfo { tab_id: usize, response_channel: crossbeam::channel::Sender>, }, EditScrollback(ClientId, bool, Option), GetPaneScrollback { pane_id: PaneId, client_id: ClientId, get_full_scrollback: bool, response_channel: crossbeam::channel::Sender, }, ScrollUp(ClientId, Option), ScrollUpAt(Position, ClientId, Option), ScrollDown(ClientId, Option), ScrollDownAt(Position, ClientId, Option), ScrollToBottom(ClientId, Option), ScrollToTop(ClientId, Option), PageScrollUp(ClientId, Option), PageScrollDown(ClientId, Option), HalfPageScrollUp(ClientId, Option), HalfPageScrollDown(ClientId, Option), ClearScroll(ClientId), CloseFocusedPane(ClientId, Option), ToggleActiveTerminalFullscreen(ClientId, Option), ToggleActiveTerminalNoUiFullscreen(ClientId, Option), TogglePaneFrames(Option), SetPaneFrameStyle(PaneFrameStyle, Option), SetSelectable(PaneId, bool), ShowPluginCursor(u32, ClientId, Option<(usize, usize)>), ClosePane( PaneId, Option, Option, Option, ), // i32 -> optional exit // status HoldPane(PaneId, Option, RunCommand), UpdatePaneName(Vec, ClientId, Option), UndoRenamePane(ClientId, Option), NewTab( Option, Option, Option, Vec, Option, // `None` falls back to `Screen::default_layout`'s swap layouts. ( Option>, Option>, ), Option>, // initial_panes bool, // block_on_first_terminal bool, // should_change_focus_to_new_tab (ClientId, bool), // bool -> is_web_client Option, // completion signal ), /// Apply layout to tab with given stable ID. /// /// The sixth parameter (usize) is a stable identifier (not position) from the /// NewTab → ApplyLayout async flow that passes IDs between threads. ApplyLayout( TiledPaneLayout, Vec, Vec<(u32, HoldForCommand)>, // new pane pids Vec<(u32, HoldForCommand)>, // new floating pane pids HashMap>, usize, // tab_id - stable identifier from NewTab instruction bool, // should change focus to new tab (ClientId, bool), // bool -> is_web_client Option, // regular completion signal Option<(u32, NotificationEnd)>, // blocking_terminal (terminal_id, completion_tx) ), SwitchTabNext(ClientId, Option), SwitchTabPrev(ClientId, Option), ToggleActiveSyncTab(ClientId, Option), CloseTab(ClientId, Option), GoToTab(u32, Option, Option), // this Option is a hacky workaround, please do not copy this behaviour GoToTabName( String, Option, // default_shell bool, Option, Option, ), ToggleTab(ClientId, Option), UpdateTabName(Vec, ClientId, Option), UndoRenameTab(ClientId, Option), MoveTabLeft(ClientId, Option), MoveTabRight(ClientId, Option), GoToTabWithId(usize, Option, Option), CloseTabWithId(usize, Option), RenameTabWithId(usize, Vec, Option), BreakPanesToTabWithId { pane_ids: Vec, tab_id: usize, should_change_focus_to_target_tab: bool, client_id: ClientId, completion_tx: Option, }, TerminalResize(Size), RecomputeTabSize(ClientId, Size), TerminalPixelDimensions(PixelDimensions), TerminalBackgroundColor(String), TerminalForegroundColor(String), TerminalColorRegisters(Vec<(usize, String)>), SetKittyGraphicsSupport { client_id: ClientId, supported: bool, }, /// A pane's Grid intercepted an app-in-pane whitelisted query; Screen /// assigns a token, queues the forward, and dispatches to the client. /// `query` carries the classified form so Screen can match on it /// directly (for cache-fallback synthesis) without re-parsing bytes. ForwardHostQuery { pane_id: PaneId, query: crate::host_query::HostQuery, }, NestedSessionMessageFromPane { pane_id: PaneId, message: NestedSessionMessage, }, NestedGuestPingTick { pane_id: PaneId, }, NestedSessionMessageFromHost { client_id: ClientId, message: NestedSessionMessage, }, GuestModalChoice { client_id: ClientId, pane_id: PaneId, outcome: GuestModalOutcome, }, /// The client observed the host's reply to a previously forwarded /// query (closed by the Primary-DA barrier or the 500 ms timeout). /// The reply bytes are written verbatim to the originating pane. ForwardedReplyFromHost { token: u32, reply_bytes: Vec, }, /// Internal: a forwarded reply (or its cache-fallback synthesis, /// or a locally-answered query like `ColorPaletteMode`) is ready /// to be delivered to the originating pane. Routed through the /// owning Tab so that the bytes are written to PTY *and* any /// PTY input that arrived while the pane was forward-paused is /// drained and re-fed through vte in stream order. This preserves /// query/reply ordering even when sync replies (DA1, DSR, DECQRM) /// would otherwise race ahead of an async host round-trip. ResumePaneAfterForward { pane_id: PaneId, reply_bytes: Vec, }, /// The host terminal reported a color-palette theme mode (DSR 997 reply /// to `CSI ? 996 n`, or unsolicited notification while `CSI ? 2031 h` /// is enabled). Triggers auto-theme switch (when configured), the /// `HostTerminalThemeChanged` plugin event, and per-pane DSR forwarding /// for panes that opted in via `CSI ? 2031 h`. HostTerminalThemeChanged(HostTerminalThemeMode), /// Manual theme actions issued via the CLI (e.g. `zellij action set-dark-theme`) /// or a keybinding. They share the same convergence point as /// `HostTerminalThemeChanged`, but additionally surface a CLI-friendly error /// via `NotificationEnd` if `theme_dark` and `theme_light` are not both set /// (the auto-switch gate). "Last one wins": these compete naively with /// terminal-driven notifications via the dedupe in /// `update_host_terminal_theme_mode`. SetDarkTheme(Option), SetLightTheme(Option), ToggleTheme(Option), ChangeMode( InputMode, Option, ClientId, Option, ), ChangeModeForAllClients(InputMode, Option, Option), MouseEvent(MouseEvent, ClientId, Option), Copy(ClientId, Option), AddClient( ClientId, bool, // is_web_client Size, // client viewport size Option, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus ), RemoveClient(ClientId), SuppressRenderUntilMobile(ClientId), MobileSizeSettled(ClientId), ForceMobileUngate(ClientId), UpdateSearch(Vec, ClientId, Option), SearchDown(ClientId, Option), SearchUp(ClientId, Option), SearchToggleCaseSensitivity(ClientId, Option), SearchToggleWholeWord(ClientId, Option), SearchToggleWrap(ClientId, Option), AddRedPaneFrameColorOverride(Vec, Option), // Option => optional error text ClearPaneFrameColorOverride(Vec), SetTabBellFlash(usize, bool), // tab_id, is_flashing PreviousSwapLayout(ClientId, Option), NextSwapLayout(ClientId, Option), OverrideLayout( Option, // cwd (applies to all tabs) Option, // default_shell (applies to all tabs) Vec, // layouts for each tab to override bool, // retain_existing_terminal_panes bool, // retain_existing_plugin_panes bool, // apply_only_to_focused_tab ClientId, Option, ), OverrideLayoutComplete( Vec, // results for each tab bool, // retain_existing_terminal_panes bool, // retain_existing_plugin_panes ClientId, Option, ), QueryTabNames(ClientId, Option), NewTiledPluginPane( RunPluginOrAlias, Option, bool, Option, ClientId, Option, Option, // tab_id bool, ), // Option is // optional pane title, bool is skip cache, Option is an optional cwd NewFloatingPluginPane( RunPluginOrAlias, Option, bool, Option, Option, ClientId, Option, Option, // tab_id bool, ), // Option is an // optional pane title, bool // is skip cache, Option is an optional cwd NewInPlacePluginPane( RunPluginOrAlias, Option, PaneId, bool, bool, ClientId, Option, Option, // tab_id bool, ), // Option is an // optional pane title, first bool is skip cache, second bool is close_replaced_pane StartOrReloadPluginPane(RunPluginOrAlias, Option, Option), AddPlugin( Option, // should_float bool, // should be opened in place bool, // close_replaced_pane RunPluginOrAlias, Option, // pane title Option, // tab index u32, // plugin id Option, Option, // cwd bool, // start suppressed Option, Option, // should focus plugin Option, Option, // completion signal ), UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, LaunchOrFocusPlugin( RunPluginOrAlias, bool, bool, bool, bool, Option, bool, ClientId, Option, Option, // tab_id ), // bools are: should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, Option is the pane id to replace, bool following it is skip_cache LaunchPlugin( RunPluginOrAlias, bool, bool, bool, Option, bool, Option, ClientId, Option, Option, // tab_id bool, ), // bools are: should_float, should_open_in_place, close_replaced_pane, Option is the pane id to replace, Option is an optional cwd, bool after is skip_cache SuppressPane(PaneId, ClientId), UnsuppressPane(PaneId, bool), // bool -> should float if hidden UnsuppressOrExpandPane(PaneId, bool), // bool -> should float if hidden FocusPaneWithId(PaneId, bool, bool, ClientId, Option), // bools: // should_float_if_hidden, // should_be_in_place_if_hidden RenamePane(PaneId, Vec, Option), RenameActivePane(Vec, ClientId, Option), RenameTab(usize, Vec, Option), RequestPluginPermissions( u32, // u32 - plugin_id PluginPermission, ), BreakPane(Option, ClientId, Option), BreakPaneRight(ClientId, Option), BreakPaneLeft(ClientId, Option), UpdateSessionInfos( BTreeMap, // String is the session name BTreeMap, // resurrectable sessions - ), ReplacePane( PaneId, HoldForCommand, Option, Option, bool, // close replaced pane ClientTabIndexOrPaneId, Option, // completion signal ), SerializeLayoutForResurrection, RenameSession(String, ClientId, Option), // String -> new name ListClientsMetadata(Option, ClientId, Option), // Option - default shell ListPanes { show_all: bool, response_channel: crossbeam::channel::Sender, }, ListTabs { client_id: ClientId, response_channel: crossbeam::channel::Sender, }, GetCurrentTabInfo { client_id: ClientId, response_channel: crossbeam::channel::Sender>, }, Reconfigure { client_id: ClientId, keybinds: Keybinds, default_mode: InputMode, theme: Styling, /// Resolved styling for `theme_dark`. When both this and /// `host_theme_light` are `Some`, Screen auto-switches the active /// palette in response to host CSI 2031 / DSR 997 notifications. host_theme_dark: Option, /// Resolved styling for `theme_light`. See `host_theme_dark`. host_theme_light: Option, simplified_ui: bool, default_shell: Option, pane_frame_style: PaneFrameStyle, copy_command: Option, copy_to_clipboard: Option, copy_on_select: bool, auto_layout: bool, rounded_corners: bool, hide_session_name: bool, pane_title_alignment: PaneTitleAlignment, pane_gap_x: usize, pane_gap_y: usize, pane_padding: PanePadding, stacked_resize: bool, stacked_pane_list: bool, default_editor: Option, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, visual_bell: bool, focus_follows_mouse: bool, mouse_click_through: bool, nested_session_handling: NestedSessionHandling, }, RerunCommandPane(u32, Option), // u32 - terminal pane id ResizePaneWithId(ResizeStrategy, PaneId), EditScrollbackForPaneWithId(PaneId, Option), WriteToPaneId(Vec, PaneId, Option), Paste(Vec, Option, ClientId, Option), SetPaneColor( PaneId, Option, Option, Option, ), WriteKeyToPaneId( Option, Vec, bool, // is_kitty_keyboard_protocol PaneId, Option, ), CopyTextToClipboard(String, u32), // String - text to copy, u32 - plugin_id MovePaneWithPaneId(PaneId), MovePaneWithPaneIdInDirection(PaneId, Direction), ClearScreenForPaneId(PaneId), ScrollUpInPaneId(PaneId), ScrollDownInPaneId(PaneId), ScrollToTopInPaneId(PaneId), ScrollToBottomInPaneId(PaneId), PageScrollUpInPaneId(PaneId), PageScrollDownInPaneId(PaneId), TogglePaneIdFullscreen(PaneId), SetTabFit { client_id: ClientId, tab_id: usize, fit: Option<(PaneId, Size)>, }, TogglePaneEmbedOrEjectForPaneId(PaneId), CloseTabWithIndex(usize), BreakPanesToNewTab { pane_ids: Vec, default_shell: Option, should_change_focus_to_new_tab: bool, new_tab_name: Option, client_id: ClientId, completion_tx: Option, }, BreakPanesToTabWithIndex { pane_ids: Vec, tab_index: usize, should_change_focus_to_new_tab: bool, client_id: ClientId, completion_tx: Option, }, ListClientsToPlugin(PluginId, ClientId), TogglePanePinned(ClientId, Option), SetFloatingPanePinned(PaneId, bool), StackPanes(Vec, ClientId, Option), ChangeFloatingPanesCoordinates( Vec<(PaneId, FloatingPaneCoordinates)>, Option, ), TogglePaneBorderless(PaneId, Option), SetPaneBorderless(PaneId, bool, Option), AddHighlightPaneFrameColorOverride(Vec, Option), // Option => optional // message GroupAndUngroupPanes(Vec, Vec, bool, ClientId), // panes_to_group, panes_to_ungroup, bool -> for all clients HighlightAndUnhighlightPanes(Vec, Vec, ClientId), // panes_to_highlight, panes_to_unhighlight FloatMultiplePanes(Vec, ClientId), EmbedMultiplePanes(Vec, ClientId), TogglePaneInGroup(ClientId, Option), ToggleGroupMarking(ClientId, Option), SessionSharingStatusChange(bool), SetMouseSelectionSupport(PaneId, bool), InterceptKeyPresses(PluginId, ClientId), ClearKeyPressesIntercepts(ClientId), ReplacePaneWithExistingPane(PaneId, PaneId, bool, Option), // bool -> suppress_replaced_pane AddWatcherClient(ClientId, Size), RemoveWatcherClient(ClientId), SetFollowedClient(ClientId), WatcherTerminalResize(ClientId, Size), ClearMouseHelpText(ClientId), UpdateAvailableLayouts(Vec, Vec), SetPluginRegexHighlights { pane_id: PaneId, plugin_id: u32, highlights: Vec, }, ClearPluginHighlights { pane_id: PaneId, plugin_id: u32, }, ClearAllPluginHighlights(u32), // plugin_id — clears across all panes SubscribeToPaneRenders { client_id: ClientId, pane_ids: Vec, scrollback: Option, ansi: bool, }, NotifyPaneClosedToSubscribers { pane_id: zellij_utils::data::PaneId, }, DesktopNotificationResponse(Vec, ClientId), PluginSubscribedToAnsiPaneContents(bool), // true = at least one plugin needs ANSI content UpdateBackgroundPluginSubscriptions(PluginId, ClientId, HashSet), ClearHintTextCache, BroadcastModeUpdate(ModeInfo, Option), // ModeInfo, optional specific client_id (None = all clients) // Pane-targeting CLI variants ScrollUpWithPaneId(PaneId, Option), ScrollDownWithPaneId(PaneId, Option), ScrollToTopWithPaneId(PaneId, Option), ScrollToBottomWithPaneId(PaneId, Option), PageScrollUpWithPaneId(PaneId, Option), PageScrollDownWithPaneId(PaneId, Option), HalfPageScrollUpWithPaneId(PaneId, Option), HalfPageScrollDownWithPaneId(PaneId, Option), ResizeWithPaneId(PaneId, ResizeStrategy, Option), MovePaneWithPaneIdCli(PaneId, Option, Option), MovePaneBackwardsWithPaneId(PaneId, Option), ClearScreenWithPaneId(PaneId, Option), EditScrollbackWithPaneId(PaneId, bool, Option), ToggleFullscreenWithPaneId(PaneId, Option), ToggleNoUiFullscreenWithPaneId(PaneId, Option), TogglePaneEmbedOrFloatingWithPaneId(PaneId, Option), CloseFocusWithPaneId(PaneId, Option), RenamePaneWithPaneId(PaneId, Vec, Option), UndoRenamePaneWithPaneId(PaneId, Option), TogglePanePinnedWithPaneId(PaneId, Option), // Tab-targeting CLI variants UndoRenameTabWithTabId(usize, Option), ToggleActiveSyncTabWithTabId(usize, Option), ToggleFloatingPanesWithTabId(usize, Option, Option), PreviousSwapLayoutWithTabId(usize, Option), NextSwapLayoutWithTabId(usize, Option), MoveTabWithTabId(usize, Direction, Option), EnterMobileMode(ClientId, Option), ExitMobileMode(ClientId, Option), ToggleMobileMode(ClientId, Option), ReevaluateMobileMode { client_id: ClientId, new_size: Size, mobile_layout: MobileLayoutConfiguration, threshold_cols: u16, threshold_rows: u16, }, SetSoftKeyboard { client_id: ClientId, on: bool, }, SetShadowFocus(ClientId, PaneId), FocusHostSession(ClientId, Option), FocusGuestSession(ClientId, Option), ToggleHostFullscreen(ClientId, Option), } impl From<&ScreenInstruction> for ScreenContext { fn from(screen_instruction: &ScreenInstruction) -> Self { match *screen_instruction { ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes, ScreenInstruction::PluginBytes(..) => ScreenContext::PluginBytes, ScreenInstruction::Render => ScreenContext::Render, ScreenInstruction::RenderToClients => ScreenContext::RenderToClients, ScreenInstruction::NewPane(..) => ScreenContext::NewPane, ScreenInstruction::OpenInPlaceEditor(..) => ScreenContext::OpenInPlaceEditor, ScreenInstruction::TogglePaneEmbedOrFloating(..) => { ScreenContext::TogglePaneEmbedOrFloating }, ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes, ScreenInstruction::ShowFloatingPanes { .. } => ScreenContext::ShowFloatingPanes, ScreenInstruction::HideFloatingPanes { .. } => ScreenContext::HideFloatingPanes, ScreenInstruction::AreFloatingPanesVisible { .. } => { ScreenContext::AreFloatingPanesVisible }, ScreenInstruction::WriteCharacter(..) => ScreenContext::WriteCharacter, ScreenInstruction::Resize(.., strategy, _) => match strategy { ResizeStrategy { resize: Resize::Increase, direction, .. } => match direction { Some(Direction::Left) => ScreenContext::ResizeIncreaseLeft, Some(Direction::Down) => ScreenContext::ResizeIncreaseDown, Some(Direction::Up) => ScreenContext::ResizeIncreaseUp, Some(Direction::Right) => ScreenContext::ResizeIncreaseRight, None => ScreenContext::ResizeIncreaseAll, }, ResizeStrategy { resize: Resize::Decrease, direction, .. } => match direction { Some(Direction::Left) => ScreenContext::ResizeDecreaseLeft, Some(Direction::Down) => ScreenContext::ResizeDecreaseDown, Some(Direction::Up) => ScreenContext::ResizeDecreaseUp, Some(Direction::Right) => ScreenContext::ResizeDecreaseRight, None => ScreenContext::ResizeDecreaseAll, }, }, ScreenInstruction::SwitchFocus(..) => ScreenContext::SwitchFocus, ScreenInstruction::FocusNextPane(..) => ScreenContext::FocusNextPane, ScreenInstruction::FocusPreviousPane(..) => ScreenContext::FocusPreviousPane, ScreenInstruction::FocusLastPane(..) => ScreenContext::FocusLastPane, ScreenInstruction::MoveFocusLeft(..) => ScreenContext::MoveFocusLeft, ScreenInstruction::MoveFocusLeftOrPreviousTab(..) => { ScreenContext::MoveFocusLeftOrPreviousTab }, ScreenInstruction::MoveFocusDown(..) => ScreenContext::MoveFocusDown, ScreenInstruction::MoveFocusUp(..) => ScreenContext::MoveFocusUp, ScreenInstruction::MoveFocusRight(..) => ScreenContext::MoveFocusRight, ScreenInstruction::MoveFocusRightOrNextTab(..) => { ScreenContext::MoveFocusRightOrNextTab }, ScreenInstruction::MovePane(..) => ScreenContext::MovePane, ScreenInstruction::MovePaneBackwards(..) => ScreenContext::MovePaneBackwards, ScreenInstruction::MovePaneDown(..) => ScreenContext::MovePaneDown, ScreenInstruction::MovePaneUp(..) => ScreenContext::MovePaneUp, ScreenInstruction::MovePaneRight(..) => ScreenContext::MovePaneRight, ScreenInstruction::MovePaneLeft(..) => ScreenContext::MovePaneLeft, ScreenInstruction::Exit => ScreenContext::Exit, ScreenInstruction::ClearScreen(..) => ScreenContext::ClearScreen, ScreenInstruction::DumpScreen(..) => ScreenContext::DumpScreen, ScreenInstruction::DumpLayout(..) => ScreenContext::DumpLayout, ScreenInstruction::SaveSession(..) => ScreenContext::SaveSession, ScreenInstruction::DumpLayoutToPlugin { .. } => ScreenContext::DumpLayoutToPlugin, ScreenInstruction::GetFocusedPaneInfo { .. } => ScreenContext::GetFocusedPaneInfo, ScreenInstruction::GetPaneInfo { .. } => ScreenContext::GetPaneInfo, ScreenInstruction::GetTabInfo { .. } => ScreenContext::GetTabInfo, ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, ScreenInstruction::GetPaneScrollback { .. } => ScreenContext::GetPaneScrollback, ScreenInstruction::ScrollUp(..) => ScreenContext::ScrollUp, ScreenInstruction::ScrollDown(..) => ScreenContext::ScrollDown, ScreenInstruction::ScrollToBottom(..) => ScreenContext::ScrollToBottom, ScreenInstruction::ScrollToTop(..) => ScreenContext::ScrollToTop, ScreenInstruction::PageScrollUp(..) => ScreenContext::PageScrollUp, ScreenInstruction::PageScrollDown(..) => ScreenContext::PageScrollDown, ScreenInstruction::HalfPageScrollUp(..) => ScreenContext::HalfPageScrollUp, ScreenInstruction::HalfPageScrollDown(..) => ScreenContext::HalfPageScrollDown, ScreenInstruction::ClearScroll(..) => ScreenContext::ClearScroll, ScreenInstruction::CloseFocusedPane(..) => ScreenContext::CloseFocusedPane, ScreenInstruction::ToggleActiveTerminalFullscreen(..) => { ScreenContext::ToggleActiveTerminalFullscreen }, ScreenInstruction::ToggleActiveTerminalNoUiFullscreen(..) => { ScreenContext::ToggleActiveTerminalNoUiFullscreen }, ScreenInstruction::TogglePaneFrames(..) => ScreenContext::TogglePaneFrames, ScreenInstruction::SetPaneFrameStyle(..) => ScreenContext::SetPaneFrameStyle, ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, ScreenInstruction::ShowPluginCursor(..) => ScreenContext::ShowPluginCursor, ScreenInstruction::ClosePane(..) => ScreenContext::ClosePane, ScreenInstruction::HoldPane(..) => ScreenContext::HoldPane, ScreenInstruction::UpdatePaneName(..) => ScreenContext::UpdatePaneName, ScreenInstruction::UndoRenamePane(..) => ScreenContext::UndoRenamePane, ScreenInstruction::NewTab(..) => ScreenContext::NewTab, ScreenInstruction::ApplyLayout(..) => ScreenContext::ApplyLayout, ScreenInstruction::SwitchTabNext(..) => ScreenContext::SwitchTabNext, ScreenInstruction::SwitchTabPrev(..) => ScreenContext::SwitchTabPrev, ScreenInstruction::CloseTab(..) => ScreenContext::CloseTab, ScreenInstruction::GoToTab(..) => ScreenContext::GoToTab, ScreenInstruction::GoToTabName(..) => ScreenContext::GoToTabName, ScreenInstruction::UpdateTabName(..) => ScreenContext::UpdateTabName, ScreenInstruction::UndoRenameTab(..) => ScreenContext::UndoRenameTab, ScreenInstruction::MoveTabLeft(..) => ScreenContext::MoveTabLeft, ScreenInstruction::MoveTabRight(..) => ScreenContext::MoveTabRight, ScreenInstruction::GoToTabWithId(..) => ScreenContext::GoToTabWithId, ScreenInstruction::CloseTabWithId(..) => ScreenContext::CloseTabWithId, ScreenInstruction::RenameTabWithId(..) => ScreenContext::RenameTabWithId, ScreenInstruction::BreakPanesToTabWithId { .. } => ScreenContext::BreakPanesToTabWithId, ScreenInstruction::TerminalResize(..) => ScreenContext::TerminalResize, ScreenInstruction::RecomputeTabSize(..) => ScreenContext::RecomputeTabSize, ScreenInstruction::TerminalPixelDimensions(..) => { ScreenContext::TerminalPixelDimensions }, ScreenInstruction::TerminalBackgroundColor(..) => { ScreenContext::TerminalBackgroundColor }, ScreenInstruction::TerminalForegroundColor(..) => { ScreenContext::TerminalForegroundColor }, ScreenInstruction::TerminalColorRegisters(..) => ScreenContext::TerminalColorRegisters, ScreenInstruction::SetKittyGraphicsSupport { .. } => { ScreenContext::SetKittyGraphicsSupport }, ScreenInstruction::ForwardHostQuery { .. } => ScreenContext::ForwardHostQuery, ScreenInstruction::NestedSessionMessageFromPane { .. } => { ScreenContext::NestedSessionMessageFromPane }, ScreenInstruction::NestedGuestPingTick { .. } => ScreenContext::NestedGuestPingTick, ScreenInstruction::NestedSessionMessageFromHost { .. } => { ScreenContext::NestedSessionMessageFromHost }, ScreenInstruction::GuestModalChoice { .. } => ScreenContext::GuestModalChoice, ScreenInstruction::ForwardedReplyFromHost { .. } => { ScreenContext::ForwardedReplyFromHost }, ScreenInstruction::ResumePaneAfterForward { .. } => { ScreenContext::ResumePaneAfterForward }, ScreenInstruction::HostTerminalThemeChanged(..) => { ScreenContext::HostTerminalThemeChanged }, ScreenInstruction::SetDarkTheme(..) => ScreenContext::SetDarkTheme, ScreenInstruction::SetLightTheme(..) => ScreenContext::SetLightTheme, ScreenInstruction::ToggleTheme(..) => ScreenContext::ToggleTheme, ScreenInstruction::ChangeMode(..) => ScreenContext::ChangeMode, ScreenInstruction::ChangeModeForAllClients(..) => { ScreenContext::ChangeModeForAllClients }, ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab, ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, ScreenInstruction::MouseEvent(..) => ScreenContext::MouseEvent, ScreenInstruction::Copy(..) => ScreenContext::Copy, ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, ScreenInstruction::RemoveClient(..) => ScreenContext::RemoveClient, ScreenInstruction::SuppressRenderUntilMobile(..) => { ScreenContext::SuppressRenderUntilMobile }, ScreenInstruction::MobileSizeSettled(..) => ScreenContext::MobileSizeSettled, ScreenInstruction::ForceMobileUngate(..) => ScreenContext::ForceMobileUngate, ScreenInstruction::UpdateSearch(..) => ScreenContext::UpdateSearch, ScreenInstruction::SearchDown(..) => ScreenContext::SearchDown, ScreenInstruction::SearchUp(..) => ScreenContext::SearchUp, ScreenInstruction::SearchToggleCaseSensitivity(..) => { ScreenContext::SearchToggleCaseSensitivity }, ScreenInstruction::SearchToggleWholeWord(..) => ScreenContext::SearchToggleWholeWord, ScreenInstruction::SearchToggleWrap(..) => ScreenContext::SearchToggleWrap, ScreenInstruction::AddRedPaneFrameColorOverride(..) => { ScreenContext::AddRedPaneFrameColorOverride }, ScreenInstruction::ClearPaneFrameColorOverride(..) => { ScreenContext::ClearPaneFrameColorOverride }, ScreenInstruction::SetTabBellFlash(..) => ScreenContext::SetTabBellFlash, ScreenInstruction::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout, ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout, ScreenInstruction::OverrideLayout(..) => ScreenContext::OverrideLayout, ScreenInstruction::OverrideLayoutComplete(..) => ScreenContext::OverrideLayoutComplete, ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames, ScreenInstruction::NewTiledPluginPane(..) => ScreenContext::NewTiledPluginPane, ScreenInstruction::NewFloatingPluginPane(..) => ScreenContext::NewFloatingPluginPane, ScreenInstruction::StartOrReloadPluginPane(..) => { ScreenContext::StartOrReloadPluginPane }, ScreenInstruction::AddPlugin(..) => ScreenContext::AddPlugin, ScreenInstruction::UpdatePluginLoadingStage(..) => { ScreenContext::UpdatePluginLoadingStage }, ScreenInstruction::ProgressPluginLoadingOffset(..) => { ScreenContext::ProgressPluginLoadingOffset }, ScreenInstruction::StartPluginLoadingIndication(..) => { ScreenContext::StartPluginLoadingIndication }, ScreenInstruction::RequestStateUpdateForPlugins => { ScreenContext::RequestStateUpdateForPlugins }, ScreenInstruction::LaunchOrFocusPlugin(..) => ScreenContext::LaunchOrFocusPlugin, ScreenInstruction::LaunchPlugin(..) => ScreenContext::LaunchPlugin, ScreenInstruction::SuppressPane(..) => ScreenContext::SuppressPane, ScreenInstruction::UnsuppressPane(..) => ScreenContext::UnsuppressPane, ScreenInstruction::UnsuppressOrExpandPane(..) => ScreenContext::UnsuppressOrExpandPane, ScreenInstruction::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, ScreenInstruction::RenameActivePane(..) => ScreenContext::RenameActivePane, ScreenInstruction::RenameTab(..) => ScreenContext::RenameTab, ScreenInstruction::RequestPluginPermissions(..) => { ScreenContext::RequestPluginPermissions }, ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane, ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight, ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft, ScreenInstruction::UpdateSessionInfos(..) => ScreenContext::UpdateSessionInfos, ScreenInstruction::ReplacePane(..) => ScreenContext::ReplacePane, ScreenInstruction::NewInPlacePluginPane(..) => ScreenContext::NewInPlacePluginPane, ScreenInstruction::SerializeLayoutForResurrection => { ScreenContext::SerializeLayoutForResurrection }, ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession, ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata, ScreenInstruction::ListPanes { .. } => ScreenContext::ListPanes, ScreenInstruction::ListTabs { .. } => ScreenContext::ListTabs, ScreenInstruction::GetCurrentTabInfo { .. } => ScreenContext::GetCurrentTabInfo, ScreenInstruction::Reconfigure { .. } => ScreenContext::Reconfigure, ScreenInstruction::RerunCommandPane { .. } => ScreenContext::RerunCommandPane, ScreenInstruction::ResizePaneWithId(..) => ScreenContext::ResizePaneWithId, ScreenInstruction::EditScrollbackForPaneWithId(..) => { ScreenContext::EditScrollbackForPaneWithId }, ScreenInstruction::WriteToPaneId(..) => ScreenContext::WriteToPaneId, ScreenInstruction::Paste(..) => ScreenContext::Paste, ScreenInstruction::SetPaneColor(..) => ScreenContext::SetPaneColor, ScreenInstruction::WriteKeyToPaneId(..) => ScreenContext::WriteKeyToPaneId, ScreenInstruction::CopyTextToClipboard(..) => ScreenContext::CopyTextToClipboard, ScreenInstruction::MovePaneWithPaneId(..) => ScreenContext::MovePaneWithPaneId, ScreenInstruction::MovePaneWithPaneIdInDirection(..) => { ScreenContext::MovePaneWithPaneIdInDirection }, ScreenInstruction::ClearScreenForPaneId(..) => ScreenContext::ClearScreenForPaneId, ScreenInstruction::ScrollUpInPaneId(..) => ScreenContext::ScrollUpInPaneId, ScreenInstruction::ScrollDownInPaneId(..) => ScreenContext::ScrollDownInPaneId, ScreenInstruction::ScrollToTopInPaneId(..) => ScreenContext::ScrollToTopInPaneId, ScreenInstruction::ScrollToBottomInPaneId(..) => ScreenContext::ScrollToBottomInPaneId, ScreenInstruction::PageScrollUpInPaneId(..) => ScreenContext::PageScrollUpInPaneId, ScreenInstruction::PageScrollDownInPaneId(..) => ScreenContext::PageScrollDownInPaneId, ScreenInstruction::TogglePaneIdFullscreen(..) => ScreenContext::TogglePaneIdFullscreen, ScreenInstruction::SetTabFit { .. } => ScreenContext::SetTabFit, ScreenInstruction::TogglePaneEmbedOrEjectForPaneId(..) => { ScreenContext::TogglePaneEmbedOrEjectForPaneId }, ScreenInstruction::CloseTabWithIndex(..) => ScreenContext::CloseTabWithIndex, ScreenInstruction::BreakPanesToNewTab { .. } => ScreenContext::BreakPanesToNewTab, ScreenInstruction::BreakPanesToTabWithIndex { .. } => { ScreenContext::BreakPanesToTabWithIndex }, ScreenInstruction::ListClientsToPlugin(..) => ScreenContext::ListClientsToPlugin, ScreenInstruction::TogglePanePinned(..) => ScreenContext::TogglePanePinned, ScreenInstruction::SetFloatingPanePinned(..) => ScreenContext::SetFloatingPanePinned, ScreenInstruction::StackPanes(..) => ScreenContext::StackPanes, ScreenInstruction::ChangeFloatingPanesCoordinates(..) => { ScreenContext::ChangeFloatingPanesCoordinates }, ScreenInstruction::TogglePaneBorderless(..) => ScreenContext::TogglePaneBorderless, ScreenInstruction::SetPaneBorderless(..) => ScreenContext::SetPaneBorderless, ScreenInstruction::AddHighlightPaneFrameColorOverride(..) => { ScreenContext::AddHighlightPaneFrameColorOverride }, ScreenInstruction::GroupAndUngroupPanes(..) => ScreenContext::GroupAndUngroupPanes, ScreenInstruction::HighlightAndUnhighlightPanes(..) => { ScreenContext::HighlightAndUnhighlightPanes }, ScreenInstruction::FloatMultiplePanes(..) => ScreenContext::FloatMultiplePanes, ScreenInstruction::EmbedMultiplePanes(..) => ScreenContext::EmbedMultiplePanes, ScreenInstruction::TogglePaneInGroup(..) => ScreenContext::TogglePaneInGroup, ScreenInstruction::ToggleGroupMarking(..) => ScreenContext::ToggleGroupMarking, ScreenInstruction::SessionSharingStatusChange(..) => { ScreenContext::SessionSharingStatusChange }, ScreenInstruction::SetMouseSelectionSupport(..) => { ScreenContext::SetMouseSelectionSupport }, ScreenInstruction::InterceptKeyPresses(..) => ScreenContext::InterceptKeyPresses, ScreenInstruction::ClearKeyPressesIntercepts(..) => { ScreenContext::ClearKeyPressesIntercepts }, ScreenInstruction::ReplacePaneWithExistingPane(..) => { ScreenContext::ReplacePaneWithExistingPane }, ScreenInstruction::AddWatcherClient(..) => ScreenContext::AddWatcherClient, ScreenInstruction::RemoveWatcherClient(..) => ScreenContext::RemoveWatcherClient, ScreenInstruction::SetFollowedClient(..) => ScreenContext::SetFollowedClient, ScreenInstruction::WatcherTerminalResize(..) => ScreenContext::WatcherTerminalResize, ScreenInstruction::ClearMouseHelpText(..) => ScreenContext::ClearMouseHelpText, ScreenInstruction::UpdateAvailableLayouts(..) => ScreenContext::UpdateAvailableLayouts, ScreenInstruction::SetPluginRegexHighlights { .. } => { ScreenContext::SetPluginRegexHighlights }, ScreenInstruction::ClearPluginHighlights { .. } => ScreenContext::ClearPluginHighlights, ScreenInstruction::ClearAllPluginHighlights(..) => ScreenContext::ClearPluginHighlights, ScreenInstruction::DesktopNotificationResponse(..) => { ScreenContext::DesktopNotificationResponse }, ScreenInstruction::SubscribeToPaneRenders { .. } => { ScreenContext::SubscribeToPaneRenders }, ScreenInstruction::NotifyPaneClosedToSubscribers { .. } => { ScreenContext::NotifyPaneClosedToSubscribers }, ScreenInstruction::PluginSubscribedToAnsiPaneContents(..) => { ScreenContext::PluginSubscribedToAnsiPaneContents }, ScreenInstruction::UpdateBackgroundPluginSubscriptions(..) => { ScreenContext::UpdateBackgroundPluginSubscriptions }, ScreenInstruction::ClearHintTextCache => ScreenContext::ClearHintTextCache, ScreenInstruction::BroadcastModeUpdate(..) => ScreenContext::BroadcastModeUpdate, // Pane-targeting CLI variants ScreenInstruction::ScrollUpWithPaneId(..) => ScreenContext::ScrollUpWithPaneId, ScreenInstruction::ScrollDownWithPaneId(..) => ScreenContext::ScrollDownWithPaneId, ScreenInstruction::ScrollToTopWithPaneId(..) => ScreenContext::ScrollToTopWithPaneId, ScreenInstruction::ScrollToBottomWithPaneId(..) => { ScreenContext::ScrollToBottomWithPaneId }, ScreenInstruction::PageScrollUpWithPaneId(..) => ScreenContext::PageScrollUpWithPaneId, ScreenInstruction::PageScrollDownWithPaneId(..) => { ScreenContext::PageScrollDownWithPaneId }, ScreenInstruction::HalfPageScrollUpWithPaneId(..) => { ScreenContext::HalfPageScrollUpWithPaneId }, ScreenInstruction::HalfPageScrollDownWithPaneId(..) => { ScreenContext::HalfPageScrollDownWithPaneId }, ScreenInstruction::ResizeWithPaneId(..) => ScreenContext::ResizeWithPaneId, ScreenInstruction::MovePaneWithPaneIdCli(..) => ScreenContext::MovePaneWithPaneIdCli, ScreenInstruction::MovePaneBackwardsWithPaneId(..) => { ScreenContext::MovePaneBackwardsWithPaneId }, ScreenInstruction::ClearScreenWithPaneId(..) => ScreenContext::ClearScreenWithPaneId, ScreenInstruction::EditScrollbackWithPaneId(..) => { ScreenContext::EditScrollbackWithPaneId }, ScreenInstruction::ToggleFullscreenWithPaneId(..) => { ScreenContext::ToggleFullscreenWithPaneId }, ScreenInstruction::ToggleNoUiFullscreenWithPaneId(..) => { ScreenContext::ToggleNoUiFullscreenWithPaneId }, ScreenInstruction::TogglePaneEmbedOrFloatingWithPaneId(..) => { ScreenContext::TogglePaneEmbedOrFloatingWithPaneId }, ScreenInstruction::CloseFocusWithPaneId(..) => ScreenContext::CloseFocusWithPaneId, ScreenInstruction::RenamePaneWithPaneId(..) => ScreenContext::RenamePaneWithPaneId, ScreenInstruction::UndoRenamePaneWithPaneId(..) => { ScreenContext::UndoRenamePaneWithPaneId }, ScreenInstruction::TogglePanePinnedWithPaneId(..) => { ScreenContext::TogglePanePinnedWithPaneId }, // Tab-targeting CLI variants ScreenInstruction::UndoRenameTabWithTabId(..) => ScreenContext::UndoRenameTabWithTabId, ScreenInstruction::ToggleActiveSyncTabWithTabId(..) => { ScreenContext::ToggleActiveSyncTabWithTabId }, ScreenInstruction::ToggleFloatingPanesWithTabId(..) => { ScreenContext::ToggleFloatingPanesWithTabId }, ScreenInstruction::PreviousSwapLayoutWithTabId(..) => { ScreenContext::PreviousSwapLayoutWithTabId }, ScreenInstruction::NextSwapLayoutWithTabId(..) => { ScreenContext::NextSwapLayoutWithTabId }, ScreenInstruction::MoveTabWithTabId(..) => ScreenContext::MoveTabWithTabId, ScreenInstruction::EnterMobileMode(..) => ScreenContext::EnterMobileMode, ScreenInstruction::ExitMobileMode(..) => ScreenContext::ExitMobileMode, ScreenInstruction::ToggleMobileMode(..) => ScreenContext::ToggleMobileMode, ScreenInstruction::ReevaluateMobileMode { .. } => ScreenContext::ReevaluateMobileMode, ScreenInstruction::SetSoftKeyboard { .. } => ScreenContext::SetSoftKeyboard, ScreenInstruction::SetShadowFocus(..) => ScreenContext::SetShadowFocus, ScreenInstruction::FocusHostSession(..) => ScreenContext::FocusHostSession, ScreenInstruction::FocusGuestSession(..) => ScreenContext::FocusGuestSession, ScreenInstruction::ToggleHostFullscreen(..) => ScreenContext::ToggleHostFullscreen, } } } #[derive(Debug, Clone)] pub(crate) struct CopyOptions { pub command: Option, pub clipboard: Clipboard, pub copy_on_select: bool, } impl CopyOptions { pub(crate) fn new( copy_command: Option, copy_clipboard: Clipboard, copy_on_select: bool, ) -> Self { Self { command: copy_command, clipboard: copy_clipboard, copy_on_select, } } #[cfg(test)] pub(crate) fn default() -> Self { Self { command: None, clipboard: Clipboard::default(), copy_on_select: true, } } } // We use this to delay rendering when a new tab opens so that we make sure all plugins // (representing portions of the UI) have been fully loaded before the tab is first rendered (with // a sensible timeout of 100ms) #[derive(Debug, Clone)] pub struct RenderBlocker { blocking_plugins: HashMap, #[cfg_attr(test, allow(dead_code))] timeout_ms: u64, } impl RenderBlocker { pub fn new(timeout_ms: u64) -> Self { Self { blocking_plugins: HashMap::new(), timeout_ms, } } pub fn register_blocking_plugin(&mut self, plugin_id: u32) { self.blocking_plugins.insert(plugin_id, Instant::now()); } pub fn remove_blocking_plugin(&mut self, plugin_id: u32) { self.blocking_plugins.remove(&plugin_id); } #[cfg(test)] pub fn can_render(&mut self) -> bool { // we want the tests to be more deterministic and so we always render without any // optimizations true } #[cfg(not(test))] pub fn can_render(&mut self) -> bool { let ret = if self.blocking_plugins.is_empty() { true } else { let timeout = Duration::from_millis(self.timeout_ms); let now = Instant::now(); self.blocking_plugins .values() .all(|®istered_at| now.duration_since(registered_at) >= timeout) }; if ret { self.blocking_plugins.clear(); } ret } } #[derive(Debug, Clone)] pub(crate) struct WatcherState { size: Size, should_force_render: bool, } impl WatcherState { pub fn new(size: Size) -> Self { WatcherState { size, should_force_render: true, } } pub fn size(&self) -> Size { self.size } pub fn set_size(&mut self, size: Size) { self.size = size; } pub fn should_force_render(&self) -> bool { self.should_force_render } pub fn clear_force_render(&mut self) { self.should_force_render = false; } pub fn set_force_render(&mut self) { self.should_force_render = true; } } struct PaneRenderSubscription { pane_ids: HashSet, previous_viewports: HashMap>, ansi: bool, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub(crate) enum NestedGuestChoice { Zoom, Descend, Dismissed, } impl NestedGuestChoice { fn enters_passthrough(&self) -> bool { matches!(self, NestedGuestChoice::Zoom | NestedGuestChoice::Descend) } } #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum GuestModalOutcome { Zoom, Descend, } fn format_guest_modal_shortcut(keys: &[KeyWithModifier]) -> Vec { keys.iter().map(|key| key.to_string()).collect() } /// A [`Screen`] holds multiple [`Tab`]s, each one holding multiple [`panes`](crate::client::panes). /// It only directly controls which tab is active, delegating the rest to the individual `Tab`. pub(crate) struct Screen { /// A Bus for sending and receiving messages with the other threads. pub bus: Bus, /// An optional maximal amount of panes allowed per [`Tab`] in this [`Screen`] instance. max_panes: Option, /// A map between this [`Screen`]'s tabs and their ID/key. tabs: BTreeMap, last_single_pane_tab_names: HashMap>, /// The full size of this [`Screen`]. size: Size, pixel_dimensions: PixelDimensions, character_cell_size: Rc>>, stacked_resize: Rc>, stacked_pane_list: Rc>, sixel_image_store: Rc>, kitty_image_store: Rc>, kitty_host_capabilities: Rc>>, client_kitty_host_state: Rc>>, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, connected_clients: Rc>>, // bool -> is_web_client /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_ids: BTreeMap, client_sizes: HashMap, global_last_active_tab_id: usize, tab_history: BTreeMap>, pane_history: BTreeMap>, mode_info: BTreeMap, default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication style: Style, pane_frame_style: PaneFrameStyle, auto_layout: bool, session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option, session_is_mirrored: bool, copy_options: CopyOptions, debug: bool, session_name: String, peer_sessions_cache: BTreeMap, // String is the session name, can // also be this session resurrectable_sessions_cache: BTreeMap, // String is the session name, // duration is its creation time default_layout: Box, default_shell: PathBuf, styled_underlines: bool, osc8_hyperlinks: bool, arrow_fonts: bool, #[cfg_attr(test, allow(dead_code))] layout_dir: Option, #[cfg_attr(test, allow(dead_code))] default_layout_name: Option, explicitly_disable_kitty_keyboard_protocol: bool, support_kitty_graphics_protocol: bool, default_editor: Option, web_clients_allowed: bool, web_sharing: WebSharing, current_pane_group: Rc>, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, visual_bell: bool, focus_follows_mouse: bool, mouse_click_through: bool, currently_marking_pane_group: Rc>>, // the below are the configured values - the ones that will be set if and when the web server // is brought online web_server_ip: IpAddr, web_server_port: u16, render_blocker: RenderBlocker, watcher_clients: HashMap, followed_client_id: Option, cached_layouts: Vec, cached_layout_errors: Vec, pane_render_subscribers: HashMap, plugins_need_ansi_pane_contents: bool, background_plugin_subscriptions: HashMap<(PluginId, ClientId), HashSet>, next_forward_token: u32, pending_forwarded_queries: HashMap, forward_queue: VecDeque, forward_in_flight_token: Option, nested_guest_tracker: NestedGuestTracker, nested_ancestry: Vec, nested_via_client_id: Option, nested_fullscreen_panes: HashSet, own_fullscreen_requested: bool, reported_guest_fullscreen: HashMap, host_fullscreen: bool, dimmed_clients: HashSet, nested_guest_choices: HashMap<(ClientId, PaneId), NestedGuestChoice>, guest_ascend_keys: HashMap>, host_descend_keys: Vec, host_descended: bool, host_terminal_theme_mode: Option, host_theme_dark_styling: Option, host_theme_light_styling: Option, nested_session_handling: NestedSessionHandling, mobile_state: MobileState, mobile_render_gate: MobileRenderGate, } /// A pending forward waiting to be dispatched once the current in-flight /// forward's barrier reply (or timeout) arrives. #[derive(Debug, Clone)] struct PendingForward { token: u32, pane_id: PaneId, query: crate::host_query::HostQuery, } /// A forward currently in flight (dispatched to the client, waiting /// for a reply). Retains the `HostQuery` classification so we can /// fall back to cache-synthesized replies when the live reply is /// empty (no client attached, client-side timeout, etc.) without /// re-parsing byte strings. #[derive(Debug, Clone)] struct PendingForwardEntry { pane_id: PaneId, query: crate::host_query::HostQuery, } /// Reserved sentinel token for Zellij's own startup batch of host /// queries (pixel dims, bg/fg, sync-output, palette registers). The /// existing fire-and-forget startup plumbing is unchanged today; the /// sentinel exists so a future migration can route the startup batch /// through the same forwarded-query mechanism as per-pane queries /// while keeping its replies routable to `Screen`'s cached state /// rather than to a pane's pty. Reserved value 0 is excluded from /// `next_forward_token` allocation by the wrap-skip in /// `forward_host_query`. const STARTUP_SENTINEL_TOKEN: u32 = 0; /// Server-side deadline for a forwarded host query. If no /// `ForwardedReplyFromHost` arrives within this window, the server /// synthesizes an empty reply for itself so the in-flight slot /// releases and queued forwards continue to drain. /// /// This recovers the worst-case where the connected client doesn't /// understand `ServerToClientMsg::ForwardQueryToHost` (an old client /// against a new server) — without it the slot would deadlock and /// every app-issued host query would hang. The window is generous /// relative to the 500 ms client-side deadline so a well-behaved new /// client always replies first; only the old-client and /// network-pathological cases ever see this fire. const SERVER_FORWARD_TIMEOUT_MS: u64 = 1000; impl Screen { /// Creates and returns a new [`Screen`]. pub fn new( bus: Bus, client_attributes: &ClientAttributes, max_panes: Option, mode_info: ModeInfo, pane_frame_style: PaneFrameStyle, auto_layout: bool, session_is_mirrored: bool, copy_options: CopyOptions, debug: bool, default_layout: Box, default_layout_name: Option, default_shell: PathBuf, session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option, styled_underlines: bool, osc8_hyperlinks: bool, arrow_fonts: bool, layout_dir: Option, explicitly_disable_kitty_keyboard_protocol: bool, support_kitty_graphics_protocol: bool, stacked_resize: bool, stacked_pane_list: bool, default_editor: Option, web_clients_allowed: bool, web_sharing: WebSharing, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, visual_bell: bool, focus_follows_mouse: bool, mouse_click_through: bool, web_server_ip: IpAddr, web_server_port: u16, nested_session_handling: NestedSessionHandling, ) -> Self { let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_info = SessionInfo::new(session_name.clone()); let mut peer_sessions_cache = BTreeMap::new(); let resurrectable_sessions_cache = BTreeMap::new(); peer_sessions_cache.insert(session_name.clone(), session_info); let current_pane_group = PaneGroups::new(bus.senders.clone()); Screen { bus, max_panes, size: client_attributes.size, pixel_dimensions: Default::default(), character_cell_size: Rc::new(RefCell::new(None)), stacked_resize: Rc::new(RefCell::new(stacked_resize)), stacked_pane_list: Rc::new(RefCell::new(stacked_pane_list)), sixel_image_store: Rc::new(RefCell::new(SixelImageStore::default())), kitty_image_store: Rc::new(RefCell::new(KittyImageStore::default())), kitty_host_capabilities: Rc::new(RefCell::new(HashMap::new())), client_kitty_host_state: Rc::new(RefCell::new(HashMap::new())), style: client_attributes.style, connected_clients: Rc::new(RefCell::new(HashMap::new())), active_tab_ids: BTreeMap::new(), client_sizes: HashMap::new(), global_last_active_tab_id: 0, tabs: BTreeMap::new(), last_single_pane_tab_names: HashMap::new(), terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())), terminal_emulator_color_codes: Rc::new(RefCell::new(HashMap::new())), tab_history: BTreeMap::new(), pane_history: BTreeMap::new(), mode_info: BTreeMap::new(), default_mode_info: mode_info, pane_frame_style, auto_layout, session_is_mirrored, copy_options, debug, session_name, peer_sessions_cache, default_layout, default_layout_name, default_shell, session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, styled_underlines, osc8_hyperlinks, arrow_fonts, resurrectable_sessions_cache, layout_dir, explicitly_disable_kitty_keyboard_protocol, support_kitty_graphics_protocol, default_editor, web_clients_allowed, web_sharing, current_pane_group: Rc::new(RefCell::new(current_pane_group)), currently_marking_pane_group: Rc::new(RefCell::new(HashMap::new())), advanced_mouse_actions, mouse_scroll_resize, mouse_hover_effects, visual_bell, focus_follows_mouse, mouse_click_through, web_server_ip, web_server_port, render_blocker: RenderBlocker::new(100), watcher_clients: HashMap::new(), followed_client_id: None, cached_layouts: vec![], cached_layout_errors: vec![], pane_render_subscribers: HashMap::new(), plugins_need_ansi_pane_contents: false, background_plugin_subscriptions: HashMap::new(), next_forward_token: 1, // 0 is reserved as the startup sentinel pending_forwarded_queries: HashMap::new(), forward_queue: VecDeque::new(), forward_in_flight_token: None, nested_guest_tracker: NestedGuestTracker::default(), nested_ancestry: vec![], nested_via_client_id: None, nested_fullscreen_panes: HashSet::new(), own_fullscreen_requested: false, reported_guest_fullscreen: HashMap::new(), host_fullscreen: false, dimmed_clients: HashSet::new(), nested_guest_choices: HashMap::new(), guest_ascend_keys: HashMap::new(), host_descend_keys: vec![], host_descended: false, host_terminal_theme_mode: None, host_theme_dark_styling: None, host_theme_light_styling: None, mobile_state: MobileState::default(), nested_session_handling, mobile_render_gate: MobileRenderGate::default(), } } fn client_is_web(&self, client_id: ClientId) -> bool { self.connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false) } fn try_lift_mobile_gate(&mut self, client_id: ClientId) -> bool { let is_web_client = self.client_is_web(client_id); let reported_size = self.client_sizes.get(&client_id).copied(); let lifted = self .mobile_render_gate .try_reveal(client_id, is_web_client, reported_size); if lifted { self.force_render_mobile_tab(client_id); } lifted } fn force_render_mobile_tab(&mut self, client_id: ClientId) { if let Some(tab_id) = self.mobile_state.mobile_tab_id(client_id) { if let Some(tab) = self.tabs.get_mut(&tab_id) { tab.set_should_clear_display_before_rendering(); tab.set_force_render(); } } } fn ungate_clients_for_mobile_plugin(&mut self, plugin_id: u32) { if self.mobile_render_gate.is_empty() { return; } let Some(tab_id) = self .tabs .values() .find(|tab| tab.has_plugin(plugin_id)) .map(|tab| tab.id) else { return; }; for client_id in self.mobile_render_gate.gated_clients() { if self.mobile_state.mobile_tab_id(client_id) == Some(tab_id) { self.mobile_render_gate.ungate(client_id); } } } fn get_new_tab_id(&self) -> usize { if let Some(id) = self.tabs.keys().last() { *id + 1 } else { 0 } } /// Gets a tab by its stable ID (BTreeMap key). /// /// Use this when you have a tab ID from active_tab_ids, tab_history, or tab.id. fn get_tab_by_id(&self, id: usize) -> Option<&Tab> { self.tabs.get(&id) } /// Gets a mutable tab by its stable ID (BTreeMap key). fn get_tab_by_id_mut(&mut self, id: usize) -> Option<&mut Tab> { self.tabs.get_mut(&id) } /// Gets a mutable tab by its display position (0-based). fn get_tab_by_position_mut(&mut self, position: usize) -> Option<&mut Tab> { self.tabs.values_mut().find(|t| t.position == position) } /// Gets the stable ID of the tab at the given display position. /// /// Use this to convert position → ID for BTreeMap lookups. fn get_tab_id_at_position(&self, position: usize) -> Option { self.tabs .values() .find(|t| t.position == position) .map(|t| t.id) } /// Gets the display position of the tab with the given ID. /// /// Use this to convert ID → position for user-facing operations. fn get_tab_position_by_id(&self, id: usize) -> Option { self.tabs.get(&id).map(|t| t.position) } pub fn tab_visible_to(&self, client_id: ClientId, tab_id: usize) -> bool { match self.tabs.get(&tab_id) { Some(tab) => match &tab.visible_to { None => true, Some(set) => set.contains(&client_id), }, None => false, } } pub fn is_in_mobile_mode(&self, client_id: ClientId) -> bool { self.mobile_state.is_in_mobile_mode(client_id) } pub(crate) fn set_shadow_focus(&mut self, client_id: ClientId, pane_id: PaneId) -> Result<()> { if let ShadowFocusOutcome::NewlyApplied = self.mobile_state .apply_shadow_focus(client_id, pane_id, &mut self.tabs) { self.log_and_report_session_state()?; self.render(None)?; } Ok(()) } pub fn enter_mobile_mode(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to enter mobile mode for client {client_id}"); if self.mobile_state.is_in_mobile_mode(client_id) { return Ok(()); } let prior = self.active_tab_ids.get(&client_id).copied(); self.mobile_state.set_previous_tab(client_id, prior); self.mobile_state .clear_shadow_focus(client_id, &mut self.tabs); let tab_layout = MobileState::mobile_tab_layout().with_context(err_context)?; let tab_id = self.get_new_tab_id(); self.new_tab( tab_id, (vec![], vec![]), Some("Mobile".to_string()), Some(client_id), ) .with_context(err_context)?; if let Some(tab) = self.tabs.get_mut(&tab_id) { let mut set = HashSet::new(); set.insert(client_id); tab.visible_to = Some(set); } self.mobile_state.register_tab(client_id, tab_id); let is_web_client = self .connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false); let client_is_gated = self.mobile_render_gate.is_gated(client_id); if client_is_gated { self.bus .senders .send_to_plugin(PluginInstruction::HoldMobileRender(client_id)) .with_context(err_context)?; } self.bus .senders .send_to_plugin(PluginInstruction::NewTab( None, None, Some(tab_layout), vec![], tab_id, None, false, true, (client_id, is_web_client), None, )) .with_context(err_context)?; Ok(()) } pub fn exit_mobile_mode(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to exit mobile mode for client {client_id}"); self.mobile_render_gate.ungate(client_id); self.bus .senders .send_to_plugin(PluginInstruction::ReleaseMobileRender(client_id)) .non_fatal(); let Some((mobile_tab_id, prior_tab_id)) = self.mobile_state.begin_exit(client_id) else { return Ok(()); }; let destination = prior_tab_id .filter(|id| self.tabs.contains_key(id) && self.tab_visible_to(client_id, *id)) .or_else(|| { let mobile_tab_ids = self.mobile_state.mobile_tab_ids(); self.tabs .values() .filter(|t| { t.id != mobile_tab_id && !mobile_tab_ids.contains(&t.id) && match &t.visible_to { None => true, Some(set) => set.contains(&client_id), } }) .min_by_key(|t| t.position) .map(|t| t.id) }); if let Some(dest_id) = destination { if let Some(dest_pos) = self.get_tab_position_by_id(dest_id) { self.switch_active_tab(dest_pos, None, true, client_id) .with_context(err_context)?; } } if self.tabs.contains_key(&mobile_tab_id) { self.close_tab_by_id(mobile_tab_id) .with_context(err_context)?; } self.mobile_state .clear_shadow_focus(client_id, &mut self.tabs); Ok(()) } pub fn toggle_mobile_mode(&mut self, client_id: ClientId) -> Result<()> { if self.is_in_mobile_mode(client_id) { self.exit_mobile_mode(client_id) } else { self.enter_mobile_mode(client_id) } } pub fn reevaluate_mobile_mode( &mut self, client_id: ClientId, new_size: Size, mobile_layout: MobileLayoutConfiguration, threshold_cols: u16, threshold_rows: u16, ) -> Result<()> { let is_web_client = self .connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false); let should_be_mobile = mobile_layout.should_route_to_mobile( is_web_client, new_size.cols, new_size.rows, threshold_cols, threshold_rows, ); let is_mobile = self.is_in_mobile_mode(client_id); let was_auto = self.mobile_state.was_auto_entered(client_id); if should_be_mobile && !is_mobile { self.enter_mobile_mode(client_id)?; self.mobile_state.mark_auto_entered(client_id); self.render(None)?; } else if !should_be_mobile && is_mobile && was_auto { self.exit_mobile_mode(client_id)?; self.render(None)?; } if !should_be_mobile && self.mobile_render_gate.is_gated(client_id) { self.mobile_render_gate.ungate(client_id); self.render(None)?; } Ok(()) } fn visible_tab_positions_for_client(&self, client_id: ClientId) -> Vec { let mut positions: Vec = self .tabs .values() .filter(|t| match &t.visible_to { None => true, Some(set) => set.contains(&client_id), }) .map(|t| t.position) .collect(); positions.sort_unstable(); positions } fn move_clients_from_closed_tab( &mut self, client_ids_and_mode_infos: Vec<(ClientId, ModeInfo)>, ) -> Result<()> { let err_context = || "failed to move clients from closed tab".to_string(); if self.tabs.is_empty() { Err::<(), _>(anyhow!( "No tabs left, cannot move clients: {:?} from closed tab", client_ids_and_mode_infos )) .with_context(err_context) .non_fatal(); return Ok(()); } let first_tab_index = *self .tabs .keys() .next() .context("screen contained no tabs") .with_context(err_context)?; for (client_id, client_mode_info) in client_ids_and_mode_infos { let client_tab_history = self.tab_history.entry(client_id).or_insert_with(Vec::new); if let Some(client_previous_tab) = client_tab_history.pop() { if let Some(client_active_tab) = self.tabs.get_mut(&client_previous_tab) { self.active_tab_ids.insert(client_id, client_previous_tab); client_active_tab .add_client(client_id, Some(client_mode_info)) .with_context(err_context)?; continue; } } self.active_tab_ids.insert(client_id, first_tab_index); self.tabs .get_mut(&first_tab_index) .with_context(err_context)? .add_client(client_id, Some(client_mode_info)) .with_context(err_context)?; } Ok(()) } fn move_suppressed_panes_from_closed_tab( &mut self, suppressed_panes: SuppressedPanes, ) -> Result<()> { // TODO: this is not entirely accurate, these also sometimes contain a pane who's // scrollback is being edited - in this case we need to close it or to move it to the // appropriate tab let err_context = || "Failed to move suppressed panes from closed tab"; let first_tab_index = *self .tabs .keys() .next() .context("screen contains no tabs") .with_context(err_context)?; self.tabs .get_mut(&first_tab_index) .with_context(err_context)? .add_suppressed_panes(suppressed_panes); Ok(()) } fn move_clients_between_tabs( &mut self, source_tab_index: usize, destination_tab_index: usize, update_mode_infos: bool, clients_to_move: Option>, ) -> Result<()> { let err_context = || { format!( "failed to move clients from tab {source_tab_index} to tab {destination_tab_index}" ) }; // None ==> move all clients let drained_clients = self .get_indexed_tab_mut(source_tab_index) .map(|t| t.drain_connected_clients(clients_to_move)); if let Some(client_mode_info_in_source_tab) = drained_clients { let destination_tab = self .get_indexed_tab_mut(destination_tab_index) .context("failed to get destination tab by index") .with_context(err_context)?; destination_tab .add_multiple_clients(client_mode_info_in_source_tab) .with_context(err_context)?; if update_mode_infos { destination_tab .update_input_modes() .with_context(err_context)?; } destination_tab.set_force_render(); if destination_tab.has_stack_lists() { destination_tab.set_should_clear_display_before_rendering(); } destination_tab.visible(true).with_context(err_context)?; } Ok(()) } fn update_client_tab_focus(&mut self, client_id: ClientId, new_tab_index: usize) { match self.active_tab_ids.remove(&client_id) { Some(old_active_index) => { self.active_tab_ids.insert(client_id, new_tab_index); let client_tab_history = self.tab_history.entry(client_id).or_insert_with(Vec::new); client_tab_history.retain(|&e| e != new_tab_index); client_tab_history.push(old_active_index); }, None => { self.active_tab_ids.insert(client_id, new_tab_index); }, } self.clear_bell_for_focused_pane(client_id); } /// A helper function to switch to a new tab at specified position. fn switch_active_tab( &mut self, new_tab_pos: usize, should_change_pane_focus: Option, update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || { format!( "Failed to switch to active tab at position {new_tab_pos} for client id: {client_id:?}" ) }; if let Some(new_tab) = self.tabs.values().find(|t| t.position == new_tab_pos) { match self.get_active_tab(client_id) { Ok(current_tab) => { // If new active tab is same as the current one, do nothing. if current_tab.position == new_tab_pos { return Ok(()); } let current_tab_index = current_tab.id; let new_tab_index = new_tab.id; let affected_client_ids: Vec = if self.session_is_mirrored { self.connected_clients .borrow() .iter() .map(|(c, _i)| *c) .collect() } else { vec![client_id] }; let old_focused_panes: Vec<(ClientId, Option)> = affected_client_ids .iter() .map(|c| (*c, self.get_active_pane_id(c))) .collect(); if self.session_is_mirrored { self.move_clients_between_tabs( current_tab_index, new_tab_index, update_mode_infos, None, ) .with_context(err_context)?; let all_connected_clients: Vec = self .connected_clients .borrow() .iter() .map(|(c, _i)| *c) .collect(); for client_id in all_connected_clients { self.update_client_tab_focus(client_id, new_tab_index); match ( should_change_pane_focus, self.get_indexed_tab_mut(new_tab_index), ) { (Some(direction), Some(new_tab)) => { new_tab.focus_pane_on_edge(direction, client_id); }, _ => {}, } } } else { self.move_clients_between_tabs( current_tab_index, new_tab_index, update_mode_infos, Some(vec![client_id]), ) .with_context(err_context)?; match ( should_change_pane_focus, self.get_indexed_tab_mut(new_tab_index), ) { (Some(direction), Some(new_tab)) => { new_tab.focus_pane_on_edge(direction, client_id); }, _ => {}, } self.update_client_tab_focus(client_id, new_tab_index); } if let Some(current_tab) = self.get_indexed_tab_mut(current_tab_index) { if current_tab.has_no_connected_clients() { current_tab.visible(false).with_context(err_context)?; } } else { Err::<(), _>(anyhow!("Tab index {:?} not found", current_tab_index)) .with_context(err_context) .non_fatal(); } // Clear tab bell notification for the newly active tab if let Some(new_tab) = self.tabs.get_mut(&new_tab_index) { let tab_id = new_tab.id; new_tab.clear_tab_bell_notification(); let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); } self.recompute_tab_size(current_tab_index) .with_context(err_context)?; self.recompute_tab_size(new_tab_index) .with_context(err_context)?; self.log_and_report_session_state() .with_context(err_context)?; for (affected_client_id, old_pane_id) in old_focused_panes { let new_pane_id = self.get_active_pane_id(&affected_client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { self.report_key_passthrough_state(affected_client_id, old, new); } } return self.render(None).with_context(err_context); }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), } } Ok(()) } /// A helper function to switch to a new tab with specified name. Return true if tab [name] has /// been created, else false. fn switch_active_tab_name(&mut self, name: String, client_id: ClientId) -> Result { match self .tabs .values() .find(|t| t.name == name && self.tab_visible_to(client_id, t.id)) { Some(new_tab) => { self.switch_active_tab(new_tab.position, None, true, client_id)?; Ok(true) }, None => Ok(false), } } /// Sets this [`Screen`]'s active [`Tab`] to the next tab. pub fn switch_tab_next( &mut self, should_change_pane_focus: Option, update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to switch to next tab for client {client_id}"); let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; if let Some(client_id) = client_id { match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; let visible = self.visible_tab_positions_for_client(client_id); if visible.is_empty() { return Ok(()); } let new_tab_pos = match visible.iter().position(|p| *p > active_tab_pos) { Some(idx) => visible[idx], None => visible[0], }; return self.switch_active_tab( new_tab_pos, should_change_pane_focus, update_mode_infos, client_id, ); }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), } } Ok(()) } /// Sets this [`Screen`]'s active [`Tab`] to the previous tab. pub fn switch_tab_prev( &mut self, should_change_pane_focus: Option, update_mode_infos: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to switch to previous tab for client {client_id}"); let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; if let Some(client_id) = client_id { match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; let visible = self.visible_tab_positions_for_client(client_id); if visible.is_empty() { return Ok(()); } let new_tab_pos = match visible.iter().rev().find(|p| **p < active_tab_pos) { Some(p) => *p, None => *visible.last().unwrap(), }; return self.switch_active_tab( new_tab_pos, should_change_pane_focus, update_mode_infos, client_id, ); }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), } } Ok(()) } pub fn go_to_tab(&mut self, tab_index: usize, client_id: ClientId) -> Result<()> { let target_pos = tab_index.saturating_sub(1); if let Some(target_id) = self.get_tab_id_at_position(target_pos) { if !self.tab_visible_to(client_id, target_id) { return Ok(()); } } self.switch_active_tab(target_pos, None, true, client_id) } pub fn go_to_tab_name(&mut self, name: String, client_id: ClientId) -> Result { self.switch_active_tab_name(name, client_id) } fn close_tab_by_id(&mut self, tab_id: usize) -> Result<()> { let err_context = || format!("failed to close tab at index {tab_id:?}"); let mut tab_to_close = self.tabs.remove(&tab_id).with_context(err_context)?; self.mobile_state.remove_fit_for_tab(tab_id); let mut pane_ids = tab_to_close.get_all_pane_ids(); // here we extract the suppressed panes (these are background panes that don't care which // tab they are in, and in the future we should probably make them global to screen rather // than to each tab) and move them to another tab if there is one let suppressed_panes = tab_to_close.extract_suppressed_panes(); for suppressed_pane_id in suppressed_panes.keys() { pane_ids.retain(|p| p != suppressed_pane_id); } let _ = self.bus.senders.send_to_plugin(PluginInstruction::Update( pane_ids .iter() .copied() .map(|p_id| (None, None, Event::PaneClosed(p_id.into()))) .collect(), )); // Notify pane render subscribers of each closed pane for p_id in &pane_ids { self.notify_pane_closed_to_subscribers((*p_id).into()); } // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread // has already closed and this would result in an error self.bus .senders .send_to_pty(PtyInstruction::CloseTab(pane_ids)) .with_context(err_context)?; if self.tabs.is_empty() { self.active_tab_ids.clear(); self.bus .senders .send_to_server(ServerInstruction::Render(None)) .with_context(err_context) } else { let client_mode_infos_in_closed_tab = tab_to_close.drain_connected_clients(None); self.move_clients_from_closed_tab(client_mode_infos_in_closed_tab) .with_context(err_context)?; self.move_suppressed_panes_from_closed_tab(suppressed_panes) .with_context(err_context)?; let visible_tab_indices: HashSet = self.active_tab_ids.values().copied().collect(); for t in self.tabs.values_mut() { if visible_tab_indices.contains(&t.id) { t.set_force_render(); t.visible(true).with_context(err_context)?; } if t.position > tab_to_close.position { t.position -= 1; } } self.log_and_report_session_state() .with_context(err_context)?; self.render(None).with_context(err_context) } } // Closes the client_id's focused tab pub fn close_tab(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to close tab for client {client_id:?}"); let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; match client_id { Some(client_id) => { let active_tab_index = *self .active_tab_ids .get(&client_id) .with_context(err_context)?; self.close_tab_by_id(active_tab_index) .with_context(err_context) }, None => Ok(()), } } pub fn resize_to_screen(&mut self, new_screen_size: Size) -> Result<()> { let err_context = || format!("failed to resize to screen size: {new_screen_size:#?}"); if self.size != new_screen_size { self.size = new_screen_size; for tab in self.tabs.values_mut() { tab.resize_whole_tab(new_screen_size) .with_context(err_context)?; tab.set_force_render(); } self.log_and_report_session_state() .with_context(err_context)?; self.render(None).with_context(err_context) } else { Ok(()) } } pub fn set_client_size(&mut self, client_id: ClientId, size: Size) { self.client_sizes.insert(client_id, size); } pub fn recompute_tab_size(&mut self, tab_id: usize) -> Result<()> { let err_context = || format!("failed to recompute size for tab {tab_id}"); if self.mobile_state.has_fit(tab_id) { for _ in 0..FIT_RESIZE_MAX_ITERS { let Some(new_size) = self.compute_fit_size(tab_id) else { break; }; let Some(tab) = self.tabs.get_mut(&tab_id) else { break; }; if tab.size == new_size { break; } tab.resize_whole_tab(new_size).with_context(err_context)?; } if let Some(tab) = self.tabs.get_mut(&tab_id) { tab.set_should_clear_display_before_rendering(); tab.set_force_render(); } return Ok(()); } let mut rows: Vec = Vec::new(); let mut cols: Vec = Vec::new(); for (client_id, active_tab) in self.active_tab_ids.iter() { if *active_tab == tab_id { if let Some(size) = self.client_sizes.get(client_id) { rows.push(size.rows); cols.push(size.cols); } } } if rows.is_empty() || cols.is_empty() { return Ok(()); } rows.sort_unstable(); cols.sort_unstable(); let new_size = Size { rows: rows[0], cols: cols[0], }; if let Some(tab) = self.tabs.get_mut(&tab_id) { if tab.size != new_size { tab.resize_whole_tab(new_size).with_context(err_context)?; tab.set_force_render(); } } Ok(()) } pub(crate) fn compute_fit_size(&self, tab_id: usize) -> Option { self.mobile_state.compute_fit_size(tab_id, &self.tabs) } pub fn set_tab_fit( &mut self, client_id: ClientId, tab_id: usize, pane_id: PaneId, size: Size, ) -> Result<()> { self.mobile_state .set_fit(client_id, tab_id, pane_id, size, &mut self.tabs); self.recompute_tab_size(tab_id) } pub fn exit_fit_mode(&mut self, client_id: ClientId) -> Result { match self .mobile_state .clear_fit_owned_by(client_id, &mut self.tabs) { Some(tab_id) => { self.recompute_tab_size(tab_id)?; Ok(true) }, None => Ok(false), } } pub(crate) fn clear_fit_for_closed_pane(&mut self, pane_id: PaneId) -> Result { match self.mobile_state.clear_fit_for_pane(pane_id) { Some(tab_id) => { self.recompute_tab_size(tab_id)?; Ok(true) }, None => Ok(false), } } pub fn update_pixel_dimensions(&mut self, pixel_dimensions: PixelDimensions) { self.pixel_dimensions.merge(pixel_dimensions); if let Some(character_cell_size) = self.pixel_dimensions.character_cell_size { *self.character_cell_size.borrow_mut() = Some(character_cell_size); } else if let Some(text_area_size) = self.pixel_dimensions.text_area_size { let character_cell_size_height = text_area_size.height / self.size.rows; let character_cell_size_width = text_area_size.width / self.size.cols; let character_cell_size = SizeInPixels { height: character_cell_size_height, width: character_cell_size_width, }; *self.character_cell_size.borrow_mut() = Some(character_cell_size); } } pub fn update_kitty_graphics_support(&mut self, client_id: ClientId, supported: bool) { let supported = supported && self.support_kitty_graphics_protocol; self.kitty_host_capabilities .borrow_mut() .insert(client_id, supported); self.push_kitty_host_support_to_tabs(); } fn kitty_host_support_aggregate(&self) -> Option { if !self.support_kitty_graphics_protocol { return Some(KittyHostSupport::ProtocolDisabled); } let capabilities = self.kitty_host_capabilities.borrow(); if capabilities.is_empty() { None } else { Some(KittyHostSupport::from_host_capability( capabilities.values().any(|supported| *supported), )) } } fn push_kitty_host_support_to_tabs(&mut self) { if let Some(aggregate) = self.kitty_host_support_aggregate() { for tab in self.tabs.values_mut() { tab.update_kitty_host_support(aggregate); } } } pub fn update_terminal_background_color(&mut self, background_color_instruction: String) { if let Some(AnsiCode::RgbCode((r, g, b))) = xparse_color(background_color_instruction.as_bytes()) { let bg_palette_color = PaletteColor::Rgb((r, g, b)); self.terminal_emulator_colors.borrow_mut().bg = bg_palette_color; } } pub fn update_terminal_foreground_color(&mut self, foreground_color_instruction: String) { if let Some(AnsiCode::RgbCode((r, g, b))) = xparse_color(foreground_color_instruction.as_bytes()) { let fg_palette_color = PaletteColor::Rgb((r, g, b)); self.terminal_emulator_colors.borrow_mut().fg = fg_palette_color; } } pub fn update_terminal_color_registers(&mut self, color_registers: Vec<(usize, String)>) { let mut terminal_emulator_color_codes = self.terminal_emulator_color_codes.borrow_mut(); for (color_register, color_sequence) in color_registers { terminal_emulator_color_codes.insert(color_register, color_sequence); } } /// Enqueue a whitelisted host-terminal query from pane `pane_id`. /// Queries are serialized globally: at most one is in flight to the /// client at a time; the rest wait in `forward_queue`. Returns the /// allocated token so callers (tests) can observe it if useful. pub fn forward_host_query( &mut self, pane_id: PaneId, query: crate::host_query::HostQuery, ) -> u32 { // ColorPaletteMode is answered locally — Zellij already knows // the host's mode from its own startup query and from // unsolicited DSR 997 updates. Skip the entire forwarding // machinery (no token, no in-flight slot, no host round-trip) // and write the reply straight to the originating pane's pty. if let crate::host_query::HostQuery::ColorPaletteMode = query { self.answer_color_palette_mode_query_locally(pane_id); return STARTUP_SENTINEL_TOKEN; // sentinel: no real forward happened } let token = self.next_forward_token; // Skip over the reserved sentinel (0) on wrap; allocate a fresh // u32 for every forward. self.next_forward_token = self.next_forward_token.wrapping_add(1); if self.next_forward_token == STARTUP_SENTINEL_TOKEN { self.next_forward_token = 1; } if self.forward_in_flight_token.is_some() { self.forward_queue.push_back(PendingForward { token, pane_id, query, }); } else { self.dispatch_forward(token, pane_id, query); } token } fn answer_color_palette_mode_query_locally(&mut self, pane_id: PaneId) { if matches!(pane_id, PaneId::Plugin(_)) { return; } let code: u8 = match self.effective_host_terminal_theme_mode() { HostTerminalThemeMode::Dark => 1, HostTerminalThemeMode::Light => 2, }; let reply = format!("\u{1b}[?997;{}n", code).into_bytes(); // Route via Tab so the reply lands on the pane in the correct // stream position and any PTY input the app emitted while // waiting is replayed. let _ = self.resume_pane_after_forward(pane_id, reply); } fn effective_host_terminal_theme_mode(&self) -> HostTerminalThemeMode { self.host_terminal_theme_mode.unwrap_or_else(|| { match detect_theme_hue(self.style.colors.text_unselected.background) { ThemeHue::Dark => HostTerminalThemeMode::Dark, ThemeHue::Light => HostTerminalThemeMode::Light, } }) } /// Dispatch a forward to the client and mark the slot as in-flight. /// Spawns a one-shot timeout task on the global tokio runtime that /// synthesizes an empty reply for `token` after /// `SERVER_FORWARD_TIMEOUT_MS`. The handler's token-equality guard /// makes the timeout a no-op if a real reply arrived first, so no /// explicit cancellation is needed. fn dispatch_forward( &mut self, token: u32, pane_id: PaneId, query: crate::host_query::HostQuery, ) { let query_bytes = query.to_query_bytes(); self.pending_forwarded_queries .insert(token, PendingForwardEntry { pane_id, query }); self.forward_in_flight_token = Some(token); let _ = self .bus .senders .send_to_server(ServerInstruction::ForwardQueryToHost(token, query_bytes)); let senders = self.bus.senders.clone(); crate::global_async_runtime::get_tokio_runtime().spawn(async move { tokio::time::sleep(std::time::Duration::from_millis(SERVER_FORWARD_TIMEOUT_MS)).await; let _ = senders.send_to_screen(ScreenInstruction::ForwardedReplyFromHost { token, reply_bytes: Vec::new(), }); }); } /// Handle a host-reply observed by the client for token `token`. /// Writes the bytes to the originating pane's pty (if still present) /// and releases the in-flight slot so the next queued forward can /// dispatch. /// /// An empty `reply_bytes` is treated as "nobody answered" — either /// because no client was attached to forward the query, or the /// chosen client's host timed out. In that case we try to answer /// from Zellij's cached view of the host (pixel dims, bg/fg, /// palette) so the pane still gets a well-formed reply instead of /// zero bytes. If the cache has nothing relevant either, the pane /// receives an empty write and the app decides what to do. pub fn handle_forwarded_reply_from_host( &mut self, token: u32, reply_bytes: Vec, ) -> Result<()> { // Stale-reply guard. Both the real `ForwardedReplyFromHost` // path and the server-side timeout path land here. If a real // reply landed first (releasing the slot AND dispatching the // next queued forward), the late timeout's empty reply for the // already-cleared token must be a no-op — releasing the slot // again would clobber the new in-flight forward. The same // logic also rejects duplicate replies and replies for tokens // belonging to a previously-closed pane. if self.forward_in_flight_token != Some(token) { log::debug!( "Dropping stale forwarded reply (token={}, in-flight={:?}, len={} bytes)", token, self.forward_in_flight_token, reply_bytes.len(), ); return Ok(()); } if let Some(entry) = self.pending_forwarded_queries.remove(&token) { let PendingForwardEntry { pane_id, query } = entry; match pane_id { PaneId::Terminal(_) => { let payload = if reply_bytes.is_empty() { self.synthesize_cached_reply(&query) } else { reply_bytes }; // Route via Tab so the reply lands on the pane in // the correct stream position and any PTY input // the app emitted while waiting is replayed. self.resume_pane_after_forward(pane_id, payload)?; }, PaneId::Plugin(_) => { // Plugin panes do not issue whitelisted host queries; // if we reach here the mapping was populated // erroneously. Drop the reply. log::warn!( "Discarding host reply for plugin pane (token={}); plugins do not forward CSI/OSC queries", token ); }, } } // Release the slot and dispatch the next queued forward, if any. self.forward_in_flight_token = None; if let Some(next) = self.forward_queue.pop_front() { self.dispatch_forward(next.token, next.pane_id, next.query); } Ok(()) } /// Deliver a forwarded reply (or cache-fallback synthesis, or a /// locally-answered query payload) to the originating pane via /// the owning Tab. The Tab handler writes the bytes to PTY and /// then re-feeds any PTY input that was buffered while the pane /// was forward-paused, preserving query/reply ordering. pub fn resume_pane_after_forward( &mut self, pane_id: PaneId, reply_bytes: Vec, ) -> Result<()> { let terminal_id = match pane_id { PaneId::Terminal(id) => id, PaneId::Plugin(_) => { // Plugin panes do not forward queries — dropping is // the correct behaviour matching the existing // forwarding path's plugin-pane guard. return Ok(()); }, }; let mut found = false; for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.resume_pane_after_forward(terminal_id, reply_bytes.clone()) .non_fatal(); found = true; break; } } if !found { // No tab owns the pane — the pane was closed between the // forward dispatch and the reply arrival. The terminal id // is probably defunct; the write goes to the PTY writer // anyway because an empty payload is the canonical "host // declined" reply some apps rely on, and a non-empty // payload may still be deliverable until the pty drains. let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write(reply_bytes, terminal_id, None)); log::debug!( "ResumePaneAfterForward: pane {:?} not in any tab, fell back to direct PTY write", pane_id ); } Ok(()) } pub fn handle_nested_session_message_from_pane( &mut self, pane_id: PaneId, message: NestedSessionMessage, ) { match message { NestedSessionMessage::Announce { session_name, .. } => { self.handle_nested_guest_announce(pane_id, session_name); }, NestedSessionMessage::Pong => { self.nested_guest_tracker.on_pong(pane_id, Instant::now()); log::debug!("nested session guest in pane {:?} ponged", pane_id); }, NestedSessionMessage::Bye => { log::info!( "nested session guest in pane {:?} sent bye, clearing guest flag", pane_id ); self.clear_nested_guest(pane_id); }, NestedSessionMessage::FocusHost { direction } => { let clients_focused_on_pane = self.clients_with_pane_focused(pane_id); match direction { None => { self.unset_nested_fullscreen_if_active(pane_id); for client_id in clients_focused_on_pane { self.nested_guest_choices .insert((client_id, pane_id), NestedGuestChoice::Dismissed); self.sync_guest_choice_indicator(client_id, pane_id); self.set_client_dimmed(client_id, false, None); let _ = self.bus.senders.send_to_server( ServerInstruction::KeyPassthroughChanged( client_id, pane_id, pane_id, false, None, true, ), ); } let _ = self.render(None); }, Some(direction) => { for client_id in clients_focused_on_pane { let new_pane_id = self .tabs .values_mut() .find(|tab| tab.has_pane_with_pid(&pane_id)) .and_then(|tab| { tab.focus_pane_adjacent_to(pane_id, direction, client_id) }); match new_pane_id { Some(new_pane_id) => { self.report_key_passthrough_state_with_direction( client_id, pane_id, new_pane_id, Some(direction), ); let _ = self.render(None); let _ = self.log_and_report_session_state(); }, None => { self.bubble_focus_to_host(client_id, direction); }, } } }, } }, NestedSessionMessage::ShortcutUpdate { ascend_keys, .. } => { if !self.nested_guest_tracker.is_tracked(pane_id) { log::debug!( "ignoring nested shortcut update from non-live guest pane {:?}", pane_id ); return; } self.guest_ascend_keys.insert(pane_id, ascend_keys); self.refresh_nested_ascend_keys_for_pane(pane_id); }, NestedSessionMessage::ToggleHostFullscreen { fullscreen } => { if !self.nested_guest_tracker.is_tracked(pane_id) { log::debug!( "ignoring nested fullscreen request from non-live guest pane {:?}", pane_id ); return; } self.apply_nested_guest_fullscreen(pane_id, fullscreen); if let Some(client_id) = self.nested_via_client_id { let payload = nested_session::encode_payload( &NestedSessionMessage::ToggleHostFullscreen { fullscreen }, ); let _ = self.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(client_id, payload), ); } let _ = self.render(None); let _ = self.log_and_report_session_state(); }, other => { log::debug!( "dropping unsupported nested session message from pane {:?}: {:?}", pane_id, other ); }, } } fn handle_nested_guest_announce(&mut self, pane_id: PaneId, guest_session_name: String) { use crate::nested_guest::AnnounceKind; let terminal_id = match pane_id { PaneId::Terminal(terminal_id) => terminal_id, PaneId::Plugin(_) => return, }; let announce_kind = self .nested_guest_tracker .on_announce(pane_id, Instant::now()); let is_fresh = announce_kind == AnnounceKind::New; if is_fresh { self.nested_guest_choices .retain(|(_, choice_pane_id), _| *choice_pane_id != pane_id); } let connected_client_ids: Vec = self.connected_clients.borrow().keys().copied().collect(); let guest_modal_shortcuts = self.guest_modal_shortcuts(); let owning_tab = self .tabs .values_mut() .find(|tab| tab.has_pane_with_pid(&pane_id)); match owning_tab { Some(tab) => { tab.set_pane_is_nested_guest(pane_id, true); tab.set_guest_session_name_on_pane(pane_id, Some(guest_session_name.clone())); tab.set_guest_modal_shortcuts_on_pane(pane_id, guest_modal_shortcuts); if is_fresh { tab.clear_all_guest_choice_indicators_on_pane(pane_id); if self.nested_session_handling == NestedSessionHandling::Ask { tab.set_guest_modal_on_pane(pane_id, &connected_client_ids); } } }, None => { self.nested_guest_tracker.remove(pane_id); return; }, } let mut ancestry = self.nested_ancestry.clone(); ancestry.push(self.session_name.clone()); let announce_ack = NestedSessionMessage::AnnounceAck { ancestry, capabilities: vec![NestedSessionCapability::NestedControl], descend_keys: self.own_descend_shortcut(), }; let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write( nested_session::encode_frame(&announce_ack), terminal_id, None, )); let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StartNestedGuestPing(pane_id)); if is_fresh { self.auto_apply_nested_session_handling(pane_id, &connected_client_ids); } else { self.revive_nested_guest(pane_id); } let _ = self.render(None); log::info!( "nested session handshake ({:?}): guest session {:?} announced in pane {:?}, sent announce_ack", announce_kind, guest_session_name, pane_id ); } fn suspend_nested_guest(&mut self, pane_id: PaneId) { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopNestedGuestPing(pane_id)); self.unset_nested_fullscreen_if_active(pane_id); self.reported_guest_fullscreen.remove(&pane_id); let affected_clients: Vec = self .clients_with_pane_focused(pane_id) .into_iter() .chain(self.dimmed_clients.iter().copied()) .collect(); for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_all_guest_modals_on_pane(pane_id); break; } } for client_id in affected_clients { self.set_client_dimmed(client_id, false, None); let _ = self .bus .senders .send_to_server(ServerInstruction::KeyPassthroughChanged( client_id, pane_id, pane_id, false, None, false, )); } } fn revive_nested_guest(&mut self, pane_id: PaneId) { self.broadcast_ancestry_update_to_guests(); self.sync_nested_guest_fullscreen_state(); let client_ids: Vec = self.connected_clients.borrow().keys().copied().collect(); for client_id in client_ids { self.sync_guest_choice_indicator(client_id, pane_id); if self.get_active_pane_id(&client_id) == Some(pane_id) { self.report_key_passthrough_state(client_id, pane_id, pane_id); } } } fn handle_guest_modal_choice( &mut self, client_id: ClientId, pane_id: PaneId, outcome: GuestModalOutcome, ) { for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_guest_modal_on_pane(pane_id, client_id); break; } } match outcome { GuestModalOutcome::Descend => { self.nested_guest_choices .insert((client_id, pane_id), NestedGuestChoice::Descend); self.sync_guest_choice_indicator(client_id, pane_id); if self.get_active_pane_id(&client_id) == Some(pane_id) { self.report_key_passthrough_state(client_id, pane_id, pane_id); } let _ = self.render(None); let _ = self.log_and_report_session_state(); }, GuestModalOutcome::Zoom => { self.nested_guest_choices .insert((client_id, pane_id), NestedGuestChoice::Zoom); self.sync_guest_choice_indicator(client_id, pane_id); if self.get_active_pane_id(&client_id) == Some(pane_id) { self.report_key_passthrough_state(client_id, pane_id, pane_id); } self.apply_nested_guest_fullscreen(pane_id, true); self.sync_nested_guest_fullscreen_state(); if let Some(via_client_id) = self.nested_via_client_id { let payload = nested_session::encode_payload( &NestedSessionMessage::ToggleHostFullscreen { fullscreen: true }, ); let _ = self.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(via_client_id, payload), ); } let _ = self.render(None); let _ = self.log_and_report_session_state(); }, } } fn record_nested_guest_choice_for_client( &mut self, client_id: ClientId, pane_id: PaneId, choice: NestedGuestChoice, ) { for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_guest_modal_on_pane(pane_id, client_id); break; } } self.nested_guest_choices .insert((client_id, pane_id), choice); self.sync_guest_choice_indicator(client_id, pane_id); if self.get_active_pane_id(&client_id) == Some(pane_id) { self.report_key_passthrough_state(client_id, pane_id, pane_id); } } fn auto_apply_nested_session_handling(&mut self, pane_id: PaneId, client_ids: &[ClientId]) { match self.nested_session_handling { NestedSessionHandling::Descend => { for client_id in client_ids { self.record_nested_guest_choice_for_client( *client_id, pane_id, NestedGuestChoice::Descend, ); } }, NestedSessionHandling::Fullscreen => { for client_id in client_ids { self.record_nested_guest_choice_for_client( *client_id, pane_id, NestedGuestChoice::Zoom, ); } self.apply_nested_guest_fullscreen(pane_id, true); self.sync_nested_guest_fullscreen_state(); if let Some(via_client_id) = self.nested_via_client_id { let payload = nested_session::encode_payload( &NestedSessionMessage::ToggleHostFullscreen { fullscreen: true }, ); let _ = self.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(via_client_id, payload), ); } }, NestedSessionHandling::Ask | NestedSessionHandling::Never => {}, } } fn focus_guest_session(&mut self, client_id: ClientId) { let focused_pane_id = match self.get_active_pane_id(&client_id) { Some(pane_id) => pane_id, None => return, }; let is_live_guest = self.tabs.values().any(|tab| { tab.has_pane_with_pid(&focused_pane_id) && tab.is_pane_nested_guest(focused_pane_id) }) && self.nested_guest_tracker.is_tracked(focused_pane_id); if !is_live_guest { return; } self.nested_guest_choices .insert((client_id, focused_pane_id), NestedGuestChoice::Descend); for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&focused_pane_id) { tab.clear_guest_modal_on_pane(focused_pane_id, client_id); break; } } self.sync_guest_choice_indicator(client_id, focused_pane_id); self.report_key_passthrough_state(client_id, focused_pane_id, focused_pane_id); let _ = self.render(None); let _ = self.log_and_report_session_state(); } pub fn clear_nested_guest(&mut self, pane_id: PaneId) { self.nested_guest_tracker.remove(pane_id); self.guest_ascend_keys.remove(&pane_id); let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopNestedGuestPing(pane_id)); self.unset_nested_fullscreen_if_active(pane_id); self.reported_guest_fullscreen.remove(&pane_id); let mut affected_clients: Vec = self .nested_guest_choices .iter() .filter(|((_, choice_pane_id), choice)| { *choice_pane_id == pane_id && choice.enters_passthrough() }) .map(|((choice_client_id, _), _)| *choice_client_id) .collect(); self.nested_guest_choices .retain(|(_, choice_pane_id), _| *choice_pane_id != pane_id); for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_pane_is_nested_guest(pane_id, false); tab.clear_all_guest_modals_on_pane(pane_id); tab.clear_all_guest_choice_indicators_on_pane(pane_id); break; } } for client_id in self.clients_with_pane_focused(pane_id) { if !affected_clients.contains(&client_id) { affected_clients.push(client_id); } } for client_id in affected_clients { self.set_client_dimmed(client_id, false, None); let _ = self .bus .senders .send_to_server(ServerInstruction::KeyPassthroughChanged( client_id, pane_id, pane_id, false, None, false, )); } } fn show_guest_modals_for_new_client(&mut self, client_id: ClientId) { let mut live_guest_panes = vec![]; for tab in self.tabs.values() { for pane_id in tab.nested_guest_pane_ids() { if self.nested_guest_tracker.is_tracked(pane_id) { live_guest_panes.push(pane_id); } } } for pane_id in live_guest_panes { if self .nested_guest_choices .contains_key(&(client_id, pane_id)) { continue; } match self.nested_session_handling { NestedSessionHandling::Ask => { for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_guest_modal_on_pane(pane_id, &[client_id]); break; } } }, NestedSessionHandling::Descend => { self.record_nested_guest_choice_for_client( client_id, pane_id, NestedGuestChoice::Descend, ); }, NestedSessionHandling::Fullscreen => { self.record_nested_guest_choice_for_client( client_id, pane_id, NestedGuestChoice::Zoom, ); }, NestedSessionHandling::Never => {}, } } } fn should_route_keys_to_pane(&self, client_id: ClientId, pane_id: PaneId) -> bool { for tab in self.tabs.values() { if tab.has_pane_with_pid(&pane_id) { let is_guest = tab.is_pane_nested_guest(pane_id); let is_tracked = self.nested_guest_tracker.is_tracked(pane_id); let has_descended = self .nested_guest_choices .get(&(client_id, pane_id)) .map(|choice| choice.enters_passthrough()) .unwrap_or(false); return is_guest && is_tracked && has_descended; } } false } fn downgrade_zoom_choice_to_descend(&mut self, pane_id: PaneId) { let mut affected_clients = vec![]; for ((client_id, choice_pane_id), choice) in self.nested_guest_choices.iter_mut() { if *choice_pane_id == pane_id && *choice == NestedGuestChoice::Zoom { *choice = NestedGuestChoice::Descend; affected_clients.push(*client_id); } } for client_id in affected_clients { self.sync_guest_choice_indicator(client_id, pane_id); } } fn guest_choice_indicator_for( &self, client_id: ClientId, pane_id: PaneId, ) -> Option { match self.nested_guest_choices.get(&(client_id, pane_id)) { Some(NestedGuestChoice::Descend) => Some(GuestChoiceIndicator::Descended), Some(NestedGuestChoice::Dismissed) => Some(GuestChoiceIndicator::Dismissed), Some(NestedGuestChoice::Zoom) | None => None, } } fn sync_guest_choice_indicator(&mut self, client_id: ClientId, pane_id: PaneId) { let indicator = self.guest_choice_indicator_for(client_id, pane_id); for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_guest_choice_indicator_on_pane(pane_id, client_id, indicator); break; } } } fn report_key_passthrough_state( &mut self, client_id: ClientId, old_pane_id: PaneId, new_pane_id: PaneId, ) { self.report_key_passthrough_state_with_direction(client_id, old_pane_id, new_pane_id, None); } fn report_key_passthrough_state_with_direction( &mut self, client_id: ClientId, old_pane_id: PaneId, new_pane_id: PaneId, entered_from_direction: Option, ) { let should_route = self.should_route_keys_to_pane(client_id, new_pane_id); let guest_pane_id = if should_route { Some(new_pane_id) } else { None }; self.set_client_dimmed(client_id, should_route, guest_pane_id); let _ = self .bus .senders .send_to_server(ServerInstruction::KeyPassthroughChanged( client_id, old_pane_id, new_pane_id, should_route, entered_from_direction, true, )); } fn set_client_dimmed( &mut self, client_id: ClientId, dimmed: bool, guest_pane_id: Option, ) { let changed = if dimmed { self.dimmed_clients.insert(client_id) } else { self.dimmed_clients.remove(&client_id) }; let ascend_keys = if dimmed { guest_pane_id .and_then(|pane_id| self.guest_ascend_keys.get(&pane_id).cloned()) .unwrap_or_default() } else { vec![] }; let ascend_keys_changed = self .mode_info .get(&client_id) .map(|mode_info| mode_info.nested_ascend_keys != ascend_keys) .unwrap_or_else(|| !ascend_keys.is_empty()); if !changed && !ascend_keys_changed { return; } let mode_info = self .mode_info .entry(client_id) .or_insert_with(|| self.default_mode_info.clone()); mode_info.session_dimmed = if dimmed { Some(true) } else { None }; mode_info.nested_ascend_keys = ascend_keys; let mode_info = mode_info.clone(); for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); tab.set_client_dimmed(client_id, dimmed); } for tab in self.tabs.values_mut() { let _ = tab.update_input_modes(); } let _ = self.render(None); } fn base_input_mode(&self) -> InputMode { self.default_mode_info.base_mode.unwrap_or_default() } fn own_ascend_shortcut(&self) -> Vec { shortcut_for_action( &self.default_mode_info.keybinds, self.base_input_mode(), |action| matches!(action, Action::FocusHostSession), ) .unwrap_or_default() } fn own_descend_shortcut(&self) -> Vec { shortcut_for_action( &self.default_mode_info.keybinds, self.base_input_mode(), |action| matches!(action, Action::FocusGuestSession), ) .unwrap_or_default() } fn own_host_zoom_shortcut(&self) -> Vec { shortcut_for_action( &self.default_mode_info.keybinds, self.base_input_mode(), |action| matches!(action, Action::ToggleHostFullscreen), ) .unwrap_or_default() } fn guest_modal_shortcuts(&self) -> GuestModalShortcuts { GuestModalShortcuts { zoom: format_guest_modal_shortcut(&self.own_host_zoom_shortcut()), ascend: format_guest_modal_shortcut(&self.own_ascend_shortcut()), descend: format_guest_modal_shortcut(&self.own_descend_shortcut()), } } fn refresh_nested_ascend_keys_for_pane(&mut self, pane_id: PaneId) { let keys = self .guest_ascend_keys .get(&pane_id) .cloned() .unwrap_or_default(); let dimmed_client_ids: Vec = self.dimmed_clients.iter().copied().collect(); let mut clients_to_update: Vec<(ClientId, ModeInfo)> = vec![]; for client_id in dimmed_client_ids { if self.get_active_pane_id(&client_id) != Some(pane_id) { continue; } if let Some(mode_info) = self.mode_info.get_mut(&client_id) { if mode_info.nested_ascend_keys != keys { mode_info.nested_ascend_keys = keys.clone(); clients_to_update.push((client_id, mode_info.clone())); } } } if clients_to_update.is_empty() { return; } for (client_id, mode_info) in clients_to_update { for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); } } for tab in self.tabs.values_mut() { let _ = tab.update_input_modes(); } let _ = self.render(None); } fn set_host_descended(&mut self, host_descended: bool) { if self.host_descended != host_descended { self.host_descended = host_descended; self.update_all_clients_nesting_mode_info(); } } fn broadcast_nested_shortcuts(&mut self) { if let Some(client_id) = self.nested_via_client_id { let payload = nested_session::encode_payload(&NestedSessionMessage::ShortcutUpdate { ascend_keys: self.own_ascend_shortcut(), descend_keys: vec![], }); let _ = self.bus .senders .send_to_server(ServerInstruction::EmitNestedSessionFrameToClient( client_id, payload, )); } let frame = nested_session::encode_frame(&NestedSessionMessage::ShortcutUpdate { ascend_keys: vec![], descend_keys: self.own_descend_shortcut(), }); for terminal_id in self.nested_guest_terminal_ids() { let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write(frame.clone(), terminal_id, None)); } } fn update_all_clients_nesting_mode_info(&mut self) { let ancestry = self.nested_ancestry.clone(); let host_fullscreen = if self.host_fullscreen { Some(true) } else { None }; let session_ascended = if !ancestry.is_empty() && !self.host_descended { Some(true) } else { None }; let descend_keys = self.host_descend_keys.clone(); self.default_mode_info.session_ancestry = ancestry.clone(); self.default_mode_info.host_fullscreen = host_fullscreen; self.default_mode_info.session_ascended = session_ascended; self.default_mode_info.nested_descend_keys = descend_keys.clone(); let mut client_ids: Vec = self.mode_info.keys().copied().collect(); for connected_client_id in self.connected_clients.borrow().keys() { if !client_ids.contains(connected_client_id) { client_ids.push(*connected_client_id); } } for client_id in client_ids { let mode_info = self .mode_info .entry(client_id) .or_insert_with(|| self.default_mode_info.clone()); mode_info.session_ancestry = ancestry.clone(); mode_info.host_fullscreen = host_fullscreen; mode_info.session_ascended = session_ascended; mode_info.nested_descend_keys = descend_keys.clone(); let mode_info = mode_info.clone(); for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); } } for tab in self.tabs.values_mut() { let _ = tab.update_input_modes(); } let _ = self.render(None); } fn nested_guest_terminal_ids(&self) -> Vec { let mut terminal_ids = vec![]; for tab in self.tabs.values() { for pane_id in tab.nested_guest_pane_ids() { if let PaneId::Terminal(terminal_id) = pane_id { if self.nested_guest_tracker.is_tracked(pane_id) { terminal_ids.push(terminal_id); } } } } terminal_ids } fn broadcast_ancestry_update_to_guests(&self) { let mut ancestry = self.nested_ancestry.clone(); ancestry.push(self.session_name.clone()); let message = NestedSessionMessage::AncestryUpdate { ancestry }; let frame = nested_session::encode_frame(&message); for terminal_id in self.nested_guest_terminal_ids() { let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write(frame.clone(), terminal_id, None)); } } fn apply_nested_guest_fullscreen(&mut self, pane_id: PaneId, fullscreen: bool) { let mut actually_fullscreen = false; let mut displaced_pane_id = None; if let Some(tab) = self .tabs .values_mut() .find(|tab| tab.has_pane_with_pid(&pane_id)) { let currently_fullscreen = tab.fullscreen_pane_id() == Some(pane_id) && tab.fullscreen_covers_ui(); if fullscreen && !currently_fullscreen { let existing_fullscreen = tab.fullscreen_pane_id(); if existing_fullscreen.is_some() && existing_fullscreen != Some(pane_id) { displaced_pane_id = existing_fullscreen; tab.toggle_pane_no_ui_fullscreen(existing_fullscreen.unwrap()); } tab.toggle_pane_no_ui_fullscreen(pane_id); } else if !fullscreen && currently_fullscreen { tab.toggle_pane_no_ui_fullscreen(pane_id); } actually_fullscreen = tab.fullscreen_pane_id() == Some(pane_id) && tab.fullscreen_covers_ui(); } if let Some(displaced_pane_id) = displaced_pane_id { self.nested_fullscreen_panes.remove(&displaced_pane_id); } if actually_fullscreen { self.nested_fullscreen_panes.insert(pane_id); } else { self.nested_fullscreen_panes.remove(&pane_id); } } fn unset_nested_fullscreen_if_active(&mut self, pane_id: PaneId) { if !self.nested_fullscreen_panes.contains(&pane_id) { return; } self.apply_nested_guest_fullscreen(pane_id, false); if let Some(client_id) = self.nested_via_client_id { let payload = nested_session::encode_payload(&NestedSessionMessage::ToggleHostFullscreen { fullscreen: false, }); let _ = self.bus .senders .send_to_server(ServerInstruction::EmitNestedSessionFrameToClient( client_id, payload, )); } } fn sync_nested_guest_fullscreen_state(&mut self) { let mut actual: HashMap = HashMap::new(); for tab in self.tabs.values() { let fullscreen_pane_id = tab.fullscreen_pane_id(); let covers_ui = tab.fullscreen_covers_ui(); for pane_id in tab.nested_guest_pane_ids() { if !self.nested_guest_tracker.is_tracked(pane_id) { continue; } let is_fullscreen = covers_ui && fullscreen_pane_id == Some(pane_id); actual.insert(pane_id, is_fullscreen); } } let mut updates: Vec<(u32, bool)> = vec![]; for (pane_id, is_fullscreen) in actual.iter() { let previously = self.reported_guest_fullscreen.get(pane_id).copied(); if previously != Some(*is_fullscreen) { if let PaneId::Terminal(terminal_id) = pane_id { updates.push((*terminal_id, *is_fullscreen)); } self.reported_guest_fullscreen .insert(*pane_id, *is_fullscreen); } } self.reported_guest_fullscreen .retain(|pane_id, _| actual.contains_key(pane_id)); for (pane_id, is_fullscreen) in actual.iter() { if !is_fullscreen { self.downgrade_zoom_choice_to_descend(*pane_id); } } for (terminal_id, fullscreen) in updates { let message = NestedSessionMessage::FullscreenState { fullscreen }; let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write( nested_session::encode_frame(&message), terminal_id, None, )); } } fn clients_with_pane_focused(&self, pane_id: PaneId) -> Vec { let mut clients = vec![]; for (client_id, tab_id) in self.active_tab_ids.iter() { if let Some(focused) = self .tabs .get(tab_id) .and_then(|t| t.get_active_pane_id(*client_id)) { if focused == pane_id { clients.push(*client_id); } } } clients } fn bubble_focus_to_host(&self, client_id: ClientId, direction: Direction) { let payload = nested_session::encode_payload(&NestedSessionMessage::FocusHost { direction: Some(direction), }); let _ = self .bus .senders .send_to_server(ServerInstruction::EmitNestedSessionFrameToClient( client_id, payload, )); } pub fn handle_nested_session_message_from_host( &mut self, client_id: ClientId, message: NestedSessionMessage, ) { match message { NestedSessionMessage::AnnounceAck { ancestry, descend_keys, .. } => { log::info!( "nested session handshake complete: this session is nested inside ancestry {:?}", ancestry ); self.nested_ancestry = ancestry; self.nested_via_client_id = Some(client_id); self.host_descend_keys = descend_keys; self.update_all_clients_nesting_mode_info(); self.broadcast_ancestry_update_to_guests(); let payload = nested_session::encode_payload(&NestedSessionMessage::ShortcutUpdate { ascend_keys: self.own_ascend_shortcut(), descend_keys: vec![], }); let _ = self.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(client_id, payload), ); }, NestedSessionMessage::ShortcutUpdate { descend_keys, .. } => { if self.host_descend_keys != descend_keys { self.host_descend_keys = descend_keys; self.update_all_clients_nesting_mode_info(); } }, NestedSessionMessage::FullscreenState { fullscreen } => { self.host_fullscreen = fullscreen; self.own_fullscreen_requested = fullscreen; self.update_all_clients_nesting_mode_info(); }, NestedSessionMessage::AncestryUpdate { ancestry } => { self.nested_ancestry = ancestry; self.update_all_clients_nesting_mode_info(); self.broadcast_ancestry_update_to_guests(); }, NestedSessionMessage::FocusGained { from_direction: Some(direction), } => { self.set_host_descended(true); let old_pane_id = self.get_active_pane_id(&client_id); if let Ok(tab) = self.get_active_tab_mut(client_id) { tab.focus_pane_on_edge(direction, client_id); } let new_pane_id = self.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { self.report_key_passthrough_state_with_direction( client_id, old, new, Some(direction), ); } let _ = self.render(None); let _ = self.log_and_report_session_state(); }, NestedSessionMessage::FocusGained { from_direction: None, } => { self.set_host_descended(true); let _ = self.render(None); }, NestedSessionMessage::FocusLost => { self.set_host_descended(false); let _ = self.render(None); }, other => { log::debug!( "dropping unsupported nested session message from host: {:?}", other ); }, } } pub fn handle_nested_guest_ping_tick(&mut self, pane_id: PaneId) { use crate::nested_guest::GuestPingTickAction; match self.nested_guest_tracker.on_tick(pane_id, Instant::now()) { GuestPingTickAction::SendPing => { if let PaneId::Terminal(terminal_id) = pane_id { let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write( nested_session::encode_frame(&NestedSessionMessage::Ping), terminal_id, None, )); log::debug!("pinged nested session guest in pane {:?}", pane_id); } }, GuestPingTickAction::Suspended => { log::info!( "nested session guest in pane {:?} missed the pong timeout, suspending until it re-announces", pane_id ); self.suspend_nested_guest(pane_id); }, GuestPingTickAction::Unknown => { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopNestedGuestPing(pane_id)); }, } } /// Build a reply for `query` from whatever host state Zellij has /// cached, or an empty `Vec` if nothing relevant is cached. The /// classification happened at intercept time (Grid produces a /// [`HostQuery`]), so this method is pure structural dispatch — /// no re-parsing of bytes. fn synthesize_cached_reply(&self, query: &crate::host_query::HostQuery) -> Vec { use crate::host_query::HostQuery; match query { HostQuery::TextAreaPixelSize => self .pixel_dimensions .text_area_size .map(|dims| format!("\x1b[4;{};{}t", dims.height, dims.width).into_bytes()) .unwrap_or_default(), HostQuery::CharacterCellPixelSize => self .pixel_dimensions .character_cell_size .map(|dims| format!("\x1b[6;{};{}t", dims.height, dims.width).into_bytes()) .unwrap_or_default(), HostQuery::DefaultForeground { terminator } | HostQuery::DefaultBackground { terminator } => { let palette = self.terminal_emulator_colors.borrow(); let (channel, color) = match query { HostQuery::DefaultForeground { .. } => (10u32, palette.fg), HostQuery::DefaultBackground { .. } => (11u32, palette.bg), _ => unreachable!(), }; if let PaletteColor::Rgb((r, g, b)) = color { let mut out = format!( "\x1b]{};rgb:{:04x}/{:04x}/{:04x}", channel, (r as u16) * 0x0101, (g as u16) * 0x0101, (b as u16) * 0x0101, ) .into_bytes(); out.extend_from_slice(terminator.as_bytes()); out } else { Vec::new() } }, HostQuery::PaletteRegister { index, terminator } => { let codes = self.terminal_emulator_color_codes.borrow(); if let Some(color) = codes.get(&(*index as usize)) { let mut out = format!("\x1b]4;{};{}", index, color).into_bytes(); out.extend_from_slice(terminator.as_bytes()); out } else { Vec::new() } }, // Should not reach here: ColorPaletteMode short-circuits in // `forward_host_query` before any cache-fallback path runs. HostQuery::ColorPaletteMode => match self.effective_host_terminal_theme_mode() { HostTerminalThemeMode::Dark => b"\x1b[?997;1n".to_vec(), HostTerminalThemeMode::Light => b"\x1b[?997;2n".to_vec(), }, } } pub fn render(&mut self, plugin_render_assets: Option>) -> Result<()> { // here we schedule the RenderToClients background job which debounces renders every 10ms // rather than actually rendering // // when this job decides to render, it sends back the ScreenInstruction::RenderToClients // message, triggering our render_to_clients method which does the actual rendering self.sync_nested_guest_fullscreen_state(); let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::RenderToClients); if let Some(plugin_render_assets) = plugin_render_assets { let _ = self .bus .senders .send_to_plugin(PluginInstruction::UnblockCliPipes(plugin_render_assets)) .context("failed to unblock input pipe"); } Ok(()) } pub fn render_to_clients(&mut self) -> Result<()> { // this method does the actual rendering and is triggered by a debounced BackgroundJob (see // the render method for more details) let err_context = "failed to render screen"; // Separate rendering for regular clients and watchers let has_regular_clients = self .connected_clients .borrow() .keys() .any(|id| !self.watcher_clients.contains_key(id)); let has_watchers = !self.watcher_clients.is_empty(); // No change needed // Track whether non-watcher output was dirty for conditional watcher rendering let non_watcher_output_was_dirty; let mut tabs_to_close = vec![]; // === PHASE 1: Render for regular clients === if has_regular_clients { let mut output = Output::new( self.sixel_image_store.clone(), self.character_cell_size.clone(), self.styled_underlines, self.osc8_hyperlinks, self.kitty_image_store.clone(), self.kitty_host_capabilities.clone(), self.client_kitty_host_state.clone(), ); let has_ansi_subscribers = self.pane_render_subscribers.values().any(|s| s.ansi); output.collect_ansi_pane_contents = has_ansi_subscribers || self.plugins_need_ansi_pane_contents; for (tab_index, tab) in &mut self.tabs { if tab.has_selectable_tiled_panes() { // Pass None for normal client rendering tab.render(&mut output, None).context(err_context)?; } else if !tab.is_pending() { tabs_to_close.push(*tab_index); } } if self.plugins_need_ansi_pane_contents { let all_regular_clients: Vec = self .connected_clients .borrow() .keys() .copied() .filter(|id| !self.watcher_clients.contains_key(id)) .collect(); if !all_regular_clients.is_empty() { for tab in self.tabs.values() { for pane_id in tab.get_static_and_floating_pane_ids() { if let Some(pane) = tab.get_pane_with_id(pane_id) { match pane_id { PaneId::Terminal(_) => { let contents = pane.pane_contents_with_ansi(None, false, None); output.add_pane_contents_with_ansi( &all_regular_clients, pane_id, contents, ); }, PaneId::Plugin(_) => { for client_id in &all_regular_clients { let contents = pane.pane_contents_with_ansi( Some(*client_id), false, None, ); output.add_pane_contents_with_ansi( &[*client_id], pane_id, contents, ); } }, } } } } } } let pane_render_report = output.drain_pane_render_report(); // Subscriber delivery — gated behind is_empty() for zero overhead if !self.pane_render_subscribers.is_empty() { self.deliver_to_pane_subscribers_from_report(&pane_render_report); } let _ = self .bus .senders .send_to_plugin(PluginInstruction::PaneRenderReport(pane_render_report)); non_watcher_output_was_dirty = output.is_dirty(); let mut bell_state_changed = false; let mut has_bell = false; if self.visual_bell { let mut panes_to_flash: Vec = vec![]; let mut tabs_to_flash: Vec = vec![]; let active_tab_ids_snapshot: Vec = self.active_tab_ids.values().copied().collect(); for tab in self.tabs.values_mut() { let is_active = active_tab_ids_snapshot.contains(&tab.id); let (new_panes, tab_newly_set) = tab.check_and_handle_bell_notifications(is_active); if !new_panes.is_empty() { panes_to_flash.extend(new_panes); bell_state_changed = true; } if tab_newly_set { tabs_to_flash.push(tab.id); bell_state_changed = true; } } has_bell = !panes_to_flash.is_empty() || !tabs_to_flash.is_empty(); if !panes_to_flash.is_empty() { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::FlashPaneBell(panes_to_flash)); } for tab_id in tabs_to_flash { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::FlashTabBell(tab_id)); } } else { // visual_bell disabled: still detect bell for ANSI BEL forwarding only for tab in self.tabs.values_mut() { if tab.check_and_consume_bells_without_visual_notification() { has_bell = true; } } } if has_bell { output.add_post_vte_instruction_to_multiple_clients( self.active_tab_ids.keys().copied(), "\u{7}", // ANSI BEL ); } for (client_id, tab_index) in &self.active_tab_ids { if self.watcher_clients.contains_key(client_id) { continue; } if let Some(tab) = self.tabs.get(tab_index) { output.set_kitty_visible_panes(*client_id, tab.kitty_visible_pane_ids()); } } if non_watcher_output_was_dirty || has_bell { let mut serialized_output = output.serialize().context(err_context)?; self.mobile_render_gate .blank_gated_clients(&mut serialized_output); if !serialized_output.is_empty() { let _ = self .bus .senders .send_to_server(ServerInstruction::Render(Some(serialized_output))) .context(err_context); } } let single_pane_names_changed = self.update_single_pane_tab_names(); if bell_state_changed || single_pane_names_changed { self.log_and_report_session_state()?; } } else { // No regular clients, output is not dirty non_watcher_output_was_dirty = false; // No regular clients but subscribers exist — query panes directly if !self.pane_render_subscribers.is_empty() { self.deliver_to_pane_subscribers_directly(); } } // === PHASE 2: Render for watchers === if has_watchers { if let Some(followed_client_id) = self.followed_client_id { // Create fresh output for watchers let mut watcher_output = Output::new( self.sixel_image_store.clone(), self.character_cell_size.clone(), self.styled_underlines, self.osc8_hyperlinks, self.kitty_image_store.clone(), self.kitty_host_capabilities.clone(), self.client_kitty_host_state.clone(), ); let focused_tab_index_of_followed_client_id = *self.active_tab_ids.get(&followed_client_id).unwrap_or(&0); if let Some(tab) = self .tabs .get_mut(&focused_tab_index_of_followed_client_id) .as_mut() { // Only force render if: // 1. Non-watcher output was dirty, OR // 2. Any watcher needs a forced render (first render or after resize), OR // 3. No non-watcher clients are connected let any_watcher_needs_force_render = self .watcher_clients .values() .any(|state| state.should_force_render()); let should_force_render = non_watcher_output_was_dirty || any_watcher_needs_force_render || !has_regular_clients; if should_force_render { tab.set_force_render(); } tab.render(&mut watcher_output, Some(followed_client_id)) .context(err_context)?; } // Send the rendered output to all watcher clients if watcher_output.is_dirty() { let mut watcher_render_output: HashMap = HashMap::new(); // For each watcher, clone the output and serialize with size constraints for (watcher_id, watcher_state) in &self.watcher_clients { let mut watcher_specific_output = watcher_output.clone(); // Serialize this watcher's output with size constraints (cropping and padding handled inside) let mut serialized_output = watcher_specific_output .serialize_with_size(Some(watcher_state.size()), Some(self.size)) .context(err_context)?; // Get the output for the followed client and map it to this watcher if let Some(followed_output) = serialized_output.remove(&followed_client_id) { watcher_render_output.insert(*watcher_id, followed_output); } } // Send to server for delivery to watcher clients if !watcher_render_output.is_empty() { let _ = self .bus .senders .send_to_server(ServerInstruction::Render(Some(watcher_render_output))) .context(err_context); } // Clear force render flag for all watchers after successful render for watcher_state in self.watcher_clients.values_mut() { watcher_state.clear_force_render(); } } } } for tab_index in tabs_to_close { self.close_tab_by_id(tab_index) .context(err_context) .non_fatal(); } Ok(()) } /// Returns a mutable reference to this [`Screen`]'s tabs. pub fn get_tabs_mut(&mut self) -> &mut BTreeMap { &mut self.tabs } pub fn get_tabs(&self) -> &BTreeMap { &self.tabs } /// Returns an immutable reference to this [`Screen`]'s active [`Tab`]. pub fn get_active_tab(&self, client_id: ClientId) -> Result<&Tab> { match self.active_tab_ids.get(&client_id) { Some(tab) => self .tabs .get(tab) .ok_or_else(|| anyhow!("active tab {} does not exist", tab)), None => Err(anyhow!("active tab not found for client {:?}", client_id)), } } pub fn get_client_input_mode(&self, client_id: ClientId) -> Option { self.get_active_tab(client_id) .ok() .and_then(|tab| tab.get_client_input_mode(client_id)) } pub fn get_first_client_id(&self) -> Option { self.active_tab_ids.keys().next().copied() } /// Returns an immutable reference to this [`Screen`]'s previous active [`Tab`]. /// Consumes the last entry in tab history. pub fn get_previous_tab(&mut self, client_id: ClientId) -> Result> { Ok( match self .tab_history .get_mut(&client_id) .with_context(|| { format!("failed to retrieve tab history for client {client_id:?}") })? .pop() { Some(tab) => self.tabs.get(&tab), None => None, }, ) } /// Returns a mutable reference to this [`Screen`]'s active [`Tab`]. pub fn get_active_tab_mut(&mut self, client_id: ClientId) -> Result<&mut Tab> { match self.active_tab_ids.get(&client_id) { Some(tab) => self .tabs .get_mut(tab) .ok_or_else(|| anyhow!("active tab {} does not exist", tab)), None => Err(anyhow!("active tab not found for client {:?}", client_id)), } } /// Returns a mutable reference to this [`Screen`]'s indexed [`Tab`]. pub fn get_indexed_tab_mut(&mut self, tab_index: usize) -> Option<&mut Tab> { self.get_tabs_mut().get_mut(&tab_index) } /// Clear bell notification for the currently focused pane of the given client. /// Also cancels any running flash jobs if applicable. pub fn clear_bell_for_focused_pane(&mut self, client_id: ClientId) { let tab_id_and_pane_id: Option<(usize, PaneId)> = self.get_active_tab_mut(client_id).ok().and_then(|tab| { tab.get_active_pane_id(client_id) .map(|pane_id| (tab.id, pane_id)) }); if let Some((tab_id, focused_pane_id)) = tab_id_and_pane_id { if let Some(tab) = self.tabs.get_mut(&tab_id) { if tab.panes_with_pending_bell.contains(&focused_pane_id) { let tab_had_bell = tab.tab_has_pending_bell; tab.clear_bell_notification_for_pane(focused_pane_id); let tab_bell_now_cleared = tab_had_bell && !tab.tab_has_pending_bell; let _ = self.bus .senders .send_to_background_jobs(BackgroundJob::StopFlashPaneBell(vec![ focused_pane_id, ])); if tab_bell_now_cleared { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); } } } } } /// Clear bell notification for a specific pane ID in the given client's active tab. pub fn clear_bell_for_pane_id(&mut self, pane_id: PaneId, client_id: ClientId) { let tab_id: Option = self.get_active_tab_mut(client_id).ok().map(|tab| tab.id); if let Some(tab_id) = tab_id { if let Some(tab) = self.tabs.get_mut(&tab_id) { if tab.panes_with_pending_bell.contains(&pane_id) { let tab_had_bell = tab.tab_has_pending_bell; tab.clear_bell_notification_for_pane(pane_id); let tab_bell_now_cleared = tab_had_bell && !tab.tab_has_pending_bell; let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopFlashPaneBell(vec![pane_id])); if tab_bell_now_cleared { let _ = self .bus .senders .send_to_background_jobs(BackgroundJob::StopFlashTabBell(tab_id)); } } } } } pub fn show_floating_panes_in_tab( &mut self, client_id: ClientId, tab_id: Option, completion: Option, ) -> Result<()> { let tab = match tab_id { Some(id) => self.tabs.get_mut(&id), None => self.get_active_tab_mut(client_id).ok(), }; match tab { None => { let mut completion = completion; if let Some(c) = completion.as_mut() { c.set_exit_status(1); c.set_error_message("Tab not found".to_string()); } drop(completion); }, Some(tab) => tab.show_floating_panes_atomic(completion), } Ok(()) } pub fn hide_floating_panes_in_tab( &mut self, client_id: ClientId, tab_id: Option, completion: Option, ) -> Result<()> { let tab = match tab_id { Some(id) => self.tabs.get_mut(&id), None => self.get_active_tab_mut(client_id).ok(), }; match tab { None => { let mut completion = completion; if let Some(c) = completion.as_mut() { c.set_exit_status(1); c.set_error_message("Tab not found".to_string()); } drop(completion); }, Some(tab) => tab.hide_floating_panes_atomic(completion), } Ok(()) } pub fn are_floating_panes_visible_in_tab( &self, client_id: ClientId, tab_id: Option, completion: Option, ) -> Result<()> { let tab = match tab_id { Some(id) => self.tabs.get(&id), None => self.get_active_tab(client_id).ok(), }; let mut completion = completion; match tab { None => { if let Some(c) = completion.as_mut() { c.set_error_message("Tab not found".to_string()); } }, Some(tab) => { if tab.are_floating_panes_visible() { if let Some(c) = completion.as_mut() { c.set_stdout_message("true".to_string()); } } else { if let Some(c) = completion.as_mut() { c.set_error_message("false".to_string()); } } }, } drop(completion); Ok(()) } /// Creates a new [`Tab`] in this [`Screen`] pub fn new_tab( &mut self, tab_id: usize, swap_layouts: (Vec, Vec), tab_name: Option, client_id: Option, ) -> Result<()> { let err_context = || format!("failed to create new tab for client {client_id:?}",); let client_id = client_id.map(|client_id| { if self.get_active_tab(client_id).is_ok() { client_id } else if let Some(first_client_id) = self.get_first_client_id() { first_client_id } else { client_id } }); let tab_name = tab_name.unwrap_or_else(|| String::new()); let mobile_tab_count = self.mobile_state.mobile_tab_count(); let position = self.tabs.len(); let mut tab = Tab::new( tab_id, position, tab_name, self.size, self.character_cell_size.clone(), self.stacked_resize.clone(), self.stacked_pane_list.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.bus .os_input .as_ref() .with_context(err_context)? .clone(), self.bus.senders.clone(), self.max_panes, self.style, self.default_mode_info.clone(), self.pane_frame_style, self.auto_layout, self.connected_clients.clone(), self.session_is_mirrored, client_id, self.copy_options.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), swap_layouts, self.default_shell.clone(), self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, self.default_editor.clone(), self.web_clients_allowed, self.web_sharing, self.current_pane_group.clone(), self.currently_marking_pane_group.clone(), self.advanced_mouse_actions, self.mouse_scroll_resize, self.mouse_hover_effects, self.focus_follows_mouse, self.mouse_click_through, self.web_server_ip, self.web_server_port, mobile_tab_count, ); for (client_id, mode_info) in &self.mode_info { tab.change_mode_info(mode_info.clone(), *client_id); } for dimmed_client_id in &self.dimmed_clients { tab.set_client_dimmed(*dimmed_client_id, true); } if let Some(aggregate) = self.kitty_host_support_aggregate() { tab.update_kitty_host_support(aggregate); } self.tabs.insert(tab_id, tab); Ok(()) } pub fn apply_layout( &mut self, layout: TiledPaneLayout, floating_panes_layout: Vec, new_terminal_ids: Vec<(u32, HoldForCommand)>, new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, new_plugin_ids: HashMap>, tab_id: usize, should_change_client_focus: bool, client_id_and_is_web_client: (ClientId, bool), blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { if self.tabs.get(&tab_id).is_none() { // TODO: we should prevent this situation with a UI - eg. cannot close tabs with a // pending state log::error!("Tab with index {tab_id} not found. Cannot apply layout!"); return Ok(()); } let (client_id, mut is_web_client) = client_id_and_is_web_client; let client_id = if self.get_active_tab(client_id).is_ok() { if let Some(connected_client_is_web_client) = self.connected_clients.borrow().get(&client_id) { is_web_client = *connected_client_is_web_client; } client_id } else if let Some(first_client_id) = self.get_first_client_id() { if let Some(first_client_is_web_client) = self.connected_clients.borrow().get(&first_client_id) { is_web_client = *first_client_is_web_client; } first_client_id } else { client_id }; let err_context = || format!("failed to apply layout for tab {tab_id:?}",); let passthrough_affected_client_ids: Vec = if should_change_client_focus { if self.session_is_mirrored { self.connected_clients .borrow() .iter() .map(|(c, _i)| *c) .collect() } else { vec![client_id] } } else { vec![] }; let passthrough_old_focused_panes: Vec<(ClientId, Option)> = passthrough_affected_client_ids .iter() .map(|c| (*c, self.get_active_pane_id(c))) .collect(); // move the relevant clients out of the current tab and place them in the new one let drained_clients = if should_change_client_focus { if self.session_is_mirrored { let client_mode_infos_in_source_tab = if let Ok(active_tab) = self.get_active_tab_mut(client_id) { let client_mode_infos_in_source_tab = active_tab.drain_connected_clients(None); if active_tab.has_no_connected_clients() { active_tab .visible(false) .with_context(err_context) .non_fatal(); } Some(client_mode_infos_in_source_tab) } else { None }; let all_connected_clients: Vec = self .connected_clients .borrow() .iter() .map(|(c, _i)| *c) .collect(); for client_id in all_connected_clients { self.update_client_tab_focus(client_id, tab_id); } client_mode_infos_in_source_tab } else if let Ok(active_tab) = self.get_active_tab_mut(client_id) { let client_mode_info_in_source_tab = active_tab.drain_connected_clients(Some(vec![client_id])); if active_tab.has_no_connected_clients() { active_tab .visible(false) .with_context(err_context) .non_fatal(); } self.update_client_tab_focus(client_id, tab_id); Some(client_mode_info_in_source_tab) } else { None } } else { None }; if !self.active_tab_ids.contains_key(&client_id) { self.connected_clients .borrow_mut() .insert(client_id, is_web_client); } // apply the layout to the new tab self.tabs .get_mut(&tab_id) .context("couldn't find tab with index {tab_id}") .and_then(|tab| { tab.apply_layout( layout, floating_panes_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, client_id, blocking_terminal, )?; tab.update_input_modes()?; if let Some(drained_clients) = drained_clients { tab.visible(true)?; tab.add_multiple_clients(drained_clients)?; } tab.resize_whole_tab(self.size).with_context(err_context)?; tab.set_force_render(); Ok(()) }) .with_context(err_context)?; if !self.active_tab_ids.contains_key(&client_id) { // this means this is a new client and we need to add it to our state properly self.add_client(client_id, is_web_client) .with_context(err_context)?; } self.recompute_tab_size(tab_id).with_context(err_context)?; for (affected_client_id, old_pane_id) in passthrough_old_focused_panes { let new_pane_id = self.get_active_pane_id(&affected_client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { self.report_key_passthrough_state(affected_client_id, old, new); } } self.log_and_report_session_state() .and_then(|_| self.render(None)) .with_context(err_context) } pub fn add_client(&mut self, client_id: ClientId, is_web_client: bool) -> Result<()> { let err_context = |tab_index| { format!("failed to attach client {client_id} to tab with index {tab_index}") }; // Set followed_client_id to the first regular client if not already set if self.followed_client_id.is_none() && !self.watcher_clients.contains_key(&client_id) { self.followed_client_id = Some(client_id); } let mut tab_history = vec![]; if let Some((_first_client, first_tab_history)) = self.tab_history.iter().next() { tab_history = first_tab_history.clone(); } let tab_index = if let Some((_first_client, first_active_tab_index)) = self.active_tab_ids.iter().next() { *first_active_tab_index } else if self.tabs.contains_key(&self.global_last_active_tab_id) { self.global_last_active_tab_id } else if self.tabs.contains_key(&0) { 0 } else if let Some(tab_index) = self.tabs.keys().next() { tab_index.to_owned() } else { bail!("Can't find a valid tab to attach client to!"); }; self.active_tab_ids.insert(client_id, tab_index); self.client_kitty_host_state.borrow_mut().remove(&client_id); self.connected_clients .borrow_mut() .insert(client_id, is_web_client); if is_web_client { self.kitty_host_capabilities .borrow_mut() .insert(client_id, false); self.push_kitty_host_support_to_tabs(); } self.tab_history.insert(client_id, tab_history); self.tabs .get_mut(&tab_index) .with_context(|| err_context(tab_index))? .add_client(client_id, None) .with_context(|| err_context(tab_index))?; self.recompute_tab_size(tab_index) .with_context(|| err_context(tab_index))?; if !self.nested_ancestry.is_empty() || !self.host_descend_keys.is_empty() { self.update_all_clients_nesting_mode_info(); } Ok(()) } pub fn remove_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to remove client {client_id}"); self.set_client_dimmed(client_id, false, None); let passthrough_panes: Vec = self .nested_guest_choices .iter() .filter(|((choice_client_id, _), choice)| { *choice_client_id == client_id && choice.enters_passthrough() }) .map(|((_, choice_pane_id), _)| *choice_pane_id) .collect(); for pane_id in passthrough_panes { let _ = self .bus .senders .send_to_server(ServerInstruction::KeyPassthroughChanged( client_id, pane_id, pane_id, false, None, true, )); } self.nested_guest_choices .retain(|(choice_client_id, _), _| *choice_client_id != client_id); for tab in self.tabs.values_mut() { tab.clear_guest_modal_for_client_on_all_panes(client_id); tab.clear_guest_choice_indicator_for_client_on_all_panes(client_id); } // If the followed client disconnected, find the next regular client if Some(client_id) == self.followed_client_id { // Try to find another regular (non-watcher) client self.followed_client_id = self .connected_clients .borrow() .keys() .copied() .find(|id| !self.watcher_clients.contains_key(id) && id != &client_id); // If no regular client remains but we have watchers, keep the old followed_client_id // for terminal rendering (plugins will use their last state) if self.followed_client_id.is_none() && !self.watcher_clients.is_empty() { self.followed_client_id = Some(client_id); // Keep the disconnected client's ID } } self.mobile_state.forget_client(client_id); self.mobile_render_gate.ungate(client_id); let to_recompute = self .mobile_state .clear_fits_owned_by(client_id, &mut self.tabs); for tab_id in to_recompute { self.recompute_tab_size(tab_id).with_context(err_context)?; } let mut tabs_to_garbage_collect: Vec = vec![]; for (tab_id, tab) in self.tabs.iter_mut() { tab.remove_client(client_id); if tab.has_no_connected_clients() { tab.visible(false).with_context(err_context)?; } if let Some(set) = tab.visible_to.as_mut() { set.remove(&client_id); if set.is_empty() { tabs_to_garbage_collect.push(*tab_id); } } } self.mobile_state .clear_shadow_focus(client_id, &mut self.tabs); let previously_active_tab_id = self.active_tab_ids.get(&client_id).copied(); if let Some(prev_tab_id) = previously_active_tab_id { self.global_last_active_tab_id = prev_tab_id; self.active_tab_ids.remove(&client_id); } if self.tab_history.contains_key(&client_id) { self.tab_history.remove(&client_id); } self.connected_clients.borrow_mut().remove(&client_id); self.client_kitty_host_state.borrow_mut().remove(&client_id); let removed_kitty_capability = self .kitty_host_capabilities .borrow_mut() .remove(&client_id) .is_some(); if removed_kitty_capability { self.push_kitty_host_support_to_tabs(); } self.client_sizes.remove(&client_id); self.pane_render_subscribers.remove(&client_id); if let Some(prev_tab_id) = previously_active_tab_id { self.recompute_tab_size(prev_tab_id) .with_context(err_context)?; } for tab_id in tabs_to_garbage_collect { if self.tabs.contains_key(&tab_id) { self.close_tab_by_id(tab_id).with_context(err_context)?; } } if self.nested_via_client_id == Some(client_id) { self.nested_via_client_id = None; self.nested_ancestry = vec![]; self.host_descended = false; self.host_descend_keys = vec![]; self.update_all_clients_nesting_mode_info(); self.broadcast_ancestry_update_to_guests(); } self.log_and_report_session_state().non_fatal(); Ok(()) } pub fn add_watcher_client(&mut self, client_id: ClientId) -> Result<()> { // Initialize with a default size - will be updated when we receive the actual size let default_size = Size { rows: 24, cols: 80 }; // Reasonable default self.watcher_clients .insert(client_id, WatcherState::new(default_size)); // Force a full render for the new watcher // This ensures they get complete state, not just delta self.render(None)?; Ok(()) } pub fn remove_watcher_client(&mut self, client_id: ClientId) { self.watcher_clients.remove(&client_id); } pub fn set_followed_client(&mut self, client_id: ClientId) -> Result<()> { self.followed_client_id = Some(client_id); // Trigger re-render with new followed client self.render(None)?; Ok(()) } pub fn set_watcher_size(&mut self, client_id: ClientId, size: Size) { // Update size if this client is a watcher if let Some(watcher_state) = self.watcher_clients.get_mut(&client_id) { watcher_state.set_size(size); watcher_state.set_force_render(); } } pub fn generate_and_report_tab_state(&mut self) -> Result> { let mut plugin_updates = vec![]; let mut tab_infos_for_screen_state = BTreeMap::new(); for tab in self.tabs.values() { let mut all_focused_clients: Vec = self .active_tab_ids .iter() .filter(|(_c_id, tab_position)| **tab_position == tab.id) .map(|(c_id, _)| c_id) .copied() .collect(); for s_id in tab.shadow_focus_clients() { if !all_focused_clients.contains(&s_id) { all_focused_clients.push(s_id); } } let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let tab_viewport = tab.get_viewport(); let tab_display_area = tab.get_display_area(); let selectable_tiled_panes_count = tab.get_selectable_tiled_panes_count(); let selectable_floating_panes_count = tab.get_selectable_floating_panes_count(); let tab_info_for_screen = TabInfo { position: tab.position, name: tab.name.clone(), active: self.active_tab_ids.values().any(|i| i == &tab.id), panes_to_hide: tab.panes_to_hide_count(), is_fullscreen_active: tab.is_fullscreen_active(), is_sync_panes_active: tab.is_sync_panes_active(), are_floating_panes_visible: tab.are_floating_panes_visible(), other_focused_clients: all_focused_clients, active_swap_layout_name, is_swap_layout_dirty, viewport_rows: tab_viewport.rows, viewport_columns: tab_viewport.cols, display_area_rows: tab_display_area.rows, display_area_columns: tab_display_area.cols, selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, has_bell_notification: tab.tab_has_pending_bell && !self.active_tab_ids.values().any(|i| i == &tab.id), is_flashing_bell: tab.tab_bell_flash && !self.active_tab_ids.values().any(|i| i == &tab.id), }; tab_infos_for_screen_state.insert(tab.position, tab_info_for_screen); } for (client_id, active_tab_index) in self.active_tab_ids.iter() { let mut plugin_tab_updates = vec![]; for tab in self.tabs.values() { if let Some(set) = &tab.visible_to { if !set.contains(client_id) { continue; } } let other_focused_clients: Vec = if self.session_is_mirrored { vec![] } else { let mut clients: Vec = self .active_tab_ids .iter() .filter(|(c_id, tab_position)| { **tab_position == tab.id && *c_id != client_id }) .map(|(c_id, _)| c_id) .copied() .collect(); for s_id in tab.shadow_focus_clients() { if s_id != *client_id && !clients.contains(&s_id) { clients.push(s_id); } } clients }; let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let tab_viewport = tab.get_viewport(); let tab_display_area = tab.get_display_area(); let selectable_tiled_panes_count = tab.get_selectable_tiled_panes_count(); let selectable_floating_panes_count = tab.get_selectable_floating_panes_count(); let tab_info_for_plugins = TabInfo { position: tab.position, name: tab .single_pane_tab_name() .unwrap_or_else(|| tab.name.clone()), active: *active_tab_index == tab.id, panes_to_hide: tab.panes_to_hide_count(), is_fullscreen_active: tab.is_fullscreen_active(), is_sync_panes_active: tab.is_sync_panes_active(), are_floating_panes_visible: tab.are_floating_panes_visible(), other_focused_clients, active_swap_layout_name, is_swap_layout_dirty, viewport_rows: tab_viewport.rows, viewport_columns: tab_viewport.cols, display_area_rows: tab_display_area.rows, display_area_columns: tab_display_area.cols, selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, has_bell_notification: tab.tab_has_pending_bell && *active_tab_index != tab.id, is_flashing_bell: tab.tab_bell_flash && *active_tab_index != tab.id, }; plugin_tab_updates.push(tab_info_for_plugins); } plugin_tab_updates.sort_by(|a, b| a.position.cmp(&b.position)); let target_plugin_ids = self.targeted_plugin_ids(*client_id, EventType::TabUpdate); for plugin_id in target_plugin_ids { plugin_updates.push(( Some(plugin_id), Some(*client_id), Event::TabUpdate(plugin_tab_updates.clone()), )); } } self.bus .senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .context("failed to update tabs")?; Ok(tab_infos_for_screen_state.values().cloned().collect()) } fn generate_and_report_pane_state(&mut self) -> Result { let mut pane_manifest = PaneManifest::default(); for tab in self.tabs.values() { pane_manifest.panes.insert(tab.position, tab.pane_infos()); } let mut plugin_updates = vec![]; let client_ids: Vec = self.active_tab_ids.keys().copied().collect(); for client_id in client_ids { let target_plugin_ids = self.targeted_plugin_ids(client_id, EventType::PaneUpdate); for plugin_id in target_plugin_ids { plugin_updates.push(( Some(plugin_id), Some(client_id), Event::PaneUpdate(pane_manifest.clone()), )); } } if !plugin_updates.is_empty() { self.bus .senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .context("failed to update pane state")?; } Ok(pane_manifest) } fn collect_pane_list(&self, show_all: bool) -> Result { fn should_include_pane(pane_info: &PaneInfo, show_all: bool) -> bool { pane_info.is_selectable || show_all } fn create_pane_list_entry(pane_info: PaneInfo, tab: &crate::tab::Tab) -> PaneListEntry { PaneListEntry { pane_info, tab_id: tab.id, tab_position: tab.position, tab_name: tab.name.clone(), pane_command: None, pane_cwd: None, } } fn sort_panes_by_tab_and_type(pane_entries: &mut [PaneListEntry]) { pane_entries.sort_by_key(|e| (e.tab_position, !e.pane_info.is_plugin, e.pane_info.id)); } let mut pane_entries = Vec::new(); for tab in self.tabs.values() { let pane_infos = tab.pane_infos(); for pane_info in pane_infos { if should_include_pane(&pane_info, show_all) { pane_entries.push(create_pane_list_entry(pane_info, tab)); } } } sort_panes_by_tab_and_type(&mut pane_entries); Ok(pane_entries) } fn collect_tab_list(&self, _client_id: ClientId) -> Result { let mut tab_infos = Vec::new(); for tab in self.tabs.values() { if let Some(tab_info) = self.get_tab_info(tab.id) { tab_infos.push(tab_info); } } // Sort by position (display order) tab_infos.sort_by_key(|t| t.position); Ok(tab_infos) } fn get_current_tab_info(&self, client_id: ClientId) -> Result> { match self.active_tab_ids.get(&client_id) { Some(active_tab_id) => Ok(self.get_tab_info(*active_tab_id)), None => Ok(None), } } fn update_single_pane_tab_names(&mut self) -> bool { let current: HashMap> = self .tabs .values() .map(|tab| (tab.id, tab.single_pane_tab_name())) .collect(); if current != self.last_single_pane_tab_names { self.last_single_pane_tab_names = current; true } else { false } } fn log_and_report_session_state(&mut self) -> Result<()> { let err_context = || format!("Failed to log and report session state"); self.update_active_pane_ids(); // generate own session info let pane_manifest = self.generate_and_report_pane_state()?; let tab_infos = self.generate_and_report_tab_state()?; // Lazy-load layouts on first call if cache is empty // After that, cache is updated by watcher via UpdateAvailableLayouts instruction if self.cached_layouts.is_empty() { #[cfg(not(test))] { let (layouts, errors) = Layout::list_available_layouts( self.layout_dir.clone(), &self.default_layout_name, ); self.cached_layouts = layouts; self.cached_layout_errors = errors; } #[cfg(test)] { self.cached_layouts = vec![]; self.cached_layout_errors = vec![]; } } let available_layouts = self.cached_layouts.clone(); let creation_time = { let sock_path = ZELLIJ_SOCK_DIR.join(&self.session_name); std::fs::metadata(&sock_path) .ok() .and_then(|f| f.created().ok().or_else(|| f.modified().ok())) .and_then(|d| d.elapsed().ok()) .map(|d| Duration::from_secs(d.as_secs())) .unwrap_or_default() }; let session_info = SessionInfo { name: self.session_name.clone(), tabs: tab_infos, panes: pane_manifest, connected_clients: self.active_tab_ids.keys().len(), is_current_session: true, available_layouts, web_clients_allowed: self.web_sharing.web_clients_allowed(), web_client_count: self .connected_clients .borrow() .iter() .filter(|(_client_id, is_web_client)| **is_web_client) .count(), plugins: Default::default(), // these are filled in by the wasm thread tab_history: self.tab_history.clone(), pane_history: self .pane_history .iter() .map(|(k, v)| (*k, v.iter().map(|v| (*v).into()).collect())) .collect(), creation_time, }; self.bus .senders .send_to_background_jobs(BackgroundJob::ReportSessionInfo( self.session_name.to_owned(), session_info.clone(), )) .with_context(err_context)?; self.peer_sessions_cache .insert(self.session_name.clone(), session_info); let mut live_sessions: Vec = self.peer_sessions_cache.values().cloned().collect(); for info in live_sessions.iter_mut() { info.is_current_session = info.name == self.session_name; } let resurrectable_sessions: Vec<(String, Duration)> = self .resurrectable_sessions_cache .iter() .map(|(n, d)| (n.clone(), *d)) .collect(); self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::SessionUpdate(live_sessions, resurrectable_sessions), )])) .with_context(err_context)?; self.bus .senders .send_to_background_jobs(BackgroundJob::QueryZellijWebServerStatus) .with_context(err_context)?; Ok(()) } fn dump_layout_to_hd(&mut self) -> Result<()> { let err_context = || format!("Failed to log and report session state"); let session_layout_metadata = self.get_layout_metadata(Some(self.default_shell.clone()), None); self.bus .senders .send_to_plugin(PluginInstruction::LogLayoutToHd(session_layout_metadata)) .with_context(err_context)?; Ok(()) } pub fn update_session_infos( &mut self, new_session_infos: BTreeMap, resurrectable_sessions: BTreeMap, ) -> Result<()> { self.peer_sessions_cache = new_session_infos; self.resurrectable_sessions_cache = resurrectable_sessions; self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::SessionUpdate( self.peer_sessions_cache.values().cloned().collect(), self.resurrectable_sessions_cache .iter() .map(|(n, c)| (n.clone(), c.clone())) .collect(), ), )])) .context("failed to update session info")?; Ok(()) } pub fn update_available_layouts( &mut self, layouts: Vec, errors: Vec, ) { self.cached_layouts = layouts; self.cached_layout_errors = errors; } pub fn update_active_tab_name(&mut self, buf: Vec, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to update active tabs name for client id: {client_id:?}"); let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; match client_id { Some(client_id) => { let s = str::from_utf8(&buf) .with_context(|| format!("failed to construct tab name from buf: {buf:?}")) .with_context(err_context)?; match self.get_active_tab_mut(client_id) { Ok(active_tab) => { match s { "\0" => { active_tab.name = String::new(); }, "\u{007F}" | "\u{0008}" => { // delete and backspace keys active_tab.name.pop(); }, c => { active_tab .name .push_str(&clean_string_from_control_and_linebreak(c)); }, } self.log_and_report_session_state() .with_context(err_context) }, Err(err) => { Err::<(), _>(err).with_context(err_context).non_fatal(); Ok(()) }, } }, None => Ok(()), } } pub fn undo_active_rename_tab(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to undo active tab rename for client {}", client_id); let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; match client_id { Some(client_id) => { match self.get_active_tab_mut(client_id) { Ok(active_tab) => { if active_tab.name != active_tab.prev_name { active_tab.name = active_tab.prev_name.clone(); self.log_and_report_session_state() .context("failed to undo renaming of active tab")?; } }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), }; Ok(()) }, None => Ok(()), } } pub fn move_active_tab_to_left(&mut self, client_id: ClientId) -> Result<()> { let err_context = || "Failed to move active tab left"; if self.tabs.len() < 2 { debug!("cannot move tab to left: only one tab exists"); return Ok(()); } let Some(client_id) = self.client_id(client_id) else { return Ok(()); }; match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; if active_tab_pos == 0 { self.rotate_tab_positions_left(); } else { self.switch_tabs(active_tab_pos, active_tab_pos.saturating_sub(1)); } self.log_and_report_session_state() .context("failed to move tab to left")?; }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), } Ok(()) } fn client_id(&mut self, client_id: ClientId) -> Option { if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() } } /// Switches tabs at two positions, swapping their display order. /// /// # Arguments /// * `active_tab_pos` - Current position of active tab (0-based) /// * `other_tab_pos` - Position to swap with (0-based) /// /// NOTE: this expects positions rather than IDs (see distinction at top of file) fn switch_tabs(&mut self, active_tab_pos: usize, other_tab_pos: usize) { let Some(active_tab_id) = self .tabs .values() .find(|t| t.position == active_tab_pos) .map(|t| t.id) else { log::error!("Failed to find active tab at position: {}", active_tab_pos); return; }; let Some(other_tab_id) = self .tabs .values() .find(|t| t.position == other_tab_pos) .map(|t| t.id) else { log::error!( "Failed to find tab to switch to at position: {}", other_tab_pos ); return; }; if !self.tabs.contains_key(&active_tab_id) || !self.tabs.contains_key(&other_tab_id) { warn!( "failed to switch tabs: index {} or {} not found in {:?}", active_tab_id, other_tab_id, self.tabs.keys() ); return; } // NOTE: Can `expect` here, because we checked that the keys exist above let mut active_tab = self .tabs .remove(&active_tab_id) .expect("active tab not found"); let mut other_tab = self .tabs .remove(&other_tab_id) .expect("other tab not found"); std::mem::swap(&mut active_tab.position, &mut other_tab.position); self.tabs.insert(active_tab_id, active_tab); self.tabs.insert(other_tab_id, other_tab); } fn rotate_tab_positions_left(&mut self) { let last_position = self.tabs.len().saturating_sub(1); for tab in self.tabs.values_mut() { tab.position = if tab.position == 0 { last_position } else { tab.position.saturating_sub(1) }; } } fn rotate_tab_positions_right(&mut self) { let num_tabs = self.tabs.len(); for tab in self.tabs.values_mut() { tab.position = (tab.position + 1) % num_tabs; } } pub fn move_active_tab_to_right(&mut self, client_id: ClientId) -> Result<()> { let err_context = || "Failed to move active tab right "; if self.tabs.len() < 2 { debug!("cannot move tab to right: only one tab exists"); return Ok(()); } let Some(client_id) = self.client_id(client_id) else { return Ok(()); }; match self.get_active_tab(client_id) { Ok(active_tab) => { let active_tab_pos = active_tab.position; if active_tab_pos == self.tabs.len().saturating_sub(1) { self.rotate_tab_positions_right(); } else { self.switch_tabs(active_tab_pos, active_tab_pos + 1); } self.log_and_report_session_state() .context("failed to move tab to the right")?; }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), } Ok(()) } pub fn move_tab_by_id(&mut self, tab_id: usize, direction: Direction) -> Result<()> { if self.tabs.len() < 2 { return Ok(()); } if let Some(tab) = self.tabs.get(&tab_id) { let tab_pos = tab.position; let num_tabs = self.tabs.len(); match direction { Direction::Left => { if tab_pos == 0 { self.rotate_tab_positions_left(); } else { self.switch_tabs(tab_pos, tab_pos.saturating_sub(1)); } }, Direction::Right => { if tab_pos == num_tabs.saturating_sub(1) { self.rotate_tab_positions_right(); } else { self.switch_tabs(tab_pos, tab_pos + 1); } }, _ => return Ok(()), } self.log_and_report_session_state()?; } else { log::error!("Tab with id {} not found", tab_id); } Ok(()) } pub fn change_mode( &mut self, new_mode: InputMode, base_mode: Option, client_id: ClientId, ) -> Result<()> { let mut mode_info = self .mode_info .get(&client_id) .cloned() .unwrap_or_else(|| self.default_mode_info.clone()); let previous_mode = mode_info.mode; mode_info.mode = new_mode; mode_info.base_mode = base_mode; if mode_info.session_name.as_ref() != Some(&self.session_name) { mode_info.session_name = Some(self.session_name.clone()); } let err_context = || { format!( "failed to change from mode '{:?}' to mode '{:?}' for client {client_id}", previous_mode, new_mode ) }; // If we leave the Search-related modes, we need to clear all previous searches let search_related_modes = [InputMode::EnterSearch, InputMode::Search, InputMode::Scroll]; if search_related_modes.contains(&previous_mode) && !search_related_modes.contains(&mode_info.mode) { active_tab!(self, client_id, |tab: &mut Tab| tab.clear_search(client_id)); } if previous_mode == InputMode::Scroll && (mode_info.mode == InputMode::Normal || mode_info.mode == InputMode::Locked) { if let Ok(active_tab) = self.get_active_tab_mut(client_id) { active_tab .clear_active_terminal_scroll(client_id) .with_context(err_context)?; } } if mode_info.mode == InputMode::RenameTab { if let Ok(active_tab) = self.get_active_tab_mut(client_id) { active_tab.prev_name = active_tab.name.clone(); } } if mode_info.mode == InputMode::RenamePane { if let Ok(active_tab) = self.get_active_tab_mut(client_id) { if let Some(active_pane) = active_tab.get_active_pane_or_floating_pane_mut(client_id) { active_pane.store_pane_name(); } } } self.style = mode_info.style; self.mode_info.insert(client_id, mode_info.clone()); for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); tab.update_input_modes()?; } // Notify background plugins subscribed to ModeUpdate let mut bg_updates = vec![]; for ((bg_pid, bg_cid), subs) in &self.background_plugin_subscriptions { if subs.contains(&EventType::ModeUpdate) && *bg_cid == client_id { bg_updates.push(( Some(*bg_pid), Some(*bg_cid), Event::ModeUpdate(mode_info.clone()), )); } } if !bg_updates.is_empty() { self.bus .senders .send_to_plugin(PluginInstruction::Update(bg_updates)) .context("failed to update background plugins with mode info")?; } Ok(()) } pub fn change_mode_for_all_clients( &mut self, new_mode: InputMode, base_mode: Option, ) -> Result<()> { let err_context = || { format!( "failed to change input mode to {:?} for all clients", new_mode ) }; let connected_client_ids: Vec = self.active_tab_ids.keys().copied().collect(); for client_id in connected_client_ids { self.change_mode(new_mode, base_mode, client_id) .with_context(err_context)?; } Ok(()) } /// Collect plugin IDs that should receive a broadcast event for a given client. /// Returns plugin IDs from the client's active tab plus background plugins /// subscribed to the given event type. fn targeted_plugin_ids(&self, client_id: ClientId, event_type: EventType) -> Vec { let mut plugin_ids = Vec::new(); // Active-tab plugins if let Some(active_tab_id) = self.active_tab_ids.get(&client_id) { if let Some(tab) = self.tabs.get(active_tab_id) { plugin_ids.extend(tab.get_plugin_ids()); } } // Background plugins subscribed to this event type for ((bg_pid, bg_cid), subs) in &self.background_plugin_subscriptions { if subs.contains(&event_type) && *bg_cid == client_id { if !plugin_ids.contains(bg_pid) { plugin_ids.push(*bg_pid); } } } plugin_ids } /// Broadcast a ModeUpdate event to active-tab plugins and subscribed background plugins. pub fn broadcast_mode_update( &mut self, mode_info: ModeInfo, target_client_id: Option, ) -> Result<()> { let mut plugin_updates = vec![]; let client_ids: Vec = if let Some(cid) = target_client_id { vec![cid] } else { self.active_tab_ids.keys().copied().collect() }; for client_id in client_ids { let plugin_ids = self.targeted_plugin_ids(client_id, EventType::ModeUpdate); for plugin_id in plugin_ids { plugin_updates.push(( Some(plugin_id), Some(client_id), Event::ModeUpdate(mode_info.clone()), )); } } if !plugin_updates.is_empty() { self.bus .senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .context("failed to broadcast mode update")?; } Ok(()) } pub fn move_focus_left_or_previous_tab(&mut self, client_id: ClientId) -> Result<()> { let err_context = || { format!( "failed to move focus left or to previous tab for client {}", client_id ) }; let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; if let Some(client_id) = client_id { match self.get_active_tab_mut(client_id) { Ok(active_tab) => { active_tab .move_focus_left(client_id) .and_then(|success| { if !success { if self.tabs.len() == 1 { self.bubble_focus_to_host(client_id, Direction::Left); Ok(()) } else { self.switch_tab_prev(Some(Direction::Left), true, client_id) .context("failed to move focus to previous tab") } } else { Ok(()) } }) .with_context(err_context)?; }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), }; } self.log_and_report_session_state() .with_context(err_context)?; Ok(()) } pub fn move_focus_right_or_next_tab(&mut self, client_id: ClientId) -> Result<()> { let err_context = || { format!( "failed to move focus right or to next tab for client {}", client_id ) }; let client_id = if self.get_active_tab(client_id).is_ok() { Some(client_id) } else { self.get_first_client_id() }; if let Some(client_id) = client_id { match self.get_active_tab_mut(client_id) { Ok(active_tab) => { active_tab .move_focus_right(client_id) .and_then(|success| { if !success { if self.tabs.len() == 1 { self.bubble_focus_to_host(client_id, Direction::Right); Ok(()) } else { self.switch_tab_next(Some(Direction::Right), true, client_id) .context("failed to move focus to next tab") } } else { Ok(()) } }) .with_context(err_context)?; }, Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), }; } self.log_and_report_session_state() .with_context(err_context)?; Ok(()) } pub fn toggle_tab(&mut self, client_id: ClientId) -> Result<()> { let tab = self .get_previous_tab(client_id) .context("failed to toggle tabs")?; if let Some(t) = tab { let position = t.position; self.go_to_tab(position + 1, client_id) .context("failed to toggle tabs")?; }; self.log_and_report_session_state() .context("failed to toggle tabs")?; self.render(None) } pub fn focus_plugin_pane( &mut self, run_plugin: &RunPluginOrAlias, should_float: bool, move_to_focused_tab: bool, should_be_in_place: bool, client_id: ClientId, completion_tx: &mut Option, ) -> Result { // true => found and focused, false => not let err_context = || format!("failed to focus_plugin_pane"); let mut tab_index_and_plugin_pane_id = None; let mut plugin_pane_to_move_to_active_tab = None; let focused_tab_index = *self.active_tab_ids.get(&client_id).unwrap_or(&0); let all_tabs = self.get_tabs_mut(); for (tab_index, tab) in all_tabs.iter_mut() { if let Some(plugin_pane_id) = tab.find_plugin(&run_plugin) { tab_index_and_plugin_pane_id = Some((*tab_index, plugin_pane_id)); if move_to_focused_tab && focused_tab_index != *tab_index { plugin_pane_to_move_to_active_tab = tab.extract_pane(plugin_pane_id, true); } break; } } if let Some(plugin_pane_to_move_to_active_tab) = plugin_pane_to_move_to_active_tab.take() { let pane_id = plugin_pane_to_move_to_active_tab.pid(); let new_active_tab = self.get_active_tab_mut(client_id)?; if should_float { new_active_tab.show_floating_panes(); new_active_tab.add_floating_pane( plugin_pane_to_move_to_active_tab, pane_id, None, true, Some(client_id), )?; // TODO: also should_be_in_place } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( plugin_pane_to_move_to_active_tab, pane_id, false, Some(client_id), )?; } // Set affected pane ID for CLI client output if let Some(ref mut completion) = completion_tx { completion.set_affected_pane_id(pane_id); } return Ok(true); } match tab_index_and_plugin_pane_id { Some((tab_index, plugin_pane_id)) => { self.go_to_tab(tab_index + 1, client_id)?; self.tabs .get_mut(&tab_index) .with_context(err_context)? .focus_pane_with_id(plugin_pane_id, should_float, should_be_in_place, client_id) .context("failed to focus plugin pane")?; self.log_and_report_session_state() .with_context(err_context)?; // Set affected pane ID for CLI client output if let Some(ref mut completion) = completion_tx { completion.set_affected_pane_id(plugin_pane_id); } Ok(true) }, None => Ok(false), } } pub fn focus_pane_with_id( &mut self, pane_id: PaneId, should_float_if_hidden: bool, should_open_in_place: bool, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to focus_plugin_pane"); let tab_index = self .tabs .iter() .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id)) .map(|(_tab_index, tab)| tab.position); match tab_index { Some(tab_index) => { self.go_to_tab(tab_index + 1, client_id)?; self.tabs .iter_mut() .find(|(_, t)| t.position == tab_index) .map(|(_, t)| { t.focus_pane_with_id( pane_id, should_float_if_hidden, should_open_in_place, client_id, ) }) .with_context(err_context) .non_fatal(); }, None => { log::error!("Could not find pane with id: {:?}", pane_id); }, }; Ok(()) } pub fn rerun_command_pane_with_id( &mut self, terminal_pane_id: u32, completion_tx: Option, ) { let mut found = false; for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&PaneId::Terminal(terminal_pane_id)) { tab.rerun_terminal_pane_with_id(terminal_pane_id, completion_tx); found = true; break; } } if !found { log::error!( "Failed to find terminal pane with id: {} to run", terminal_pane_id ); } } pub fn resize_pane_with_id(&mut self, resize: ResizeStrategy, pane_id: PaneId) { let mut found = false; for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.resize_pane_with_id(resize, pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Failed to find pane with id: {:?} to resize", pane_id); } } pub fn break_pane( &mut self, default_shell: Option, default_layout: Box, client_id: ClientId, ) -> Result<()> { let err_context = || "failed break pane out of tab".to_string(); let active_tab = self.get_active_tab_mut(client_id)?; let active_pane_id = active_tab .get_active_pane_id(client_id) .with_context(err_context)?; if active_tab.get_selectable_tiled_panes_count() > 1 || active_tab.get_visible_selectable_floating_panes_count() > 0 || active_tab.pane_is_stack_list_member(&active_pane_id) { let active_pane = active_tab .extract_pane(active_pane_id, false) .with_context(err_context)?; let active_pane_run_instruction = active_pane.invoked_with().clone(); let tab_index = self.get_new_tab_id(); let swap_layouts = ( default_layout.swap_tiled_layouts.clone(), default_layout.swap_floating_layouts.clone(), ); self.new_tab(tab_index, swap_layouts, None, Some(client_id))?; let tab = self.tabs.get_mut(&tab_index).with_context(err_context)?; let (mut tiled_panes_layout, floating_panes_layout) = default_layout.new_tab(); let without_relayout = true; tab.add_tiled_pane( active_pane, active_pane_id, without_relayout, Some(client_id), )?; tiled_panes_layout.ignore_run_instruction(active_pane_run_instruction.clone()); let should_change_focus_to_new_tab = true; let is_web_client = self .connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false); self.bus.senders.send_to_plugin(PluginInstruction::NewTab( None, default_shell, Some(tiled_panes_layout), floating_panes_layout, tab_index, None, // initial_panes false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, ))?; } else { self.bus .senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( vec![active_pane_id], "Cannot break single pane out!".into(), )) .with_context(err_context)?; } Ok(()) } pub fn break_multiple_panes_to_new_tab( &mut self, pane_ids: Vec, default_shell: Option, should_change_focus_to_new_tab: bool, new_tab_name: Option, client_id: ClientId, ) -> Result { let err_context = || "failed break multiple panes to a new tab".to_string(); let all_tabs = self.get_tabs_mut(); let mut extracted_panes = vec![]; for pane_id in pane_ids { for tab in all_tabs.values_mut() { // here we pass None instead of the client_id we have because we do not need to // necessarily trigger a relayout for this tab if let Some(pane) = tab.extract_pane(pane_id, true).take() { extracted_panes.push(pane); break; } } } let (mut tiled_panes_layout, floating_panes_layout) = self.default_layout.new_tab(); let tab_index = self.get_new_tab_id(); let swap_layouts = ( self.default_layout.swap_tiled_layouts.clone(), self.default_layout.swap_floating_layouts.clone(), ); if should_change_focus_to_new_tab { self.new_tab(tab_index, swap_layouts, None, Some(client_id))?; } else { self.new_tab(tab_index, swap_layouts, None, None)?; } let tab = self.tabs.get_mut(&tab_index).with_context(err_context)?; if let Some(new_tab_name) = new_tab_name { tab.name = new_tab_name.clone(); } for mut pane in extracted_panes { let run_instruction = pane.invoked_with().clone(); let pane_id = pane.pid(); let without_relayout = true; // we reset the pane geom here to screen size so that we won't have trouble adding it // temporarily to the new tab (eg. if it was stacked or had a fixed size), the size // will be adjusted before the next render, further down the pipeline, when we apply // the layout to this new tab let new_geom = PaneGeom::from(&self.size); pane.set_geom(new_geom); // here we pass None instead of the ClientId, because we do not want this pane to be // necessarily focused tab.add_tiled_pane(pane, pane_id, without_relayout, None)?; tiled_panes_layout.ignore_run_instruction(run_instruction.clone()); } let is_web_client = self .connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false); self.bus.senders.send_to_plugin(PluginInstruction::NewTab( None, default_shell, Some(tiled_panes_layout), floating_panes_layout, tab_index, None, // initial_panes false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), None, ))?; Ok(tab_index) } pub fn break_pane_to_new_tab( &mut self, direction: Direction, client_id: ClientId, ) -> Result<()> { let err_context = || "failed break pane out of tab".to_string(); if self.tabs.len() > 1 { let (active_pane_id, active_pane, pane_to_break_is_floating) = { let active_tab = self.get_active_tab_mut(client_id)?; let active_pane_id = active_tab .get_active_pane_id(client_id) .with_context(err_context)?; let pane_to_break_is_floating = active_tab.are_floating_panes_visible(); let active_pane = active_tab .extract_pane(active_pane_id, false) .with_context(err_context)?; (active_pane_id, active_pane, pane_to_break_is_floating) }; let update_mode_infos = false; match direction { Direction::Right | Direction::Down => { self.switch_tab_next(None, update_mode_infos, client_id)?; }, Direction::Left | Direction::Up => { self.switch_tab_prev(None, update_mode_infos, client_id)?; }, }; let new_active_tab = self.get_active_tab_mut(client_id)?; if pane_to_break_is_floating { new_active_tab.show_floating_panes(); new_active_tab.add_floating_pane( active_pane, active_pane_id, None, true, Some(client_id), )?; } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( active_pane, active_pane_id, false, Some(client_id), )?; } self.log_and_report_session_state()?; } else { let active_pane_id = { let active_tab = self.get_active_tab_mut(client_id)?; active_tab .get_active_pane_id(client_id) .with_context(err_context)? }; self.bus .senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( vec![active_pane_id], "No other tabs to add pane to!".into(), )) .with_context(err_context)?; } self.render(None)?; Ok(()) } pub fn break_multiple_panes_to_tab_with_index( &mut self, pane_ids: Vec, tab_index: usize, should_change_focus_to_new_tab: bool, client_id: ClientId, ) -> Result<()> { let all_tabs = self.get_tabs_mut(); let has_tab_with_index = all_tabs .values() .find(|t| t.position == tab_index) .is_some(); if !has_tab_with_index { log::error!("Cannot find tab with index: {tab_index}"); return Ok(()); } let mut extracted_panes = vec![]; for pane_id in pane_ids { for tab in all_tabs.values_mut() { if tab.position == tab_index { continue; } // here we pass None instead of the client_id we have because we do not need to // necessarily trigger a relayout for this tab let pane_was_floating = tab.pane_id_is_floating(&pane_id); if let Some(pane) = tab.extract_pane(pane_id, true).take() { extracted_panes.push((pane_was_floating, pane)); break; } } } if should_change_focus_to_new_tab { self.go_to_tab(tab_index + 1, client_id)?; } if extracted_panes.is_empty() { // nothing to do here... return Ok(()); } let screen_size = self.size; if let Some(new_active_tab) = self.get_indexed_tab_mut(tab_index) { for (pane_was_floating, mut pane) in extracted_panes { let pane_id = pane.pid(); if pane_was_floating { let floating_pane_coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(pane.x())), y: Some(PercentOrFixed::Fixed(pane.y())), width: Some(PercentOrFixed::Fixed(pane.cols())), height: Some(PercentOrFixed::Fixed(pane.rows())), pinned: Some(pane.current_geom().is_pinned), borderless: Some(pane.borderless()), }; new_active_tab.add_floating_pane( pane, pane_id, Some(floating_pane_coordinates), false, Some(client_id), )?; } else { // here we pass None instead of the ClientId, because we do not want this pane to be // necessarily focused // we reset the pane geom here to screen size so that we won't have trouble adding it // temporarily to the new tab (eg. if it was stacked or had a fixed size), the size // will be adjusted before the next render, further down the pipeline, when we apply // the layout to this new tab let new_geom = PaneGeom::from(&screen_size); pane.set_geom(new_geom); new_active_tab.add_tiled_pane(pane, pane_id, false, None)?; } } } else { log::error!("Could not find tab with index: {:?}", tab_index); } self.log_and_report_session_state()?; Ok(()) } pub fn replace_pane( &mut self, new_pane_id: PaneId, hold_for_command: HoldForCommand, run: Option, pane_title: Option, close_replaced_pane: bool, client_id_tab_index_or_pane_id: ClientTabIndexOrPaneId, ) -> Result<()> { let suppress_pane = |tab: &mut Tab, pane_id: PaneId, new_pane_id: PaneId| { let _ = tab.suppress_pane_and_replace_with_pid( pane_id, new_pane_id, close_replaced_pane, run, None, None, ); if let Some(pane_title) = pane_title { let _ = tab.rename_pane(pane_title.as_bytes().to_vec(), new_pane_id); } if let Some(hold_for_command) = hold_for_command { let is_first_run = true; tab.hold_pane(new_pane_id, None, is_first_run, hold_for_command) } }; match client_id_tab_index_or_pane_id { ClientTabIndexOrPaneId::ClientId(client_id) | ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) => { active_tab!(self, client_id, |tab: &mut Tab| { match tab.get_active_pane_id(client_id) { Some(pane_id) => { suppress_pane(tab, pane_id, new_pane_id); }, None => { log::error!( "Failed to find active pane for client id: {:?}", client_id ); }, } }); }, ClientTabIndexOrPaneId::PaneId(pane_id) => { let tab_index = self .tabs .iter() .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id)) .map(|(_tab_index, tab)| tab.position); match tab_index { Some(tab_index) => { if let Some(tab) = self.tabs.iter_mut().find(|(_, t)| t.position == tab_index) { suppress_pane(tab.1, pane_id, new_pane_id); } }, None => { log::error!("Could not find pane with id: {:?}", pane_id); }, }; }, ClientTabIndexOrPaneId::TabIndex(_tab_index) | ClientTabIndexOrPaneId::TabIndexNoFocus(_tab_index) => { log::error!("Cannot replace pane with tab index"); }, } Ok(()) } pub fn replace_pane_with_existing_pane( &mut self, pane_id_to_replace: PaneId, pane_id_of_existing_pane: PaneId, suppress_replaced_pane: bool, _completion_tx: Option, // ends here ) { let Some(tab_index_of_pane_id_to_replace) = self .tabs .iter() .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_to_replace)) .map(|(_tab_index, tab)| tab.position) else { log::error!( "Could not find tab with pane_id: {:?} to replace", pane_id_to_replace ); return; }; let Some(tab_index_of_existing_pane) = self .tabs .iter() .find(|(_tab_index, tab)| tab.has_pane_with_pid(&pane_id_of_existing_pane)) .map(|(_tab_index, tab)| tab.position) else { log::error!( "Could not find tab with pane_id: {:?} to be replaced by", pane_id_of_existing_pane ); return; }; let Some(extracted_pane_from_other_tab) = self .tabs .iter_mut() .find(|(_, t)| t.position == tab_index_of_existing_pane) .and_then(|(_, t)| t.extract_pane(pane_id_of_existing_pane, true)) else { log::error!("Failed to find pane"); return; }; if let Some(tab) = self .tabs .iter_mut() .find(|(_, t)| t.position == tab_index_of_pane_id_to_replace) { if suppress_replaced_pane { tab.1.suppress_pane_and_replace_with_other_pane( pane_id_to_replace, extracted_pane_from_other_tab, None, ); } else { tab.1.close_pane_and_replace_with_other_pane( pane_id_to_replace, extracted_pane_from_other_tab, None, ); } } let _ = self.log_and_report_session_state(); } pub fn reconfigure( &mut self, new_keybinds: Keybinds, new_default_mode: InputMode, theme: Styling, simplified_ui: bool, default_shell: Option, pane_frame_style: PaneFrameStyle, copy_command: Option, copy_to_clipboard: Option, copy_on_select: bool, auto_layout: bool, rounded_corners: bool, hide_session_name: bool, pane_title_alignment: PaneTitleAlignment, pane_gap_x: usize, pane_gap_y: usize, pane_padding: PanePadding, stacked_resize: bool, stacked_pane_list: bool, default_editor: Option, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, visual_bell: bool, focus_follows_mouse: bool, mouse_click_through: bool, nested_session_handling: NestedSessionHandling, client_id: ClientId, ) -> Result<()> { let should_support_arrow_fonts = !simplified_ui; self.arrow_fonts = should_support_arrow_fonts; // global configuration self.default_mode_info.update_theme(theme); self.default_mode_info .update_rounded_corners(rounded_corners); // `default_mode_info` is the fallback used by `change_mode` for // clients that don't yet have a per-client `mode_info` entry, so its // keybinds and base mode must be kept in sync with reconfigures. self.default_mode_info.update_keybinds(new_keybinds.clone()); self.default_mode_info.update_default_mode(new_default_mode); self.default_shell = default_shell.clone().unwrap_or_else(|| get_default_shell()); self.default_editor = default_editor.clone().or_else(|| get_default_editor()); self.auto_layout = auto_layout; self.copy_options.command = copy_command.clone(); self.copy_options.copy_on_select = copy_on_select; self.pane_frame_style = pane_frame_style; self.advanced_mouse_actions = advanced_mouse_actions; self.mouse_scroll_resize = mouse_scroll_resize; self.mouse_hover_effects = mouse_hover_effects; self.visual_bell = visual_bell; self.focus_follows_mouse = focus_follows_mouse; self.mouse_click_through = mouse_click_through; self.nested_session_handling = nested_session_handling; self.default_mode_info .update_arrow_fonts(should_support_arrow_fonts); self.default_mode_info .update_hide_session_name(hide_session_name); { *self.stacked_resize.borrow_mut() = stacked_resize; } { *self.stacked_pane_list.borrow_mut() = stacked_pane_list; } if let Some(copy_to_clipboard) = copy_to_clipboard { self.copy_options.clipboard = copy_to_clipboard; } for tab in self.tabs.values_mut() { tab.update_theme(theme); tab.update_rounded_corners(rounded_corners); tab.update_default_shell(default_shell.clone()); tab.update_default_editor(self.default_editor.clone()); tab.update_auto_layout(auto_layout); tab.update_copy_options(&self.copy_options); // must precede set_pane_frames: gap/padding are baked into // content_offset by the sweep that set_pane_frames triggers tab.update_frame_layout(pane_title_alignment, pane_gap_x, pane_gap_y, pane_padding); tab.set_pane_frames(pane_frame_style); tab.update_arrow_fonts(should_support_arrow_fonts); tab.update_advanced_mouse_actions(advanced_mouse_actions); tab.update_mouse_scroll_resize(mouse_scroll_resize); tab.update_mouse_hover_effects(mouse_hover_effects); tab.update_focus_follows_mouse(focus_follows_mouse); tab.update_mouse_click_through(mouse_click_through); tab.sync_stacked_pane_list_mode(); } // Clear hover state when disabled if !mouse_hover_effects { for tab in self.tabs.values_mut() { tab.clear_mouse_hover_state(); } } // client specific configuration if self.connected_clients_contains(&client_id) { let mode_info = self .mode_info .entry(client_id) .or_insert_with(|| self.default_mode_info.clone()); mode_info.update_keybinds(new_keybinds); mode_info.update_default_mode(new_default_mode); mode_info.update_theme(theme); mode_info.update_arrow_fonts(should_support_arrow_fonts); mode_info.update_hide_session_name(hide_session_name); for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); } } // this needs to be done separately at the end because it applies some of the above changes // and propagates them to plugins for tab in self.tabs.values_mut() { tab.update_input_modes()?; } self.broadcast_nested_shortcuts(); Ok(()) } /// Apply a host-reported color-palette theme mode (CSI 2031 / DSR 997). /// /// This is the Phase 2 entry-point: it /// 1. de-duplicates against the last-known mode, /// 2. swaps the active palette to the configured `theme_dark` / `theme_light` /// (when both are configured) by reusing the reconfigure propagation, /// 3. fans out an `Event::HostTerminalThemeChanged` plugin event, /// 4. forwards a `CSI ?997;{1|2}n` DSR onto the pty of every terminal pane /// whose app opted in via `CSI ? 2031 h`. pub fn update_host_terminal_theme_mode(&mut self, mode: HostTerminalThemeMode) -> Result<()> { let err_context = || "Failed to update host terminal theme mode".to_string(); // dedupe if self.host_terminal_theme_mode == Some(mode) { return Ok(()); } self.host_terminal_theme_mode = Some(mode); // resolve target styling let resolved = match mode { HostTerminalThemeMode::Dark => self.host_theme_dark_styling, HostTerminalThemeMode::Light => self.host_theme_light_styling, }; // theme propagation when both keys configured and the resolved // styling exists. (If only one of theme_dark/theme_light is set, // skip auto-switch; the static `theme` stays authoritative.) let auto_switch_enabled = self.host_theme_dark_styling.is_some() && self.host_theme_light_styling.is_some(); if auto_switch_enabled { if let Some(theme) = resolved { self.default_mode_info.update_theme(theme); for tab in self.tabs.values_mut() { tab.update_theme(theme); } // Iterate every connected client (active_tab_ids is the // canonical "who is connected" map). Iterating // self.mode_info.keys() would skip any client that has // never manually changed mode let client_ids: Vec = self.active_tab_ids.keys().copied().collect(); let default_for_new = self.default_mode_info.clone(); for client_id in client_ids { let mode_info = self .mode_info .entry(client_id) .or_insert_with(|| default_for_new.clone()); mode_info.update_theme(theme); let mode_info_clone = mode_info.clone(); // Push the freshly-themed mode_info into every tab's // per-client mode_info map. `update_input_modes` below // reads from THAT map (not Screen's) when fanning out // ModeUpdate to plugins, so without this step plugins // receive ModeUpdate carrying the *old* style and the // status-bar / tab-bar do not repaint. Also mark the // active pane for rerender so terminal panes refresh // their borders/title using the new palette. for tab in self.tabs.values_mut() { tab.change_mode_info(mode_info_clone.clone(), client_id); tab.mark_active_pane_for_rerender(client_id); } } for tab in self.tabs.values_mut() { tab.update_input_modes().with_context(err_context)?; } } else { log::warn!( "host theme auto-switch enabled but resolved styling missing for {:?}", mode ); } } // fan out the plugin event self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::HostTerminalThemeChanged(mode), )])) .with_context(err_context)?; // forward DSR to opted-in terminal panes let mut pty_writes: Vec<(Vec, u32)> = vec![]; for tab in self.tabs.values_mut() { for pane_id in tab.get_all_pane_ids() { if let PaneId::Terminal(terminal_id) = pane_id { if let Some(pane) = tab.get_pane_with_id_mut(pane_id) { pane.push_color_palette_dsr(mode); for bytes in pane.drain_messages_to_pty() { pty_writes.push((bytes, terminal_id)); } } } } } for (bytes, terminal_id) in pty_writes { let _ = self .bus .senders .send_to_pty_writer(PtyWriteInstruction::Write(bytes, terminal_id, None)); } self.render(None)?; Ok(()) } /// Apply a manual host-terminal theme mode change requested via the CLI or a /// keybinding. Surfaces a clear error to the CLI if the auto-switch gate /// (both `theme_dark` and `theme_light` configured) is not satisfied; /// otherwise delegates to `update_host_terminal_theme_mode`, which dedupes, /// repaints, fans out the plugin event, and forwards DSR to opted-in panes. pub fn apply_manual_host_terminal_theme_mode( &mut self, mode: HostTerminalThemeMode, completion_tx: &mut Option, ) -> Result<()> { let auto_switch_enabled = self.host_theme_dark_styling.is_some() && self.host_theme_light_styling.is_some(); if !auto_switch_enabled { if let Some(c) = completion_tx.as_mut() { c.set_exit_status(1); c.set_error_message( "Manual theme switching requires both `theme_dark` and `theme_light` to be configured." .to_string(), ); } return Ok(()); } self.update_host_terminal_theme_mode(mode) } pub fn toggle_pane_pinned(&mut self, client_id: ClientId) { active_tab_and_connected_client_id!( self, client_id, |tab: &mut Tab, client_id: ClientId| { tab.toggle_pane_pinned(client_id); } ); } pub fn set_floating_pane_pinned(&mut self, pane_id: PaneId, should_be_pinned: bool) { let mut found = false; for tab in self.tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_floating_pane_pinned(pane_id, should_be_pinned); found = true; break; } } if !found { log::error!( "Failed to find pane with id: {:?} to set as pinned", pane_id ); } } pub fn stack_panes(&mut self, mut pane_ids_to_stack: Vec) -> Option { // if successful, returns the pane id of the last pane in the stack if pane_ids_to_stack.is_empty() { log::error!("Got an empty list of pane_ids to stack"); return None; } let stack_size = pane_ids_to_stack.len(); let root_pane_id = pane_ids_to_stack.remove(0); let last_pane_id = pane_ids_to_stack.last(); let Some(root_tab_id) = self .tabs .iter() .find_map(|(tab_id, tab)| { if tab.has_pane_with_pid(&root_pane_id) { Some(tab_id) } else { None } }) .copied() else { log::error!("Failed to find tab for root_pane_id: {:?}", root_pane_id); return None; }; let root_pane_id_is_floating = self .tabs .get(&root_tab_id) .map(|t| t.pane_id_is_floating(&root_pane_id)) .unwrap_or(false); if root_pane_id_is_floating { self.tabs.get_mut(&root_tab_id).map(|tab| { let _ = tab.toggle_pane_embed_or_floating_for_pane_id(root_pane_id, None); }); } let mut panes_to_stack = vec![]; let target_tab_has_room_for_stack = self .tabs .get_mut(&root_tab_id) .map(|t| t.has_room_for_stack(root_pane_id, stack_size)) .unwrap_or(false); if !target_tab_has_room_for_stack { log::error!("No room for stack with root pane id: {:?}", root_pane_id); return None; } for (tab_id, tab) in self.tabs.iter_mut() { if tab_id == &root_tab_id { // we do this before we extract panes so that the extraction won't trigger a // relayout according to the next swapped tiled pane tab.set_tiled_panes_damaged(); } for pane_id in &pane_ids_to_stack { if tab.has_pane_with_pid(&pane_id) { match tab.extract_pane(*pane_id, false) { Some(pane) => { panes_to_stack.push(pane); }, None => { log::error!("Failed to extract pane: {:?}", pane_id); }, } } } } self.tabs .get_mut(&root_tab_id) .map(|t| t.stack_panes(root_pane_id, panes_to_stack)); return last_pane_id.copied(); } pub fn change_floating_panes_coordinates( &mut self, pane_ids_and_coordinates: Vec<(PaneId, FloatingPaneCoordinates)>, ) { for (pane_id, coordinates) in pane_ids_and_coordinates { for (_tab_id, tab) in self.tabs.iter_mut() { if tab.has_pane_with_pid(&pane_id) { tab.change_floating_pane_coordinates(&pane_id, coordinates) .non_fatal(); break; } } } } pub fn toggle_pane_borderless(&mut self, pane_id: PaneId) { for (_tab_id, tab) in self.tabs.iter_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_pane_borderless(&pane_id).non_fatal(); break; } } } pub fn set_pane_borderless(&mut self, pane_id: PaneId, borderless: bool) { for (_tab_id, tab) in self.tabs.iter_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_pane_borderless(&pane_id, borderless).non_fatal(); break; } } } pub fn handle_mouse_event(&mut self, event: MouseEvent, client_id: ClientId) { let is_bare_motion = event.event_type == MouseEventType::Motion && !event.left && !event.right && !event.middle && !event.wheel_up && !event.wheel_down; let active_pane_id_before = self .get_active_tab(client_id) .ok() .and_then(|tab| tab.get_active_pane_id(client_id)); match self .get_active_tab_mut(client_id) .and_then(|tab| tab.handle_mouse_event(&event, client_id)) { Ok(mouse_effect) => { let mut should_render = false; if let Some(pane_id) = mouse_effect.group_toggle { if self.advanced_mouse_actions { self.toggle_pane_id_in_group(pane_id, &client_id); should_render = true; } } if let Some(pane_id) = mouse_effect.group_add { if self.advanced_mouse_actions { self.add_pane_id_to_group(pane_id, &client_id); should_render = true; } } if mouse_effect.ungroup { if self.advanced_mouse_actions { self.clear_pane_group(&client_id); should_render = true; } } if mouse_effect.state_changed { if !is_bare_motion { let _ = self.log_and_report_session_state(); } let active_pane_id_after = self .get_active_tab(client_id) .ok() .and_then(|tab| tab.get_active_pane_id(client_id)); if active_pane_id_before.is_some() && active_pane_id_before != active_pane_id_after { self.clear_bell_for_focused_pane(client_id); if let (Some(old), Some(new)) = (active_pane_id_before, active_pane_id_after) { self.report_key_passthrough_state(client_id, old, new); } } should_render = true; } if !mouse_effect.leave_clipboard_message && !is_bare_motion { let target_plugin_ids = self.targeted_plugin_ids(client_id, EventType::InputReceived); let plugin_updates: Vec<_> = target_plugin_ids .into_iter() .map(|pid| (Some(pid), Some(client_id), Event::InputReceived)) .collect(); if !plugin_updates.is_empty() { let _ = self .bus .senders .send_to_plugin(PluginInstruction::Update(plugin_updates)); } should_render = true; } if should_render { self.render(None).non_fatal(); } }, Err(e) => { log::error!("Failed to process MouseEvent: {}", e); }, } } pub fn toggle_pane_in_group(&mut self, client_id: ClientId) -> Result<()> { let err_context = "Can't add pane to group"; let active_tab = self .get_active_tab(client_id) .with_context(|| err_context)?; let active_pane_id = active_tab .get_active_pane_id(client_id) .with_context(|| err_context)?; self.toggle_pane_id_in_group(active_pane_id, &client_id); let _ = self.log_and_report_session_state(); Ok(()) } pub fn toggle_group_marking(&mut self, client_id: ClientId) -> Result<()> { let (was_marking_before, marking_pane_group_now) = { let mut currently_marking_pane_group = self.currently_marking_pane_group.borrow_mut(); let previous_value = currently_marking_pane_group .remove(&client_id) .unwrap_or(false); let new_value = !previous_value; if new_value { currently_marking_pane_group.insert(client_id, true); } (previous_value, new_value) }; if marking_pane_group_now { let active_pane_id = self.get_active_pane_id(&client_id); if let Some(active_pane_id) = active_pane_id { self.add_pane_id_to_group(active_pane_id, &client_id); } } let value_changed = was_marking_before != marking_pane_group_now; if value_changed { for tab in self.tabs.values_mut() { tab.update_input_modes()?; } let _ = self.log_and_report_session_state(); } Ok(()) } fn get_layout_metadata( &self, default_shell: Option, tab_index: Option, ) -> SessionLayoutMetadata { let mut session_layout_metadata = SessionLayoutMetadata::new(self.default_layout.clone()); if let Some(default_shell) = default_shell { session_layout_metadata.update_default_shell(default_shell); } let first_client_id = self.get_first_client_id(); let active_tab_index = first_client_id.and_then(|client_id| self.active_tab_ids.get(&client_id)); // Filter tabs based on optional tab_index parameter let tabs_to_process: Vec<_> = self .tabs .iter() .filter(|(idx, _)| tab_index.map_or(true, |target| **idx == target)) .collect(); for (tab_index, tab) in tabs_to_process { let tab_is_focused = active_tab_index == Some(&tab_index); let hide_floating_panes = !tab.are_floating_panes_visible(); let mut suppressed_panes = HashMap::new(); for (triggering_pane_id, p) in tab.get_suppressed_panes() { suppressed_panes.insert(*triggering_pane_id, p); } let all_connected_clients: Vec = self .connected_clients .borrow() .iter() .map(|(c, _i)| *c) .filter(|c| self.active_tab_ids.get(&c) == Some(&tab_index)) .collect(); let mut active_pane_ids: HashMap> = HashMap::new(); for connected_client_id in &all_connected_clients { active_pane_ids.insert( *connected_client_id, tab.get_active_pane_id(*connected_client_id), ); } let stack_list_geoms = tab.stack_list_serialization_geoms(); let tiled_panes: Vec = tab .get_tiled_panes() .map(|(pane_id, p)| { // here we look to see if this pane triggers any suppressed pane, // and if so we take that suppressed pane - we do this because this // is currently only the case the scrollback editing panes, and // when dumping the layout we want the "real" pane and not the // editor pane let geom_override = stack_list_geoms.get(pane_id).copied(); let (pane_id, p) = match suppressed_panes.remove(pane_id) { Some((is_scrollback_editor, suppressed_pane)) if *is_scrollback_editor => { (suppressed_pane.pid(), suppressed_pane) }, _ => (*pane_id, p), }; ( pane_id, p, geom_override.unwrap_or_else(|| p.position_and_size()), ) }) .chain(tab.hidden_stack_list_members_for_serialization()) .map(|(pane_id, p, geom)| { let focused_clients: Vec = active_pane_ids .iter() .filter_map(|(c_id, p_id)| { p_id.and_then(|p_id| if p_id == pane_id { Some(*c_id) } else { None }) }) .collect(); let (default_fg, default_bg) = p.get_pane_default_colors(); PaneLayoutMetadata::new( pane_id, geom, p.borderless(), p.invoked_with().clone(), p.custom_title(), !focused_clients.is_empty(), if self.serialize_pane_viewport { p.serialize(self.scrollback_lines_to_serialize) } else { None }, focused_clients, default_fg, default_bg, ) }) .collect(); let floating_panes: Vec = tab .get_floating_panes() .map(|(pane_id, p)| { // here we look to see if this pane triggers any suppressed pane, // and if so we take that suppressed pane - we do this because this // is currently only the case the scrollback editing panes, and // when dumping the layout we want the "real" pane and not the // editor pane match suppressed_panes.remove(pane_id) { Some((is_scrollback_editor, suppressed_pane)) if *is_scrollback_editor => { (suppressed_pane.pid(), suppressed_pane) }, _ => (*pane_id, p), } }) .map(|(pane_id, p)| { let focused_clients: Vec = active_pane_ids .iter() .filter_map(|(c_id, p_id)| { p_id.and_then(|p_id| if p_id == pane_id { Some(*c_id) } else { None }) }) .collect(); let (default_fg, default_bg) = p.get_pane_default_colors(); PaneLayoutMetadata::new( pane_id, p.position_and_size(), false, // floating panes are never borderless p.invoked_with().clone(), p.custom_title(), !focused_clients.is_empty(), if self.serialize_pane_viewport { p.serialize(self.scrollback_lines_to_serialize) } else { None }, focused_clients, default_fg, default_bg, ) }) .collect(); session_layout_metadata.add_tab( tab.name.clone(), tab_is_focused, hide_floating_panes, tiled_panes, floating_panes, ); } session_layout_metadata } fn update_plugin_loading_stage( &mut self, pid: u32, loading_indication: LoadingIndication, ) -> bool { let all_tabs = self.get_tabs_mut(); let mut found_plugin = false; for tab in all_tabs.values_mut() { if tab.has_plugin(pid) { found_plugin = true; tab.update_plugin_loading_stage(pid, loading_indication); break; } } found_plugin } fn connected_clients_contains(&self, client_id: &ClientId) -> bool { self.connected_clients.borrow().contains_key(client_id) } fn get_client_pane_group(&self, client_id: &ClientId) -> HashSet { self.current_pane_group .borrow() .get_client_pane_group(client_id) } fn clear_pane_group(&mut self, client_id: &ClientId) { self.current_pane_group .borrow_mut() .clear_pane_group(client_id); self.currently_marking_pane_group .borrow_mut() .remove(client_id); } fn toggle_pane_id_in_group(&mut self, pane_id: PaneId, client_id: &ClientId) { { let mut pane_groups = self.current_pane_group.borrow_mut(); pane_groups.toggle_pane_id_in_group(pane_id, self.size, client_id); } self.retain_only_existing_panes_in_pane_groups(); } fn add_pane_id_to_group(&mut self, pane_id: PaneId, client_id: &ClientId) { { let mut pane_groups = self.current_pane_group.borrow_mut(); pane_groups.add_pane_id_to_group(pane_id, self.size, client_id); } self.retain_only_existing_panes_in_pane_groups(); } fn add_active_pane_to_group_if_marking(&mut self, client_id: &ClientId) { { if self .currently_marking_pane_group .borrow() .get(client_id) .copied() .unwrap_or(false) { let active_pane_id = self.get_active_pane_id(&client_id); if let Some(active_pane_id) = active_pane_id { self.add_pane_id_to_group(active_pane_id, &client_id); } } } self.retain_only_existing_panes_in_pane_groups(); } fn get_active_pane_id(&self, client_id: &ClientId) -> Option { let active_tab = self.get_active_tab(*client_id).ok()?; active_tab.get_active_pane_id(*client_id) } fn get_pane_info(&self, pane_id: PaneId) -> Option { // Search through all tabs to find the pane for tab in self.tabs.values() { if let Some(pane_info) = tab.get_pane_info(pane_id) { return Some(pane_info); } } None } fn get_tab_info(&self, tab_id: usize) -> Option { // Look up tab by its stable ID self.tabs.get(&tab_id).map(|tab| { let mut all_focused_clients: Vec = self .active_tab_ids .iter() .filter(|(_c_id, active_tab_id)| **active_tab_id == tab.id) .map(|(c_id, _)| c_id) .copied() .collect(); for s_id in tab.shadow_focus_clients() { if !all_focused_clients.contains(&s_id) { all_focused_clients.push(s_id); } } let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let tab_viewport = tab.get_viewport(); let tab_display_area = tab.get_display_area(); let selectable_tiled_panes_count = tab.get_selectable_tiled_panes_count(); let selectable_floating_panes_count = tab.get_selectable_floating_panes_count(); TabInfo { position: tab.position, name: tab.name.clone(), active: self.active_tab_ids.values().any(|i| i == &tab.id), panes_to_hide: tab.panes_to_hide_count(), is_fullscreen_active: tab.is_fullscreen_active(), is_sync_panes_active: tab.is_sync_panes_active(), are_floating_panes_visible: tab.are_floating_panes_visible(), other_focused_clients: all_focused_clients, active_swap_layout_name, is_swap_layout_dirty, viewport_rows: tab_viewport.rows, viewport_columns: tab_viewport.cols, display_area_rows: tab_display_area.rows, display_area_columns: tab_display_area.cols, selectable_tiled_panes_count, selectable_floating_panes_count, tab_id: tab.id, has_bell_notification: tab.tab_has_pending_bell && !self.active_tab_ids.values().any(|i| i == &tab.id), is_flashing_bell: tab.tab_bell_flash && !self.active_tab_ids.values().any(|i| i == &tab.id), } }) } fn group_and_ungroup_panes( &mut self, pane_ids_to_group: Vec, pane_ids_to_ungroup: Vec, for_all_clients: bool, client_id: ClientId, ) { if for_all_clients { { let mut current_pane_group = self.current_pane_group.borrow_mut(); current_pane_group.group_and_ungroup_panes_for_all_clients( pane_ids_to_group, pane_ids_to_ungroup, self.size, ); } } else { { let mut current_pane_group = self.current_pane_group.borrow_mut(); current_pane_group.group_and_ungroup_panes( pane_ids_to_group, pane_ids_to_ungroup, self.size, &client_id, ); } } self.retain_only_existing_panes_in_pane_groups(); let _ = self.log_and_report_session_state(); } fn retain_only_existing_panes_in_pane_groups(&mut self) { let clients_with_empty_group = { let mut clients_with_empty_group = vec![]; let mut current_pane_group = { self.current_pane_group.borrow().clone_inner() }; for (client_id, panes_in_group) in current_pane_group.iter_mut() { let all_tabs = self.get_tabs(); panes_in_group.retain(|p_id| { let mut found = false; for tab in all_tabs.values() { if tab.has_pane_with_pid(&p_id) { found = true; break; } } found }); if panes_in_group.is_empty() { clients_with_empty_group.push(*client_id) } } self.current_pane_group .borrow_mut() .override_groups_with(current_pane_group); clients_with_empty_group }; for client_id in &clients_with_empty_group { self.currently_marking_pane_group .borrow_mut() .remove(client_id); } if !clients_with_empty_group.is_empty() { let all_tabs = self.get_tabs_mut(); for tab in all_tabs.values_mut() { let _ = tab.update_input_modes(); } } } fn update_active_pane_ids(&mut self) { let connected_clients: Vec = self.connected_clients.borrow().keys().copied().collect(); for client_id in connected_clients { if let Some(active_pane_id) = self.get_active_pane_id(&client_id) { let active_pane_id: PaneId = active_pane_id.into(); let history = self.pane_history.entry(client_id).or_insert_with(|| vec![]); history.retain(|e| e != &active_pane_id); history.push(active_pane_id.into()); } } } fn subscribe_to_pane_renders( &mut self, subscriber_client_id: ClientId, pane_ids: Vec, scrollback: Option, ansi: bool, ) { let mut previous_viewports = HashMap::new(); let mut valid_pane_ids = HashSet::new(); // Get a regular client ID for plugin pane content queries let regular_client_id = self .connected_clients .borrow() .keys() .find(|id| !self.watcher_clients.contains_key(id)) .copied(); for pane_id in &pane_ids { let server_pane_id: PaneId = (*pane_id).into(); let mut found = false; for tab in self.tabs.values() { if let Some(pane) = tab.get_pane_with_id(server_pane_id) { let get_full_scrollback = scrollback.is_some(); let max_lines = scrollback.and_then(|n| if n == 0 { None } else { Some(n) }); // For plugin panes, use a regular client_id; for terminal panes, None is fine let query_client_id = match server_pane_id { PaneId::Plugin(_) => regular_client_id, PaneId::Terminal(_) => None, }; let contents = if ansi { pane.pane_contents_with_ansi( query_client_id, get_full_scrollback, max_lines, ) } else { pane.pane_contents(query_client_id, get_full_scrollback, max_lines) }; if let Some(os_input) = &self.bus.os_input { let scrollback_data = if scrollback.is_some() { Some(contents.lines_above_viewport.clone()) } else { None }; let _ = os_input.send_to_client( subscriber_client_id, ServerToClientMsg::PaneRenderUpdate { pane_id: *pane_id, viewport: contents.viewport.clone(), scrollback: scrollback_data, is_initial: true, }, ); } previous_viewports.insert(*pane_id, contents.viewport); valid_pane_ids.insert(*pane_id); found = true; break; } } if !found { if let Some(os_input) = &self.bus.os_input { let _ = os_input.send_to_client( subscriber_client_id, ServerToClientMsg::LogError { lines: vec![format!("Pane {} not found", pane_id)], }, ); } } } if !valid_pane_ids.is_empty() { self.pane_render_subscribers.insert( subscriber_client_id, PaneRenderSubscription { pane_ids: valid_pane_ids, previous_viewports, ansi, }, ); } } fn deliver_to_pane_subscribers_from_report(&mut self, report: &PaneRenderReport) { let Some(pane_map) = report.all_pane_contents.values().next() else { return; }; let ansi_pane_map = report.all_pane_contents_with_ansi.values().next(); self.deliver_subscriber_updates_from_map(pane_map, ansi_pane_map); } fn deliver_to_pane_subscribers_directly(&mut self) { // Collect unique pane IDs across all subscribers let all_subscribed_ids: HashSet = self .pane_render_subscribers .values() .flat_map(|sub| sub.pane_ids.iter().copied()) .collect(); let has_ansi_subscribers = self.pane_render_subscribers.values().any(|s| s.ansi); // Query pane contents directly from tabs let mut pane_map: HashMap = HashMap::new(); let mut ansi_pane_map: HashMap = HashMap::new(); for pane_id in &all_subscribed_ids { let server_pane_id: PaneId = (*pane_id).into(); for tab in self.tabs.values() { if let Some(pane) = tab.get_pane_with_id(server_pane_id) { pane_map.insert(*pane_id, pane.pane_contents(None, false, None)); if has_ansi_subscribers { ansi_pane_map .insert(*pane_id, pane.pane_contents_with_ansi(None, false, None)); } break; } } } let ansi_map_ref = if has_ansi_subscribers { Some(&ansi_pane_map) } else { None }; self.deliver_subscriber_updates_from_map(&pane_map, ansi_map_ref); } fn deliver_subscriber_updates_from_map( &mut self, pane_map: &HashMap, ansi_pane_map: Option<&HashMap>, ) { // Collect updates to send, avoiding borrow conflicts let mut updates_to_send: Vec<(ClientId, ServerToClientMsg)> = Vec::new(); let mut dead_subscribers: Vec = Vec::new(); for (subscriber_id, subscription) in &self.pane_render_subscribers { let effective_map = if subscription.ansi { ansi_pane_map.unwrap_or(pane_map) } else { pane_map }; for pane_id in &subscription.pane_ids { if let Some(contents) = effective_map.get(pane_id) { let changed = subscription .previous_viewports .get(pane_id) .map(|prev| prev != &contents.viewport) .unwrap_or(true); if changed { updates_to_send.push(( *subscriber_id, ServerToClientMsg::PaneRenderUpdate { pane_id: *pane_id, viewport: contents.viewport.clone(), scrollback: None, is_initial: false, }, )); } } } } // Send updates and track dead subscribers for (subscriber_id, msg) in &updates_to_send { if let Some(os_input) = &self.bus.os_input { if os_input .send_to_client(*subscriber_id, msg.clone()) .is_err() { dead_subscribers.push(*subscriber_id); } } } // Update previous viewports for successful sends for (subscriber_id, msg) in updates_to_send { if dead_subscribers.contains(&subscriber_id) { continue; } if let ServerToClientMsg::PaneRenderUpdate { pane_id, viewport, .. } = msg { if let Some(subscription) = self.pane_render_subscribers.get_mut(&subscriber_id) { subscription.previous_viewports.insert(pane_id, viewport); } } } for id in dead_subscribers { self.pane_render_subscribers.remove(&id); } } fn notify_pane_closed_to_subscribers(&mut self, pane_id: zellij_utils::data::PaneId) { let mut dead_subscribers = Vec::new(); for (subscriber_id, subscription) in &mut self.pane_render_subscribers { if subscription.pane_ids.remove(&pane_id) { if let Some(os_input) = &self.bus.os_input { let _ = os_input.send_to_client( *subscriber_id, ServerToClientMsg::SubscribedPaneClosed { pane_id }, ); } if subscription.pane_ids.is_empty() { if let Some(os_input) = &self.bus.os_input { let _ = os_input.send_to_client( *subscriber_id, ServerToClientMsg::Exit { exit_reason: ExitReason::Normal, }, ); } dead_subscribers.push(*subscriber_id); } } } for id in dead_subscribers { self.pane_render_subscribers.remove(&id); } } } #[cfg(not(test))] fn get_default_editor() -> Option { std::env::var("EDITOR") .or_else(|_| std::env::var("VISUAL")) .map(|e| PathBuf::from(e)) .ok() } #[cfg(test)] fn get_default_editor() -> Option { None } fn find_already_running_panes( tiled_layout: &TiledPaneLayout, floating_layouts: &[FloatingPaneLayout], active_tab: &Tab, ) -> (Vec>, Vec) { let mut layout_tiled_instructions = tiled_layout.extract_run_instructions(); let running_tiled_instructions: Vec> = active_tab .get_tiled_panes() .map(|(_, pane)| pane.invoked_with().clone()) .chain( active_tab .suppressed_stack_list_members() .map(|(_, pane)| pane.invoked_with().clone()), ) .collect(); let mut tiled_to_ignore = Vec::new(); for running_instr in running_tiled_instructions { if let Some(pos) = layout_tiled_instructions .iter() .position(|layout_instr| layout_instr == &running_instr) { layout_tiled_instructions.remove(pos); tiled_to_ignore.push(running_instr); } } let mut running_floating_instructions: Vec> = active_tab .get_floating_panes() .map(|(_, pane)| pane.invoked_with().clone()) .collect(); let mut floating_indices = Vec::new(); for (idx, floating_layout) in floating_layouts.iter().enumerate() { if let Some(pos) = running_floating_instructions .iter() .position(|instr| instr == &floating_layout.run) { running_floating_instructions.remove(pos); floating_indices.push(idx); } } (tiled_to_ignore, floating_indices) } // The box is here in order to make the // NewClient enum smaller #[allow(clippy::boxed_local)] pub(crate) fn screen_thread_main( bus: Bus, max_panes: Option, client_attributes: ClientAttributes, config: Config, debug: bool, default_layout: Box, ) -> Result<()> { // Resolve `theme_dark` / `theme_light` to concrete `Styling` from the // bundled themes BEFORE `config.options` is moved out below. These // populate Screen's auto-switch state at startup; runtime updates // continue to flow through `propagate_configuration_changes`. let host_theme_dark_styling = config .options .theme_dark .as_ref() .and_then(|name| config.themes.get_theme(name).map(|t| t.palette)); let host_theme_light_styling = config .options .theme_light .as_ref() .and_then(|name| config.themes.get_theme(name).map(|t| t.palette)); if config.options.theme_dark.is_some() && host_theme_dark_styling.is_none() { log::warn!( "theme_dark='{}' not found in themes; auto-theme switch disabled for dark.", config.options.theme_dark.as_deref().unwrap_or("?") ); } if config.options.theme_light.is_some() && host_theme_light_styling.is_none() { log::warn!( "theme_light='{}' not found in themes; auto-theme switch disabled for light.", config.options.theme_light.as_deref().unwrap_or("?") ); } let config_options = config.options; let arrow_fonts = !config_options.simplified_ui.unwrap_or_default(); let pane_frame_style = PaneFrameStyle::from_options(&config_options); let auto_layout = config_options.auto_layout.unwrap_or(true); let session_serialization = config_options.session_serialization.unwrap_or(true); let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false); let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize; let session_is_mirrored = config_options.mirror_session.unwrap_or(false); let layout_dir = config_options.layout_dir; #[cfg(test)] let default_shell = config_options .default_shell .clone() .unwrap_or(PathBuf::from("/bin/sh")); #[cfg(not(test))] let default_shell = config_options .default_shell .clone() .unwrap_or_else(|| get_default_shell()); let default_editor = config_options .scrollback_editor .clone() .or_else(|| get_default_editor()); let default_layout_name = config_options .default_layout .map(|l| format!("{}", l.display())); let copy_options = CopyOptions::new( config_options.copy_command, config_options.copy_clipboard.unwrap_or_default(), config_options.copy_on_select.unwrap_or(true), ); let web_server_ip = config_options .web_server_ip .unwrap_or_else(|| IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))); let web_server_port = config_options.web_server_port.unwrap_or(8082); let styled_underlines = config_options.styled_underlines.unwrap_or(true); let osc8_hyperlinks = config_options.osc8_hyperlinks.unwrap_or(true); let explicitly_disable_kitty_keyboard_protocol = config_options .support_kitty_keyboard_protocol .map(|e| !e) // this is due to the config options wording, if // "support_kitty_keyboard_protocol" is true, // explicitly_disable_kitty_keyboard_protocol is false and vice versa .unwrap_or(false); // by default, we try to support this if the terminal supports it and // the program running inside a pane requests it let support_kitty_graphics_protocol = config_options .support_kitty_graphics_protocol .unwrap_or(true); let stacked_resize = config_options.stacked_resize.unwrap_or(true); let stacked_pane_list = config_options.stacked_pane_list.unwrap_or(true); let web_clients_allowed = config_options .web_sharing .map(|s| s.web_clients_allowed()) .unwrap_or(false); let web_sharing = config_options.web_sharing.unwrap_or_else(Default::default); let advanced_mouse_actions = config_options.advanced_mouse_actions.unwrap_or(true); let mouse_scroll_resize = config_options.mouse_scroll_resize.unwrap_or(true); let mouse_hover_effects = config_options.mouse_hover_effects.unwrap_or(true); let visual_bell = config_options.visual_bell.unwrap_or(true); let focus_follows_mouse = config_options.focus_follows_mouse.unwrap_or(false); let mouse_click_through = config_options.mouse_click_through.unwrap_or(false); let nested_session_handling = config_options.nested_session_handling.unwrap_or_default(); let thread_senders = bus.senders.clone(); let mut screen = Screen::new( bus, &client_attributes, max_panes, get_mode_info( config_options.default_mode.unwrap_or_default(), &client_attributes, PluginCapabilities { // ¯\_(ツ)_/¯ arrow_fonts: !arrow_fonts, }, &config.keybinds, config_options.default_mode, ), pane_frame_style, auto_layout, session_is_mirrored, copy_options, debug, default_layout, default_layout_name, default_shell, session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, styled_underlines, osc8_hyperlinks, arrow_fonts, layout_dir, explicitly_disable_kitty_keyboard_protocol, support_kitty_graphics_protocol, stacked_resize, stacked_pane_list, default_editor, web_clients_allowed, web_sharing, advanced_mouse_actions, mouse_scroll_resize, mouse_hover_effects, visual_bell, focus_follows_mouse, mouse_click_through, web_server_ip, web_server_port, nested_session_handling, ); screen.host_theme_dark_styling = host_theme_dark_styling; screen.host_theme_light_styling = host_theme_light_styling; let mut pending_tab_ids: HashSet = HashSet::new(); let mut pending_tab_switches: HashSet<(usize, ClientId)> = HashSet::new(); // usize is the // tab_index let mut pending_events_waiting_for_tab: Vec = vec![]; let mut pending_events_waiting_for_client: Vec = vec![]; let mut pending_events_waiting_for_pane: HashMap> = HashMap::new(); let mut plugin_loading_message_cache = HashMap::new(); let mut keybind_intercepts = HashMap::new(); loop { let (event, mut err_ctx) = screen .bus .recv() .context("failed to receive event on channel")?; err_ctx.add_call(ContextType::Screen((&event).into())); // here we start caching resizes, so that we'll send them in bulk at the end of each event // when this cache is Dropped, for more information, see the comments in PtyWriter let _resize_cache = ResizeCache::new(thread_senders.clone()); match event { ScreenInstruction::PtyBytes(pid, vte_bytes) => { let all_tabs = screen.get_tabs_mut(); let mut vte_bytes = Some(vte_bytes); for tab in all_tabs.values_mut() { if tab.has_terminal_pid(pid) { if let Some(bytes) = vte_bytes.take() { tab.handle_pty_bytes(pid, bytes) .context("failed to process pty bytes")?; } break; } } if let Some(vte_bytes) = vte_bytes { pending_events_waiting_for_pane .entry(PaneId::Terminal(pid)) .or_default() .push(ScreenInstruction::PtyBytes(pid, vte_bytes)); } let _ = screen .bus .senders .send_to_background_jobs(BackgroundJob::RenderToClients); }, ScreenInstruction::PluginBytes(mut plugin_render_assets) => { for plugin_render_asset in plugin_render_assets.iter_mut() { let plugin_id = plugin_render_asset.plugin_id; let client_id = plugin_render_asset.client_id; let vte_bytes: Vec = plugin_render_asset.bytes.drain(..).collect(); let painted_content = !vte_bytes.is_empty(); let mobile_tab_id = screen.mobile_state.mobile_tab_id(client_id); let mut painted_tab_id = None; let mut painted_size = None; let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_plugin(plugin_id) { tab.handle_plugin_bytes(plugin_id, client_id, vte_bytes) .context("failed to process plugin bytes")?; painted_tab_id = Some(tab.id); painted_size = tab.get_pane_with_id(PaneId::Plugin(plugin_id)) .map(|pane| Size { rows: pane.get_content_rows(), cols: pane.get_content_columns(), }); break; } } if painted_content && painted_tab_id == mobile_tab_id && mobile_tab_id.is_some() { if let Some(painted_size) = painted_size { screen .mobile_render_gate .record_paint_size(client_id, painted_size); } screen.try_lift_mobile_gate(client_id); } screen.render_blocker.remove_blocking_plugin(plugin_id); } screen.render(Some(plugin_render_assets))?; }, ScreenInstruction::Render => { screen.render(None)?; }, ScreenInstruction::RenderToClients => { // render_blocker.can_render() returning true means that either all pending plugins // (only those waiting for a new tab layout to be applied!) have been rendered or // that a 100ms timeout has been reached (more info in the RenderBlocker comment) if screen.render_blocker.can_render() { screen.render_to_clients()?; } else { screen.render(None)?; } }, ScreenInstruction::NewPane( pid, initial_pane_title, hold_for_command, invoked_with, new_pane_placement, start_suppressed, client_or_tab_index, mut completion_tx, set_blocking, ) => { completion_tx.as_mut().map(|c| c.set_affected_pane_id(pid)); let blocking_notification = if set_blocking { completion_tx } else { None }; match client_or_tab_index { ClientTabIndexOrPaneId::ClientId(client_id) | ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) => { let should_focus_pane = matches!(client_or_tab_index, ClientTabIndexOrPaneId::ClientId(_)); active_tab_and_connected_client_id_with_first_tab_fallback!(screen, client_id, |tab: &mut Tab, client_id: Option| { tab.new_pane(pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, new_pane_placement, client_id, blocking_notification ) }, ?); if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab_and_connected_client_id_with_first_tab_fallback!( screen, client_id, |tab: &mut Tab, _client_id: Option| tab.hold_pane( pid, None, is_first_run, hold_for_command ) ) } }, ClientTabIndexOrPaneId::TabIndex(tab_index) | ClientTabIndexOrPaneId::TabIndexNoFocus(tab_index) => { let should_focus_pane = matches!(client_or_tab_index, ClientTabIndexOrPaneId::TabIndex(_)); // Some placements (directional split, stacked without a // target pane) need a client_id to know which pane to // split relative to. Only resolve one when required. let needs_client_id = matches!( new_pane_placement, NewPanePlacement::Tiled { direction: Some(_), .. } | NewPanePlacement::Stacked { pane_id_to_stack_under: None, .. } ); let client_id = if needs_client_id { screen .active_tab_ids .iter() .find(|(_, tid)| **tid == tab_index) .map(|(cid, _)| *cid) .or_else(|| screen.active_tab_ids.keys().next().copied()) } else { None }; if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { active_tab.new_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, new_pane_placement, client_id, blocking_notification, )?; if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab.hold_pane(pid, None, is_first_run, hold_for_command); } } else { log::error!("Tab index not found: {:?}", tab_index); } }, ClientTabIndexOrPaneId::PaneId(pane_id) => { let mut found = false; let all_tabs = screen.get_tabs_mut(); let should_focus_pane = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { match new_pane_placement { NewPanePlacement::Tiled { direction: Some(direction), borderless, } => { if direction == Direction::Left || direction == Direction::Right { tab.vertical_split_of_pane_id( pid, initial_pane_title, invoked_with, pane_id, blocking_notification, borderless, )?; } else { tab.horizontal_split_of_pane_id( pid, initial_pane_title, invoked_with, pane_id, blocking_notification, borderless, )?; } }, _ => { tab.new_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, new_pane_placement, None, blocking_notification, )?; }, } if let Some(hold_for_command) = hold_for_command { let is_first_run = true; tab.hold_pane(pid, None, is_first_run, hold_for_command); } found = true; break; } } if !found { log::error!( "Failed to find tab containing pane with id: {:?}", pane_id ); } }, }; if let Some(pending_events) = pending_events_waiting_for_pane.remove(&pid) { for event in pending_events { screen.bus.senders.send_to_screen(event).non_fatal(); } } screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::OpenInPlaceEditor(pid, client_tab_index_or_pane_id) => { match client_tab_index_or_pane_id { ClientTabIndexOrPaneId::ClientId(client_id) | ClientTabIndexOrPaneId::ClientIdNoFocus(client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .replace_active_pane_with_editor_pane(pid, client_id), ?); screen.log_and_report_session_state()?; }, ClientTabIndexOrPaneId::TabIndex(_tab_index) | ClientTabIndexOrPaneId::TabIndexNoFocus(_tab_index) => { log::error!("Cannot OpenInPlaceEditor with a TabIndex"); }, ClientTabIndexOrPaneId::PaneId(pane_id_to_replace) => { let mut found = false; let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id_to_replace) { tab.replace_pane_with_editor_pane(pid, pane_id_to_replace) .non_fatal(); found = true; break; } } if !found { log::error!( "Could not find pane with id {:?} to replace", pane_id_to_replace ); } }, } if let Some(pending_events) = pending_events_waiting_for_pane.remove(&pid) { for event in pending_events { screen.bus.senders.send_to_screen(event).non_fatal(); } } screen.render(None)?; }, ScreenInstruction::TogglePaneEmbedOrFloating( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_pane_embed_or_floating(client_id), ?); screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::ToggleFloatingPanes(client_id, default_shell, completion_tx) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_floating_panes(Some(client_id), default_shell, completion_tx), ?); screen.clear_bell_for_focused_pane(client_id); screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::ShowFloatingPanes { client_id, tab_id, completion, } => { screen.show_floating_panes_in_tab(client_id, tab_id, completion)?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::HideFloatingPanes { client_id, tab_id, completion, } => { screen.hide_floating_panes_in_tab(client_id, tab_id, completion)?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::AreFloatingPanesVisible { client_id, tab_id, completion, } => { screen.are_floating_panes_visible_in_tab(client_id, tab_id, completion)?; }, ScreenInstruction::WriteCharacter( key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { if let Some(plugin_id) = keybind_intercepts.get(&client_id) { if let Some(key_with_modifier) = key_with_modifier { let _ = screen .bus .senders .send_to_plugin(PluginInstruction::Update(vec![( Some(*plugin_id), Some(client_id), Event::InterceptedKeyPress(key_with_modifier), )])); continue; } } let mut state_changed = false; let client_input_mode = screen.get_client_input_mode(client_id); match client_input_mode { Some(InputMode::RenameTab) => { if !(raw_bytes == BRACKETED_PASTE_BEGIN || raw_bytes == BRACKETED_PASTE_END) { screen.update_active_tab_name(raw_bytes, client_id)?; state_changed = true; } }, _ => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| { match client_input_mode { Some(InputMode::EnterSearch) => { if !(raw_bytes == BRACKETED_PASTE_BEGIN || raw_bytes == BRACKETED_PASTE_END) { if let Err(e) = tab.update_search_term(raw_bytes, client_id) { log::error!("{}", e); } } state_changed = true; }, Some(InputMode::RenamePane) => { if !(raw_bytes == BRACKETED_PASTE_BEGIN || raw_bytes == BRACKETED_PASTE_END) { if let Err(e) = tab.update_active_pane_name(raw_bytes, client_id) { log::error!("{}", e); } state_changed = true; } }, _ => { let write_result = match tab.is_sync_panes_active() { true => tab.write_to_terminals_on_current_tab( &key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id, ), false => tab.write_to_active_terminal( &key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id, ), }; if let Ok(true) = write_result { state_changed = true; } }, } } ); }, }; if state_changed { screen.log_and_report_session_state()?; } screen.render(None)?; }, ScreenInstruction::Resize( client_id, strategy, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.resize(client_id, strategy), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SwitchFocus(client_id, _completion_tx) => { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { screen.report_key_passthrough_state(client_id, old, new); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::FocusNextPane(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to change focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to change focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { screen.report_key_passthrough_state(client_id, old, new); } screen.render(None)?; } }, ScreenInstruction::FocusPreviousPane(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to change focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to change focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_previous_pane(client_id) ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { screen.report_key_passthrough_state(client_id, old, new); } screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::FocusLastPane(client_id, _completion_tx) => { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_last_pane(client_id) ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { screen.report_key_passthrough_state(client_id, old, new); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusLeft(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id), ? ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old == new { screen.bubble_focus_to_host(client_id, Direction::Left); } else { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Left), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); screen.move_focus_left_or_previous_tab(client_id)?; let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old != new { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Left), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::MoveFocusDown(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id), ? ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old == new { screen.bubble_focus_to_host(client_id, Direction::Down); } else { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Down), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::MoveFocusRight(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id), ? ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old == new { screen.bubble_focus_to_host(client_id, Direction::Right); } else { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Right), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::MoveFocusRightOrNextTab(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); screen.move_focus_right_or_next_tab(client_id)?; let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old != new { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Right), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::MoveFocusUp(client_id, mut _completion_tx) => { if screen.get_first_client_id().is_none() { log::error!("No connected clients to move focus for"); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message("No connected clients to move focus for".to_string()); } } else { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id), ? ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { if old == new { screen.bubble_focus_to_host(client_id, Direction::Up); } else { screen.report_key_passthrough_state_with_direction( client_id, old, new, Some(Direction::Up), ); } } screen.clear_bell_for_focused_pane(client_id); screen.add_active_pane_to_group_if_marking(&client_id); screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::ClearScreen( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.clear_active_terminal_screen( client_id, ), ? ); screen.render(None)?; }, ScreenInstruction::DumpScreen( file, client_id, full, pane_id, completion_tx, cli_client_id, ansi, ) => { match file { Some(file_path) => { // Write dump to file (existing behavior) match pane_id { Some(pane_id) => { for tab in screen.get_tabs_mut().values_mut() { if tab.has_pane_with_pid(&pane_id) { if ansi { tab.dump_with_ansi_terminal_screen( Some(file_path.clone()), pane_id, full, )?; } else { tab.dump_terminal_screen( Some(file_path.clone()), pane_id, full, )?; } break; } } }, None => { if ansi { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.dump_with_ansi_active_terminal_screen( Some(file_path.to_string()), client_id, full ), ? ); } else { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.dump_active_terminal_screen( Some(file_path.to_string()), client_id, full ), ? ); } }, } screen.render(None)?; drop(completion_tx); }, None => { // Dump to STDOUT via Log let dump = match pane_id { Some(pane_id) => { let mut result = String::new(); for tab in screen.get_tabs_mut().values_mut() { if tab.has_pane_with_pid(&pane_id) { if ansi { if let Some(dump) = tab .get_dump_with_ansi_terminal_screen(pane_id, full) { result = dump; } } else { if let Some(dump) = tab.get_dump_terminal_screen(pane_id, full) { result = dump; } } break; } } result }, None => { let mut result = String::new(); if ansi { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| { result = tab.get_dump_with_ansi_active_terminal_screen(client_id, full); Ok::<(), anyhow::Error>(()) }, ? ); } else { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| { result = tab.get_dump_active_terminal_screen(client_id, full); Ok::<(), anyhow::Error>(()) }, ? ); } result }, }; screen.bus.senders.send_to_server(ServerInstruction::Log( vec![dump], cli_client_id.unwrap_or(client_id), completion_tx, ))?; }, } }, ScreenInstruction::DumpLayout(default_shell, client_id, completion_tx) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(default_shell, None); screen .bus .senders .send_to_plugin(PluginInstruction::DumpLayout( session_layout_metadata, client_id, completion_tx, )) .with_context(err_context)?; }, ScreenInstruction::ListClientsMetadata(default_shell, client_id, completion_tx) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(default_shell, None); screen .bus .senders .send_to_plugin(PluginInstruction::ListClientsMetadata( session_layout_metadata, client_id, completion_tx, )) .with_context(err_context)?; }, ScreenInstruction::ListPanes { show_all, response_channel, } => { let err_context = || "Failed to list panes"; let pane_entries = screen .collect_pane_list(show_all) .with_context(err_context)?; let _ = response_channel.send(pane_entries); }, ScreenInstruction::ListTabs { client_id, response_channel, } => { let err_context = || "Failed to list tabs"; let tab_infos = screen .collect_tab_list(client_id) .with_context(err_context)?; let _ = response_channel.send(tab_infos); }, ScreenInstruction::GetCurrentTabInfo { client_id, response_channel, } => { let err_context = || "Failed to get current tab info"; let tab_info = screen .get_current_tab_info(client_id) .with_context(err_context)?; let _ = response_channel.send(tab_info); }, ScreenInstruction::DumpLayoutToPlugin { plugin_id, tab_index, response_channel, } => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(Some(screen.default_shell.clone()), tab_index); screen .bus .senders .send_to_pty(PtyInstruction::DumpLayoutToPlugin { session_layout_metadata, plugin_id, response_channel, }) .with_context(err_context) .non_fatal(); }, ScreenInstruction::GetFocusedPaneInfo { client_id, response_channel, } => { let response = match screen.active_tab_ids.get(&client_id) { Some(&focused_tab_index) => match screen.get_active_pane_id(&client_id) { Some(focused_pane_id) => GetFocusedPaneInfoResponse::Ok { tab_index: focused_tab_index, pane_id: focused_pane_id.into(), }, None => GetFocusedPaneInfoResponse::Err(format!( "No active pane found for client {:?}", client_id )), }, None => GetFocusedPaneInfoResponse::Err(format!( "Client {:?} not found in active_tab_indices", client_id )), }; let _ = response_channel.send(response); }, ScreenInstruction::GetPaneInfo { pane_id, response_channel, } => { let pane_info = screen.get_pane_info(pane_id); let _ = response_channel.send(pane_info); }, ScreenInstruction::GetTabInfo { tab_id, response_channel, } => { let tab_info = screen.get_tab_info(tab_id); let _ = response_channel.send(tab_info); }, ScreenInstruction::ListClientsToPlugin(plugin_id, client_id) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(Some(screen.default_shell.clone()), None); screen .bus .senders .send_to_pty(PtyInstruction::ListClientsToPlugin( session_layout_metadata, plugin_id, client_id, )) .with_context(err_context) .non_fatal(); }, ScreenInstruction::EditScrollback(client_id, ansi, completion_tx) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| { if ansi { tab.edit_scrollback_raw(client_id, completion_tx) } else { tab.edit_scrollback(client_id, completion_tx) } }, ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::GetPaneScrollback { pane_id, client_id, get_full_scrollback, response_channel, } => { let mut pane_contents: Option = None; for tab in screen.get_tabs_mut().values() { if let Some(pane) = tab.get_pane_with_id(pane_id) { pane_contents = Some(pane.pane_contents(Some(client_id), get_full_scrollback, None)); break; } } // Send response back through channel let response = match pane_contents { Some(contents) => PaneScrollbackResponse::Ok(contents), None => { log::warn!( "Plugin requested scrollback for pane {:?} but pane was not found", pane_id ); PaneScrollbackResponse::Err(format!("Pane {:?} not found", pane_id)) }, }; if let Err(_) = response_channel.send(response) { // the plugin likely timed out and dropped the receiver log::debug!( "Plugin timed out before pane scrollback response was sent for pane {:?}", pane_id ); } }, ScreenInstruction::ScrollUp( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_up(client_id) ); screen.render(None)?; }, ScreenInstruction::MovePane( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneBackwards( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_backwards(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneDown( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_down(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneUp( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_up(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneRight( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_right(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneLeft( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_left(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ScrollUpAt( point, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .handle_scrollwheel_up(&point, 3, client_id), ? ); screen.render(None)?; }, ScreenInstruction::ScrollDown( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_down(client_id), ? ); screen.render(None)?; }, ScreenInstruction::ScrollDownAt( point, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .handle_scrollwheel_down(&point, 3, client_id), ? ); screen.render(None)?; }, ScreenInstruction::ScrollToBottom( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_to_bottom(client_id), ? ); screen.render(None)?; }, ScreenInstruction::ScrollToTop( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_to_top(client_id), ? ); screen.render(None)?; }, ScreenInstruction::PageScrollUp( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_up_page(client_id) ); screen.render(None)?; }, ScreenInstruction::PageScrollDown( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_down_page(client_id), ? ); screen.render(None)?; }, ScreenInstruction::HalfPageScrollUp( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_up_half_page(client_id) ); screen.render(None)?; }, ScreenInstruction::HalfPageScrollDown( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .scroll_active_terminal_down_half_page(client_id), ? ); screen.render(None)?; }, ScreenInstruction::ClearScroll(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .clear_active_terminal_scroll(client_id), ? ); screen.render(None)?; }, ScreenInstruction::CloseFocusedPane(client_id, completion_tx) => { let old_pane_id = screen.get_active_pane_id(&client_id); active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id, completion_tx), ? ); let new_pane_id = screen.get_active_pane_id(&client_id); if let (Some(old), Some(new)) = (old_pane_id, new_pane_id) { screen.report_key_passthrough_state(client_id, old, new); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SetSelectable(pid, selectable) => { let all_tabs = screen.get_tabs_mut(); let mut found_plugin = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pid) { tab.set_pane_selectable(pid, selectable); found_plugin = true; break; } } if !found_plugin { pending_events_waiting_for_tab .push(ScreenInstruction::SetSelectable(pid, selectable)); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ShowPluginCursor(pid, client_id, cursor_position) => { let all_tabs = screen.get_tabs_mut(); let mut found_plugin = false; for tab in all_tabs.values_mut() { if tab.has_plugin(pid) { tab.show_plugin_cursor(pid, client_id, cursor_position); found_plugin = true; break; } } if !found_plugin { pending_events_waiting_for_tab.push(ScreenInstruction::ShowPluginCursor( pid, client_id, cursor_position, )); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SetMouseSelectionSupport(pid, selection_support) => { let all_tabs = screen.get_tabs_mut(); let mut found_plugin = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pid) { tab.set_mouse_selection_support(pid, selection_support); found_plugin = true; break; } } if !found_plugin { pending_events_waiting_for_tab.push( ScreenInstruction::SetMouseSelectionSupport(pid, selection_support), ); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ClosePane( id, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it exit_status, ) => { match client_id { Some(client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab.close_pane( id, false, exit_status )); }, None => { let mut found = false; for tab in screen.tabs.values_mut() { if tab.get_all_pane_ids().contains(&id) { tab.close_pane(id, false, exit_status); found = true; break; } } if !found { pending_events_waiting_for_pane .entry(id) .or_default() .push(ScreenInstruction::ClosePane(id, None, None, exit_status)); } }, } screen.clear_nested_guest(id); // Clean up PTY-side resources (async reader task, child PID mapping, // terminal_id_to_raw_fd entry). This is needed because the natural // child exit path (quit_cb) only sends ScreenInstruction::ClosePane // and never sends PtyInstruction::ClosePane. The handler in Pty is // idempotent, so this is safe even if ClosePane was already sent. let _ = screen .bus .senders .send_to_pty(PtyInstruction::ClosePane(id, None)); screen.log_and_report_session_state().non_fatal(); screen.retain_only_existing_panes_in_pane_groups(); }, ScreenInstruction::HoldPane(id, exit_status, run_command) => { let is_first_run = false; let mut found = false; for tab in screen.tabs.values_mut() { if tab.get_all_pane_ids().contains(&id) { tab.hold_pane(id, exit_status, is_first_run, run_command.clone()); found = true; break; } } if !found { pending_events_waiting_for_pane .entry(id) .or_default() .push(ScreenInstruction::HoldPane(id, exit_status, run_command)); } screen.log_and_report_session_state()?; }, ScreenInstruction::UpdatePaneName( c, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.update_active_pane_name(c, client_id), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::UndoRenamePane( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.undo_active_rename_pane(client_id), ? ); screen.render(None)?; }, ScreenInstruction::ToggleActiveTerminalFullscreen( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_active_pane_fullscreen(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ToggleActiveTerminalNoUiFullscreen(client_id, _completion_tx) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_active_pane_no_ui_fullscreen(client_id) ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::TogglePaneFrames( _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.pane_frame_style = match screen.pane_frame_style { PaneFrameStyle::Full => PaneFrameStyle::Titles, PaneFrameStyle::Titles => PaneFrameStyle::None, PaneFrameStyle::None => PaneFrameStyle::Full, }; for tab in screen.tabs.values_mut() { tab.set_pane_frames(screen.pane_frame_style); tab.update_input_modes()?; } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SetPaneFrameStyle(pane_frame_style, _completion_tx) => { screen.pane_frame_style = pane_frame_style; for tab in screen.tabs.values_mut() { tab.set_pane_frames(screen.pane_frame_style); tab.update_input_modes()?; } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SwitchTabNext( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.switch_tab_next(None, true, client_id)?; screen.render(None)?; }, ScreenInstruction::SwitchTabPrev( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.switch_tab_prev(None, true, client_id)?; screen.render(None)?; }, ScreenInstruction::CloseTab( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.close_tab(client_id)?; screen.render(None)?; }, ScreenInstruction::NewTab( cwd, default_shell, 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), completion_tx, ) => { let tab_index = screen.get_new_tab_id(); pending_tab_ids.insert(tab_index); let client_id_for_new_tab = if should_change_focus_to_new_tab { Some(client_id) } else { None }; let resolved_swap_layouts = ( swap_tiled_layouts .unwrap_or_else(|| screen.default_layout.swap_tiled_layouts.clone()), swap_floating_layouts .unwrap_or_else(|| screen.default_layout.swap_floating_layouts.clone()), ); screen.new_tab( tab_index, resolved_swap_layouts, tab_name.clone(), client_id_for_new_tab, )?; screen .bus .senders .send_to_plugin(PluginInstruction::NewTab( cwd, default_shell, layout, floating_panes_layout, tab_index, initial_panes, block_on_first_terminal, should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, ))?; }, ScreenInstruction::ApplyLayout( layout, floating_panes_layout, new_pane_pids, new_floating_pane_pids, new_plugin_ids, tab_id, should_change_focus_to_new_tab, (client_id, is_web_client), mut completion_tx, blocking_terminal, ) => { log::info!( "ScreenInstruction::ApplyLayout: applying layout for tab {}", tab_id ); // tab_id is a stable identifier from NewTab instruction if let Some(first_terminal_pane) = new_pane_pids.iter().next() { completion_tx .as_mut() .map(|c| c.set_affected_pane_id(PaneId::Terminal(first_terminal_pane.0))); } else if let Some(plugin_id) = new_plugin_ids.values().next().and_then(|v| v.first()) { completion_tx .as_mut() .map(|c| c.set_affected_pane_id(PaneId::Plugin(*plugin_id))); } // Set the affected tab ID for plugin API return value completion_tx .as_mut() .map(|c| c.set_affected_tab_id(tab_id)); screen.apply_layout( layout, floating_panes_layout, new_pane_pids.clone(), new_floating_pane_pids, new_plugin_ids.clone(), tab_id, should_change_focus_to_new_tab, (client_id, is_web_client), blocking_terminal, )?; pending_tab_ids.remove(&tab_id); if pending_tab_ids.is_empty() { for (tab_index, client_id) in pending_tab_switches.drain() { screen.go_to_tab(tab_index as usize + 1, client_id)?; } if should_change_focus_to_new_tab { // Convert ID → position for go_to_tab (which expects 1-based position) if let Some(tab_position) = screen.get_tab_position_by_id(tab_id) { screen.go_to_tab(tab_position + 1, client_id)?; } } } else if should_change_focus_to_new_tab { let client_id_to_switch = if screen.active_tab_ids.contains_key(&client_id) { Some(client_id) } else { screen.active_tab_ids.keys().next().copied() }; if let Some(client_id_to_switch) = client_id_to_switch { // Convert ID → position for pending_tab_switches (which stores positions) if let Some(tab_position) = screen.get_tab_position_by_id(tab_id) { pending_tab_switches.insert((tab_position, client_id_to_switch)); } } } for plugin_ids in new_plugin_ids.values() { for plugin_id in plugin_ids { if let Some(loading_indication) = plugin_loading_message_cache.remove(plugin_id) { screen.update_plugin_loading_stage(*plugin_id, loading_indication); screen.render(None)?; } screen.render_blocker.register_blocking_plugin(*plugin_id); } } for event in pending_events_waiting_for_client.drain(..) { screen.bus.senders.send_to_screen(event).non_fatal(); } for event in pending_events_waiting_for_tab.drain(..) { screen.bus.senders.send_to_screen(event).non_fatal(); } screen.render(None)?; // we do this here in order to recover from a race condition on app start // that sometimes causes Zellij to think the terminal window is a different size // than it actually is - here, we query the client for its terminal size after // we've finished the setup and handle it as we handle a normal resize, // while this can affect other instances of a layout being applied, the query is // very short and cheap and shouldn't cause any trouble if let Some(os_input) = &mut screen.bus.os_input { for (client_id, _is_web_client) in screen.connected_clients.borrow().iter() { log::info!( "ApplyLayout: sending QueryTerminalSize to client {}", client_id ); let _ = os_input .send_to_client(*client_id, ServerToClientMsg::QueryTerminalSize); } } }, ScreenInstruction::GoToTab( tab_index, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { let client_id_to_switch = if client_id.is_none() { None } else if screen .active_tab_ids .contains_key(&client_id.expect("This is checked above")) { client_id } else { screen.active_tab_ids.keys().next().copied() }; match client_id_to_switch { // we must make sure pending_tab_ids is empty because otherwise we cannot be // sure this instruction is applied at the right time (eg. we might have a // pending tab that will become not-pending after this instruction and change // the client focus, which should have happened before this instruction and not // after) Some(client_id) if pending_tab_ids.is_empty() => { screen.go_to_tab(tab_index as usize, client_id)?; screen.render(None)?; }, _ => { if let Some(client_id) = client_id { pending_tab_switches.insert((tab_index as usize, client_id)); } }, } }, ScreenInstruction::GoToTabName( tab_name, default_shell, create, client_id, mut completion_tx, ) => { let swap_layouts = ( screen.default_layout.swap_tiled_layouts.clone(), screen.default_layout.swap_floating_layouts.clone(), ); let client_id = if client_id.is_none() { None } else if screen .active_tab_ids .contains_key(&client_id.expect("This is checked above")) { client_id } else { screen.active_tab_ids.keys().next().copied() }; if let Some(client_id) = client_id { let is_web_client = screen .connected_clients .borrow() .get(&client_id) .copied() .unwrap_or(false); if let Ok(tab_exists) = screen.go_to_tab_name(tab_name.clone(), client_id) { screen.render(None)?; if tab_exists { // Tab already exists - find its ID and set in completion if let Some(existing_tab) = screen.tabs.values().find(|t| t.name == tab_name) { completion_tx .as_mut() .map(|c| c.set_affected_tab_id(existing_tab.id)); } } if create && !tab_exists { let tab_index = screen.get_new_tab_id(); let should_change_focus_to_new_tab = true; screen.new_tab( tab_index, swap_layouts, Some(tab_name), Some(client_id), )?; screen .bus .senders .send_to_plugin(PluginInstruction::NewTab( None, default_shell, None, vec![], tab_index, None, // initial_panes false, // block_on_first_terminal should_change_focus_to_new_tab, (client_id, is_web_client), completion_tx, ))?; continue; // so we don't get to the completion signalling below } } } }, ScreenInstruction::UpdateTabName( c, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.update_active_tab_name(c, client_id)?; screen.render(None)?; }, ScreenInstruction::UndoRenameTab( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.undo_active_rename_tab(client_id)?; screen.render(None)?; }, ScreenInstruction::MoveTabLeft(client_id, completion_tx) => { if pending_tab_ids.is_empty() { screen.move_active_tab_to_left(client_id)?; screen.render(None)?; } else { // Defer execution, forward completion_tx pending_events_waiting_for_tab .push(ScreenInstruction::MoveTabLeft(client_id, completion_tx)); } }, ScreenInstruction::MoveTabRight(client_id, completion_tx) => { if pending_tab_ids.is_empty() { screen.move_active_tab_to_right(client_id)?; screen.render(None)?; } else { // Defer execution, forward completion_tx pending_events_waiting_for_tab .push(ScreenInstruction::MoveTabRight(client_id, completion_tx)); } }, ScreenInstruction::TerminalResize(new_size) => { screen.resize_to_screen(new_size)?; screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins screen.render(None)?; }, ScreenInstruction::RecomputeTabSize(client_id, new_size) => { screen.set_client_size(client_id, new_size); let active_tab_id = screen.active_tab_ids.get(&client_id).copied(); if let Some(tab_id) = active_tab_id { screen.recompute_tab_size(tab_id)?; screen.log_and_report_session_state()?; screen.render(None)?; } }, ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => { screen.update_pixel_dimensions(pixel_dimensions); }, ScreenInstruction::TerminalBackgroundColor(background_color_instruction) => { screen.update_terminal_background_color(background_color_instruction); }, ScreenInstruction::TerminalForegroundColor(background_color_instruction) => { screen.update_terminal_foreground_color(background_color_instruction); }, ScreenInstruction::TerminalColorRegisters(color_registers) => { screen.update_terminal_color_registers(color_registers); }, ScreenInstruction::SetKittyGraphicsSupport { client_id, supported, } => { screen.update_kitty_graphics_support(client_id, supported); }, ScreenInstruction::ForwardHostQuery { pane_id, query } => { screen.forward_host_query(pane_id, query); }, ScreenInstruction::NestedSessionMessageFromPane { pane_id, message } => { screen.handle_nested_session_message_from_pane(pane_id, message); }, ScreenInstruction::NestedGuestPingTick { pane_id } => { screen.handle_nested_guest_ping_tick(pane_id); }, ScreenInstruction::NestedSessionMessageFromHost { client_id, message } => { screen.handle_nested_session_message_from_host(client_id, message); }, ScreenInstruction::GuestModalChoice { client_id, pane_id, outcome, } => { screen.handle_guest_modal_choice(client_id, pane_id, outcome); }, ScreenInstruction::ForwardedReplyFromHost { token, reply_bytes } => { screen.handle_forwarded_reply_from_host(token, reply_bytes)?; // The handler's replay of `pending_pty_input` mutates the // grid. Without a render scheduling here the clients // keep displaying the pre-reply frame screen.render(None)?; }, ScreenInstruction::ResumePaneAfterForward { pane_id, reply_bytes, } => { screen.resume_pane_after_forward(pane_id, reply_bytes)?; // Same rationale as ForwardedReplyFromHost above — the // resume's replay paints the grid, so we must schedule // a render before yielding back to the screen loop. screen.render(None)?; }, ScreenInstruction::HostTerminalThemeChanged(mode) => { screen.update_host_terminal_theme_mode(mode)?; }, ScreenInstruction::SetDarkTheme(mut completion_tx) => { screen.apply_manual_host_terminal_theme_mode( HostTerminalThemeMode::Dark, &mut completion_tx, )?; }, ScreenInstruction::SetLightTheme(mut completion_tx) => { screen.apply_manual_host_terminal_theme_mode( HostTerminalThemeMode::Light, &mut completion_tx, )?; }, ScreenInstruction::ToggleTheme(mut completion_tx) => { let next = match screen.host_terminal_theme_mode { Some(HostTerminalThemeMode::Dark) => HostTerminalThemeMode::Light, Some(HostTerminalThemeMode::Light) => HostTerminalThemeMode::Dark, // No prior mode: treat as Dark and toggle to Light. None => HostTerminalThemeMode::Light, }; screen.apply_manual_host_terminal_theme_mode(next, &mut completion_tx)?; }, ScreenInstruction::ChangeMode( input_mode, base_mode, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.change_mode(input_mode, base_mode, client_id)?; screen.render(None)?; }, ScreenInstruction::ChangeModeForAllClients( input_mode, base_mode, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.change_mode_for_all_clients(input_mode, base_mode)?; screen.render(None)?; }, ScreenInstruction::ToggleActiveSyncTab( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.toggle_sync_panes_is_active() ); screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::MouseEvent( event, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.handle_mouse_event(event, client_id); }, ScreenInstruction::Copy( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .copy_selection(client_id), ?); screen.render(None)?; }, ScreenInstruction::Exit => { break; }, ScreenInstruction::ToggleTab( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.toggle_tab(client_id)?; screen.render(None)?; }, ScreenInstruction::AddClient( client_id, is_web_client, client_size, tab_position_to_focus, pane_id_to_focus, ) => { screen.set_client_size(client_id, client_size); screen.add_client(client_id, is_web_client)?; let pane_id = pane_id_to_focus.map(|(pane_id, is_plugin)| { if is_plugin { PaneId::Plugin(pane_id) } else { PaneId::Terminal(pane_id) } }); if let Some(pane_id) = pane_id { screen.focus_pane_with_id(pane_id, true, false, client_id)?; } else if let Some(tab_position_to_focus) = tab_position_to_focus { screen.go_to_tab(tab_position_to_focus, client_id)?; } screen.show_guest_modals_for_new_client(client_id); let focused_pane_id = screen.get_active_pane_id(&client_id); if let Some(focused) = focused_pane_id { screen.report_key_passthrough_state(client_id, focused, focused); } for event in pending_events_waiting_for_client.drain(..) { screen.bus.senders.send_to_screen(event).non_fatal(); } screen.log_and_report_session_state()?; if is_web_client { // we do this because // we need to query the client for its size, and we must do it only after we've // added it to our state. // // we have to do this specifically for web clients because the browser (as opposed // to a traditional terminal) can only figure out its dimensions after we sent it relevant // state (eg. font, which is controlled by our config and it needs to determine cell size) if let Some(os_input) = &mut screen.bus.os_input { let _ = os_input .send_to_client(client_id, ServerToClientMsg::QueryTerminalSize); } } screen.render(None)?; }, ScreenInstruction::RemoveClient(client_id) => { screen.remove_client(client_id).non_fatal(); screen.log_and_report_session_state().non_fatal(); screen.render(None).non_fatal(); }, ScreenInstruction::SuppressRenderUntilMobile(client_id) => { screen.mobile_render_gate.gate(client_id); screen .bus .senders .send_to_background_jobs(BackgroundJob::MobileGateTimeout(client_id)) .non_fatal(); }, ScreenInstruction::MobileSizeSettled(client_id) => { screen.mobile_render_gate.record_settled_size(client_id); screen .bus .senders .send_to_plugin(PluginInstruction::ReleaseMobileRender(client_id)) .non_fatal(); if screen.try_lift_mobile_gate(client_id) { screen.render(None)?; } }, ScreenInstruction::ForceMobileUngate(client_id) => { if screen.mobile_render_gate.is_gated(client_id) { screen .bus .senders .send_to_plugin(PluginInstruction::ReleaseMobileRender(client_id)) .non_fatal(); screen.mobile_render_gate.ungate(client_id); screen.force_render_mobile_tab(client_id); screen.render(None)?; } }, ScreenInstruction::UpdateSearch( c, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.update_search_term(c, client_id), ? ); screen.render(None)?; }, ScreenInstruction::SearchDown( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.search_down(client_id) ); screen.render(None)?; }, ScreenInstruction::SearchUp( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.search_up(client_id) ); screen.render(None)?; }, ScreenInstruction::SearchToggleCaseSensitivity( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_search_case_sensitivity(client_id) ); screen.render(None)?; }, ScreenInstruction::SearchToggleWrap( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.toggle_search_wrap(client_id) ); screen.render(None)?; }, ScreenInstruction::SearchToggleWholeWord( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.toggle_search_whole_words(client_id) ); screen.render(None)?; }, ScreenInstruction::AddRedPaneFrameColorOverride(pane_ids, error_text) => { let all_tabs = screen.get_tabs_mut(); for pane_id in pane_ids { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.add_red_pane_frame_color_override(pane_id, error_text.clone()); break; } } } screen.render(None)?; }, ScreenInstruction::AddHighlightPaneFrameColorOverride(pane_ids, error_text) => { let all_tabs = screen.get_tabs_mut(); for pane_id in pane_ids { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.add_highlight_pane_frame_color_override( pane_id, error_text.clone(), None, ); break; } } } screen.render(None)?; }, ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => { let all_tabs = screen.get_tabs_mut(); for pane_id in pane_ids { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_pane_frame_color_override(pane_id, None); break; } } } screen.render(None)?; }, ScreenInstruction::SetTabBellFlash(tab_id, is_flashing) => { if let Some(tab) = screen.tabs.values_mut().find(|t| t.id == tab_id) { tab.tab_bell_flash = is_flashing; tab.clear_tab_bell_ring(); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::PreviousSwapLayout( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.previous_swap_layout(), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::NextSwapLayout( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.next_swap_layout(), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::OverrideLayout( cwd, default_shell, mut tab_layouts, retain_existing_terminal_panes, retain_existing_plugin_panes, apply_only_to_focused_tab, client_id, completion_tx, ) => { // 1. Determine which tabs to close (exist but not in layout) let existing_tab_indices: HashSet = screen.tabs.keys().copied().collect(); let layout_tab_indices: HashSet = tab_layouts.iter().map(|tl| tl.tab_index).collect(); let tabs_to_close: Vec = existing_tab_indices .difference(&layout_tab_indices) .copied() .collect(); // 2. Process each tab layout let mut processed_tab_layouts = Vec::new(); if apply_only_to_focused_tab { match screen.get_active_tab_mut(client_id) { Ok(active_tab) => { if tab_layouts.is_empty() { log::error!("No tab layouts found, cannot override."); continue; } let mut tab_layout_info = tab_layouts.remove(0); tab_layout_info.tab_index = active_tab.id; // Set the tab name if provided if let Some(name) = tab_layout_info.tab_name.take() { active_tab.name = name; } // Find already-running panes for this tab let (tiled_to_ignore, floating_indices) = find_already_running_panes( &tab_layout_info.tiled_layout, &tab_layout_info.floating_layouts, active_tab, ); // Mark run instructions to ignore (prevents re-spawning) for run_instruction in tiled_to_ignore { tab_layout_info .tiled_layout .ignore_run_instruction(run_instruction); } for idx in floating_indices { tab_layout_info .floating_layouts .get_mut(idx) .map(|f| f.already_running = true); } processed_tab_layouts.push(tab_layout_info); }, Err(e) => { log::error!("Failed to override layout of active tab: {}", e); }, } } else { for mut tab_layout_info in tab_layouts { // Get the tab by index let tab = match screen.tabs.get_mut(&tab_layout_info.tab_index) { Some(t) => t, None => { // no corresponding tab exists, we'll create it processed_tab_layouts.push(tab_layout_info); continue; }, }; // Set the tab name if provided if let Some(name) = tab_layout_info.tab_name.take() { tab.name = name; } // Find already-running panes for this tab let (tiled_to_ignore, floating_indices) = find_already_running_panes( &tab_layout_info.tiled_layout, &tab_layout_info.floating_layouts, tab, ); // Mark run instructions to ignore (prevents re-spawning) for run_instruction in tiled_to_ignore { tab_layout_info .tiled_layout .ignore_run_instruction(run_instruction); } for idx in floating_indices { tab_layout_info .floating_layouts .get_mut(idx) .map(|f| f.already_running = true); } processed_tab_layouts.push(tab_layout_info); } } // 3. Send to plugin thread with all tab layouts screen .bus .senders .send_to_plugin(PluginInstruction::OverrideLayout( cwd, default_shell, processed_tab_layouts, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, completion_tx, ))?; // 4. Close tabs that aren't in the layout if !apply_only_to_focused_tab { for tab_index in tabs_to_close { screen.close_tab_by_id(tab_index)?; } } }, ScreenInstruction::OverrideLayoutComplete( tab_results, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, completion_tx, ) => { // Process each tab result for tab_result in tab_results { if let Some(tab) = screen.tabs.get_mut(&tab_result.tab_index) { if let Err(e) = tab.override_layout( tab_result.tiled_layout, tab_result.floating_layouts, tab_result.swap_tiled_layouts.clone(), tab_result.swap_floating_layouts.clone(), tab_result.new_terminal_pids, tab_result.new_floating_pane_pids, tab_result.plugin_ids, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, None, ) { log::error!( "Failed to override layout for tab {}: {:?}", tab_result.tab_index, e ); } } else { // Tab doesn't exist - create it let swap_layouts = ( tab_result.swap_tiled_layouts.clone().unwrap_or_default(), tab_result.swap_floating_layouts.clone().unwrap_or_default(), ); if let Err(e) = screen.new_tab( tab_result.tab_index, swap_layouts, tab_result.tab_name.clone(), None, ) { log::error!( "Failed to create new tab {} during override completion: {:?}", tab_result.tab_index, e ); continue; } // Now override the newly created tab's layout if let Some(tab) = screen.tabs.get_mut(&tab_result.tab_index) { if let Err(e) = tab.override_layout( tab_result.tiled_layout, tab_result.floating_layouts, tab_result.swap_tiled_layouts, tab_result.swap_floating_layouts, tab_result.new_terminal_pids, tab_result.new_floating_pane_pids, tab_result.plugin_ids, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, None, ) { log::error!( "Failed to override layout for new tab {}: {:?}", tab_result.tab_index, e ); } } } } for event in pending_events_waiting_for_client.drain(..) { screen.bus.senders.send_to_screen(event).non_fatal(); } for event in pending_events_waiting_for_tab.drain(..) { screen.bus.senders.send_to_screen(event).non_fatal(); } // Single render and log after all tabs are updated (performance optimization) screen.log_and_report_session_state()?; let _ = screen.render(None); drop(completion_tx); // action ends here, notify the action initiator }, ScreenInstruction::QueryTabNames(client_id, completion_tx) => { let tab_names = screen .get_tabs_mut() .values() .map(|tab| tab.name.clone()) .collect::>(); screen.bus.senders.send_to_server(ServerInstruction::Log( tab_names, client_id, completion_tx, ))?; }, ScreenInstruction::NewTiledPluginPane( run_plugin, pane_title, skip_cache, cwd, client_id, completion_tx, explicit_tab_id, no_focus, ) => { let tab_index = explicit_tab_id .unwrap_or_else(|| *screen.active_tab_ids.values().next().unwrap_or(&1)); let size = Size::default(); let should_float = Some(false); let should_be_opened_in_place = false; let should_focus_plugin = if no_focus { Some(false) } else { None }; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_opened_in_place, false, // close_replaced_pane pane_title, run_plugin, tab_index, None, client_id, size, skip_cache, cwd, should_focus_plugin, None, completion_tx, ))?; }, ScreenInstruction::NewFloatingPluginPane( run_plugin, pane_title, skip_cache, cwd, floating_pane_coordinates, client_id, completion_tx, explicit_tab_id, no_focus, ) => { let resolved_tab_index = explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); match resolved_tab_index { Some(tab_index) => { let size = Size::default(); let should_float = Some(true); let should_be_opened_in_place = false; let should_focus_plugin = if no_focus { Some(false) } else { None }; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_opened_in_place, false, // close_replaced_pane pane_title, run_plugin, tab_index, None, client_id, size, skip_cache, cwd, should_focus_plugin, floating_pane_coordinates, completion_tx, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, } }, ScreenInstruction::NewInPlacePluginPane( run_plugin, pane_title, pane_id_to_replace, skip_cache, close_replaced_pane, client_id, completion_tx, explicit_tab_id, no_focus, ) => { let resolved_tab_index = explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); match resolved_tab_index { Some(tab_index) => { let size = Size::default(); let should_float = None; let should_be_in_place = true; let should_focus_plugin = if no_focus { Some(false) } else { None }; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_in_place, close_replaced_pane, pane_title, run_plugin, tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, None, should_focus_plugin, None, completion_tx, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, } }, ScreenInstruction::StartOrReloadPluginPane(run_plugin, pane_title, completion_tx) => { let tab_index = screen.active_tab_ids.values().next().unwrap_or(&1); let size = Size::default(); let should_float = Some(false); screen .bus .senders .send_to_plugin(PluginInstruction::Reload( should_float, pane_title, run_plugin, *tab_index, size, completion_tx, ))?; }, ScreenInstruction::AddPlugin( should_float, should_be_in_place, close_replaced_pane, run_plugin_or_alias, pane_title, tab_index, plugin_id, pane_id_to_replace, cwd, start_suppressed, floating_pane_coordinates, should_focus_plugin, client_id, mut completion_tx, ) => { let mut new_pane_placement = NewPanePlacement::default(); let maybe_should_float = should_float; let should_be_tiled = maybe_should_float.map(|f| !f).unwrap_or(false); let should_float = maybe_should_float.unwrap_or(false); if floating_pane_coordinates.is_some() || should_float { new_pane_placement = NewPanePlacement::with_floating_pane_coordinates( floating_pane_coordinates.clone(), ); } if should_be_tiled { new_pane_placement = NewPanePlacement::Tiled { direction: None, borderless: None, }; } if should_be_in_place { new_pane_placement = NewPanePlacement::with_pane_id_to_replace( pane_id_to_replace.map(|id| id.into()), close_replaced_pane, ); } if screen.active_tab_ids.is_empty() && tab_index.is_none() { pending_events_waiting_for_client.push(ScreenInstruction::AddPlugin( maybe_should_float, should_be_in_place, close_replaced_pane, run_plugin_or_alias, pane_title, tab_index, plugin_id, pane_id_to_replace, cwd, start_suppressed, floating_pane_coordinates, should_focus_plugin, client_id, completion_tx, )); continue; } let pane_title = pane_title.unwrap_or_else(|| { format!( "({}) - {}", cwd.map(|cwd| cwd.display().to_string()) .unwrap_or(".".to_owned()), run_plugin_or_alias.location_string() ) }); let run_plugin = Run::Plugin(run_plugin_or_alias); // Set affected pane ID for CLI client output if let Some(ref mut completion) = completion_tx { completion.set_affected_pane_id(PaneId::Plugin(plugin_id)); } if should_be_in_place { if let Some(pane_id_to_replace) = pane_id_to_replace { let client_tab_index_or_pane_id = ClientTabIndexOrPaneId::PaneId(pane_id_to_replace); screen.replace_pane( PaneId::Plugin(plugin_id), None, Some(run_plugin), Some(pane_title), close_replaced_pane, client_tab_index_or_pane_id, )?; } else if let Some(client_id) = client_id { let client_tab_index_or_pane_id = ClientTabIndexOrPaneId::ClientId(client_id); screen.replace_pane( PaneId::Plugin(plugin_id), None, Some(run_plugin), Some(pane_title), close_replaced_pane, client_tab_index_or_pane_id, )?; } else { log::error!("Must have pane id to replace or connected client_id if replacing a pane"); } } else if let Some(client_id) = client_id { active_tab_and_connected_client_id!(screen, client_id, |active_tab: &mut Tab, _client_id: ClientId| { active_tab.new_pane( PaneId::Plugin(plugin_id), Some(pane_title), Some(run_plugin), start_suppressed, should_focus_plugin.unwrap_or(true), new_pane_placement, Some(client_id), None, ) }, ?); } else if let Some(active_tab) = tab_index.and_then(|tab_index| screen.tabs.get_mut(&tab_index)) { active_tab.new_pane( PaneId::Plugin(plugin_id), Some(pane_title), Some(run_plugin), start_suppressed, should_focus_plugin.unwrap_or(true), new_pane_placement, None, None, )?; } else { log::error!("Tab index not found: {:?}", tab_index); } if let Some(loading_indication) = plugin_loading_message_cache.remove(&plugin_id) { screen.update_plugin_loading_stage(plugin_id, loading_indication); screen.render(None)?; } screen.log_and_report_session_state()?; }, ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => { if loading_indication.is_error() { screen.ungate_clients_for_mobile_plugin(pid); } let found_plugin = screen.update_plugin_loading_stage(pid, loading_indication.clone()); if !found_plugin { plugin_loading_message_cache.insert(pid, loading_indication); } screen.render(None)?; }, ScreenInstruction::StartPluginLoadingIndication(pid, loading_indication) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_plugin(pid) { tab.start_plugin_loading_indication(pid, loading_indication); break; } } screen.render(None)?; }, ScreenInstruction::ProgressPluginLoadingOffset(pid) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_plugin(pid) { tab.progress_plugin_loading_offset(pid); break; } } screen.render(None)?; }, ScreenInstruction::RequestStateUpdateForPlugins => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { tab.update_input_modes()?; } screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::LaunchOrFocusPlugin( run_plugin, should_float, move_to_focused_tab, should_open_in_place, close_replaced_pane, pane_id_to_replace, skip_cache, client_id, mut completion_tx, explicit_tab_id, ) => match pane_id_to_replace { Some(pane_id_to_replace) if should_open_in_place => { let resolved_tab_index = explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); match resolved_tab_index { Some(tab_index) => { let size = Size::default(); screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, close_replaced_pane, None, run_plugin, tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, None, None, None, completion_tx, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, } }, _ => { let client_id = if screen.active_tab_ids.contains_key(&client_id) { Some(client_id) } else { screen.get_first_client_id() }; let client_id_and_focused_tab = client_id.and_then(|client_id| { screen .active_tab_ids .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); let resolved_tab_and_client = explicit_tab_id .and_then(|tid| client_id.map(|cid| (tid, cid))) .or(client_id_and_focused_tab); match resolved_tab_and_client { Some((tab_index, client_id)) => { if screen.focus_plugin_pane( &run_plugin, should_float, move_to_focused_tab, should_open_in_place, client_id, &mut completion_tx, )? { screen.render(None)?; screen.log_and_report_session_state()?; } else { screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, close_replaced_pane, None, run_plugin, tab_index, None, client_id, Size::default(), skip_cache, None, None, None, completion_tx, ))?; } }, None => { log::error!("No connected clients found - cannot load or focus plugin") }, } }, }, ScreenInstruction::LaunchPlugin( run_plugin, should_float, should_open_in_place, close_replaced_pane, pane_id_to_replace, skip_cache, cwd, client_id, completion_tx, explicit_tab_id, no_focus, ) => match pane_id_to_replace { Some(pane_id_to_replace) => { let resolved_tab_index = explicit_tab_id.or_else(|| screen.active_tab_ids.values().next().copied()); match resolved_tab_index { Some(tab_index) => { let size = Size::default(); screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, close_replaced_pane, None, run_plugin, tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, cwd, if no_focus { Some(false) } else { None }, None, completion_tx, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, } }, None => { let client_id = if screen.active_tab_ids.contains_key(&client_id) { Some(client_id) } else { screen.get_first_client_id() }; let client_id_and_focused_tab = client_id.and_then(|client_id| { screen .active_tab_ids .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); let resolved_tab_and_client = explicit_tab_id .and_then(|tid| client_id.map(|cid| (tid, cid))) .or(client_id_and_focused_tab); match resolved_tab_and_client { Some((tab_index, client_id)) => { screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, close_replaced_pane, None, run_plugin, tab_index, None, client_id, Size::default(), skip_cache, cwd, if no_focus { Some(false) } else { None }, None, completion_tx, ))?; }, None => { log::error!("No connected clients found - cannot load or focus plugin") }, } }, }, ScreenInstruction::SuppressPane(pane_id, client_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_non_suppressed_pane_with_pid(&pane_id) { tab.suppress_pane(pane_id, Some(client_id)); drop(screen.render(None)); break; } } screen.log_and_report_session_state()?; }, ScreenInstruction::UnsuppressPane(pane_id, should_float_if_hidden) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.unsuppress_pane(pane_id, should_float_if_hidden); drop(screen.render(None)); break; } } screen.log_and_report_session_state()?; }, ScreenInstruction::UnsuppressOrExpandPane(pane_id, should_float_if_hidden) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.unsuppress_or_expand_pane(pane_id, should_float_if_hidden); drop(screen.render(None)); break; } } screen.log_and_report_session_state()?; }, ScreenInstruction::FocusPaneWithId( pane_id, should_float_if_hidden, should_be_in_place_if_hidden, client_id, mut completion_tx, ) => { let pane_exists = screen .tabs .iter() .any(|(_, tab)| tab.has_pane_with_pid(&pane_id)); if !pane_exists { if let Some(c) = completion_tx.as_mut() { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } else { let already_focused = screen .get_active_pane_id(&client_id) .map(|active| active == pane_id) .unwrap_or(false); if already_focused { if let Some(c) = completion_tx.as_mut() { c.set_exit_status(1); c.set_error_message(format!("Pane {:?} is already focused", pane_id)); } } else { screen.focus_pane_with_id( pane_id, should_float_if_hidden, should_be_in_place_if_hidden, client_id, )?; screen.clear_bell_for_pane_id(pane_id, client_id); screen.log_and_report_session_state()?; } } }, ScreenInstruction::RenamePane( pane_id, new_name, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { match tab.rename_pane(new_name, pane_id) { Ok(()) => drop(screen.render(None)), Err(e) => log::error!("Failed to rename pane: {:?}", e), } break; } } screen.log_and_report_session_state()?; }, ScreenInstruction::RenameActivePane(new_name, client_id, _completion_tx) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .rename_active_pane(new_name, client_id), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::RenameTab( tab_index, new_name, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { // tab_index here is 1-based user input representing display position let tab_position = tab_index.saturating_sub(1); // Convert to 0-based match screen.get_tab_by_position_mut(tab_position) { Some(tab) => { tab.name = String::from_utf8_lossy(&new_name).to_string(); }, None => { log::error!("Failed to find tab at position: {}", tab_position); }, } screen.log_and_report_session_state()?; }, ScreenInstruction::GoToTabWithId(tab_id, client_id, _completion_tx) => { let client_id_to_switch = client_id .and_then(|cid| { if screen.active_tab_ids.contains_key(&cid) { Some(cid) } else { screen.active_tab_ids.keys().next().copied() } }) .or_else(|| screen.active_tab_ids.keys().next().copied()); if let Some(client_id) = client_id_to_switch { // Get the position from the ID if let Some(tab_position) = screen.get_tab_position_by_id(tab_id) { // switch_active_tab expects 0-based position screen.switch_active_tab(tab_position, None, true, client_id)?; screen.render(None)?; screen .tab_history .entry(client_id) .or_insert_with(Vec::new) .push(tab_id); } else { log::error!("Tab with ID {} not found", tab_id); } } }, ScreenInstruction::RenameTabWithId(tab_id, new_name, _completion_tx) => { // Use get_tab_by_id_mut() helper method if let Some(tab) = screen.get_tab_by_id_mut(tab_id) { tab.name = String::from_utf8_lossy(&new_name).to_string(); screen.log_and_report_session_state()?; } else { log::error!("Failed to find tab with ID: {}", tab_id); } }, ScreenInstruction::CloseTabWithId(tab_id, _completion_tx) => { if screen.get_tab_by_id(tab_id).is_some() { screen.close_tab_by_id(tab_id).non_fatal(); } else { log::error!("Failed to find tab with ID: {}", tab_id); } }, ScreenInstruction::BreakPanesToTabWithId { pane_ids, tab_id, should_change_focus_to_target_tab, client_id, mut completion_tx, } => { // Verify tab exists if screen.get_tab_by_id(tab_id).is_none() { log::error!("Tab with ID {} not found", tab_id); // Don't set affected_tab_id, it will remain None to signal failure } else { // break_multiple_panes_to_tab_with_index uses tab ID screen.break_multiple_panes_to_tab_with_index( pane_ids, tab_id, should_change_focus_to_target_tab, client_id, )?; // Set affected tab ID (tab_id is the ID here) completion_tx .as_mut() .map(|c| c.set_affected_tab_id(tab_id)); let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( pane_group.iter().copied().collect(), "BROKEN OUT".to_owned(), ), ); } screen.clear_pane_group(&client_id); } }, ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => { let all_tabs = screen.get_tabs_mut(); let found = all_tabs.values_mut().any(|tab| { if tab.has_plugin(plugin_id) { tab.request_plugin_permissions(plugin_id, Some(plugin_permission.clone())); true } else { false } }); if !found { log::error!("PluginId '{}' not found - caching request", plugin_id); pending_events_waiting_for_client.push( ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission), ); } }, ScreenInstruction::BreakPane( default_shell, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { let default_layout = screen.default_layout.clone(); screen.break_pane(default_shell, default_layout, client_id)?; }, ScreenInstruction::BreakPaneRight( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.break_pane_to_new_tab(Direction::Right, client_id)?; }, ScreenInstruction::BreakPaneLeft( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.break_pane_to_new_tab(Direction::Left, client_id)?; }, ScreenInstruction::UpdateSessionInfos(new_session_infos, resurrectable_sessions) => { screen.update_session_infos(new_session_infos, resurrectable_sessions)?; }, ScreenInstruction::UpdateAvailableLayouts(layouts, errors) => { screen.update_available_layouts(layouts, errors); }, ScreenInstruction::ReplacePane( new_pane_id, hold_for_command, pane_title, invoked_with, close_replaced_pane, client_id_tab_index_or_pane_id, mut completion_tx, ) => { completion_tx .as_mut() .map(|c| c.set_affected_pane_id(new_pane_id)); screen.replace_pane( new_pane_id, hold_for_command, invoked_with, pane_title, close_replaced_pane, client_id_tab_index_or_pane_id, )?; screen.log_and_report_session_state()?; }, ScreenInstruction::SerializeLayoutForResurrection => { if screen.session_serialization { screen.dump_layout_to_hd()?; } }, ScreenInstruction::SaveSession(_client_id, completion_tx) => { let err_context = || "Failed to save session"; screen.update_active_pane_ids(); let pane_manifest = screen.generate_and_report_pane_state()?; let tab_infos = screen.generate_and_report_tab_state()?; #[cfg(not(test))] let (available_layouts, _layout_errors) = Layout::list_available_layouts( screen.layout_dir.clone(), &screen.default_layout_name, ); #[cfg(test)] let available_layouts = vec![]; let creation_time = { let sock_path = ZELLIJ_SOCK_DIR.join(&screen.session_name); std::fs::metadata(&sock_path) .ok() .and_then(|f| f.created().ok().or_else(|| f.modified().ok())) .and_then(|d| d.elapsed().ok()) .map(|d| Duration::from_secs(d.as_secs())) .unwrap_or_default() }; let session_info = SessionInfo { name: screen.session_name.clone(), tabs: tab_infos, panes: pane_manifest, connected_clients: screen.active_tab_ids.keys().len(), is_current_session: true, available_layouts, web_clients_allowed: screen.web_sharing.web_clients_allowed(), web_client_count: screen .connected_clients .borrow() .iter() .filter(|(_client_id, is_web_client)| **is_web_client) .count(), plugins: Default::default(), tab_history: screen.tab_history.clone(), pane_history: screen .pane_history .iter() .map(|(k, v)| (*k, v.iter().map(|v| (*v).into()).collect())) .collect(), creation_time, }; let session_layout_metadata = if screen.session_serialization { screen.get_layout_metadata(Some(screen.default_shell.clone()), None) } else { // Create empty metadata if serialization is disabled SessionLayoutMetadata::new(screen.default_layout.clone()) }; screen .bus .senders .send_to_pty(PtyInstruction::SaveSessionToDisk { session_name: screen.session_name.clone(), session_info, session_layout_metadata, completion_tx, }) .with_context(err_context)?; }, ScreenInstruction::RenameSession( name, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { if screen.peer_sessions_cache.contains_key(&name) { let error_text = "A session by this name already exists."; log::error!("{}", error_text); if let Some(os_input) = &mut screen.bus.os_input { let _ = os_input.send_to_client( client_id, ServerToClientMsg::LogError { lines: vec![error_text.to_owned()], }, ); } } else if screen.resurrectable_sessions_cache.contains_key(&name) { let error_text = "A resurrectable session by this name exists, cannot use this name."; log::error!("{}", error_text); if let Some(os_input) = &mut screen.bus.os_input { let _ = os_input.send_to_client( client_id, ServerToClientMsg::LogError { lines: vec![error_text.to_owned()], }, ); } } else { let err_context = || format!("Failed to rename session"); let old_session_name = screen.session_name.clone(); // update state screen.session_name = name.clone(); screen.default_mode_info.session_name = Some(name.clone()); for (_client_id, mode_info) in screen.mode_info.iter_mut() { mode_info.session_name = Some(name.clone()); } for (_, tab) in screen.tabs.iter_mut() { tab.rename_session(name.clone()).with_context(err_context)?; } // rename socket file let old_socket_file_path = ZELLIJ_SOCK_DIR.join(&old_session_name); let new_socket_file_path = ZELLIJ_SOCK_DIR.join(&name); if let Err(e) = std::fs::rename(old_socket_file_path, new_socket_file_path) { log::error!("Failed to rename ipc socket: {:?}", e); } // rename session_info folder (TODO: make this atomic, right now there is a // chance background_jobs will re-create this folder before it knows the // session was renamed) let old_session_info_folder = session_info_folder_for_session(&old_session_name); let new_session_info_folder = session_info_folder_for_session(&name); if let Err(e) = std::fs::rename(old_session_info_folder, new_session_info_folder) { log::error!("Failed to rename session_info folder: {:?}", e); } // report screen .log_and_report_session_state() .with_context(err_context)?; // set the env variable set_session_name(name.clone()); let connected_client_ids: Vec = screen.active_tab_ids.keys().copied().collect(); for client_id in connected_client_ids { if let Some(os_input) = &mut screen.bus.os_input { let _ = os_input.send_to_client( client_id, ServerToClientMsg::RenamedSession { name: name.clone() }, ); } } } }, ScreenInstruction::Reconfigure { client_id, keybinds, default_mode, theme, host_theme_dark, host_theme_light, simplified_ui, default_shell, pane_frame_style, copy_to_clipboard, copy_command, copy_on_select, auto_layout, rounded_corners, hide_session_name, pane_title_alignment, pane_gap_x, pane_gap_y, pane_padding, stacked_resize, stacked_pane_list, default_editor, advanced_mouse_actions, mouse_scroll_resize, mouse_hover_effects, visual_bell, focus_follows_mouse, mouse_click_through, nested_session_handling, } => { screen.host_theme_dark_styling = host_theme_dark; screen.host_theme_light_styling = host_theme_light; screen .reconfigure( keybinds, default_mode, theme, simplified_ui, default_shell, pane_frame_style, copy_command, copy_to_clipboard, copy_on_select, auto_layout, rounded_corners, hide_session_name, pane_title_alignment, pane_gap_x, pane_gap_y, pane_padding, stacked_resize, stacked_pane_list, default_editor, advanced_mouse_actions, mouse_scroll_resize, mouse_hover_effects, visual_bell, focus_follows_mouse, mouse_click_through, nested_session_handling, client_id, ) .non_fatal(); }, ScreenInstruction::RerunCommandPane(terminal_pane_id, completion_tx) => { screen.rerun_command_pane_with_id(terminal_pane_id, completion_tx) }, ScreenInstruction::ResizePaneWithId(resize, pane_id) => { screen.resize_pane_with_id(resize, pane_id) }, ScreenInstruction::EditScrollbackForPaneWithId(pane_id, completion_tx) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.edit_scrollback_for_pane_with_id(pane_id, completion_tx) .non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::WriteToPaneId(bytes, pane_id, _completion) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.write_to_pane_id(&None, bytes, false, pane_id, None, None) .non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::Paste(bytes, pane_id, client_id, _completion) => { match pane_id { Some(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.paste_to_pane_id(bytes, pane_id, _completion) .non_fatal(); break; } } }, None => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| { tab.paste_to_active_terminal(bytes, client_id, _completion) .non_fatal(); } ); }, } screen.render(None)?; }, ScreenInstruction::SetPaneColor(pane_id, fg, bg, _completion) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_pane_color(pane_id, fg, bg).non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::WriteKeyToPaneId( key_with_modifier, bytes, is_kitty, pane_id, _completion, ) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.write_to_pane_id( &key_with_modifier, bytes, is_kitty, pane_id, None, // client_id not needed for targeted write None, // completion handled by instruction ) .non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::CopyTextToClipboard(text, plugin_id) => { let plugin_pane_id = PaneId::Plugin(plugin_id); let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&plugin_pane_id) { tab.copy_text_to_clipboard(&text) .with_context(|| { format!( "failed to copy text to clipboard from plugin {}", plugin_id ) }) .non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::MovePaneWithPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.move_pane(pane_id); break; } } screen.render(None)?; }, ScreenInstruction::MovePaneWithPaneIdInDirection(pane_id, direction) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { match direction { Direction::Down => tab.move_pane_down(pane_id), Direction::Up => tab.move_pane_up(pane_id), Direction::Left => tab.move_pane_left(pane_id), Direction::Right => tab.move_pane_right(pane_id), } break; } } screen.render(None)?; }, ScreenInstruction::ClearScreenForPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_screen_for_pane_id(pane_id); break; } } screen.render(None)?; }, ScreenInstruction::ScrollUpInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_up(terminal_pane_id)?; } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!( "Currently only terminal panes are supported for scrolling up" ); } break; } } screen.render(None)?; }, ScreenInstruction::ScrollDownInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_down(terminal_pane_id)?; } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!( "Currently only terminal panes are supported for scrolling down" ); } break; } } screen.render(None)?; }, ScreenInstruction::ScrollToTopInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_to_top(terminal_pane_id); } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!( "Currently only terminal panes are supported for scrolling to top" ); } break; } } screen.render(None)?; }, ScreenInstruction::ScrollToBottomInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_to_bottom(terminal_pane_id); } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!("Currently only terminal panes are supported for scrolling to bottom"); } break; } } screen.render(None)?; }, ScreenInstruction::PageScrollUpInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_page_up(terminal_pane_id); } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!( "Currently only terminal panes are supported for scrolling" ); } break; } } screen.render(None)?; }, ScreenInstruction::PageScrollDownInPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if let PaneId::Terminal(terminal_pane_id) = pane_id { tab.scroll_terminal_page_down(terminal_pane_id); } else { // this is because to do this with plugins, we need the client_id - // which we do not have (yet?) in this context... log::error!( "Currently only terminal panes are supported for scrolling" ); } break; } } screen.render(None)?; }, ScreenInstruction::TogglePaneIdFullscreen(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_pane_fullscreen(pane_id); break; } } screen.render(None)?; }, ScreenInstruction::SetTabFit { client_id, tab_id, fit, } => { let changed = match fit { Some((pane_id, size)) => { screen.set_tab_fit(client_id, tab_id, pane_id, size)?; true }, None => screen.exit_fit_mode(client_id)?, }; if changed { screen.render(None)?; } }, ScreenInstruction::TogglePaneEmbedOrEjectForPaneId(pane_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_pane_embed_or_floating_for_pane_id(pane_id, None) .non_fatal(); break; } } screen.render(None)?; }, ScreenInstruction::CloseTabWithIndex(tab_index) => { // tab_index here means display position (0-based) per API convention // Must map to stable ID for close_tab_by_id() if let Some(tab_id) = screen.get_tab_id_at_position(tab_index) { screen.close_tab_by_id(tab_id).non_fatal(); } else { log::error!("Failed to find tab at position: {}", tab_index); } }, ScreenInstruction::BreakPanesToNewTab { pane_ids, default_shell, should_change_focus_to_new_tab, new_tab_name, client_id, mut completion_tx, } => { let tab_id = screen.break_multiple_panes_to_new_tab( pane_ids, default_shell, should_change_focus_to_new_tab, new_tab_name, client_id, )?; // Set affected tab ID for plugin API return value completion_tx .as_mut() .map(|c| c.set_affected_tab_id(tab_id)); // TODO: is this a race? let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( pane_group.iter().copied().collect(), "BROKEN OUT".to_owned(), ), ); } screen.clear_pane_group(&client_id); }, ScreenInstruction::BreakPanesToTabWithIndex { pane_ids, tab_index, should_change_focus_to_new_tab, client_id, mut completion_tx, } => { // tab_index is the target tab ID screen.break_multiple_panes_to_tab_with_index( pane_ids, tab_index, should_change_focus_to_new_tab, client_id, )?; // Set affected tab ID (tab_index is the ID here) completion_tx .as_mut() .map(|c| c.set_affected_tab_id(tab_index)); let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( pane_group.iter().copied().collect(), "BROKEN OUT".to_owned(), ), ); } screen.clear_pane_group(&client_id); }, ScreenInstruction::TogglePanePinned( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.toggle_pane_pinned(client_id); }, ScreenInstruction::SetFloatingPanePinned(pane_id, should_be_pinned) => { screen.set_floating_pane_pinned(pane_id, should_be_pinned); }, ScreenInstruction::StackPanes( pane_ids_to_stack, client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { if let Some(last_pane_id) = screen.stack_panes(pane_ids_to_stack) { let _ = screen.focus_pane_with_id(last_pane_id, false, false, client_id); let _ = screen.render(None); let pane_group = screen.get_client_pane_group(&client_id); if !pane_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( pane_group.iter().copied().collect(), "STACKED".to_owned(), ), ); } screen.clear_pane_group(&client_id); } }, ScreenInstruction::ChangeFloatingPanesCoordinates( pane_ids_and_coordinates, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.change_floating_panes_coordinates(pane_ids_and_coordinates); let _ = screen.render(None); }, ScreenInstruction::TogglePaneBorderless(pane_id, _completion_tx) => { screen.toggle_pane_borderless(pane_id); let _ = screen.render(None); }, ScreenInstruction::SetPaneBorderless(pane_id, borderless, _completion_tx) => { screen.set_pane_borderless(pane_id, borderless); let _ = screen.render(None); }, ScreenInstruction::GroupAndUngroupPanes( pane_ids_to_group, pane_ids_to_ungroup, for_all_clients, client_id, ) => { screen.group_and_ungroup_panes( pane_ids_to_group, pane_ids_to_ungroup, for_all_clients, client_id, ); let _ = screen.log_and_report_session_state(); }, ScreenInstruction::TogglePaneInGroup( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.toggle_pane_in_group(client_id).non_fatal(); }, ScreenInstruction::ToggleGroupMarking( client_id, _completion_tx, // the action ends here, dropping this will release anything // waiting for it ) => { screen.toggle_group_marking(client_id).non_fatal(); }, ScreenInstruction::SessionSharingStatusChange(web_sharing) => { if web_sharing { screen.web_sharing = WebSharing::On; } else { screen.web_sharing = WebSharing::Off; } for tab in screen.tabs.values_mut() { tab.update_web_sharing(screen.web_sharing); } let _ = screen.log_and_report_session_state(); let _ = screen.render(None); }, ScreenInstruction::HighlightAndUnhighlightPanes( pane_ids_to_highlight, pane_ids_to_unhighlight, client_id, ) => { { let all_tabs = screen.get_tabs_mut(); for pane_id in pane_ids_to_highlight { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.add_highlight_pane_frame_color_override( pane_id, None, Some(client_id), ); } } } for pane_id in pane_ids_to_unhighlight { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_pane_frame_color_override(pane_id, Some(client_id)); } } } screen.render(None)?; } let _ = screen.log_and_report_session_state(); }, ScreenInstruction::FloatMultiplePanes(pane_ids_to_float, client_id) => { { let all_tabs = screen.get_tabs_mut(); let mut ejected_panes_in_group = vec![]; for pane_id in pane_ids_to_float { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if !tab.pane_id_is_floating(&pane_id) { ejected_panes_in_group.push(pane_id); tab.toggle_pane_embed_or_floating_for_pane_id( pane_id, Some(client_id), ) .non_fatal(); } tab.show_floating_panes(); } } } screen.render(None)?; if !ejected_panes_in_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( ejected_panes_in_group, "EJECTED".to_owned(), ), ); } } let _ = screen.log_and_report_session_state(); }, ScreenInstruction::EmbedMultiplePanes(pane_ids_to_float, client_id) => { { let all_tabs = screen.get_tabs_mut(); let mut embedded_panes_in_group = vec![]; for pane_id in pane_ids_to_float { for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if tab.pane_id_is_floating(&pane_id) { embedded_panes_in_group.push(pane_id); tab.toggle_pane_embed_or_floating_for_pane_id( pane_id, Some(client_id), ) .non_fatal(); } tab.hide_floating_panes(); } } } screen.render(None)?; if !embedded_panes_in_group.is_empty() { let _ = screen.bus.senders.send_to_background_jobs( BackgroundJob::HighlightPanesWithMessage( embedded_panes_in_group, "EMBEDDED".to_owned(), ), ); } } let _ = screen.log_and_report_session_state(); }, ScreenInstruction::InterceptKeyPresses(plugin_id, client_id) => { keybind_intercepts.insert(client_id, plugin_id); }, ScreenInstruction::ClearKeyPressesIntercepts(client_id) => { keybind_intercepts.remove(&client_id); }, ScreenInstruction::ReplacePaneWithExistingPane( old_pane_id, new_pane_id, suppress_replaced_pane, completion_tx, ) => screen.replace_pane_with_existing_pane( old_pane_id, new_pane_id, suppress_replaced_pane, completion_tx, ), ScreenInstruction::AddWatcherClient(client_id, size) => { screen .add_watcher_client(client_id) .context("failed to add watcher client")?; screen.set_watcher_size(client_id, size); screen.render(None)?; }, ScreenInstruction::RemoveWatcherClient(client_id) => { screen.remove_watcher_client(client_id); }, ScreenInstruction::SetFollowedClient(client_id) => { screen .set_followed_client(client_id) .context("failed to set followed client")?; }, ScreenInstruction::WatcherTerminalResize(client_id, size) => { screen.set_watcher_size(client_id, size); screen.render(None)?; }, ScreenInstruction::ClearMouseHelpText(client_id) => { if let Ok(tab) = screen.get_active_tab_mut(client_id) { tab.clear_mouse_help_text(client_id); screen.render(None)?; } }, ScreenInstruction::SetPluginRegexHighlights { pane_id, plugin_id, highlights, } => { let style = screen.style.clone(); let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.set_plugin_regex_highlights_for_pane( pane_id, plugin_id, highlights, &style, ); break; } } screen.render(None)?; }, ScreenInstruction::ClearPluginHighlights { pane_id, plugin_id } => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_plugin_highlights_for_pane(pane_id, plugin_id); break; } } screen.render(None)?; }, ScreenInstruction::ClearAllPluginHighlights(plugin_id) => { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { tab.clear_all_plugin_highlights(plugin_id); } screen.render(None)?; }, ScreenInstruction::DesktopNotificationResponse(raw_bytes, client_id) => { if let Some((terminal_id, app_wants_report, is_query, rewritten_bytes)) = denormalize_notification_response(&raw_bytes) { let pane_id = PaneId::Terminal(terminal_id); // Write response to the pane if the app expects it: // capability query answers (q flag) or activation reports (r flag) if app_wants_report || is_query { let all_tabs = screen.get_tabs_mut(); for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.write_to_pane_id( &None, rewritten_bytes, false, pane_id, None, None, ) .non_fatal(); break; } } } // Focus the pane on activation click (not on query responses) if !is_query { screen .focus_pane_with_id(pane_id, false, false, client_id) .non_fatal(); } screen.render(None)?; screen.log_and_report_session_state()?; } }, ScreenInstruction::SubscribeToPaneRenders { client_id, pane_ids, scrollback, ansi, } => { screen.subscribe_to_pane_renders(client_id, pane_ids, scrollback, ansi); }, ScreenInstruction::NotifyPaneClosedToSubscribers { pane_id } => { screen.notify_pane_closed_to_subscribers(pane_id); screen.clear_fit_for_closed_pane(pane_id.into())?; }, ScreenInstruction::PluginSubscribedToAnsiPaneContents(has_subscribers) => { let previously_subscribed = screen.plugins_need_ansi_pane_contents; screen.plugins_need_ansi_pane_contents = has_subscribers; if has_subscribers && !previously_subscribed { if let Err(e) = screen.render(None) { log::error!("Failed to render after ANSI subscription enabled: {:?}", e); } } }, ScreenInstruction::UpdateBackgroundPluginSubscriptions( plugin_id, client_id, subscriptions, ) => { if subscriptions.is_empty() { screen .background_plugin_subscriptions .remove(&(plugin_id, client_id)); } else { screen .background_plugin_subscriptions .insert((plugin_id, client_id), subscriptions); } }, ScreenInstruction::ClearHintTextCache => { for tab in screen.tabs.values_mut() { tab.clear_hint_text_cache(); } screen.render(None)?; }, ScreenInstruction::BroadcastModeUpdate(mode_info, target_client_id) => { screen.broadcast_mode_update(mode_info, target_client_id)?; }, // Pane-targeting CLI handlers ScreenInstruction::ScrollUpWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.scroll_up_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::ScrollDownWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.scroll_down_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::ScrollToTopWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.scroll_to_top_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::ScrollToBottomWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.scroll_to_bottom_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::PageScrollUpWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.page_scroll_up_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::PageScrollDownWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.page_scroll_down_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::HalfPageScrollUpWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.half_page_scroll_up_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::HalfPageScrollDownWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.half_page_scroll_down_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::ResizeWithPaneId(pane_id, strategy, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.resize_by_pane_id(pane_id, strategy); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneWithPaneIdCli(pane_id, direction, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.move_pane_by_pane_id(pane_id, direction); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneBackwardsWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.move_pane_backwards_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ClearScreenWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.clear_screen_by_pane_id(pane_id).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::EditScrollbackWithPaneId(pane_id, ansi, completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { if ansi { tab.edit_scrollback_raw_for_pane_with_id(pane_id, completion_tx) .non_fatal(); } else { tab.edit_scrollback_for_pane_with_id(pane_id, completion_tx) .non_fatal(); } found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); } screen.render(None)?; }, ScreenInstruction::ToggleFullscreenWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_fullscreen_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ToggleNoUiFullscreenWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_no_ui_fullscreen_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::TogglePaneEmbedOrFloatingWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_pane_embed_or_floating_for_pane_id(pane_id, None) .non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::CloseFocusWithPaneId(pane_id, completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.close_pane_by_pane_id(pane_id, completion_tx) .non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::RenamePaneWithPaneId(pane_id, name, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.rename_pane_by_pane_id(pane_id, name).non_fatal(); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::UndoRenamePaneWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.undo_rename_pane_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } screen.render(None)?; }, ScreenInstruction::TogglePanePinnedWithPaneId(pane_id, mut _completion_tx) => { let all_tabs = screen.get_tabs_mut(); let mut found = false; for tab in all_tabs.values_mut() { if tab.has_pane_with_pid(&pane_id) { tab.toggle_pane_pinned_by_pane_id(pane_id); found = true; break; } } if !found { log::error!("Pane with id {:?} not found", pane_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Pane with id {:?} not found", pane_id)); } } }, // Tab-targeting CLI handlers ScreenInstruction::UndoRenameTabWithTabId(tab_id, mut _completion_tx) => { if let Some(tab) = screen.tabs.get_mut(&tab_id) { if tab.name != tab.prev_name { tab.name = tab.prev_name.clone(); screen.log_and_report_session_state()?; } } else { log::error!("Tab with id {} not found", tab_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Tab with id {} not found", tab_id)); } } screen.render(None)?; }, ScreenInstruction::ToggleActiveSyncTabWithTabId(tab_id, mut _completion_tx) => { if let Some(tab) = screen.tabs.get_mut(&tab_id) { tab.toggle_sync_panes_is_active(); } else { log::error!("Tab with id {} not found", tab_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Tab with id {} not found", tab_id)); } } screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::ToggleFloatingPanesWithTabId( tab_id, default_shell, mut completion_tx, ) => { if let Some(tab) = screen.tabs.get_mut(&tab_id) { // Pass None as client_id so that if a new floating pane must be spawned, // it targets this tab (via TabIndex) rather than the focused tab of some client. tab.toggle_floating_panes(None, default_shell, completion_tx) .non_fatal(); } else { log::error!("Tab with id {} not found", tab_id); if let Some(ref mut c) = completion_tx { c.set_exit_status(1); c.set_error_message(format!("Tab with id {} not found", tab_id)); } drop(completion_tx); } screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::PreviousSwapLayoutWithTabId(tab_id, mut _completion_tx) => { if let Some(tab) = screen.tabs.get_mut(&tab_id) { tab.previous_swap_layout().non_fatal(); } else { log::error!("Tab with id {} not found", tab_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Tab with id {} not found", tab_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::NextSwapLayoutWithTabId(tab_id, mut _completion_tx) => { if let Some(tab) = screen.tabs.get_mut(&tab_id) { tab.next_swap_layout().non_fatal(); } else { log::error!("Tab with id {} not found", tab_id); if let Some(ref mut c) = _completion_tx { c.set_exit_status(1); c.set_error_message(format!("Tab with id {} not found", tab_id)); } } screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveTabWithTabId(tab_id, direction, _completion_tx) => { if pending_tab_ids.is_empty() { screen.move_tab_by_id(tab_id, direction)?; screen.render(None)?; } else { pending_events_waiting_for_tab.push(ScreenInstruction::MoveTabWithTabId( tab_id, direction, _completion_tx, )); } }, ScreenInstruction::EnterMobileMode(client_id, _completion_tx) => { if let Err(e) = screen.enter_mobile_mode(client_id) { screen.mobile_render_gate.ungate(client_id); screen.render(None)?; return Err(e); } screen.mobile_state.mark_auto_entered(client_id); screen.render(None)?; }, ScreenInstruction::ExitMobileMode(client_id, _completion_tx) => { screen.exit_mobile_mode(client_id)?; screen.render(None)?; }, ScreenInstruction::ToggleMobileMode(client_id, _completion_tx) => { screen.toggle_mobile_mode(client_id)?; screen.render(None)?; }, ScreenInstruction::ReevaluateMobileMode { client_id, new_size, mobile_layout, threshold_cols, threshold_rows, } => { screen.reevaluate_mobile_mode( client_id, new_size, mobile_layout, threshold_cols, threshold_rows, )?; }, ScreenInstruction::SetSoftKeyboard { client_id, on } => { if let Some(os_input) = &screen.bus.os_input { let _ = os_input .send_to_client(client_id, ServerToClientMsg::SetSoftKeyboard { on }); } }, ScreenInstruction::SetShadowFocus(client_id, pane_id) => { screen.set_shadow_focus(client_id, pane_id)?; }, ScreenInstruction::FocusHostSession(client_id, _completion_tx) => { let payload = nested_session::encode_payload(&NestedSessionMessage::FocusHost { direction: None, }); let _ = screen.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(client_id, payload), ); }, ScreenInstruction::FocusGuestSession(client_id, _completion_tx) => { screen.focus_guest_session(client_id); }, ScreenInstruction::ToggleHostFullscreen(client_id, _completion_tx) => { screen.own_fullscreen_requested = !screen.own_fullscreen_requested; let fullscreen = screen.own_fullscreen_requested; let payload = nested_session::encode_payload(&NestedSessionMessage::ToggleHostFullscreen { fullscreen, }); let _ = screen.bus.senders.send_to_server( ServerInstruction::EmitNestedSessionFrameToClient(client_id, payload), ); }, } } Ok(()) } #[path = "./unit/screen_tests.rs"] #[cfg(test)] mod screen_tests;