//! `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; pub mod floating_pane_grid; pub mod pane_resizer; pub mod tiled_pane_grid; use copy_command::CopyCommand; use zellij_utils::input::options::Clipboard; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde, zellij_tile}; use crate::ui::pane_boundaries_frame::FrameParams; use tiled_pane_grid::{split, TiledPaneGrid}; use self::clipboard::ClipboardProvider; use crate::{ os_input_output::ServerOsApi, output::{CharacterChunk, Output}, panes::FloatingPanes, panes::{LinkHandler, PaneId, PluginPane, TerminalPane}, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, thread_bus::ThreadSenders, ui::boundaries::Boundaries, ui::pane_contents_and_ui::PaneContentsAndUi, wasm_vm::PluginInstruction, ClientId, ServerInstruction, }; use serde::{Deserialize, Serialize}; use std::cell::RefCell; use std::cmp::Reverse; use std::os::unix::io::RawFd; use std::rc::Rc; use std::sync::mpsc::channel; use std::time::Instant; use std::{ collections::{BTreeMap, HashMap, HashSet}, str, }; use zellij_tile::data::{Event, InputMode, ModeInfo, Palette, PaletteColor}; use zellij_utils::{ input::{ command::TerminalAction, layout::{Direction, Layout, Run}, parse_keys, }, pane_size::{Offset, PaneGeom, Size, Viewport}, }; macro_rules! resize_pty { ($pane:expr, $os_input:expr) => { if let PaneId::Terminal(ref pid) = $pane.pid() { // FIXME: This `set_terminal_size_using_fd` call would be best in // `TerminalPane::reflow_lines` $os_input.set_terminal_size_using_fd( *pid, $pane.get_content_columns() as u16, $pane.get_content_rows() as u16, ); } }; } // 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; fn pane_content_offset(position_and_size: &PaneGeom, viewport: &Viewport) -> (usize, usize) { // (columns_offset, rows_offset) // if the pane is not on the bottom or right edge on the screen, we need to reserve one space // from its content to leave room for the boundary between it and the next pane (if it doesn't // draw its own frame) let columns_offset = if position_and_size.x + position_and_size.cols.as_usize() < viewport.cols { 1 } else { 0 }; let rows_offset = if position_and_size.y + position_and_size.rows.as_usize() < viewport.rows { 1 } else { 0 }; (columns_offset, rows_offset) } pub(crate) struct Tab { pub index: usize, pub position: usize, pub name: String, panes: BTreeMap>, floating_panes: FloatingPanes, pub panes_to_hide: HashSet, pub active_panes: HashMap, 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) fullscreen_is_active: bool, os_api: Box, pub senders: ThreadSenders, synchronize_is_active: bool, should_clear_display_before_rendering: bool, mode_info: HashMap, default_mode_info: ModeInfo, pub colors: Palette, connected_clients_in_app: Rc>>, // TODO: combine this and connected_clients connected_clients: HashSet, draw_pane_frames: bool, session_is_mirrored: bool, pending_vte_events: HashMap>, pub selecting_with_mouse: bool, // this is only pub for the tests TODO: remove this once we combine write_text_to_clipboard with render 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, } #[derive(Clone, Debug, Default, Serialize, Deserialize)] #[serde(crate = "self::serde")] pub(crate) struct TabData { pub position: usize, pub name: String, pub active: bool, pub mode_info: ModeInfo, pub colors: Palette, } // 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 get_geom_override(&mut self, pane_geom: PaneGeom); fn handle_pty_bytes(&mut self, bytes: VteBytes); fn cursor_coordinates(&self) -> Option<(usize, usize)>; fn adjust_input_to_terminal(&self, input_bytes: Vec) -> Vec; 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 render( &mut self, client_id: Option, ) -> Option<(Vec, Option)>; // TODO: better fn render_frame( &mut self, client_id: ClientId, frame_params: FrameParams, input_mode: InputMode, ) -> Option<(Vec, 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 scroll_up(&mut self, count: usize, client_id: ClientId); fn scroll_down(&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 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) {} fn get_selected_text(&self) -> Option { None } fn right_boundary_x_coords(&self) -> usize { self.x() + self.cols() } fn bottom_boundary_y_coords(&self) -> usize { self.y() + self.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 render_full_viewport(&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_on_screen: &Position) -> bool { // TODO: handle cases where we have no frame position_on_screen.line() == self.y() as isize || position_on_screen.line() == (self.y() as isize + self.rows() as isize).saturating_sub(1) || position_on_screen.column() == self.x() || position_on_screen.column() == (self.x() + self.cols()).saturating_sub(1) } fn set_borderless(&mut self, borderless: bool); fn borderless(&self) -> bool; fn handle_right_click(&mut self, _to: &Position, _client_id: ClientId) {} } impl Tab { // FIXME: Still too many arguments for clippy to be happy... #[allow(clippy::too_many_arguments)] pub fn new( index: usize, position: usize, name: String, display_area: Size, os_api: Box, senders: ThreadSenders, max_panes: Option, mode_info: ModeInfo, colors: Palette, draw_pane_frames: bool, connected_clients_in_app: Rc>>, session_is_mirrored: bool, client_id: ClientId, copy_command: Option, copy_clipboard: Clipboard, ) -> Self { let panes = BTreeMap::new(); let name = if name.is_empty() { format!("Tab #{}", index + 1) } else { name }; let mut connected_clients = HashSet::new(); connected_clients.insert(client_id); let viewport: Viewport = display_area.into(); let viewport = Rc::new(RefCell::new(viewport)); let display_area = Rc::new(RefCell::new(display_area)); let floating_panes = FloatingPanes::new(display_area.clone(), viewport.clone()); let clipboard_provider = match copy_command { Some(command) => ClipboardProvider::Command(CopyCommand::new(command)), None => ClipboardProvider::Osc52(copy_clipboard), }; Tab { index, position, panes, floating_panes, name, max_panes, panes_to_hide: HashSet::new(), active_panes: HashMap::new(), viewport, display_area, fullscreen_is_active: false, synchronize_is_active: false, os_api, senders, should_clear_display_before_rendering: false, mode_info: HashMap::new(), default_mode_info: mode_info, colors, draw_pane_frames, session_is_mirrored, pending_vte_events: HashMap::new(), connected_clients_in_app, connected_clients, selecting_with_mouse: false, link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, } } pub fn apply_layout( &mut self, layout: Layout, new_pids: Vec, tab_index: usize, client_id: ClientId, ) { // TODO: this should be an attribute on Screen instead of full_screen_ws let (viewport_cols, viewport_rows) = { let viewport = self.viewport.borrow(); (viewport.cols, viewport.rows) }; let mut free_space = PaneGeom::default(); free_space.cols.set_inner(viewport_cols); free_space.rows.set_inner(viewport_rows); self.panes_to_hide.clear(); let positions_in_layout = layout.position_panes_in_space(&free_space); let mut positions_and_size = positions_in_layout.iter(); for (pane_kind, terminal_pane) in &mut self.panes { // for now the layout only supports terminal panes if let PaneId::Terminal(pid) = pane_kind { match positions_and_size.next() { Some(&(_, position_and_size)) => { terminal_pane.reset_size_and_position_override(); terminal_pane.set_geom(position_and_size); } None => { // we filled the entire layout, no room for this pane // TODO: handle active terminal self.panes_to_hide.insert(PaneId::Terminal(*pid)); } } } } let mut new_pids = new_pids.iter(); let mut focus_pane_id: Option = None; let mut set_focus_pane_id = |layout: &Layout, pane_id: PaneId| { if layout.focus.unwrap_or(false) && focus_pane_id.is_none() { focus_pane_id = Some(pane_id); } }; for (layout, position_and_size) in positions_and_size { // A plugin pane if let Some(Run::Plugin(run)) = layout.run.clone() { let (pid_tx, pid_rx) = channel(); let pane_title = run.location.to_string(); self.senders .send_to_plugin(PluginInstruction::Load(pid_tx, run, tab_index, client_id)) .unwrap(); let pid = pid_rx.recv().unwrap(); let mut new_plugin = PluginPane::new( pid, *position_and_size, self.senders.to_plugin.as_ref().unwrap().clone(), pane_title, layout.pane_name.clone().unwrap_or_default(), ); new_plugin.set_borderless(layout.borderless); self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); set_focus_pane_id(layout, PaneId::Plugin(pid)); } else { // there are still panes left to fill, use the pids we received in this method let pid = new_pids.next().unwrap(); // if this crashes it means we got less pids than there are panes in this layout let next_terminal_position = self.get_next_terminal_position(); let mut new_pane = TerminalPane::new( *pid, *position_and_size, self.colors, next_terminal_position, layout.pane_name.clone().unwrap_or_default(), self.link_handler.clone(), ); new_pane.set_borderless(layout.borderless); self.panes .insert(PaneId::Terminal(*pid), Box::new(new_pane)); set_focus_pane_id(layout, PaneId::Terminal(*pid)); } } for unused_pid in new_pids { // this is a bit of a hack and happens because we don't have any central location that // can query the screen as to how many panes it needs to create a layout // fixing this will require a bit of an architecture change self.senders .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) .unwrap(); } // FIXME: This is another hack to crop the viewport to fixed-size panes. Once you can have // non-fixed panes that are part of the viewport, get rid of this! let display_area = { let display_area = self.display_area.borrow(); *display_area }; self.resize_whole_tab(display_area); let boundary_geom: Vec<_> = self .panes .values() .filter_map(|p| { let geom = p.position_and_size(); if geom.cols.is_fixed() || geom.rows.is_fixed() { Some(geom.into()) } else { None } }) .collect(); for geom in boundary_geom { self.offset_viewport(&geom) } self.set_pane_frames(self.draw_pane_frames); let mut active_pane = |pane_id: PaneId| { let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, pane_id); } }; if let Some(pane_id) = focus_pane_id { self.focus_pane_id = Some(pane_id); active_pane(pane_id); } else { // This is the end of the nasty viewport hack... let next_selectable_pane_id = self .panes .iter() .filter(|(_id, pane)| pane.selectable()) .map(|(id, _)| id.to_owned()) .next(); match next_selectable_pane_id { Some(active_pane_id) => { active_pane(active_pane_id); } None => { // this is very likely a configuration error (layout with no selectable panes) self.active_panes.clear(); } } } } pub fn update_input_modes(&mut self) { // this updates all plugins with the client's input mode for client_id in &self.connected_clients { let mode_info = self .mode_info .get(client_id) .unwrap_or(&self.default_mode_info); self.senders .send_to_plugin(PluginInstruction::Update( None, Some(*client_id), Event::ModeUpdate(mode_info.clone()), )) .unwrap(); } } pub fn add_client(&mut self, client_id: ClientId, mode_info: Option) { let first_connected_client = self.connected_clients.iter().next(); match first_connected_client { Some(first_client_id) => { let first_active_pane_id = *self.active_panes.get(first_client_id).unwrap(); if self.floating_panes.panes_are_visible() { if let Some(first_active_floating_pane_id) = self.floating_panes.first_active_floating_pane_id() { self.floating_panes .focus_pane(first_active_floating_pane_id, client_id); } } self.connected_clients.insert(client_id); self.active_panes.insert(client_id, first_active_pane_id); self.mode_info.insert( client_id, mode_info.unwrap_or_else(|| self.default_mode_info.clone()), ); } None => { let mut pane_ids: Vec = self.panes.keys().copied().collect(); if pane_ids.is_empty() { // no panes here, bye bye return; } self.active_panes.insert( client_id, self.focus_pane_id.unwrap_or_else(|| { pane_ids.sort(); // TODO: make this predictable pane_ids.retain(|p| !self.panes_to_hide.contains(p)); *pane_ids.get(0).unwrap() }), ); self.connected_clients.insert(client_id); self.mode_info.insert( client_id, mode_info.unwrap_or_else(|| self.default_mode_info.clone()), ); } } // TODO: we might be able to avoid this, we do this so that newly connected clients will // necessarily get a full render self.set_force_render(); self.update_input_modes(); } pub fn change_mode_info(&mut self, mode_info: ModeInfo, client_id: ClientId) { self.mode_info.insert(client_id, mode_info); } pub fn add_multiple_clients(&mut self, client_ids_to_mode_infos: Vec<(ClientId, ModeInfo)>) { for (client_id, client_mode_info) in client_ids_to_mode_infos { self.add_client(client_id, None); self.mode_info.insert(client_id, client_mode_info); } } pub fn remove_client(&mut self, client_id: ClientId) { self.focus_pane_id = None; self.active_panes.remove(&client_id); self.connected_clients.remove(&client_id); 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.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 .remove(&client_id) .unwrap_or_else(|| self.default_mode_info.clone()); self.connected_clients.remove(&client_id); (client_id, client_mode_info) } pub fn has_no_connected_clients(&self) -> bool { self.connected_clients.is_empty() } pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { if let Some(focused_floating_pane_id) = self.floating_panes.active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let terminal_id_and_split_direction = pane_grid.find_room_for_new_pane(); if let Some((terminal_id_to_split, split_direction)) = terminal_id_and_split_direction { // this unwrap is safe because floating panes should not be visible if there are no floating panes let mut floating_pane_to_embed = self.close_pane(focused_floating_pane_id).unwrap(); let pane_to_split = self.panes.get_mut(&terminal_id_to_split).unwrap(); let size_of_both_panes = pane_to_split.position_and_size(); if let Some((first_geom, second_geom)) = split(split_direction, &size_of_both_panes) { pane_to_split.set_geom(first_geom); floating_pane_to_embed.set_geom(second_geom); self.panes .insert(focused_floating_pane_id, floating_pane_to_embed); // ¯\_(ツ)_/¯ let relayout_direction = match split_direction { Direction::Vertical => Direction::Horizontal, Direction::Horizontal => Direction::Vertical, }; self.relayout_tab(relayout_direction); } if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes .insert(client_id, focused_floating_pane_id); } } else { self.active_panes .insert(client_id, focused_floating_pane_id); } self.floating_panes.toggle_show_panes(false); } } } else if let Some(focused_pane_id) = self.active_panes.get(&client_id).copied() { if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { if self.get_selectable_panes().count() <= 1 { // don't close the only pane on screen... return; } if let Some(mut embedded_pane_to_float) = self.close_pane(focused_pane_id) { embedded_pane_to_float.set_geom(new_pane_geom); resize_pty!(embedded_pane_to_float, self.os_api); embedded_pane_to_float.set_active_at(Instant::now()); self.floating_panes .add_pane(focused_pane_id, embedded_pane_to_float); self.floating_panes.toggle_show_panes(true); // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.floating_panes.focus_pane(focused_pane_id, client_id); } self.floating_panes.set_force_render(); } } } } pub fn toggle_floating_panes( &mut self, client_id: ClientId, default_shell: Option, ) { if self.floating_panes.panes_are_visible() { self.floating_panes.toggle_show_panes(false); self.set_force_render(); } else { self.floating_panes.toggle_show_panes(true); match self.floating_panes.first_floating_pane_id() { Some(first_floating_pane_id) => { if !self.floating_panes.active_panes_contain(&client_id) { self.floating_panes .focus_pane(first_floating_pane_id, client_id); } } None => { // there aren't any floating panes, we need to open a new one // // ************************************************************************************************ // BEWARE - THIS IS NOT ATOMIC - this sends an instruction to the pty thread to open a new terminal // the pty thread will do its thing and eventually come back to the new_pane // method on this tab which will open a new floating pane because we just // toggled their visibility above us. // If the pty thread takes too long, weird things can happen... // ************************************************************************************************ // let instruction = PtyInstruction::SpawnTerminal( default_shell, ClientOrTabIndex::ClientId(client_id), ); self.senders.send_to_pty(instruction).unwrap(); } } self.floating_panes.set_force_render(); } self.set_force_render(); } pub fn new_pane(&mut self, pid: PaneId, client_id: Option) { self.close_down_to_max_terminals(); if self.floating_panes.panes_are_visible() { if let Some(new_pane_geom) = self.floating_panes.find_room_for_new_pane() { let next_terminal_position = self.get_next_terminal_position(); if let PaneId::Terminal(term_pid) = pid { let mut new_pane = TerminalPane::new( term_pid, new_pane_geom, self.colors, next_terminal_position, String::new(), self.link_handler.clone(), ); new_pane.set_content_offset(Offset::frame(1)); // floating panes always have a frame resize_pty!(new_pane, self.os_api); self.floating_panes.add_pane(pid, Box::new(new_pane)); // move all clients to new floating pane let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.floating_panes.focus_pane(pid, client_id); } } } } else { if self.fullscreen_is_active { self.unset_fullscreen(); } let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let terminal_id_and_split_direction = pane_grid.find_room_for_new_pane(); if let Some((terminal_id_to_split, split_direction)) = terminal_id_and_split_direction { let next_terminal_position = self.get_next_terminal_position(); let terminal_to_split = self.panes.get_mut(&terminal_id_to_split).unwrap(); let terminal_ws = terminal_to_split.position_and_size(); if let PaneId::Terminal(term_pid) = pid { if let Some((first_winsize, second_winsize)) = split(split_direction, &terminal_ws) { let new_terminal = TerminalPane::new( term_pid, second_winsize, self.colors, next_terminal_position, String::new(), self.link_handler.clone(), ); terminal_to_split.set_geom(first_winsize); self.panes.insert(pid, Box::new(new_terminal)); // ¯\_(ツ)_/¯ let relayout_direction = match split_direction { Direction::Vertical => Direction::Horizontal, Direction::Horizontal => Direction::Vertical, }; self.relayout_tab(relayout_direction); } } if let Some(client_id) = client_id { if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, pid); } } else { self.active_panes.insert(client_id, pid); } } } } } pub fn horizontal_split(&mut self, pid: PaneId, client_id: ClientId) { self.close_down_to_max_terminals(); if self.fullscreen_is_active { self.toggle_active_pane_fullscreen(client_id); } if let PaneId::Terminal(term_pid) = pid { let next_terminal_position = self.get_next_terminal_position(); let active_pane_id = &self.get_active_pane_id(client_id).unwrap(); let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.rows() < MIN_TERMINAL_HEIGHT * 2 { self.senders .send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty .unwrap(); return; } let terminal_ws = active_pane.position_and_size(); if let Some((top_winsize, bottom_winsize)) = split(Direction::Horizontal, &terminal_ws) { let new_terminal = TerminalPane::new( term_pid, bottom_winsize, self.colors, next_terminal_position, String::new(), self.link_handler.clone(), ); active_pane.set_geom(top_winsize); self.panes.insert(pid, Box::new(new_terminal)); if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, pid); } } else { self.active_panes.insert(client_id, pid); } self.relayout_tab(Direction::Vertical); } } } pub fn vertical_split(&mut self, pid: PaneId, client_id: ClientId) { self.close_down_to_max_terminals(); if self.fullscreen_is_active { self.toggle_active_pane_fullscreen(client_id); } if let PaneId::Terminal(term_pid) = pid { // TODO: check minimum size of active terminal let next_terminal_position = self.get_next_terminal_position(); let active_pane_id = &self.get_active_pane_id(client_id).unwrap(); let active_pane = self.panes.get_mut(active_pane_id).unwrap(); if active_pane.cols() < MIN_TERMINAL_WIDTH * 2 { self.senders .send_to_pty(PtyInstruction::ClosePane(pid)) // we can't open this pane, close the pty .unwrap(); return; } let terminal_ws = active_pane.position_and_size(); if let Some((left_winsize, right_winsize)) = split(Direction::Vertical, &terminal_ws) { let new_terminal = TerminalPane::new( term_pid, right_winsize, self.colors, next_terminal_position, String::new(), self.link_handler.clone(), ); active_pane.set_geom(left_winsize); self.panes.insert(pid, Box::new(new_terminal)); } if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, pid); } } else { self.active_panes.insert(client_id, pid); } self.relayout_tab(Direction::Horizontal); } } pub fn has_active_panes(&self) -> bool { // a tab without active panes is a dead tab and should close // a pane can be active even if there are no connected clients, // we remember that pane for one the client focuses the tab next !self.active_panes.is_empty() } pub fn get_active_pane(&self, client_id: ClientId) -> Option<&dyn Pane> { self.get_active_pane_id(client_id) .and_then(|ap| self.panes.get(&ap).map(Box::as_ref)) } pub fn get_active_pane_mut(&mut self, client_id: ClientId) -> Option<&mut Box> { self.get_active_pane_id(client_id) .and_then(|ap| self.panes.get_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) } } fn get_active_pane_id(&self, client_id: ClientId) -> Option { // TODO: why do we need this? self.active_panes.get(&client_id).copied() } fn get_active_terminal_id(&self, client_id: ClientId) -> Option { if let Some(PaneId::Terminal(pid)) = self.active_panes.get(&client_id).copied() { Some(pid) } else { None } } pub fn has_terminal_pid(&self, pid: RawFd) -> bool { self.panes.contains_key(&PaneId::Terminal(pid)) || self.floating_panes.panes_contain(&PaneId::Terminal(pid)) } pub fn handle_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { if let Some(terminal_output) = self .panes .get_mut(&PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_mut(&PaneId::Terminal(pid))) { // 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); } } return; } } self.process_pty_bytes(pid, bytes); } pub fn process_pending_vte_events(&mut self, pid: RawFd) { 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); } } } fn process_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { if let Some(terminal_output) = self .panes .get_mut(&PaneId::Terminal(pid)) .or_else(|| self.floating_panes.get_mut(&PaneId::Terminal(pid))) { terminal_output.handle_pty_bytes(bytes); let messages_to_pty = terminal_output.drain_messages_to_pty(); for message in messages_to_pty { self.write_to_pane_id(message, PaneId::Terminal(pid)); } } } pub fn write_to_terminals_on_current_tab(&mut self, input_bytes: Vec) { let pane_ids = self.get_static_and_floating_pane_ids(); pane_ids.iter().for_each(|&pane_id| { self.write_to_pane_id(input_bytes.clone(), pane_id); }); } pub fn write_to_active_terminal(&mut self, input_bytes: Vec, client_id: ClientId) { let pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .active_pane_id(client_id) .unwrap_or_else(|| *self.active_panes.get(&client_id).unwrap()) } else { *self.active_panes.get(&client_id).unwrap() }; self.write_to_pane_id(input_bytes, pane_id); } pub fn write_to_pane_id(&mut self, input_bytes: Vec, pane_id: PaneId) { match pane_id { PaneId::Terminal(active_terminal_id) => { let active_terminal = self .floating_panes .get(&pane_id) .unwrap_or_else(|| self.panes.get(&pane_id).unwrap()); let adjusted_input = active_terminal.adjust_input_to_terminal(input_bytes); self.os_api .write_to_tty_stdin(active_terminal_id, &adjusted_input) .expect("failed to write to terminal"); self.os_api .tcdrain(active_terminal_id) .expect("failed to drain terminal"); } PaneId::Plugin(pid) => { for key in parse_keys(&input_bytes) { self.senders .send_to_plugin(PluginInstruction::Update(Some(pid), None, Event::Key(key))) .unwrap() } } } } pub fn get_active_terminal_cursor_position( &self, client_id: ClientId, ) -> Option<(usize, usize)> { // (x, y) let active_pane_id = if self.floating_panes.panes_are_visible() { self.floating_panes .active_pane_id(client_id) .or_else(|| self.active_panes.get(&client_id).copied())? } else { self.active_panes.get(&client_id).copied()? }; let active_terminal = &self .floating_panes .get(&active_pane_id) .or_else(|| self.panes.get(&active_pane_id))?; active_terminal .cursor_coordinates() .map(|(x_in_terminal, y_in_terminal)| { let x = active_terminal.x() + x_in_terminal; let y = active_terminal.y() + y_in_terminal; (x, y) }) } pub fn unset_fullscreen(&mut self) { if self.fullscreen_is_active { let first_client_id = self.connected_clients.iter().next().unwrap(); // this is a temporary hack until we fix the ui for multiple clients let active_pane_id = self.active_panes.get(first_client_id).unwrap(); for terminal_id in &self.panes_to_hide { let pane = self.panes.get_mut(terminal_id).unwrap(); pane.set_should_render(true); pane.set_should_render_boundaries(true); } let viewport_pane_ids: Vec<_> = self .get_embedded_pane_ids() .into_iter() .filter(|id| !self.is_inside_viewport(id)) .collect(); for pid in viewport_pane_ids { let viewport_pane = self.panes.get_mut(&pid).unwrap(); viewport_pane.reset_size_and_position_override(); } self.panes_to_hide.clear(); let active_terminal = self.panes.get_mut(active_pane_id).unwrap(); active_terminal.reset_size_and_position_override(); self.set_force_render(); let display_area = *self.display_area.borrow(); self.resize_whole_tab(display_area); self.toggle_fullscreen_is_active(); } } pub fn toggle_active_pane_fullscreen(&mut self, client_id: ClientId) { if let Some(active_pane_id) = self.get_active_pane_id(client_id) { if self.fullscreen_is_active { self.unset_fullscreen(); } else { let panes = self.get_panes(); let pane_ids_to_hide = panes.filter_map(|(&id, _pane)| { if id != active_pane_id && self.is_inside_viewport(&id) { Some(id) } else { None } }); self.panes_to_hide = pane_ids_to_hide.collect(); if self.panes_to_hide.is_empty() { // nothing to do, pane is already as fullscreen as it can be, let's bail return; } else { // For all of the panes outside of the viewport staying on the fullscreen // screen, switch them to using override positions as well so that the resize // system doesn't get confused by viewport and old panes that no longer line up let viewport_pane_ids: Vec<_> = self .get_embedded_pane_ids() .into_iter() .filter(|id| !self.is_inside_viewport(id)) .collect(); for pid in viewport_pane_ids { let viewport_pane = self.panes.get_mut(&pid).unwrap(); viewport_pane.get_geom_override(viewport_pane.position_and_size()); } let active_terminal = self.panes.get_mut(&active_pane_id).unwrap(); let viewport = { *self.viewport.borrow() }; let full_screen_geom = PaneGeom { x: viewport.x, y: viewport.y, ..Default::default() }; active_terminal.get_geom_override(full_screen_geom); } let active_panes: Vec = self.active_panes.keys().copied().collect(); for client_id in active_panes { self.active_panes.insert(client_id, active_pane_id); } self.set_force_render(); let display_area = *self.display_area.borrow(); self.resize_whole_tab(display_area); self.toggle_fullscreen_is_active(); } } } pub fn is_fullscreen_active(&self) -> bool { self.fullscreen_is_active } pub fn are_floating_panes_visible(&self) -> bool { self.floating_panes.panes_are_visible() } pub fn toggle_fullscreen_is_active(&mut self) { self.fullscreen_is_active = !self.fullscreen_is_active; } pub fn set_force_render(&mut self) { for pane in self.panes.values_mut() { pane.set_should_render(true); pane.set_should_render_boundaries(true); pane.render_full_viewport(); } self.floating_panes.set_force_render(); } 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_id) = self.get_active_pane_id(client_id) { self.panes .get_mut(&active_pane_id) .unwrap() .set_should_render(true) } } pub fn set_pane_frames(&mut self, draw_pane_frames: bool) { self.draw_pane_frames = draw_pane_frames; self.should_clear_display_before_rendering = true; let viewport = *self.viewport.borrow(); for pane in self.panes.values_mut() { if !pane.borderless() { pane.set_frame(draw_pane_frames); } #[allow(clippy::if_same_then_else)] if draw_pane_frames & !pane.borderless() { // there's definitely a frame around this pane, offset its contents pane.set_content_offset(Offset::frame(1)); } else if draw_pane_frames && pane.borderless() { // there's no frame around this pane, and the tab isn't handling the boundaries // between panes (they each draw their own frames as they please) // this one doesn't - do not offset its content pane.set_content_offset(Offset::default()); } else if !is_inside_viewport(&viewport, pane) { // this pane is outside the viewport and has no border - it should not have an offset pane.set_content_offset(Offset::default()); } else { // no draw_pane_frames and this pane should have a separation to other panes // according to its position in the viewport (eg. no separation if its at the // viewport bottom) - offset its content accordingly let position_and_size = pane.current_geom(); let (pane_columns_offset, pane_rows_offset) = pane_content_offset(&position_and_size, &viewport); pane.set_content_offset(Offset::shift(pane_rows_offset, pane_columns_offset)); } resize_pty!(pane, self.os_api); } self.floating_panes.set_pane_frames(&mut self.os_api); } fn update_active_panes_in_pty_thread(&self) { // this is a bit hacky and we should ideally not keep this state in two different places at // some point for &connected_client in &self.connected_clients { self.senders .send_to_pty(PtyInstruction::UpdateActivePane( self.active_panes.get(&connected_client).copied(), connected_client, )) .unwrap(); } } pub fn render(&mut self, output: &mut Output, overlay: Option) { if self.connected_clients.is_empty() || self.active_panes.is_empty() { return; } self.update_active_panes_in_pty_thread(); let floating_panes_stack = if self.floating_panes.panes_are_visible() { Some(self.floating_panes.stack()) } else { None }; output.add_clients( &self.connected_clients, self.link_handler.clone(), floating_panes_stack, ); let mut client_id_to_boundaries: HashMap = HashMap::new(); self.hide_cursor_and_clear_display_as_needed(output); let active_non_floating_panes = self.active_non_floating_panes(); // render panes and their frames for (kind, pane) in self.panes.iter_mut() { if !self.panes_to_hide.contains(&pane.pid()) { let mut active_panes = if self.floating_panes.panes_are_visible() { active_non_floating_panes.clone() } else { self.active_panes.clone() }; let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 }; active_panes.retain(|c_id, _| self.connected_clients.contains(c_id)); let mut pane_contents_and_ui = PaneContentsAndUi::new( pane, output, self.colors, &active_panes, multiple_users_exist_in_session, None, ); for &client_id in &self.connected_clients { let client_mode = self .mode_info .get(&client_id) .unwrap_or(&self.default_mode_info) .mode; if let PaneId::Plugin(..) = kind { pane_contents_and_ui.render_pane_contents_for_client(client_id); } if self.draw_pane_frames { pane_contents_and_ui.render_pane_frame( client_id, client_mode, self.session_is_mirrored, ); } else { let boundaries = client_id_to_boundaries .entry(client_id) .or_insert_with(|| Boundaries::new(*self.viewport.borrow())); pane_contents_and_ui.render_pane_boundaries( client_id, client_mode, boundaries, self.session_is_mirrored, ); } pane_contents_and_ui.render_terminal_title_if_needed(client_id, client_mode); // this is done for panes that don't have their own cursor (eg. panes of // another user) pane_contents_and_ui.render_fake_cursor_if_needed(client_id); } if let PaneId::Terminal(..) = kind { pane_contents_and_ui.render_pane_contents_to_multiple_clients( self.connected_clients.iter().copied(), ); } } } if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { self.floating_panes.render( &self.connected_clients_in_app, &self.connected_clients, &self.mode_info, &self.default_mode_info, self.session_is_mirrored, output, self.colors, ); } // render boundaries if needed for (client_id, boundaries) in &mut client_id_to_boundaries { // TODO: add some conditional rendering here so this isn't rendered for every character output.add_character_chunks_to_client(*client_id, boundaries.render(), None); } // FIXME: Once clients can be distinguished if let Some(overlay_vte) = &overlay { output.add_post_vte_instruction_to_multiple_clients( self.connected_clients.iter().copied(), overlay_vte, ); } self.render_cursor(output); } fn active_non_floating_panes(&self) -> HashMap { let mut active_non_floating_panes = self.active_panes.clone(); active_non_floating_panes.retain(|c_id, _| !self.floating_panes.active_panes_contain(c_id)); active_non_floating_panes } fn hide_cursor_and_clear_display_as_needed(&mut self, output: &mut Output) { let hide_cursor = "\u{1b}[?25l"; output.add_pre_vte_instruction_to_multiple_clients( self.connected_clients.iter().copied(), hide_cursor, ); if self.should_clear_display_before_rendering { let clear_display = "\u{1b}[2J"; output.add_pre_vte_instruction_to_multiple_clients( self.connected_clients.iter().copied(), clear_display, ); self.should_clear_display_before_rendering = false; } } fn render_cursor(&self, output: &mut Output) { for &client_id in &self.connected_clients { match self.get_active_terminal_cursor_position(client_id) { Some((cursor_position_x, cursor_position_y)) => { let show_cursor = "\u{1b}[?25h"; let change_cursor_shape = self.get_active_pane(client_id).unwrap().cursor_shape_csi(); let goto_cursor_position = &format!( "\u{1b}[{};{}H\u{1b}[m{}", cursor_position_y + 1, cursor_position_x + 1, change_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); } None => { let hide_cursor = "\u{1b}[?25l"; output.add_post_vte_instruction_to_client(client_id, hide_cursor); } } } } fn get_panes(&self) -> impl Iterator)> { self.panes.iter() } fn get_selectable_panes(&self) -> impl Iterator)> { self.panes.iter().filter(|(_, p)| p.selectable()) } fn get_next_terminal_position(&self) -> usize { self.panes .iter() .filter(|(k, _)| match k { PaneId::Plugin(_) => false, PaneId::Terminal(_) => true, }) .count() + 1 } fn has_selectable_panes(&self) -> bool { let mut all_terminals = self.get_selectable_panes(); all_terminals.next().is_some() } fn next_active_pane(&self, panes: &[PaneId]) -> Option { let mut panes: Vec<_> = panes .iter() .map(|p_id| self.panes.get(p_id).unwrap()) .collect(); panes.sort_by_key(|b| Reverse(b.active_at())); panes.iter().find(|pane| pane.selectable()).map(|p| p.pid()) } pub fn relayout_tab(&mut self, direction: Direction) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let result = match direction { Direction::Horizontal => { pane_grid.layout(direction, (*self.display_area.borrow()).cols) } Direction::Vertical => pane_grid.layout(direction, (*self.display_area.borrow()).rows), }; if let Err(e) = &result { log::error!("{:?} relayout of the tab failed: {}", direction, e); } self.set_pane_frames(self.draw_pane_frames); } pub fn resize_whole_tab(&mut self, new_screen_size: Size) { self.floating_panes.resize(new_screen_size); { // this is blocked out to appease the borrow checker let mut display_area = self.display_area.borrow_mut(); let mut viewport = self.viewport.borrow_mut(); let panes = self .panes .iter_mut() .filter(|(pid, _)| !self.panes_to_hide.contains(pid)); let Size { rows, cols } = new_screen_size; let mut pane_grid = TiledPaneGrid::new(panes, *display_area, *viewport); if pane_grid.layout(Direction::Horizontal, cols).is_ok() { let column_difference = cols as isize - display_area.cols as isize; // FIXME: Should the viewport be an Offset? viewport.cols = (viewport.cols as isize + column_difference) as usize; display_area.cols = cols; } else { log::error!("Failed to horizontally resize the tab!!!"); } if pane_grid.layout(Direction::Vertical, rows).is_ok() { let row_difference = rows as isize - display_area.rows as isize; viewport.rows = (viewport.rows as isize + row_difference) as usize; display_area.rows = rows; } else { log::error!("Failed to vertically resize the tab!!!"); } self.should_clear_display_before_rendering = true; } self.set_pane_frames(self.draw_pane_frames); } pub fn resize_left(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_left(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_pane_left(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } pub fn resize_right(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_right(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_pane_right(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } pub fn resize_down(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_down(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_pane_down(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } pub fn resize_up(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_up(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_pane_up(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } pub fn resize_increase(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_increase(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_increase(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } pub fn resize_decrease(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { let successfully_resized = self .floating_panes .resize_active_pane_decrease(client_id, &mut self.os_api); if successfully_resized { self.set_force_render(); // we force render here to make sure the panes under the floating pane render and don't leave "garbage" incase of a decrease return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); pane_grid.resize_decrease(&active_pane_id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } } } fn set_pane_active_at(&mut self, pane_id: PaneId) { if let Some(pane) = self.panes.get_mut(&pane_id) { pane.set_active_at(Instant::now()); } } pub fn move_focus(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let current_active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_active_pane_id = pane_grid.next_selectable_pane_id(¤t_active_pane_id); let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, next_active_pane_id); } self.set_pane_active_at(next_active_pane_id); } pub fn focus_next_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_active_pane_id = pane_grid.next_selectable_pane_id(&active_pane_id); let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, next_active_pane_id); } self.set_pane_active_at(next_active_pane_id); } pub fn focus_previous_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_active_pane_id = pane_grid.previous_selectable_pane_id(&active_pane_id); let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, next_active_pane_id); } self.set_pane_active_at(next_active_pane_id); } // returns a boolean that indicates whether the focus moved pub fn move_focus_left(&mut self, client_id: ClientId) -> bool { if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus_left(client_id, &self.connected_clients) } else { if !self.has_selectable_panes() { return false; } if self.fullscreen_is_active { return false; } let active_pane_id = self.get_active_pane_id(client_id); let updated_active_pane = if let Some(active_pane_id) = active_pane_id { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_to_the_left(&active_pane_id); match next_index { Some(p) => { // render previously active pane so that its frame does not remain actively // colored let previously_active_pane = self .panes .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) next_active_pane.render_full_viewport(); if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, p); } } else { self.active_panes.insert(client_id, p); } self.set_pane_active_at(p); return true; } None => Some(active_pane_id), } } else { active_pane_id }; match updated_active_pane { Some(updated_active_pane) => { let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, updated_active_pane); } self.set_pane_active_at(updated_active_pane); } None => { // TODO: can this happen? self.active_panes.clear(); } } false } } pub fn move_focus_down(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus_down(client_id, &self.connected_clients); } else { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_pane_id = self.get_active_pane_id(client_id); let updated_active_pane = if let Some(active_pane_id) = active_pane_id { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_below(&active_pane_id); match next_index { Some(p) => { // render previously active pane so that its frame does not remain actively // colored let previously_active_pane = self .panes .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) next_active_pane.render_full_viewport(); Some(p) } None => Some(active_pane_id), } } else { active_pane_id }; match updated_active_pane { Some(updated_active_pane) => { if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, updated_active_pane); } self.set_pane_active_at(updated_active_pane); } else { self.active_panes.insert(client_id, updated_active_pane); self.set_pane_active_at(updated_active_pane); } } None => { // TODO: can this happen? self.active_panes.clear(); } } } } pub fn move_focus_up(&mut self, client_id: ClientId) { if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus_up(client_id, &self.connected_clients); } else { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_pane_id = self.get_active_pane_id(client_id); let updated_active_pane = if let Some(active_pane_id) = active_pane_id { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_above(&active_pane_id); match next_index { Some(p) => { // render previously active pane so that its frame does not remain actively // colored let previously_active_pane = self .panes .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) next_active_pane.render_full_viewport(); Some(p) } None => Some(active_pane_id), } } else { active_pane_id }; match updated_active_pane { Some(updated_active_pane) => { if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, updated_active_pane); } self.set_pane_active_at(updated_active_pane); } else { self.active_panes.insert(client_id, updated_active_pane); self.set_pane_active_at(updated_active_pane); } } None => { // TODO: can this happen? self.active_panes.clear(); } } } } // returns a boolean that indicates whether the focus moved pub fn move_focus_right(&mut self, client_id: ClientId) -> bool { if self.floating_panes.panes_are_visible() { self.floating_panes .move_focus_right(client_id, &self.connected_clients) } else { if !self.has_selectable_panes() { return false; } if self.fullscreen_is_active { return false; } let active_pane_id = self.get_active_pane_id(client_id); let updated_active_pane = if let Some(active_pane_id) = active_pane_id { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_to_the_right(&active_pane_id); match next_index { Some(p) => { // render previously active pane so that its frame does not remain actively // colored let previously_active_pane = self .panes .get_mut(self.active_panes.get(&client_id).unwrap()) .unwrap(); previously_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) previously_active_pane.render_full_viewport(); let next_active_pane = self.panes.get_mut(&p).unwrap(); next_active_pane.set_should_render(true); // we render the full viewport to remove any ui elements that might have been // there before (eg. another user's cursor) next_active_pane.render_full_viewport(); if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, p); } } else { self.active_panes.insert(client_id, p); } self.set_pane_active_at(p); return true; } None => Some(active_pane_id), } } else { active_pane_id }; match updated_active_pane { Some(updated_active_pane) => { if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, updated_active_pane); } self.set_pane_active_at(updated_active_pane); } else { self.active_panes.insert(client_id, updated_active_pane); self.set_pane_active_at(updated_active_pane); } } None => { // TODO: can this happen? self.active_panes.clear(); } } false } } pub fn move_active_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { return; } if self.fullscreen_is_active { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let new_position_id = pane_grid.next_selectable_pane_id(&active_pane_id); let current_position = self.panes.get(&active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); let prev_geom_override = current_position.geom_override(); let new_position = self.panes.get_mut(&new_position_id).unwrap(); let next_geom = new_position.position_and_size(); let next_geom_override = new_position.geom_override(); new_position.set_geom(prev_geom); if let Some(geom) = prev_geom_override { new_position.get_geom_override(geom); } resize_pty!(new_position, self.os_api); new_position.set_should_render(true); let current_position = self.panes.get_mut(&active_pane_id).unwrap(); current_position.set_geom(next_geom); if let Some(geom) = next_geom_override { current_position.get_geom_override(geom); } resize_pty!(current_position, self.os_api); current_position.set_should_render(true); } 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.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.fullscreen_is_active { return; } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_below(&active_pane_id); if let Some(p) = next_index { let active_pane_id = self.active_panes.get(&client_id).unwrap(); let current_position = self.panes.get(active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); let prev_geom_override = current_position.geom_override(); let new_position = self.panes.get_mut(&p).unwrap(); let next_geom = new_position.position_and_size(); let next_geom_override = new_position.geom_override(); new_position.set_geom(prev_geom); if let Some(geom) = prev_geom_override { new_position.get_geom_override(geom); } resize_pty!(new_position, self.os_api); new_position.set_should_render(true); let current_position = self.panes.get_mut(active_pane_id).unwrap(); current_position.set_geom(next_geom); if let Some(geom) = next_geom_override { current_position.get_geom_override(geom); } resize_pty!(current_position, self.os_api); current_position.set_should_render(true); } } } } 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.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.fullscreen_is_active { return; } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_above(&active_pane_id); if let Some(p) = next_index { let active_pane_id = self.active_panes.get(&client_id).unwrap(); let current_position = self.panes.get(active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); let prev_geom_override = current_position.geom_override(); let new_position = self.panes.get_mut(&p).unwrap(); let next_geom = new_position.position_and_size(); let next_geom_override = new_position.geom_override(); new_position.set_geom(prev_geom); if let Some(geom) = prev_geom_override { new_position.get_geom_override(geom); } resize_pty!(new_position, self.os_api); new_position.set_should_render(true); let current_position = self.panes.get_mut(active_pane_id).unwrap(); current_position.set_geom(next_geom); if let Some(geom) = next_geom_override { current_position.get_geom_override(geom); } resize_pty!(current_position, self.os_api); current_position.set_should_render(true); } } } } 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.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.fullscreen_is_active { return; } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_to_the_right(&active_pane_id); if let Some(p) = next_index { let active_pane_id = self.active_panes.get(&client_id).unwrap(); let current_position = self.panes.get(active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); let prev_geom_override = current_position.geom_override(); let new_position = self.panes.get_mut(&p).unwrap(); let next_geom = new_position.position_and_size(); let next_geom_override = new_position.geom_override(); new_position.set_geom(prev_geom); if let Some(geom) = prev_geom_override { new_position.get_geom_override(geom); } resize_pty!(new_position, self.os_api); new_position.set_should_render(true); let current_position = self.panes.get_mut(active_pane_id).unwrap(); current_position.set_geom(next_geom); if let Some(geom) = next_geom_override { current_position.get_geom_override(geom); } resize_pty!(current_position, self.os_api); current_position.set_should_render(true); } } } } 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.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.fullscreen_is_active { return; } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { let pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); let next_index = pane_grid.next_selectable_pane_id_to_the_left(&active_pane_id); if let Some(p) = next_index { let active_pane_id = self.active_panes.get(&client_id).unwrap(); let current_position = self.panes.get(active_pane_id).unwrap(); let prev_geom = current_position.position_and_size(); let prev_geom_override = current_position.geom_override(); let new_position = self.panes.get_mut(&p).unwrap(); let next_geom = new_position.position_and_size(); let next_geom_override = new_position.geom_override(); new_position.set_geom(prev_geom); if let Some(geom) = prev_geom_override { new_position.get_geom_override(geom); } resize_pty!(new_position, self.os_api); new_position.set_should_render(true); let current_position = self.panes.get_mut(active_pane_id).unwrap(); current_position.set_geom(next_geom); if let Some(geom) = next_geom_override { current_position.get_geom_override(geom); } resize_pty!(current_position, self.os_api); current_position.set_should_render(true); } } } } fn close_down_to_max_terminals(&mut self) { if let Some(max_panes) = self.max_panes { let terminals = self.get_embedded_pane_ids(); for &pid in terminals.iter().skip(max_panes - 1) { self.senders .send_to_pty(PtyInstruction::ClosePane(pid)) .unwrap(); self.close_pane(pid); } } } pub fn get_embedded_pane_ids(&self) -> Vec { self.get_panes().map(|(&pid, _)| pid).collect() } pub fn get_all_pane_ids(&self) -> Vec { // this is here just as a naming thing to make things more explicit self.get_static_and_floating_pane_ids() } pub fn get_static_and_floating_pane_ids(&self) -> Vec { self.panes .keys() .chain(self.floating_panes.pane_ids()) .copied() .collect() } pub fn set_pane_selectable(&mut self, id: PaneId, selectable: bool) { if let Some(pane) = self.panes.get_mut(&id) { pane.set_selectable(selectable); if !selectable { // there are some edge cases in which this causes a hard crash when there are no // other selectable panes - ideally this should never happen unless it's a // configuration error - but this *does* sometimes happen with the default // configuration as well since we set this at run time. I left this here because // this should very rarely happen and I hope in my heart that we will stop setting // this at runtime in the default configuration at some point // // If however this is not the case and we find this does cause crashes, we can // solve it by adding a "dangling_clients" struct to Tab which we would fill with // the relevant client ids in this case and drain as soon as a new selectable pane // is opened self.move_clients_out_of_pane(id); } } } fn move_clients_out_of_pane(&mut self, pane_id: PaneId) { let active_panes: Vec<(ClientId, PaneId)> = self .active_panes .iter() .map(|(cid, pid)| (*cid, *pid)) .collect(); for (client_id, active_pane_id) in active_panes { if active_pane_id == pane_id { self.active_panes.insert( client_id, self.next_active_pane(&self.get_embedded_pane_ids()) .unwrap(), ); } } } pub fn close_pane(&mut self, id: PaneId) -> Option> { if self.floating_panes.panes_contain(&id) { let closed_pane = self.floating_panes.remove_pane(id); self.floating_panes.move_clients_out_of_pane(id); if !self.floating_panes.has_panes() { self.floating_panes.toggle_show_panes(false); } self.set_force_render(); self.floating_panes.set_force_render(); closed_pane } else { if self.fullscreen_is_active { self.unset_fullscreen(); } let mut pane_grid = TiledPaneGrid::new( &mut self.panes, *self.display_area.borrow(), *self.viewport.borrow(), ); if pane_grid.fill_space_over_pane(id) { // successfully filled space over pane let closed_pane = self.panes.remove(&id); self.move_clients_out_of_pane(id); for pane in self.panes.values_mut() { resize_pty!(pane, self.os_api); } closed_pane } else { self.panes.remove(&id); // this is a bit of a roundabout way to say: this is the last pane and so the tab // should be destroyed self.active_panes.clear(); None } } } pub fn close_focused_pane(&mut self, client_id: ClientId) { 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); self.senders .send_to_pty(PtyInstruction::ClosePane(active_floating_pane_id)) .unwrap(); return; } } if let Some(active_pane_id) = self.get_active_pane_id(client_id) { self.close_pane(active_pane_id); self.senders .send_to_pty(PtyInstruction::ClosePane(active_pane_id)) .unwrap(); } } 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_active_terminal_down(&mut self, client_id: ClientId) { 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); } } } } 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) - 1; active_pane.scroll_up(scroll_rows, client_id); } } pub fn scroll_active_terminal_down_page(&mut self, client_id: ClientId) { 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); } } } } 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) - 1) / 2; active_pane.scroll_up(scroll_rows, client_id); } } pub fn scroll_active_terminal_down_half_page(&mut self, client_id: ClientId) { 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); } } } } pub fn scroll_active_terminal_to_bottom(&mut self, client_id: ClientId) { 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); } } } } pub fn clear_active_terminal_scroll(&mut self, client_id: ClientId) { // TODO: is this a thing? 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); } } } } pub fn scroll_terminal_up(&mut self, point: &Position, lines: usize, client_id: ClientId) { if let Some(pane) = self.get_pane_at(point, false) { pane.scroll_up(lines, client_id); } } pub fn scroll_terminal_down(&mut self, point: &Position, lines: usize, client_id: ClientId) { if let Some(pane) = self.get_pane_at(point, false) { pane.scroll_down(lines, client_id); if !pane.is_scrolled() { if let PaneId::Terminal(pid) = pane.pid() { self.process_pending_vte_events(pid); } } } } fn get_pane_at( &mut self, point: &Position, search_selectable: bool, ) -> Option<&mut Box> { if self.floating_panes.panes_are_visible() { if let Some(pane_id) = self.floating_panes.get_pane_id_at(point, search_selectable) { return self.floating_panes.get_mut(&pane_id); } } if let Some(pane_id) = self.get_pane_id_at(point, search_selectable) { self.panes.get_mut(&pane_id) } else { None } } fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option { if self.fullscreen_is_active && self.is_position_inside_viewport(point) { let first_client_id = self.connected_clients.iter().next().unwrap(); // TODO: instead of doing this, record the pane that is in fullscreen return self.get_active_pane_id(*first_client_id); } if search_selectable { self.get_selectable_panes() .find(|(_, p)| p.contains(point)) .map(|(&id, _)| id) } else { self.get_panes() .find(|(_, p)| p.contains(point)) .map(|(&id, _)| id) } } pub fn handle_left_click(&mut self, position: &Position, client_id: ClientId) { self.focus_pane_at(position, client_id); let search_selectable = false; if self.floating_panes.panes_are_visible() && self .floating_panes .move_pane_with_mouse(*position, search_selectable) { self.set_force_render(); return; } if let Some(pane) = self.get_pane_at(position, false) { let relative_position = pane.relative_position(position); pane.start_selection(&relative_position, client_id); self.selecting_with_mouse = true; }; } pub fn handle_right_click(&mut self, position: &Position, client_id: ClientId) { self.focus_pane_at(position, client_id); if let Some(pane) = self.get_pane_at(position, false) { let relative_position = pane.relative_position(position); pane.handle_right_click(&relative_position, client_id); }; } fn focus_pane_at(&mut self, point: &Position, client_id: ClientId) { if self.floating_panes.panes_are_visible() { if let Some(clicked_pane) = self.floating_panes.get_pane_id_at(point, true) { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.floating_panes.focus_pane(clicked_pane, client_id); } self.set_pane_active_at(clicked_pane); return; } } if let Some(clicked_pane) = self.get_pane_id_at(point, true) { if self.session_is_mirrored { // move all clients let connected_clients: Vec = self.connected_clients.iter().copied().collect(); for client_id in connected_clients { self.active_panes.insert(client_id, clicked_pane); } } else { self.active_panes.insert(client_id, clicked_pane); } self.set_pane_active_at(clicked_pane); if self.floating_panes.panes_are_visible() { self.floating_panes.toggle_show_panes(false); self.set_force_render(); } } } pub fn handle_mouse_release(&mut self, position: &Position, client_id: ClientId) { if self.selecting_with_mouse { let mut selected_text = None; let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); if let Some(active_pane) = active_pane { let relative_position = active_pane.relative_position(position); active_pane.end_selection(&relative_position, client_id); selected_text = active_pane.get_selected_text(); active_pane.reset_selection(); } if let Some(selected_text) = selected_text { self.write_selection_to_clipboard(&selected_text); } self.selecting_with_mouse = false; } else if self.floating_panes.panes_are_visible() { self.floating_panes.stop_moving_pane_with_mouse(*position); } } pub fn handle_mouse_hold(&mut self, position_on_screen: &Position, client_id: ClientId) { let search_selectable = true; if self.selecting_with_mouse { let active_pane = self.get_active_pane_or_floating_pane_mut(client_id); if let Some(active_pane) = active_pane { let relative_position = active_pane.relative_position(position_on_screen); active_pane.update_selection(&relative_position, client_id); } } else if self.floating_panes.panes_are_visible() && self .floating_panes .move_pane_with_mouse(*position_on_screen, search_selectable) { self.set_force_render(); } } pub fn copy_selection(&self, client_id: ClientId) { let selected_text = self .get_active_pane(client_id) .and_then(|p| p.get_selected_text()); if let Some(selected_text) = selected_text { self.write_selection_to_clipboard(&selected_text); self.senders .send_to_plugin(PluginInstruction::Update( None, None, Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()), )) .unwrap(); } } fn write_selection_to_clipboard(&self, selection: &str) { let mut output = Output::default(); output.add_clients(&self.connected_clients, self.link_handler.clone(), None); let client_ids = self.connected_clients.iter().copied(); let clipboard_event = match self .clipboard_provider .set_content(selection, &mut output, client_ids) { Ok(_) => { let serialized_output = output.serialize(); self.senders .send_to_server(ServerInstruction::Render(Some(serialized_output))) .unwrap(); Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()) } Err(err) => { log::error!("could not write selection to clipboard: {}", err); Event::SystemClipboardFailure } }; self.senders .send_to_plugin(PluginInstruction::Update(None, None, clipboard_event)) .unwrap(); } fn is_inside_viewport(&self, pane_id: &PaneId) -> bool { // this is mostly separated to an outside function in order to allow us to pass a clone to // it sometimes when we need to get around the borrow checker is_inside_viewport(&*self.viewport.borrow(), self.panes.get(pane_id).unwrap()) } fn offset_viewport(&mut self, position_and_size: &Viewport) { let mut viewport = self.viewport.borrow_mut(); if position_and_size.x == viewport.x && position_and_size.x + position_and_size.cols == viewport.x + viewport.cols { if position_and_size.y == viewport.y { viewport.y += position_and_size.rows; viewport.rows -= position_and_size.rows; } else if position_and_size.y + position_and_size.rows == viewport.y + viewport.rows { viewport.rows -= position_and_size.rows; } } if position_and_size.y == viewport.y && position_and_size.y + position_and_size.rows == viewport.y + viewport.rows { if position_and_size.x == viewport.x { viewport.x += position_and_size.cols; viewport.cols -= position_and_size.cols; } else if position_and_size.x + position_and_size.cols == viewport.x + viewport.cols { viewport.cols -= position_and_size.cols; } } } pub fn visible(&self, visible: bool) { let pids_in_this_tab = self.panes.keys().filter_map(|p| match p { PaneId::Plugin(pid) => Some(pid), _ => None, }); for pid in pids_in_this_tab { self.senders .send_to_plugin(PluginInstruction::Update( Some(*pid), None, Event::Visible(visible), )) .unwrap(); } } pub fn update_active_pane_name(&mut self, buf: Vec, client_id: ClientId) { if let Some(active_terminal_id) = self.get_active_terminal_id(client_id) { let active_terminal = self .panes .get_mut(&PaneId::Terminal(active_terminal_id)) .unwrap(); // It only allows printable unicode, delete and backspace keys. let is_updatable = buf.iter().all(|u| matches!(u, 0x20..=0x7E | 0x08 | 0x7F)); if is_updatable { let s = str::from_utf8(&buf).unwrap(); active_terminal.update_name(s); } } } pub fn is_position_inside_viewport(&self, point: &Position) -> bool { let Position { line: Line(line), column: Column(column), } = *point; let line: usize = line.try_into().unwrap(); let viewport = self.viewport.borrow(); line >= viewport.y && column >= viewport.x && line <= viewport.y + viewport.rows && column <= viewport.x + viewport.cols } } #[allow(clippy::borrowed_box)] pub fn is_inside_viewport(viewport: &Viewport, pane: &Box) -> bool { let pane_position_and_size = pane.current_geom(); pane_position_and_size.y >= viewport.y && pane_position_and_size.y + pane_position_and_size.rows.as_usize() <= viewport.y + viewport.rows } pub fn pane_geom_is_inside_viewport(viewport: &Viewport, geom: &PaneGeom) -> bool { geom.y >= viewport.y && geom.y + geom.rows.as_usize() <= viewport.y + viewport.rows && geom.x >= viewport.x && geom.x + geom.cols.as_usize() <= viewport.x + viewport.cols } #[cfg(test)] #[path = "./unit/tab_tests.rs"] mod tab_tests; #[cfg(test)] #[path = "./unit/tab_integration_tests.rs"] mod tab_integration_tests;