//! Things related to [`Screen`]s. use std::cell::RefCell; use std::collections::{BTreeMap, HashMap, HashSet}; use std::path::PathBuf; use std::rc::Rc; use std::str; use std::time::Duration; use log::{debug, warn}; use zellij_utils::data::{ Direction, KeyWithModifier, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::command::RunCommand; use zellij_utils::input::keybinds::Keybinds; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::{ consts::{session_info_folder_for_session, ZELLIJ_SOCK_DIR}, envs::set_session_name, input::command::TerminalAction, input::layout::{ FloatingPaneLayout, Layout, Run, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, position::Position, }; use crate::background_jobs::BackgroundJob; use crate::os_input_output::ResizeCache; use crate::panes::alacritty_functions::xparse_color; use crate::panes::terminal_character::AnsiCode; use crate::session_layout_metadata::{PaneLayoutMetadata, SessionLayoutMetadata}; use crate::{ output::Output, panes::sixel::SixelImageStore, panes::PaneId, plugins::{PluginId, PluginInstruction, PluginRenderAsset}, pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, tab::Tab, thread_bus::Bus, ui::{ loading_indication::LoadingIndication, overlay::{Overlay, OverlayWindow}, }, ClientId, ServerInstruction, }; use zellij_utils::{ data::{ Event, FloatingPaneCoordinates, InputMode, ModeInfo, Palette, PaletteColor, PluginCapabilities, Style, TabInfo, }, errors::{ContextType, ScreenContext}, input::{get_mode_info, options::Options}, ipc::{ClientAttributes, PixelDimensions, ServerToClientMsg}, }; /// 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"); }; }, } }; } type InitialTitle = String; type ShouldFloat = bool; type HoldForCommand = Option; /// Instructions that can be sent to the [`Screen`]. #[derive(Debug, Clone)] pub enum ScreenInstruction { PtyBytes(u32, VteBytes), PluginBytes(Vec), Render, NewPane( PaneId, Option, Option, HoldForCommand, Option, // invoked with Option, bool, // start suppressed ClientTabIndexOrPaneId, ), OpenInPlaceEditor(PaneId, ClientId), TogglePaneEmbedOrFloating(ClientId), ToggleFloatingPanes(ClientId, Option), HorizontalSplit(PaneId, Option, HoldForCommand, ClientId), VerticalSplit(PaneId, Option, HoldForCommand, ClientId), WriteCharacter(Option, Vec, bool, ClientId), // bool -> // is_kitty_keyboard_protocol Resize(ClientId, ResizeStrategy), SwitchFocus(ClientId), FocusNextPane(ClientId), FocusPreviousPane(ClientId), MoveFocusLeft(ClientId), MoveFocusLeftOrPreviousTab(ClientId), MoveFocusDown(ClientId), MoveFocusUp(ClientId), MoveFocusRight(ClientId), MoveFocusRightOrNextTab(ClientId), MovePane(ClientId), MovePaneBackwards(ClientId), MovePaneUp(ClientId), MovePaneDown(ClientId), MovePaneRight(ClientId), MovePaneLeft(ClientId), Exit, ClearScreen(ClientId), DumpScreen(String, ClientId, bool), DumpLayout(Option, ClientId), // PathBuf is the default configured // shell DumpLayoutToPlugin(PluginId), EditScrollback(ClientId), ScrollUp(ClientId), ScrollUpAt(Position, ClientId), ScrollDown(ClientId), ScrollDownAt(Position, ClientId), ScrollToBottom(ClientId), ScrollToTop(ClientId), PageScrollUp(ClientId), PageScrollDown(ClientId), HalfPageScrollUp(ClientId), HalfPageScrollDown(ClientId), ClearScroll(ClientId), CloseFocusedPane(ClientId), ToggleActiveTerminalFullscreen(ClientId), TogglePaneFrames, SetSelectable(PaneId, bool, usize), ClosePane(PaneId, Option), HoldPane( PaneId, Option, RunCommand, Option, Option, ), // Option is the exit status, Option is the tab_index UpdatePaneName(Vec, ClientId), UndoRenamePane(ClientId), NewTab( Option, Option, Option, Vec, Option, (Vec, Vec), // swap layouts ClientId, ), ApplyLayout( TiledPaneLayout, Vec, Vec<(u32, HoldForCommand)>, // new pane pids Vec<(u32, HoldForCommand)>, // new floating pane pids HashMap>, usize, // tab_index ClientId, ), SwitchTabNext(ClientId), SwitchTabPrev(ClientId), ToggleActiveSyncTab(ClientId), CloseTab(ClientId), GoToTab(u32, Option), // this Option is a hacky workaround, please do not copy this behaviour GoToTabName( String, (Vec, Vec), // swap layouts Option, // default_shell bool, Option, ), ToggleTab(ClientId), UpdateTabName(Vec, ClientId), UndoRenameTab(ClientId), MoveTabLeft(ClientId), MoveTabRight(ClientId), TerminalResize(Size), TerminalPixelDimensions(PixelDimensions), TerminalBackgroundColor(String), TerminalForegroundColor(String), TerminalColorRegisters(Vec<(usize, String)>), ChangeMode(ModeInfo, ClientId), ChangeModeForAllClients(ModeInfo), LeftClick(Position, ClientId), RightClick(Position, ClientId), MiddleClick(Position, ClientId), LeftMouseRelease(Position, ClientId), RightMouseRelease(Position, ClientId), MiddleMouseRelease(Position, ClientId), MouseHoldLeft(Position, ClientId), MouseHoldRight(Position, ClientId), MouseHoldMiddle(Position, ClientId), Copy(ClientId), AddClient( ClientId, Option, // tab position to focus Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus ), RemoveClient(ClientId), AddOverlay(Overlay, ClientId), RemoveOverlay(ClientId), ConfirmPrompt(ClientId), DenyPrompt(ClientId), UpdateSearch(Vec, ClientId), SearchDown(ClientId), SearchUp(ClientId), SearchToggleCaseSensitivity(ClientId), SearchToggleWholeWord(ClientId), SearchToggleWrap(ClientId), AddRedPaneFrameColorOverride(Vec, Option), // Option => optional error text ClearPaneFrameColorOverride(Vec), PreviousSwapLayout(ClientId), NextSwapLayout(ClientId), QueryTabNames(ClientId), NewTiledPluginPane( RunPluginOrAlias, Option, bool, Option, ClientId, ), // Option is // optional pane title, bool is skip cache, Option is an optional cwd NewFloatingPluginPane( RunPluginOrAlias, Option, bool, Option, Option, ClientId, ), // Option is an // optional pane title, bool // is skip cache, Option is an optional cwd NewInPlacePluginPane(RunPluginOrAlias, Option, PaneId, bool, ClientId), // Option is an // optional pane title, bool is skip cache StartOrReloadPluginPane(RunPluginOrAlias, Option), AddPlugin( Option, // should_float bool, // should be opened in place RunPluginOrAlias, Option, // pane title Option, // tab index u32, // plugin id Option, Option, // cwd bool, // start suppressed Option, ), UpdatePluginLoadingStage(u32, LoadingIndication), // u32 - plugin_id StartPluginLoadingIndication(u32, LoadingIndication), // u32 - plugin_id ProgressPluginLoadingOffset(u32), // u32 - plugin id RequestStateUpdateForPlugins, LaunchOrFocusPlugin( RunPluginOrAlias, bool, bool, bool, Option, bool, ClientId, ), // bools are: should_float, move_to_focused_tab, should_open_in_place, Option is the pane id to replace, bool following it is skip_cache LaunchPlugin( RunPluginOrAlias, bool, bool, Option, bool, Option, ClientId, ), // bools are: should_float, should_open_in_place Option is the pane id to replace, Option is an optional cwd, bool after is skip_cache SuppressPane(PaneId, ClientId), // bool is should_float FocusPaneWithId(PaneId, bool, ClientId), // bool is should_float RenamePane(PaneId, Vec), RenameTab(usize, Vec), RequestPluginPermissions( u32, // u32 - plugin_id PluginPermission, ), BreakPane(Box, Option, ClientId), BreakPaneRight(ClientId), BreakPaneLeft(ClientId), UpdateSessionInfos( BTreeMap, // String is the session name BTreeMap, // resurrectable sessions - ), ReplacePane( PaneId, HoldForCommand, Option, Option, ClientTabIndexOrPaneId, ), DumpLayoutToHd, RenameSession(String, ClientId), // String -> new name ListClientsMetadata(Option, ClientId), // Option - default shell Reconfigure { client_id: ClientId, keybinds: Option, default_mode: Option, }, RerunCommandPane(u32), // u32 - terminal pane id } 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::NewPane(..) => ScreenContext::NewPane, ScreenInstruction::OpenInPlaceEditor(..) => ScreenContext::OpenInPlaceEditor, ScreenInstruction::TogglePaneEmbedOrFloating(..) => { ScreenContext::TogglePaneEmbedOrFloating }, ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes, ScreenInstruction::HorizontalSplit(..) => ScreenContext::HorizontalSplit, ScreenInstruction::VerticalSplit(..) => ScreenContext::VerticalSplit, 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::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::DumpLayoutToPlugin(..) => ScreenContext::DumpLayoutToPlugin, ScreenInstruction::EditScrollback(..) => ScreenContext::EditScrollback, 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::TogglePaneFrames => ScreenContext::TogglePaneFrames, ScreenInstruction::SetSelectable(..) => ScreenContext::SetSelectable, 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::TerminalResize(..) => ScreenContext::TerminalResize, ScreenInstruction::TerminalPixelDimensions(..) => { ScreenContext::TerminalPixelDimensions }, ScreenInstruction::TerminalBackgroundColor(..) => { ScreenContext::TerminalBackgroundColor }, ScreenInstruction::TerminalForegroundColor(..) => { ScreenContext::TerminalForegroundColor }, ScreenInstruction::TerminalColorRegisters(..) => ScreenContext::TerminalColorRegisters, ScreenInstruction::ChangeMode(..) => ScreenContext::ChangeMode, ScreenInstruction::ChangeModeForAllClients(..) => { ScreenContext::ChangeModeForAllClients }, ScreenInstruction::ToggleActiveSyncTab(..) => ScreenContext::ToggleActiveSyncTab, ScreenInstruction::ScrollUpAt(..) => ScreenContext::ScrollUpAt, ScreenInstruction::ScrollDownAt(..) => ScreenContext::ScrollDownAt, ScreenInstruction::LeftClick(..) => ScreenContext::LeftClick, ScreenInstruction::RightClick(..) => ScreenContext::RightClick, ScreenInstruction::MiddleClick(..) => ScreenContext::MiddleClick, ScreenInstruction::LeftMouseRelease(..) => ScreenContext::LeftMouseRelease, ScreenInstruction::RightMouseRelease(..) => ScreenContext::RightMouseRelease, ScreenInstruction::MiddleMouseRelease(..) => ScreenContext::MiddleMouseRelease, ScreenInstruction::MouseHoldLeft(..) => ScreenContext::MouseHoldLeft, ScreenInstruction::MouseHoldRight(..) => ScreenContext::MouseHoldRight, ScreenInstruction::MouseHoldMiddle(..) => ScreenContext::MouseHoldMiddle, ScreenInstruction::Copy(..) => ScreenContext::Copy, ScreenInstruction::ToggleTab(..) => ScreenContext::ToggleTab, ScreenInstruction::AddClient(..) => ScreenContext::AddClient, ScreenInstruction::RemoveClient(..) => ScreenContext::RemoveClient, ScreenInstruction::AddOverlay(..) => ScreenContext::AddOverlay, ScreenInstruction::RemoveOverlay(..) => ScreenContext::RemoveOverlay, ScreenInstruction::ConfirmPrompt(..) => ScreenContext::ConfirmPrompt, ScreenInstruction::DenyPrompt(..) => ScreenContext::DenyPrompt, 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::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout, ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout, 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::FocusPaneWithId(..) => ScreenContext::FocusPaneWithId, ScreenInstruction::RenamePane(..) => ScreenContext::RenamePane, 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::DumpLayoutToHd => ScreenContext::DumpLayoutToHd, ScreenInstruction::RenameSession(..) => ScreenContext::RenameSession, ScreenInstruction::ListClientsMetadata(..) => ScreenContext::ListClientsMetadata, ScreenInstruction::Reconfigure { .. } => ScreenContext::Reconfigure, ScreenInstruction::RerunCommandPane { .. } => ScreenContext::RerunCommandPane, } } } #[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, } } } /// 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, /// The full size of this [`Screen`]. size: Size, pixel_dimensions: PixelDimensions, character_cell_size: Rc>>, sixel_image_store: Rc>, /// The overlay that is drawn on top of [`Pane`]'s', [`Tab`]'s and the [`Screen`] overlay: OverlayWindow, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, connected_clients: Rc>>, /// The indices of this [`Screen`]'s active [`Tab`]s. active_tab_indices: BTreeMap, tab_history: BTreeMap>, mode_info: BTreeMap, default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication style: Style, draw_pane_frames: bool, 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, session_infos_on_machine: BTreeMap, // String is the session name, can // also be this session resurrectable_sessions: BTreeMap, // String is the session name, duration is // its creation time default_layout: Box, default_shell: Option, styled_underlines: bool, arrow_fonts: bool, layout_dir: Option, default_layout_name: Option, explicitly_disable_kitty_keyboard_protocol: bool, } impl Screen { /// Creates and returns a new [`Screen`]. pub fn new( bus: Bus, client_attributes: &ClientAttributes, max_panes: Option, mode_info: ModeInfo, draw_pane_frames: bool, auto_layout: bool, session_is_mirrored: bool, copy_options: CopyOptions, debug: bool, default_layout: Box, default_layout_name: Option, default_shell: Option, session_serialization: bool, serialize_pane_viewport: bool, scrollback_lines_to_serialize: Option, styled_underlines: bool, arrow_fonts: bool, layout_dir: Option, explicitly_disable_kitty_keyboard_protocol: bool, ) -> Self { let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_info = SessionInfo::new(session_name.clone()); let mut session_infos_on_machine = BTreeMap::new(); let resurrectable_sessions = BTreeMap::new(); session_infos_on_machine.insert(session_name.clone(), session_info); Screen { bus, max_panes, size: client_attributes.size, pixel_dimensions: Default::default(), character_cell_size: Rc::new(RefCell::new(None)), sixel_image_store: Rc::new(RefCell::new(SixelImageStore::default())), style: client_attributes.style, connected_clients: Rc::new(RefCell::new(HashSet::new())), active_tab_indices: BTreeMap::new(), tabs: BTreeMap::new(), overlay: OverlayWindow::default(), terminal_emulator_colors: Rc::new(RefCell::new(Palette::default())), terminal_emulator_color_codes: Rc::new(RefCell::new(HashMap::new())), tab_history: BTreeMap::new(), mode_info: BTreeMap::new(), default_mode_info: mode_info, draw_pane_frames, auto_layout, session_is_mirrored, copy_options, debug, session_name, session_infos_on_machine, default_layout, default_layout_name, default_shell, session_serialization, serialize_pane_viewport, scrollback_lines_to_serialize, styled_underlines, arrow_fonts, resurrectable_sessions, layout_dir, explicitly_disable_kitty_keyboard_protocol, } } /// Returns the index where a new [`Tab`] should be created in this [`Screen`]. /// Currently, this is right after the last currently existing tab, or `0` if /// no tabs exist in this screen yet. fn get_new_tab_index(&self) -> usize { if let Some(index) = self.tabs.keys().last() { *index + 1 } else { 0 } } 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_indices .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_indices.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_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(); 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_indices.remove(&client_id) { Some(old_active_index) => { self.active_tab_indices.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_indices.insert(client_id, new_tab_index); }, } } /// 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.index; let new_tab_index = new_tab.index; 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().copied().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(); } self.log_and_report_session_state() .with_context(err_context)?; 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) { 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 new_tab_pos = (active_tab_pos + 1) % self.tabs.len(); 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 new_tab_pos = if active_tab_pos == 0 { self.tabs.len() - 1 } else { active_tab_pos - 1 }; 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<()> { self.switch_active_tab(tab_index.saturating_sub(1), 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_at_index(&mut self, tab_index: usize) -> Result<()> { let err_context = || format!("failed to close tab at index {tab_index:?}"); let mut tab_to_close = self.tabs.remove(&tab_index).with_context(err_context)?; let pane_ids = tab_to_close.get_all_pane_ids(); // 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_indices.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)?; let visible_tab_indices: HashSet = self.active_tab_indices.values().copied().collect(); for t in self.tabs.values_mut() { if visible_tab_indices.contains(&t.index) { 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_indices .get(&client_id) .with_context(err_context)?; self.close_tab_at_index(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 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_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); } } /// Renders this [`Screen`], which amounts to rendering its active [`Tab`]. pub fn render(&mut self, plugin_render_assets: Option>) -> Result<()> { let err_context = "failed to render screen"; let mut output = Output::new( self.sixel_image_store.clone(), self.character_cell_size.clone(), self.styled_underlines, ); let mut tabs_to_close = vec![]; for (tab_index, tab) in &mut self.tabs { if tab.has_selectable_tiled_panes() { tab.render(&mut output).context(err_context)?; } else if !tab.is_pending() { tabs_to_close.push(*tab_index); } } for tab_index in tabs_to_close { self.close_tab_at_index(tab_index).context(err_context)?; } if output.is_dirty() { let serialized_output = output.serialize().context(err_context)?; let _ = self .bus .senders .send_to_server(ServerInstruction::Render(Some(serialized_output))) .context(err_context); } 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(()) } /// Returns a mutable reference to this [`Screen`]'s tabs. pub fn get_tabs_mut(&mut self) -> &mut BTreeMap { &mut 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_indices.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_first_client_id(&self) -> Option { self.active_tab_indices.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_indices.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 active [`Overlays`]. pub fn get_active_overlays_mut(&mut self) -> &mut Vec { &mut self.overlay.overlay_stack } /// 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) } /// Creates a new [`Tab`] in this [`Screen`] pub fn new_tab( &mut self, tab_index: usize, swap_layouts: (Vec, Vec), tab_name: Option, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to create new tab for client {client_id:?}",); let 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 position = self.tabs.len(); let mut tab = Tab::new( tab_index, position, tab_name, self.size, self.character_cell_size.clone(), self.sixel_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.draw_pane_frames, 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.explicitly_disable_kitty_keyboard_protocol, ); for (client_id, mode_info) in &self.mode_info { tab.change_mode_info(mode_info.clone(), *client_id); } self.tabs.insert(tab_index, 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_index: usize, client_id: ClientId, ) -> Result<()> { if self.tabs.get(&tab_index).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_index} not found. Cannot apply layout!"); return Ok(()); } let 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 err_context = || format!("failed to apply layout for tab {tab_index:?}",); // move the relevant clients out of the current tab and place them in the new one let drained_clients = 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)?; } Some(client_mode_infos_in_source_tab) } else { None }; let all_connected_clients: Vec = self.connected_clients.borrow().iter().copied().collect(); for client_id in all_connected_clients { self.update_client_tab_focus(client_id, tab_index); } 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)?; } self.update_client_tab_focus(client_id, tab_index); Some(client_mode_info_in_source_tab) } else { None }; // apply the layout to the new tab self.tabs .get_mut(&tab_index) .context("couldn't find tab with index {tab_index}") .and_then(|tab| { tab.apply_layout( layout, floating_panes_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, client_id, )?; tab.update_input_modes()?; tab.visible(true)?; if let Some(drained_clients) = drained_clients { tab.add_multiple_clients(drained_clients)?; } Ok(()) }) .with_context(err_context)?; if !self.active_tab_indices.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).with_context(err_context)?; } self.log_and_report_session_state() .and_then(|_| self.render(None)) .with_context(err_context) } pub fn add_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = |tab_index| { format!("failed to attach client {client_id} to tab with index {tab_index}") }; 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_indices.iter().next() { *first_active_tab_index } 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_indices.insert(client_id, tab_index); self.connected_clients.borrow_mut().insert(client_id); 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)) } pub fn remove_client(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to remove client {client_id}"); for (_, 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 self.active_tab_indices.contains_key(&client_id) { self.active_tab_indices.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.log_and_report_session_state() .with_context(err_context) } 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 all_focused_clients: Vec = self .active_tab_indices .iter() .filter(|(_c_id, tab_position)| **tab_position == tab.index) .map(|(c_id, _)| c_id) .copied() .collect(); let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let tab_info_for_screen = TabInfo { position: tab.position, name: tab.name.clone(), active: self.active_tab_indices.values().any(|i| i == &tab.index), 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, }; tab_infos_for_screen_state.insert(tab.position, tab_info_for_screen); } for (client_id, active_tab_index) in self.active_tab_indices.iter() { let mut plugin_tab_updates = vec![]; for tab in self.tabs.values() { let other_focused_clients: Vec = if self.session_is_mirrored { vec![] } else { self.active_tab_indices .iter() .filter(|(c_id, tab_position)| { **tab_position == tab.index && *c_id != client_id }) .map(|(c_id, _)| c_id) .copied() .collect() }; let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let tab_info_for_plugins = TabInfo { position: tab.position, name: tab.name.clone(), active: *active_tab_index == tab.index, 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, }; plugin_tab_updates.push(tab_info_for_plugins); } plugin_updates.push((None, Some(*client_id), Event::TabUpdate(plugin_tab_updates))); } 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()); } self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::PaneUpdate(pane_manifest.clone()), )])) .context("failed to update tabs")?; Ok(pane_manifest) } fn log_and_report_session_state(&mut self) -> Result<()> { let err_context = || format!("Failed to log and report session state"); // generate own session info let pane_manifest = self.generate_and_report_pane_state()?; let tab_infos = self.generate_and_report_tab_state()?; // in the context of unit/integration tests, we don't need to list available layouts // because this is mostly about HD access - it does however throw off the timing in the // tests and causes them to flake, which is why we skip it here #[cfg(not(test))] let available_layouts = Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name); #[cfg(test)] let available_layouts = vec![]; let session_info = SessionInfo { name: self.session_name.clone(), tabs: tab_infos, panes: pane_manifest, connected_clients: self.active_tab_indices.keys().len(), is_current_session: true, available_layouts, }; self.bus .senders .send_to_background_jobs(BackgroundJob::ReportSessionInfo( self.session_name.to_owned(), session_info, )) .with_context(err_context)?; self.bus .senders .send_to_background_jobs(BackgroundJob::ReadAllSessionInfosOnMachine) .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(self.default_shell.clone()); 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.session_infos_on_machine = new_session_infos; self.resurrectable_sessions = resurrectable_sessions; self.bus .senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::SessionUpdate( self.session_infos_on_machine.values().cloned().collect(), self.resurrectable_sessions .iter() .map(|(n, c)| (n.clone(), c.clone())) .collect(), ), )])) .context("failed to update session info")?; Ok(()) } 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 => { // It only allows printable unicode if buf.iter().all(|u| matches!(u, 0x20..=0x7E | 0xA0..=0xFF)) { active_tab.name.push_str(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; let left_tab_pos = if active_tab_pos == 0 { self.tabs.len() - 1 } else { active_tab_pos - 1 }; self.switch_tabs(active_tab_pos, left_tab_pos, client_id); 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() } } fn switch_tabs(&mut self, active_tab_pos: usize, other_tab_pos: usize, client_id: u16) { let Some(active_tab_idx) = self .tabs .values() .find(|t| t.position == active_tab_pos) .map(|t| t.index) else { log::error!("Failed to find active tab at position: {}", active_tab_pos); return; }; let Some(other_tab_idx) = self .tabs .values() .find(|t| t.position == other_tab_pos) .map(|t| t.index) else { log::error!( "Failed to find tab to switch to at position: {}", other_tab_pos ); return; }; if !self.tabs.contains_key(&active_tab_idx) || !self.tabs.contains_key(&other_tab_idx) { warn!( "failed to switch tabs: index {} or {} not found in {:?}", active_tab_idx, other_tab_idx, 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_idx) .expect("active tab not found"); let mut other_tab = self .tabs .remove(&other_tab_idx) .expect("other tab not found"); std::mem::swap(&mut active_tab.index, &mut other_tab.index); std::mem::swap(&mut active_tab.position, &mut other_tab.position); // now, `active_tab.index` is changed, so we need to update it self.active_tab_indices.insert(client_id, active_tab.index); self.tabs.insert(active_tab.index, active_tab); self.tabs.insert(other_tab.index, other_tab); } 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; let right_tab_pos = (active_tab_pos + 1) % self.tabs.len(); self.switch_tabs(active_tab_pos, right_tab_pos, client_id); 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 change_mode(&mut self, mut mode_info: ModeInfo, client_id: ClientId) -> Result<()> { if mode_info.session_name.as_ref() != Some(&self.session_name) { mode_info.session_name = Some(self.session_name.clone()); } let previous_mode = self .mode_info .get(&client_id) .unwrap_or(&self.default_mode_info) .mode; let err_context = || { format!( "failed to change from mode '{:?}' to mode '{:?}' for client {client_id}", previous_mode, mode_info.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()?; } Ok(()) } pub fn change_mode_for_all_clients(&mut self, mode_info: ModeInfo) -> Result<()> { let err_context = || { format!( "failed to change input mode to {:?} for all clients", mode_info.mode ) }; let connected_client_ids: Vec = self.active_tab_indices.keys().copied().collect(); for client_id in connected_client_ids { self.change_mode(mode_info.clone(), client_id) .with_context(err_context)?; } 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 { 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 { 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, client_id: ClientId, ) -> 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_indices.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, false, Some(client_id)); } 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, Some(client_id), )?; } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane( plugin_pane_to_move_to_active_tab, pane_id, Some(client_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, client_id) .context("failed to focus plugin pane")?; self.log_and_report_session_state() .with_context(err_context)?; Ok(true) }, None => Ok(false), } } pub fn focus_pane_with_id( &mut self, pane_id: PaneId, should_float_if_hidden: 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_index); match tab_index { Some(tab_index) => { self.go_to_tab(tab_index + 1, client_id)?; self.tabs .get_mut(&tab_index) .with_context(err_context)? .focus_pane_with_id(pane_id, should_float_if_hidden, client_id) .context("failed to focus pane with id")?; }, 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) { 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); found = true; break; } } if !found { log::error!( "Failed to find terminal pane with id: {} to run", terminal_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)?; if active_tab.get_selectable_tiled_panes_count() > 1 || active_tab.get_visible_selectable_floating_panes_count() > 0 { 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, Some(client_id)) .with_context(err_context)?; let active_pane_run_instruction = active_pane.invoked_with().clone(); let tab_index = self.get_new_tab_index(); let swap_layouts = ( default_layout.swap_tiled_layouts.clone(), default_layout.swap_floating_layouts.clone(), ); self.new_tab(tab_index, swap_layouts, None, client_id)?; let tab = self.tabs.get_mut(&tab_index).with_context(err_context)?; let (mut tiled_panes_layout, mut floating_panes_layout) = default_layout.new_tab(); if pane_to_break_is_floating { tab.show_floating_panes(); tab.add_floating_pane(active_pane, active_pane_id, None, Some(client_id))?; if let Some(already_running_layout) = floating_panes_layout .iter_mut() .find(|i| i.run == active_pane_run_instruction) { already_running_layout.already_running = true; } } else { tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?; tiled_panes_layout.ignore_run_instruction(active_pane_run_instruction.clone()); } self.bus.senders.send_to_plugin(PluginInstruction::NewTab( None, default_shell, Some(tiled_panes_layout), floating_panes_layout, tab_index, client_id, ))?; } else { let active_pane_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], "Cannot break single pane out!".into(), )) .with_context(err_context)?; self.unblock_input()?; } Ok(()) } 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, Some(client_id)) .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, Some(client_id), )?; } else { new_active_tab.hide_floating_panes(); new_active_tab.add_tiled_pane(active_pane, active_pane_id, 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.unblock_input()?; self.render(None)?; Ok(()) } pub fn replace_pane( &mut self, new_pane_id: PaneId, hold_for_command: HoldForCommand, run: Option, pane_title: Option, client_id_tab_index_or_pane_id: ClientTabIndexOrPaneId, ) -> Result<()> { let err_context = || format!("failed to replace pane"); 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, run); 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) => { 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_index); match tab_index { Some(tab_index) => { let tab = self.tabs.get_mut(&tab_index).with_context(err_context)?; suppress_pane(tab, pane_id, new_pane_id); }, None => { log::error!("Could not find pane with id: {:?}", pane_id); }, }; }, ClientTabIndexOrPaneId::TabIndex(_tab_index) => { log::error!("Cannot replace pane with tab index"); }, } Ok(()) } pub fn reconfigure_mode_info( &mut self, new_keybinds: Option, new_default_mode: Option, client_id: ClientId, ) -> Result<()> { if self.connected_clients_contains(&client_id) { let should_update_mode_info = new_keybinds.is_some() || new_default_mode.is_some(); let mode_info = self .mode_info .entry(client_id) .or_insert_with(|| self.default_mode_info.clone()); if let Some(new_keybinds) = new_keybinds { mode_info.update_keybinds(new_keybinds); } if let Some(new_default_mode) = new_default_mode { mode_info.update_default_mode(new_default_mode); } if should_update_mode_info { 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()?; } } } else { log::error!("Could not find client_id {client_id} to rebind keys"); } Ok(()) } fn unblock_input(&self) -> Result<()> { self.bus .senders .send_to_server(ServerInstruction::UnblockInputThread) .context("failed to unblock input") } fn get_layout_metadata(&self, default_shell: 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_indices.get(&client_id)); for (tab_index, tab) in self.tabs.values().enumerate() { 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() .copied() .filter(|c| self.active_tab_indices.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 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 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(); PaneLayoutMetadata::new( pane_id, p.position_and_size(), 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, ) }) .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(); 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, ) }) .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(client_id) } } // 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_options: Box, debug: bool, default_layout: Box, ) -> Result<()> { let arrow_fonts = !config_options.simplified_ui.unwrap_or_default(); let draw_pane_frames = config_options.pane_frames.unwrap_or(true); 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; let default_shell = config_options.default_shell; 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 styled_underlines = config_options.styled_underlines.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 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, }, &client_attributes.keybinds, config_options.default_mode, ), draw_pane_frames, 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, arrow_fonts, layout_dir, explicitly_disable_kitty_keyboard_protocol, ); 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 plugin_loading_message_cache = 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(); for tab in all_tabs.values_mut() { if tab.has_terminal_pid(pid) { tab.handle_pty_bytes(pid, vte_bytes) .context("failed to process pty bytes")?; break; } } }, 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 = plugin_render_asset.bytes.drain(..).collect(); 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")?; break; } } } screen.render(Some(plugin_render_assets))?; }, ScreenInstruction::Render => { screen.render(None)?; }, ScreenInstruction::NewPane( pid, initial_pane_title, should_float, hold_for_command, invoked_with, floating_pane_coordinates, start_suppressed, client_or_tab_index, ) => { match client_or_tab_index { ClientTabIndexOrPaneId::ClientId(client_id) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| { tab.new_pane(pid, initial_pane_title, should_float, invoked_with, floating_pane_coordinates, start_suppressed, Some(client_id) ) }, ?); if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( pid, None, is_first_run, hold_for_command ) ) } }, ClientTabIndexOrPaneId::TabIndex(tab_index) => { if let Some(active_tab) = screen.tabs.get_mut(&tab_index) { active_tab.new_pane( pid, initial_pane_title, should_float, invoked_with, floating_pane_coordinates, start_suppressed, None, )?; 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) => { log::error!("cannot open a pane with a pane id??"); }, }; screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::OpenInPlaceEditor(pid, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .replace_active_pane_with_editor_pane(pid, client_id), ?); screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_pane_embed_or_floating(client_id), ?); screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => { active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab .toggle_floating_panes(Some(client_id), default_shell), ?); screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::HorizontalSplit( pid, initial_pane_title, hold_for_command, client_id, ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.horizontal_split(pid, initial_pane_title, client_id), ? ); if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( pid, None, is_first_run, hold_for_command ) ); } screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::VerticalSplit( pid, initial_pane_title, hold_for_command, client_id, ) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.vertical_split(pid, initial_pane_title, client_id), ? ); if let Some(hold_for_command) = hold_for_command { let is_first_run = true; active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, _client_id: ClientId| tab.hold_pane( pid, None, is_first_run, hold_for_command ) ); } screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::WriteCharacter( key_with_modifier, raw_bytes, is_kitty_keyboard_protocol, client_id, ) => { let mut state_changed = false; active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| { 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; } write_result }, ? ); if state_changed { screen.log_and_report_session_state()?; } }, ScreenInstruction::Resize(client_id, strategy) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.resize(client_id, strategy), ? ); screen.unblock_input()?; screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::SwitchFocus(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) ); screen.unblock_input()?; screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::FocusNextPane(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_next_pane(client_id) ); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::FocusPreviousPane(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.focus_previous_pane(client_id) ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusLeft(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id), ? ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => { screen.move_focus_left_or_previous_tab(client_id)?; screen.unblock_input()?; screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusDown(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id), ? ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusRight(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id), ? ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusRightOrNextTab(client_id) => { screen.move_focus_right_or_next_tab(client_id)?; screen.unblock_input()?; screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::MoveFocusUp(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id), ? ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::ClearScreen(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::DumpScreen(file, client_id, full) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.dump_active_terminal_screen( Some(file.to_string()), client_id, full ), ? ); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::DumpLayout(default_shell, client_id) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(default_shell); screen .bus .senders .send_to_plugin(PluginInstruction::DumpLayout( session_layout_metadata, client_id, )) .with_context(err_context)?; }, ScreenInstruction::ListClientsMetadata(default_shell, client_id) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(default_shell); screen .bus .senders .send_to_plugin(PluginInstruction::ListClientsMetadata( session_layout_metadata, client_id, )) .with_context(err_context)?; }, ScreenInstruction::DumpLayoutToPlugin(plugin_id) => { let err_context = || format!("Failed to dump layout"); let session_layout_metadata = screen.get_layout_metadata(screen.default_shell.clone()); screen .bus .senders .send_to_pty(PtyInstruction::DumpLayoutToPlugin( session_layout_metadata, plugin_id, )) .with_context(err_context) .non_fatal(); }, ScreenInstruction::EditScrollback(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.edit_scrollback(client_id), ? ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ScrollUp(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.scroll_active_terminal_up(client_id) ); screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::MovePane(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneBackwards(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneDown(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneUp(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneRight(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::MovePaneLeft(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::ScrollUpAt(point, client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::ScrollDown(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::ScrollDownAt(point, client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::ScrollToBottom(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::ScrollToTop(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::PageScrollUp(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::PageScrollDown(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::HalfPageScrollUp(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::HalfPageScrollDown(client_id) => { 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)?; screen.unblock_input()?; }, 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)?; screen.unblock_input()?; }, ScreenInstruction::CloseFocusedPane(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ? ); screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::SetSelectable(id, selectable, tab_index) => { screen.get_indexed_tab_mut(tab_index).map_or_else( || { log::warn!( "Tab index #{} not found, could not set selectable for plugin #{:?}.", tab_index, id ) }, |tab| tab.set_pane_selectable(id, selectable), ); screen.render(None)?; screen.log_and_report_session_state()?; }, ScreenInstruction::ClosePane(id, client_id) => { match client_id { Some(client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab.close_pane( id, false, Some(client_id) )); }, None => { for tab in screen.tabs.values_mut() { if tab.get_all_pane_ids().contains(&id) { tab.close_pane(id, false, None); break; } } }, } screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::HoldPane(id, exit_status, run_command, tab_index, client_id) => { let is_first_run = false; match (client_id, tab_index) { (Some(client_id), _) => { active_tab!(screen, client_id, |tab: &mut Tab| tab.hold_pane( id, exit_status, is_first_run, run_command )); }, (_, Some(tab_index)) => match screen.tabs.get_mut(&tab_index) { Some(tab) => tab.hold_pane(id, exit_status, is_first_run, run_command), None => log::warn!( "Tab with index {tab_index} not found. Cannot hold pane with id {:?}", id ), }, _ => { 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); break; } } }, } screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::UpdatePaneName(c, client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::UndoRenamePane(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::ToggleActiveTerminalFullscreen(client_id) => { 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.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::TogglePaneFrames => { screen.draw_pane_frames = !screen.draw_pane_frames; for tab in screen.tabs.values_mut() { tab.set_pane_frames(screen.draw_pane_frames); } screen.render(None)?; screen.unblock_input()?; screen.log_and_report_session_state()?; }, ScreenInstruction::SwitchTabNext(client_id) => { screen.switch_tab_next(None, true, client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::SwitchTabPrev(client_id) => { screen.switch_tab_prev(None, true, client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::CloseTab(client_id) => { screen.close_tab(client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::NewTab( cwd, default_shell, layout, floating_panes_layout, tab_name, swap_layouts, client_id, ) => { let tab_index = screen.get_new_tab_index(); pending_tab_ids.insert(tab_index); screen.new_tab(tab_index, swap_layouts, tab_name.clone(), client_id)?; screen .bus .senders .send_to_plugin(PluginInstruction::NewTab( cwd, default_shell, layout, floating_panes_layout, tab_index, client_id, ))?; }, ScreenInstruction::ApplyLayout( layout, floating_panes_layout, new_pane_pids, new_floating_pane_pids, new_plugin_ids, tab_index, client_id, ) => { screen.apply_layout( layout, floating_panes_layout, new_pane_pids.clone(), new_floating_pane_pids, new_plugin_ids.clone(), tab_index, client_id, )?; pending_tab_ids.remove(&tab_index); if pending_tab_ids.is_empty() { for (tab_index, client_id) in pending_tab_switches.drain() { screen.go_to_tab(tab_index as usize, client_id)?; } } 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.unblock_input()?; 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 in screen.connected_clients.borrow().iter() { let _ = os_input .send_to_client(*client_id, ServerToClientMsg::QueryTerminalSize); } } }, ScreenInstruction::GoToTab(tab_index, client_id) => { let client_id_to_switch = if client_id.is_none() { None } else if screen .active_tab_indices .contains_key(&client_id.expect("This is checked above")) { client_id } else { screen.active_tab_indices.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.unblock_input()?; screen.render(None)?; }, _ => { if let Some(client_id) = client_id { pending_tab_switches.insert((tab_index as usize, client_id)); } }, } }, ScreenInstruction::GoToTabName( tab_name, swap_layouts, default_shell, create, client_id, ) => { let client_id = if client_id.is_none() { None } else if screen .active_tab_indices .contains_key(&client_id.expect("This is checked above")) { client_id } else { screen.active_tab_indices.keys().next().copied() }; if let Some(client_id) = client_id { if let Ok(tab_exists) = screen.go_to_tab_name(tab_name.clone(), client_id) { screen.unblock_input()?; screen.render(None)?; if create && !tab_exists { let tab_index = screen.get_new_tab_index(); screen.new_tab(tab_index, swap_layouts, Some(tab_name), client_id)?; screen .bus .senders .send_to_plugin(PluginInstruction::NewTab( None, default_shell, None, vec![], tab_index, client_id, ))?; } } } }, ScreenInstruction::UpdateTabName(c, client_id) => { screen.update_active_tab_name(c, client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::UndoRenameTab(client_id) => { screen.undo_active_rename_tab(client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::MoveTabLeft(client_id) => { screen.move_active_tab_to_left(client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::MoveTabRight(client_id) => { screen.move_active_tab_to_right(client_id)?; screen.unblock_input()?; screen.render(None)?; }, 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::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::ChangeMode(mode_info, client_id) => { screen.change_mode(mode_info, client_id)?; screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::ChangeModeForAllClients(mode_info) => { screen.change_mode_for_all_clients(mode_info)?; screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::ToggleActiveSyncTab(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::LeftClick(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_left_click(&point, client_id), ?); screen.log_and_report_session_state()?; screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::RightClick(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_right_click(&point, client_id), ?); screen.log_and_report_session_state()?; screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::MiddleClick(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_middle_click(&point, client_id), ?); screen.log_and_report_session_state()?; screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::LeftMouseRelease(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_left_mouse_release(&point, client_id), ?); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::RightMouseRelease(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_right_mouse_release(&point, client_id), ?); screen.render(None)?; }, ScreenInstruction::MiddleMouseRelease(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_middle_mouse_release(&point, client_id), ?); screen.render(None)?; }, ScreenInstruction::MouseHoldLeft(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_mouse_hold_left(&point, client_id), ?); screen.render(None)?; }, ScreenInstruction::MouseHoldRight(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_mouse_hold_right(&point, client_id), ?); screen.render(None)?; }, ScreenInstruction::MouseHoldMiddle(point, client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .handle_mouse_hold_middle(&point, client_id), ?); screen.render(None)?; }, ScreenInstruction::Copy(client_id) => { active_tab!(screen, client_id, |tab: &mut Tab| tab .copy_selection(client_id), ?); screen.render(None)?; }, ScreenInstruction::Exit => { break; }, ScreenInstruction::ToggleTab(client_id) => { screen.toggle_tab(client_id)?; screen.unblock_input()?; screen.render(None)?; }, ScreenInstruction::AddClient(client_id, tab_position_to_focus, pane_id_to_focus) => { screen.add_client(client_id)?; 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, 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.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::RemoveClient(client_id) => { screen.remove_client(client_id)?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::AddOverlay(overlay, _client_id) => { screen.get_active_overlays_mut().pop(); screen.get_active_overlays_mut().push(overlay); screen.unblock_input()?; }, ScreenInstruction::RemoveOverlay(_client_id) => { screen.get_active_overlays_mut().pop(); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::ConfirmPrompt(_client_id) => { let overlay = screen.get_active_overlays_mut().pop(); let instruction = overlay.and_then(|o| o.prompt_confirm()); if let Some(instruction) = instruction { screen .bus .senders .send_to_server(*instruction) .context("failed to confirm prompt")?; } screen.unblock_input()?; }, ScreenInstruction::DenyPrompt(_client_id) => { screen.get_active_overlays_mut().pop(); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::UpdateSearch(c, client_id) => { 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) => { 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) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.search_up(client_id) ); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::SearchToggleCaseSensitivity(client_id) => { 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)?; screen.unblock_input()?; }, ScreenInstruction::SearchToggleWrap(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.toggle_search_wrap(client_id) ); screen.render(None)?; screen.unblock_input()?; }, ScreenInstruction::SearchToggleWholeWord(client_id) => { 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)?; screen.unblock_input()?; }, 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::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); break; } } } screen.render(None)?; }, ScreenInstruction::PreviousSwapLayout(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.previous_swap_layout(Some(client_id)), ? ); screen.render(None)?; screen.log_and_report_session_state()?; screen.unblock_input()?; }, ScreenInstruction::NextSwapLayout(client_id) => { active_tab_and_connected_client_id!( screen, client_id, |tab: &mut Tab, client_id: ClientId| tab.next_swap_layout(Some(client_id), true), ? ); screen.render(None)?; screen.log_and_report_session_state()?; screen.unblock_input()?; }, ScreenInstruction::QueryTabNames(client_id) => { 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))?; }, ScreenInstruction::NewTiledPluginPane( run_plugin, pane_title, skip_cache, cwd, client_id, ) => { let tab_index = screen.active_tab_indices.values().next().unwrap_or(&1); let size = Size::default(); let should_float = Some(false); let should_be_opened_in_place = false; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_opened_in_place, pane_title, run_plugin, *tab_index, None, client_id, size, skip_cache, cwd, None, ))?; }, ScreenInstruction::NewFloatingPluginPane( run_plugin, pane_title, skip_cache, cwd, floating_pane_coordinates, client_id, ) => match screen.active_tab_indices.values().next() { Some(tab_index) => { let size = Size::default(); let should_float = Some(true); let should_be_opened_in_place = false; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_opened_in_place, pane_title, run_plugin, *tab_index, None, client_id, size, skip_cache, cwd, floating_pane_coordinates, ))?; }, 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, client_id, ) => match screen.active_tab_indices.values().next() { Some(tab_index) => { let size = Size::default(); let should_float = None; let should_be_in_place = true; screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( should_float, should_be_in_place, pane_title, run_plugin, *tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, None, None, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, }, ScreenInstruction::StartOrReloadPluginPane(run_plugin, pane_title) => { let tab_index = screen.active_tab_indices.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, ))?; }, ScreenInstruction::AddPlugin( should_float, should_be_in_place, run_plugin_or_alias, pane_title, tab_index, plugin_id, pane_id_to_replace, cwd, start_suppressed, client_id, ) => { 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); 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), 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), 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), should_float, Some(run_plugin), None, start_suppressed, Some(client_id), ) }, ?); } 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), should_float, Some(run_plugin), None, start_suppressed, 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()?; screen.unblock_input()?; }, ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => { 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, pane_id_to_replace, skip_cache, client_id, ) => match pane_id_to_replace { Some(pane_id_to_replace) if should_open_in_place => { match screen.active_tab_indices.values().next() { Some(tab_index) => { let size = Size::default(); screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, None, run_plugin, *tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, None, None, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, } }, _ => { let client_id = if screen.active_tab_indices.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_indices .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); match client_id_and_focused_tab { Some((tab_index, client_id)) => { if screen.focus_plugin_pane( &run_plugin, should_float, move_to_focused_tab, client_id, )? { 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, None, run_plugin, tab_index, None, client_id, Size::default(), skip_cache, None, None, ))?; } }, None => { log::error!("No connected clients found - cannot load or focus plugin") }, } }, }, ScreenInstruction::LaunchPlugin( run_plugin, should_float, should_open_in_place, pane_id_to_replace, skip_cache, cwd, client_id, ) => match pane_id_to_replace { Some(pane_id_to_replace) => match screen.active_tab_indices.values().next() { Some(tab_index) => { let size = Size::default(); screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, None, run_plugin, *tab_index, Some(pane_id_to_replace), client_id, size, skip_cache, cwd, None, ))?; }, None => { log::error!( "Could not find an active tab - is there at least 1 connected user?" ); }, }, None => { let client_id = if screen.active_tab_indices.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_indices .get(&client_id) .map(|tab_index| (*tab_index, client_id)) }); match client_id_and_focused_tab { Some((tab_index, client_id)) => { screen .bus .senders .send_to_pty(PtyInstruction::FillPluginCwd( Some(should_float), should_open_in_place, None, run_plugin, tab_index, None, client_id, Size::default(), skip_cache, cwd, None, ))?; }, 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, client_id); drop(screen.render(None)); break; } } screen.log_and_report_session_state()?; }, ScreenInstruction::FocusPaneWithId(pane_id, should_float_if_hidden, client_id) => { screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?; screen.log_and_report_session_state()?; }, ScreenInstruction::RenamePane(pane_id, new_name) => { 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::RenameTab(tab_index, new_name) => { match screen.tabs.get_mut(&tab_index.saturating_sub(1)) { Some(tab) => { tab.name = String::from_utf8_lossy(&new_name).to_string(); }, None => { log::error!("Failed to find tab with index: {:?}", tab_index); }, } screen.log_and_report_session_state()?; }, 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 - cannot request permissions", plugin_id ); } }, ScreenInstruction::BreakPane(default_layout, default_shell, client_id) => { screen.break_pane(default_shell, default_layout, client_id)?; }, ScreenInstruction::BreakPaneRight(client_id) => { screen.break_pane_to_new_tab(Direction::Right, client_id)?; }, ScreenInstruction::BreakPaneLeft(client_id) => { 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::ReplacePane( new_pane_id, hold_for_command, pane_title, invoked_with, client_id_tab_index_or_pane_id, ) => { screen.replace_pane( new_pane_id, hold_for_command, invoked_with, pane_title, client_id_tab_index_or_pane_id, )?; screen.unblock_input()?; screen.log_and_report_session_state()?; screen.render(None)?; }, ScreenInstruction::DumpLayoutToHd => { if screen.session_serialization { screen.dump_layout_to_hd()?; } }, ScreenInstruction::RenameSession(name, client_id) => { if screen.session_infos_on_machine.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(vec![error_text.to_owned()]), ); } } else if screen.resurrectable_sessions.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(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); } screen.unblock_input()?; }, ScreenInstruction::Reconfigure { client_id, keybinds, default_mode, } => { screen .reconfigure_mode_info(keybinds, default_mode, client_id) .non_fatal(); }, ScreenInstruction::RerunCommandPane(terminal_pane_id) => { screen.rerun_command_pane_with_id(terminal_pane_id) }, } } Ok(()) } #[path = "./unit/screen_tests.rs"] #[cfg(test)] mod screen_tests;