//! `Tab`s holds multiple panes. It tracks their coordinates (x/y) and size, //! as well as how they should be resized mod clipboard; mod copy_command; mod layout_applier; mod mouse_handler; mod swap_layouts; use crate::plugins::PluginId; use copy_command::CopyCommand; pub use mouse_handler::{MouseEffect, MouseHandler, PaneEdge, PaneResizeState}; use std::env::temp_dir; use std::net::IpAddr; use std::path::PathBuf; use unicode_width::UnicodeWidthStr; use uuid::Uuid; use zellij_utils::data::PaneContents; use zellij_utils::data::{ Direction, KeyWithModifier, NewPanePlacement, PaneInfo, PermissionStatus, PermissionType, PluginPermission, RegexHighlight, ResizeStrategy, Style, StyledText, WebSharing, }; use zellij_utils::errors::prelude::*; use zellij_utils::input::layout::StatusBarButton; use zellij_utils::input::theme::StackListStyle; use zellij_utils::input::command::RunCommand; use zellij_utils::input::mouse::MouseEvent; use zellij_utils::position::Position; use zellij_utils::position::{Column, Line}; use zellij_utils::shared::clean_string_from_control_and_linebreak; use crate::background_jobs::BackgroundJob; use crate::pane_groups::PaneGroups; use crate::pty_writer::PtyWriteInstruction; use crate::screen::{CopyOptions, GuestModalOutcome, ScreenInstruction}; use crate::ui::hint_text::{ held_hint_variants, hover_hint_variants, resize_hint_variants, HintExitStatus, }; use crate::ui::{ loading_indication::LoadingIndication, pane_boundaries_frame::{stack_index_prefix, FrameParams}, pane_contents_and_ui::PaneContentsAndUi, }; use layout_applier::LayoutApplier; use swap_layouts::SwapLayouts; use self::clipboard::ClipboardProvider; use crate::route::NotificationEnd; use crate::{ os_input_output::ServerOsApi, output::{CharacterChunk, KittyImageChunk, Output, SixelImageChunk}, panes::floating_panes::floating_pane_grid::half_size_middle_geom, panes::grid::namespace_notification_id, panes::kitty_graphics::{KittyHostSupport, KittyImageStore}, panes::nested_session_modal::GuestModalShortcuts, panes::sixel::SixelImageStore, panes::{FloatingPanes, TiledPanes}, panes::{ LinkHandler, PaneId, PluginPane, TerminalCharacter, TerminalPane, EMPTY_TERMINAL_CHARACTER, }, plugins::PluginInstruction, pty::{ClientTabIndexOrPaneId, PtyInstruction, VteBytes}, thread_bus::ThreadSenders, ClientId, ServerInstruction, }; use std::cell::RefCell; use std::rc::Rc; use std::time::Instant; use std::{ collections::{BTreeMap, HashMap, HashSet}, str, }; use zellij_utils::{ data::{Event, FloatingPaneCoordinates, InputMode, ModeInfo, Palette, PaletteColor, Styling}, input::{ command::TerminalAction, layout::{ FloatingPaneLayout, Run, RunPluginOrAlias, SplitDirection, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }, options::PaneFrameStyle, parse_keys, }, nested_session::NestedSessionMessage, pane_size::{Dimension, Offset, PaneGeom, Size, SizeInPixels, Viewport}, }; #[macro_export] macro_rules! resize_pty { ($pane:expr, $os_input:expr, $senders:expr) => {{ match $pane.pid() { PaneId::Terminal(ref pid) => { $senders .send_to_pty_writer(PtyWriteInstruction::ResizePty( *pid, $pane.get_content_columns() as u16, $pane.get_content_rows() as u16, None, None, )) .with_context(err_context); }, PaneId::Plugin(ref pid) => { let err_context = || format!("failed to resize plugin {pid}"); $senders .send_to_plugin(PluginInstruction::Resize( *pid, $pane.get_content_columns(), $pane.get_content_rows(), )) .with_context(err_context) }, } }}; ($pane:expr, $os_input:expr, $senders:expr, $character_cell_size:expr) => {{ let (width_in_pixels, height_in_pixels) = { let character_cell_size = $character_cell_size.borrow(); match *character_cell_size { Some(size_in_pixels) => { let width_in_pixels = (size_in_pixels.width * $pane.get_content_columns()) as u16; let height_in_pixels = (size_in_pixels.height * $pane.get_content_rows()) as u16; (Some(width_in_pixels), Some(height_in_pixels)) }, None => (None, None), } }; match $pane.pid() { PaneId::Terminal(ref pid) => { use crate::PtyWriteInstruction; let err_context = || format!("Failed to send resize pty instruction"); $senders .send_to_pty_writer(PtyWriteInstruction::ResizePty( *pid, $pane.get_content_columns() as u16, $pane.get_content_rows() as u16, width_in_pixels, height_in_pixels, )) .with_context(err_context) }, PaneId::Plugin(ref pid) => { let err_context = || format!("failed to resize plugin {pid}"); $senders .send_to_plugin(PluginInstruction::Resize( *pid, $pane.get_content_columns(), $pane.get_content_rows(), )) .with_context(err_context) }, } }}; } // FIXME: This should be replaced by `RESIZE_PERCENT` at some point pub const MIN_TERMINAL_HEIGHT: usize = 5; pub const MIN_TERMINAL_WIDTH: usize = 5; const MAX_PENDING_VTE_EVENTS: usize = 7000; type HoldForCommand = Option; pub type SuppressedPanes = HashMap)>; // bool => is scrollback editor /// Internal names for the layouts the responsive collapse synthesizes. Prefixed /// so they cannot collide with a name from a user's swap file. const RESPONSIVE_COLLAPSE_LAYOUT_NAME: &str = "sl0p:responsive-collapse"; const RESPONSIVE_EXPAND_LAYOUT_NAME: &str = "sl0p:responsive-expand"; pub type StackListId = usize; #[derive(Debug, Clone)] pub struct StackList { pub members: Vec, pub visible: PaneId, } impl StackList { pub fn rank_of(&self, pane_id: &PaneId) -> Option { self.members.iter().position(|p| p == pane_id) } } /// One rendered cell of the stacked-pane picker. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct StackListCell { /// Index into `StackList::members`. pub member_index: usize, /// Offset from the list's left edge. pub x_offset: usize, /// Offset from the list's top edge. pub row: usize, pub width: usize, } /// The picker's complete cell layout. /// /// The single source of truth for "which member does this cell show, and where /// is it". The renderer, the reserved-row budget and the mouse hit-test all read /// it, so they cannot disagree -- a desync between just two of them already /// caused clicks to select the wrong pane once. #[derive(Debug, Clone, PartialEq, Eq)] pub struct StackListLayout { pub cells: Vec, /// Rows the cells occupy, NOT counting the indicator row beneath them. pub rows: usize, pub hidden: usize, } /// Below this an entry has no room for any label at all. const STACK_LIST_MIN_ENTRY_COLS: usize = 8; /// Blank columns between grid cells. Without a gap the focused entry's "> " /// marker sits flush against the previous cell and reads as belonging to it. const STACK_LIST_CELL_GAP_COLS: usize = 2; /// Which members are on screen, given a column count and a row budget. /// /// Windows by ROW rather than by member so that a grid scrolls a whole row at a /// time; with one column this reduces to "window of N members around the focused /// one", which is what the list did before it could be a grid. /// /// Returns (first member index, member count shown, rows occupied). fn stack_list_window_core( member_count: usize, visible_rank: usize, columns: usize, max_rows: usize, ) -> (usize, usize, usize) { let columns = columns.max(1); if member_count == 0 { return (0, 0, 0); } let total_rows = member_count.div_ceil(columns); let rows = if max_rows == 0 { total_rows } else { total_rows.min(max_rows.max(1)) }; let focus_row = visible_rank / columns; let first_row = focus_row .saturating_sub(rows / 2) .min(total_rows.saturating_sub(rows)); let start = first_row * columns; let shown = (member_count - start).min(rows * columns); (start, shown, shown.div_ceil(columns)) } /// Spans of a picker row that no cell covers, as (start, end) column offsets. /// /// The picker draws each entry as its own chunk, so the space between and around /// them is never painted and keeps whatever was there before -- which matters /// because the grid genuinely re-flows when a label's width changes. /// /// Returns only the uncovered spans, never the cells: chunks are not composited /// last-wins, so blanking a region a cell also draws to erases the entry instead /// of the stale contents beneath it. fn stack_list_row_gaps( layout: &StackListLayout, row: usize, available_cols: usize, ) -> Vec<(usize, usize)> { let mut spans: Vec<(usize, usize)> = layout .cells .iter() .filter(|cell| cell.row == row) .map(|cell| (cell.x_offset, cell.x_offset + cell.width)) .collect(); spans.sort_unstable(); let mut gaps = vec![]; let mut cursor = 0; for (start, end) in spans { if start > cursor { gaps.push((cursor, start)); } cursor = cursor.max(end); } if cursor < available_cols { gaps.push((cursor, available_cols)); } gaps } enum StackListRemoval { NotRemoved, DissolvedInPlace, Removed(Option>), } enum BufferedTabInstruction { SetPaneSelectable(PaneId, bool), HandlePtyBytes(u32, VteBytes), HoldPane(PaneId, Option, bool, RunCommand), // Option is the exit status, bool is is_first_run } pub(crate) struct Tab { pub id: usize, pub position: usize, pub name: String, /// Opaque agent-facing hint for what this tab is for; see /// `sl0p/workspace-hints-proposal.md`. pub purpose: Option, pub prev_name: String, default_name: String, pub size: Size, pub visible_to: Option>, tiled_panes: TiledPanes, floating_panes: FloatingPanes, suppressed_panes: SuppressedPanes, stacked_pane_list: Rc>, reserved_top_rows: Rc>>, stack_lists: HashMap, stack_list_of_member: HashMap, stack_list_parked_pairs: HashMap, next_stack_list_id: StackListId, stack_list_session_is_mirrored: bool, max_panes: Option, viewport: Rc>, // includes all non-UI panes display_area: Rc>, // includes all panes (including eg. the status bar and tab bar in the default layout) character_cell_size: Rc>>, sixel_image_store: Rc>, kitty_image_store: Rc>, os_api: Box, pub senders: ThreadSenders, synchronize_is_active: bool, should_clear_display_before_rendering: bool, mode_info: Rc>>, default_mode_info: ModeInfo, pub style: Style, connected_clients: Rc>>, pane_frame_style: PaneFrameStyle, auto_layout: bool, pending_vte_events: HashMap>, pub selecting_with_mouse_in_pane: Option, // this is only pub for the tests pane_being_resized_with_mouse: Option, link_handler: Rc>, clipboard_provider: ClipboardProvider, // TODO: used only to focus the pane when the layout is loaded // it seems that optimization is possible using `active_panes` focus_pane_id: Option, copy_on_select: bool, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, pids_waiting_resize: HashSet, // u32 is the terminal_id cursor_positions_and_shape: HashMap, // (x_position, // y_position, // cursor_shape_csi) is_pending: bool, // a pending tab is one that is still being loaded or otherwise waiting pending_instructions: Vec, // instructions that came while the tab was // pending and need to be re-applied swap_layouts: SwapLayouts, default_shell: PathBuf, default_editor: Option, debug: bool, arrow_fonts: bool, kitty_host_support: Option, styled_underlines: bool, osc8_hyperlinks: bool, explicitly_disable_kitty_keyboard_protocol: bool, web_clients_allowed: bool, web_sharing: WebSharing, mouse_hover_pane_id: HashMap, dimmed_clients: HashSet, plugin_hover_pane_id: HashMap, mouse_last_pane_id: HashMap, mouse_help_text_visible: HashMap, last_mouse_activity_time: HashMap, last_hint_text: HashMap>, last_active_pane_scroll: HashMap>, current_pane_group: Rc>, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, /// Whether hovering shows the resize hint. Separate from mouse_hover_effects /// so the hint can be dropped without losing the hover frame highlight. mouse_hover_help_text: bool, focus_follows_mouse: bool, mouse_click_through: bool, /// Repeated clicks on pane FRAMES, per client. Separate from the grid's own /// click tracking (which is for selecting text inside a pane) because a /// click that lands on the frame never reaches the grid. frame_clicks: HashMap, /// Tiled pane currently being dragged by its title bar, per client. tiled_pane_being_dragged: HashMap, /// While dragging, the pane the dragged one is currently swapped WITH, so /// the live layout acts as the preview. Undone when the drag moves to a /// different target. drag_preview_swapped_with: HashMap, /// Whether the tab is currently collapsed by the responsive layout. The /// only state the feature keeps: everything else is resolved from the panes /// as they are, and this is flipped only after a layout was applied. responsive_collapsed: bool, responsive_min_cols: usize, responsive_layout_name: String, /// Cap on picker rows; see stack_list_window. 0 = uncapped. stack_list_max_rows: usize, /// Stacks the user has expanded past the cap by clicking "+N more". expanded_stack_lists: HashSet, /// Pack picker entries into a grid rather than one per row. stack_list_grid: bool, /// Visual treatment of picker entries. stack_list_style: StackListStyle, /// So the "no such swap layout" warning is logged once, not per resize. responsive_missing_layout_warned: bool, currently_marking_pane_group: Rc>>, connected_clients_in_app: Rc>>, // bool -> is_web_client // the below are the configured values - the ones that will be set if and when the web server // is brought online web_server_ip: IpAddr, web_server_port: u16, pub panes_with_pending_bell: HashSet, pub tab_has_pending_bell: bool, pub tab_bell_flash: bool, // currently in mid-notification-flash pub tab_bell_ring: bool, // need to send ANSI BEL to the controlling terminal } // FIXME: Use a struct that has a pane_type enum, to reduce all of the duplication pub trait Pane { fn x(&self) -> usize; fn y(&self) -> usize; fn rows(&self) -> usize; fn cols(&self) -> usize; fn get_content_x(&self) -> usize; fn get_content_y(&self) -> usize; fn get_content_columns(&self) -> usize; fn get_content_rows(&self) -> usize; fn reset_size_and_position_override(&mut self); fn set_geom(&mut self, position_and_size: PaneGeom); fn set_geom_override(&mut self, pane_geom: PaneGeom); fn handle_pty_bytes(&mut self, _bytes: VteBytes) {} fn handle_plugin_bytes(&mut self, _client_id: ClientId, _bytes: VteBytes) {} fn show_cursor(&mut self, _client_id: ClientId, _cursor_position: Option<(usize, usize)>) {} /// Returns the cursor position and whether it is visible. /// The position is returned unconditionally (as long as the cursor is within /// bounds) so that the host terminal can position the cursor for IME even /// when the app has hidden it. The bool is true when the cursor is visible. fn cursor_coordinates(&self, _client_id: Option) -> Option<(usize, usize, bool)>; fn is_mid_frame(&self) -> bool { false } fn adjust_input_to_terminal( &mut self, _key_with_modifier: &Option, _raw_input_bytes: Vec, _raw_input_bytes_are_kitty: bool, _client_id: Option, ) -> Option { None } fn position_and_size(&self) -> PaneGeom; fn current_geom(&self) -> PaneGeom; fn geom_override(&self) -> Option; fn should_render(&self) -> bool; fn set_should_render(&mut self, should_render: bool); fn set_should_render_boundaries(&mut self, _should_render: bool) {} fn selectable(&self) -> bool; fn set_selectable(&mut self, selectable: bool); fn request_permissions_from_user(&mut self, _permissions: Option) {} fn render( &mut self, client_id: Option, ) -> Result< Option<( Vec, Option, Vec, Vec, )>, >; // TODO: better fn render_frame( &mut self, client_id: ClientId, frame_params: FrameParams, input_mode: InputMode, ) -> Result, Option)>>; // TODO: better fn render_fake_cursor( &mut self, cursor_color: PaletteColor, text_color: PaletteColor, ) -> Option; fn render_terminal_title(&mut self, _input_mode: InputMode) -> String; fn update_name(&mut self, name: &str); fn pid(&self) -> PaneId; fn reduce_height(&mut self, percent: f64); fn increase_height(&mut self, percent: f64); fn reduce_width(&mut self, percent: f64); fn increase_width(&mut self, percent: f64); fn push_down(&mut self, count: usize); fn push_right(&mut self, count: usize); fn pull_left(&mut self, count: usize); fn pull_up(&mut self, count: usize); fn clear_screen(&mut self); fn dump_screen(&self, _full: bool, _client_id: Option) -> String { "".to_owned() } fn dump_screen_with_ansi(&self, _full: bool, _client_id: Option) -> String { "".to_owned() } fn scroll_up(&mut self, count: usize, client_id: ClientId); fn scroll_down(&mut self, count: usize, client_id: ClientId); fn scroll_left(&mut self, _count: usize, _client_id: ClientId) {} fn scroll_right(&mut self, _count: usize, _client_id: ClientId) {} fn clear_scroll(&mut self); fn is_scrolled(&self) -> bool; fn active_at(&self) -> Instant; fn set_active_at(&mut self, instant: Instant); fn set_frame(&mut self, frame: bool); fn set_content_offset(&mut self, offset: Offset); fn get_content_offset(&self) -> Offset { Offset::default() } fn cursor_shape_csi(&self) -> String { "\u{1b}[0 q".to_string() // default to non blinking block } fn contains(&self, position: &Position) -> bool { match self.geom_override() { Some(position_and_size) => position_and_size.contains(position), None => self.position_and_size().contains(position), } } fn start_selection(&mut self, _start: &Position, _client_id: ClientId) {} fn update_selection(&mut self, _position: &Position, _client_id: ClientId) {} fn end_selection(&mut self, _end: &Position, _client_id: ClientId) {} fn reset_selection(&mut self, _client_id: Option) {} fn supports_mouse_selection(&self) -> bool { true } fn get_selected_text(&self, _client_id: ClientId) -> Option { None } fn set_pane_default_colors(&mut self, _fg: Option, _bg: Option) {} fn get_pane_default_colors(&self) -> (Option, Option) { (None, None) } fn right_boundary_x_coords(&self) -> usize { self.x() + self.cols() } fn right_boundary_x_content_coords(&self) -> usize { self.get_content_x() + self.get_content_columns() } fn bottom_boundary_y_coords(&self) -> usize { self.y() + self.rows() } fn bottom_boundary_y_content_coords(&self) -> usize { self.get_content_y() + self.get_content_rows() } fn is_right_of(&self, other: &dyn Pane) -> bool { self.x() > other.x() } fn is_directly_right_of(&self, other: &dyn Pane) -> bool { self.x() == other.x() + other.cols() } fn is_left_of(&self, other: &dyn Pane) -> bool { self.x() < other.x() } fn is_directly_left_of(&self, other: &dyn Pane) -> bool { self.x() + self.cols() == other.x() } fn is_below(&self, other: &dyn Pane) -> bool { self.y() > other.y() } fn is_directly_below(&self, other: &dyn Pane) -> bool { self.y() == other.y() + other.rows() } fn is_above(&self, other: &dyn Pane) -> bool { self.y() < other.y() } fn is_directly_above(&self, other: &dyn Pane) -> bool { self.y() + self.rows() == other.y() } fn horizontally_overlaps_with(&self, other: &dyn Pane) -> bool { (self.y() >= other.y() && self.y() < (other.y() + other.rows())) || ((self.y() + self.rows()) <= (other.y() + other.rows()) && (self.y() + self.rows()) > other.y()) || (self.y() <= other.y() && (self.y() + self.rows() >= (other.y() + other.rows()))) || (other.y() <= self.y() && (other.y() + other.rows() >= (self.y() + self.rows()))) } fn get_horizontal_overlap_with(&self, other: &dyn Pane) -> usize { std::cmp::min(self.y() + self.rows(), other.y() + other.rows()) - std::cmp::max(self.y(), other.y()) } fn vertically_overlaps_with(&self, other: &dyn Pane) -> bool { (self.x() >= other.x() && self.x() < (other.x() + other.cols())) || ((self.x() + self.cols()) <= (other.x() + other.cols()) && (self.x() + self.cols()) > other.x()) || (self.x() <= other.x() && (self.x() + self.cols() >= (other.x() + other.cols()))) || (other.x() <= self.x() && (other.x() + other.cols() >= (self.x() + self.cols()))) } fn get_vertical_overlap_with(&self, other: &dyn Pane) -> usize { std::cmp::min(self.x() + self.cols(), other.x() + other.cols()) - std::cmp::max(self.x(), other.x()) } fn can_reduce_height_by(&self, reduce_by: usize) -> bool { self.rows() > reduce_by && self.rows() - reduce_by >= self.min_height() } fn can_reduce_width_by(&self, reduce_by: usize) -> bool { self.cols() > reduce_by && self.cols() - reduce_by >= self.min_width() } fn min_width(&self) -> usize { MIN_TERMINAL_WIDTH } fn min_height(&self) -> usize { MIN_TERMINAL_HEIGHT } fn drain_messages_to_pty(&mut self) -> Vec> { // TODO: this is only relevant to terminal panes // we should probably refactor away from this trait at some point vec![] } fn drain_forwarded_queries(&mut self) -> Vec { // Only terminal panes forward whitelisted queries to the host; // plugin panes have no such concept. vec![] } fn drain_nested_session_messages(&mut self) -> Vec { vec![] } fn is_nested_guest(&self) -> bool { false } fn set_is_nested_guest(&mut self, _is_nested_guest: bool) {} fn set_guest_modal(&mut self, _client_ids: &[ClientId]) {} fn clear_guest_modal(&mut self, _client_id: ClientId) {} fn clear_all_guest_modals(&mut self) {} fn guest_modal_selection(&self, _client_id: ClientId) -> Option { None } fn has_guest_modal_for_any_client(&self) -> bool { false } fn set_guest_choice_indicator( &mut self, _client_id: ClientId, _indicator: Option, ) { } fn guest_choice_indicator(&self, _client_id: ClientId) -> Option { None } fn clear_all_guest_choice_indicators(&mut self) {} fn set_guest_session_name(&mut self, _session_name: Option) {} fn guest_session_name(&self) -> Option { None } fn set_guest_modal_shortcuts(&mut self, _shortcuts: GuestModalShortcuts) {} fn guest_modal_shortcuts(&self) -> GuestModalShortcuts { GuestModalShortcuts::default() } /// Mark this pane as awaiting a host-terminal reply for a forward /// it just dispatched. While paused, vte byte feeding is buffered /// so that the eventual host reply lands on the pane's stdin in /// the same stream position the original query occupied. Default /// no-op for non-terminal panes (plugins do not forward queries). fn arm_forward_pause(&mut self) {} /// Clear the forward-pause flag set by `arm_forward_pause`, returning /// `true` if the pane was previously paused. Default no-op. fn clear_forward_pause(&mut self) -> bool { false } /// Drain and return any PTY bytes that arrived while the pane was /// forward-paused. Tab calls this on resume and re-feeds the bytes /// through `handle_pty_bytes`. Default empty for non-terminal panes. fn drain_pending_pty_input(&mut self) -> Vec { vec![] } /// Whether this pane is currently waiting for a host-forward reply. fn is_forward_paused(&self) -> bool { false } /// Push a CSI ?997 DSR notification (host color-palette theme mode) /// onto this pane's pending pty-write queue, but only if the pane's /// underlying app opted in via `CSI ? 2031 h`. /// Only relevant to terminal panes, plugin panes receive this through /// `Event::HostTerminalThemeChanged` fn push_color_palette_dsr(&mut self, _mode: zellij_utils::data::HostTerminalThemeMode) {} fn drain_clipboard_update(&mut self) -> Option { None } fn drain_desktop_notifications(&mut self) -> Vec<(String, String)> { vec![] } fn drain_osc7_cwd(&mut self) -> Option { None } fn render_full_viewport(&mut self) {} /// Called when this pane gains or loses focus. /// /// The output buffer only emits CHANGED cells, so focus-dependent styling /// (desaturating unfocused panes) would otherwise not appear until the pane /// happened to print something - an idle pane would keep its old colours /// indefinitely. Panes that have no focus-dependent styling do nothing /// here, so this costs nothing when the feature is off. fn refresh_for_focus_change(&mut self) {} fn relative_position(&self, position_on_screen: &Position) -> Position { position_on_screen.relative_to(self.get_content_y(), self.get_content_x()) } fn position_is_on_frame(&self, position: &Position) -> bool { if !self.contains(position) { return false; } if (self.x()..self.get_content_x()).contains(&position.column()) { // position is on left border return true; } if (self.get_content_x() + self.get_content_columns()..(self.x() + self.cols())) .contains(&position.column()) { // position is on right border return true; } if (self.y() as isize..self.get_content_y() as isize).contains(&position.line()) { // position is on top border return true; } if ((self.get_content_y() + self.get_content_rows()) as isize ..(self.y() + self.rows()) as isize) .contains(&position.line()) { // position is on bottom border return true; } false } /// Is this position on the pane's TOP border, i.e. the row the title is /// drawn on? /// /// Deliberately geometric rather than derived from get_edge_at_position(): /// that function quadrant-splits the pane and only ever returns the four /// CORNER variants, never PaneEdge::Top, so a title-row click reports as /// TopLeft/TopRight. fn position_is_on_title_row(&self, position: &Position) -> bool { if !self.contains(position) { return false; } (self.y() as isize..self.get_content_y() as isize).contains(&position.line()) } /// Is this position on the part of the title row that acts as a MOVE /// handle, i.e. the title row excluding the corners? /// /// The corners stay resize handles, so two-axis resize from the top is /// preserved. Losing plain top-edge resize costs nothing: every boundary is /// shared, so the boundary above this pane is the bottom edge of the pane /// above and can be dragged from there. A pane at the top of the viewport /// has the screen edge above it, which was never resizable. fn position_is_on_title_move_handle(&self, position: &Position) -> bool { const CORNER_WIDTH: usize = 2; if !self.position_is_on_title_row(position) { return false; } let left_edge = self.x(); let right_edge = self.x() + self.cols(); let col = position.column(); col >= left_edge.saturating_add(CORNER_WIDTH) && col < right_edge.saturating_sub(CORNER_WIDTH) } fn get_edge_at_position(&self, position: &Position) -> Option { if !self.contains(position) { return None; } let pos_col = position.column(); let pos_line = position.line(); let pane_x = self.x(); let pane_y = self.y(); let pane_cols = self.cols(); let pane_rows = self.rows(); let mid_col = pane_x + pane_cols / 2; let mid_line = pane_y + pane_rows / 2; let is_left = pos_col < mid_col; let is_top = (pos_line as usize) < mid_line; return Some(match (is_top, is_left) { (true, true) => PaneEdge::TopLeft, (true, false) => PaneEdge::TopRight, (false, true) => PaneEdge::BottomLeft, (false, false) => PaneEdge::BottomRight, }); } // TODO: get rid of this in favor of intercept_mouse_event_on_frame fn intercept_left_mouse_click(&mut self, _position: &Position, _client_id: ClientId) -> bool { let intercepted = false; intercepted } fn intercept_mouse_event_on_frame( &mut self, _event: &MouseEvent, _client_id: ClientId, ) -> bool { let intercepted = false; intercepted } fn store_pane_name(&mut self); fn load_pane_name(&mut self); fn set_borderless(&mut self, borderless: bool); fn borderless(&self) -> bool; fn set_exclude_from_sync(&mut self, exclude_from_sync: bool); fn exclude_from_sync(&self) -> bool; // TODO: this should probably be merged with the mouse_right_click fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} fn mouse_event(&self, _event: &MouseEvent, _client_id: ClientId) -> Option { None } fn mouse_left_click(&self, _position: &Position, _is_held: bool) -> Option { None } fn mouse_left_click_release(&self, _position: &Position) -> Option { None } fn mouse_right_click(&self, _position: &Position, _is_held: bool) -> Option { None } fn mouse_right_click_release(&self, _position: &Position) -> Option { None } fn mouse_middle_click(&self, _position: &Position, _is_held: bool) -> Option { None } fn mouse_middle_click_release(&self, _position: &Position) -> Option { None } fn mouse_scroll_up(&self, _position: &Position) -> Option { None } fn mouse_scroll_down(&self, _position: &Position) -> Option { None } fn focus_event(&self) -> Option { None } fn unfocus_event(&self) -> Option { None } fn get_line_number(&self) -> Option { None } fn update_search_term(&mut self, _needle: &str) { // No-op by default (only terminal-panes currently have search capability) } fn search_down(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn search_up(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn toggle_search_case_sensitivity(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn toggle_search_whole_words(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn toggle_search_wrap(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn clear_search(&mut self) { // No-op by default (only terminal-panes currently have search capability) } fn is_alternate_mode_active(&self) -> bool { // False by default (only terminal-panes support alternate mode) false } fn hold(&mut self, _exit_status: Option, _is_first_run: bool, _run_command: RunCommand) { // No-op by default, only terminal panes support holding } fn has_bell(&self) -> bool { false } fn consume_bell(&mut self) {} fn set_bell_notification(&mut self, _val: bool) {} fn get_bell_notification(&self) -> bool { false } fn add_red_pane_frame_color_override(&mut self, _error_text: Option); fn add_highlight_pane_frame_color_override( &mut self, _text: Option, _client_id: Option, ) { } fn clear_pane_frame_color_override(&mut self, _client_id: Option); fn frame_color_override(&self) -> Option; fn invoked_with(&self) -> &Option; fn set_title(&mut self, title: String); fn update_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins fn start_loading_indication(&mut self, _loading_indication: LoadingIndication) {} // only relevant for plugins fn progress_animation_offset(&mut self) {} // only relevant for plugins fn current_title(&self) -> String; /// Text the program asked to show in this pane's bottom frame row, if any. /// See OSC 5577 in grid.rs. fn status_bar_text(&self) -> Option { None } fn set_status_bar_text(&mut self, _text: Option) {} fn status_bar_buttons(&self) -> Vec { vec![] } fn set_status_bar_buttons(&mut self, _buttons: Vec) {} fn status_bar_button_at(&self, _position: &Position) -> Option { None } fn hovered_status_bar_button(&self, _client_id: ClientId) -> Option { None } /// Opaque agent-facing hint for what this pane is for. Zellij never /// interprets it; see `sl0p/workspace-hints-proposal.md`. fn purpose(&self) -> Option { None } fn set_purpose(&mut self, _purpose: Option) {} /// Returns true if the hovered button changed, i.e. a re-render is needed. fn set_hovered_status_bar_button( &mut self, _client_id: ClientId, _button_id: Option, ) -> bool { false } fn stack_list_entry_label(&self) -> String { self.current_title() } fn custom_title(&self) -> Option; fn has_explicit_title(&self) -> bool { false } fn scroll_position(&self) -> (usize, usize) { (0, 0) } fn is_held(&self) -> bool { false } fn exited(&self) -> bool { false } fn exit_status(&self) -> Option { None } fn rename(&mut self, _buf: Vec) {} fn serialize(&self, _scrollback_lines_to_serialize: Option) -> Option { None } fn rerun(&mut self) -> Option { None } // only relevant to terminal panes fn update_theme(&mut self, _theme: Styling) {} fn update_arrow_fonts(&mut self, _should_support_arrow_fonts: bool) {} fn update_kitty_host_support(&mut self, _supported: KittyHostSupport) {} fn update_rounded_corners(&mut self, _rounded_corners: bool) {} /// Per-side outer gap, decided by the container (which knows the viewport). /// MUST be followed by set_content_offset: the gap is baked into /// content_offset, so it only takes effect on the next offset sweep. fn set_gap_offset(&mut self, _gap_offset: zellij_utils::pane_size::Offset) {} /// Live-update the sl0p frame layout options (title alignment, outer gap, /// inner padding). Callers MUST follow this with the usual /// `set_pane_frames` sweep: gap/padding are baked into `content_offset` /// when it is set, so changing them here only takes effect once /// `set_content_offset` is re-run. fn update_frame_layout( &mut self, _title_alignment: zellij_utils::data::PaneTitleAlignment, _gap_x: usize, _gap_y: usize, _padding: zellij_utils::data::PanePadding, _inactive_grayscale: u8, _hide_default_pane_names: bool, _status_bar_button_style: zellij_utils::input::theme::StatusBarButtonStyle, ) { } fn set_should_be_suppressed(&mut self, _should_be_suppressed: bool) {} fn query_should_be_suppressed(&self) -> bool { false } fn drain_fake_cursors(&mut self) -> Option> { None } fn toggle_pinned(&mut self) {} fn set_pinned(&mut self, _should_be_pinned: bool) {} fn reset_logical_position(&mut self) {} fn set_mouse_selection_support(&mut self, _selection_support: bool) {} fn pane_contents( &self, client_id: Option, _get_full_scrollback: bool, _max_scrollback_lines: Option, ) -> PaneContents; fn pane_contents_with_ansi( &self, _client_id: Option, _get_full_scrollback: bool, _max_scrollback_lines: Option, ) -> PaneContents { Default::default() } fn update_exit_status(&mut self, _exit_status: i32) {} fn set_plugin_regex_highlights( &mut self, _plugin_id: u32, _highlights: Vec, _style: &Style, ) { } fn clear_plugin_highlights(&mut self, _plugin_id: u32) {} fn set_hover_position(&mut self, _position: Option) -> bool { false } fn cached_hover_tooltip(&self) -> Option { None } fn plugin_highlight_at( &self, _position: &Position, ) -> Option<(u32, String, String, BTreeMap)> { None } fn terminal_emulator_wants_mouse(&self) -> bool { false } } #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum GuestChoiceIndicator { Descended, Dismissed, } #[derive(Clone, Debug)] pub enum AdjustedInput { WriteBytesToTerminal(Vec), ReRunCommandInThisPane(RunCommand), PermissionRequestResult(Vec, PermissionStatus), CloseThisPane, DropToShellInThisPane { working_dir: Option }, WriteKeyToPlugin(KeyWithModifier), GuestModalSelectionChanged, GuestModalZoom, GuestModalDescend, } pub fn get_next_terminal_position( tiled_panes: &TiledPanes, floating_panes: &FloatingPanes, ) -> usize { let tiled_panes_count = tiled_panes .get_panes() .filter(|(k, _)| match k { PaneId::Plugin(_) => false, PaneId::Terminal(_) => true, }) .count(); let floating_panes_count = floating_panes .get_panes() .filter(|(k, _)| match k { PaneId::Plugin(_) => false, PaneId::Terminal(_) => true, }) .count(); tiled_panes_count + floating_panes_count + 1 } impl Tab { // FIXME: Still too many arguments for clippy to be happy... #[allow(clippy::too_many_arguments)] pub fn new( id: usize, position: usize, name: String, display_area: Size, character_cell_size: Rc>>, stacked_resize: Rc>, stacked_pane_list: Rc>, sixel_image_store: Rc>, kitty_image_store: Rc>, os_api: Box, senders: ThreadSenders, max_panes: Option, style: Style, default_mode_info: ModeInfo, pane_frame_style: PaneFrameStyle, auto_layout: bool, connected_clients_in_app: Rc>>, // bool -> is_web_client session_is_mirrored: bool, client_id: Option, copy_options: CopyOptions, terminal_emulator_colors: Rc>, terminal_emulator_color_codes: Rc>>, swap_layouts: (Vec, Vec), default_shell: PathBuf, debug: bool, arrow_fonts: bool, styled_underlines: bool, osc8_hyperlinks: bool, explicitly_disable_kitty_keyboard_protocol: bool, default_editor: Option, web_clients_allowed: bool, web_sharing: WebSharing, current_pane_group: Rc>, currently_marking_pane_group: Rc>>, advanced_mouse_actions: bool, mouse_scroll_resize: bool, mouse_hover_effects: bool, mouse_hover_help_text: bool, focus_follows_mouse: bool, mouse_click_through: bool, web_server_ip: IpAddr, web_server_port: u16, mobile_tab_count: usize, ) -> Self { let default_name = if name.is_empty() { format!("Tab #{}", (id + 1).saturating_sub(mobile_tab_count)) } else { String::new() }; let name = if name.is_empty() { default_name.clone() } else { name }; let mut connected_clients = HashSet::new(); if let Some(client_id) = client_id { connected_clients.insert(client_id); } let initial_size = display_area; let viewport: Viewport = display_area.into(); let viewport = Rc::new(RefCell::new(viewport)); let display_area = Rc::new(RefCell::new(display_area)); let connected_clients = Rc::new(RefCell::new(connected_clients)); let mode_info = Rc::new(RefCell::new(HashMap::new())); let reserved_top_rows: Rc>> = Rc::new(RefCell::new(HashMap::new())); let fullscreen_covers_ui = Rc::new(RefCell::new(false)); let tiled_panes = TiledPanes::new( display_area.clone(), viewport.clone(), connected_clients.clone(), connected_clients_in_app.clone(), mode_info.clone(), character_cell_size.clone(), stacked_resize.clone(), stacked_pane_list.clone(), reserved_top_rows.clone(), fullscreen_covers_ui.clone(), session_is_mirrored, pane_frame_style, default_mode_info.clone(), style, os_api.clone(), senders.clone(), ); let floating_panes = FloatingPanes::new( display_area.clone(), viewport.clone(), connected_clients.clone(), connected_clients_in_app.clone(), mode_info.clone(), character_cell_size.clone(), fullscreen_covers_ui.clone(), pane_frame_style, session_is_mirrored, default_mode_info.clone(), style, os_api.clone(), senders.clone(), ); let clipboard_provider = match copy_options.command { Some(command) => ClipboardProvider::Command(CopyCommand::new(command)), None => ClipboardProvider::Osc52(copy_options.clipboard), }; let swap_layouts = SwapLayouts::new(swap_layouts, display_area.clone()); Tab { id, position, // set when a layout is applied; a tab created ad hoc has none purpose: None, tiled_panes, floating_panes, suppressed_panes: HashMap::new(), stacked_pane_list, reserved_top_rows, stack_lists: HashMap::new(), stack_list_of_member: HashMap::new(), stack_list_parked_pairs: HashMap::new(), next_stack_list_id: 0, stack_list_session_is_mirrored: session_is_mirrored, name: name.clone(), prev_name: name, default_name, size: initial_size, visible_to: None, max_panes, viewport, display_area, character_cell_size, sixel_image_store, kitty_image_store, synchronize_is_active: false, os_api, senders, should_clear_display_before_rendering: false, style, mode_info, default_mode_info, pane_frame_style, auto_layout, pending_vte_events: HashMap::new(), connected_clients, selecting_with_mouse_in_pane: None, pane_being_resized_with_mouse: None, link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, copy_on_select: copy_options.copy_on_select, terminal_emulator_colors, terminal_emulator_color_codes, pids_waiting_resize: HashSet::new(), cursor_positions_and_shape: HashMap::new(), is_pending: true, // will be switched to false once the layout is applied pending_instructions: vec![], swap_layouts, default_shell, debug, arrow_fonts, kitty_host_support: None, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, default_editor, web_clients_allowed, web_sharing, mouse_hover_pane_id: HashMap::new(), plugin_hover_pane_id: HashMap::new(), mouse_last_pane_id: HashMap::new(), mouse_help_text_visible: HashMap::new(), last_mouse_activity_time: HashMap::new(), last_hint_text: HashMap::new(), last_active_pane_scroll: HashMap::new(), current_pane_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, mouse_hover_effects, mouse_hover_help_text, focus_follows_mouse, mouse_click_through, frame_clicks: HashMap::new(), tiled_pane_being_dragged: HashMap::new(), drag_preview_swapped_with: HashMap::new(), responsive_collapsed: false, responsive_min_cols: 0, responsive_layout_name: String::from("stacked"), stack_list_max_rows: 0, expanded_stack_lists: HashSet::new(), stack_list_grid: false, stack_list_style: StackListStyle::default(), responsive_missing_layout_warned: false, connected_clients_in_app, web_server_ip, web_server_port, panes_with_pending_bell: HashSet::new(), tab_has_pending_bell: false, tab_bell_flash: false, tab_bell_ring: false, dimmed_clients: HashSet::new(), } } pub fn set_client_dimmed(&mut self, client_id: ClientId, dimmed: bool) { if dimmed { self.dimmed_clients.insert(client_id); } else { self.dimmed_clients.remove(&client_id); } self.tiled_panes.set_client_dimmed(client_id, dimmed); self.floating_panes.set_client_dimmed(client_id, dimmed); } pub fn stacked_pane_list_is_active(&self) -> bool { *self.stacked_pane_list.borrow() } pub fn pane_is_stack_list_member(&self, pane_id: &PaneId) -> bool { self.stack_list_of_member.contains_key(pane_id) } pub fn has_stack_lists(&self) -> bool { !self.stack_lists.is_empty() } fn stack_list_id_of_member(&self, pane_id: &PaneId) -> Option { self.stack_list_of_member.get(pane_id).copied() } pub fn pane_is_hidden_stack_list_member(&self, pane_id: &PaneId) -> bool { self.stack_list_id_of_member(pane_id) .and_then(|stack_list_id| self.stack_lists.get(&stack_list_id)) .map(|list| list.visible != *pane_id) .unwrap_or(false) } pub fn focus_hidden_stack_list_member(&mut self, pane_id: PaneId, client_id: ClientId) -> bool { self.swap_in_hidden_stack_list_member(pane_id, Some(client_id)) } fn swap_in_hidden_stack_list_member( &mut self, pane_id: PaneId, client_id: Option, ) -> bool { if !self.pane_is_hidden_stack_list_member(&pane_id) { return false; } match self.stack_list_id_of_member(&pane_id) { Some(stack_list_id) => { self.select_stack_list_member(stack_list_id, pane_id, client_id); true }, None => false, } } fn stack_list_member_at_point(&self, pane_id: PaneId, point: &Position) -> PaneId { let Some(stack_list_id) = self.stack_list_id_of_member(&pane_id) else { return pane_id; }; let Some(pane) = self.tiled_panes.get_pane(pane_id) else { return pane_id; }; // the same geometry the renderer laid the list out against let geom = pane.position_and_size(); if point.line() < geom.y as isize { return pane_id; } // A screen CELL, not the member's rank: the picker draws a window of the // members and may pack several per row, so neither the row nor the // column maps directly onto an index. Resolved through the same layout // the renderer used, which is the only way these cannot drift apart. let row = (point.line() - geom.y as isize) as usize; let Some((list, entry_x, layout)) = self.stack_list_layout_of(stack_list_id) else { return pane_id; }; let column = (point.column()).saturating_sub(entry_x); layout .cells .iter() .find(|cell| { cell.row == row && column >= cell.x_offset && column < cell.x_offset + cell.width }) .and_then(|cell| list.members.get(cell.member_index).copied()) .unwrap_or(pane_id) } pub fn suppressed_stack_list_members(&self) -> impl Iterator)> { self.suppressed_panes .iter() .filter(|(pane_id, _)| self.stack_list_of_member.contains_key(pane_id)) .map(|(pane_id, (_, pane))| (pane_id, pane)) } pub fn stack_list_serialization_geoms(&self) -> HashMap { let mut synthetic_geoms = HashMap::new(); if self.stack_lists.is_empty() { return synthetic_geoms; } let mut next_synthetic_stack_id = self .tiled_panes .get_panes() .filter_map(|(_, pane)| pane.position_and_size().stacked) .max() .map(|highest_stack_id| highest_stack_id + 1) .unwrap_or(0); for list in self.stack_lists.values() { let Some(visible_pane) = self.tiled_panes.get_pane(list.visible) else { continue; }; let full_rect = visible_pane.position_and_size(); let collapsed_member_count = list.members.len().saturating_sub(1); let mut running_y = full_rect.y; for member in &list.members { let mut member_geom = full_rect; member_geom.stacked = Some(next_synthetic_stack_id); member_geom.y = running_y; if *member == list.visible { member_geom.rows.set_inner( full_rect .rows .as_usize() .saturating_sub(collapsed_member_count), ); } else { member_geom.rows = Dimension::fixed(1); member_geom.logical_position = self .suppressed_panes .get(member) .and_then(|(_, pane)| pane.position_and_size().logical_position); } running_y += member_geom.rows.as_usize(); synthetic_geoms.insert(*member, member_geom); } next_synthetic_stack_id += 1; } synthetic_geoms } pub fn hidden_stack_list_members_for_serialization( &self, ) -> Vec<(PaneId, &Box, PaneGeom)> { let synthetic_geoms = self.stack_list_serialization_geoms(); let mut hidden_members = vec![]; for list in self.stack_lists.values() { for member in &list.members { if *member == list.visible { continue; } let Some(member_geom) = synthetic_geoms.get(member) else { continue; }; let resolved_pane = self .stack_list_parked_pairs .get(member) .and_then(|parked_pid| self.suppressed_panes.get(parked_pid)) .filter(|(is_scrollback_editor, _)| *is_scrollback_editor) .or_else(|| self.suppressed_panes.get(member)); if let Some((_, pane)) = resolved_pane { hidden_members.push((pane.pid(), pane, *member_geom)); } } } hidden_members } fn pane_parked_by(&self, id: &PaneId) -> Option { self.suppressed_panes .get(id) .map(|(_, parked)| parked.pid()) .filter(|parked_pid| parked_pid != id) } fn adopt_parked_pair(&mut self, member: PaneId) { if let Some(parked_pid) = self.stack_list_parked_pairs.remove(&member) { if let Some(parked_entry) = self.suppressed_panes.remove(&parked_pid) { self.suppressed_panes.insert(member, parked_entry); } } } /// Where the picker's visible window starts, and how many entries it shows. /// /// `stack_list_max_rows` caps the height; the window slides to keep the /// currently visible member on screen, so ordinary focus movement scrolls /// the list instead of needing its own binding. /// /// Both the reserved-row arithmetic and the renderer go through this. If /// they ever disagree the list draws into rows the geometry did not reserve, /// i.e. over the pane below it. /// Build the picker's cell layout for a stack. /// /// `available_cols` is the width the list may use, `widest_label` the widest /// entry label including its index prefix. In rows mode every cell spans the /// full width, exactly as before the grid existed. fn stack_list_layout( &self, stack_list_id: StackListId, member_count: usize, visible_rank: usize, available_cols: usize, widest_label: usize, ) -> StackListLayout { let expanded = self.expanded_stack_lists.contains(&stack_list_id); // from the style, so the column arithmetic and the renderer cannot // disagree about how wide an entry's decoration is let natural_cell = (widest_label + self.stack_list_style.decoration_cols()) .max(STACK_LIST_MIN_ENTRY_COLS); let columns = if self.stack_list_grid { // standard packing: n cells and n-1 gaps must fit ((available_cols + STACK_LIST_CELL_GAP_COLS) / (natural_cell + STACK_LIST_CELL_GAP_COLS).max(1)) .max(1) } else { 1 }; // one column keeps the historical full-width entry, which centres its own // label; only a real grid needs fixed-width cells and a centred block let (cell_width, cell_stride, left_margin) = if columns <= 1 { (available_cols, available_cols, 0) } else { let stride = natural_cell + STACK_LIST_CELL_GAP_COLS; // the block spans n cells and n-1 gaps; centre that, not n strides let block = columns * natural_cell + (columns - 1) * STACK_LIST_CELL_GAP_COLS; ( natural_cell, stride, available_cols.saturating_sub(block) / 2, ) }; let max_rows = if expanded { 0 } else { self.stack_list_max_rows }; let (start, shown, rows) = stack_list_window_core(member_count, visible_rank, columns, max_rows); let cells = (0..shown) .map(|i| StackListCell { member_index: start + i, x_offset: left_margin + (i % columns) * cell_stride, row: i / columns, width: cell_width, }) .collect(); StackListLayout { cells, rows, hidden: member_count - shown, } } /// Text for the picker's indicator row, if it has one. /// /// Rendered in the padding row the list already reserves, so it never costs /// a row or displaces a member. Also the click target that toggles the /// expansion, which is why collapse gets a label too: an invisible way back /// out is not an affordance. fn stack_list_indicator(&self, stack_list_id: StackListId, hidden: usize) -> Option { if hidden > 0 { Some(format!("+{} more", hidden)) } else if self.expanded_stack_lists.contains(&stack_list_id) { Some(String::from("- fewer")) } else { None } } /// Toggle a stack's expansion if `point` is on its indicator row. /// /// Returns true if the click was consumed. Expanding changes the reserved /// row count, hence the refresh: that is a one-off pane resize, which is /// why this hangs off a deliberate click and not off hover. fn toggle_stack_list_expansion_at_point(&mut self, pane_id: PaneId, point: &Position) -> bool { let Some(stack_list_id) = self.stack_list_id_of_member(&pane_id) else { return false; }; let Some(visible) = self.stack_lists.get(&stack_list_id).map(|list| list.visible) else { return false; }; let Some(pane) = self.tiled_panes.get_pane(visible) else { return false; }; let geom = pane.position_and_size(); if point.line() < geom.y as isize { return false; } let row = (point.line() - geom.y as isize) as usize; let Some((_list, _entry_x, layout)) = self.stack_list_layout_of(stack_list_id) else { return false; }; if row != layout.rows || self .stack_list_indicator(stack_list_id, layout.hidden) .is_none() { return false; } if !self.expanded_stack_lists.remove(&stack_list_id) { self.expanded_stack_lists.insert(stack_list_id); } self.refresh_or_dissolve_stack_list(stack_list_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); true } /// Geometry and label metrics the picker needs, read from the stack's visible /// pane. Shared so the renderer, the row budget and the hit-test all derive /// the column count from identical inputs. /// /// Returns (list left edge, width available to the list, widest label). fn stack_list_metrics(&self, list: &StackList) -> Option<(usize, usize, usize)> { let pane = self.tiled_panes.get_pane(list.visible)?; let rect = pane.position_and_size(); let content_offset = pane.get_content_offset(); let entry_x = rect.x + content_offset.left; let available_cols = rect .cols .as_usize() .saturating_sub(content_offset.left + content_offset.right); // measured over ALL members, not just the shown ones, so the cell width // does not jitter as the window scrolls let widest_label = list .members .iter() .enumerate() .filter_map(|(rank, member)| { let pane = if *member == list.visible { self.tiled_panes.get_pane(*member).map(|p| &**p) } else { self.suppressed_panes.get(member).map(|(_, p)| &**p) }; pane.map(|pane| { stack_index_prefix(rank + 1).width() + pane.stack_list_entry_label().width() }) }) .max() .unwrap_or(0); Some((entry_x, available_cols, widest_label)) } /// The picker layout for a stack, or None if its pane has gone. fn stack_list_layout_of(&self, stack_list_id: StackListId) -> Option<(StackList, usize, StackListLayout)> { let list = self.stack_lists.get(&stack_list_id)?.clone(); let (entry_x, available_cols, widest_label) = self.stack_list_metrics(&list)?; let visible_rank = list.rank_of(&list.visible).unwrap_or(0); let layout = self.stack_list_layout( stack_list_id, list.members.len(), visible_rank, available_cols, widest_label, ); Some((list, entry_x, layout)) } fn refresh_or_dissolve_stack_list(&mut self, stack_list_id: StackListId) { let (member_count, visible) = match self.stack_lists.get(&stack_list_id) { Some(list) => (list.members.len(), list.visible), None => return, }; if member_count <= 1 { self.dissolve_stack_list(stack_list_id); } else { let rows = self .stack_list_layout_of(stack_list_id) .map(|(_list, _x, layout)| layout.rows) .unwrap_or(member_count); let previous = self .reserved_top_rows .borrow_mut() .insert(visible, rows + 1); self.tiled_panes.reapply_pane_frames(); self.resize_stack_list_hidden_members(stack_list_id); if previous != Some(rows + 1) { // the list changed height, so the visible pane moved: rows the // list used to own are now nobody's and keep their old contents // until something repaints them self.set_force_render(); self.set_should_clear_display_before_rendering(); } } } fn select_stack_list_member( &mut self, stack_list_id: StackListId, target_member: PaneId, client_id: Option, ) { let current_visible = match self.stack_lists.get(&stack_list_id) { Some(list) => list.visible, None => return, }; if current_visible == target_member { return; } let pane_parked_by_current_visible = self.pane_parked_by(¤t_visible); let target_box = match self.suppressed_panes.remove(&target_member) { Some((_is_scrollback_editor, pane)) => pane, None => { log::error!( "stack-list member {:?} not found in suppressed panes", target_member ); return; }, }; let removed_visible = self.tiled_panes.replace_pane(current_visible, target_box); if let Some(removed_visible) = removed_visible { self.insert_suppressed_pane(current_visible, (false, removed_visible)); if let Some(parked_pid) = pane_parked_by_current_visible { self.stack_list_parked_pairs .insert(current_visible, parked_pid); } } self.adopt_parked_pair(target_member); if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { list.visible = target_member; } self.reserved_top_rows.borrow_mut().remove(¤t_visible); if let Some(client_id) = client_id { self.tiled_panes.focus_pane(target_member, client_id); } self.refresh_or_dissolve_stack_list(stack_list_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); } fn move_focus_within_stack_list(&mut self, client_id: ClientId, down: bool) -> bool { let active_pane_id = match self.tiled_panes.get_active_pane_id(client_id) { Some(id) => id, None => return false, }; let stack_list_id = match self.stack_list_id_of_member(&active_pane_id) { Some(id) => id, None => return false, }; let target_member = { let list = match self.stack_lists.get(&stack_list_id) { Some(l) => l, None => return false, }; if list.visible != active_pane_id { return false; } let rank = match list.rank_of(&active_pane_id) { Some(r) => r, None => return false, }; let target_rank = if down { rank + 1 } else if rank == 0 { return false; } else { rank - 1 }; match list.members.get(target_rank) { Some(member) => *member, None => return false, } }; self.select_stack_list_member(stack_list_id, target_member, Some(client_id)); true } fn close_stack_list_member(&mut self, id: PaneId, exit_status: Option) -> bool { match self.remove_stack_list_member(id) { StackListRemoval::Removed(removed) => { if let (Some(exit_status), Some(mut removed)) = (exit_status, removed) { removed.update_exit_status(exit_status); } true }, StackListRemoval::DissolvedInPlace | StackListRemoval::NotRemoved => false, } } fn remove_stack_list_member(&mut self, id: PaneId) -> StackListRemoval { let stack_list_id = match self.stack_list_id_of_member(&id) { Some(sid) => sid, None => return StackListRemoval::NotRemoved, }; let is_visible = self .stack_lists .get(&stack_list_id) .map(|l| l.visible == id) .unwrap_or(false); let removed = if is_visible { let (members, rank) = { let list = match self.stack_lists.get(&stack_list_id) { Some(l) => l, None => return StackListRemoval::NotRemoved, }; (list.members.clone(), list.rank_of(&id).unwrap_or(0)) }; let promote_target = members .iter() .skip(rank + 1) .next() .copied() .or_else(|| members[..rank].iter().last().copied()); let promote_target = match promote_target { Some(target) => target, None => { self.dissolve_stack_list(stack_list_id); self.reserved_top_rows.borrow_mut().remove(&id); return StackListRemoval::DissolvedInPlace; }, }; let target_box = match self.suppressed_panes.remove(&promote_target) { Some((_is_scrollback_editor, pane)) => pane, None => { log::error!("stack-list promote target not found in suppressed panes"); return StackListRemoval::NotRemoved; }, }; let removed_visible = self.tiled_panes.replace_pane(id, target_box); self.adopt_parked_pair(promote_target); self.stack_list_of_member.remove(&id); self.reserved_top_rows.borrow_mut().remove(&id); if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { list.members.retain(|m| *m != id); list.visible = promote_target; } self.refresh_or_dissolve_stack_list(stack_list_id); removed_visible } else { let removed = self.suppressed_panes.remove(&id).map(|(_, p)| p); let substitute_member = self .stack_list_parked_pairs .remove(&id) .filter(|parked_pid| self.suppressed_panes.contains_key(parked_pid)); self.stack_list_of_member.remove(&id); self.reserved_top_rows.borrow_mut().remove(&id); if let Some(substitute_member) = substitute_member { self.stack_list_of_member .insert(substitute_member, stack_list_id); if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { for member in list.members.iter_mut() { if *member == id { *member = substitute_member; } } } } else if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { list.members.retain(|m| *m != id); } self.refresh_or_dissolve_stack_list(stack_list_id); removed }; self.set_force_render(); self.set_should_clear_display_before_rendering(); StackListRemoval::Removed(removed) } fn track_stack_list_member_id_swap(&mut self, old_id: PaneId, new_id: PaneId) { let stack_list_id = match self.stack_list_id_of_member(&old_id) { Some(sid) => sid, None => return, }; self.stack_list_of_member.remove(&old_id); self.stack_list_of_member.insert(new_id, stack_list_id); let mut swapped_visible_member = false; if let Some(list) = self.stack_lists.get_mut(&stack_list_id) { for member in list.members.iter_mut() { if *member == old_id { *member = new_id; } } if list.visible == old_id { list.visible = new_id; swapped_visible_member = true; } } { let mut reserved = self.reserved_top_rows.borrow_mut(); if let Some(reserved_rows) = reserved.remove(&old_id) { reserved.insert(new_id, reserved_rows); } } if swapped_visible_member { self.refresh_or_dissolve_stack_list(stack_list_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); } } fn dissolve_stack_list(&mut self, stack_list_id: StackListId) { if let Some(list) = self.stack_lists.get(&stack_list_id) { let members = list.members.clone(); self.drop_stack_list_ledger(stack_list_id, &members); self.tiled_panes.reapply_pane_frames(); } } fn drop_stack_list_ledger(&mut self, stack_list_id: StackListId, members: &[PaneId]) { self.stack_lists.remove(&stack_list_id); self.expanded_stack_lists.remove(&stack_list_id); let mut reserved = self.reserved_top_rows.borrow_mut(); for member in members { self.stack_list_of_member.remove(member); self.stack_list_parked_pairs.remove(member); reserved.remove(member); } } fn degroupify(&mut self, stack_list_id: StackListId) { let (members, visible) = match self.stack_lists.get(&stack_list_id) { Some(list) => (list.members.clone(), list.visible), None => return, }; let n = members.len(); if n == 0 || !self.tiled_panes.panes_contain(&visible) { self.drop_stack_list_ledger(stack_list_id, &members); return; } let geoms = self.tiled_panes.stack_panes(visible, n); if geoms.len() != n { log::error!("degroupify: could not compute classic stack geoms"); return; } for (i, member) in members.iter().enumerate() { let mut geom = geoms[i]; if *member == visible { geom.logical_position = self .tiled_panes .get_pane(*member) .and_then(|p| p.position_and_size().logical_position); self.tiled_panes.set_geom_for_pane_with_id(member, geom); } else if let Some((_is_scrollback_editor, mut pane)) = self.suppressed_panes.remove(member) { geom.logical_position = pane.position_and_size().logical_position; pane.set_geom(geom); self.tiled_panes.add_pane_with_existing_geom(*member, pane); self.adopt_parked_pair(*member); } } self.drop_stack_list_ledger(stack_list_id, &members); self.tiled_panes.expand_pane_in_stack(visible); self.tiled_panes.reapply_pane_frames(); self.set_force_render(); self.set_should_clear_display_before_rendering(); } // the classic layout solver cannot see suppressed stack-list members; dissolve // lists into in-grid stacks first, groupify_all re-forms them on mode entry fn degroupify_all(&mut self) { let ids: Vec = self.stack_lists.keys().copied().collect(); for id in ids { self.degroupify(id); } } fn dissolve_stack_lists_for_classic_mutation(&mut self) { if self.stacked_pane_list_is_active() { self.degroupify_all(); } } pub fn sync_stacked_pane_list_mode(&mut self) { if self.stacked_pane_list_is_active() { self.groupify_all(); } else if self.has_stack_lists() { self.degroupify_all(); } } // the inverse of degroupify_all: adopts classic in-grid stacks into stack // lists, suppressing all but the visible member of each fn groupify_all(&mut self) { if self.tiled_panes.fullscreen_is_active() { return; } let mut stacks: HashMap> = HashMap::new(); for (id, pane) in self.tiled_panes.get_panes() { let geom = pane.position_and_size(); if let Some(stack_id) = geom.stacked { let is_expanded = !geom.rows.is_fixed(); stacks .entry(stack_id) .or_default() .push((geom.y, *id, is_expanded)); } } let mut converted_any = false; for (_stack_id, mut members) in stacks { if members.len() < 2 { continue; } members.sort_by_key(|(y, _, _)| *y); let representative = members[0].1; let rect = match self.tiled_panes.position_and_size_of_stack(&representative) { Some(r) => r, None => continue, }; let visible = members .iter() .find(|(_, _, expanded)| *expanded) .map(|(_, id, _)| *id) .unwrap_or_else(|| members.last().map(|(_, id, _)| *id).unwrap()); let ordered_ids: Vec = members.iter().map(|(_, id, _)| *id).collect(); let logical_position = self .tiled_panes .get_pane(visible) .and_then(|p| p.position_and_size().logical_position); for (_, id, _) in &members { if *id == visible { continue; } if let Some(pane) = self.tiled_panes.extract_pane(*id) { let pane_parked_by_member = self.pane_parked_by(id); self.insert_suppressed_pane(*id, (false, pane)); if let Some(parked_pid) = pane_parked_by_member { self.stack_list_parked_pairs.insert(*id, parked_pid); } } } if let Some(vpane) = self.tiled_panes.get_pane_mut(visible) { let mut g = rect; g.stacked = None; g.logical_position = logical_position; vpane.set_geom(g); } let stack_list_id = self.next_stack_list_id; self.next_stack_list_id += 1; for id in &ordered_ids { self.stack_list_of_member.insert(*id, stack_list_id); } self.stack_lists.insert( stack_list_id, StackList { members: ordered_ids, visible, }, ); self.refresh_or_dissolve_stack_list(stack_list_id); converted_any = true; } if converted_any { self.set_force_render(); self.set_should_clear_display_before_rendering(); } } fn resize_all_stack_list_hidden_members(&mut self) { let ids: Vec = self.stack_lists.keys().copied().collect(); for id in ids { self.resize_stack_list_hidden_members(id); } } fn resize_stack_list_hidden_members(&mut self, stack_list_id: StackListId) { let (visible_geom, visible_offset, hidden_members) = { let list = match self.stack_lists.get(&stack_list_id) { Some(l) => l, None => return, }; let (geom, offset) = match self.tiled_panes.get_pane(list.visible) { Some(p) => (p.position_and_size(), p.get_content_offset()), None => return, }; let hidden: Vec = list .members .iter() .copied() .filter(|m| *m != list.visible) .collect(); (geom, offset, hidden) }; for member in hidden_members { if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(&member) { pane.set_geom(visible_geom); pane.set_content_offset(visible_offset); let _ = resize_pty!(pane, self.os_api, self.senders, self.character_cell_size); } } } fn render_stack_list_headers( &mut self, output: &mut Output, client_id_override: Option, ) -> Result<()> { if self.stack_lists.is_empty() || self.tiled_panes.fullscreen_is_active() { return Ok(()); } let mut connected_clients: HashSet = { self.connected_clients.borrow().iter().copied().collect() }; if let Some(override_id) = client_id_override { connected_clients.insert(override_id); } if connected_clients.is_empty() { return Ok(()); } let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 }; let should_draw_pane_frames = self.pane_frame_style.draws_full_frames(); let session_is_mirrored = self.stack_list_session_is_mirrored; let current_pane_group: HashMap> = { self.current_pane_group.borrow().clone_inner() }; let active_panes: HashMap = connected_clients .iter() .filter_map(|c| self.tiled_panes.get_active_pane_id(*c).map(|p| (*c, p))) .collect(); let client_modes: Vec<(ClientId, InputMode)> = { let mode_info = self.mode_info.borrow(); connected_clients .iter() .map(|c| { let mode = mode_info.get(c).unwrap_or(&self.default_mode_info).mode; (*c, mode) }) .collect() }; let lists: Vec<(StackListId, StackList)> = self .stack_lists .iter() .map(|(id, list)| (*id, list.clone())) .collect(); for (stack_list_id, list) in lists { let (rect, visible_content_offset) = match self.tiled_panes.get_pane(list.visible) { Some(p) => (p.position_and_size(), p.get_content_offset()), None => continue, }; let entry_x = rect.x + visible_content_offset.left; let entry_cols = rect .cols .as_usize() .saturating_sub(visible_content_offset.left + visible_content_offset.right); // NB: measured INCLUDING the index prefix each entry will be // rendered with, otherwise every entry is one prefix too narrow and // the list misaligns. let widest_member_title = list .members .iter() .enumerate() .filter_map(|(rank, member)| { let pane = if *member == list.visible { self.tiled_panes.get_pane(*member).map(|p| &**p) } else { self.suppressed_panes.get(member).map(|(_, p)| &**p) }; pane.map(|pane| { stack_index_prefix(rank + 1).width() + pane.stack_list_entry_label().width() }) }) .max() .unwrap_or(0); let stack_is_focused = active_panes.values().any(|p_id| *p_id == list.visible); let visible_rank = list.rank_of(&list.visible).unwrap_or(0); let layout = self.stack_list_layout( stack_list_id, list.members.len(), visible_rank, entry_cols, widest_member_title, ); let indicator = self.stack_list_indicator(stack_list_id, layout.hidden); // Blank the parts of the list region no cell covers. // // Entries are drawn as individual cells, so the gaps between them // keep whatever was drawn there before -- and the layout genuinely // changes shape when a pane is added, because an unnamed pane's // "Pane #N" label is far wider than a named one and re-flows the // whole grid. // // Only the gaps, never the cells: chunks are not composited // last-wins, so blanking a region a cell also draws to can erase the // cell instead of the stale pixels underneath it. for row in 0..layout.rows { for (start, end) in stack_list_row_gaps(&layout, row, entry_cols) { let blanks = vec![EMPTY_TERMINAL_CHARACTER; end - start]; output.add_character_chunks_to_multiple_clients( vec![CharacterChunk::new(blanks, entry_x + start, rect.y + row)], connected_clients.iter().copied(), None, )?; } } let cells = layout.cells.clone(); for cell in cells { let rank = cell.member_index; let Some(member) = list.members.get(rank).copied() else { continue; }; let member = &member; let mut header_geom = rect; header_geom.x = entry_x + cell.x_offset; header_geom.cols.set_inner(cell.width); // cell.row is the position on screen, `rank` stays the member's // real position in the stack, so the numbering keeps meaning // something once the window has scrolled away from the top header_geom.y = rect.y + cell.row; header_geom.rows = Dimension::fixed(1); header_geom.stacked = None; let pane_box = if *member == list.visible { self.tiled_panes.get_pane_mut(*member) } else { self.suppressed_panes.get_mut(member).map(|(_, p)| p) }; let pane_box = match pane_box { Some(p) => p, None => continue, }; let mut pane_contents_and_ui = PaneContentsAndUi::new( pane_box, output, self.style, &active_panes, multiple_users_exist_in_session, None, false, false, should_draw_pane_frames, &self.mouse_hover_pane_id, current_pane_group.clone(), false, false, self.mouse_scroll_resize, self.dimmed_clients.clone(), ); pane_contents_and_ui.set_frame_geom_override(Some(header_geom)); pane_contents_and_ui.set_stack_list_entry( Some(widest_member_title), *member == list.visible, stack_is_focused, Some(rank + 1), self.stack_list_style, ); for (client_id, client_mode) in &client_modes { pane_contents_and_ui.render_pane_frame( *client_id, *client_mode, session_is_mirrored, false, true, )?; } } // The row after the entries is already part of the reserved budget, // so the "+N hidden" indicator costs nothing extra and never steals a // row from a real entry. let mut padding_row = Vec::with_capacity(entry_cols); if let Some(indicator) = indicator { let indicator_start = entry_cols.saturating_sub(indicator.width()) / 2; for _ in 0..indicator_start { padding_row.push(EMPTY_TERMINAL_CHARACTER); } for character in indicator.chars() { padding_row.push(TerminalCharacter::new_styled( character, EMPTY_TERMINAL_CHARACTER.styles, )); } } for _ in padding_row.len()..entry_cols { padding_row.push(EMPTY_TERMINAL_CHARACTER); } let padding_chunk = CharacterChunk::new(padding_row, entry_x, rect.y + layout.rows); output.add_character_chunks_to_multiple_clients( vec![padding_chunk], connected_clients.iter().copied(), None, )?; } 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>, client_id: ClientId, blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { // The tab's purpose rides its root layout node (the `tab` node itself), // which is why this is read here and not from any pane. if layout.purpose.is_some() { self.purpose = layout .purpose .as_deref() .and_then(zellij_utils::input::layout::sanitize_pane_purpose); } self.swap_layouts .set_base_layout((layout.clone(), floating_panes_layout.clone())); match LayoutApplier::new( &self.viewport, &self.senders, &self.sixel_image_store, &self.kitty_image_store, &self.link_handler, &self.terminal_emulator_colors, &self.terminal_emulator_color_codes, &self.character_cell_size, &self.connected_clients_in_app, &self.style, &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_terminal, ) .apply_layout( layout, floating_panes_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, client_id, ) { Ok(should_show_floating_panes) => { if should_show_floating_panes && !self.floating_panes.panes_are_visible() { self.toggle_floating_panes(Some(client_id), None, None) .non_fatal(); } else if !should_show_floating_panes && self.floating_panes.panes_are_visible() { self.toggle_floating_panes(Some(client_id), None, None) .non_fatal(); } self.tiled_panes.reapply_pane_frames(); self.is_pending = false; self.apply_buffered_instructions().non_fatal(); }, Err(e) => { // TODO: this should only happen due to an erroneous layout created by user // configuration that was somehow not caught in our KDL layout parser // we should still be able to properly recover from this with a useful error // message though log::error!("Failed to apply layout: {}", e); self.tiled_panes.reapply_pane_frames(); self.is_pending = false; self.apply_buffered_instructions().non_fatal(); }, } if let Some(supported) = self.kitty_host_support { self.tiled_panes.update_pane_kitty_host_support(supported); self.floating_panes .update_pane_kitty_host_support(supported); } Ok(()) } pub fn override_layout( &mut self, layout: TiledPaneLayout, floating_panes_layout: Vec, mut new_swap_tiled_layouts: Option>, mut new_swap_floating_layouts: Option>, new_terminal_ids: Vec<(u32, HoldForCommand)>, new_floating_terminal_ids: Vec<(u32, HoldForCommand)>, new_plugin_ids: HashMap>, retain_existing_terminal_panes: bool, retain_existing_plugin_panes: bool, client_id: ClientId, blocking_terminal: Option<(u32, NotificationEnd)>, ) -> Result<()> { let new_swap_tiled_layouts = new_swap_tiled_layouts.take().unwrap_or_else(|| vec![]); let new_swap_floating_layouts = new_swap_floating_layouts.take().unwrap_or_else(|| vec![]); self.swap_layouts .set_swap_tiled_layouts(new_swap_tiled_layouts); self.swap_layouts .set_swap_floating_layouts(new_swap_floating_layouts); self.swap_layouts .set_base_layout((layout.clone(), floating_panes_layout.clone())); match LayoutApplier::new( &self.viewport, &self.senders, &self.sixel_image_store, &self.kitty_image_store, &self.link_handler, &self.terminal_emulator_colors, &self.terminal_emulator_color_codes, &self.character_cell_size, &self.connected_clients_in_app, &self.style, &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_terminal, ) .override_layout( layout, floating_panes_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, retain_existing_terminal_panes, retain_existing_plugin_panes, client_id, ) { Ok(should_show_floating_panes) => { if should_show_floating_panes && !self.floating_panes.panes_are_visible() { self.toggle_floating_panes(Some(client_id), None, None) .non_fatal(); } else if !should_show_floating_panes && self.floating_panes.panes_are_visible() { self.toggle_floating_panes(Some(client_id), None, None) .non_fatal(); } // this is essentially another pass of the layout applier // we do this because the layout applier does not know about swap layouts, and in // this case we might have had to re-add existing panes that were not in the // overridden layout (eg. if we had more panes than were in the layout). In such a // case, we would like to make sure these extra panes fit the current swap layout self.swap_layouts.set_is_tiled_damaged(); self.swap_layouts.set_is_floating_damaged(); let _ = self.relayout_tiled_panes(false); let _ = self.relayout_floating_panes(false); self.tiled_panes.reapply_pane_frames(); self.is_pending = false; LayoutApplier::offset_viewport( self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, self.pane_frame_style, ); self.apply_buffered_instructions().non_fatal(); }, Err(e) => { // TODO: this should only happen due to an erroneous layout created by user // configuration that was somehow not caught in our KDL layout parser // we should still be able to properly recover from this with a useful error // message though log::error!("Failed to apply layout: {}", e); }, } Ok(()) } pub fn swap_layout_info(&self) -> (Option, bool) { if self.floating_panes.panes_are_visible() { self.swap_layouts.floating_layout_info() } else { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); if selectable_tiled_panes.count() + self.suppressed_stack_list_members().count() > 1 { self.swap_layouts.tiled_layout_info() } else { // no layout for single pane (None, false) } } } fn relayout_floating_panes(&mut self, search_backwards: bool) -> Result<()> { if let Some(layout_candidate) = self .swap_layouts .swap_floating_panes(&self.floating_panes, search_backwards) { LayoutApplier::new( &self.viewport, &self.senders, &self.sixel_image_store, &self.kitty_image_store, &self.link_handler, &self.terminal_emulator_colors, &self.terminal_emulator_color_codes, &self.character_cell_size, &self.connected_clients_in_app, &self.style, &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, None, ) .apply_floating_panes_layout_to_existing_panes(&layout_candidate) .non_fatal(); } self.set_force_render(); self.senders .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) .with_context(|| format!("failed to apply cached resizes"))?; Ok(()) } /// Apply a resolved swap layout to the existing panes. /// /// Extracted from relayout_tiled_panes so the responsive-layout switch /// applies layouts through exactly the same path rather than a lookalike. fn apply_tiled_layout_candidate(&mut self, layout_candidate: &TiledPaneLayout) { let application_res = LayoutApplier::new( &self.viewport, &self.senders, &self.sixel_image_store, &self.kitty_image_store, &self.link_handler, &self.terminal_emulator_colors, &self.terminal_emulator_color_codes, &self.character_cell_size, &self.connected_clients_in_app, &self.style, &self.display_area, &mut self.tiled_panes, &mut self.floating_panes, self.pane_frame_style, &mut self.focus_pane_id, &self.os_api, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, None, ) .apply_tiled_panes_layout_to_existing_panes(layout_candidate); if application_res.is_err() { self.swap_layouts.set_is_tiled_damaged(); application_res.non_fatal(); } } fn relayout_tiled_panes(&mut self, search_backwards: bool) -> Result<()> { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } self.dissolve_stack_lists_for_classic_mutation(); if let Some(layout_candidate) = self .swap_layouts .swap_tiled_panes(&self.tiled_panes, search_backwards) { self.apply_tiled_layout_candidate(&layout_candidate); } else { self.swap_layouts.set_is_tiled_damaged(); } self.tiled_panes.reapply_pane_frames(); let display_area = *self.display_area.borrow(); // we do this so that the new swap layout has a chance to pass through the constraint system self.tiled_panes.resize(display_area); self.set_should_clear_display_before_rendering(); self.senders .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) .with_context(|| format!("failed to apply cached resizes"))?; Ok(()) } pub fn previous_swap_layout(&mut self) -> Result<()> { let search_backwards = true; if self.floating_panes.panes_are_visible() { self.relayout_floating_panes(search_backwards)?; } else { self.relayout_tiled_panes(search_backwards)?; } Ok(()) } pub fn next_swap_layout(&mut self) -> Result<()> { let search_backwards = false; if self.floating_panes.panes_are_visible() { self.relayout_floating_panes(search_backwards)?; } else { self.relayout_tiled_panes(search_backwards)?; } Ok(()) } pub fn apply_buffered_instructions(&mut self) -> Result<()> { let buffered_instructions: Vec = self.pending_instructions.drain(..).collect(); for buffered_instruction in buffered_instructions { match buffered_instruction { BufferedTabInstruction::SetPaneSelectable(pane_id, selectable) => { self.set_pane_selectable(pane_id, selectable); }, BufferedTabInstruction::HandlePtyBytes(terminal_id, bytes) => { self.handle_pty_bytes(terminal_id, bytes)?; }, BufferedTabInstruction::HoldPane( terminal_id, exit_status, is_first_run, run_command, ) => { self.hold_pane(terminal_id, exit_status, is_first_run, run_command); }, } } Ok(()) } pub fn rename_session(&mut self, new_session_name: String) -> Result<()> { { let mode_infos = &mut self.mode_info.borrow_mut(); for (_client_id, mode_info) in mode_infos.iter_mut() { mode_info.session_name = Some(new_session_name.clone()); } self.default_mode_info.session_name = Some(new_session_name); } self.update_input_modes() } pub fn update_input_modes(&mut self) -> Result<()> { // this updates only this tab's plugins with the client's input mode let mode_infos = self.mode_info.borrow(); let mut plugin_updates = vec![]; let currently_marking_pane_group = self.currently_marking_pane_group.borrow(); let tab_plugin_ids = self.get_plugin_ids(); for client_id in self.connected_clients.borrow().iter() { let mut mode_info = mode_infos .get(client_id) .unwrap_or(&self.default_mode_info) .clone(); mode_info.shell = Some(self.default_shell.clone()); mode_info.editor = self.default_editor.clone(); mode_info.web_clients_allowed = Some(self.web_clients_allowed); mode_info.web_sharing = Some(self.web_sharing); mode_info.currently_marking_pane_group = currently_marking_pane_group.get(client_id).copied(); mode_info.web_server_ip = Some(self.web_server_ip); mode_info.web_server_port = Some(self.web_server_port); mode_info.is_web_client = self .connected_clients_in_app .borrow() .get(&client_id) .copied(); if cfg!(feature = "web_server_capability") { mode_info.web_server_capability = Some(true); } else { mode_info.web_server_capability = Some(false); } mode_info.pane_frame_style = Some(self.pane_frame_style); for plugin_id in &tab_plugin_ids { plugin_updates.push(( Some(*plugin_id), Some(*client_id), Event::ModeUpdate(mode_info.clone()), )); } } if !plugin_updates.is_empty() { self.senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .with_context(|| format!("failed to update plugins with mode info"))?; } Ok(()) } fn resolve_hint_text(&self, client_id: ClientId) -> BTreeMap { let focused_pane_id = self.get_active_pane_id(client_id); let hovered_pane_id = self.mouse_hover_pane_id.get(&client_id).copied(); if let Some(hovered_pane_id) = hovered_pane_id { if Some(hovered_pane_id) != focused_pane_id { return hover_hint_variants(); } } if let Some(focused_pane) = self.get_active_pane(client_id) { if focused_pane.is_held() { let exited = focused_pane.exited(); let is_first_run = !exited; let exit_status = if exited { Some(match focused_pane.exit_status() { Some(exit_code) => HintExitStatus::Code(exit_code), None => HintExitStatus::Exited, }) } else { None }; return held_hint_variants(is_first_run, exit_status); } } let resize_help_visible = self .mouse_help_text_visible .get(&client_id) .copied() .unwrap_or(false); if resize_help_visible { let selectable_pane_count = self.get_selectable_tiled_panes().count() + self.get_selectable_floating_panes().count(); if selectable_pane_count > 1 && !self.is_fullscreen_active() { let focused_pane_is_floating = self.floating_panes.panes_are_visible(); return resize_hint_variants(focused_pane_is_floating, self.mouse_scroll_resize); } } BTreeMap::new() } pub fn emit_hint_text_if_changed(&mut self) -> Result<()> { let connected_clients: Vec = self.connected_clients.borrow().iter().copied().collect(); let mut plugin_updates = vec![]; for client_id in connected_clients { let hint_text = self.resolve_hint_text(client_id); if self.last_hint_text.get(&client_id) != Some(&hint_text) { self.last_hint_text.insert(client_id, hint_text.clone()); plugin_updates.push((None, Some(client_id), Event::HintText(hint_text))); } } if !plugin_updates.is_empty() { self.senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .with_context(|| format!("failed to update plugins with hint text"))?; } Ok(()) } pub fn clear_hint_text_cache(&mut self) { self.last_hint_text.clear(); } fn get_selectable_tiled_content_panes( &self, ) -> impl Iterator)> { self.get_selectable_tiled_panes() .filter(|(_, p)| !p.borderless()) } pub fn single_pane_titles(&self) -> bool { self.pane_frame_style.draws_titles() && self.get_selectable_tiled_content_panes().count() == 1 } fn tab_name_is_default(&self) -> bool { !self.name.is_empty() && self.name == self.default_name } pub fn single_pane_tab_name(&self) -> Option { if !self.single_pane_titles() { return None; } let (_, pane) = self.get_selectable_tiled_content_panes().next()?; let mut base = if self.tab_name_is_default() && pane.has_explicit_title() { pane.current_title() } else { self.name.clone() }; if pane.is_held() && pane.exited() { match pane.exit_status() { Some(exit_code) => base.push_str(&format!(" [ EXIT CODE: {} ] ", exit_code)), None => base.push_str(" [ EXITED ] "), } } Some(base) } fn resolve_active_pane_scroll(&self, _client_id: ClientId) -> Option<(usize, usize)> { if !self.single_pane_titles() { return None; } let (_, pane) = self.get_selectable_tiled_content_panes().next()?; let (position, length) = pane.scroll_position(); if position > 0 || length > 0 { Some((position, length)) } else { None } } pub fn emit_active_pane_scroll_if_changed(&mut self) -> Result<()> { let connected_clients: Vec = self.connected_clients.borrow().iter().copied().collect(); let mut plugin_updates = vec![]; for client_id in connected_clients { let active_pane_scroll = self.resolve_active_pane_scroll(client_id); if self.last_active_pane_scroll.get(&client_id) != Some(&active_pane_scroll) { self.last_active_pane_scroll .insert(client_id, active_pane_scroll); plugin_updates.push(( None, Some(client_id), Event::ActivePaneScroll(active_pane_scroll), )); } } if !plugin_updates.is_empty() { self.senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .with_context(|| format!("failed to update plugins with active pane scroll"))?; } Ok(()) } pub fn add_client(&mut self, client_id: ClientId, mode_info: Option) -> Result<()> { let other_clients_exist_in_tab = { !self.connected_clients.borrow().is_empty() }; if other_clients_exist_in_tab { if let Some(first_active_floating_pane_id) = self.floating_panes.first_active_floating_pane_id() { self.floating_panes .focus_pane_if_client_not_focused(first_active_floating_pane_id, client_id); } if let Some(first_active_tiled_pane_id) = self.tiled_panes.first_active_pane_id() { self.tiled_panes .focus_pane_if_client_not_focused(first_active_tiled_pane_id, client_id); } self.connected_clients.borrow_mut().insert(client_id); self.mode_info.borrow_mut().insert( client_id, mode_info.unwrap_or_else(|| self.default_mode_info.clone()), ); } else { let mut pane_ids: Vec = self.tiled_panes.pane_ids().copied().collect(); if pane_ids.is_empty() { // no panes here, bye bye return Ok(()); } let focus_pane_id = if let Some(id) = self.focus_pane_id { id } else { pane_ids.sort(); // TODO: make this predictable pane_ids.retain(|p| !self.tiled_panes.panes_to_hide_contains(*p)); *(pane_ids.get(0).with_context(|| { format!("failed to acquire id of focused pane while adding client {client_id}",) })?) }; self.tiled_panes .focus_pane_if_client_not_focused(focus_pane_id, client_id); self.floating_panes .focus_first_pane_if_client_not_focused(client_id); self.connected_clients.borrow_mut().insert(client_id); self.mode_info.borrow_mut().insert( client_id, mode_info.unwrap_or_else(|| self.default_mode_info.clone()), ); } self.set_force_render(); Ok(()) } pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) { self.mode_info.borrow_mut().insert(client_id, mode_info); } pub fn add_multiple_clients( &mut self, client_ids_to_mode_infos: Vec<(ClientId, ModeInfo)>, ) -> Result<()> { for (client_id, client_mode_info) in client_ids_to_mode_infos { self.add_client(client_id, None) .context("failed to add clients")?; self.mode_info .borrow_mut() .insert(client_id, client_mode_info); } Ok(()) } pub fn remove_client(&mut self, client_id: ClientId) { self.focus_pane_id = None; self.mode_info .borrow_mut() .get_mut(&client_id) .map(|c| c.change_to_default_mode()); // TODO: no races? self.connected_clients.borrow_mut().remove(&client_id); self.mouse_help_text_visible.remove(&client_id); self.mouse_last_pane_id.remove(&client_id); self.last_mouse_activity_time.remove(&client_id); self.set_client_dimmed(client_id, false); self.set_force_render(); } pub fn drain_connected_clients( &mut self, clients_to_drain: Option>, ) -> Vec<(ClientId, ModeInfo)> { // None => all clients let mut client_ids_to_mode_infos = vec![]; let clients_to_drain = clients_to_drain .unwrap_or_else(|| self.connected_clients.borrow_mut().drain().collect()); for client_id in clients_to_drain { client_ids_to_mode_infos.push(self.drain_single_client(client_id)); } client_ids_to_mode_infos } pub fn drain_single_client(&mut self, client_id: ClientId) -> (ClientId, ModeInfo) { let client_mode_info = self .mode_info .borrow_mut() .remove(&client_id) .unwrap_or_else(|| self.default_mode_info.clone()); self.connected_clients.borrow_mut().remove(&client_id); (client_id, client_mode_info) } pub fn has_no_connected_clients(&self) -> bool { self.connected_clients.borrow().is_empty() } pub fn pane_id_is_floating(&self, pane_id: &PaneId) -> bool { self.floating_panes.panes_contain(pane_id) } pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to toggle embedded/floating pane for client {client_id}"); if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } if self.floating_panes.panes_are_visible() { if let Some(focused_floating_pane_id) = self.floating_panes.active_pane_id(client_id) { if self.tiled_panes.has_room_for_new_pane() { let floating_pane_to_embed = self .extract_pane(focused_floating_pane_id, true) .with_context(|| format!( "failed to find floating pane (ID: {focused_floating_pane_id:?}) to embed for client {client_id}", )) .with_context(err_context)?; self.hide_floating_panes(); self.add_tiled_pane( floating_pane_to_embed, focused_floating_pane_id, false, Some(client_id), )?; } } } else if let Some(focused_pane_id) = self.tiled_panes.focused_pane_id(client_id) { if self.get_selectable_tiled_panes().count() <= 1 && !self.pane_is_stack_list_member(&focused_pane_id) { // don't close the only pane on screen... return Ok(()); } if let Some(embedded_pane_to_float) = self.extract_pane(focused_pane_id, true) { self.show_floating_panes(); self.add_floating_pane( embedded_pane_to_float, focused_pane_id, None, true, Some(client_id), )?; } } Ok(()) } pub fn toggle_pane_embed_or_floating_for_pane_id( &mut self, pane_id: PaneId, client_id: Option, ) -> Result<()> { let err_context = || { format!( "failed to toggle embedded/floating pane for pane_id {:?}", pane_id ) }; if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } if self.floating_panes.panes_contain(&pane_id) { if self.tiled_panes.has_room_for_new_pane() { let floating_pane_to_embed = self .extract_pane(pane_id, true) .with_context(|| { format!("failed to find floating pane (ID: {pane_id:?}) to embed",) }) .with_context(err_context)?; self.add_tiled_pane(floating_pane_to_embed, pane_id, false, client_id)?; } } else if self.tiled_panes.panes_contain(&pane_id) { if self.get_selectable_tiled_panes().count() <= 1 && !self.pane_is_stack_list_member(&pane_id) { log::error!("Cannot float the last tiled pane..."); // don't close the only pane on screen... return Ok(()); } if let Some(embedded_pane_to_float) = self.extract_pane(pane_id, true) { self.add_floating_pane(embedded_pane_to_float, pane_id, None, true, client_id)?; } } else if self.pane_is_hidden_stack_list_member(&pane_id) { if let Some(hidden_member_to_float) = self.extract_pane(pane_id, true) { self.add_floating_pane(hidden_member_to_float, pane_id, None, true, client_id)?; } } Ok(()) } pub fn toggle_floating_panes( &mut self, client_id: Option, default_shell: Option, completion_tx: Option, ) -> Result<()> { if self.floating_panes.panes_are_visible() { self.hide_floating_panes(); self.set_force_render(); } else { self.show_floating_panes(); match self.floating_panes.last_selectable_floating_pane_id() { Some(last_selectable_floating_pane_id) => match client_id { Some(client_id) => { if !self.floating_panes.active_panes_contain(&client_id) { self.floating_panes .focus_pane(last_selectable_floating_pane_id, client_id); } }, None => { self.floating_panes .focus_pane_for_all_clients(last_selectable_floating_pane_id); }, }, None => { let name = None; let client_id_or_tab_index = match client_id { Some(client_id) => ClientTabIndexOrPaneId::ClientId(client_id), None => ClientTabIndexOrPaneId::TabIndex(self.id), }; let should_start_suppressed = false; let instruction = PtyInstruction::SpawnTerminal( default_shell, name, NewPanePlacement::Floating(None), should_start_suppressed, client_id_or_tab_index, completion_tx, false, // set_blocking None, // purpose ); self.senders .send_to_pty(instruction) .with_context(|| format!("failed to open a floating pane for client"))?; }, } self.floating_panes.set_force_render(); } self.set_force_render(); Ok(()) } fn normalize_invoked_with_for_default_shell(&self, invoked_with: Option) -> Option { let default_shell_run_command = Run::Command(RunCommand { command: self.default_shell.clone(), use_terminal_title: true, ..Default::default() }); if invoked_with == Some(default_shell_run_command) { None } else { invoked_with } } pub fn new_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, start_suppressed: bool, should_focus_pane: bool, new_pane_placement: NewPanePlacement, client_id: Option, blocking_notification: Option, ) -> Result<()> { let invoked_with = self.normalize_invoked_with_for_default_shell(invoked_with); match new_pane_placement { NewPanePlacement::NoPreference { borderless } => self.new_no_preference_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, client_id, blocking_notification, borderless, ), NewPanePlacement::Tiled { direction: None, borderless, } => self.new_tiled_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, client_id, blocking_notification, borderless, ), NewPanePlacement::Tiled { direction: Some(direction), borderless, } => { let is_vertical = direction == Direction::Left || direction == Direction::Right; if should_focus_pane { if let Some(client_id) = client_id { if is_vertical { self.vertical_split( pid, initial_pane_title, client_id, blocking_notification, borderless, )?; } else { self.horizontal_split( pid, initial_pane_title, client_id, blocking_notification, borderless, )?; } } } else if let Some(target_pane_id) = client_id.and_then(|client_id| self.tiled_panes.get_active_pane_id(client_id)) { if is_vertical { self.vertical_split_of_pane_id( pid, initial_pane_title, invoked_with, target_pane_id, blocking_notification, borderless, )?; } else { self.horizontal_split_of_pane_id( pid, initial_pane_title, invoked_with, target_pane_id, blocking_notification, borderless, )?; } } Ok(()) }, NewPanePlacement::Floating(floating_pane_coordinates) => self.new_floating_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, floating_pane_coordinates, blocking_notification, ), NewPanePlacement::InPlace { pane_id_to_replace, close_replaced_pane, borderless, } => self.new_in_place_pane( pid, initial_pane_title, invoked_with, pane_id_to_replace.map(|id| id.into()), close_replaced_pane, client_id, blocking_notification, borderless, ), NewPanePlacement::Stacked { pane_id_to_stack_under, borderless, } => self.new_stacked_pane( pid, initial_pane_title, invoked_with, start_suppressed, should_focus_pane, pane_id_to_stack_under.map(|id| id.into()), client_id, blocking_notification, borderless, ), } } pub fn new_no_preference_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, start_suppressed: bool, should_focus_pane: bool, client_id: Option, blocking_notification: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to create new pane with id {pid:?}"); self.close_down_to_max_terminals() .with_context(err_context)?; let mut new_pane = match pid { PaneId::Terminal(term_pid) => { let next_terminal_position = self.get_next_terminal_position(); Box::new(TerminalPane::new( term_pid, PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_notification, )) as Box }, PaneId::Plugin(plugin_pid) => { Box::new(PluginPane::new( plugin_pid, PaneGeom::default(), // this will be filled out later self.senders .to_plugin .as_ref() .with_context(err_context)? .clone(), initial_pane_title.unwrap_or("".to_owned()), String::new(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), self.link_handler.clone(), self.character_cell_size.clone(), self.connected_clients_in_app .borrow() .keys() .copied() .collect(), self.style, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, )) as Box }, }; if let Some(borderless) = borderless { new_pane.set_borderless(borderless); } if start_suppressed { // this pane needs to start in the background (suppressed), only accessible if a plugin takes it out // of there in one way or another // we need to do some bookkeeping for this pane, namely setting its geom and // content_offset so that things will appear properly in the terminal - we set it to // the default geom of the first floating pane - this is just in order to give it some // reasonable size, when it is shown - if needed - it will be given the proper geom as if it were // resized let viewport = { self.viewport.borrow().clone() }; let new_pane_geom = half_size_middle_geom(&viewport, 0); new_pane.set_active_at(Instant::now()); new_pane.set_geom(new_pane_geom); new_pane.set_content_offset(Offset::frame(1)); resize_pty!( new_pane, self.os_api, self.senders, self.character_cell_size ) .with_context(err_context)?; let is_scrollback_editor = false; self.suppressed_panes .insert(pid, (is_scrollback_editor, new_pane)); Ok(()) } else if should_focus_pane { if self.floating_panes.panes_are_visible() { self.add_floating_pane(new_pane, pid, None, true, client_id) } else { self.add_tiled_pane(new_pane, pid, false, client_id) } } else { if self.floating_panes.panes_are_visible() { self.add_floating_pane(new_pane, pid, None, false, client_id) } else { self.add_tiled_pane(new_pane, pid, false, None) } } } pub fn new_tiled_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, start_suppressed: bool, should_focus_pane: bool, client_id: Option, blocking_notification: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to create new pane with id {pid:?}"); if should_focus_pane { self.hide_floating_panes(); } self.close_down_to_max_terminals() .with_context(err_context)?; let mut new_pane = match pid { PaneId::Terminal(term_pid) => { let next_terminal_position = self.get_next_terminal_position(); Box::new(TerminalPane::new( term_pid, PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_notification, )) as Box }, PaneId::Plugin(plugin_pid) => { Box::new(PluginPane::new( plugin_pid, PaneGeom::default(), // this will be filled out later self.senders .to_plugin .as_ref() .with_context(err_context)? .clone(), initial_pane_title.unwrap_or("".to_owned()), String::new(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), self.link_handler.clone(), self.character_cell_size.clone(), self.connected_clients_in_app .borrow() .keys() .copied() .collect(), self.style, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, )) as Box }, }; if let Some(borderless) = borderless { new_pane.set_borderless(borderless); } if start_suppressed { // this pane needs to start in the background (suppressed), only accessible if a plugin takes it out // of there in one way or another // we need to do some bookkeeping for this pane, namely setting its geom and // content_offset so that things will appear properly in the terminal - we set it to // the default geom of the first floating pane - this is just in order to give it some // reasonable size, when it is shown - if needed - it will be given the proper geom as if it were // resized let viewport = { self.viewport.borrow().clone() }; let new_pane_geom = half_size_middle_geom(&viewport, 0); new_pane.set_active_at(Instant::now()); new_pane.set_geom(new_pane_geom); new_pane.set_content_offset(Offset::frame(1)); resize_pty!( new_pane, self.os_api, self.senders, self.character_cell_size ) .with_context(err_context)?; let is_scrollback_editor = false; self.suppressed_panes .insert(pid, (is_scrollback_editor, new_pane)); Ok(()) } else { let focus_client_id = if should_focus_pane { client_id } else { None }; self.add_tiled_pane(new_pane, pid, false, focus_client_id) } } pub fn new_floating_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, start_suppressed: bool, should_focus_pane: bool, floating_pane_coordinates: Option, blocking_notification: Option, ) -> Result<()> { let err_context = || format!("failed to create new pane with id {pid:?}"); if should_focus_pane { self.show_floating_panes(); } self.close_down_to_max_terminals() .with_context(err_context)?; let mut new_pane = match pid { PaneId::Terminal(term_pid) => { let next_terminal_position = self.get_next_terminal_position(); Box::new(TerminalPane::new( term_pid, PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_notification, )) as Box }, PaneId::Plugin(plugin_pid) => { Box::new(PluginPane::new( plugin_pid, PaneGeom::default(), // this will be filled out later self.senders .to_plugin .as_ref() .with_context(err_context)? .clone(), initial_pane_title.unwrap_or("".to_owned()), String::new(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), self.link_handler.clone(), self.character_cell_size.clone(), self.connected_clients_in_app .borrow() .keys() .copied() .collect(), self.style, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, )) as Box }, }; if start_suppressed { // this pane needs to start in the background (suppressed), only accessible if a plugin takes it out // of there in one way or another // we need to do some bookkeeping for this pane, namely setting its geom and // content_offset so that things will appear properly in the terminal - we set it to // the default geom of the first floating pane - this is just in order to give it some // reasonable size, when it is shown - if needed - it will be given the proper geom as if it were // resized let viewport = { self.viewport.borrow().clone() }; let new_pane_geom = half_size_middle_geom(&viewport, 0); new_pane.set_active_at(Instant::now()); new_pane.set_geom(new_pane_geom); new_pane.set_content_offset(Offset::frame(1)); resize_pty!( new_pane, self.os_api, self.senders, self.character_cell_size ) .with_context(err_context)?; let is_scrollback_editor = false; self.suppressed_panes .insert(pid, (is_scrollback_editor, new_pane)); Ok(()) } else { self.add_floating_pane( new_pane, pid, floating_pane_coordinates, should_focus_pane, None, ) } } pub fn new_in_place_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, pane_id_to_replace: Option, close_replaced_pane: bool, client_id: Option, blocking_notification: Option, borderless: Option, ) -> Result<()> { match (pane_id_to_replace, client_id) { (Some(pane_id_to_replace), _) => { self.suppress_pane_and_replace_with_pid( pane_id_to_replace, pid, close_replaced_pane, invoked_with, blocking_notification, borderless, )?; }, (None, Some(client_id)) => match self.get_active_pane_id(client_id) { Some(active_pane_id) => { self.suppress_pane_and_replace_with_pid( active_pane_id, pid, close_replaced_pane, invoked_with, blocking_notification, borderless, )?; }, None => { log::error!("Cannot find active pane"); }, }, _ => { log::error!("Must have pane id to replace or client id to start pane in place>"); }, } if let Some(initial_pane_title) = initial_pane_title { let _ = self.rename_pane(initial_pane_title.as_bytes().to_vec(), pid); } Ok(()) } pub fn new_stacked_pane( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, start_suppressed: bool, should_focus_pane: bool, pane_id_to_stack_under: Option, client_id: Option, blocking_notification: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to create new pane with id {pid:?}"); if should_focus_pane { self.hide_floating_panes(); } self.close_down_to_max_terminals() .with_context(err_context)?; let mut new_pane = match pid { PaneId::Terminal(term_pid) => { let next_terminal_position = self.get_next_terminal_position(); Box::new(TerminalPane::new( term_pid, PaneGeom::default(), // this will be filled out later self.style, next_terminal_position, initial_pane_title.clone().unwrap_or_default(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, blocking_notification, )) as Box }, PaneId::Plugin(plugin_pid) => { Box::new(PluginPane::new( plugin_pid, PaneGeom::default(), // this will be filled out later self.senders .to_plugin .as_ref() .with_context(err_context)? .clone(), initial_pane_title.unwrap_or("".to_owned()), String::new(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), self.link_handler.clone(), self.character_cell_size.clone(), self.connected_clients_in_app .borrow() .keys() .copied() .collect(), self.style, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, )) as Box }, }; if let Some(borderless) = borderless { new_pane.set_borderless(borderless); } if start_suppressed { // this pane needs to start in the background (suppressed), only accessible if a plugin takes it out // of there in one way or another // we need to do some bookkeeping for this pane, namely setting its geom and // content_offset so that things will appear properly in the terminal - we set it to // the default geom of the first floating pane - this is just in order to give it some // reasonable size, when it is shown - if needed - it will be given the proper geom as if it were // resized let viewport = { self.viewport.borrow().clone() }; let new_pane_geom = half_size_middle_geom(&viewport, 0); new_pane.set_active_at(Instant::now()); new_pane.set_geom(new_pane_geom); new_pane.set_content_offset(Offset::frame(1)); resize_pty!( new_pane, self.os_api, self.senders, self.character_cell_size ) .with_context(err_context)?; let is_scrollback_editor = false; self.suppressed_panes .insert(pid, (is_scrollback_editor, new_pane)); Ok(()) } else { if let Some(pane_id_to_stack_under) = pane_id_to_stack_under { self.add_stacked_pane_to_pane_id( new_pane, pid, pane_id_to_stack_under, should_focus_pane, ) } else if let Some(client_id) = client_id { self.add_stacked_pane_to_active_pane(new_pane, pid, client_id, should_focus_pane) } else { log::error!("Must have client id or pane id to stack pane"); return Ok(()); } } } pub fn replace_active_pane_with_editor_pane( &mut self, pid: PaneId, client_id: ClientId, ) -> Result<()> { // this method creates a new pane from pid and replaces it with the active pane // the active pane is then suppressed (hidden and not rendered) until the current // created pane is closed, in which case it will be replaced back by it let err_context = || format!("failed to suppress active pane for client {client_id}"); match pid { PaneId::Terminal(pid) => { let new_pane = self.new_scrollback_editor_pane(pid); let replaced_pane = if self.floating_panes.panes_are_visible() { self.floating_panes .replace_active_pane(Box::new(new_pane), client_id) .ok() } else { self.tiled_panes .replace_active_pane(Box::new(new_pane), client_id) }; match replaced_pane { Some(replaced_pane) => { self.track_stack_list_member_id_swap( replaced_pane.pid(), PaneId::Terminal(pid), ); self.insert_scrollback_editor_replaced_pane(replaced_pane, pid); self.get_active_pane(client_id) .with_context(|| format!("no active pane found for client {client_id}")) .and_then(|current_active_pane| { resize_pty!( current_active_pane, self.os_api, self.senders, self.character_cell_size ) }) .with_context(err_context)?; }, None => { Err::<(), _>(anyhow!( "Could not find editor pane to replace - is no pane focused?" )) .with_context(err_context) .non_fatal(); }, } }, PaneId::Plugin(_pid) => { // TBD, currently unsupported }, } Ok(()) } pub fn replace_pane_with_editor_pane( &mut self, pid: PaneId, pane_id_to_replace: PaneId, ) -> Result<()> { // this method creates a new pane from pid and replaces it with the pane iwth the given pane_id_to_replace // the pane with the given pane_id_to_replace is then suppressed (hidden and not rendered) until the current // created pane is closed, in which case it will be replaced back by it let err_context = || format!("failed to suppress pane"); match pid { PaneId::Terminal(pid) => { let new_pane = self.new_scrollback_editor_pane(pid); let replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { self.floating_panes .replace_pane(pane_id_to_replace, Box::new(new_pane)) .ok() } else if self.tiled_panes.panes_contain(&pane_id_to_replace) { self.tiled_panes .replace_pane(pane_id_to_replace, Box::new(new_pane)) } else if self .suppressed_panes .values() .any(|s_p| s_p.1.pid() == pane_id_to_replace) { log::error!("Cannot replace suppressed pane"); None } else { // not a thing None }; match replaced_pane { Some(replaced_pane) => { resize_pty!( replaced_pane, self.os_api, self.senders, self.character_cell_size ) .non_fatal(); self.track_stack_list_member_id_swap( pane_id_to_replace, PaneId::Terminal(pid), ); self.insert_scrollback_editor_replaced_pane(replaced_pane, pid); }, None => { Err::<(), _>(anyhow!("Could not find editor pane to replace")) .with_context(err_context) .non_fatal(); }, } }, PaneId::Plugin(_pid) => { // TBD, currently unsupported }, } Ok(()) } pub fn suppress_pane_and_replace_with_pid( &mut self, old_pane_id: PaneId, new_pane_id: PaneId, close_replaced_pane: bool, run: Option, completion_tx: Option, borderless: Option, ) -> Result<()> { // this method creates a new pane from pid and replaces it with the active pane // the active pane is then suppressed (hidden and not rendered) until the current // created pane is closed, in which case it will be replaced back by it let err_context = || format!("failed to suppress active pane"); match new_pane_id { PaneId::Terminal(new_pane_id) => { let next_terminal_position = self.get_next_terminal_position(); // TODO: this is not accurate in this case let mut new_pane = TerminalPane::new( new_pane_id, PaneGeom::default(), // the initial size will be set later self.style, next_terminal_position, String::new(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), None, run, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, completion_tx, ); if let Some(supported) = self.kitty_host_support { new_pane.update_kitty_host_support(supported); } if let Some(borderless) = borderless { new_pane.set_borderless(borderless); } let replaced_pane = if self.floating_panes.panes_contain(&old_pane_id) { self.floating_panes .replace_pane(old_pane_id, Box::new(new_pane)) .ok() } else { self.tiled_panes .replace_pane(old_pane_id, Box::new(new_pane)) }; if replaced_pane.is_some() { self.track_stack_list_member_id_swap( old_pane_id, PaneId::Terminal(new_pane_id), ); } if close_replaced_pane { if let Some(pid) = replaced_pane.as_ref().map(|p| p.pid()) { self.senders .send_to_pty(PtyInstruction::ClosePane(pid, None)) .with_context(err_context)?; } drop(replaced_pane); } else { match replaced_pane { Some(replaced_pane) => { let _ = resize_pty!( replaced_pane, self.os_api, self.senders, self.character_cell_size ); let is_scrollback_editor = false; self.suppressed_panes.insert( PaneId::Terminal(new_pane_id), (is_scrollback_editor, replaced_pane), ); }, None => { Err::<(), _>(anyhow!( "Could not find editor pane to replace - is no pane focused?" )) .with_context(err_context) .non_fatal(); }, } } }, PaneId::Plugin(plugin_pid) => { let mut new_pane = PluginPane::new( plugin_pid, PaneGeom::default(), // this will be filled out later self.senders .to_plugin .as_ref() .with_context(err_context)? .clone(), String::new(), String::new(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), self.link_handler.clone(), self.character_cell_size.clone(), self.connected_clients_in_app .borrow() .keys() .copied() .collect(), self.style, run, self.debug, self.arrow_fonts, self.styled_underlines, ); if let Some(borderless) = borderless { new_pane.set_borderless(borderless); } let replaced_pane = if self.floating_panes.panes_contain(&old_pane_id) { self.floating_panes .replace_pane(old_pane_id, Box::new(new_pane)) .ok() } else { self.tiled_panes .replace_pane(old_pane_id, Box::new(new_pane)) }; if replaced_pane.is_some() { self.track_stack_list_member_id_swap(old_pane_id, PaneId::Plugin(plugin_pid)); } if close_replaced_pane { drop(replaced_pane); } else { match replaced_pane { Some(replaced_pane) => { let _ = resize_pty!( replaced_pane, self.os_api, self.senders, self.character_cell_size ); let is_scrollback_editor = false; self.suppressed_panes.insert( PaneId::Plugin(plugin_pid), (is_scrollback_editor, replaced_pane), ); }, None => { Err::<(), _>(anyhow!( "Could not find editor pane to replace - is no pane focused?" )) .with_context(err_context) .non_fatal(); }, } } }, } Ok(()) } pub fn close_pane_and_replace_with_other_pane( &mut self, pane_id_to_replace: PaneId, pane_to_replace_with: Box, completion_tx: Option, ) { let replacement_pane_id = pane_to_replace_with.pid(); let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { self.floating_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) .ok() } else { self.tiled_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) }; if replaced_pane.is_some() { self.track_stack_list_member_id_swap(pane_id_to_replace, replacement_pane_id); } if let Some(replaced_pane) = replaced_pane.take() { let pane_id = replaced_pane.pid(); let _ = self .senders .send_to_pty(PtyInstruction::ClosePane(pane_id, completion_tx)); let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::PaneClosed(pane_id.into()), )])); let _ = self .senders .send_to_screen(ScreenInstruction::NotifyPaneClosedToSubscribers { pane_id: pane_id.into(), }); drop(replaced_pane); } } pub fn suppress_pane_and_replace_with_other_pane( &mut self, pane_id_to_replace: PaneId, pane_to_replace_with: Box, _completion_tx: Option, ) { let mut replaced_pane = if self.floating_panes.panes_contain(&pane_id_to_replace) { self.floating_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) .ok() } else { self.tiled_panes .replace_pane(pane_id_to_replace, pane_to_replace_with) }; if let Some(replaced_pane) = replaced_pane.take() { let is_scrollback_editor = false; self.insert_suppressed_pane(replaced_pane.pid(), (is_scrollback_editor, replaced_pane)); } } pub fn horizontal_split( &mut self, pid: PaneId, initial_pane_title: Option, client_id: ClientId, completion_tx: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to split pane {pid:?} horizontally for client {client_id}"); if self.floating_panes.panes_are_visible() { return Ok(()); } self.close_down_to_max_terminals() .with_context(err_context)?; if self.tiled_panes.fullscreen_is_active() { self.toggle_active_pane_fullscreen(client_id); } self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.can_split_pane_horizontally(client_id) { if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); let mut new_terminal = TerminalPane::new( term_pid, PaneGeom::default(), // the initial size will be set later self.style, next_terminal_position, String::new(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, None, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, completion_tx, ); if let Some(supported) = self.kitty_host_support { new_terminal.update_kitty_host_support(supported); } if let Some(borderless) = borderless { new_terminal.set_borderless(borderless); } self.tiled_panes .split_pane_horizontally(pid, Box::new(new_terminal), client_id); self.set_should_clear_display_before_rendering(); self.tiled_panes.focus_pane(pid, client_id); self.swap_layouts.set_is_tiled_damaged(); } } else { log::error!("No room to split pane horizontally"); if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { self.senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( vec![active_pane_id], "CAN'T SPLIT!".into(), )) .with_context(err_context)?; } self.senders .send_to_pty(PtyInstruction::ClosePane(pid, completion_tx)) .with_context(err_context)?; return Ok(()); } Ok(()) } pub fn vertical_split( &mut self, pid: PaneId, initial_pane_title: Option, client_id: ClientId, completion_tx: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to split pane {pid:?} vertically for client {client_id}"); if self.floating_panes.panes_are_visible() { return Ok(()); } self.close_down_to_max_terminals() .with_context(err_context)?; if self.tiled_panes.fullscreen_is_active() { self.toggle_active_pane_fullscreen(client_id); } self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.can_split_pane_vertically(client_id) { if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); let mut new_terminal = TerminalPane::new( term_pid, PaneGeom::default(), // the initial size will be set later self.style, next_terminal_position, String::new(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, None, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, completion_tx, ); if let Some(supported) = self.kitty_host_support { new_terminal.update_kitty_host_support(supported); } if let Some(borderless) = borderless { new_terminal.set_borderless(borderless); } self.tiled_panes .split_pane_vertically(pid, Box::new(new_terminal), client_id); self.set_should_clear_display_before_rendering(); self.tiled_panes.focus_pane(pid, client_id); self.swap_layouts.set_is_tiled_damaged(); } } else { log::error!("No room to split pane vertically"); if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { self.senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( vec![active_pane_id], "CAN'T SPLIT!".into(), )) .with_context(err_context)?; } self.senders .send_to_pty(PtyInstruction::ClosePane(pid, completion_tx)) .with_context(err_context)?; return Ok(()); } Ok(()) } pub fn horizontal_split_of_pane_id( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, target_pane_id: PaneId, completion_tx: Option, borderless: Option, ) -> Result<()> { self.split_of_pane_id( pid, initial_pane_title, invoked_with, target_pane_id, SplitDirection::Horizontal, completion_tx, borderless, ) } pub fn vertical_split_of_pane_id( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, target_pane_id: PaneId, completion_tx: Option, borderless: Option, ) -> Result<()> { self.split_of_pane_id( pid, initial_pane_title, invoked_with, target_pane_id, SplitDirection::Vertical, completion_tx, borderless, ) } fn split_of_pane_id( &mut self, pid: PaneId, initial_pane_title: Option, invoked_with: Option, target_pane_id: PaneId, split_direction: SplitDirection, completion_tx: Option, borderless: Option, ) -> Result<()> { let err_context = || format!("failed to split pane {target_pane_id:?} without changing focus"); if self.floating_panes.panes_are_visible() { return Ok(()); } if !self.tiled_panes.panes_contain(&target_pane_id) { return Ok(()); } self.close_down_to_max_terminals() .with_context(err_context)?; if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } self.dissolve_stack_lists_for_classic_mutation(); let can_split = match split_direction { SplitDirection::Horizontal => self .tiled_panes .can_split_pane_id_horizontally(target_pane_id), SplitDirection::Vertical => self .tiled_panes .can_split_pane_id_vertically(target_pane_id), }; if !can_split { self.senders .send_to_pty(PtyInstruction::ClosePane(pid, completion_tx)) .with_context(err_context)?; return Ok(()); } if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); let mut new_terminal = TerminalPane::new( term_pid, PaneGeom::default(), self.style, next_terminal_position, String::new(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), initial_pane_title, invoked_with, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, completion_tx, ); if let Some(supported) = self.kitty_host_support { new_terminal.update_kitty_host_support(supported); } if let Some(borderless) = borderless { new_terminal.set_borderless(borderless); } match split_direction { SplitDirection::Horizontal => self.tiled_panes.split_pane_id_horizontally( pid, Box::new(new_terminal), target_pane_id, ), SplitDirection::Vertical => self.tiled_panes.split_pane_id_vertically( pid, Box::new(new_terminal), target_pane_id, ), } self.set_should_clear_display_before_rendering(); self.swap_layouts.set_is_tiled_damaged(); } Ok(()) } pub fn get_active_pane(&self, client_id: ClientId) -> Option<&dyn Pane> { self.get_active_pane_id(client_id).and_then(|ap| { if self.floating_panes.panes_are_visible() { self.floating_panes.get_pane(ap).map(Box::as_ref) } else { self.tiled_panes.get_pane(ap).map(Box::as_ref) } }) } pub fn get_pane_with_id(&self, pane_id: PaneId) -> Option<&dyn Pane> { self.floating_panes .get_pane(pane_id) .map(Box::as_ref) .or_else(|| self.tiled_panes.get_pane(pane_id).map(Box::as_ref)) .or_else(|| self.suppressed_panes.get(&pane_id).map(|p| p.1.as_ref())) } pub fn get_pane_with_id_mut(&mut self, pane_id: PaneId) -> Option<&mut Box> { self.floating_panes .get_pane_mut(pane_id) .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .get_mut(&pane_id) .map(|(_, pane)| pane) }) } pub fn get_active_pane_mut(&mut self, client_id: ClientId) -> Option<&mut Box> { self.get_active_pane_id(client_id).and_then(|ap| { if self.floating_panes.panes_are_visible() { self.floating_panes.get_pane_mut(ap) } else { self.tiled_panes.get_pane_mut(ap) } }) } pub fn get_active_pane_or_floating_pane_mut( &mut self, client_id: ClientId, ) -> Option<&mut Box> { if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { self.floating_panes.get_active_pane_mut(client_id) } else { self.get_active_pane_mut(client_id) } } pub fn get_active_pane_id(&self, client_id: ClientId) -> Option { if self.floating_panes.panes_are_visible() { self.floating_panes.get_active_pane_id(client_id) } else { self.tiled_panes.get_active_pane_id(client_id) } } fn get_active_terminal_id(&self, client_id: ClientId) -> Option { if let Some(PaneId::Terminal(pid)) = self.get_active_pane_id(client_id) { Some(pid) } else { None } } pub fn check_and_handle_bell_notifications( &mut self, is_active_tab: bool, ) -> (Vec, bool) { let mut newly_notified_panes = vec![]; let mut tab_bell_newly_set = false; let focused_pane_ids: HashSet = self .connected_clients .borrow() .iter() .filter_map(|c_id| self.get_active_pane_id(*c_id)) .collect(); // Collect ringing pane IDs first (immutable borrow) let ringing_panes: Vec = self .tiled_panes .get_panes() .chain(self.floating_panes.get_panes()) .chain(self.suppressed_stack_list_members()) .filter(|(_, pane)| pane.has_bell()) .map(|(pane_id, _)| *pane_id) .collect(); for pane_id in ringing_panes { // Consume the bell from the pane if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.consume_bell(); } let is_focused = focused_pane_ids.contains(&pane_id); if !is_focused && !self.panes_with_pending_bell.contains(&pane_id) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.set_bell_notification(true); } self.panes_with_pending_bell.insert(pane_id); newly_notified_panes.push(pane_id); } if !self.tab_bell_ring { self.tab_bell_ring = true; tab_bell_newly_set = true; } if !is_active_tab { self.tab_has_pending_bell = true; } } (newly_notified_panes, tab_bell_newly_set) } pub fn clear_bell_notification_for_pane(&mut self, pane_id: PaneId) { self.panes_with_pending_bell.remove(&pane_id); if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.set_bell_notification(false); pane.clear_pane_frame_color_override(None); } self.tab_has_pending_bell = false; } pub fn clear_tab_bell_notification(&mut self) { self.tab_has_pending_bell = false; } pub fn clear_tab_bell_ring(&mut self) { self.tab_bell_ring = false; } /// Checks if any pane in the tab has a pending bell, consumes all such bells, and returns /// whether any were found. Does not update notification state (used when visual_bell is /// disabled but ANSI BEL forwarding is still desired). pub fn check_and_consume_bells_without_visual_notification(&mut self) -> bool { let ringing_panes: Vec = self .tiled_panes .get_panes() .chain(self.floating_panes.get_panes()) .chain(self.suppressed_stack_list_members()) .filter(|(_, pane)| pane.has_bell()) .map(|(pane_id, _)| *pane_id) .collect(); let had_bell = !ringing_panes.is_empty(); for pane_id in ringing_panes { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.consume_bell(); } } had_bell } pub fn has_terminal_pid(&self, pid: u32) -> bool { self.tiled_panes.panes_contain(&PaneId::Terminal(pid)) || self.floating_panes.panes_contain(&PaneId::Terminal(pid)) || self .suppressed_panes .values() .any(|s_p| s_p.1.pid() == PaneId::Terminal(pid)) } pub fn has_plugin(&self, plugin_id: u32) -> bool { self.tiled_panes.panes_contain(&PaneId::Plugin(plugin_id)) || self .floating_panes .panes_contain(&PaneId::Plugin(plugin_id)) || self .suppressed_panes .values() .any(|s_p| s_p.1.pid() == PaneId::Plugin(plugin_id)) } pub fn set_pane_color( &mut self, pane_id: PaneId, fg: Option, bg: Option, ) -> Result<()> { let pane = self .floating_panes .get_mut(&pane_id) .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| self.suppressed_panes.get_mut(&pane_id).map(|p| &mut p.1)); if let Some(pane) = pane { pane.set_pane_default_colors(fg, bg); } Ok(()) } pub fn has_pane_with_pid(&self, pid: &PaneId) -> bool { self.tiled_panes.panes_contain(pid) || self.floating_panes.panes_contain(pid) || self .suppressed_panes .values() .any(|s_p| s_p.1.pid() == *pid) } pub fn has_non_suppressed_pane_with_pid(&self, pid: &PaneId) -> bool { self.tiled_panes.panes_contain(pid) || self.floating_panes.panes_contain(pid) } pub fn set_pane_is_nested_guest(&mut self, pane_id: PaneId, is_nested_guest: bool) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.set_is_nested_guest(is_nested_guest); } } pub fn is_pane_nested_guest(&self, pane_id: PaneId) -> bool { if let Some(pane) = self .tiled_panes .get_pane(pane_id) .or_else(|| self.floating_panes.get_pane(pane_id)) .or_else(|| { self.suppressed_panes .values() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &s_p.1) }) { pane.is_nested_guest() } else { false } } pub fn nested_guest_pane_ids(&self) -> Vec { self.get_all_pane_ids() .into_iter() .filter(|pane_id| self.is_pane_nested_guest(*pane_id)) .collect() } pub fn set_guest_modal_on_pane(&mut self, pane_id: PaneId, client_ids: &[ClientId]) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.set_guest_modal(client_ids); } } pub fn clear_guest_modal_on_pane(&mut self, pane_id: PaneId, client_id: ClientId) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.clear_guest_modal(client_id); } } pub fn clear_all_guest_modals_on_pane(&mut self, pane_id: PaneId) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.clear_all_guest_modals(); } } pub fn set_guest_choice_indicator_on_pane( &mut self, pane_id: PaneId, client_id: ClientId, indicator: Option, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.set_guest_choice_indicator(client_id, indicator); } } pub fn clear_all_guest_choice_indicators_on_pane(&mut self, pane_id: PaneId) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.clear_all_guest_choice_indicators(); } } pub fn pane_has_guest_modal_for_client(&self, pane_id: PaneId, client_id: ClientId) -> bool { if let Some(pane) = self .tiled_panes .get_pane(pane_id) .or_else(|| self.floating_panes.get_pane(pane_id)) .or_else(|| { self.suppressed_panes .values() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &s_p.1) }) { pane.guest_modal_selection(client_id).is_some() } else { false } } pub fn clear_guest_modal_for_client_on_all_panes(&mut self, client_id: ClientId) { for pane_id in self.get_all_pane_ids() { self.clear_guest_modal_on_pane(pane_id, client_id); } } pub fn clear_guest_choice_indicator_for_client_on_all_panes(&mut self, client_id: ClientId) { for pane_id in self.get_all_pane_ids() { self.set_guest_choice_indicator_on_pane(pane_id, client_id, None); } } pub fn set_guest_session_name_on_pane( &mut self, pane_id: PaneId, session_name: Option, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.set_guest_session_name(session_name); } } pub fn set_guest_modal_shortcuts_on_pane( &mut self, pane_id: PaneId, shortcuts: GuestModalShortcuts, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.set_guest_modal_shortcuts(shortcuts); } } pub fn set_shadow_focus(&mut self, client_id: ClientId, pane_id: PaneId) -> bool { if self.tiled_panes.panes_contain(&pane_id) { self.tiled_panes.set_shadow_focus(client_id, pane_id); true } else if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.set_shadow_focus(client_id, pane_id); true } else { false } } pub fn clear_shadow_focus(&mut self, client_id: ClientId) { if self.tiled_panes.is_shadow_focus_client(&client_id) { self.tiled_panes.clear_shadow_focus(client_id); } if self.floating_panes.is_shadow_focus_client(&client_id) { self.floating_panes.clear_shadow_focus(client_id); } } pub fn shadow_focus_clients(&self) -> Vec { let mut out = self.tiled_panes.shadow_focus_clients(); out.extend(self.floating_panes.shadow_focus_clients()); out.sort_unstable(); out.dedup(); out } pub fn has_shadow_focus_on(&self, client_id: ClientId, pane_id: PaneId) -> bool { self.tiled_panes.has_shadow_focus_on(client_id, pane_id) || self.floating_panes.has_shadow_focus_on(client_id, pane_id) } pub fn handle_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> { if self.is_pending { self.pending_instructions .push(BufferedTabInstruction::HandlePtyBytes(pid, bytes)); return Ok(()); } let err_context = || format!("failed to handle pty bytes from fd {pid}"); if let Some(terminal_output) = self .tiled_panes .get_pane_mut(PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Terminal(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Terminal(pid)) .map(|s_p| &mut s_p.1) }) { // If the pane is scrolled buffer the vte events if terminal_output.is_scrolled() { self.pending_vte_events.entry(pid).or_default().push(bytes); if let Some(evs) = self.pending_vte_events.get(&pid) { // Reset scroll - and process all pending events for this pane if evs.len() >= MAX_PENDING_VTE_EVENTS { terminal_output.clear_scroll(); self.process_pending_vte_events(pid) .with_context(err_context)?; } } return Ok(()); } } self.process_pty_bytes(pid, bytes).with_context(err_context) } pub fn handle_plugin_bytes( &mut self, pid: u32, client_id: ClientId, bytes: VteBytes, ) -> Result<()> { if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1) }) { plugin_pane.handle_plugin_bytes(client_id, bytes); } Ok(()) } pub fn process_pending_vte_events(&mut self, pid: u32) -> Result<()> { if let Some(pending_vte_events) = self.pending_vte_events.get_mut(&pid) { let vte_events: Vec = pending_vte_events.drain(..).collect(); for vte_event in vte_events { self.process_pty_bytes(pid, vte_event) .context("failed to process pending vte events")?; } } Ok(()) } /// Deliver a forwarded host reply (or cache-fallback synthesis, /// or a locally-answered query payload) to a pane that is currently /// forward-paused. The reply bytes are written to the pane's PTY /// first (so the app reads them on its stdin in the same stream /// position the original query occupied), then the pause flag is /// cleared and any PTY bytes that arrived while the pane was /// paused are re-fed through vte. The re-feed itself may produce /// another forward and re-arm the pause; the cycle bounds at the /// buffered byte count. pub fn resume_pane_after_forward(&mut self, pid: u32, reply_bytes: Vec) -> Result<()> { let err_context = || format!("failed to resume pane {pid} after forward"); // Write the reply first so the app sees it on stdin before // any subsequent (sync or async) reply that follows in the // buffered byte stream. if !reply_bytes.is_empty() { self.write_to_pane_id_without_preprocessing(reply_bytes, PaneId::Terminal(pid)) .with_context(err_context)?; } // Clear the pause and pull out any bytes that were buffered // while it waited for the host reply. let buffered = if let Some(terminal_output) = self .tiled_panes .get_pane_mut(PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Terminal(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Terminal(pid)) .map(|s_p| &mut s_p.1) }) { terminal_output.clear_forward_pause(); terminal_output.drain_pending_pty_input() } else { // Pane closed between forward dispatch and reply arrival. return Ok(()); }; if !buffered.is_empty() { self.process_pty_bytes(pid, buffered) .with_context(err_context)?; } Ok(()) } fn process_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> { let err_context = || format!("failed to process pty bytes from pid {pid}"); if let Some(terminal_output) = self .tiled_panes .get_pane_mut(PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Terminal(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Terminal(pid)) .map(|s_p| &mut s_p.1) }) { if self.pids_waiting_resize.remove(&pid) { resize_pty!( terminal_output, self.os_api, self.senders, self.character_cell_size ) .with_context(err_context)?; } terminal_output.handle_pty_bytes(bytes); let messages_to_pty = terminal_output.drain_messages_to_pty(); let forwarded_queries = terminal_output.drain_forwarded_queries(); // If at least one forward was produced, the pane stopped // processing its `pending_pty_input` queue at the byte // that produced the forward. Arm the pause so subsequent // PTY bytes accumulate in the same queue rather than // being processed; they will be drained in stream order // when the host reply (or cache-fallback synthesis, or // 500 ms timeout) lands via `resume_pane_after_forward`. // Pause arming is independent of which forward bears the // reply: resume is keyed by pane_id, not by token, so a // single flag suffices for any number of queued forwards. if !forwarded_queries.is_empty() { terminal_output.arm_forward_pause(); } let nested_session_messages = terminal_output.drain_nested_session_messages(); let clipboard_update = terminal_output.drain_clipboard_update(); let desktop_notifications = terminal_output.drain_desktop_notifications(); let osc7_cwd = terminal_output.drain_osc7_cwd(); for message in messages_to_pty { self.write_to_pane_id_without_preprocessing(message, PaneId::Terminal(pid)) .with_context(err_context)?; } for query in forwarded_queries { let _ = self .senders .send_to_screen(ScreenInstruction::ForwardHostQuery { pane_id: PaneId::Terminal(pid), query, }); } for message in nested_session_messages { let _ = self.senders .send_to_screen(ScreenInstruction::NestedSessionMessageFromPane { pane_id: PaneId::Terminal(pid), message, }); } if let Some(string) = clipboard_update { self.write_selection_to_clipboard(&string) .with_context(err_context)?; } if !desktop_notifications.is_empty() { self.forward_desktop_notifications(desktop_notifications, pid) .with_context(err_context)?; } if let Some(path) = osc7_cwd { let _ = self .senders .send_to_pty(PtyInstruction::NotifyCwdFromOsc7(pid, path)); } } Ok(()) } pub fn write_to_terminals_on_current_tab( &mut self, key_with_modifier: &Option, raw_input_bytes: Vec, raw_input_bytes_are_kitty: bool, client_id: ClientId, ) -> Result { // returns true if a UI update should be triggered (eg. when closing a command pane with // ctrl-c) let mut should_trigger_ui_change = false; let mut pane_ids = self.get_static_and_floating_pane_ids(); pane_ids.extend( self.stack_list_of_member .keys() .filter(|member| self.pane_is_hidden_stack_list_member(member)) .copied(), ); for pane_id in pane_ids { let ui_change_triggered = self .write_to_pane_id( key_with_modifier, raw_input_bytes.clone(), raw_input_bytes_are_kitty, pane_id, Some(client_id), None, ) .context("failed to write to terminals on current tab")?; if ui_change_triggered { should_trigger_ui_change = true; } } Ok(should_trigger_ui_change) } pub fn write_to_active_terminal( &mut self, key_with_modifier: &Option, raw_input_bytes: Vec, raw_input_bytes_are_kitty: bool, client_id: ClientId, ) -> Result { // returns true if a UI update should be triggered (eg. if a command pane // was closed with ctrl-c) let err_context = || { format!( "failed to write to active terminal for client {client_id} - msg: {raw_input_bytes:?}" ) }; self.clear_search(client_id); self.mouse_help_text_visible.clear(); let pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .get_active_pane_id(client_id) .or_else(|| self.tiled_panes.get_active_pane_id(client_id)) .ok_or_else(|| { anyhow!(format!( "failed to find active pane id for client {client_id}" )) }) .with_context(err_context)? } else { self.tiled_panes .get_active_pane_id(client_id) .with_context(err_context)? }; // Can't use 'err_context' here since it borrows 'raw_input_bytes' self.write_to_pane_id( key_with_modifier, raw_input_bytes, raw_input_bytes_are_kitty, pane_id, Some(client_id), None, ) .with_context(|| format!("failed to write to active terminal for client {client_id}")) } pub fn write_to_terminal_at( &mut self, input_bytes: Vec, position: &Position, client_id: ClientId, ) -> Result<()> { let err_context = || format!("failed to write to terminal at position {position:?}"); if self.floating_panes.panes_are_visible() { let pane_id = self .floating_panes .get_pane_id_at(position, false) .with_context(err_context)?; if let Some(pane_id) = pane_id { self.write_to_pane_id(&None, input_bytes, false, pane_id, Some(client_id), None) .with_context(err_context)?; return Ok(()); } } let pane_id = self .get_pane_id_at(position, false) .with_context(err_context)?; if let Some(pane_id) = pane_id { self.write_to_pane_id(&None, input_bytes, false, pane_id, Some(client_id), None) .with_context(err_context)?; return Ok(()); } Ok(()) } pub fn paste_to_pane_id( &mut self, bytes: Vec, pane_id: PaneId, completion: Option, ) -> Result<()> { let bracketed_paste_begin = vec![27, 91, 50, 48, 48, 126]; let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; self.write_to_pane_id(&None, bracketed_paste_begin, false, pane_id, None, None)?; self.write_to_pane_id(&None, bytes, false, pane_id, None, None)?; self.write_to_pane_id(&None, bracketed_paste_end, false, pane_id, None, completion)?; Ok(()) } pub fn paste_to_active_terminal( &mut self, bytes: Vec, client_id: ClientId, completion: Option, ) -> Result<()> { let err_context = || format!("failed to paste to active terminal for client {client_id}"); let active_pane_id = self .get_active_pane_id(client_id) .ok_or_else(|| anyhow!("no active pane for client {client_id}")) .with_context(err_context)?; self.paste_to_pane_id(bytes, active_pane_id, completion) } pub fn write_to_pane_id( &mut self, key_with_modifier: &Option, raw_input_bytes: Vec, raw_input_bytes_are_kitty: bool, pane_id: PaneId, client_id: Option, completion_tx: Option, ) -> Result { // returns true if we need to update the UI (eg. when a command pane is closed with ctrl-c) let err_context = || format!("failed to write to pane with id {pane_id:?}"); let mut should_update_ui = false; let is_sync_panes_active = self.is_sync_panes_active(); let active_pane = self .floating_panes .get_mut(&pane_id) .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| self.suppressed_panes.get_mut(&pane_id).map(|p| &mut p.1)) .ok_or_else(|| anyhow!(format!("failed to find pane with id {pane_id:?}"))) .with_context(err_context)?; // We always write for non-synced terminals. // However if the terminal is part of a tab-sync, we need to // check if the terminal should receive input or not (depending on its // 'exclude_from_sync' configuration). let should_not_write_to_terminal = is_sync_panes_active && active_pane.exclude_from_sync(); if should_not_write_to_terminal { return Ok(should_update_ui); } match pane_id { PaneId::Terminal(active_terminal_id) => { match active_pane.adjust_input_to_terminal( key_with_modifier, raw_input_bytes, raw_input_bytes_are_kitty, client_id, ) { Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => { self.senders .send_to_pty_writer(PtyWriteInstruction::Write( adjusted_input, active_terminal_id, completion_tx, )) .with_context(err_context)?; }, Some(AdjustedInput::ReRunCommandInThisPane(command)) => { self.pids_waiting_resize.insert(active_terminal_id); self.senders .send_to_pty(PtyInstruction::ReRunCommandInPane( PaneId::Terminal(active_terminal_id), command, completion_tx, )) .with_context(err_context)?; should_update_ui = true; }, Some(AdjustedInput::CloseThisPane) => { self.close_pane(PaneId::Terminal(active_terminal_id), false, None); should_update_ui = true; }, Some(AdjustedInput::DropToShellInThisPane { working_dir }) => { self.pids_waiting_resize.insert(active_terminal_id); self.senders .send_to_pty(PtyInstruction::DropToShellInPane { pane_id: PaneId::Terminal(active_terminal_id), shell: Some(self.default_shell.clone()), working_dir, completion_tx, }) .with_context(err_context)?; should_update_ui = true; }, Some(AdjustedInput::GuestModalSelectionChanged) => { should_update_ui = true; }, Some(AdjustedInput::GuestModalZoom) => { if let Some(client_id) = client_id { let _ = self.senders .send_to_screen(ScreenInstruction::GuestModalChoice { client_id, pane_id: PaneId::Terminal(active_terminal_id), outcome: GuestModalOutcome::Zoom, }); } }, Some(AdjustedInput::GuestModalDescend) => { if let Some(client_id) = client_id { let _ = self.senders .send_to_screen(ScreenInstruction::GuestModalChoice { client_id, pane_id: PaneId::Terminal(active_terminal_id), outcome: GuestModalOutcome::Descend, }); } }, Some(_) => {}, None => {}, } }, PaneId::Plugin(pid) => match active_pane.adjust_input_to_terminal( key_with_modifier, raw_input_bytes, raw_input_bytes_are_kitty, client_id, ) { Some(AdjustedInput::WriteKeyToPlugin(key_with_modifier)) => { self.senders .send_to_plugin(PluginInstruction::Update(vec![( Some(pid), client_id, Event::Key(key_with_modifier), )])) .with_context(err_context)?; }, Some(AdjustedInput::WriteBytesToTerminal(adjusted_input)) => { let mut plugin_updates = vec![]; for key in parse_keys(&adjusted_input) { plugin_updates.push((Some(pid), client_id, Event::Key(key))); } self.senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .with_context(err_context)?; }, Some(AdjustedInput::PermissionRequestResult(permissions, status)) => { if active_pane.query_should_be_suppressed() { active_pane.set_should_be_suppressed(false); self.suppress_pane(PaneId::Plugin(pid), client_id); } self.request_plugin_permissions(pid, None); self.senders .send_to_plugin(PluginInstruction::PermissionRequestResult( pid, client_id, permissions, status, None, )) .with_context(err_context)?; should_update_ui = true; }, Some(_) => {}, None => {}, }, } Ok(should_update_ui) } pub fn write_to_pane_id_without_preprocessing( &mut self, raw_input_bytes: Vec, pane_id: PaneId, ) -> Result { // returns true if we need to update the UI (eg. when a command pane is closed with ctrl-c) let err_context = || format!("failed to write to pane with id {pane_id:?}"); let mut should_update_ui = false; match pane_id { PaneId::Terminal(active_terminal_id) => { self.senders .send_to_pty_writer(PtyWriteInstruction::Write( raw_input_bytes, active_terminal_id, None, )) .with_context(err_context)?; should_update_ui = true; }, PaneId::Plugin(_pid) => { log::error!("Unsupported plugin action"); }, } Ok(should_update_ui) } pub fn active_terminal_is_mid_frame(&self, client_id: ClientId) -> Option { let active_pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .get_active_pane_id(client_id) .or_else(|| self.tiled_panes.get_active_pane_id(client_id))? } else { self.tiled_panes.get_active_pane_id(client_id)? }; let active_terminal = &self .floating_panes .get(&active_pane_id) .or_else(|| self.tiled_panes.get_pane(active_pane_id))?; Some(active_terminal.is_mid_frame()) } pub fn get_active_terminal_cursor_position( &self, client_id: ClientId, ) -> Option<(usize, usize, bool)> { // (x, y, is_cursor_visible) let active_pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .get_active_pane_id(client_id) .or_else(|| self.tiled_panes.get_active_pane_id(client_id))? } else { self.tiled_panes.get_active_pane_id(client_id)? }; let active_terminal = &self .floating_panes .get(&active_pane_id) .or_else(|| self.tiled_panes.get_pane(active_pane_id))?; active_terminal.cursor_coordinates(Some(client_id)).map( |(x_in_terminal, y_in_terminal, is_visible)| { let x = active_terminal.x() + x_in_terminal; let y = active_terminal.y() + y_in_terminal; (x, y, is_visible) }, ) } pub fn toggle_active_pane_fullscreen(&mut self, client_id: ClientId) { if self.floating_panes.fullscreen_is_active() || (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) { self.floating_panes.toggle_active_pane_fullscreen(client_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); return; } if self.floating_panes.panes_are_visible() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.toggle_active_pane_fullscreen(client_id); } pub fn toggle_pane_fullscreen(&mut self, pane_id: PaneId) { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.toggle_pane_fullscreen(pane_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); return; } if self.pane_is_hidden_stack_list_member(&pane_id) { self.make_hidden_stack_list_member_visible(pane_id); } self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.panes_contain(&pane_id) { self.tiled_panes.toggle_pane_fullscreen(pane_id); } else { log::error!("No tiled pane with id: {:?} found", pane_id); } } pub fn toggle_active_pane_no_ui_fullscreen(&mut self, client_id: ClientId) { if self.floating_panes.fullscreen_is_active() || (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) { self.floating_panes .toggle_active_pane_no_ui_fullscreen(client_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); return; } if self.floating_panes.panes_are_visible() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .toggle_active_pane_no_ui_fullscreen(client_id); } pub fn toggle_pane_no_ui_fullscreen(&mut self, pane_id: PaneId) { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.toggle_pane_no_ui_fullscreen(pane_id); self.set_force_render(); self.set_should_clear_display_before_rendering(); return; } if self.pane_is_hidden_stack_list_member(&pane_id) { self.make_hidden_stack_list_member_visible(pane_id); } self.dissolve_stack_lists_for_classic_mutation(); if self.tiled_panes.panes_contain(&pane_id) { self.tiled_panes.toggle_pane_no_ui_fullscreen(pane_id); } else { log::error!("No tiled pane with id: {:?} found", pane_id); } } pub fn is_fullscreen_active(&self) -> bool { self.tiled_panes.fullscreen_is_active() || self.floating_panes.fullscreen_is_active() } pub fn fullscreen_covers_ui(&self) -> bool { self.tiled_panes.fullscreen_covers_ui() || self.floating_panes.fullscreen_covers_ui() } pub fn fullscreen_pane_id(&self) -> Option { self.tiled_panes .fullscreen_pane_id() .or_else(|| self.floating_panes.fullscreen_pane_id()) } pub fn are_floating_panes_visible(&self) -> bool { self.floating_panes.panes_are_visible() } pub fn focus_pane_left_fullscreen(&mut self, client_id: ClientId) -> bool { if !self.is_fullscreen_active() { return false; } return self.tiled_panes.focus_pane_left_fullscreen(client_id); } pub fn focus_pane_right_fullscreen(&mut self, client_id: ClientId) -> bool { if !self.is_fullscreen_active() { return false; } return self.tiled_panes.focus_pane_right_fullscreen(client_id); } pub fn focus_pane_up_fullscreen(&mut self, client_id: ClientId) -> bool { if !self.is_fullscreen_active() { return false; } return self.tiled_panes.focus_pane_up_fullscreen(client_id); } pub fn focus_pane_down_fullscreen(&mut self, client_id: ClientId) -> bool { if !self.is_fullscreen_active() { return false; } return self.tiled_panes.focus_pane_down_fullscreen(client_id); } pub fn switch_next_pane_fullscreen(&mut self, client_id: ClientId) { if !self.is_fullscreen_active() { return; } self.tiled_panes.switch_next_pane_fullscreen(client_id); } pub fn switch_prev_pane_fullscreen(&mut self, client_id: ClientId) { if !self.is_fullscreen_active() { return; } self.tiled_panes.switch_prev_pane_fullscreen(client_id); } pub fn switch_last_pane_fullscreen(&mut self, client_id: ClientId) { if !self.is_fullscreen_active() { return; } self.tiled_panes.switch_last_pane_fullscreen(client_id); } pub fn set_force_render(&mut self) { self.tiled_panes.set_force_render(); self.floating_panes.set_force_render(); for member in self.stack_list_of_member.keys() { if let Some((_is_scrollback_editor, pane)) = self.suppressed_panes.get_mut(member) { pane.set_should_render(true); pane.render_full_viewport(); } } } pub fn set_should_clear_display_before_rendering(&mut self) { self.should_clear_display_before_rendering = true; self.floating_panes.set_force_render(); // we do this to make sure pinned panes are // rendered even if their surface is not visible } #[cfg(test)] pub fn should_clear_display_before_rendering(&self) -> bool { self.should_clear_display_before_rendering } pub fn is_sync_panes_active(&self) -> bool { self.synchronize_is_active } pub fn toggle_sync_panes_is_active(&mut self) { self.synchronize_is_active = !self.synchronize_is_active; } pub fn mark_active_pane_for_rerender(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_mut(client_id) { active_pane.set_should_render(true); } } fn update_active_panes_in_pty_thread(&self) -> Result<()> { // this is a bit hacky and we should ideally not keep this state in two different places at // some point let connected_clients: Vec = { self.connected_clients.borrow().iter().copied().collect() }; for client_id in connected_clients { self.senders .send_to_pty(PtyInstruction::UpdateActivePane( self.get_active_pane_id(client_id), client_id, )) .with_context(|| format!("failed to update active pane for client {client_id}"))?; } Ok(()) } pub fn render( &mut self, output: &mut Output, client_id_override: Option, ) -> Result<()> { let err_context = || "failed to render tab".to_string(); let mut connected_clients: HashSet = { self.connected_clients.borrow().iter().copied().collect() }; // If we have a client_id_override (for watcher rendering), add it temporarily if let Some(override_id) = client_id_override { connected_clients.insert(override_id); } if connected_clients.is_empty() || !self.tiled_panes.has_active_panes() { return Ok(()); } if self.stacked_pane_list_is_active() { self.groupify_all(); } self.update_active_panes_in_pty_thread() .with_context(err_context)?; let floating_panes_stack = self.floating_panes.stack(); output.add_clients( &connected_clients, self.link_handler.clone(), floating_panes_stack, ); let current_pane_group: HashMap> = { self.current_pane_group.borrow().clone_inner() }; self.tiled_panes .render( output, self.floating_panes.panes_are_visible(), &self.mouse_hover_pane_id, current_pane_group.clone(), client_id_override, &self.mouse_help_text_visible, self.mouse_scroll_resize, ) .with_context(err_context)?; self.render_stack_list_headers(output, client_id_override) .with_context(err_context)?; let no_ui_fullscreen_active = self.tiled_panes.fullscreen_covers_ui(); if !no_ui_fullscreen_active && ((self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) || self.floating_panes.has_pinned_panes()) { self.floating_panes .render( output, &self.mouse_hover_pane_id, current_pane_group, client_id_override, &self.mouse_help_text_visible, self.mouse_scroll_resize, ) .with_context(err_context)?; } self.render_cursor(output); if output.has_rendered_assets() { self.hide_cursor_and_clear_display_as_needed(output); } self.emit_hint_text_if_changed().with_context(err_context)?; self.emit_active_pane_scroll_if_changed() .with_context(err_context)?; Ok(()) } fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) { let hide_cursor = "\u{1b}[?25l"; let connected_clients: Vec = { self.connected_clients.borrow().iter().copied().collect() }; output.add_pre_vte_instruction_to_multiple_clients( connected_clients.iter().copied(), hide_cursor, ); if self.should_clear_display_before_rendering { let clear_display = "\u{1b}[m\u{1b}[2J"; output.add_pre_vte_instruction_to_multiple_clients( connected_clients.iter().copied(), clear_display, ); output.mark_host_display_cleared_for_clients(connected_clients.iter().copied()); self.should_clear_display_before_rendering = false; } } fn render_cursor(&mut self, output: &mut Output) { let connected_clients: Vec = { self.connected_clients.borrow().iter().copied().collect() }; for client_id in connected_clients { match self.get_active_terminal_cursor_position(client_id) { Some((cursor_position_x, cursor_position_y, is_cursor_visible)) => { let active_pane_z_index = self .get_active_pane_id(client_id) .and_then(|pane_id| self.floating_panes.get_pane_z_index(pane_id)); let not_occluded = output.cursor_is_visible( cursor_position_x, cursor_position_y, active_pane_z_index, ); let active_terminal_is_mid_frame = self .active_terminal_is_mid_frame(client_id) .unwrap_or(false); if active_terminal_is_mid_frame { // no-op, this means the active terminal is currently rendering a frame, // which means the cursor can be jumping around and we definitely do not // want to render it } else if not_occluded && is_cursor_visible { let desired_cursor_shape = self .get_active_pane(client_id) .map(|ap| ap.cursor_shape_csi()) .unwrap_or_default(); let cursor_changed_position_or_shape = self .cursor_positions_and_shape .get(&client_id) .map(|(previous_x, previous_y, previous_shape)| { previous_x != &cursor_position_x || previous_y != &cursor_position_y || previous_shape != &desired_cursor_shape }) .unwrap_or(true); if output.is_dirty() || cursor_changed_position_or_shape { let show_cursor = "\u{1b}[?25h"; let goto_cursor_position = &format!( "\u{1b}[{};{}H\u{1b}[m{}", cursor_position_y + 1, cursor_position_x + 1, desired_cursor_shape ); // goto row/col output.add_post_vte_instruction_to_client(client_id, show_cursor); output.add_post_vte_instruction_to_client( client_id, goto_cursor_position, ); self.cursor_positions_and_shape.insert( client_id, (cursor_position_x, cursor_position_y, desired_cursor_shape), ); } } else if not_occluded { // Cursor is hidden by the app but not occluded by a floating // pane. Position the host terminal cursor at the correct // location (for IME) then hide it. The IME subsystem reads // the cursor position regardless of visibility. let hide_cursor = "\u{1b}[?25l"; let goto_cursor_position = &format!( "\u{1b}[{};{}H", cursor_position_y + 1, cursor_position_x + 1, ); output.add_post_vte_instruction_to_client(client_id, hide_cursor); output.add_post_vte_instruction_to_client(client_id, goto_cursor_position); } else { let hide_cursor = "\u{1b}[?25l"; output.add_post_vte_instruction_to_client(client_id, hide_cursor); } }, None => { let hide_cursor = "\u{1b}[?25l"; output.add_post_vte_instruction_to_client(client_id, hide_cursor); }, } } } pub(crate) fn get_tiled_panes(&self) -> impl Iterator)> { self.tiled_panes.get_panes() } pub(crate) fn get_floating_panes(&self) -> impl Iterator)> { self.floating_panes.get_panes() } pub(crate) fn get_suppressed_panes( &self, ) -> impl Iterator))> { // bool => is_scrollback_editor self.suppressed_panes.iter() } fn get_selectable_tiled_panes(&self) -> impl Iterator)> { self.get_tiled_panes().filter(|(_, p)| p.selectable()) } fn get_selectable_floating_panes(&self) -> impl Iterator)> { self.get_floating_panes().filter(|(_, p)| p.selectable()) } pub fn get_selectable_tiled_panes_count(&self) -> usize { self.get_selectable_tiled_panes().count() } pub fn get_selectable_floating_panes_count(&self) -> usize { self.get_selectable_floating_panes().count() } pub fn get_visible_selectable_floating_panes_count(&self) -> usize { if self.are_floating_panes_visible() { self.get_selectable_floating_panes().count() } else { 0 } } fn get_next_terminal_position(&self) -> usize { let tiled_panes_count = self .tiled_panes .get_panes() .filter(|(k, _)| match k { PaneId::Plugin(_) => false, PaneId::Terminal(_) => true, }) .count(); let floating_panes_count = self .floating_panes .get_panes() .filter(|(k, _)| match k { PaneId::Plugin(_) => false, PaneId::Terminal(_) => true, }) .count(); let hidden_stack_list_members_count = self .suppressed_stack_list_members() .filter(|(pane_id, _)| matches!(pane_id, PaneId::Terminal(_))) .count(); tiled_panes_count + floating_panes_count + hidden_stack_list_members_count + 1 } pub fn has_selectable_panes(&self) -> bool { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); let selectable_floating_panes = self .floating_panes .get_panes() .filter(|(_, p)| p.selectable()); selectable_tiled_panes.count() > 0 || selectable_floating_panes.count() > 0 } pub fn has_selectable_tiled_panes(&self) -> bool { let selectable_tiled_panes = self.tiled_panes.get_panes().filter(|(_, p)| p.selectable()); selectable_tiled_panes.count() > 0 } /// Collapse into a narrow layout when the terminal is too narrow, and spread /// back out when it grows again. /// /// Hysteresis: collapsing happens below `responsive_min_cols`, expanding only /// above `responsive_min_cols + RESPONSIVE_HYSTERESIS_COLS`. Without a dead /// band, dragging a terminal edge that lands exactly on the threshold flips /// the layout on every motion event, and each flip resizes every PTY. /// /// BOTH directions resolve their target from the panes as they are now, /// rather than replaying a remembered one. Remembering a position was the /// source of every bug this feature has had: the remembered layout stops /// applying when the pane count changes, positions are only stable if /// nothing is ever inserted, and a failed apply left the state claiming a /// transition that never happened. The only state kept now is one bit -- /// which side of the threshold we are on -- and even that is flipped only /// after a layout has actually been applied. fn apply_responsive_layout(&mut self, new_size: Size) -> Result<()> { const RESPONSIVE_HYSTERESIS_COLS: usize = 10; if self.responsive_min_cols == 0 { return Ok(()); } let collapse = match ( self.responsive_collapsed, new_size.cols < self.responsive_min_cols, new_size.cols >= self.responsive_min_cols + RESPONSIVE_HYSTERESIS_COLS, ) { (false, true, _) => true, (true, _, true) => false, // inside the dead band, or already on the right side of it _ => return Ok(()), }; let resolved = if collapse { self.responsive_collapse_target() } else { self.responsive_expand_target() }; let Some((position, layout)) = resolved else { // Nothing applied, so change nothing -- not even the flag. Claiming // a transition that did not happen is what previously left a tab // stacked while believing it had expanded, with no path back. return Ok(()); }; self.swap_layouts.set_tiled_layout_position(position); self.dissolve_stack_lists_for_classic_mutation(); self.apply_tiled_layout_candidate(&layout); self.tiled_panes.reapply_pane_frames(); self.set_should_clear_display_before_rendering(); self.responsive_collapsed = collapse; Ok(()) } /// Where to collapse to: the configured swap layout, else a synthesized /// single stack. /// /// The synthesized one is kept under its own name rather than the /// configured one, so a layout that exists but does not currently apply -- /// a `stacked` layout with `min_panes=4` and three panes open, say -- falls /// back instead of failing. fn responsive_collapse_target(&mut self) -> Option<(usize, TiledPaneLayout)> { if let Some(position) = self .swap_layouts .position_of_tiled_layout_named(&self.responsive_layout_name) { if let Some(layout) = self.swap_layouts.tiled_layout_at(position, &self.tiled_panes) { return Some((position, layout)); } } // Logged once per tab: a synthesized layout is not what someone who DID // write a swap file expects, and staying silent would hide both a name // that does not match and a constraint that never fits. if !self.responsive_missing_layout_warned { self.responsive_missing_layout_warned = true; log::info!( "no usable swap layout named {:?} in this tab, so the responsive collapse \ synthesized one that flattens every pane into a single stack. Define \ `swap_tiled_layout name={:?}` in a `.swap.kdl` beside your layout \ file (and check its min_panes/max_panes) to control this yourself.", self.responsive_layout_name, self.responsive_layout_name ); } let position = self .swap_layouts .append_synthesized_stack_layout(RESPONSIVE_COLLAPSE_LAYOUT_NAME); self.swap_layouts .tiled_layout_at(position, &self.tiled_panes) .map(|layout| (position, layout)) } /// Where to expand to, resolved from the current panes. /// /// BASE first, because when the pane count has not changed it reproduces the /// user's actual layout, which no synthesized grid can. It is pinned to /// ExactPanes though, so a single added or closed pane rules it out -- hence /// the grid, which carries no constraint and so always applies. fn responsive_expand_target(&mut self) -> Option<(usize, TiledPaneLayout)> { if let Some(position) = self .swap_layouts .position_of_tiled_layout_named(swap_layouts::BASE_LAYOUT_NAME) { if let Some(layout) = self.swap_layouts.tiled_layout_at(position, &self.tiled_panes) { return Some((position, layout)); } } let position = self .swap_layouts .append_synthesized_grid_layout(RESPONSIVE_EXPAND_LAYOUT_NAME); self.swap_layouts .tiled_layout_at(position, &self.tiled_panes) .map(|layout| (position, layout)) } pub fn resize_whole_tab(&mut self, new_screen_size: Size) -> Result<()> { let err_context = || format!("failed to resize whole tab (id {})", self.id); self.size = new_screen_size; // If a tiled pane is fullscreen, exit fullscreen first so that *all* // tiled panes (including the currently hidden ones) participate in the // resize/relayout. We re-enter fullscreen on the same pane after the // resize so the user-visible state is preserved. Without this, hidden // panes retain stale geometry from before the resize and the layout // solver fails when fullscreen is later toggled off. let fullscreen_pane_to_restore = self.tiled_panes.fullscreen_pane_id(); let fullscreen_covered_ui = self.tiled_panes.fullscreen_covers_ui(); if fullscreen_pane_to_restore.is_some() { self.tiled_panes.unset_fullscreen(); } let floating_fullscreen_pane_to_restore = self.floating_panes.fullscreen_pane_id(); let floating_fullscreen_covered_ui = self.floating_panes.fullscreen_covers_ui(); if floating_fullscreen_pane_to_restore.is_some() { self.floating_panes.unset_fullscreen(); } self.floating_panes.resize(new_screen_size); // we need to do this explicitly because floating_panes.resize does not do this self.floating_panes .resize_pty_all_panes(&mut self.os_api) .with_context(err_context)?; self.tiled_panes.resize(new_screen_size); if self.auto_layout && !self.swap_layouts.is_floating_damaged() { // we do this only for floating panes, because the constraint system takes care of the // tiled panes self.swap_layouts.set_is_floating_damaged(); let _ = self.relayout_floating_panes(false); } if self.auto_layout && !self.swap_layouts.is_tiled_damaged() && fullscreen_pane_to_restore.is_none() { self.swap_layouts.set_is_tiled_damaged(); let _ = self.relayout_tiled_panes(false); } self.set_should_clear_display_before_rendering(); self.senders .send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes) .with_context(|| format!("failed to update plugins with mode info"))?; LayoutApplier::offset_viewport( self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, self.pane_frame_style, ); if let Some(pane_id) = fullscreen_pane_to_restore { if self.tiled_panes.panes_contain(&pane_id) { if fullscreen_covered_ui { self.tiled_panes.toggle_pane_no_ui_fullscreen(pane_id); } else { self.tiled_panes.toggle_pane_fullscreen(pane_id); } } } if let Some(pane_id) = floating_fullscreen_pane_to_restore { if self.floating_panes.panes_contain(&pane_id) { if floating_fullscreen_covered_ui { self.floating_panes.toggle_pane_no_ui_fullscreen(pane_id); } else { self.floating_panes.toggle_pane_fullscreen(pane_id); } } } self.resize_all_stack_list_hidden_members(); // after the geometry settles, decide whether this size warrants // collapsing or expanding self.apply_responsive_layout(new_screen_size)?; Ok(()) } pub fn resize(&mut self, client_id: ClientId, strategy: ResizeStrategy) -> Result<()> { let err_context = || format!("unable to resize pane"); if self.floating_panes.fullscreen_is_active() { return Ok(()); } if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane(client_id, &mut self.os_api, &strategy) .with_context(err_context)?; if successfully_resized { self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" in case of a decrease } } else { self.dissolve_stack_lists_for_classic_mutation(); match self.tiled_panes.resize_active_pane(client_id, &strategy) { Ok(_) => { self.swap_layouts.set_is_tiled_damaged(); }, Err(err) => match err.downcast_ref::() { Some(ZellijError::CantResizeFixedPanes { pane_ids }) => { let mut pane_ids_to_error = vec![]; for (id, is_terminal) in pane_ids { if *is_terminal { pane_ids_to_error.push(PaneId::Terminal(*id)); } else { pane_ids_to_error.push(PaneId::Plugin(*id)); }; } self.senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( pane_ids_to_error, "FIXED!".into(), )) .with_context(err_context)?; }, _ => Err::<(), _>(err).non_fatal(), }, } } Ok(()) } fn set_pane_active_at(&mut self, pane_id: PaneId) { if let Some(pane) = self.tiled_panes.get_pane_mut(pane_id) { pane.set_active_at(Instant::now()); } else if let Some(pane) = self.floating_panes.get_pane_mut(pane_id) { pane.set_active_at(Instant::now()); } } pub fn focus_next_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { self.switch_next_pane_fullscreen(client_id); return; } self.tiled_panes.focus_next_pane(client_id); } pub fn focus_previous_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { self.switch_prev_pane_fullscreen(client_id); return; } self.tiled_panes.focus_previous_pane(client_id); } pub fn focus_last_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.floating_panes.panes_are_visible() { self.floating_panes.focus_last_pane(client_id); return; } else if self.tiled_panes.fullscreen_is_active() { self.switch_last_pane_fullscreen(client_id); return; } self.tiled_panes.focus_last_pane(client_id); } pub fn focus_pane_on_edge(&mut self, direction: Direction, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.focus_pane_on_edge(direction, client_id); } else if self.has_selectable_panes() && !self.tiled_panes.fullscreen_is_active() { self.tiled_panes.focus_pane_on_edge(direction, client_id); } } pub fn focus_pane_adjacent_to( &mut self, pane_id: PaneId, direction: Direction, client_id: ClientId, ) -> Option { if self.floating_panes.panes_are_visible() { return self .floating_panes .focus_pane_adjacent_to(pane_id, direction, client_id); } if self.tiled_panes.fullscreen_is_active() { return None; } self.tiled_panes .focus_pane_adjacent_to(pane_id, direction, client_id) } // returns a boolean that indicates whether the focus moved pub fn move_focus_left(&mut self, client_id: ClientId) -> Result { let err_context = || format!("failed to move focus left for client {}", client_id); if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus( client_id, &self.connected_clients.borrow().iter().copied().collect(), &Direction::Left, ) .with_context(err_context) } else { if !self.has_selectable_panes() { return Ok(false); } if self.tiled_panes.fullscreen_is_active() { return Ok(self.focus_pane_left_fullscreen(client_id)); } Ok(self.tiled_panes.move_focus_left(client_id)) } } pub fn move_focus_down(&mut self, client_id: ClientId) -> Result { let err_context = || format!("failed to move focus down for client {}", client_id); if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus( client_id, &self.connected_clients.borrow().iter().copied().collect(), &Direction::Down, ) .with_context(err_context) } else { if !self.has_selectable_panes() { return Ok(false); } if self.tiled_panes.fullscreen_is_active() { return Ok(self.focus_pane_down_fullscreen(client_id)); } if self.stacked_pane_list_is_active() && self.move_focus_within_stack_list(client_id, true) { return Ok(true); } Ok(self.tiled_panes.move_focus_down(client_id)) } } pub fn move_focus_up(&mut self, client_id: ClientId) -> Result { let err_context = || format!("failed to move focus up for client {}", client_id); if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus( client_id, &self.connected_clients.borrow().iter().copied().collect(), &Direction::Up, ) .with_context(err_context) } else { if !self.has_selectable_panes() { return Ok(false); } if self.tiled_panes.fullscreen_is_active() { return Ok(self.focus_pane_up_fullscreen(client_id)); } if self.stacked_pane_list_is_active() && self.move_focus_within_stack_list(client_id, false) { return Ok(true); } Ok(self.tiled_panes.move_focus_up(client_id)) } } // returns a boolean that indicates whether the focus moved pub fn move_focus_right(&mut self, client_id: ClientId) -> Result { let err_context = || format!("failed to move focus right for client {}", client_id); if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus( client_id, &self.connected_clients.borrow().iter().copied().collect(), &Direction::Right, ) .with_context(err_context) } else { if !self.has_selectable_panes() { return Ok(false); } if self.tiled_panes.fullscreen_is_active() { return Ok(self.focus_pane_right_fullscreen(client_id)); } Ok(self.tiled_panes.move_focus_right(client_id)) } } pub fn move_active_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } let search_backwards = false; if self.floating_panes.panes_are_visible() { self.floating_panes .move_active_pane(search_backwards, &mut self.os_api, client_id); } else { self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .move_active_pane(search_backwards, client_id); } } pub fn move_pane(&mut self, pane_id: PaneId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } let search_backwards = false; if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane(search_backwards, pane_id); } else { self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane(search_backwards, pane_id); } } pub fn move_active_pane_backwards(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } let search_backwards = true; if self.floating_panes.panes_are_visible() { self.floating_panes .move_active_pane(search_backwards, &mut self.os_api, client_id); } else { self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .move_active_pane(search_backwards, client_id); } } pub fn move_active_pane_down(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_active_pane_down(client_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_down(client_id); } } pub fn move_pane_down(&mut self, pane_id: PaneId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane_down(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_down(pane_id); } } pub fn move_active_pane_up(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_active_pane_up(client_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_up(client_id); } } pub fn move_pane_up(&mut self, pane_id: PaneId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane_up(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_up(pane_id); } } pub fn move_active_pane_right(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_active_pane_right(client_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_right(client_id); } } pub fn move_pane_right(&mut self, pane_id: PaneId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane_right(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_right(pane_id); } } pub fn move_active_pane_left(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_active_pane_left(client_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_active_pane_left(client_id); } } pub fn move_pane_left(&mut self, pane_id: PaneId) { if self.floating_panes.panes_are_visible() { self.floating_panes.move_pane_left(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" behind } else { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane_left(pane_id); } } fn close_down_to_max_terminals(&mut self) -> Result<()> { if let Some(max_panes) = self.max_panes { self.dissolve_stack_lists_for_classic_mutation(); let terminals = self.get_tiled_pane_ids(); for &pid in terminals.iter().skip(max_panes - 1) { self.senders .send_to_pty(PtyInstruction::ClosePane(pid, None)) .context("failed to close down to max terminals")?; self.close_pane(pid, false, None); } } Ok(()) } pub fn get_tiled_pane_ids(&self) -> Vec { self.get_tiled_panes().map(|(&pid, _)| pid).collect() } pub fn kitty_visible_pane_ids(&self) -> HashSet { let mut pane_ids: HashSet = self.tiled_panes.rendered_pane_ids().into_iter().collect(); pane_ids.extend(self.floating_panes.rendered_pane_ids()); pane_ids } pub fn get_all_pane_ids(&self) -> Vec { let mut static_and_floating_pane_ids = self.get_static_and_floating_pane_ids(); let mut suppressed_pane_ids = self .suppressed_panes .values() .map(|(_key, pane)| pane.pid()) .collect(); static_and_floating_pane_ids.append(&mut suppressed_pane_ids); static_and_floating_pane_ids } pub fn get_static_and_floating_pane_ids(&self) -> Vec { self.tiled_panes .pane_ids() .chain(self.floating_panes.pane_ids()) .copied() .collect() } pub fn get_plugin_ids(&self) -> Vec { self.get_static_and_floating_pane_ids() .into_iter() .filter_map(|pane_id| match pane_id { PaneId::Plugin(pid) => Some(pid), _ => None, }) .collect() } pub fn get_pane_info(&self, pane_id: PaneId) -> Option { let current_pane_group: HashMap> = { self.current_pane_group.borrow().clone_inner() }; // Check tiled panes if let Some(pane) = self.tiled_panes.get_pane(pane_id) { let mut info = pane_info_for_pane(&pane_id, pane, ¤t_pane_group); // Note: is_focused will be false since we don't have a specific client_id context // Plugins calling this API would need to compare the pane_id with their own focused pane info.is_focused = false; info.is_fullscreen = self.tiled_panes.fullscreen_is_active(); info.is_floating = false; info.is_suppressed = false; return Some(info); } // Check floating panes if let Some(pane) = self.floating_panes.get_pane(pane_id) { let mut info = pane_info_for_pane(&pane_id, pane, ¤t_pane_group); info.is_focused = false; info.is_fullscreen = self.floating_panes.fullscreen_pane_id() == Some(pane_id); info.is_floating = true; info.is_suppressed = false; return Some(info); } // Check suppressed panes if let Some((_previous_id, pane)) = self.suppressed_panes.get(&pane_id) { let mut info = pane_info_for_pane(&pane_id, pane, ¤t_pane_group); info.is_focused = false; info.is_fullscreen = false; info.is_floating = false; info.is_suppressed = true; return Some(info); } None } pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) { if self.is_pending { self.pending_instructions .push(BufferedTabInstruction::SetPaneSelectable(id, selectable)); return; } if let Some(pane) = self.tiled_panes.get_pane_mut(id) { pane.set_selectable(selectable); if !selectable { self.tiled_panes.move_clients_out_of_pane(id); } } else if let Some(pane) = self.floating_panes.get_pane_mut(id) { pane.set_selectable(selectable); if !selectable { self.floating_panes.move_clients_out_of_pane(id); if !self.floating_panes.has_selectable_panes() { self.hide_floating_panes(); } } } // we do this here because if there is a non-selectable pane on the edge, we consider it // outside the viewport (a ui-pane, eg. the status-bar and tab-bar) and need to adjust for it LayoutApplier::offset_viewport( self.viewport.clone(), self.display_area.clone(), &mut self.tiled_panes, self.pane_frame_style, ); } pub fn set_mouse_selection_support(&mut self, pane_id: PaneId, selection_support: bool) { MouseHandler::set_mouse_selection_support(self, pane_id, selection_support); } pub fn close_pane( &mut self, id: PaneId, ignore_suppressed_panes: bool, exit_status: Option, ) { let id_parks_a_different_pane = self.pane_parked_by(&id).is_some(); if !ignore_suppressed_panes && !id_parks_a_different_pane && self.pane_is_stack_list_member(&id) { if self.close_stack_list_member(id, exit_status) { let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::PaneClosed(id.into()), )])); let _ = self.senders .send_to_screen(ScreenInstruction::NotifyPaneClosedToSubscribers { pane_id: id.into(), }); return; } } // we need to ignore suppressed panes when we toggle a pane to be floating/embedded(tiled) // this is because in that case, while we do use this logic, we're not actually closing the // pane, we're moving it if !ignore_suppressed_panes && self.suppressed_panes.contains_key(&id) { return match self.replace_pane_with_suppressed_pane(id) { Ok(_pane) => {}, Err(e) => { Err::<(), _>(e) .with_context(|| format!("failed to close pane {:?}", id)) .non_fatal(); }, }; } let closed_pane = if self.floating_panes.panes_contain(&id) { if self.floating_panes.fullscreen_pane_id() == Some(id) { self.floating_panes.unset_fullscreen(); } let closed_pane = self.floating_panes.remove_pane(id); self.floating_panes.move_clients_out_of_pane(id); if !self.floating_panes.has_selectable_panes() { self.swap_layouts.reset_floating_damage(); self.hide_floating_panes(); } self.set_force_render(); self.floating_panes.set_force_render(); if self.auto_layout && !self.swap_layouts.is_floating_damaged() && self.floating_panes.visible_panes_count() > 0 { self.swap_layouts.set_is_floating_damaged(); // only relayout if the user is already "in" a layout, otherwise this might be // confusing let _ = self.relayout_floating_panes(false); } closed_pane } else { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } let closed_pane = self.tiled_panes.remove_pane(id); self.set_force_render(); self.tiled_panes.set_force_render(); if self.auto_layout && !self.swap_layouts.is_tiled_damaged() { self.swap_layouts.set_is_tiled_damaged(); // only relayout if the user is already "in" a layout, otherwise this might be // confusing let _ = self.relayout_tiled_panes(false); } self.resize_all_stack_list_hidden_members(); closed_pane }; if let Some(exit_status) = exit_status { if let Some(mut closed_pane) = closed_pane { // in case we need to update on Drop closed_pane.update_exit_status(exit_status); } } let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::PaneClosed(id.into()), )])); let _ = self .senders .send_to_screen(ScreenInstruction::NotifyPaneClosedToSubscribers { pane_id: id.into(), }); } pub fn extract_pane( &mut self, id: PaneId, dont_swap_if_suppressed: bool, ) -> Option> { let id_parks_a_different_pane = self.pane_parked_by(&id).is_some(); if self.pane_is_stack_list_member(&id) && (dont_swap_if_suppressed || !id_parks_a_different_pane) { if let StackListRemoval::Removed(removed) = self.remove_stack_list_member(id) { return removed.map(|mut pane| { pane.reset_logical_position(); pane }); } } if !dont_swap_if_suppressed && self.suppressed_panes.contains_key(&id) { // this is done for the scrollback editor return match self.replace_pane_with_suppressed_pane(id) { Ok(mut pane) => { // we do this so that the logical index will not affect ordering in the target tab if let Some(pane) = pane.as_mut() { pane.reset_logical_position(); } pane }, Err(e) => { Err::<(), _>(e) .with_context(|| format!("failed to close pane {:?}", id)) .non_fatal(); None }, }; } if self.floating_panes.panes_contain(&id) { if self.floating_panes.fullscreen_pane_id() == Some(id) { self.floating_panes.unset_fullscreen(); } let mut closed_pane = self.floating_panes.remove_pane(id); self.floating_panes.move_clients_out_of_pane(id); if !self.floating_panes.has_panes() { self.swap_layouts.reset_floating_damage(); self.hide_floating_panes(); } self.set_force_render(); self.floating_panes.set_force_render(); if self.auto_layout && !self.swap_layouts.is_floating_damaged() && self.floating_panes.visible_panes_count() > 0 { self.swap_layouts.set_is_floating_damaged(); // only relayout if the user is already "in" a layout, otherwise this might be // confusing let _ = self.relayout_floating_panes(false); } // we do this so that the logical index will not affect ordering in the target tab if let Some(closed_pane) = closed_pane.as_mut() { closed_pane.reset_logical_position(); } closed_pane } else if self.tiled_panes.panes_contain(&id) { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } let mut closed_pane = self.tiled_panes.remove_pane(id); self.set_force_render(); self.tiled_panes.set_force_render(); if self.auto_layout && !self.swap_layouts.is_tiled_damaged() { self.swap_layouts.set_is_tiled_damaged(); // only relayout if the user is already "in" a layout, otherwise this might be // confusing let _ = self.relayout_tiled_panes(false); } self.resize_all_stack_list_hidden_members(); // we do this so that the logical index will not affect ordering in the target tab if let Some(closed_pane) = closed_pane.as_mut() { closed_pane.reset_logical_position(); } closed_pane } else if let Some(suppressed_key_of_pane) = self .suppressed_panes .iter() .find_map(|(key, (_, pane))| if &pane.pid() == &id { Some(*key) } else { None }) { // TODO: test this (from the path in screen.rs focus_plugin_pane ~line 2519 self.suppressed_panes .remove(&suppressed_key_of_pane) .map(|s_p| s_p.1) } else { None } } pub fn hold_pane( &mut self, id: PaneId, exit_status: Option, is_first_run: bool, run_command: RunCommand, ) { if self.is_pending { self.pending_instructions .push(BufferedTabInstruction::HoldPane( id, exit_status, is_first_run, run_command, )); return; } if self.floating_panes.panes_contain(&id) { self.floating_panes .hold_pane(id, exit_status, is_first_run, run_command); } else if self.tiled_panes.panes_contain(&id) { self.tiled_panes .hold_pane(id, exit_status, is_first_run, run_command); } else if let Some(pane) = self.suppressed_panes.values_mut().find(|p| p.1.pid() == id) { pane.1.hold(exit_status, is_first_run, run_command); } } pub fn replace_pane_with_suppressed_pane( &mut self, pane_id: PaneId, ) -> Result>> { self.suppressed_panes .remove(&pane_id) .with_context(|| { format!( "couldn't find pane with id {:?} in suppressed panes", pane_id ) }) .and_then(|(_is_scrollback_editor, suppressed_pane)| { let suppressed_pane_id = suppressed_pane.pid(); let replaced_pane = if self.are_floating_panes_visible() { Some(self.floating_panes.replace_pane(pane_id, suppressed_pane)).transpose()? } else { self.tiled_panes.replace_pane(pane_id, suppressed_pane) }; if replaced_pane.is_some() { self.track_stack_list_member_id_swap(pane_id, suppressed_pane_id); } if let Some(suppressed_pane) = self .floating_panes .get_pane(suppressed_pane_id) .or_else(|| self.tiled_panes.get_pane(suppressed_pane_id)) { // You may be thinking: why aren't we using the original "suppressed_pane" here, // isn't it the same one? // // Yes, you are right! However, we moved it into its correct environment above // (either floating_panes or tiled_panes) where it received a new geometry based on // the pane there we replaced. Now, we need to update its pty about its new size. // We couldn't do that before, and we can't use the original moved item now - so we // need to refetch it resize_pty!( suppressed_pane, self.os_api, self.senders, self.character_cell_size )?; } Ok(replaced_pane) }) .with_context(|| { format!( "failed to replace active pane with suppressed pane {:?}", pane_id ) }) } pub fn close_focused_pane( &mut self, client_id: ClientId, completion_tx: Option, ) -> Result<()> { let err_context = |pane_id| { format!("failed to close focused pane (ID {pane_id:?}) for client {client_id}") }; if self.floating_panes.panes_are_visible() { if let Some(active_floating_pane_id) = self.floating_panes.active_pane_id(client_id) { self.close_pane(active_floating_pane_id, false, None); self.senders .send_to_pty(PtyInstruction::ClosePane( active_floating_pane_id, completion_tx, )) .with_context(|| err_context(active_floating_pane_id))?; return Ok(()); } } if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { self.close_pane(active_pane_id, false, None); self.senders .send_to_pty(PtyInstruction::ClosePane(active_pane_id, completion_tx)) .with_context(|| err_context(active_pane_id))?; } Ok(()) } pub fn clear_active_terminal_screen(&mut self, client_id: ClientId) -> Result<()> { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.clear_screen(); } Ok(()) } pub fn clear_screen_for_pane_id(&mut self, pane_id: PaneId) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.clear_screen(); } } pub fn dump_active_terminal_screen( &mut self, file: Option, client_id: ClientId, full: bool, ) -> Result<()> { let err_context = || format!("failed to dump active terminal screen for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { let dump = active_pane.dump_screen(full, Some(client_id)); self.os_api .write_to_file(dump, file) .with_context(err_context)?; } Ok(()) } pub fn dump_with_ansi_active_terminal_screen( &mut self, file: Option, client_id: ClientId, full: bool, ) -> Result<()> { let err_context = || format!("failed to dump active terminal screen for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { let dump = active_pane.dump_screen_with_ansi(full, Some(client_id)); self.os_api .write_to_file(dump, file) .with_context(err_context)?; } Ok(()) } pub fn dump_terminal_screen( &mut self, file: Option, pane_id: PaneId, full: bool, ) -> Result<()> { if let Some(pane) = self.get_pane_with_id(pane_id) { let dump = pane.dump_screen(full, None); self.os_api.write_to_file(dump, file).non_fatal() } Ok(()) } pub fn dump_with_ansi_terminal_screen( &mut self, file: Option, pane_id: PaneId, full: bool, ) -> Result<()> { if let Some(pane) = self.get_pane_with_id(pane_id) { let dump = pane.dump_screen_with_ansi(full, None); self.os_api.write_to_file(dump, file).non_fatal() } Ok(()) } pub fn get_dump_active_terminal_screen(&mut self, client_id: ClientId, full: bool) -> String { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.dump_screen(full, Some(client_id)) } else { String::new() } } pub fn get_dump_with_ansi_active_terminal_screen( &mut self, client_id: ClientId, full: bool, ) -> String { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.dump_screen_with_ansi(full, Some(client_id)) } else { String::new() } } pub fn get_dump_terminal_screen(&mut self, pane_id: PaneId, full: bool) -> Option { if let Some(pane) = self.get_pane_with_id(pane_id) { Some(pane.dump_screen(full, None)) } else { None } } pub fn get_dump_with_ansi_terminal_screen( &mut self, pane_id: PaneId, full: bool, ) -> Option { if let Some(pane) = self.get_pane_with_id(pane_id) { Some(pane.dump_screen_with_ansi(full, None)) } else { None } } pub fn edit_scrollback( &mut self, client_id: ClientId, completion_tx: Option, ) -> Result<()> { let err_context = || format!("failed to edit scrollback for client {client_id}"); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_active_terminal_screen( Some(String::from(file.to_string_lossy())), client_id, true, ) .with_context(err_context)?; let line_number = self .get_active_pane(client_id) .and_then(|a_t| a_t.get_line_number()); self.senders .send_to_pty(PtyInstruction::OpenInPlaceEditor( file, line_number, ClientTabIndexOrPaneId::ClientId(client_id), completion_tx, )) .with_context(err_context) } pub fn edit_scrollback_raw( &mut self, client_id: ClientId, completion_tx: Option, ) -> Result<()> { let err_context = || format!("failed to edit scrollback for client {client_id}"); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_with_ansi_active_terminal_screen( Some(String::from(file.to_string_lossy())), client_id, true, ) .with_context(err_context)?; let line_number = self .get_active_pane(client_id) .and_then(|a_t| a_t.get_line_number()); self.senders .send_to_pty(PtyInstruction::OpenInPlaceEditor( file, line_number, ClientTabIndexOrPaneId::ClientId(client_id), completion_tx, )) .with_context(err_context) } pub fn edit_scrollback_for_pane_with_id( &mut self, pane_id: PaneId, completion_tx: Option, ) -> Result<()> { if let PaneId::Terminal(_terminal_pane_id) = pane_id { self.make_hidden_stack_list_member_visible(pane_id); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_terminal_screen(Some(String::from(file.to_string_lossy())), pane_id, true) .non_fatal(); let line_number = self .get_pane_with_id(pane_id) .and_then(|a_t| a_t.get_line_number()); self.senders.send_to_pty(PtyInstruction::OpenInPlaceEditor( file, line_number, ClientTabIndexOrPaneId::PaneId(pane_id), completion_tx, )) } else { log::error!("Editing plugin pane scrollback is currently unsupported."); Ok(()) } } pub fn edit_scrollback_raw_for_pane_with_id( &mut self, pane_id: PaneId, completion_tx: Option, ) -> Result<()> { if let PaneId::Terminal(_terminal_pane_id) = pane_id { self.make_hidden_stack_list_member_visible(pane_id); let mut file = temp_dir(); file.push(format!("{}.dump", Uuid::new_v4())); self.dump_with_ansi_terminal_screen( Some(String::from(file.to_string_lossy())), pane_id, true, ) .non_fatal(); let line_number = self .get_pane_with_id(pane_id) .and_then(|a_t| a_t.get_line_number()); self.senders.send_to_pty(PtyInstruction::OpenInPlaceEditor( file, line_number, ClientTabIndexOrPaneId::PaneId(pane_id), completion_tx, )) } else { log::error!("Editing plugin pane scrollback is currently unsupported."); Ok(()) } } pub fn scroll_active_terminal_up(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.scroll_up(1, client_id); } } pub fn scroll_terminal_up(&mut self, terminal_pane_id: u32) -> Result<()> { let pane_id = PaneId::Terminal(terminal_pane_id); let err_context = || format!("failed to scroll up pane {pane_id:?}"); let fictitious_client_id = 1; let synthetic_position = Position::new(0, 0); let (mouse_sgr, is_alt) = match self.get_pane_with_id(pane_id) { Some(pane) => ( pane.mouse_scroll_up(&synthetic_position), pane.is_alternate_mode_active(), ), None => return Ok(()), }; if let Some(sgr) = mouse_sgr { self.write_to_pane_id(&None, sgr.into_bytes(), false, pane_id, None, None) .with_context(err_context)?; } else if is_alt { self.write_to_pane_id(&None, b"\x1b[A".to_vec(), false, pane_id, None, None) .with_context(err_context)?; } else if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.scroll_up(1, fictitious_client_id); } Ok(()) } pub fn scroll_active_terminal_down(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to scroll down active pane for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.scroll_down(1, client_id); if !active_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = active_pane.pid() { self.process_pending_vte_events(raw_fd) .with_context(err_context)?; } } } Ok(()) } pub fn scroll_terminal_down(&mut self, terminal_pane_id: u32) -> Result<()> { let pane_id = PaneId::Terminal(terminal_pane_id); let err_context = || format!("failed to scroll down pane {pane_id:?}"); let fictitious_client_id = 1; let synthetic_position = Position::new(0, 0); let (mouse_sgr, is_alt) = match self.get_pane_with_id(pane_id) { Some(pane) => ( pane.mouse_scroll_down(&synthetic_position), pane.is_alternate_mode_active(), ), None => return Ok(()), }; if let Some(sgr) = mouse_sgr { self.write_to_pane_id(&None, sgr.into_bytes(), false, pane_id, None, None) .with_context(err_context)?; } else if is_alt { self.write_to_pane_id(&None, b"\x1b[B".to_vec(), false, pane_id, None, None) .with_context(err_context)?; } else { let needs_flush = if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.scroll_down(1, fictitious_client_id); !pane.is_scrolled() } else { false }; if needs_flush { self.process_pending_vte_events(terminal_pane_id) .with_context(err_context)?; } } Ok(()) } pub fn scroll_active_terminal_up_page(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { // prevent overflow when row == 0 let scroll_rows = active_pane.rows().max(1).saturating_sub(1); active_pane.scroll_up(scroll_rows, client_id); } } pub fn scroll_terminal_page_up(&mut self, terminal_pane_id: u32) { if let Some(terminal_pane) = self.get_pane_with_id_mut(PaneId::Terminal(terminal_pane_id)) { let fictitious_client_id = 1; // this is not checked for terminal panes and we // don't have an actual client id here // TODO: traits were a mistake // prevent overflow when row == 0 let scroll_rows = terminal_pane.rows().max(1).saturating_sub(1); terminal_pane.scroll_up(scroll_rows, fictitious_client_id); } } pub fn scroll_active_terminal_down_page(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to scroll down one page in active pane for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { let scroll_rows = active_pane.get_content_rows(); active_pane.scroll_down(scroll_rows, client_id); if !active_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = active_pane.pid() { self.process_pending_vte_events(raw_fd) .with_context(err_context)?; } } } Ok(()) } pub fn scroll_terminal_page_down(&mut self, terminal_pane_id: u32) { if let Some(terminal_pane) = self.get_pane_with_id_mut(PaneId::Terminal(terminal_pane_id)) { let fictitious_client_id = 1; // this is not checked for terminal panes and we // don't have an actual client id here // TODO: traits were a mistake let scroll_rows = terminal_pane.get_content_rows(); terminal_pane.scroll_down(scroll_rows, fictitious_client_id); if !terminal_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = terminal_pane.pid() { self.process_pending_vte_events(raw_fd).non_fatal() } } } } pub fn scroll_active_terminal_up_half_page(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { // prevent overflow when row == 0 let scroll_rows = (active_pane.rows().max(1).saturating_sub(1)) / 2; active_pane.scroll_up(scroll_rows, client_id); } } pub fn scroll_active_terminal_down_half_page(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to scroll down half a page in active pane for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { let scroll_rows = (active_pane.rows().max(1) - 1) / 2; active_pane.scroll_down(scroll_rows, client_id); if !active_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = active_pane.pid() { self.process_pending_vte_events(raw_fd) .with_context(err_context)?; } } } Ok(()) } pub fn scroll_active_terminal_to_bottom(&mut self, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to scroll to bottom in active pane for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.clear_scroll(); if !active_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = active_pane.pid() { self.process_pending_vte_events(raw_fd) .with_context(err_context)?; } } } Ok(()) } pub fn scroll_terminal_to_bottom(&mut self, terminal_pane_id: u32) { if let Some(terminal_pane) = self.get_pane_with_id_mut(PaneId::Terminal(terminal_pane_id)) { terminal_pane.clear_scroll(); if !terminal_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = terminal_pane.pid() { self.process_pending_vte_events(raw_fd).non_fatal(); } } } } pub fn scroll_active_terminal_to_top(&mut self, client_id: ClientId) -> Result<()> { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.clear_scroll(); if let Some(size) = active_pane.get_line_number() { active_pane.scroll_up(size, client_id); } } Ok(()) } pub fn scroll_terminal_to_top(&mut self, terminal_pane_id: u32) { if let Some(terminal_pane) = self.get_pane_with_id_mut(PaneId::Terminal(terminal_pane_id)) { terminal_pane.clear_scroll(); if let Some(size) = terminal_pane.get_line_number() { let fictitious_client_id = 1; // this is not checked for terminal panes and we // don't have an actual client id here // TODO: traits were a mistake terminal_pane.scroll_up(size, fictitious_client_id); } } } pub fn clear_active_terminal_scroll(&mut self, client_id: ClientId) -> Result<()> { // TODO: is this a thing? let err_context = || format!("failed to clear scroll in active pane for client {client_id}"); if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.clear_scroll(); if !active_pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = active_pane.pid() { self.process_pending_vte_events(raw_fd) .with_context(err_context)?; } } } Ok(()) } pub fn handle_scrollwheel_up( &mut self, point: &Position, lines: usize, client_id: ClientId, ) -> Result { MouseHandler::handle_scrollwheel_up(self, point, lines, client_id) } pub fn handle_scrollwheel_down( &mut self, point: &Position, lines: usize, client_id: ClientId, ) -> Result { MouseHandler::handle_scrollwheel_down(self, point, lines, client_id) } fn get_pane_id_at( &mut self, point: &Position, search_selectable: bool, ) -> Result> { let err_context = || format!("failed to get id of pane at position {point:?}"); if self.tiled_panes.fullscreen_is_active() && (self.tiled_panes.fullscreen_covers_ui() || self .is_position_inside_viewport(point) .with_context(err_context)?) { return Ok(self.tiled_panes.fullscreen_pane_id()); } let (stacked_pane_ids_under_flexible_pane, _stacked_pane_ids_over_flexible_pane) = { self.tiled_panes .stacked_pane_ids_under_and_over_flexible_panes() .with_context(err_context)? }; let pane_contains_point = |p: &Box, point: &Position, stacked_pane_ids_under_flexible_pane: &HashSet| -> bool { let is_flexible_in_stack = p.current_geom().is_stacked() && !p.current_geom().rows.is_fixed(); let is_stacked_under = stacked_pane_ids_under_flexible_pane.contains(&p.pid()); let geom_to_compare_against = if is_stacked_under && !self.pane_frame_style.draws_full_frames() { // these sort of panes are one-liner panes under a flexible pane in a stack when we // don't draw pane frames - because the whole stack's content is offset to allow // room for the boundary between panes, they are actually drawn 1 line above where // they are let mut geom = p.current_geom(); geom.y = geom.y.saturating_sub(p.get_content_offset().bottom); geom } else if is_flexible_in_stack && !self.pane_frame_style.draws_full_frames() { // these sorts of panes are flexible panes inside a stack when we don't draw pane // frames - because the whole stack's content is offset to give room for the // boundary between panes, we need to take this offset into account when figuring // out whether the position is inside them let mut geom = p.current_geom(); geom.rows.decrease_inner(p.get_content_offset().bottom); geom } else { p.current_geom() }; geom_to_compare_against.contains(point) }; let found_pane_id = if search_selectable { self.get_selectable_tiled_panes() .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane)) .map(|(&id, _)| id) } else { self.get_tiled_panes() .find(|(_, p)| pane_contains_point(p, point, &stacked_pane_ids_under_flexible_pane)) .map(|(&id, _)| id) }; let resolved_pane_id = found_pane_id.map(|id| self.stack_list_member_at_point(id, point)); if search_selectable { Ok(resolved_pane_id.filter(|id| { self.get_pane_with_id(*id) .map(|p| p.selectable()) .unwrap_or(false) })) } else { Ok(resolved_pane_id) } } pub fn handle_mouse_event( &mut self, event: &MouseEvent, client_id: ClientId, ) -> Result { MouseHandler::handle_mouse_event(self, event, client_id) } pub fn copy_selection(&self, client_id: ClientId) -> Result<()> { let selected_text = self .get_active_pane(client_id) .and_then(|p| p.get_selected_text(client_id)); if let Some(selected_text) = selected_text { self.write_selection_to_clipboard(&selected_text) .with_context(|| { format!("failed to write selection to clipboard for client {client_id}") })?; self.senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), )])) .with_context(|| { format!("failed to inform plugins about copy selection for client {client_id}") }) .non_fatal(); } Ok(()) } pub fn copy_text_to_clipboard(&self, text: &str) -> Result<()> { self.write_selection_to_clipboard(text) .with_context(|| format!("failed to write text to clipboard"))?; self.senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), )])) .with_context(|| "failed to inform plugins about clipboard copy") .non_fatal(); Ok(()) } fn write_selection_to_clipboard(&self, selection: &str) -> Result<()> { let err_context = || format!("failed to write selection to clipboard: '{}'", selection); let mut output = Output::default(); let connected_clients: HashSet = { self.connected_clients.borrow().iter().copied().collect() }; output.add_clients(&connected_clients, self.link_handler.clone(), None); let client_ids = connected_clients.iter().copied(); let clipboard_event = match self .clipboard_provider .set_content(selection, &mut output, client_ids) { Ok(_) => output .serialize() .and_then(|serialized_output| { self.senders .send_to_server(ServerInstruction::Render(Some(serialized_output))) }) .and_then(|_| { Ok(Event::CopyToClipboard( self.clipboard_provider.as_copy_destination(), )) }) .with_context(err_context)?, Err(err) => { Err::<(), _>(err).with_context(err_context).non_fatal(); Event::SystemClipboardFailure }, }; self.senders .send_to_plugin(PluginInstruction::Update(vec![( None, None, clipboard_event, )])) .context("failed to notify plugins about new clipboard event") .non_fatal(); Ok(()) } fn forward_desktop_notifications( &self, notifications: Vec<(String, String)>, pane_id: u32, ) -> Result<()> { let err_context = || "failed to forward desktop notifications to host terminal".to_string(); let mut output = Output::default(); // Use all clients in the app, not just those viewing this tab — // desktop notifications should reach the host terminal regardless // of which tab is currently focused. let all_clients: HashSet = { self.connected_clients_in_app .borrow() .keys() .copied() .collect() }; output.add_clients(&all_clients, self.link_handler.clone(), None); for (payload, terminator) in notifications { // Apply identifier namespacing (Phase 3) // The first semicolon-delimited part of payload is the metadata let (metadata, rest) = match payload.find(';') { Some(idx) => ( payload.get(..idx).unwrap_or_default(), payload.get(idx..).unwrap_or_default(), ), None => (payload.as_str(), ""), }; let namespaced_metadata = namespace_notification_id(metadata, pane_id); let raw = if rest.is_empty() { format!("\x1b]99;{}{}", namespaced_metadata, terminator) } else { format!("\x1b]99;{}{}{}", namespaced_metadata, rest, terminator) }; output.add_post_vte_instruction_to_multiple_clients(all_clients.iter().copied(), &raw); } let serialized_output = output.serialize().with_context(err_context)?; self.senders .send_to_server(ServerInstruction::Render(Some(serialized_output))) .with_context(err_context)?; Ok(()) } pub fn visible(&mut self, visible: bool) -> Result<()> { let pids_in_this_tab = self.tiled_panes.pane_ids().filter_map(|p| match p { PaneId::Plugin(pid) => Some(pid), _ => None, }); let mut plugin_updates = vec![]; for pid in pids_in_this_tab { plugin_updates.push((Some(*pid), None, Event::Visible(visible))); } self.senders .send_to_plugin(PluginInstruction::Update(plugin_updates)) .with_context(|| format!("failed to set visibility of tab to {visible}"))?; if !visible { self.mouse_hover_pane_id.clear(); self.plugin_hover_pane_id.clear(); } Ok(()) } pub fn clear_mouse_help_text(&mut self, client_id: ClientId) { self.mouse_help_text_visible.insert(client_id, false); } pub fn update_active_pane_name(&mut self, buf: Vec, client_id: ClientId) -> Result<()> { let err_context = || format!("failed to update name of active pane to '{buf:?}' for client {client_id}"); let s = str::from_utf8(&buf).with_context(err_context)?; self.get_active_pane_mut(client_id) .with_context(|| format!("no active pane found for client {client_id}")) .map(|active_pane| { let to_update = match s { "\0" => s, // pane name terminator "\u{007F}" | "\u{0008}" => { // delete and backspace keys s }, _ => &clean_string_from_control_and_linebreak(s), }; active_pane.update_name(&to_update); })?; Ok(()) } pub fn rename_pane(&mut self, buf: Vec, pane_id: PaneId) -> Result<()> { let err_context = || { format!( "failed to update name of active pane to '{buf:?}' for pane_id {:?}", pane_id ) }; let pane = self .floating_panes .get_pane_mut(pane_id) .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .get_mut(&pane_id) .map(|s_p| &mut s_p.1) }) .with_context(err_context)?; pane.rename(buf); Ok(()) } pub fn rename_active_pane(&mut self, buf: Vec, client_id: ClientId) -> Result<()> { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.rename(buf); } Ok(()) } pub fn undo_active_rename_pane(&mut self, client_id: ClientId) -> Result<()> { if let Some(active_terminal_id) = self.get_active_terminal_id(client_id) { let active_terminal = if self.are_floating_panes_visible() { self.floating_panes .get_pane_mut(PaneId::Terminal(active_terminal_id)) } else { self.tiled_panes .get_pane_mut(PaneId::Terminal(active_terminal_id)) } .with_context(|| { format!("failed to undo rename of active pane for client {client_id}") })?; active_terminal.load_pane_name(); } Ok(()) } pub fn is_position_inside_viewport(&self, point: &Position) -> Result { let Position { line: Line(line), column: Column(column), } = *point; let line: usize = line.try_into().with_context(|| { format!("failed to determine if position {point:?} is inside viewport") })?; let viewport = self.viewport.borrow(); Ok(line >= viewport.y && column >= viewport.x && line < viewport.y + viewport.rows && column < viewport.x + viewport.cols) } pub fn set_pane_frames(&mut self, pane_frame_style: PaneFrameStyle) { self.tiled_panes.set_pane_frames(pane_frame_style); self.floating_panes.set_pane_frame_style(pane_frame_style); self.pane_frame_style = pane_frame_style; self.set_should_clear_display_before_rendering(); self.set_force_render(); } pub fn panes_to_hide_count(&self) -> usize { self.tiled_panes.panes_to_hide_count() } pub fn update_search_term(&mut self, buf: Vec, client_id: ClientId) -> Result<()> { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { // It only allows terminating char(\0), printable unicode, delete and backspace keys. // TODO: we should really remove this limitation to allow searching for emojis and // other wide chars - currently the search mechanism itself ignores wide chars, so we // should first fix that before removing this condition let is_updatable = buf .iter() .all(|u| matches!(u, 0x00 | 0x20..=0x7E | 0x08 | 0x7F)); if is_updatable { let s = str::from_utf8(&buf).with_context(|| { format!("failed to update search term to '{buf:?}' for client {client_id}") })?; active_pane.update_search_term(s); } } Ok(()) } pub fn search_down(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.search_down(); } } pub fn search_up(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.search_up(); } } pub fn toggle_search_case_sensitivity(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.toggle_search_case_sensitivity(); } } pub fn toggle_search_wrap(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.toggle_search_wrap(); } } pub fn toggle_search_whole_words(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.toggle_search_whole_words(); } } pub fn clear_search(&mut self, client_id: ClientId) { if let Some(active_pane) = self.get_active_pane_or_floating_pane_mut(client_id) { active_pane.clear_search(); } } pub fn is_pending(&self) -> bool { self.is_pending } pub fn add_red_pane_frame_color_override( &mut self, pane_id: PaneId, error_text: Option, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.add_red_pane_frame_color_override(error_text); } } pub fn add_highlight_pane_frame_color_override( &mut self, pane_id: PaneId, error_text: Option, client_id: Option, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.add_highlight_pane_frame_color_override(error_text, client_id); } } pub fn clear_pane_frame_color_override( &mut self, pane_id: PaneId, client_id: Option, ) { if let Some(pane) = self .tiled_panes .get_pane_mut(pane_id) .or_else(|| self.floating_panes.get_pane_mut(pane_id)) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == pane_id) .map(|s_p| &mut s_p.1) }) { pane.clear_pane_frame_color_override(client_id); } } pub fn set_plugin_regex_highlights_for_pane( &mut self, pane_id: PaneId, plugin_id: u32, highlights: Vec, style: &Style, ) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.set_plugin_regex_highlights(plugin_id, highlights, style); } } pub fn clear_all_plugin_highlights(&mut self, plugin_id: u32) { for pane_id in self.get_all_pane_ids() { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.clear_plugin_highlights(plugin_id); } } } pub fn clear_plugin_highlights_for_pane(&mut self, pane_id: PaneId, plugin_id: u32) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.clear_plugin_highlights(plugin_id); } } pub fn update_plugin_loading_stage(&mut self, pid: u32, loading_indication: LoadingIndication) { if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1) }) { plugin_pane.update_loading_indication(loading_indication); } } pub fn show_plugin_cursor( &mut self, pid: u32, client_id: ClientId, cursor_position: Option<(usize, usize)>, ) { if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1) }) { plugin_pane.show_cursor(client_id, cursor_position); } } pub fn start_plugin_loading_indication( &mut self, pid: u32, loading_indication: LoadingIndication, ) { if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1) }) { plugin_pane.start_loading_indication(loading_indication); } } pub fn progress_plugin_loading_offset(&mut self, pid: u32) { if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { self.suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1) }) { plugin_pane.progress_animation_offset(); } } pub fn show_floating_panes(&mut self) { // this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true) self.floating_panes.toggle_show_panes(true); self.tiled_panes.unfocus_all_panes(); self.set_force_render(); } pub fn hide_floating_panes(&mut self) { // this function is to be preferred to directly invoking // floating_panes.toggle_show_panes(false) if self.floating_panes.fullscreen_is_active() { self.floating_panes.unset_fullscreen(); } self.floating_panes.toggle_show_panes(false); self.tiled_panes.focus_all_panes(); self.set_force_render(); } pub fn show_floating_panes_atomic(&mut self, mut completion: Option) { if self.floating_panes.panes_are_visible() { if let Some(c) = completion.as_mut() { c.set_exit_status(2); } } else { match self.floating_panes.last_selectable_floating_pane_id() { Some(last_selectable_floating_pane_id) => { self.show_floating_panes(); self.floating_panes .focus_pane_for_all_clients(last_selectable_floating_pane_id); if let Some(c) = completion.as_mut() { c.set_exit_status(0); } }, None => { // No selectable floating panes exist — surface must not be shown if let Some(c) = completion.as_mut() { c.set_exit_status(1); } }, } } drop(completion); } pub fn hide_floating_panes_atomic(&mut self, mut completion: Option) { if !self.floating_panes.panes_are_visible() { if let Some(c) = completion.as_mut() { c.set_exit_status(2); } } else { self.hide_floating_panes(); if let Some(c) = completion.as_mut() { c.set_exit_status(0); } } drop(completion); } pub fn find_plugin(&self, run_plugin_or_alias: &RunPluginOrAlias) -> Option { self.tiled_panes .get_plugin_pane_id(run_plugin_or_alias) .or_else(|| self.floating_panes.get_plugin_pane_id(run_plugin_or_alias)) .or_else(|| { self.suppressed_panes .iter() .find(|(_id, (_, pane))| { run_plugin_or_alias.is_equivalent_to_run(pane.invoked_with()) }) .map(|(_, (_, pane))| pane.pid()) // TODO: does this break things???? }) } pub fn focus_pane_with_id( &mut self, pane_id: PaneId, should_float: bool, should_be_in_place: bool, client_id: ClientId, ) -> Result<()> { // TODO: should error if pane is not selectable if self.focus_hidden_stack_list_member(pane_id, client_id) { return Ok(()); } self.tiled_panes .focus_pane_if_exists(pane_id, client_id) .map(|_| self.hide_floating_panes()) .or_else(|_| { let focused_floating_pane = self.floating_panes.focus_pane_if_exists(pane_id, client_id); if focused_floating_pane.is_ok() { self.show_floating_panes() }; focused_floating_pane }) // TODO: change suppressed_panes to be a proper struct with methods that make sense rather // than doing this dance every time .or_else(|_| { match self .suppressed_panes .extract_if(|_key, (_, pane)| pane.pid() == pane_id) .next() .map(|(_key, (_, pane))| pane) { Some(mut pane) => { pane.set_selectable(true); if should_float { self.show_floating_panes(); self.add_floating_pane(pane, pane_id, None, true, Some(client_id)) } else if should_be_in_place { let replaced_pane = if self.are_floating_panes_visible() { self.floating_panes .replace_active_pane(pane, client_id) .ok() } else { self.tiled_panes.replace_active_pane(pane, client_id) }; if let Some(replaced_pane) = replaced_pane { let is_scrollback_editor = false; self.track_stack_list_member_id_swap(replaced_pane.pid(), pane_id); self.insert_suppressed_pane( pane_id, (is_scrollback_editor, replaced_pane), ); } else { log::error!("Could not find pane to replace, aborting."); } Ok(()) } else { self.hide_floating_panes(); self.add_tiled_pane(pane, pane_id, false, Some(client_id)) } }, None => Ok(()), } }) } pub fn focus_suppressed_pane_for_all_clients(&mut self, pane_id: PaneId) { if self.make_hidden_stack_list_member_visible(pane_id) { self.tiled_panes.focus_pane_for_all_clients(pane_id); return; } match self.suppressed_panes.remove(&pane_id) { Some(pane) => { self.show_floating_panes(); self.add_floating_pane(pane.1, pane_id, None, true, None) .non_fatal(); self.floating_panes.focus_pane_for_all_clients(pane_id); }, None => { log::error!("Could not find suppressed pane wiht id: {:?}", pane_id); }, } } pub fn suppress_pane(&mut self, pane_id: PaneId, _client_id: Option) { // this method places a pane in the suppressed pane with its own ID - this means we'll // not take it out of there when another pane is closed (eg. like happens with the // scrollback editor), but it has to take itself out on its own (eg. a plugin using the // show_self() method) if let Some(pane) = self.extract_pane(pane_id, true) { self.insert_suppressed_pane(pane_id, (false, pane)); } } fn make_hidden_stack_list_member_visible(&mut self, pane_id: PaneId) -> bool { self.swap_in_hidden_stack_list_member(pane_id, None) } pub fn unsuppress_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { // removes a pane from being suppressed (hidden) but does not focus it if self.make_hidden_stack_list_member_visible(pane_id) { return; } match self .suppressed_panes .extract_if(|_key, (_, pane)| pane.pid() == pane_id) .next() .map(|(_key, (_, pane))| pane) { Some(pane) => { if should_float_if_hidden { self.add_floating_pane(pane, pane_id, None, true, None) .non_fatal(); } else { self.add_tiled_pane(pane, pane_id, false, None).non_fatal(); } }, None => { log::error!("Could not find suppressed pane with id: {:?}", pane_id); }, } } pub fn unsuppress_or_expand_pane(&mut self, pane_id: PaneId, should_float_if_hidden: bool) { // removes a pane from being suppressed (hidden) but does not focus it if self.make_hidden_stack_list_member_visible(pane_id) { return; } match self .suppressed_panes .extract_if(|_key, (_, pane)| pane.pid() == pane_id) .next() .map(|(_key, (_, pane))| pane) { Some(pane) => { if should_float_if_hidden { self.add_floating_pane(pane, pane_id, None, true, None) .non_fatal(); } else { self.add_tiled_pane(pane, pane_id, false, None).non_fatal(); } }, None => { let expand_panes_success = self.tiled_panes.expand_pane_in_stack(pane_id).len() > 0; if !expand_panes_success { log::error!( "Could not find suppressed or stacked pane with id: {:?}", pane_id ); } }, } } fn insert_suppressed_pane( &mut self, suppressing_pane_id: PaneId, pane_to_suppress: (bool, Box), ) { // bool -> is_scrollback_editor // this method is intended to insert an existing provided pane into suppressed_panes, while // making sure any existing panes already in suppressed_panes still remain there // if there's a key collision (eg. the suppressing_pane_id already suppresses another // pane), we fix the colliding pane so that it now becomes suppressed by its own id (i.e. // needs to explicitly be removed from suppressed_panes by eg. a plugin action rather than // being conditionally removed when its suppressing pane is closed or itself suppressed) // // - suppressing_pane_id: the id of the pane suppressing this pane - the pane that if // closed, should trigger this pane being unsuppressed // - pane_to_suppress: the pane itself that we want to suppress // this closure removes and returns an existing pane from the map if it exists under this key // and inserts the given pane into the map under this key let mut insert_and_return_existing_value = |key: PaneId, value: (bool, Box)| -> Option<(bool, Box)> { let existing = self.suppressed_panes.remove(&key); self.suppressed_panes.insert(key, value); existing }; // here we try to insert a pane into the suppressed panes map while making sure not to drop // (close) any existing panes in the map. We repeat the action of remapping existing panes // under their own keys until all keys either reference the suppressing_pane_id (only one // can do this) or themselves let mut key_to_insert = suppressing_pane_id; let mut pane_to_insert = Some(pane_to_suppress); loop { pane_to_insert = pane_to_insert .take() .and_then(|p| insert_and_return_existing_value(key_to_insert, p)); if let Some(pane_to_insert) = pane_to_insert.as_ref() { key_to_insert = pane_to_insert.1.pid(); } else { break; } } } pub fn pane_infos(&self) -> Vec { let mut pane_info = vec![]; let current_pane_group = { self.current_pane_group.borrow().clone_inner() }; let mut tiled_pane_info = self.tiled_panes.pane_info(¤t_pane_group); let mut floating_pane_info = self.floating_panes.pane_info(¤t_pane_group); pane_info.append(&mut tiled_pane_info); pane_info.append(&mut floating_pane_info); for (_pane_id_of_suppressing_pane, (_is_scrollback_editor, pane)) in self.suppressed_panes.iter() { let mut pane_info_for_suppressed_pane = pane_info_for_pane(&pane.pid(), pane, ¤t_pane_group); pane_info_for_suppressed_pane.is_floating = false; pane_info_for_suppressed_pane.is_suppressed = true; pane_info_for_suppressed_pane.is_focused = false; pane_info_for_suppressed_pane.is_fullscreen = false; pane_info.push(pane_info_for_suppressed_pane); } pane_info } pub fn add_floating_pane( &mut self, mut pane: Box, pane_id: PaneId, floating_pane_coordinates: Option, should_focus_new_pane: bool, client_id: Option, ) -> Result<()> { let err_context = || format!("failed to add floating pane"); if self.floating_panes.fullscreen_is_active() { self.floating_panes.unset_fullscreen(); } if let Some(mut new_pane_geom) = self.floating_panes.find_room_for_new_pane() { if let Some(floating_pane_coordinates) = &floating_pane_coordinates { let viewport = self.viewport.borrow(); if let Some(pinned) = floating_pane_coordinates.pinned.as_ref() { pane.set_pinned(*pinned); } new_pane_geom.adjust_coordinates(floating_pane_coordinates.clone(), *viewport); self.swap_layouts.set_is_floating_damaged(); } pane.set_active_at(Instant::now()); pane.set_geom(new_pane_geom); let is_borderless = floating_pane_coordinates .and_then(|c| c.borderless) .unwrap_or(false); if is_borderless { pane.set_content_offset(Offset::frame(0)); pane.set_borderless(true); } else { pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame pane.render_full_viewport(); // to make sure the frame is re-rendered } resize_pty!(pane, self.os_api, self.senders, self.character_cell_size) .with_context(err_context)?; self.floating_panes.add_pane(pane_id, pane); if should_focus_new_pane { if let Some(client_id) = client_id { self.floating_panes.focus_pane(pane_id, client_id); } else { self.floating_panes.focus_pane_for_all_clients(pane_id); } } } if self.auto_layout && !self.swap_layouts.is_floating_damaged() { // only do this if we're already in this layout, otherwise it might be // confusing and not what the user intends self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the // next layout self.relayout_floating_panes(false)?; } Ok(()) } pub fn add_tiled_pane( &mut self, mut pane: Box, pane_id: PaneId, without_relayout: bool, client_id: Option, ) -> Result<()> { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } self.dissolve_stack_lists_for_classic_mutation(); let should_auto_layout = self.auto_layout && !self.swap_layouts.is_tiled_damaged() && !without_relayout; if self.tiled_panes.has_room_for_new_pane() { pane.set_active_at(Instant::now()); if should_auto_layout { // no need to relayout here, we'll do it when reapplying the swap layout // below self.tiled_panes .insert_pane_without_relayout(pane_id, pane, client_id); } else { self.tiled_panes.insert_pane(pane_id, pane, client_id); } self.tiled_panes.reapply_pane_frames(); self.set_should_clear_display_before_rendering(); if let Some(client_id) = client_id { self.tiled_panes.focus_pane(pane_id, client_id); } } if should_auto_layout { // only do this if we're already in this layout, otherwise it might be // confusing and not what the user intends self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the // next layout self.relayout_tiled_panes(false)?; } Ok(()) } pub fn add_stacked_pane_to_pane_id( &mut self, pane: Box, pane_id: PaneId, root_pane_id: PaneId, should_focus: bool, ) -> Result<()> { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .add_pane_to_stack_of_pane_id(pane_id, pane, root_pane_id); self.set_should_clear_display_before_rendering(); if should_focus { self.tiled_panes.expand_pane_in_stack(pane_id); } self.swap_layouts.set_is_tiled_damaged(); Ok(()) } pub fn add_stacked_pane_to_active_pane( &mut self, pane: Box, pane_id: PaneId, client_id: ClientId, should_focus: bool, ) -> Result<()> { if self.tiled_panes.fullscreen_is_active() { self.tiled_panes.unset_fullscreen(); } self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes .add_pane_to_stack_of_active_pane(pane_id, pane, client_id); if should_focus { self.tiled_panes.focus_pane(pane_id, client_id); } self.swap_layouts.set_is_tiled_damaged(); Ok(()) } pub fn request_plugin_permissions(&mut self, pid: u32, permissions: Option) { let mut should_focus_pane = false; if let Some(plugin_pane) = self .tiled_panes .get_pane_mut(PaneId::Plugin(pid)) .or_else(|| self.floating_panes.get_pane_mut(PaneId::Plugin(pid))) .or_else(|| { let mut suppressed_pane = self .suppressed_panes .values_mut() .find(|s_p| s_p.1.pid() == PaneId::Plugin(pid)) .map(|s_p| &mut s_p.1); if let Some(suppressed_pane) = suppressed_pane.as_mut() { if permissions.is_some() { // here what happens is that we're requesting permissions for a pane that // is suppressed, meaning the user cannot see the permission request // so we temporarily focus this pane as a floating pane, marking it so that // once the permissions are accepted/rejected by the user, it will be // suppressed again suppressed_pane.set_should_be_suppressed(true); should_focus_pane = true; } } suppressed_pane }) { plugin_pane.request_permissions_from_user(permissions); } if should_focus_pane { self.focus_suppressed_pane_for_all_clients(PaneId::Plugin(pid)); } } pub fn rerun_terminal_pane_with_id( &mut self, terminal_pane_id: u32, completion_tx: Option, ) { let pane_id = PaneId::Terminal(terminal_pane_id); match self .floating_panes .get_mut(&pane_id) .or_else(|| self.tiled_panes.get_pane_mut(pane_id)) .or_else(|| self.suppressed_panes.get_mut(&pane_id).map(|p| &mut p.1)) { Some(pane_to_rerun) => { if let Some(command_to_rerun) = pane_to_rerun.rerun() { self.pids_waiting_resize.insert(terminal_pane_id); let _ = self.senders.send_to_pty(PtyInstruction::ReRunCommandInPane( pane_id, command_to_rerun, completion_tx, )); } else { log::error!("Pane is still running!") } }, None => { log::error!( "Failed to find terminal pane with id {} to rerun in tab", terminal_pane_id ); }, } } pub fn resize_pane_with_id(&mut self, strategy: ResizeStrategy, pane_id: PaneId) -> Result<()> { let err_context = || format!("unable to resize pane"); if self.pane_is_stack_list_member(&pane_id) { self.dissolve_stack_lists_for_classic_mutation(); } if self.floating_panes.panes_contain(&pane_id) { let successfully_resized = self .floating_panes .resize_pane_with_id(strategy, pane_id) .with_context(err_context)?; if successfully_resized { self.swap_layouts.set_is_floating_damaged(); self.swap_layouts.set_is_tiled_damaged(); self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" in case of a decrease } } else if self.tiled_panes.panes_contain(&pane_id) { match self .tiled_panes .resize_pane_with_id(strategy, pane_id, None) { Ok(_) => {}, Err(err) => match err.downcast_ref::() { Some(ZellijError::CantResizeFixedPanes { pane_ids }) => { let mut pane_ids_to_error = vec![]; for (id, is_terminal) in pane_ids { if *is_terminal { pane_ids_to_error.push(PaneId::Terminal(*id)); } else { pane_ids_to_error.push(PaneId::Plugin(*id)); }; } self.senders .send_to_background_jobs(BackgroundJob::DisplayPaneError( pane_ids_to_error, "FIXED!".into(), )) .with_context(err_context)?; }, _ => Err::<(), _>(err).non_fatal(), }, } } else if self .suppressed_panes .values() .any(|s_p| s_p.1.pid() == pane_id) { log::error!("Cannot resize suppressed panes"); } Ok(()) } pub fn update_theme(&mut self, theme: Styling) { self.style.colors = theme; // The tab's `default_mode_info` is what `update_input_modes` // falls back to when no per-client entry exists in // `self.mode_info`. Without this update, plugins on a // freshly-connected client (one that has never manually // changed mode) receive `Event::ModeUpdate` carrying the old // style and skip their re-render. self.default_mode_info.update_theme(theme); self.floating_panes.update_pane_themes(theme); self.tiled_panes.update_pane_themes(theme); for (_, pane) in self.suppressed_panes.values_mut() { pane.update_theme(theme); } } pub fn update_rounded_corners(&mut self, rounded_corners: bool) { self.style.rounded_corners = rounded_corners; self.floating_panes .update_pane_rounded_corners(rounded_corners); self.tiled_panes .update_pane_rounded_corners(rounded_corners); for (_, pane) in self.suppressed_panes.values_mut() { pane.update_rounded_corners(rounded_corners); } } /// Set a pane's purpose from outside the pane (CLI spawn, layout). Outranks /// anything the program inside the pane declares via OSC. pub fn set_pane_purpose(&mut self, pane_id: PaneId, purpose: Option) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.set_purpose(purpose); } } /// Opaque agent-facing hint for what a pane is for, if it has one. pub fn pane_purpose(&self, pane_id: PaneId) -> Option { self.get_pane_with_id(pane_id).and_then(|p| p.purpose()) } pub fn update_frame_layout( &mut self, title_alignment: zellij_utils::data::PaneTitleAlignment, gap_x: usize, gap_y: usize, padding: zellij_utils::data::PanePadding, inactive_grayscale: u8, hide_default_pane_names: bool, status_bar_button_style: zellij_utils::input::theme::StatusBarButtonStyle, ) { self.style.pane_title_alignment = title_alignment; self.style.pane_gap_x = gap_x; self.style.pane_gap_y = gap_y; self.style.pane_padding = padding; self.style.inactive_pane_grayscale = inactive_grayscale; self.style.hide_default_pane_names = hide_default_pane_names; self.style.status_bar_button_style = status_bar_button_style; self.floating_panes.update_pane_frame_layout( title_alignment, gap_x, gap_y, padding, inactive_grayscale, hide_default_pane_names, status_bar_button_style, ); self.tiled_panes.update_pane_frame_layout( title_alignment, gap_x, gap_y, padding, inactive_grayscale, hide_default_pane_names, status_bar_button_style, ); for (_, pane) in self.suppressed_panes.values_mut() { pane.update_frame_layout( title_alignment, gap_x, gap_y, padding, inactive_grayscale, hide_default_pane_names, status_bar_button_style, ); } } pub fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) { self.arrow_fonts = should_support_arrow_fonts; self.floating_panes .update_pane_arrow_fonts(should_support_arrow_fonts); self.tiled_panes .update_pane_arrow_fonts(should_support_arrow_fonts); for (_, pane) in self.suppressed_panes.values_mut() { pane.update_arrow_fonts(should_support_arrow_fonts); } } pub fn update_kitty_host_support(&mut self, supported: KittyHostSupport) { self.kitty_host_support = Some(supported); self.floating_panes .update_pane_kitty_host_support(supported); self.tiled_panes.update_pane_kitty_host_support(supported); for (_, pane) in self.suppressed_panes.values_mut() { pane.update_kitty_host_support(supported); } } pub fn update_default_shell(&mut self, mut default_shell: Option) { if let Some(default_shell) = default_shell.take() { self.default_shell = default_shell; } } pub fn update_default_editor(&mut self, mut default_editor: Option) { if let Some(default_editor) = default_editor.take() { self.default_editor = Some(default_editor); } } pub fn update_copy_options(&mut self, copy_options: &CopyOptions) { self.clipboard_provider = match ©_options.command { Some(command) => ClipboardProvider::Command(CopyCommand::new(command.clone())), None => ClipboardProvider::Osc52(copy_options.clipboard), }; self.copy_on_select = copy_options.copy_on_select; } pub fn update_auto_layout(&mut self, auto_layout: bool) { self.auto_layout = auto_layout; } pub fn update_advanced_mouse_actions(&mut self, advanced_mouse_actions: bool) { self.advanced_mouse_actions = advanced_mouse_actions; } pub fn update_mouse_scroll_resize(&mut self, mouse_scroll_resize: bool) { self.mouse_scroll_resize = mouse_scroll_resize; } pub fn update_mouse_hover_help_text(&mut self, mouse_hover_help_text: bool) { self.mouse_hover_help_text = mouse_hover_help_text; } pub fn update_mouse_hover_effects(&mut self, mouse_hover_effects: bool) { self.mouse_hover_effects = mouse_hover_effects; } /// Is this client currently typing into a modal prompt (renaming a pane or /// tab, entering a search term)? /// /// These modes act on whatever is ACTIVE, so letting hover move the focus /// underneath them drags the prompt from pane to pane: rename one pane, /// then sweep the mouse across the tab and every pane you touch appears to /// enter rename mode. pub fn client_is_in_text_entry_mode(&self, client_id: ClientId) -> bool { self.mode_info .borrow() .get(&client_id) .map(|mode_info| { matches!( mode_info.mode, InputMode::RenamePane | InputMode::RenameTab | InputMode::EnterSearch | InputMode::Prompt ) }) .unwrap_or(false) } /// Threshold + target layout for the responsive collapse. Set from config /// at tab creation and on reconfigure. pub fn update_responsive_layout(&mut self, min_cols: usize, layout_name: String) { self.responsive_min_cols = min_cols; self.responsive_layout_name = layout_name; } /// Cap on stacked-pane picker rows. Takes effect on the next list refresh, /// which is what recomputes the reserved rows. pub fn update_stack_list_max_rows(&mut self, max_rows: usize) { self.stack_list_max_rows = max_rows; } pub fn update_stack_list_grid(&mut self, grid: bool) { self.stack_list_grid = grid; } pub fn update_stack_list_style(&mut self, style: StackListStyle) { self.stack_list_style = style; } pub fn update_focus_follows_mouse(&mut self, focus_follows_mouse: bool) { self.focus_follows_mouse = focus_follows_mouse; } pub fn update_mouse_click_through(&mut self, mouse_click_through: bool) { self.mouse_click_through = mouse_click_through; } pub fn clear_mouse_hover_state(&mut self) { self.mouse_hover_pane_id.clear(); self.plugin_hover_pane_id.clear(); self.mouse_last_pane_id.clear(); self.mouse_help_text_visible.clear(); } pub fn update_web_sharing(&mut self, web_sharing: WebSharing) { let old_value = self.web_sharing; self.web_sharing = web_sharing; if old_value != self.web_sharing { let _ = self.update_input_modes(); } } pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes { let stack_list_members = &self.stack_list_of_member; self.suppressed_panes .extract_if(|key, _| !stack_list_members.contains_key(key)) .collect() } pub fn add_suppressed_panes(&mut self, mut suppressed_panes: SuppressedPanes) { for (pane_id, suppressed_pane_entry) in suppressed_panes.drain() { self.insert_suppressed_pane(pane_id, suppressed_pane_entry); } } pub fn toggle_pane_pinned(&mut self, client_id: ClientId) { if let Some(pane) = self.get_active_pane_mut(client_id) { pane.toggle_pinned(); self.set_force_render(); } } pub fn set_floating_pane_pinned(&mut self, pane_id: PaneId, should_be_pinned: bool) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.set_pinned(should_be_pinned); self.set_force_render(); } } pub fn has_room_for_stack(&mut self, root_pane_id: PaneId, stack_size: usize) -> bool { self.dissolve_stack_lists_for_classic_mutation(); if self.floating_panes.panes_contain(&root_pane_id) || self.suppressed_panes.contains_key(&root_pane_id) { log::error!("Root pane of stack cannot be floating or suppressed"); return false; } if self.pane_is_stacked(root_pane_id) { let room_left_in_stack = self .tiled_panes .room_left_in_stack_of_pane_id(&root_pane_id) .unwrap_or(0); stack_size <= room_left_in_stack } else { self.get_pane_with_id(root_pane_id) .map(|p| p.position_and_size().rows.as_usize() >= stack_size + MIN_TERMINAL_HEIGHT) .unwrap_or(false) } } pub fn set_tiled_panes_damaged(&mut self) { self.swap_layouts.set_is_tiled_damaged(); } pub fn stack_panes(&mut self, root_pane_id: PaneId, mut panes_to_stack: Vec>) { if panes_to_stack.is_empty() { // nothing to do return; } self.dissolve_stack_lists_for_classic_mutation(); self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first if self.pane_is_stacked(root_pane_id) { for pane in panes_to_stack.drain(..) { self.tiled_panes.add_pane_to_stack(&root_pane_id, pane); } } else { // + 1 for the root pane let mut stack_geoms = self .tiled_panes .stack_panes(root_pane_id, panes_to_stack.len() + 1); if stack_geoms.is_empty() { log::error!("Failed to find room for stacked panes"); return; } self.tiled_panes .set_geom_for_pane_with_id(&root_pane_id, stack_geoms.remove(0)); let mut focused_pane_id_in_stack = None; for mut pane in panes_to_stack.drain(..) { let pane_id = pane.pid(); let stack_geom = stack_geoms.remove(0); pane.set_geom(stack_geom); self.tiled_panes.add_pane_with_existing_geom(pane_id, pane); if self.tiled_panes.pane_id_is_focused(&pane_id) { focused_pane_id_in_stack = Some(pane_id); } } // if we had a focused pane in the stack, we expand it if let Some(focused_pane_id_in_stack) = focused_pane_id_in_stack { self.tiled_panes .expand_pane_in_stack(focused_pane_id_in_stack); } else if self.tiled_panes.pane_id_is_focused(&root_pane_id) { self.tiled_panes.expand_pane_in_stack(root_pane_id); } } } pub fn change_floating_pane_coordinates( &mut self, pane_id: &PaneId, floating_pane_coordinates: FloatingPaneCoordinates, ) -> Result<()> { if !self.floating_panes.panes_contain(pane_id) { // if these panes are not floating, we make them floating (assuming doing so wouldn't // be removing the last selectable tiled pane in the tab, which would close it) if (self.tiled_panes.panes_contain(&pane_id) && self.get_selectable_tiled_panes().count() <= 1) || self.suppressed_panes.contains_key(pane_id) { if let Some(pane) = self.extract_pane(*pane_id, true) { self.add_floating_pane(pane, *pane_id, None, false, None)?; } } } self.floating_panes .change_pane_coordinates(*pane_id, floating_pane_coordinates)?; self.set_force_render(); self.swap_layouts.set_is_floating_damaged(); Ok(()) } pub fn toggle_pane_borderless(&mut self, pane_id: &PaneId) -> Result<()> { if let Some(pane) = self.floating_panes.get_pane_mut(*pane_id) { let is_borderless = pane.borderless(); if is_borderless { pane.set_content_offset(Offset::frame(1)); } else { pane.set_content_offset(Offset::default()); } pane.set_borderless(!is_borderless); self.set_force_render(); return Ok(()); } if let Some(pane) = self.tiled_panes.get_pane_mut(*pane_id) { let is_borderless = pane.borderless(); if is_borderless { pane.set_content_offset(Offset::frame(1)); } else { pane.set_content_offset(Offset::default()); } pane.set_borderless(!is_borderless); self.set_force_render(); return Ok(()); } if let Some(pane) = self.suppressed_panes.get_mut(pane_id) { let is_borderless = pane.1.borderless(); if is_borderless { pane.1.set_content_offset(Offset::frame(1)); } else { pane.1.set_content_offset(Offset::default()); } pane.1.set_borderless(!is_borderless); return Ok(()); } log::error!("Pane with id {:?} not found", pane_id); Ok(()) } pub fn set_pane_borderless(&mut self, pane_id: &PaneId, borderless: bool) -> Result<()> { // Try floating panes first if let Some(pane) = self.floating_panes.get_pane_mut(*pane_id) { if borderless { pane.set_content_offset(Offset::default()); // Borderless: no offset } else { pane.set_content_offset(Offset::frame(1)); // Bordered: 1-char offset } pane.set_borderless(borderless); self.set_force_render(); return Ok(()); } // Try tiled panes if let Some(pane) = self.tiled_panes.get_pane_mut(*pane_id) { if borderless { pane.set_content_offset(Offset::default()); } else { pane.set_content_offset(Offset::frame(1)); } pane.set_borderless(borderless); self.set_force_render(); return Ok(()); } // Try suppressed panes if let Some(pane) = self.suppressed_panes.get_mut(pane_id) { if borderless { pane.1.set_content_offset(Offset::default()); } else { pane.1.set_content_offset(Offset::frame(1)); } pane.1.set_borderless(borderless); return Ok(()); } log::error!("Pane with id {:?} not found", pane_id); Ok(()) } pub fn get_viewport(&self) -> Viewport { self.viewport.borrow().clone() } pub fn get_display_area(&self) -> Size { self.display_area.borrow().clone() } pub fn get_client_input_mode(&self, client_id: ClientId) -> Option { self.mode_info.borrow().get(&client_id).map(|m| m.mode) } #[allow(unused)] // this is used for tests pub fn query_mouse_hover_pane_id(&self) -> HashMap { self.mouse_hover_pane_id.clone() } fn new_scrollback_editor_pane(&self, pid: u32) -> TerminalPane { let next_terminal_position = self.get_next_terminal_position(); let mut new_pane = TerminalPane::new( pid, PaneGeom::default(), // the initial size will be set later self.style, next_terminal_position, String::new(), self.link_handler.clone(), self.character_cell_size.clone(), self.sixel_image_store.clone(), self.kitty_image_store.clone(), self.terminal_emulator_colors.clone(), self.terminal_emulator_color_codes.clone(), None, None, self.debug, self.arrow_fonts, self.styled_underlines, self.osc8_hyperlinks, self.explicitly_disable_kitty_keyboard_protocol, None, ); if let Some(supported) = self.kitty_host_support { new_pane.update_kitty_host_support(supported); } new_pane.update_name("EDITING SCROLLBACK"); // we do this here and not in the // constructor so it won't be overrided // by the editor new_pane } fn insert_scrollback_editor_replaced_pane( &mut self, replaced_pane: Box, terminal_pane_id: u32, ) { let is_scrollback_editor = true; self.suppressed_panes.insert( PaneId::Terminal(terminal_pane_id), (is_scrollback_editor, replaced_pane), ); } fn pane_is_stacked(&self, pane_id: PaneId) -> bool { self.get_pane_with_id(pane_id) .map(|p| p.position_and_size().stacked.is_some()) .unwrap_or(false) } // --- Pane-targeting CLI methods --- pub fn scroll_up_by_pane_id(&mut self, pane_id: PaneId) { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.scroll_up(1, fictitious_client_id); } } pub fn scroll_down_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.scroll_down(1, fictitious_client_id); if !pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = pane.pid() { self.process_pending_vte_events(raw_fd)?; } } } Ok(()) } pub fn scroll_to_top_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.clear_scroll(); if let Some(size) = pane.get_line_number() { pane.scroll_up(size, fictitious_client_id); } } Ok(()) } pub fn scroll_to_bottom_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.clear_scroll(); if !pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = pane.pid() { self.process_pending_vte_events(raw_fd)?; } } } Ok(()) } pub fn page_scroll_up_by_pane_id(&mut self, pane_id: PaneId) { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { let scroll_rows = pane.rows().max(1).saturating_sub(1); pane.scroll_up(scroll_rows, fictitious_client_id); } } pub fn page_scroll_down_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { let scroll_rows = pane.get_content_rows(); pane.scroll_down(scroll_rows, fictitious_client_id); if !pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = pane.pid() { self.process_pending_vte_events(raw_fd)?; } } } Ok(()) } pub fn half_page_scroll_up_by_pane_id(&mut self, pane_id: PaneId) { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { let scroll_rows = (pane.rows().max(1).saturating_sub(1)) / 2; pane.scroll_up(scroll_rows, fictitious_client_id); } } pub fn half_page_scroll_down_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { let fictitious_client_id = 1; if let Some(pane) = self.get_pane_with_id_mut(pane_id) { let scroll_rows = (pane.rows().max(1).saturating_sub(1)) / 2; pane.scroll_down(scroll_rows, fictitious_client_id); if !pane.is_scrolled() { if let PaneId::Terminal(raw_fd) = pane.pid() { self.process_pending_vte_events(raw_fd)?; } } } Ok(()) } pub fn resize_by_pane_id(&mut self, pane_id: PaneId, strategy: ResizeStrategy) { self.resize_pane_with_id(strategy, pane_id).non_fatal(); } pub fn move_pane_by_pane_id(&mut self, pane_id: PaneId, direction: Option) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } if !self.floating_panes.panes_contain(&pane_id) { self.dissolve_stack_lists_for_classic_mutation(); } match direction { Some(Direction::Left) => { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane_left(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); } else { self.tiled_panes.move_pane_left(pane_id); } }, Some(Direction::Right) => { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane_right(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); } else { self.tiled_panes.move_pane_right(pane_id); } }, Some(Direction::Up) => { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane_up(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); } else { self.tiled_panes.move_pane_up(pane_id); } }, Some(Direction::Down) => { if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane_down(pane_id); self.swap_layouts.set_is_floating_damaged(); self.set_force_render(); } else { self.tiled_panes.move_pane_down(pane_id); } }, None => { let search_backwards = false; if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane(search_backwards, pane_id); } else { self.tiled_panes.move_pane(search_backwards, pane_id); } }, } } pub fn move_pane_backwards_by_pane_id(&mut self, pane_id: PaneId) { if !self.has_selectable_panes() { return; } if self.tiled_panes.fullscreen_is_active() { return; } let search_backwards = true; if self.floating_panes.panes_contain(&pane_id) { self.floating_panes.move_pane(search_backwards, pane_id); } else { self.dissolve_stack_lists_for_classic_mutation(); self.tiled_panes.move_pane(search_backwards, pane_id); } } pub fn clear_screen_by_pane_id(&mut self, pane_id: PaneId) -> Result<()> { self.clear_screen_for_pane_id(pane_id); Ok(()) } pub fn toggle_fullscreen_by_pane_id(&mut self, pane_id: PaneId) { self.toggle_pane_fullscreen(pane_id); } pub fn toggle_no_ui_fullscreen_by_pane_id(&mut self, pane_id: PaneId) { self.toggle_pane_no_ui_fullscreen(pane_id); } pub fn close_pane_by_pane_id( &mut self, pane_id: PaneId, completion_tx: Option, ) -> Result<()> { self.close_pane(pane_id, false, None); self.senders .send_to_pty(PtyInstruction::ClosePane(pane_id, completion_tx))?; Ok(()) } pub fn rename_pane_by_pane_id(&mut self, pane_id: PaneId, name: Vec) -> Result<()> { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.rename(name); } Ok(()) } pub fn undo_rename_pane_by_pane_id(&mut self, pane_id: PaneId) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.load_pane_name(); } } pub fn toggle_pane_pinned_by_pane_id(&mut self, pane_id: PaneId) { if let Some(pane) = self.get_pane_with_id_mut(pane_id) { pane.toggle_pinned(); self.set_force_render(); } } } pub fn pane_info_for_pane( pane_id: &PaneId, pane: &Box, current_pane_group: &HashMap>, ) -> PaneInfo { let mut pane_info = PaneInfo::default(); pane_info.pane_x = pane.x(); pane_info.pane_content_x = pane.get_content_x(); pane_info.pane_y = pane.y(); pane_info.pane_content_y = pane.get_content_y(); pane_info.pane_rows = pane.rows(); pane_info.pane_content_rows = pane.get_content_rows(); pane_info.pane_columns = pane.cols(); pane_info.pane_content_columns = pane.get_content_columns(); pane_info.cursor_coordinates_in_pane = pane .cursor_coordinates(None) .and_then(|(x, y, is_visible)| if is_visible { Some((x, y)) } else { None }); pane_info.is_selectable = pane.selectable(); pane_info.title = pane.current_title(); pane_info.exited = pane.exited(); pane_info.exit_status = pane.exit_status(); pane_info.is_held = pane.is_held(); let index_in_pane_group: BTreeMap = current_pane_group .iter() .filter_map(|(client_id, pane_ids)| { if let Some(position) = pane_ids.iter().position(|p_id| p_id == &pane.pid()) { Some((*client_id, position)) } else { None } }) .collect(); pane_info.index_in_pane_group = index_in_pane_group; let (default_fg, default_bg) = pane.get_pane_default_colors(); pane_info.default_fg = default_fg; pane_info.default_bg = default_bg; match pane_id { PaneId::Terminal(terminal_id) => { pane_info.id = *terminal_id; pane_info.is_plugin = false; pane_info.terminal_command = pane.invoked_with().as_ref().and_then(|c| match c { Run::Command(run_command) => Some(run_command.to_string()), _ => None, }); }, PaneId::Plugin(plugin_id) => { pane_info.id = *plugin_id; pane_info.is_plugin = true; pane_info.plugin_url = pane.invoked_with().as_ref().and_then(|c| match c { Run::Plugin(run_plugin_or_alias) => Some(run_plugin_or_alias.location_string()), _ => None, }); }, } pane_info } #[cfg(test)] #[path = "./unit/tab_tests.rs"] mod tab_tests; #[cfg(test)] #[path = "./unit/tab_integration_tests.rs"] mod tab_integration_tests; #[cfg(test)] #[path = "./unit/layout_applier_tests.rs"] mod layout_applier_tests;