From 821e7cbc5a9711173061a272dc0378fff1fe5596 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Fri, 18 Feb 2022 21:10:06 +0100 Subject: [PATCH] feat(ui): add floating panes (#1066) * basic functionality * close and reopen scratch terminal working * embed/float and resize whole tab for floating and static floating panes * move focus working * fix focus change in floating panes * move pane with mouse * floating z indices * tests and better resize algorithm * starting to work on performance * some performance experimentations * new render engine * reverse painters algorithm for floating panes * fix frame buffering * improve ux situation * handle multiple new panes on screen without overlap * adjust keybindings * adjust key hints * fix multiuser frame ui * fix various floating/multiuser bugs * remove stuff * wide characters under floating panes * fix wide character frame override * fix non-frame boundaries interactions with floating panes * fix selection character width * fix title frame wide char overflow * fix existing tests * add tests * refactor output out of tab * refactor floating panes out of tab * refactor tab * moar refactoring * refactorings and bring back terminal window title setting * add frame vte output * remove more unused stuff * remove even more unused stuff * you know the drill * refactor floating panes and remove more stuffs * refactor pane grids * remove unused output caching * refactor output * remove unused stuff * rustfmt * some formatting * rustfmt * reduce clippy to normal * remove comment * remove unused * fix closign pane * fix tests --- .github/workflows/rust.yml | 2 +- src/tests/e2e/cases.rs | 48 + src/tests/e2e/remote_runner.rs | 20 +- ...ts__e2e__cases__toggle_floating_panes.snap | 29 + zellij-client/src/input_handler.rs | 2 + zellij-server/src/lib.rs | 12 +- zellij-server/src/output/mod.rs | 571 +++++ zellij-server/src/panes/floating_panes.rs | 813 +++++++ zellij-server/src/panes/grid.rs | 170 +- zellij-server/src/panes/link_handler.rs | 10 +- zellij-server/src/panes/mod.rs | 13 +- zellij-server/src/panes/plugin_pane.rs | 11 +- zellij-server/src/panes/selection.rs | 19 +- zellij-server/src/panes/terminal_character.rs | 11 + zellij-server/src/panes/terminal_pane.rs | 148 +- zellij-server/src/panes/unit/grid_tests.rs | 584 ++++- .../src/panes/unit/terminal_pane_tests.rs | 12 +- zellij-server/src/route.rs | 15 + zellij-server/src/screen.rs | 44 +- zellij-server/src/tab/clipboard.rs | 4 +- zellij-server/src/tab/floating_pane_grid.rs | 1243 +++++++++++ zellij-server/src/tab/mod.rs | 1881 +++++++++++------ .../tab/{pane_grid.rs => tiled_pane_grid.rs} | 13 +- ...ests__cannot_float_only_embedded_pane.snap | 26 + ...on_tests__decrease_floating_pane_size.snap | 26 + ...tegration_tests__drag_pane_with_mouse.snap | 26 + ...ntegration_tests__embed_floating_pane.snap | 26 + ...ration_tests__five_new_floating_panes.snap | 26 + ...ntegration_tests__float_embedded_pane.snap | 26 + ...floating_panes_persist_across_toggles.snap | 26 + ...on_tests__increase_floating_pane_size.snap | 26 + ...tests__mark_text_inside_floating_pane.snap | 26 + ..._tests__move_floating_pane_focus_down.snap | 26 + ..._tests__move_floating_pane_focus_left.snap | 26 + ...tests__move_floating_pane_focus_right.snap | 26 + ...on_tests__move_floating_pane_focus_up.snap | 26 + ...__move_floating_pane_focus_with_mouse.snap | 26 + ...focus_with_mouse_to_non_floating_pane.snap | 26 + ..._integration_tests__new_floating_pane.snap | 26 + ...tion_tests__resize_floating_pane_down.snap | 26 + ...tion_tests__resize_floating_pane_left.snap | 26 + ...ion_tests__resize_floating_pane_right.snap | 26 + ...ration_tests__resize_floating_pane_up.snap | 26 + ...tests__resize_tab_with_floating_panes.snap | 26 + ...ing_panes_horizontally_and_vertically.snap | 26 + ...ntally_and_vertically_and_expand_back.snap | 26 + ...tion_tests__toggle_floating_panes_off.snap | 26 + ...ation_tests__toggle_floating_panes_on.snap | 26 + .../src/tab/unit/tab_integration_tests.rs | 1058 +++++++++ zellij-server/src/ui/boundaries.rs | 35 +- zellij-server/src/ui/pane_boundaries_frame.rs | 506 +++-- zellij-server/src/ui/pane_contents_and_ui.rs | 79 +- zellij-server/src/unit/screen_tests.rs | 15 - zellij-utils/assets/config/default.yaml | 6 +- zellij-utils/assets/layouts/no-plugins.yaml | 8 + zellij-utils/src/envs.rs | 2 + zellij-utils/src/errors.rs | 2 + zellij-utils/src/input/actions.rs | 4 + zellij-utils/src/input/mod.rs | 4 +- zellij-utils/src/pane_size.rs | 6 +- zellij-utils/src/position.rs | 8 + 61 files changed, 6808 insertions(+), 1250 deletions(-) create mode 100644 src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap create mode 100644 zellij-server/src/output/mod.rs create mode 100644 zellij-server/src/panes/floating_panes.rs create mode 100644 zellij-server/src/tab/floating_pane_grid.rs rename zellij-server/src/tab/{pane_grid.rs => tiled_pane_grid.rs} (99%) create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__cannot_float_only_embedded_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__decrease_floating_pane_size.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__embed_floating_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__five_new_floating_panes.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__float_embedded_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__floating_panes_persist_across_toggles.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__increase_floating_pane_size.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__mark_text_inside_floating_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_down.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_left.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_right.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_up.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_with_mouse.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_pane_focus_with_mouse_to_non_floating_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_down.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_left.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_right.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_up.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tab_with_floating_panes.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_back.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_off.snap create mode 100644 zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_on.snap create mode 100644 zellij-server/src/tab/unit/tab_integration_tests.rs create mode 100644 zellij-utils/assets/layouts/no-plugins.yaml diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 8cde3b22..c6540546 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -75,4 +75,4 @@ jobs: - name: Install cargo-make run: test -x "${HOME}/.cargo/bin/cargo-make" || cargo install --debug cargo-make - name: Check Lints - run: cargo make clippy -D clippy::all + run: cargo make clippy diff --git a/src/tests/e2e/cases.rs b/src/tests/e2e/cases.rs index 807d71e8..e2548b52 100644 --- a/src/tests/e2e/cases.rs +++ b/src/tests/e2e/cases.rs @@ -24,6 +24,7 @@ pub const MOVE_FOCUS_IN_PANE_MODE: [u8; 1] = [112]; // p pub const SPLIT_DOWN_IN_PANE_MODE: [u8; 1] = [100]; // d pub const SPLIT_RIGHT_IN_PANE_MODE: [u8; 1] = [114]; // r pub const TOGGLE_ACTIVE_TERMINAL_FULLSCREEN_IN_PANE_MODE: [u8; 1] = [102]; // f +pub const TOGGLE_FLOATING_PANES: [u8; 1] = [119]; // w pub const CLOSE_PANE_IN_PANE_MODE: [u8; 1] = [120]; // x pub const MOVE_FOCUS_DOWN_IN_PANE_MODE: [u8; 1] = [106]; // j pub const MOVE_FOCUS_UP_IN_PANE_MODE: [u8; 1] = [107]; // k @@ -1664,6 +1665,53 @@ pub fn bracketed_paste() { assert_snapshot!(last_snapshot); } +#[test] +#[ignore] +pub fn toggle_floating_panes() { + let fake_win_size = Size { + cols: 120, + rows: 24, + }; + + let mut test_attempts = 10; + let last_snapshot = loop { + RemoteRunner::kill_running_sessions(fake_win_size); + let mut runner = RemoteRunner::new(fake_win_size).add_step(Step { + name: "Toggle floating panes", + instruction: |mut remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.status_bar_appears() && remote_terminal.cursor_position_is(3, 2) + { + remote_terminal.send_key(&PANE_MODE); + remote_terminal.send_key(&TOGGLE_FLOATING_PANES); + // back to normal mode after split + step_is_complete = true; + } + step_is_complete + }, + }); + runner.run_all_steps(); + let last_snapshot = runner.take_snapshot_after(Step { + name: "Wait for new pane to appear", + instruction: |remote_terminal: RemoteTerminal| -> bool { + let mut step_is_complete = false; + if remote_terminal.cursor_position_is(33, 7) && remote_terminal.tip_appears() { + // cursor is in the newly opened second pane + step_is_complete = true; + } + step_is_complete + }, + }); + if runner.test_timed_out && test_attempts > 0 { + test_attempts -= 1; + continue; + } else { + break last_snapshot; + } + }; + assert_snapshot!(last_snapshot); +} + #[test] #[ignore] pub fn focus_tab_with_layout() { diff --git a/src/tests/e2e/remote_runner.rs b/src/tests/e2e/remote_runner.rs index 66d622fa..9fe4cefd 100644 --- a/src/tests/e2e/remote_runner.rs +++ b/src/tests/e2e/remote_runner.rs @@ -2,7 +2,7 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use zellij_tile::data::Palette; -use zellij_server::panes::TerminalPane; +use zellij_server::panes::{LinkHandler, TerminalPane}; use zellij_utils::pane_size::{Dimension, PaneGeom, Size}; use zellij_utils::{vte, zellij_tile}; @@ -12,6 +12,9 @@ use std::net::TcpStream; use std::path::Path; +use std::cell::RefCell; +use std::rc::Rc; + const ZELLIJ_EXECUTABLE_LOCATION: &str = "/usr/src/zellij/x86_64-unknown-linux-musl/release/zellij"; const ZELLIJ_LAYOUT_PATH: &str = "/usr/src/zellij/fixtures/layouts"; const CONNECTION_STRING: &str = "127.0.0.1:2222"; @@ -141,16 +144,23 @@ fn read_from_channel( let thread = std::thread::Builder::new() .name("read_thread".into()) .spawn({ + let pane_geom = *pane_geom; let should_keep_running = should_keep_running.clone(); let channel = channel.clone(); let last_snapshot = last_snapshot.clone(); let cursor_coordinates = cursor_coordinates.clone(); - let mut vte_parser = vte::Parser::new(); - let mut terminal_output = - TerminalPane::new(0, *pane_geom, Palette::default(), 0, String::new()); // 0 is the pane index - let mut retries_left = 3; move || { + let mut retries_left = 3; let mut should_sleep = false; + let mut vte_parser = vte::Parser::new(); + let mut terminal_output = TerminalPane::new( + 0, + pane_geom, + Palette::default(), + 0, + String::new(), + Rc::new(RefCell::new(LinkHandler::new())), + ); // 0 is the pane index loop { if !should_keep_running.load(Ordering::SeqCst) { break; diff --git a/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap new file mode 100644 index 00000000..fb247c07 --- /dev/null +++ b/src/tests/e2e/snapshots/zellij__tests__e2e__cases__toggle_floating_panes.snap @@ -0,0 +1,29 @@ +--- +source: src/tests/e2e/cases.rs +expression: last_snapshot + +--- + Zellij (e2e-test)  Tab #1  +┌ Pane #1 ─────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +│$ │ +│ │ +│ │ +│ │ +│ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +│ │$ █ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ │ │ │ +│ └──────────────────────────────────────────────────────────┘ │ +│ │ +│ │ +│ │ +│ │ +│ │ +└──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + Ctrl + LOCK 

PANE  TAB  RESIZE  MOVE  SCROLL  SESSION  QUIT  + Tip: Alt + => new pane. Alt + <[] or hjkl> => navigate. Alt + <+-> => resize pane. diff --git a/zellij-client/src/input_handler.rs b/zellij-client/src/input_handler.rs index 3c2495c0..5ae838c4 100644 --- a/zellij-client/src/input_handler.rs +++ b/zellij-client/src/input_handler.rs @@ -199,6 +199,8 @@ impl InputHandler { } Action::CloseFocus | Action::NewPane(_) + | Action::ToggleFloatingPanes + | Action::TogglePaneEmbedOrFloating | Action::NewTab(_) | Action::GoToNextTab | Action::GoToPreviousTab diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 3f7ac989..9ee0bbfe 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -1,4 +1,5 @@ pub mod os_input_output; +pub mod output; pub mod panes; pub mod tab; @@ -29,7 +30,6 @@ use crate::{ os_input_output::ServerOsApi, pty::{pty_thread_main, Pty, PtyInstruction}, screen::{screen_thread_main, ScreenInstruction}, - tab::Output, thread_bus::{Bus, ThreadSenders}, wasm_vm::{wasm_thread_main, PluginInstruction}, }; @@ -63,7 +63,7 @@ pub enum ServerInstruction { ClientId, Option, ), - Render(Option), + Render(Option>), UnblockInputThread, ClientExit(ClientId), RemoveClient(ClientId), @@ -492,14 +492,12 @@ pub fn start_server(mut os_input: Box, socket_path: PathBuf) { .send_to_plugin(PluginInstruction::RemoveClient(client_id)) .unwrap(); } - ServerInstruction::Render(mut output) => { + ServerInstruction::Render(serialized_output) => { let client_ids = session_state.read().unwrap().client_ids(); - // Here the output is of the type Option sent by screen thread. // If `Some(_)`- unwrap it and forward it to the clients to render. // If `None`- Send an exit instruction. This is the case when a user closes the last Tab/Pane. - if let Some(op) = &mut output { - for (client_id, client_render_instruction) in &mut op.client_render_instructions - { + if let Some(output) = &serialized_output { + for (client_id, client_render_instruction) in output.iter() { os_input.send_to_client( *client_id, ServerToClientMsg::Render(client_render_instruction.clone()), diff --git a/zellij-server/src/output/mod.rs b/zellij-server/src/output/mod.rs new file mode 100644 index 00000000..07d27d1b --- /dev/null +++ b/zellij-server/src/output/mod.rs @@ -0,0 +1,571 @@ +use std::collections::VecDeque; + +use crate::panes::selection::Selection; +use crate::panes::Row; + +use crate::{ + panes::terminal_character::{AnsiCode, CharacterStyles}, + panes::{LinkHandler, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, + ClientId, +}; +use std::cell::RefCell; +use std::fmt::Write; +use std::rc::Rc; +use std::{ + collections::{HashMap, HashSet}, + str, +}; +use zellij_utils::pane_size::PaneGeom; + +fn vte_goto_instruction(x_coords: usize, y_coords: usize, vte_output: &mut String) { + write!( + vte_output, + "\u{1b}[{};{}H\u{1b}[m", + y_coords + 1, // + 1 because VTE is 1 indexed + x_coords + 1, + ) + .unwrap(); +} + +fn adjust_styles_for_possible_selection( + chunk_selection_and_background_color: Option<(Selection, AnsiCode)>, + character_styles: CharacterStyles, + chunk_y: usize, + chunk_width: usize, +) -> CharacterStyles { + chunk_selection_and_background_color + .and_then(|(selection, background_color)| { + if selection.contains(chunk_y, chunk_width) { + Some(character_styles.background(Some(background_color))) + } else { + None + } + }) + .unwrap_or(character_styles) +} + +fn write_changed_styles( + character_styles: &mut CharacterStyles, + current_character_styles: CharacterStyles, + chunk_changed_colors: Option<[Option; 256]>, + link_handler: Option<&std::cell::Ref>, + vte_output: &mut String, +) { + if let Some(new_styles) = + character_styles.update_and_return_diff(¤t_character_styles, chunk_changed_colors) + { + // if let Some(osc8_link) = link_handler.as_ref().and_then(|l_h| l_h.borrow().output_osc8(new_styles.link_anchor)) { + if let Some(osc8_link) = + link_handler.and_then(|l_h| l_h.output_osc8(new_styles.link_anchor)) + { + write!(vte_output, "{}{}", new_styles, osc8_link).unwrap(); + } else { + write!(vte_output, "{}", new_styles).unwrap(); + } + } +} + +fn serialize_character_chunks( + character_chunks: Vec, + link_handler: Option<&mut Rc>>, +) -> String { + let mut vte_output = String::new(); // TODO: preallocate character_chunks.len()? + let link_handler = link_handler.map(|l_h| l_h.borrow()); + for character_chunk in character_chunks { + let chunk_selection_and_background_color = character_chunk.selection_and_background_color(); + let chunk_changed_colors = character_chunk.changed_colors(); + let mut character_styles = CharacterStyles::new(); + vte_goto_instruction(character_chunk.x, character_chunk.y, &mut vte_output); + let mut chunk_width = character_chunk.x; + for t_character in character_chunk.terminal_characters.iter() { + let current_character_styles = adjust_styles_for_possible_selection( + chunk_selection_and_background_color, + t_character.styles, + character_chunk.y, + chunk_width, + ); + write_changed_styles( + &mut character_styles, + current_character_styles, + chunk_changed_colors, + link_handler.as_ref(), + &mut vte_output, + ); + chunk_width += t_character.width; + vte_output.push(t_character.character); + } + character_styles.clear(); + } + vte_output +} + +type AbsoluteMiddleStart = usize; +type AbsoluteMiddleEnd = usize; +type PadLeftEndBy = usize; +type PadRightStartBy = usize; +fn adjust_middle_segment_for_wide_chars( + middle_start: usize, + middle_end: usize, + terminal_characters: &[TerminalCharacter], +) -> ( + AbsoluteMiddleStart, + AbsoluteMiddleEnd, + PadLeftEndBy, + PadRightStartBy, +) { + let mut absolute_middle_start_index = None; + let mut absolute_middle_end_index = None; + let mut current_x = 0; + let mut pad_left_end_by = 0; + let mut pad_right_start_by = 0; + for (absolute_index, t_character) in terminal_characters.iter().enumerate() { + current_x += t_character.width; + if current_x >= middle_start && absolute_middle_start_index.is_none() { + if current_x > middle_start { + pad_left_end_by = current_x - middle_start; + absolute_middle_start_index = Some(absolute_index); + } else { + absolute_middle_start_index = Some(absolute_index + 1); + } + } + if current_x >= middle_end && absolute_middle_end_index.is_none() { + absolute_middle_end_index = Some(absolute_index + 1); + if current_x > middle_end { + pad_right_start_by = current_x - middle_end; + } + } + } + ( + absolute_middle_start_index.unwrap(), + absolute_middle_end_index.unwrap(), + pad_left_end_by, + pad_right_start_by, + ) +} + +#[derive(Clone, Debug, Default)] +pub struct Output { + pre_vte_instructions: HashMap>, + post_vte_instructions: HashMap>, + client_character_chunks: HashMap>, + link_handler: Option>>, + floating_panes_stack: Option, +} + +impl Output { + pub fn add_clients( + &mut self, + client_ids: &HashSet, + link_handler: Rc>, + floating_panes_stack: Option, + ) { + self.link_handler = Some(link_handler); + self.floating_panes_stack = floating_panes_stack; + for client_id in client_ids { + self.client_character_chunks.insert(*client_id, vec![]); + } + } + pub fn add_character_chunks_to_client( + &mut self, + client_id: ClientId, + mut character_chunks: Vec, + z_index: Option, + ) { + if let Some(client_character_chunks) = self.client_character_chunks.get_mut(&client_id) { + if let Some(floating_panes_stack) = &self.floating_panes_stack { + let mut visible_character_chunks = + floating_panes_stack.visible_character_chunks(character_chunks, z_index); + client_character_chunks.append(&mut visible_character_chunks); + } else { + client_character_chunks.append(&mut character_chunks); + } + } + } + pub fn add_character_chunks_to_multiple_clients( + &mut self, + character_chunks: Vec, + client_ids: impl Iterator, + z_index: Option, + ) { + for client_id in client_ids { + self.add_character_chunks_to_client(client_id, character_chunks.clone(), z_index); + // TODO: forgo clone by adding an all_clients thing? + } + } + pub fn add_post_vte_instruction_to_multiple_clients( + &mut self, + client_ids: impl Iterator, + vte_instruction: &str, + ) { + for client_id in client_ids { + let entry = self + .post_vte_instructions + .entry(client_id) + .or_insert_with(Vec::new); + entry.push(String::from(vte_instruction)); + } + } + pub fn add_pre_vte_instruction_to_multiple_clients( + &mut self, + client_ids: impl Iterator, + vte_instruction: &str, + ) { + for client_id in client_ids { + let entry = self + .pre_vte_instructions + .entry(client_id) + .or_insert_with(Vec::new); + entry.push(String::from(vte_instruction)); + } + } + pub fn add_post_vte_instruction_to_client( + &mut self, + client_id: ClientId, + vte_instruction: &str, + ) { + let entry = self + .post_vte_instructions + .entry(client_id) + .or_insert_with(Vec::new); + entry.push(String::from(vte_instruction)); + } + pub fn add_pre_vte_instruction_to_client( + &mut self, + client_id: ClientId, + vte_instruction: &str, + ) { + let entry = self + .pre_vte_instructions + .entry(client_id) + .or_insert_with(Vec::new); + entry.push(String::from(vte_instruction)); + } + pub fn serialize(&mut self) -> HashMap { + let mut serialized_render_instructions = HashMap::new(); + + for (client_id, client_character_chunks) in self.client_character_chunks.drain() { + let mut client_serialized_render_instructions = String::new(); + + // append pre-vte instructions for this client + if let Some(pre_vte_instructions_for_client) = + self.pre_vte_instructions.remove(&client_id) + { + for vte_instruction in pre_vte_instructions_for_client { + client_serialized_render_instructions.push_str(&vte_instruction); + } + } + + // append the actual vte + client_serialized_render_instructions.push_str(&serialize_character_chunks( + client_character_chunks, + self.link_handler.as_mut(), + )); // TODO: less allocations? + + // append post-vte instructions for this client + if let Some(post_vte_instructions_for_client) = + self.post_vte_instructions.remove(&client_id) + { + for vte_instruction in post_vte_instructions_for_client { + client_serialized_render_instructions.push_str(&vte_instruction); + } + } + + serialized_render_instructions.insert(client_id, client_serialized_render_instructions); + } + serialized_render_instructions + } +} + +// this struct represents the geometry of a group of floating panes +// we use it to filter out CharacterChunks who are behind these geometries +// and so would not be visible. If a chunk is partially covered, it is adjusted +// to include only the non-covered parts +#[derive(Debug, Clone, Default)] +pub struct FloatingPanesStack { + pub layers: Vec, +} + +impl FloatingPanesStack { + pub fn visible_character_chunks( + &self, + mut character_chunks: Vec, + z_index: Option, + ) -> Vec { + let z_index = z_index.unwrap_or(0); + let mut chunks_to_check: Vec = character_chunks.drain(..).collect(); + let mut visible_chunks = vec![]; + 'chunk_loop: loop { + match chunks_to_check.pop() { + Some(mut c_chunk) => { + let panes_to_check = self.layers.iter().skip(z_index); + for pane_geom in panes_to_check { + let new_chunk_to_check = self.remove_covered_parts(pane_geom, &mut c_chunk); + if let Some(new_chunk_to_check) = new_chunk_to_check { + // this happens when the pane covers the middle of the chunk, and so we + // end up with an extra chunk we need to check (eg. against panes above + // this one) + chunks_to_check.push(new_chunk_to_check); + } + if c_chunk.terminal_characters.is_empty() { + continue 'chunk_loop; + } + } + visible_chunks.push(c_chunk); + } + None => { + break 'chunk_loop; + } + } + } + visible_chunks + } + fn remove_covered_parts( + &self, + pane_geom: &PaneGeom, + c_chunk: &mut CharacterChunk, + ) -> Option { + let pane_top_edge = pane_geom.y; + let pane_left_edge = pane_geom.x; + let pane_bottom_edge = pane_geom.y + pane_geom.rows.as_usize().saturating_sub(1); + let pane_right_edge = pane_geom.x + pane_geom.cols.as_usize().saturating_sub(1); + let c_chunk_left_side = c_chunk.x; + let c_chunk_right_side = c_chunk.x + (c_chunk.width()).saturating_sub(1); + if pane_top_edge <= c_chunk.y && pane_bottom_edge >= c_chunk.y { + if pane_left_edge <= c_chunk_left_side && pane_right_edge >= c_chunk_right_side { + // pane covers chunk completely + drop(c_chunk.terminal_characters.drain(..)); + return None; + } else if pane_right_edge > c_chunk_left_side + && pane_right_edge < c_chunk_right_side + && pane_left_edge <= c_chunk_left_side + { + // pane covers chunk partially to the left + let covered_part = c_chunk.drain_by_width(pane_right_edge + 1 - c_chunk_left_side); + drop(covered_part); + c_chunk.x = pane_right_edge + 1; + return None; + } else if pane_left_edge > c_chunk_left_side + && pane_left_edge < c_chunk_right_side + && pane_right_edge >= c_chunk_right_side + { + // pane covers chunk partially to the right + c_chunk.retain_by_width(pane_left_edge - c_chunk_left_side); + return None; + } else if pane_left_edge >= c_chunk_left_side && pane_right_edge <= c_chunk_right_side { + // pane covers chunk middle + let (left_chunk_characters, right_chunk_characters) = c_chunk.cut_middle_out( + pane_left_edge - c_chunk_left_side, + (pane_right_edge + 1) - c_chunk_left_side, + ); + let left_chunk_x = c_chunk_left_side; + let right_chunk_x = pane_right_edge + 1; + let left_chunk = + CharacterChunk::new(left_chunk_characters, left_chunk_x, c_chunk.y); + c_chunk.x = right_chunk_x; + c_chunk.terminal_characters = right_chunk_characters; + return Some(left_chunk); + } + }; + None + } +} + +#[derive(Debug, Clone, Default)] +pub struct CharacterChunk { + pub terminal_characters: Vec, + pub x: usize, + pub y: usize, + pub changed_colors: Option<[Option; 256]>, + selection_and_background_color: Option<(Selection, AnsiCode)>, +} + +impl CharacterChunk { + pub fn new(terminal_characters: Vec, x: usize, y: usize) -> Self { + CharacterChunk { + terminal_characters, + x, + y, + ..Default::default() + } + } + pub fn add_selection_and_background( + &mut self, + selection: Selection, + background_color: AnsiCode, + offset_x: usize, + offset_y: usize, + ) { + self.selection_and_background_color = + Some((selection.offset(offset_x, offset_y), background_color)); + } + pub fn selection_and_background_color(&self) -> Option<(Selection, AnsiCode)> { + self.selection_and_background_color + } + pub fn add_changed_colors(&mut self, changed_colors: Option<[Option; 256]>) { + self.changed_colors = changed_colors; + } + pub fn changed_colors(&self) -> Option<[Option; 256]> { + self.changed_colors + } + pub fn width(&self) -> usize { + let mut width = 0; + for t_character in &self.terminal_characters { + width += t_character.width + } + width + } + pub fn drain_by_width(&mut self, x: usize) -> impl Iterator { + let mut drained_part: VecDeque = VecDeque::new(); + let mut drained_part_len = 0; + loop { + if self.terminal_characters.is_empty() { + break; + } + let next_character = self.terminal_characters.remove(0); // TODO: consider copying self.terminal_characters into a VecDeque to make this process faster? + if drained_part_len + next_character.width <= x { + drained_part.push_back(next_character); + drained_part_len += next_character.width; + } else { + if drained_part_len == x { + self.terminal_characters.insert(0, next_character); // put it back + } else if next_character.width > 1 { + for _ in 1..next_character.width { + self.terminal_characters.insert(0, EMPTY_TERMINAL_CHARACTER); + drained_part.push_back(EMPTY_TERMINAL_CHARACTER); + } + } + break; + } + } + drained_part.into_iter() + } + pub fn retain_by_width(&mut self, x: usize) { + let part_to_retain = self.drain_by_width(x); + self.terminal_characters = part_to_retain.collect(); + } + pub fn cut_middle_out( + &mut self, + middle_start: usize, + middle_end: usize, + ) -> (Vec, Vec) { + let ( + absolute_middle_start_index, + absolute_middle_end_index, + pad_left_end_by, + pad_right_start_by, + ) = adjust_middle_segment_for_wide_chars( + middle_start, + middle_end, + &self.terminal_characters, + ); + let mut terminal_characters: Vec = + self.terminal_characters.drain(..).collect(); + let mut characters_on_the_right: Vec = terminal_characters + .drain(absolute_middle_end_index..) + .collect(); + let mut characters_on_the_left: Vec = terminal_characters + .drain(..absolute_middle_start_index) + .collect(); + if pad_left_end_by > 0 { + characters_on_the_left.resize(pad_left_end_by, EMPTY_TERMINAL_CHARACTER); + } + if pad_right_start_by > 0 { + for _ in 0..pad_right_start_by { + characters_on_the_right.insert(0, EMPTY_TERMINAL_CHARACTER); + } + } + (characters_on_the_left, characters_on_the_right) + } +} + +#[derive(Clone, Debug)] +pub struct OutputBuffer { + changed_lines: Vec, // line index + should_update_all_lines: bool, +} + +impl Default for OutputBuffer { + fn default() -> Self { + OutputBuffer { + changed_lines: vec![], + should_update_all_lines: true, // first time we should do a full render + } + } +} + +impl OutputBuffer { + pub fn update_line(&mut self, line_index: usize) { + if !self.should_update_all_lines { + self.changed_lines.push(line_index); + } + } + pub fn update_all_lines(&mut self) { + self.clear(); + self.should_update_all_lines = true; + } + pub fn clear(&mut self) { + self.changed_lines.clear(); + self.should_update_all_lines = false; + } + pub fn changed_chunks_in_viewport( + &self, + viewport: &[Row], + viewport_width: usize, + viewport_height: usize, + x_offset: usize, + y_offset: usize, + ) -> Vec { + if self.should_update_all_lines { + let mut changed_chunks = Vec::with_capacity(viewport.len()); + for line_index in 0..viewport_height { + let terminal_characters = + self.extract_line_from_viewport(line_index, viewport, viewport_width); + let x = x_offset; // right now we only buffer full lines as this doesn't seem to have a huge impact on performance, but the infra is here if we want to change this + let y = line_index + y_offset; + changed_chunks.push(CharacterChunk::new(terminal_characters, x, y)); + } + changed_chunks + } else { + let mut line_changes = self.changed_lines.to_vec(); + line_changes.sort_unstable(); + line_changes.dedup(); + let mut changed_chunks = Vec::with_capacity(line_changes.len()); + for line_index in line_changes { + let terminal_characters = + self.extract_line_from_viewport(line_index, viewport, viewport_width); + let x = x_offset; + let y = line_index + y_offset; + changed_chunks.push(CharacterChunk::new(terminal_characters, x, y)); + } + changed_chunks + } + } + fn extract_characters_from_row( + &self, + row: &Row, + viewport_width: usize, + ) -> Vec { + let mut terminal_characters: Vec = row.columns.iter().copied().collect(); + // pad row + let row_width = row.width(); + if row_width < viewport_width { + let mut padding = vec![EMPTY_TERMINAL_CHARACTER; viewport_width - row_width]; + terminal_characters.append(&mut padding); + } + terminal_characters + } + fn extract_line_from_viewport( + &self, + line_index: usize, + viewport: &[Row], + viewport_width: usize, + ) -> Vec { + match viewport.get(line_index) { + // TODO: iterator? + Some(row) => self.extract_characters_from_row(row, viewport_width), + None => { + vec![EMPTY_TERMINAL_CHARACTER; viewport_width] + } + } + } +} diff --git a/zellij-server/src/panes/floating_panes.rs b/zellij-server/src/panes/floating_panes.rs new file mode 100644 index 00000000..1b9d8e3e --- /dev/null +++ b/zellij-server/src/panes/floating_panes.rs @@ -0,0 +1,813 @@ +use zellij_utils::{position::Position, zellij_tile}; + +use crate::tab::floating_pane_grid::FloatingPaneGrid; +use crate::tab::Pane; + +use crate::{ + os_input_output::ServerOsApi, + output::{FloatingPanesStack, Output}, + panes::PaneId, + ui::pane_contents_and_ui::PaneContentsAndUi, + ClientId, +}; +use std::cell::RefCell; +use std::collections::{BTreeMap, HashMap, HashSet}; +use std::rc::Rc; +use zellij_tile::data::{ModeInfo, Palette}; +use zellij_utils::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, + ); + } + }; +} + +pub struct FloatingPanes { + panes: BTreeMap>, + display_area: Rc>, + viewport: Rc>, + desired_pane_positions: HashMap, // this represents the positions of panes the user moved with intention, rather than by resizing the terminal window + z_indices: Vec, + active_panes: HashMap, + show_panes: bool, + pane_being_moved_with_mouse: Option<(PaneId, Position)>, +} + +#[allow(clippy::borrowed_box)] +impl FloatingPanes { + pub fn new(display_area: Rc>, viewport: Rc>) -> Self { + FloatingPanes { + panes: BTreeMap::new(), + display_area, + viewport, + desired_pane_positions: HashMap::new(), + z_indices: vec![], + show_panes: false, + active_panes: HashMap::new(), + pane_being_moved_with_mouse: None, + } + } + pub fn stack(&self) -> FloatingPanesStack { + let layers = self + .z_indices + .iter() + .map(|pane_id| self.panes.get(pane_id).unwrap().position_and_size()) + .collect(); + FloatingPanesStack { layers } + } + pub fn pane_ids(&self) -> impl Iterator { + self.panes.keys() + } + pub fn add_pane(&mut self, pane_id: PaneId, pane: Box) { + self.desired_pane_positions + .insert(pane_id, pane.position_and_size()); + self.panes.insert(pane_id, pane); + self.z_indices.push(pane_id); + } + pub fn remove_pane(&mut self, pane_id: PaneId) -> Option> { + self.z_indices.retain(|p_id| *p_id != pane_id); + self.desired_pane_positions.remove(&pane_id); + self.panes.remove(&pane_id) + } + pub fn get(&self, pane_id: &PaneId) -> Option<&Box> { + self.panes.get(pane_id) + } + pub fn get_mut(&mut self, pane_id: &PaneId) -> Option<&mut Box> { + self.panes.get_mut(pane_id) + } + pub fn get_active_pane(&self, client_id: ClientId) -> Option<&Box> { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get(active_pane_id)) + } + pub fn get_active_pane_mut(&mut self, client_id: ClientId) -> Option<&mut Box> { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + } + pub fn panes_are_visible(&self) -> bool { + self.show_panes + } + pub fn has_active_panes(&self) -> bool { + !self.active_panes.is_empty() + } + pub fn has_panes(&self) -> bool { + !self.panes.is_empty() + } + pub fn active_pane_id(&self, client_id: ClientId) -> Option { + self.active_panes.get(&client_id).copied() + } + pub fn toggle_show_panes(&mut self, should_show_floating_panes: bool) { + self.show_panes = should_show_floating_panes; + } + pub fn active_panes_contain(&self, client_id: &ClientId) -> bool { + self.active_panes.contains_key(client_id) + } + pub fn panes_contain(&self, pane_id: &PaneId) -> bool { + self.panes.contains_key(pane_id) + } + pub fn find_room_for_new_pane(&mut self) -> Option { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.find_room_for_new_pane() + } + pub fn first_floating_pane_id(&self) -> Option { + self.panes.keys().next().copied() + } + pub fn first_active_floating_pane_id(&self) -> Option { + self.active_panes.values().next().copied() + } + 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(); + } + } + pub fn set_pane_frames(&mut self, os_api: &mut Box) { + for pane in self.panes.values_mut() { + // floating panes should always have a frame unless explicitly set otherwise + if !pane.borderless() { + pane.set_frame(true); + pane.set_content_offset(Offset::frame(1)); + } else { + pane.set_content_offset(Offset::default()); + } + resize_pty!(pane, os_api); + } + } + #[allow(clippy::too_many_arguments)] + pub fn render( + &mut self, + connected_clients_in_app: &Rc>>, + connected_clients: &HashSet, + mode_info: &HashMap, + default_mode_info: &ModeInfo, + session_is_mirrored: bool, + output: &mut Output, + colors: Palette, + ) { + let mut floating_panes: Vec<_> = self.panes.iter_mut().collect(); + floating_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| { + self.z_indices + .iter() + .position(|id| id == *a_id) + .unwrap() + .cmp(&self.z_indices.iter().position(|id| id == *b_id).unwrap()) + }); + + for (z_index, (kind, pane)) in floating_panes.iter_mut().enumerate() { + let mut active_panes = self.active_panes.clone(); + let multiple_users_exist_in_session = { connected_clients_in_app.borrow().len() > 1 }; + active_panes.retain(|c_id, _| connected_clients.contains(c_id)); + let mut pane_contents_and_ui = PaneContentsAndUi::new( + pane, + output, + colors, + &active_panes, + multiple_users_exist_in_session, + Some(z_index + 1), // +1 because 0 is reserved for non-floating panes + ); + for &client_id in connected_clients { + let client_mode = mode_info.get(&client_id).unwrap_or(default_mode_info).mode; + pane_contents_and_ui.render_pane_frame(client_id, client_mode, session_is_mirrored); + if let PaneId::Plugin(..) = kind { + pane_contents_and_ui.render_pane_contents_for_client(client_id); + } + // 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(connected_clients.iter().copied()); + } + } + } + pub fn resize(&mut self, new_screen_size: Size) { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize(new_screen_size); + self.set_force_render(); + } + pub fn resize_active_pane_left( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_pane_left(active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + false + } + pub fn resize_active_pane_right( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_pane_right(&active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + false + } + pub fn resize_active_pane_down( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_pane_down(&active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + return false; + } + pub fn resize_active_pane_up( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_pane_up(&active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + return false; + } + pub fn resize_active_pane_increase( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_increase(&active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + return false; + } + pub fn resize_active_pane_decrease( + &mut self, + client_id: ClientId, + os_api: &mut Box, + ) -> bool { + // true => successfully resized + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_floating_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.resize_decrease(&active_floating_pane_id); + for pane in self.panes.values_mut() { + resize_pty!(pane, os_api); + } + self.set_force_render(); + return true; + } + return false; + } + pub fn move_focus_left( + &mut self, + client_id: ClientId, + connected_clients: &HashSet, + ) -> bool { + // true => successfully moved + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let active_pane_id = self.active_panes.get(&client_id).copied(); + let updated_active_pane = if let Some(active_pane_id) = active_pane_id { + let floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + let next_index = + floating_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(); + + // move all clients + let connected_clients: Vec = + connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(p, client_id); + } + + self.set_force_render(); + return true; + } + None => Some(active_pane_id), + } + } else { + active_pane_id + }; + match updated_active_pane { + Some(updated_active_pane) => { + let connected_clients: Vec = connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(updated_active_pane, client_id); + } + self.set_force_render(); + } + None => { + // TODO: can this happen? + self.active_panes.clear(); + self.z_indices.clear(); + } + } + false + } + pub fn move_focus_right( + &mut self, + client_id: ClientId, + connected_clients: &HashSet, + ) -> bool { + // true => successfully moved + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let active_pane_id = self.active_panes.get(&client_id).copied(); + let updated_active_pane = if let Some(active_pane_id) = active_pane_id { + let floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + let next_index = + floating_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(); + + // move all clients + let connected_clients: Vec = + connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(p, client_id); + } + + self.set_force_render(); + return true; + } + None => Some(active_pane_id), + } + } else { + active_pane_id + }; + match updated_active_pane { + Some(updated_active_pane) => { + let connected_clients: Vec = connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(updated_active_pane, client_id); + } + self.set_force_render(); + } + None => { + // TODO: can this happen? + self.active_panes.clear(); + self.z_indices.clear(); + } + } + false + } + pub fn move_focus_up( + &mut self, + client_id: ClientId, + connected_clients: &HashSet, + ) -> bool { + // true => successfully moved + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let active_pane_id = self.active_panes.get(&client_id).copied(); + let updated_active_pane = if let Some(active_pane_id) = active_pane_id { + let floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + let next_index = floating_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(); + + // move all clients + let connected_clients: Vec = + connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(p, client_id); + } + + self.set_force_render(); + return true; + } + None => Some(active_pane_id), + } + } else { + active_pane_id + }; + match updated_active_pane { + Some(updated_active_pane) => { + let connected_clients: Vec = connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(updated_active_pane, client_id); + } + self.set_force_render(); + } + None => { + // TODO: can this happen? + self.active_panes.clear(); + self.z_indices.clear(); + } + } + false + } + pub fn move_focus_down( + &mut self, + client_id: ClientId, + connected_clients: &HashSet, + ) -> bool { + // true => successfully moved + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let active_pane_id = self.active_panes.get(&client_id).copied(); + let updated_active_pane = if let Some(active_pane_id) = active_pane_id { + let floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + let next_index = floating_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(); + + // move all clients + let connected_clients: Vec = + connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(p, client_id); + } + + self.set_force_render(); + return true; + } + None => Some(active_pane_id), + } + } else { + active_pane_id + }; + match updated_active_pane { + Some(updated_active_pane) => { + let connected_clients: Vec = connected_clients.iter().copied().collect(); + for client_id in connected_clients { + self.focus_pane(updated_active_pane, client_id); + } + self.set_force_render(); + } + None => { + // TODO: can this happen? + self.active_panes.clear(); + self.z_indices.clear(); + } + } + false + } + pub fn move_active_pane_down(&mut self, client_id: ClientId) { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.move_pane_down(&active_pane_id); + self.set_force_render(); + } + } + pub fn move_active_pane_up(&mut self, client_id: ClientId) { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.move_pane_up(&active_pane_id); + self.set_force_render(); + } + } + pub fn move_active_pane_left(&mut self, client_id: ClientId) { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.move_pane_left(&active_pane_id); + self.set_force_render(); + } + } + pub fn move_active_pane_right(&mut self, client_id: ClientId) { + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + if let Some(active_pane_id) = self.active_panes.get(&client_id) { + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.move_pane_right(&active_pane_id); + self.set_force_render(); + } + } + pub 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(); + let next_active_pane = self.panes.keys().next().copied(); + for (client_id, active_pane_id) in active_panes { + if active_pane_id == pane_id { + match next_active_pane { + Some(next_active_pane) => { + self.active_panes.insert(client_id, next_active_pane); + self.focus_pane(next_active_pane, client_id); + } + None => { + self.defocus_pane(pane_id, client_id); + } + } + } + } + } + pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { + self.active_panes.insert(client_id, pane_id); + self.z_indices.retain(|p_id| *p_id != pane_id); + self.z_indices.push(pane_id); + self.set_force_render(); + } + pub fn defocus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { + self.z_indices.retain(|p_id| *p_id != pane_id); + self.active_panes.remove(&client_id); + self.set_force_render(); + } + pub fn get_pane_id_at(&self, point: &Position, search_selectable: bool) -> Option { + if search_selectable { + // TODO: better - loop through z-indices and check each one if it contains the point + let mut selectable_panes: Vec<_> = + self.panes.iter().filter(|(_, p)| p.selectable()).collect(); + selectable_panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| { + self.z_indices + .iter() + .position(|id| id == *b_id) + .unwrap() + .cmp(&self.z_indices.iter().position(|id| id == *a_id).unwrap()) + }); + selectable_panes + .iter() + .find(|(_, p)| p.contains(point)) + .map(|(&id, _)| id) + } else { + let mut panes: Vec<_> = self.panes.iter().collect(); + panes.sort_by(|(a_id, _a_pane), (b_id, _b_pane)| { + self.z_indices + .iter() + .position(|id| id == *b_id) + .unwrap() + .cmp(&self.z_indices.iter().position(|id| id == *a_id).unwrap()) + }); + panes + .iter() + .find(|(_, p)| p.contains(point)) + .map(|(&id, _)| id) + } + } + pub fn get_pane_at_mut( + &mut self, + position: &Position, + search_selectable: bool, + ) -> Option<&mut Box> { + self.get_pane_id_at(position, search_selectable) + .and_then(|pane_id| self.panes.get_mut(&pane_id)) + } + pub fn set_pane_being_moved_with_mouse(&mut self, pane_id: PaneId, position: Position) { + self.pane_being_moved_with_mouse = Some((pane_id, position)); + } + pub fn pane_is_being_moved_with_mouse(&self) -> bool { + self.pane_being_moved_with_mouse.is_some() + } + pub fn move_pane_to_position(&mut self, click_position: &Position) -> bool { + // true => changed position + let display_area = *self.display_area.borrow(); + let viewport = *self.viewport.borrow(); + let (pane_id, previous_position) = self.pane_being_moved_with_mouse.unwrap(); + if click_position == &previous_position { + return false; + } + let move_x_by = click_position.column() as isize - previous_position.column() as isize; + let move_y_by = click_position.line() as isize - previous_position.line() as isize; + let mut floating_pane_grid = FloatingPaneGrid::new( + &mut self.panes, + &mut self.desired_pane_positions, + display_area, + viewport, + ); + floating_pane_grid.move_pane_by(pane_id, move_x_by, move_y_by); + self.set_pane_being_moved_with_mouse(pane_id, click_position.clone()); + self.set_force_render(); + true + } + pub fn move_pane_with_mouse(&mut self, position: Position, search_selectable: bool) -> bool { + // true => handled, false => not handled (eg. no pane at this position) + let show_panes = self.show_panes; + if self.pane_being_moved_with_mouse.is_some() { + self.move_pane_to_position(&position); + self.set_force_render(); + return true; + } else if let Some(pane) = self.get_pane_at_mut(&position, search_selectable) { + let clicked_on_frame = pane.position_is_on_frame(&position); + if show_panes && clicked_on_frame { + let pid = pane.pid(); + if self.pane_being_moved_with_mouse.is_none() { + self.set_pane_being_moved_with_mouse(pid, position.clone()); + } + self.move_pane_to_position(&position); + self.set_force_render(); + return true; + } + }; + return false; + } + pub fn stop_moving_pane_with_mouse(&mut self, position: Position) { + if self.pane_being_moved_with_mouse.is_some() { + self.move_pane_to_position(&position); + self.set_force_render(); + }; + self.pane_being_moved_with_mouse = None; + } +} diff --git a/zellij-server/src/panes/grid.rs b/zellij-server/src/panes/grid.rs index ab472aea..11a77bfd 100644 --- a/zellij-server/src/panes/grid.rs +++ b/zellij-server/src/panes/grid.rs @@ -1,3 +1,5 @@ +use std::cell::RefCell; +use std::rc::Rc; use unicode_width::UnicodeWidthChar; use std::{ @@ -20,6 +22,7 @@ use vte::{Params, Perform}; use zellij_tile::data::{Palette, PaletteColor}; use zellij_utils::{consts::VERSION, shared::version_number}; +use crate::output::{CharacterChunk, OutputBuffer}; use crate::panes::alacritty_functions::{parse_number, xparse_color}; use crate::panes::link_handler::LinkHandler; use crate::panes::selection::Selection; @@ -267,107 +270,6 @@ fn subtract_isize_from_usize(u: usize, i: isize) -> usize { } } -#[derive(Debug)] -pub struct CharacterChunk { - pub terminal_characters: Vec, - pub x: usize, - pub y: usize, -} - -#[derive(Clone, Debug)] -pub struct OutputBuffer { - changed_lines: Vec, // line index - should_update_all_lines: bool, -} - -impl Default for OutputBuffer { - fn default() -> Self { - OutputBuffer { - changed_lines: vec![], - should_update_all_lines: true, // first time we should do a full render - } - } -} - -impl OutputBuffer { - pub fn update_line(&mut self, line_index: usize) { - if !self.should_update_all_lines { - self.changed_lines.push(line_index); - } - } - pub fn update_all_lines(&mut self) { - self.clear(); - self.should_update_all_lines = true; - } - pub fn clear(&mut self) { - self.changed_lines.clear(); - self.should_update_all_lines = false; - } - pub fn changed_chunks_in_viewport( - &self, - viewport: &[Row], - viewport_width: usize, - viewport_height: usize, - ) -> Vec { - if self.should_update_all_lines { - let mut changed_chunks = Vec::with_capacity(viewport.len()); - for line_index in 0..viewport_height { - let terminal_characters = - self.extract_line_from_viewport(line_index, viewport, viewport_width); - changed_chunks.push(CharacterChunk { - x: 0, - y: line_index, - terminal_characters, - }); - } - changed_chunks - } else { - let mut line_changes = self.changed_lines.to_vec(); - line_changes.sort_unstable(); - line_changes.dedup(); - let mut changed_chunks = Vec::with_capacity(line_changes.len()); - for line_index in line_changes { - let terminal_characters = - self.extract_line_from_viewport(line_index, viewport, viewport_width); - changed_chunks.push(CharacterChunk { - x: 0, - y: line_index, - terminal_characters, - }); - } - changed_chunks - } - } - fn extract_characters_from_row( - &self, - row: &Row, - viewport_width: usize, - ) -> Vec { - let mut terminal_characters: Vec = row.columns.iter().copied().collect(); - // pad row - let row_width = row.width(); - if row_width < viewport_width { - let mut padding = vec![EMPTY_TERMINAL_CHARACTER; viewport_width - row_width]; - terminal_characters.append(&mut padding); - } - terminal_characters - } - fn extract_line_from_viewport( - &self, - line_index: usize, - viewport: &[Row], - viewport_width: usize, - ) -> Vec { - match viewport.get(line_index) { - // TODO: iterator? - Some(row) => self.extract_characters_from_row(row, viewport_width), - None => { - vec![EMPTY_TERMINAL_CHARACTER; viewport_width] - } - } - } -} - #[derive(Clone)] pub struct Grid { lines_above: VecDeque, @@ -401,7 +303,7 @@ pub struct Grid { pub selection: Selection, pub title: Option, pub is_scrolled: bool, - pub link_handler: LinkHandler, + pub link_handler: Rc>, pub ring_bell: bool, scrollback_buffer_lines: usize, } @@ -420,7 +322,12 @@ impl Debug for Grid { } impl Grid { - pub fn new(rows: usize, columns: usize, colors: Palette) -> Self { + pub fn new( + rows: usize, + columns: usize, + colors: Palette, + link_handler: Rc>, + ) -> Self { Grid { lines_above: VecDeque::with_capacity( // .get_or_init() is used instead of .get().unwrap() to prevent @@ -453,7 +360,7 @@ impl Grid { title: None, changed_colors: None, is_scrolled: false, - link_handler: Default::default(), + link_handler, ring_bell: false, scrollback_buffer_lines: 0, } @@ -717,7 +624,7 @@ impl Grid { } } if let Some(trim_at) = trim_at { - line.columns.truncate(trim_at); + line.truncate(trim_at); } } @@ -865,10 +772,14 @@ impl Grid { } lines } - pub fn read_changes(&mut self) -> Vec { - let changes = - self.output_buffer - .changed_chunks_in_viewport(&self.viewport, self.width, self.height); + pub fn read_changes(&mut self, x_offset: usize, y_offset: usize) -> Vec { + let changes = self.output_buffer.changed_chunks_in_viewport( + &self.viewport, + self.width, + self.height, + x_offset, + y_offset, + ); self.output_buffer.clear(); changes } @@ -1056,7 +967,7 @@ impl Grid { pub fn add_character(&mut self, terminal_character: TerminalCharacter) { // TODO: try to separate adding characters from moving the cursors in this function let character_width = terminal_character.width; - if self.cursor.x >= self.width { + if self.cursor.x + character_width > self.width { if self.disable_linewrap { return; } @@ -1095,7 +1006,7 @@ impl Grid { self.viewport.get(y).unwrap().absolute_character_index(x) } pub fn move_cursor_forward_until_edge(&mut self, count: usize) { - let count_to_move = std::cmp::min(count, self.width - (self.cursor.x)); + let count_to_move = std::cmp::min(count, self.width - self.cursor.x); self.cursor.x += count_to_move; } pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) { @@ -1613,7 +1524,8 @@ impl Perform for Grid { if params.len() < 3 { return; } - self.cursor.pending_styles.link_anchor = self.link_handler.dispatch_osc8(params); + self.cursor.pending_styles.link_anchor = + self.link_handler.borrow_mut().dispatch_osc8(params); } // Get/set Foreground, Background, Cursor colors. @@ -2157,6 +2069,7 @@ impl Perform for Grid { pub struct Row { pub columns: VecDeque, pub is_canonical: bool, + width: Option, } impl Debug for Row { @@ -2173,12 +2086,14 @@ impl Row { Row { columns: VecDeque::with_capacity(width), is_canonical: false, + width: None, } } pub fn from_columns(columns: VecDeque) -> Self { Row { columns, is_canonical: false, + width: None, } } pub fn from_rows(mut rows: Vec, width: usize) -> Self { @@ -2194,12 +2109,25 @@ impl Row { } pub fn with_character(mut self, terminal_character: TerminalCharacter) -> Self { self.columns.push_back(terminal_character); + self.width = None; self } pub fn canonical(mut self) -> Self { self.is_canonical = true; self } + pub fn width_cached(&mut self) -> usize { + if self.width.is_some() { + self.width.unwrap() + } else { + let mut width = 0; + for terminal_character in &self.columns { + width += terminal_character.width; + } + self.width = Some(width); + width + } + } pub fn width(&self) -> usize { let mut width = 0; for terminal_character in &self.columns { @@ -2239,15 +2167,18 @@ impl Row { absolute_index } pub fn add_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { - match self.width().cmp(&x) { + match self.width_cached().cmp(&x) { Ordering::Equal => { self.columns.push_back(terminal_character); + // this is unwrapped because this always happens after self.width_cached() + *self.width.as_mut().unwrap() += terminal_character.width; } Ordering::Less => { let width_offset = self.excess_width_until(x); self.columns .resize(x.saturating_sub(width_offset), EMPTY_TERMINAL_CHARACTER); self.columns.push_back(terminal_character); + self.width = None; } Ordering::Greater => { // wide-character-aware index, where each character is counted once @@ -2275,6 +2206,7 @@ impl Row { } _ => {} } + self.width = None; } } } @@ -2291,6 +2223,7 @@ impl Row { self.columns.insert(insert_position, terminal_character); } } + self.width = None; } pub fn replace_character_at(&mut self, terminal_character: TerminalCharacter, x: usize) { // this is much more performant than remove/insert @@ -2303,12 +2236,15 @@ impl Row { self.columns.insert(x, terminal_character); } } + self.width = None; } pub fn replace_columns(&mut self, columns: VecDeque) { self.columns = columns; + self.width = None; } pub fn push(&mut self, terminal_character: TerminalCharacter) { self.columns.push_back(terminal_character); + self.width = None; } pub fn truncate(&mut self, x: usize) { let width_offset = self.excess_width_until(x); @@ -2316,6 +2252,7 @@ impl Row { if truncate_position < self.columns.len() { self.columns.truncate(truncate_position); } + self.width = None; } pub fn position_accounting_for_widechars(&self, x: usize) -> usize { let mut position = x; @@ -2343,9 +2280,11 @@ impl Row { self.columns .truncate(from_position_accounting_for_widechars); self.columns.append(&mut replace_with); + self.width = None; } pub fn append(&mut self, to_append: &mut VecDeque) { self.columns.append(to_append); + self.width = None; } pub fn drain_until(&mut self, x: usize) -> VecDeque { let mut drained_part: VecDeque = VecDeque::new(); @@ -2359,6 +2298,7 @@ impl Row { break; } } + self.width = None; drained_part } pub fn replace_and_pad_beginning(&mut self, to: usize, terminal_character: TerminalCharacter) { @@ -2378,6 +2318,7 @@ impl Row { drop(self.columns.drain(0..=to_position_accounting_for_widechars)); } replace_with.append(&mut self.columns); + self.width = None; self.columns = replace_with; } pub fn replace_beginning_with(&mut self, mut line_part: VecDeque) { @@ -2388,6 +2329,7 @@ impl Row { drop(self.columns.drain(0..line_part.len())); } line_part.append(&mut self.columns); + self.width = None; self.columns = line_part; } pub fn len(&self) -> usize { @@ -2399,6 +2341,7 @@ impl Row { pub fn delete_and_return_character(&mut self, x: usize) -> Option { let erase_position = self.absolute_character_index(x); if erase_position < self.columns.len() { + self.width = None; Some(self.columns.remove(erase_position).unwrap()) // TODO: just return the remove part? } else { None @@ -2426,6 +2369,7 @@ impl Row { if parts.is_empty() { parts.push(self.clone()); } + self.width = None; parts } } diff --git a/zellij-server/src/panes/link_handler.rs b/zellij-server/src/panes/link_handler.rs index c930e228..3de81c3a 100644 --- a/zellij-server/src/panes/link_handler.rs +++ b/zellij-server/src/panes/link_handler.rs @@ -10,7 +10,7 @@ pub struct LinkHandler { link_index: u16, } #[derive(Debug, Clone)] -struct Link { +pub struct Link { id: Option, uri: String, } @@ -49,8 +49,8 @@ impl LinkHandler { } } - pub fn output_osc8(&self, link_anchor: Option) -> String { - link_anchor.map_or("".to_string(), |link| match link { + pub fn output_osc8(&self, link_anchor: Option) -> Option { + link_anchor.map(|link| match link { LinkAnchor::Start(index) => { let link = self.links.get(&index).unwrap(); let id = link @@ -93,7 +93,7 @@ mod tests { } let expected = format!("\u{1b}]8;id=test;http://test.com{}", TERMINATOR); - assert_eq!(link_handler.output_osc8(anchor), expected); + assert_eq!(link_handler.output_osc8(anchor).unwrap(), expected); } #[test] @@ -106,6 +106,6 @@ mod tests { assert_eq!(anchor, Some(LinkAnchor::End)); let expected = format!("\u{1b}]8;;{}", TERMINATOR); - assert_eq!(link_handler.output_osc8(anchor), expected); + assert_eq!(link_handler.output_osc8(anchor).unwrap(), expected); } } diff --git a/zellij-server/src/panes/mod.rs b/zellij-server/src/panes/mod.rs index 7ee5ab62..d3fd0807 100644 --- a/zellij-server/src/panes/mod.rs +++ b/zellij-server/src/panes/mod.rs @@ -1,13 +1,16 @@ mod alacritty_functions; -mod grid; -mod link_handler; +mod floating_panes; +pub mod grid; +pub mod link_handler; mod plugin_pane; -mod selection; -mod terminal_character; +pub mod selection; +pub mod terminal_character; mod terminal_pane; pub use alacritty_functions::*; +pub use floating_panes::*; pub use grid::*; +pub use link_handler::*; pub(crate) use plugin_pane::*; -pub use terminal_character::*; +pub(crate) use terminal_character::*; pub use terminal_pane::*; diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index bebf9ffb..4356ec65 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -3,6 +3,7 @@ use std::sync::mpsc::channel; use std::time::Instant; use std::unimplemented; +use crate::output::CharacterChunk; use crate::panes::PaneId; use crate::pty::VteBytes; use crate::tab::Pane; @@ -134,7 +135,10 @@ impl Pane for PluginPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } - fn render(&mut self, client_id: Option) -> Option { + fn render( + &mut self, + client_id: Option, + ) -> Option<(Vec, Option)> { // this is a bit of a hack but works in a pinch client_id?; let client_id = client_id.unwrap(); @@ -207,7 +211,7 @@ impl Pane for PluginPane { } } } - Some(vte_output) + Some((vec![], Some(vte_output))) // TODO: PluginPanes should have their own grid so that we can return the non-serialized TerminalCharacters and have them participate in the render buffer } else { None } @@ -217,7 +221,7 @@ impl Pane for PluginPane { _client_id: ClientId, frame_params: FrameParams, input_mode: InputMode, - ) -> Option { + ) -> Option<(Vec, Option)> { // FIXME: This is a hack that assumes all fixed-size panes are borderless. This // will eventually need fixing! if self.frame && !(self.geom.rows.is_fixed() || self.geom.cols.is_fixed()) { @@ -328,7 +332,6 @@ impl Pane for PluginPane { unimplemented!(); } fn start_selection(&mut self, start: &Position, client_id: ClientId) { - log::info!("plugin pane send left click plugin instruction"); self.send_plugin_instructions .send(PluginInstruction::Update( Some(self.pid), diff --git a/zellij-server/src/panes/selection.rs b/zellij-server/src/panes/selection.rs index f01cf94c..43b546ef 100644 --- a/zellij-server/src/panes/selection.rs +++ b/zellij-server/src/panes/selection.rs @@ -4,7 +4,7 @@ use zellij_utils::position::Position; // The selection is empty when start == end // it includes the character at start, and everything before end. -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Copy)] pub struct Selection { pub start: Position, pub end: Position, @@ -59,6 +59,16 @@ impl Selection { end.line.0 == row && col < end.column.0 } + pub fn contains_row(&self, row: usize) -> bool { + let row = row as isize; + let (start, end) = if self.start <= self.end { + (self.start, self.end) + } else { + (self.end, self.start) + }; + start.line.0 <= row && end.line.0 >= row + } + pub fn is_empty(&self) -> bool { self.start == self.end } @@ -99,6 +109,13 @@ impl Selection { self.end.line.0 += lines as isize; } } + pub fn offset(mut self, offset_x: usize, offset_y: usize) -> Self { + self.start.line.0 += offset_y as isize; + self.end.line.0 += offset_y as isize; + self.start.column.0 += offset_x; + self.end.column.0 += offset_x; + self + } /// Return an iterator over the line indices, up to max, that are not present in both self and other, /// except for the indices of the first and last line of both self and s2, that are always included. diff --git a/zellij-server/src/panes/terminal_character.rs b/zellij-server/src/panes/terminal_character.rs index 4a56c16b..402c8b31 100644 --- a/zellij-server/src/panes/terminal_character.rs +++ b/zellij-server/src/panes/terminal_character.rs @@ -1,6 +1,7 @@ use std::convert::From; use std::fmt::{self, Debug, Display, Formatter}; use std::ops::{Index, IndexMut}; +use unicode_width::UnicodeWidthChar; use zellij_utils::vte::ParamsIter; @@ -704,6 +705,16 @@ pub struct TerminalCharacter { pub width: usize, } +impl TerminalCharacter { + pub fn new(character: char) -> Self { + TerminalCharacter { + character, + styles: CharacterStyles::default(), + width: character.width().unwrap_or(0), + } + } +} + impl ::std::fmt::Debug for TerminalCharacter { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { write!(f, "{}", self.character) diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index 728716b2..ee6fb559 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -1,16 +1,17 @@ -use crate::panes::AnsiCode; +use crate::output::CharacterChunk; use crate::panes::{ grid::Grid, - terminal_character::{ - CharacterStyles, CursorShape, TerminalCharacter, EMPTY_TERMINAL_CHARACTER, - }, + terminal_character::{CursorShape, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}, }; +use crate::panes::{AnsiCode, LinkHandler}; use crate::pty::VteBytes; use crate::tab::Pane; use crate::ClientId; +use std::cell::RefCell; use std::collections::{HashMap, HashSet}; -use std::fmt::{Debug, Write}; +use std::fmt::Debug; use std::os::unix::io::RawFd; +use std::rc::Rc; use std::time::{self, Instant}; use zellij_utils::pane_size::Offset; use zellij_utils::{ @@ -184,102 +185,42 @@ impl Pane for TerminalPane { fn set_selectable(&mut self, selectable: bool) { self.selectable = selectable; } - fn render(&mut self, _client_id: Option) -> Option { - // we don't use client_id because terminal panes render the same for all users + fn render( + &mut self, + _client_id: Option, + ) -> Option<(Vec, Option)> { if self.should_render() { - let mut vte_output = String::new(); - let mut character_styles = CharacterStyles::new(); + let mut raw_vte_output = String::new(); let content_x = self.get_content_x(); let content_y = self.get_content_y(); - if self.grid.clear_viewport_before_rendering { - for line_index in 0..self.grid.height { - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m", - content_y + line_index + 1, - content_x + 1 - ) - .unwrap(); // goto row/col and reset styles - for _col_index in 0..self.grid.width { - vte_output.push(EMPTY_TERMINAL_CHARACTER.character); - } + + let mut character_chunks = self.grid.read_changes(content_x, content_y); + for character_chunk in character_chunks.iter_mut() { + character_chunk.add_changed_colors(self.grid.changed_colors); + if self + .grid + .selection + .contains_row(character_chunk.y.saturating_sub(content_y)) + { + let background_color = match self.colors.bg { + PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb), + PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col), + }; + character_chunk.add_selection_and_background( + self.grid.selection, + background_color, + content_x, + content_y, + ); } - self.grid.clear_viewport_before_rendering = false; - } - // here we clear the previous cursor locations by adding an empty style-less character - // in their location, this is done before the main rendering logic so that if there - // actually is another character there, it will be overwritten - for (y, x) in self.fake_cursor_locations.drain() { - // we need to make sure to update the line in the line buffer so that if there's - // another character there it'll override it and we won't create holes with our - // empty character - self.grid.update_line_for_rendering(y); - let x = content_x + x; - let y = content_y + y; - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - y + 1, - x + 1, - EMPTY_TERMINAL_CHARACTER.character - ) - .unwrap(); - } - let max_width = self.get_content_columns(); - for character_chunk in self.grid.read_changes() { - let pane_x = self.get_content_x(); - let pane_y = self.get_content_y(); - let chunk_absolute_x = pane_x + character_chunk.x; - let chunk_absolute_y = pane_y + character_chunk.y; - let terminal_characters = character_chunk.terminal_characters; - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m", - chunk_absolute_y + 1, - chunk_absolute_x + 1 - ) - .unwrap(); // goto row/col and reset styles - - let mut chunk_width = character_chunk.x; - for mut t_character in terminal_characters { - // adjust the background of currently selected characters - // doing it here is much easier than in grid - if self.grid.selection.contains(character_chunk.y, chunk_width) { - let color = match self.colors.bg { - PaletteColor::Rgb(rgb) => AnsiCode::RgbCode(rgb), - PaletteColor::EightBit(col) => AnsiCode::ColorIndex(col), - }; - - t_character.styles = t_character.styles.background(Some(color)); - } - chunk_width += t_character.width; - if chunk_width > max_width { - break; - } - - if let Some(new_styles) = character_styles - .update_and_return_diff(&t_character.styles, self.grid.changed_colors) - { - write!( - &mut vte_output, - "{}{}", - new_styles, - self.grid.link_handler.output_osc8(new_styles.link_anchor) - ) - .unwrap(); - } - - vte_output.push(t_character.character); - } - character_styles.clear(); } if self.grid.ring_bell { let ring_bell = '\u{7}'; - vte_output.push(ring_bell); + raw_vte_output.push(ring_bell); self.grid.ring_bell = false; } self.set_should_render(false); - Some(vte_output) + Some((character_chunks, Some(raw_vte_output))) } else { None } @@ -289,9 +230,8 @@ impl Pane for TerminalPane { client_id: ClientId, frame_params: FrameParams, input_mode: InputMode, - ) -> Option { + ) -> Option<(Vec, Option)> { // TODO: remove the cursor stuff from here - let mut vte_output = None; let pane_title = if self.pane_name.is_empty() && input_mode == InputMode::RenamePane && frame_params.is_main_client @@ -316,18 +256,24 @@ impl Pane for TerminalPane { Some(last_frame) => { if &frame != last_frame { if !self.borderless { - vte_output = Some(frame.render()); + let frame_output = frame.render(); + self.frame.insert(client_id, frame); + Some(frame_output) + } else { + None } - self.frame.insert(client_id, frame); + } else { + None } - vte_output } None => { if !self.borderless { - vte_output = Some(frame.render()); + let frame_output = frame.render(); + self.frame.insert(client_id, frame); + Some(frame_output) + } else { + None } - self.frame.insert(client_id, frame); - vte_output } } } @@ -452,7 +398,7 @@ impl Pane for TerminalPane { self.grid.pending_messages_to_pty.drain(..).collect() } - fn start_selection(&mut self, start: &Position, client_id: ClientId) { + fn start_selection(&mut self, start: &Position, _client_id: ClientId) { self.grid.start_selection(start); self.set_should_render(true); } @@ -511,12 +457,14 @@ impl TerminalPane { palette: Palette, pane_index: usize, pane_name: String, + link_handler: Rc>, ) -> TerminalPane { let initial_pane_title = format!("Pane #{}", pane_index); let grid = Grid::new( position_and_size.rows.as_usize(), position_and_size.cols.as_usize(), palette, + link_handler.clone(), ); TerminalPane { frame: HashMap::new(), diff --git a/zellij-server/src/panes/unit/grid_tests.rs b/zellij-server/src/panes/unit/grid_tests.rs index 57e97725..9586352e 100644 --- a/zellij-server/src/panes/unit/grid_tests.rs +++ b/zellij-server/src/panes/unit/grid_tests.rs @@ -1,5 +1,8 @@ use super::super::Grid; +use crate::panes::link_handler::LinkHandler; use ::insta::assert_snapshot; +use std::cell::RefCell; +use std::rc::Rc; use zellij_utils::{position::Position, vte, zellij_tile::data::Palette}; fn read_fixture(fixture_name: &str) -> Vec { @@ -15,7 +18,12 @@ fn read_fixture(fixture_name: &str) -> Vec { #[test] fn vttest1_0() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-0"; let content = read_fixture(fixture_name); for byte in content { @@ -27,7 +35,12 @@ fn vttest1_0() { #[test] fn vttest1_1() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-1"; let content = read_fixture(fixture_name); for byte in content { @@ -39,7 +52,12 @@ fn vttest1_1() { #[test] fn vttest1_2() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-2"; let content = read_fixture(fixture_name); for byte in content { @@ -51,7 +69,12 @@ fn vttest1_2() { #[test] fn vttest1_3() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-3"; let content = read_fixture(fixture_name); for byte in content { @@ -63,7 +86,12 @@ fn vttest1_3() { #[test] fn vttest1_4() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-4"; let content = read_fixture(fixture_name); for byte in content { @@ -75,7 +103,12 @@ fn vttest1_4() { #[test] fn vttest1_5() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest1-5"; let content = read_fixture(fixture_name); for byte in content { @@ -87,7 +120,12 @@ fn vttest1_5() { #[test] fn vttest2_0() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-0"; let content = read_fixture(fixture_name); for byte in content { @@ -99,7 +137,12 @@ fn vttest2_0() { #[test] fn vttest2_1() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-1"; let content = read_fixture(fixture_name); for byte in content { @@ -111,7 +154,12 @@ fn vttest2_1() { #[test] fn vttest2_2() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-2"; let content = read_fixture(fixture_name); for byte in content { @@ -123,7 +171,12 @@ fn vttest2_2() { #[test] fn vttest2_3() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-3"; let content = read_fixture(fixture_name); for byte in content { @@ -135,7 +188,12 @@ fn vttest2_3() { #[test] fn vttest2_4() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-4"; let content = read_fixture(fixture_name); for byte in content { @@ -147,7 +205,12 @@ fn vttest2_4() { #[test] fn vttest2_5() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-5"; let content = read_fixture(fixture_name); for byte in content { @@ -159,7 +222,12 @@ fn vttest2_5() { #[test] fn vttest2_6() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-6"; let content = read_fixture(fixture_name); for byte in content { @@ -171,7 +239,12 @@ fn vttest2_6() { #[test] fn vttest2_7() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-7"; let content = read_fixture(fixture_name); for byte in content { @@ -183,7 +256,12 @@ fn vttest2_7() { #[test] fn vttest2_8() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-8"; let content = read_fixture(fixture_name); for byte in content { @@ -195,7 +273,12 @@ fn vttest2_8() { #[test] fn vttest2_9() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-9"; let content = read_fixture(fixture_name); for byte in content { @@ -207,7 +290,12 @@ fn vttest2_9() { #[test] fn vttest2_10() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-10"; let content = read_fixture(fixture_name); for byte in content { @@ -219,7 +307,12 @@ fn vttest2_10() { #[test] fn vttest2_11() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-11"; let content = read_fixture(fixture_name); for byte in content { @@ -231,7 +324,12 @@ fn vttest2_11() { #[test] fn vttest2_12() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-12"; let content = read_fixture(fixture_name); for byte in content { @@ -243,7 +341,12 @@ fn vttest2_12() { #[test] fn vttest2_13() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-13"; let content = read_fixture(fixture_name); for byte in content { @@ -255,7 +358,12 @@ fn vttest2_13() { #[test] fn vttest2_14() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest2-14"; let content = read_fixture(fixture_name); for byte in content { @@ -267,7 +375,12 @@ fn vttest2_14() { #[test] fn vttest3_0() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(41, 110, Palette::default()); + let mut grid = Grid::new( + 41, + 110, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest3-0"; let content = read_fixture(fixture_name); for byte in content { @@ -279,7 +392,12 @@ fn vttest3_0() { #[test] fn vttest8_0() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-0"; let content = read_fixture(fixture_name); for byte in content { @@ -291,7 +409,12 @@ fn vttest8_0() { #[test] fn vttest8_1() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-1"; let content = read_fixture(fixture_name); for byte in content { @@ -303,7 +426,12 @@ fn vttest8_1() { #[test] fn vttest8_2() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-2"; let content = read_fixture(fixture_name); for byte in content { @@ -315,7 +443,12 @@ fn vttest8_2() { #[test] fn vttest8_3() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-3"; let content = read_fixture(fixture_name); for byte in content { @@ -327,7 +460,12 @@ fn vttest8_3() { #[test] fn vttest8_4() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-4"; let content = read_fixture(fixture_name); for byte in content { @@ -339,7 +477,12 @@ fn vttest8_4() { #[test] fn vttest8_5() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vttest8-5"; let content = read_fixture(fixture_name); for byte in content { @@ -351,7 +494,12 @@ fn vttest8_5() { #[test] fn csi_b() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "csi-b"; let content = read_fixture(fixture_name); for byte in content { @@ -363,7 +511,12 @@ fn csi_b() { #[test] fn csi_capital_i() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "csi-capital-i"; let content = read_fixture(fixture_name); for byte in content { @@ -375,7 +528,12 @@ fn csi_capital_i() { #[test] fn csi_capital_z() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "csi-capital-z"; let content = read_fixture(fixture_name); for byte in content { @@ -387,7 +545,12 @@ fn csi_capital_z() { #[test] fn terminal_reports() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 97, Palette::default()); + let mut grid = Grid::new( + 51, + 97, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "terminal_reports"; let content = read_fixture(fixture_name); for byte in content { @@ -399,7 +562,12 @@ fn terminal_reports() { #[test] fn wide_characters() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters"; let content = read_fixture(fixture_name); for byte in content { @@ -411,7 +579,12 @@ fn wide_characters() { #[test] fn wide_characters_line_wrap() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_line_wrap"; let content = read_fixture(fixture_name); for byte in content { @@ -423,7 +596,12 @@ fn wide_characters_line_wrap() { #[test] fn insert_character_in_line_with_wide_character() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_middle_line_insert"; let content = read_fixture(fixture_name); for byte in content { @@ -435,7 +613,12 @@ fn insert_character_in_line_with_wide_character() { #[test] fn delete_char_in_middle_of_line_with_widechar() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide-chars-delete-middle"; let content = read_fixture(fixture_name); for byte in content { @@ -447,7 +630,12 @@ fn delete_char_in_middle_of_line_with_widechar() { #[test] fn delete_char_in_middle_of_line_with_multiple_widechars() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide-chars-delete-middle-after-multi"; let content = read_fixture(fixture_name); for byte in content { @@ -459,7 +647,12 @@ fn delete_char_in_middle_of_line_with_multiple_widechars() { #[test] fn fish_wide_characters_override_clock() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fish_wide_characters_override_clock"; let content = read_fixture(fixture_name); for byte in content { @@ -471,7 +664,12 @@ fn fish_wide_characters_override_clock() { #[test] fn bash_delete_wide_characters() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "bash_delete_wide_characters"; let content = read_fixture(fixture_name); for byte in content { @@ -483,7 +681,12 @@ fn bash_delete_wide_characters() { #[test] fn delete_wide_characters_before_cursor() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "delete_wide_characters_before_cursor"; let content = read_fixture(fixture_name); for byte in content { @@ -495,7 +698,12 @@ fn delete_wide_characters_before_cursor() { #[test] fn delete_wide_characters_before_cursor_when_cursor_is_on_wide_character() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "delete_wide_characters_before_cursor_when_cursor_is_on_wide_character"; let content = read_fixture(fixture_name); for byte in content { @@ -507,7 +715,12 @@ fn delete_wide_characters_before_cursor_when_cursor_is_on_wide_character() { #[test] fn delete_wide_character_under_cursor() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "delete_wide_character_under_cursor"; let content = read_fixture(fixture_name); for byte in content { @@ -519,7 +732,12 @@ fn delete_wide_character_under_cursor() { #[test] fn replace_wide_character_under_cursor() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 104, Palette::default()); + let mut grid = Grid::new( + 21, + 104, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "replace_wide_character_under_cursor"; let content = read_fixture(fixture_name); for byte in content { @@ -531,7 +749,12 @@ fn replace_wide_character_under_cursor() { #[test] fn wrap_wide_characters() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 90, Palette::default()); + let mut grid = Grid::new( + 21, + 90, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); for byte in content { @@ -543,7 +766,12 @@ fn wrap_wide_characters() { #[test] fn wrap_wide_characters_on_size_change() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 93, Palette::default()); + let mut grid = Grid::new( + 21, + 93, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); for byte in content { @@ -556,7 +784,12 @@ fn wrap_wide_characters_on_size_change() { #[test] fn unwrap_wide_characters_on_size_change() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 93, Palette::default()); + let mut grid = Grid::new( + 21, + 93, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_full"; let content = read_fixture(fixture_name); for byte in content { @@ -570,7 +803,12 @@ fn unwrap_wide_characters_on_size_change() { #[test] fn wrap_wide_characters_in_the_middle_of_the_line() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 91, Palette::default()); + let mut grid = Grid::new( + 21, + 91, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_line_middle"; let content = read_fixture(fixture_name); for byte in content { @@ -582,7 +820,12 @@ fn wrap_wide_characters_in_the_middle_of_the_line() { #[test] fn wrap_wide_characters_at_the_end_of_the_line() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 90, Palette::default()); + let mut grid = Grid::new( + 21, + 90, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "wide_characters_line_end"; let content = read_fixture(fixture_name); for byte in content { @@ -594,7 +837,12 @@ fn wrap_wide_characters_at_the_end_of_the_line() { #[test] fn copy_selected_text_from_viewport() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(27, 125, Palette::default()); + let mut grid = Grid::new( + 27, + 125, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); for byte in content { @@ -614,7 +862,12 @@ fn copy_selected_text_from_viewport() { #[test] fn copy_selected_text_from_lines_above() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(27, 125, Palette::default()); + let mut grid = Grid::new( + 27, + 125, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); for byte in content { @@ -634,7 +887,12 @@ fn copy_selected_text_from_lines_above() { #[test] fn copy_selected_text_from_lines_below() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(27, 125, Palette::default()); + let mut grid = Grid::new( + 27, + 125, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "grid_copy"; let content = read_fixture(fixture_name); for byte in content { @@ -662,7 +920,12 @@ fn copy_selected_text_from_lines_below() { #[test] fn run_bandwhich_from_fish_shell() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fish_and_bandwhich"; let content = read_fixture(fixture_name); for byte in content { @@ -674,7 +937,12 @@ fn run_bandwhich_from_fish_shell() { #[test] fn fish_tab_completion_options() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fish_tab_completion_options"; let content = read_fixture(fixture_name); for byte in content { @@ -691,7 +959,12 @@ pub fn fish_select_tab_completion_options() { // this is not clearly seen in the snapshot because it does not include styles, // but we can see the command line change and the cursor staying in place let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fish_select_tab_completion_options"; let content = read_fixture(fixture_name); for byte in content { @@ -711,7 +984,12 @@ pub fn vim_scroll_region_down() { // this tests also has other steps afterwards that fills the line with the next line in the // file let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vim_scroll_region_down"; let content = read_fixture(fixture_name); for byte in content { @@ -729,7 +1007,12 @@ pub fn vim_ctrl_d() { // end of the scroll region // vim makes sure to fill these empty lines with the rest of the file let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vim_ctrl_d"; let content = read_fixture(fixture_name); for byte in content { @@ -746,7 +1029,12 @@ pub fn vim_ctrl_u() { // this causes the effect of scrolling up X lines (vim replaces the lines with the ones in the // file above the current content) let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vim_ctrl_u"; let content = read_fixture(fixture_name); for byte in content { @@ -758,7 +1046,12 @@ pub fn vim_ctrl_u() { #[test] pub fn htop() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "htop"; let content = read_fixture(fixture_name); for byte in content { @@ -770,7 +1063,12 @@ pub fn htop() { #[test] pub fn htop_scrolling() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "htop_scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -782,7 +1080,12 @@ pub fn htop_scrolling() { #[test] pub fn htop_right_scrolling() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "htop_right_scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -802,7 +1105,12 @@ pub fn vim_overwrite() { // * confirm you would like to change the file by pressing 'y' and then ENTER // * if everything looks fine, this test passed :) let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "vim_overwrite"; let content = read_fixture(fixture_name); for byte in content { @@ -816,7 +1124,12 @@ pub fn clear_scroll_region() { // this is actually a test of 1049h/l (alternative buffer) // @imsnif - the name is a monument to the time I didn't fully understand this mechanism :) let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "clear_scroll_region"; let content = read_fixture(fixture_name); for byte in content { @@ -828,7 +1141,12 @@ pub fn clear_scroll_region() { #[test] pub fn display_tab_characters_properly() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "tab_characters"; let content = read_fixture(fixture_name); for byte in content { @@ -840,7 +1158,12 @@ pub fn display_tab_characters_properly() { #[test] pub fn neovim_insert_mode() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "nvim_insert"; let content = read_fixture(fixture_name); for byte in content { @@ -852,7 +1175,12 @@ pub fn neovim_insert_mode() { #[test] pub fn bash_cursor_linewrap() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 116, Palette::default()); + let mut grid = Grid::new( + 28, + 116, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "bash_cursor_linewrap"; let content = read_fixture(fixture_name); for byte in content { @@ -866,7 +1194,12 @@ pub fn fish_paste_multiline() { // here we paste a multiline command in fish shell, making sure we support it // going up and changing the colors of our line-wrapped pasted text let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 149, Palette::default()); + let mut grid = Grid::new( + 28, + 149, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fish_paste_multiline"; let content = read_fixture(fixture_name); for byte in content { @@ -878,7 +1211,12 @@ pub fn fish_paste_multiline() { #[test] pub fn git_log() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 149, Palette::default()); + let mut grid = Grid::new( + 28, + 149, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "git_log"; let content = read_fixture(fixture_name); for byte in content { @@ -892,7 +1230,12 @@ pub fn git_diff_scrollup() { // this tests makes sure that when we have a git diff that exceeds the screen size // we are able to scroll up let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(28, 149, Palette::default()); + let mut grid = Grid::new( + 28, + 149, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "git_diff_scrollup"; let content = read_fixture(fixture_name); for byte in content { @@ -904,7 +1247,12 @@ pub fn git_diff_scrollup() { #[test] pub fn emacs_longbuf() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(60, 284, Palette::default()); + let mut grid = Grid::new( + 60, + 284, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "emacs_longbuf_tutorial"; let content = read_fixture(fixture_name); for byte in content { @@ -916,7 +1264,12 @@ pub fn emacs_longbuf() { #[test] pub fn top_and_quit() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(56, 235, Palette::default()); + let mut grid = Grid::new( + 56, + 235, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "top_and_quit"; let content = read_fixture(fixture_name); for byte in content { @@ -934,7 +1287,12 @@ pub fn exa_plus_omf_theme() { // over existing on-screen content without deleting it, so we must // convert it to spaces let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(56, 235, Palette::default()); + let mut grid = Grid::new( + 56, + 235, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "exa_plus_omf_theme"; let content = read_fixture(fixture_name); for byte in content { @@ -946,7 +1304,12 @@ pub fn exa_plus_omf_theme() { #[test] pub fn scroll_up() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 50, Palette::default()); + let mut grid = Grid::new( + 10, + 50, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -959,7 +1322,12 @@ pub fn scroll_up() { #[test] pub fn scroll_down() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 50, Palette::default()); + let mut grid = Grid::new( + 10, + 50, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -973,7 +1341,12 @@ pub fn scroll_down() { #[test] pub fn scroll_up_with_line_wraps() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 25, Palette::default()); + let mut grid = Grid::new( + 10, + 25, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -986,7 +1359,12 @@ pub fn scroll_up_with_line_wraps() { #[test] pub fn scroll_down_with_line_wraps() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 25, Palette::default()); + let mut grid = Grid::new( + 10, + 25, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -1000,7 +1378,12 @@ pub fn scroll_down_with_line_wraps() { #[test] pub fn scroll_up_decrease_width_and_scroll_down() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 50, Palette::default()); + let mut grid = Grid::new( + 10, + 50, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -1019,7 +1402,12 @@ pub fn scroll_up_decrease_width_and_scroll_down() { #[test] pub fn scroll_up_increase_width_and_scroll_down() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(10, 25, Palette::default()); + let mut grid = Grid::new( + 10, + 25, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scrolling"; let content = read_fixture(fixture_name); for byte in content { @@ -1038,7 +1426,12 @@ pub fn scroll_up_increase_width_and_scroll_down() { #[test] pub fn move_cursor_below_scroll_region() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(34, 114, Palette::default()); + let mut grid = Grid::new( + 34, + 114, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "move_cursor_below_scroll_region"; let content = read_fixture(fixture_name); for byte in content { @@ -1050,7 +1443,12 @@ pub fn move_cursor_below_scroll_region() { #[test] pub fn insert_wide_characters_in_existing_line() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(21, 86, Palette::default()); + let mut grid = Grid::new( + 21, + 86, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "chinese_characters_line_middle"; let content = read_fixture(fixture_name); for byte in content { @@ -1067,7 +1465,12 @@ pub fn full_screen_scroll_region_and_scroll_up() { // lines to get deleted from the viewport rather // than moving to "lines_above" let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(54, 80, Palette::default()); + let mut grid = Grid::new( + 54, + 80, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "scroll_region_full_screen"; let content = read_fixture(fixture_name); for byte in content { @@ -1082,7 +1485,12 @@ pub fn full_screen_scroll_region_and_scroll_up() { #[test] pub fn ring_bell() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(134, 64, Palette::default()); + let mut grid = Grid::new( + 134, + 64, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "ring_bell"; let content = read_fixture(fixture_name); for byte in content { @@ -1094,7 +1502,12 @@ pub fn ring_bell() { #[test] pub fn alternate_screen_change_size() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(20, 20, Palette::default()); + let mut grid = Grid::new( + 20, + 20, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "alternate_screen_change_size"; let content = read_fixture(fixture_name); for byte in content { @@ -1110,7 +1523,12 @@ pub fn alternate_screen_change_size() { #[test] pub fn fzf_fullscreen() { let mut vte_parser = vte::Parser::new(); - let mut grid = Grid::new(51, 112, Palette::default()); + let mut grid = Grid::new( + 51, + 112, + Palette::default(), + Rc::new(RefCell::new(LinkHandler::new())), + ); let fixture_name = "fzf_fullscreen"; let content = read_fixture(fixture_name); for byte in content { diff --git a/zellij-server/src/panes/unit/terminal_pane_tests.rs b/zellij-server/src/panes/unit/terminal_pane_tests.rs index 059fea05..b5f986aa 100644 --- a/zellij-server/src/panes/unit/terminal_pane_tests.rs +++ b/zellij-server/src/panes/unit/terminal_pane_tests.rs @@ -1,6 +1,9 @@ use super::super::TerminalPane; +use crate::panes::LinkHandler; use crate::tab::Pane; use ::insta::assert_snapshot; +use std::cell::RefCell; +use std::rc::Rc; use zellij_utils::pane_size::PaneGeom; use zellij_utils::zellij_tile::data::Palette; @@ -15,7 +18,14 @@ pub fn scrolling_inside_a_pane() { let pid = 1; let palette = Palette::default(); - let mut terminal_pane = TerminalPane::new(pid, fake_win_size, palette, 0, String::new()); // 0 is the pane index + let mut terminal_pane = TerminalPane::new( + pid, + fake_win_size, + palette, + 0, + String::new(), + Rc::new(RefCell::new(LinkHandler::new())), + ); // 0 is the pane index let mut text_to_fill_pane = String::new(); for i in 0..30 { writeln!(&mut text_to_fill_pane, "\rline {}", i + 1).unwrap(); diff --git a/zellij-server/src/route.rs b/zellij-server/src/route.rs index 4626bc5f..fc14175a 100644 --- a/zellij-server/src/route.rs +++ b/zellij-server/src/route.rs @@ -226,6 +226,21 @@ fn route_action( }; session.senders.send_to_pty(pty_instr).unwrap(); } + Action::TogglePaneEmbedOrFloating => { + session + .senders + .send_to_screen(ScreenInstruction::TogglePaneEmbedOrFloating(client_id)) + .unwrap(); + } + Action::ToggleFloatingPanes => { + session + .senders + .send_to_screen(ScreenInstruction::ToggleFloatingPanes( + client_id, + session.default_shell.clone(), + )) + .unwrap(); + } Action::PaneNameInput(c) => { session .senders diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index 6039c6f6..4eebbeb9 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -8,12 +8,15 @@ use std::str; use zellij_utils::input::options::Clipboard; use zellij_utils::pane_size::Size; -use zellij_utils::{input::layout::Layout, position::Position, zellij_tile}; +use zellij_utils::{ + input::command::TerminalAction, input::layout::Layout, position::Position, zellij_tile, +}; use crate::{ + output::Output, panes::PaneId, pty::{ClientOrTabIndex, PtyInstruction, VteBytes}, - tab::{Output, Tab}, + tab::Tab, thread_bus::Bus, ui::overlay::{Overlay, OverlayWindow, Overlayable}, wasm_vm::PluginInstruction, @@ -32,6 +35,8 @@ pub enum ScreenInstruction { PtyBytes(RawFd, VteBytes), Render, NewPane(PaneId, ClientOrTabIndex), + TogglePaneEmbedOrFloating(ClientId), + ToggleFloatingPanes(ClientId, Option), HorizontalSplit(PaneId, ClientId), VerticalSplit(PaneId, ClientId), WriteCharacter(Vec, ClientId), @@ -101,6 +106,10 @@ impl From<&ScreenInstruction> for ScreenContext { ScreenInstruction::PtyBytes(..) => ScreenContext::HandlePtyBytes, ScreenInstruction::Render => ScreenContext::Render, ScreenInstruction::NewPane(..) => ScreenContext::NewPane, + ScreenInstruction::TogglePaneEmbedOrFloating(..) => { + ScreenContext::TogglePaneEmbedOrFloating + } + ScreenInstruction::ToggleFloatingPanes(..) => ScreenContext::ToggleFloatingPanes, ScreenInstruction::HorizontalSplit(..) => ScreenContext::HorizontalSplit, ScreenInstruction::VerticalSplit(..) => ScreenContext::VerticalSplit, ScreenInstruction::WriteCharacter(..) => ScreenContext::WriteCharacter, @@ -364,7 +373,7 @@ impl Screen { fn close_tab_at_index(&mut self, tab_index: usize) { let mut tab_to_close = self.tabs.remove(&tab_index).unwrap(); - let pane_ids = tab_to_close.get_pane_ids(); + let pane_ids = tab_to_close.get_all_pane_ids(); // below we don't check the result of sending the CloseTab instruction to the pty thread // because this might be happening when the app is closing, at which point the pty thread // has already closed and this would result in an error @@ -429,9 +438,10 @@ impl Screen { for tab_index in tabs_to_close { self.close_tab_at_index(tab_index); } + let serialized_output = output.serialize(); self.bus .senders - .send_to_server(ServerInstruction::Render(Some(output))) + .send_to_server(ServerInstruction::Render(Some(serialized_output))) .unwrap(); } @@ -748,6 +758,30 @@ pub(crate) fn screen_thread_main( screen.render(); } + ScreenInstruction::TogglePaneEmbedOrFloating(client_id) => { + screen + .get_active_tab_mut(client_id) + .unwrap() + .toggle_pane_embed_or_floating(client_id); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + screen.render(); + } + ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => { + screen + .get_active_tab_mut(client_id) + .unwrap() + .toggle_floating_panes(client_id, default_shell); + screen + .bus + .senders + .send_to_server(ServerInstruction::UnblockInputThread) + .unwrap(); + screen.render(); + } ScreenInstruction::HorizontalSplit(pid, client_id) => { screen .get_active_tab_mut(client_id) @@ -1055,7 +1089,7 @@ pub(crate) fn screen_thread_main( } None => { for tab in screen.tabs.values_mut() { - if tab.get_pane_ids().contains(&id) { + if tab.get_all_pane_ids().contains(&id) { tab.close_pane(id); break; } diff --git a/zellij-server/src/tab/clipboard.rs b/zellij-server/src/tab/clipboard.rs index 0529bb00..0eab13a9 100644 --- a/zellij-server/src/tab/clipboard.rs +++ b/zellij-server/src/tab/clipboard.rs @@ -29,9 +29,9 @@ impl ClipboardProvider { Clipboard::Primary => 'c', Clipboard::System => 'c', }; - output.push_str_to_multiple_clients( - &format!("\u{1b}]52;{};{}\u{1b}\\", dest, base64::encode(content)), + output.add_pre_vte_instruction_to_multiple_clients( client_ids, + &format!("\u{1b}]52;{};{}\u{1b}\\", dest, base64::encode(content)), ); } }; diff --git a/zellij-server/src/tab/floating_pane_grid.rs b/zellij-server/src/tab/floating_pane_grid.rs new file mode 100644 index 00000000..c6242d33 --- /dev/null +++ b/zellij-server/src/tab/floating_pane_grid.rs @@ -0,0 +1,1243 @@ +use super::pane_resizer::PaneResizer; +use crate::tab::{MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH}; +use crate::{panes::PaneId, tab::Pane}; +use std::cmp::Ordering; +use std::collections::{HashMap, HashSet}; +use zellij_utils::{ + input::layout::Direction, + pane_size::{Dimension, PaneGeom, Size, Viewport}, +}; + +use std::cell::RefCell; +use std::rc::Rc; + +const RESIZE_INCREMENT_WIDTH: usize = 5; +const RESIZE_INCREMENT_HEIGHT: usize = 2; +const MOVE_INCREMENT_HORIZONTAL: usize = 10; +const MOVE_INCREMENT_VERTICAL: usize = 5; + +const MAX_PANES: usize = 100; + +pub struct FloatingPaneGrid<'a> { + panes: Rc>>>, + desired_pane_positions: Rc>>, + display_area: Size, // includes all panes (including eg. the status bar and tab bar in the default layout) + viewport: Viewport, // includes all non-UI panes +} + +impl<'a> FloatingPaneGrid<'a> { + pub fn new( + panes: impl IntoIterator)>, + desired_pane_positions: &'a mut HashMap, + display_area: Size, + viewport: Viewport, + ) -> Self { + let panes: HashMap<_, _> = panes.into_iter().map(|(p_id, p)| (*p_id, p)).collect(); + FloatingPaneGrid { + panes: Rc::new(RefCell::new(panes)), + desired_pane_positions: Rc::new(RefCell::new(desired_pane_positions)), + display_area, + viewport, + } + } + + pub fn layout(&mut self, direction: Direction, space: usize) -> Result<(), String> { + let mut pane_resizer = PaneResizer::new(self.panes.clone()); + pane_resizer.layout(direction, space) + } + pub fn move_pane_by(&mut self, pane_id: PaneId, x: isize, y: isize) { + // true => succeeded to move, false => failed to move + let new_pane_position = { + let mut panes = self.panes.borrow_mut(); + let pane = panes + .iter_mut() + .find(|(p_id, _p)| **p_id == pane_id) + .unwrap() + .1; + let mut new_pane_position = pane.position_and_size(); + let min_x = self.viewport.x as isize; + let min_y = self.viewport.y as isize; + let max_x = (self.viewport.cols + self.viewport.x) + .saturating_sub(new_pane_position.cols.as_usize()); + let max_y = (self.viewport.rows + self.viewport.y) + .saturating_sub(new_pane_position.rows.as_usize()); + let new_x = std::cmp::max(min_x, new_pane_position.x as isize + x); + let new_x = std::cmp::min(new_x, max_x as isize); + let new_y = std::cmp::max(min_y, new_pane_position.y as isize + y); + let new_y = std::cmp::min(new_y, max_y as isize); + new_pane_position.x = new_x as usize; + new_pane_position.y = new_y as usize; + new_pane_position + }; + self.set_pane_geom(pane_id, new_pane_position); + } + fn set_pane_geom(&mut self, pane_id: PaneId, new_pane_geom: PaneGeom) { + let mut panes = self.panes.borrow_mut(); + let pane = panes + .iter_mut() + .find(|(p_id, _p)| **p_id == pane_id) + .unwrap() + .1; + pane.set_geom(new_pane_geom); + let mut desired_pane_positions = self.desired_pane_positions.borrow_mut(); + desired_pane_positions.insert(pane_id, new_pane_geom); + } + pub fn resize(&mut self, space: Size) { + let mut panes = self.panes.borrow_mut(); + let desired_pane_positions = self.desired_pane_positions.borrow(); + + // account for the difference between the viewport (including non-ui pane items which we + // do not want to override) and the display_area, which is the area we can go over + let display_size_row_difference = self.display_area.rows.saturating_sub(self.viewport.rows); + let display_size_column_difference = + self.display_area.cols.saturating_sub(self.viewport.cols); + + let mut new_viewport = self.viewport; + new_viewport.cols = space.cols.saturating_sub(display_size_column_difference); + new_viewport.rows = space.rows.saturating_sub(display_size_row_difference); + + for (pane_id, pane) in panes.iter_mut() { + let mut new_pane_geom = pane.current_geom(); + let desired_pane_geom = desired_pane_positions.get(pane_id).unwrap(); + let desired_pane_geom_is_inside_viewport = + pane_geom_is_inside_viewport(&new_viewport, &desired_pane_geom); + let pane_is_in_desired_position = new_pane_geom == *desired_pane_geom; + if pane_is_in_desired_position && desired_pane_geom_is_inside_viewport { + continue; + } else if desired_pane_geom_is_inside_viewport { + pane.set_geom(*desired_pane_geom); + } else { + let pane_right_side = new_pane_geom.x + new_pane_geom.cols.as_usize(); + let pane_bottom_side = new_pane_geom.y + new_pane_geom.rows.as_usize(); + let viewport_right_side = new_viewport.x + new_viewport.cols; + let viewport_bottom_side = new_viewport.y + new_viewport.rows; + let excess_width = pane_right_side.saturating_sub(viewport_right_side); + let excess_height = pane_bottom_side.saturating_sub(viewport_bottom_side); + let extra_width = viewport_right_side.saturating_sub(pane_right_side); + let extra_height = viewport_bottom_side.saturating_sub(pane_bottom_side); + + // handle shrink width + if excess_width > 0 && new_pane_geom.x.saturating_sub(excess_width) > new_viewport.x + { + new_pane_geom.x = new_pane_geom.x.saturating_sub(excess_width); + } else if excess_width > 0 + && new_pane_geom.cols.as_usize().saturating_sub(excess_width) + > MIN_TERMINAL_WIDTH + { + new_pane_geom + .cols + .set_inner(new_pane_geom.cols.as_usize().saturating_sub(excess_width)); + } else if excess_width > 0 { + let reduce_x_by = new_pane_geom.x.saturating_sub(new_viewport.x); + let reduced_width = new_pane_geom + .cols + .as_usize() + .saturating_sub(excess_width.saturating_sub(reduce_x_by)); + new_pane_geom.x = new_viewport.x; + new_pane_geom + .cols + .set_inner(std::cmp::max(reduced_width, MIN_TERMINAL_WIDTH)); + } + + // handle shrink height + if excess_height > 0 + && new_pane_geom.y.saturating_sub(excess_height) > new_viewport.y + { + new_pane_geom.y = new_pane_geom.y.saturating_sub(excess_height); + } else if excess_height > 0 + && new_pane_geom.rows.as_usize().saturating_sub(excess_height) + > MIN_TERMINAL_HEIGHT + { + new_pane_geom + .rows + .set_inner(new_pane_geom.rows.as_usize().saturating_sub(excess_height)); + } else if excess_height > 0 { + let reduce_y_by = new_pane_geom.y.saturating_sub(new_viewport.y); + let reduced_height = new_pane_geom + .rows + .as_usize() + .saturating_sub(excess_height.saturating_sub(reduce_y_by)); + new_pane_geom.y = new_viewport.y; + new_pane_geom + .rows + .set_inner(std::cmp::max(reduced_height, MIN_TERMINAL_HEIGHT)); + } + + // handle expand width + if extra_width > 0 { + let max_right_coords = new_viewport.x + new_viewport.cols; + if new_pane_geom.x < desired_pane_geom.x { + if desired_pane_geom.x + new_pane_geom.cols.as_usize() <= max_right_coords { + new_pane_geom.x = desired_pane_geom.x + } else if new_pane_geom.x + new_pane_geom.cols.as_usize() + extra_width + < max_right_coords + { + new_pane_geom.x = new_pane_geom.x + extra_width; + } else { + new_pane_geom.x = + max_right_coords.saturating_sub(new_pane_geom.cols.as_usize()); + } + } + if new_pane_geom.cols.as_usize() < desired_pane_geom.cols.as_usize() { + if new_pane_geom.x + desired_pane_geom.cols.as_usize() <= max_right_coords { + new_pane_geom + .cols + .set_inner(desired_pane_geom.cols.as_usize()); + } else if new_pane_geom.x + new_pane_geom.cols.as_usize() + extra_width + < max_right_coords + { + new_pane_geom + .cols + .set_inner(new_pane_geom.cols.as_usize() + extra_width); + } else { + new_pane_geom.cols.set_inner( + new_pane_geom.cols.as_usize() + + (max_right_coords + - (new_pane_geom.x + new_pane_geom.cols.as_usize())), + ); + } + } + } + + // handle expand height + if extra_height > 0 { + let max_bottom_coords = new_viewport.y + new_viewport.rows; + if new_pane_geom.y < desired_pane_geom.y { + if desired_pane_geom.y + new_pane_geom.rows.as_usize() <= max_bottom_coords + { + new_pane_geom.y = desired_pane_geom.y + } else if new_pane_geom.y + new_pane_geom.rows.as_usize() + extra_height + < max_bottom_coords + { + new_pane_geom.y = new_pane_geom.y + extra_height; + } else { + new_pane_geom.y = + max_bottom_coords.saturating_sub(new_pane_geom.rows.as_usize()); + } + } + if new_pane_geom.rows.as_usize() < desired_pane_geom.rows.as_usize() { + if new_pane_geom.y + desired_pane_geom.rows.as_usize() <= max_bottom_coords + { + new_pane_geom + .rows + .set_inner(desired_pane_geom.rows.as_usize()); + } else if new_pane_geom.y + new_pane_geom.rows.as_usize() + extra_height + < max_bottom_coords + { + new_pane_geom + .rows + .set_inner(new_pane_geom.rows.as_usize() + extra_height); + } else { + new_pane_geom.rows.set_inner( + new_pane_geom.rows.as_usize() + + (max_bottom_coords + - (new_pane_geom.y + new_pane_geom.rows.as_usize())), + ); + } + } + } + pane.set_geom(new_pane_geom); + } + } + } + pub fn move_pane_left(&mut self, pane_id: &PaneId) { + if let Some(move_by) = self.can_move_pane_left(pane_id, MOVE_INCREMENT_HORIZONTAL) { + self.move_pane_position_left(pane_id, move_by); + } + } + pub fn move_pane_right(&mut self, pane_id: &PaneId) { + if let Some(move_by) = self.can_move_pane_right(pane_id, MOVE_INCREMENT_HORIZONTAL) { + self.move_pane_position_right(pane_id, move_by); + } + } + pub fn move_pane_down(&mut self, pane_id: &PaneId) { + if let Some(move_by) = self.can_move_pane_down(pane_id, MOVE_INCREMENT_VERTICAL) { + self.move_pane_position_down(pane_id, move_by); + } + } + pub fn move_pane_up(&mut self, pane_id: &PaneId) { + if let Some(move_by) = self.can_move_pane_up(pane_id, MOVE_INCREMENT_VERTICAL) { + self.move_pane_position_up(pane_id, move_by); + } + } + fn can_move_pane_left(&self, pane_id: &PaneId, move_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_until_left_screen_edge = pane.x().saturating_sub(self.viewport.x); + if space_until_left_screen_edge >= move_by { + Some(move_by) + } else if space_until_left_screen_edge > 0 { + Some(space_until_left_screen_edge) + } else { + None + } + } + fn can_move_pane_right(&self, pane_id: &PaneId, move_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_until_right_screen_edge = + (self.viewport.x + self.viewport.cols).saturating_sub(pane.x() + pane.cols()); + if space_until_right_screen_edge >= move_by { + Some(move_by) + } else if space_until_right_screen_edge > 0 { + Some(space_until_right_screen_edge) + } else { + None + } + } + fn can_move_pane_up(&self, pane_id: &PaneId, move_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_until_top_screen_edge = pane.y().saturating_sub(self.viewport.y); + if space_until_top_screen_edge >= move_by { + Some(move_by) + } else if space_until_top_screen_edge > 0 { + Some(space_until_top_screen_edge) + } else { + None + } + } + fn can_move_pane_down(&self, pane_id: &PaneId, move_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_until_bottom_screen_edge = + (self.viewport.y + self.viewport.rows).saturating_sub(pane.y() + pane.rows()); + if space_until_bottom_screen_edge >= move_by { + Some(move_by) + } else if space_until_bottom_screen_edge > 0 { + Some(space_until_bottom_screen_edge) + } else { + None + } + } + fn move_pane_position_left(&mut self, pane_id: &PaneId, move_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(pane_id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.x -= move_by; + current_geom + }; + self.set_pane_geom(*pane_id, new_pane_geom); + } + fn move_pane_position_right(&mut self, pane_id: &PaneId, move_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(pane_id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.x += move_by; + current_geom + }; + self.set_pane_geom(*pane_id, new_pane_geom); + } + fn move_pane_position_down(&mut self, pane_id: &PaneId, move_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(pane_id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.y += move_by; + current_geom + }; + self.set_pane_geom(*pane_id, new_pane_geom); + } + fn move_pane_position_up(&mut self, pane_id: &PaneId, move_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(pane_id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.y -= move_by; + current_geom + }; + self.set_pane_geom(*pane_id, new_pane_geom); + } + pub fn resize_pane_left(&'a mut self, pane_id: &PaneId) { + if let Some(increase_by) = self.can_increase_pane_size_left(pane_id, RESIZE_INCREMENT_WIDTH) + { + self.increase_pane_size_left(pane_id, increase_by); + } else if let Some(decrease_by) = + self.can_decrease_pane_size_left(pane_id, RESIZE_INCREMENT_WIDTH) + { + self.decrease_pane_size_left(pane_id, decrease_by); + } + } + pub fn resize_pane_right(&mut self, pane_id: &PaneId) { + if let Some(increase_by) = + self.can_increase_pane_size_right(pane_id, RESIZE_INCREMENT_WIDTH) + { + self.increase_pane_size_right(pane_id, increase_by); + } else if let Some(decrease_by) = + self.can_decrease_pane_size_right(pane_id, RESIZE_INCREMENT_WIDTH) + { + self.decrease_pane_size_right(pane_id, decrease_by); + } + } + pub fn resize_pane_down(&mut self, pane_id: &PaneId) { + if let Some(increase_by) = + self.can_increase_pane_size_down(pane_id, RESIZE_INCREMENT_HEIGHT) + { + self.increase_pane_size_down(pane_id, increase_by); + } else if let Some(decrease_by) = + self.can_decrease_pane_size_down(pane_id, RESIZE_INCREMENT_HEIGHT) + { + self.decrease_pane_size_down(pane_id, decrease_by); + } + } + pub fn resize_pane_up(&mut self, pane_id: &PaneId) { + if let Some(increase_by) = self.can_increase_pane_size_up(pane_id, RESIZE_INCREMENT_HEIGHT) + { + self.increase_pane_size_up(pane_id, increase_by); + } else if let Some(decrease_by) = + self.can_decrease_pane_size_up(pane_id, RESIZE_INCREMENT_HEIGHT) + { + self.decrease_pane_size_up(pane_id, decrease_by); + } + } + pub fn resize_increase(&mut self, pane_id: &PaneId) { + if let Some(increase_by) = + self.can_increase_pane_size_left(pane_id, RESIZE_INCREMENT_WIDTH / 2) + { + self.increase_pane_size_left(pane_id, increase_by); + } + if let Some(increase_by) = + self.can_increase_pane_size_right(pane_id, RESIZE_INCREMENT_WIDTH / 2) + { + self.increase_pane_size_right(pane_id, increase_by); + } + if let Some(increase_by) = + self.can_increase_pane_size_down(pane_id, RESIZE_INCREMENT_HEIGHT / 2) + { + self.increase_pane_size_down(pane_id, increase_by); + } + if let Some(increase_by) = + self.can_increase_pane_size_up(pane_id, RESIZE_INCREMENT_HEIGHT / 2) + { + self.increase_pane_size_up(pane_id, increase_by); + } + } + pub fn resize_decrease(&mut self, pane_id: &PaneId) { + if let Some(decrease_by) = + self.can_decrease_pane_size_left(pane_id, RESIZE_INCREMENT_WIDTH / 2) + { + self.decrease_pane_size_left(pane_id, decrease_by); + } + if let Some(decrease_by) = + self.can_decrease_pane_size_right(pane_id, RESIZE_INCREMENT_WIDTH / 2) + { + self.decrease_pane_size_right(pane_id, decrease_by); + } + if let Some(decrease_by) = + self.can_decrease_pane_size_down(pane_id, RESIZE_INCREMENT_HEIGHT / 2) + { + self.decrease_pane_size_down(pane_id, decrease_by); + } + if let Some(decrease_by) = + self.can_decrease_pane_size_up(pane_id, RESIZE_INCREMENT_HEIGHT / 2) + { + self.decrease_pane_size_up(pane_id, decrease_by); + } + } + fn can_increase_pane_size_left( + &self, + pane_id: &PaneId, + max_increase_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let distance_to_left_edge = pane.x().saturating_sub(self.viewport.x); + if distance_to_left_edge.saturating_sub(max_increase_by) > 0 { + Some(max_increase_by) + } else if distance_to_left_edge > 0 { + Some(distance_to_left_edge) + } else { + None + } + } + fn can_decrease_pane_size_left( + &self, + pane_id: &PaneId, + max_decrease_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_left_to_decrease = pane.cols().saturating_sub(MIN_TERMINAL_WIDTH); + if space_left_to_decrease.saturating_sub(max_decrease_by) > 0 { + Some(max_decrease_by) + } else if space_left_to_decrease > 0 { + Some(space_left_to_decrease) + } else { + None + } + } + fn can_increase_pane_size_right( + &self, + pane_id: &PaneId, + max_increase_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let distance_to_right_edge = + (self.viewport.x + self.viewport.cols).saturating_sub(pane.x() + pane.cols()); + if pane.x() + pane.cols() + max_increase_by < self.viewport.cols { + Some(max_increase_by) + } else if distance_to_right_edge > 0 { + Some(distance_to_right_edge) + } else { + None + } + } + fn can_decrease_pane_size_right( + &self, + pane_id: &PaneId, + max_decrease_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_left_to_decrease = pane.cols().saturating_sub(MIN_TERMINAL_WIDTH); + let pane_right_edge = pane.x() + pane.cols(); + if space_left_to_decrease.saturating_sub(max_decrease_by) > 0 + && pane.x() + max_decrease_by <= pane_right_edge + MIN_TERMINAL_WIDTH + { + Some(max_decrease_by) + } else if space_left_to_decrease > 0 + && pane.x() + max_decrease_by <= pane_right_edge + MIN_TERMINAL_WIDTH + { + Some(space_left_to_decrease) + } else { + None + } + } + fn can_increase_pane_size_down( + &self, + pane_id: &PaneId, + max_increase_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let distance_to_bottom_edge = + (self.viewport.y + self.viewport.rows).saturating_sub(pane.y() + pane.rows()); + if pane.y() + pane.rows() + max_increase_by < self.viewport.rows { + Some(max_increase_by) + } else if distance_to_bottom_edge > 0 { + Some(distance_to_bottom_edge) + } else { + None + } + } + fn can_decrease_pane_size_down( + &self, + pane_id: &PaneId, + max_decrease_by: usize, + ) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_left_to_decrease = pane.rows().saturating_sub(MIN_TERMINAL_HEIGHT); + let pane_bottom_edge = pane.y() + pane.rows(); + if space_left_to_decrease.saturating_sub(max_decrease_by) > 0 + && pane.y() + max_decrease_by <= pane_bottom_edge + MIN_TERMINAL_HEIGHT + { + Some(max_decrease_by) + } else if space_left_to_decrease > 0 + && pane.y() + max_decrease_by <= pane_bottom_edge + MIN_TERMINAL_HEIGHT + { + Some(space_left_to_decrease) + } else { + None + } + } + fn can_increase_pane_size_up(&self, pane_id: &PaneId, max_increase_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let distance_to_top_edge = pane.y().saturating_sub(self.viewport.y); + if distance_to_top_edge.saturating_sub(max_increase_by) > 0 { + Some(max_increase_by) + } else if distance_to_top_edge > 0 { + Some(distance_to_top_edge) + } else { + None + } + } + fn can_decrease_pane_size_up(&self, pane_id: &PaneId, max_decrease_by: usize) -> Option { + let panes = self.panes.borrow(); + let pane = panes.get(pane_id).unwrap(); + let space_left_to_decrease = pane.rows().saturating_sub(MIN_TERMINAL_HEIGHT); + if space_left_to_decrease.saturating_sub(max_decrease_by) > 0 { + Some(max_decrease_by) + } else if space_left_to_decrease > 0 { + Some(space_left_to_decrease) + } else { + None + } + } + fn increase_pane_height(&mut self, id: &PaneId, percent: f64) { + let mut panes = self.panes.borrow_mut(); + let terminal = panes.get_mut(id).unwrap(); + terminal.increase_height(percent); + } + fn increase_pane_width(&mut self, id: &PaneId, percent: f64) { + let mut panes = self.panes.borrow_mut(); + let terminal = panes.get_mut(id).unwrap(); + terminal.increase_width(percent); + } + fn increase_pane_size_left(&mut self, id: &PaneId, increase_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.x -= increase_by; + current_geom + .cols + .set_inner(current_geom.cols.as_usize() + increase_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn decrease_pane_size_left(&mut self, id: &PaneId, decrease_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom + .cols + .set_inner(current_geom.cols.as_usize() - decrease_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn increase_pane_size_right(&mut self, id: &PaneId, increase_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom + .cols + .set_inner(current_geom.cols.as_usize() + increase_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn decrease_pane_size_right(&mut self, id: &PaneId, decrease_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.x += decrease_by; + current_geom + .cols + .set_inner(current_geom.cols.as_usize() - decrease_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn increase_pane_size_down(&mut self, id: &PaneId, increase_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom + .rows + .set_inner(current_geom.rows.as_usize() + increase_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn decrease_pane_size_down(&mut self, id: &PaneId, decrease_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.y += decrease_by; + current_geom + .rows + .set_inner(current_geom.rows.as_usize() - decrease_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn increase_pane_size_up(&mut self, id: &PaneId, increase_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom.y -= increase_by; + current_geom + .rows + .set_inner(current_geom.rows.as_usize() + increase_by); + pane.set_geom(current_geom); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn decrease_pane_size_up(&mut self, id: &PaneId, decrease_by: usize) { + let new_pane_geom = { + let mut panes = self.panes.borrow_mut(); + let pane = panes.get_mut(id).unwrap(); + let mut current_geom = pane.position_and_size(); + current_geom + .rows + .set_inner(current_geom.rows.as_usize() - decrease_by); + current_geom + }; + self.set_pane_geom(*id, new_pane_geom); + } + fn pane_ids_directly_left_of(&self, id: &PaneId) -> Option> { + let panes = self.panes.borrow(); + let mut ids = vec![]; + let terminal_to_check = panes.get(id).unwrap(); + if terminal_to_check.x() == 0 { + return None; + } + // for (&pid, terminal) in self.get_panes() { + for (&pid, terminal) in panes.iter() { + if terminal.x() + terminal.cols() == terminal_to_check.x() { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_right_of(&self, id: &PaneId) -> Option> { + let mut ids = vec![]; + let panes = self.panes.borrow(); + let terminal_to_check = panes.get(id).unwrap(); + // for (&pid, terminal) in self.get_panes() { + for (&pid, terminal) in panes.iter() { + if terminal.x() == terminal_to_check.x() + terminal_to_check.cols() { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_below(&self, id: &PaneId) -> Option> { + let mut ids = vec![]; + let panes = self.panes.borrow(); + let terminal_to_check = panes.get(id).unwrap(); + // for (&pid, terminal) in self.get_panes() { + for (&pid, terminal) in panes.iter() { + if terminal.y() == terminal_to_check.y() + terminal_to_check.rows() { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_ids_directly_above(&self, id: &PaneId) -> Option> { + let mut ids = vec![]; + let panes = self.panes.borrow(); + let terminal_to_check = panes.get(id).unwrap(); + // for (&pid, terminal) in self.get_panes() { + for (&pid, terminal) in panes.iter() { + if terminal.y() + terminal.rows() == terminal_to_check.y() { + ids.push(pid); + } + } + if ids.is_empty() { + None + } else { + Some(ids) + } + } + fn pane_is_between_vertical_borders( + &self, + id: &PaneId, + left_border_x: usize, + right_border_x: usize, + ) -> bool { + let panes = self.panes.borrow(); + let pane = panes.get(id).unwrap(); + pane.x() >= left_border_x && pane.x() + pane.cols() <= right_border_x + } + fn pane_is_between_horizontal_borders( + &self, + id: &PaneId, + top_border_y: usize, + bottom_border_y: usize, + ) -> bool { + let panes = self.panes.borrow(); + let pane = panes.get(id).unwrap(); + pane.y() >= top_border_y && pane.y() + pane.rows() <= bottom_border_y + } + pub fn next_selectable_pane_id(&self, current_pane_id: &PaneId) -> PaneId { + let panes = self.panes.borrow(); + let mut panes: Vec<(&PaneId, &&mut Box)> = + panes.iter().filter(|(_, p)| p.selectable()).collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == current_pane_id) // TODO: better + .unwrap(); + + let next_active_pane_id = panes + .get(active_pane_position + 1) + .or_else(|| panes.get(0)) + .map(|p| *p.0) + .unwrap(); + next_active_pane_id + } + pub fn previous_selectable_pane_id(&self, current_pane_id: &PaneId) -> PaneId { + let panes = self.panes.borrow(); + let mut panes: Vec<(&PaneId, &&mut Box)> = + panes.iter().filter(|(_, p)| p.selectable()).collect(); + panes.sort_by(|(_a_id, a_pane), (_b_id, b_pane)| { + if a_pane.y() == b_pane.y() { + a_pane.x().cmp(&b_pane.x()) + } else { + a_pane.y().cmp(&b_pane.y()) + } + }); + let last_pane = panes.last().unwrap(); + let active_pane_position = panes + .iter() + .position(|(id, _)| *id == current_pane_id) // TODO: better + .unwrap(); + + let previous_active_pane_id = if active_pane_position == 0 { + *last_pane.0 + } else { + *panes.get(active_pane_position - 1).unwrap().0 + }; + previous_active_pane_id + } + pub fn next_selectable_pane_id_to_the_left(&self, current_pane_id: &PaneId) -> Option { + let panes = self.panes.borrow(); + let current_pane = panes.get(current_pane_id)?; + let panes: Vec<(PaneId, &&mut Box)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + let next_index = panes + .iter() + .enumerate() + .filter(|(_, (_, c))| { + c.is_left_of(Box::as_ref(current_pane)) + && c.horizontally_overlaps_with(Box::as_ref(current_pane)) + }) + .max_by(|(_, (_, a)), (_, (_, b))| { + let x_comparison = a.x().cmp(&b.x()); + match x_comparison { + Ordering::Equal => a.y().cmp(&b.y()), + _ => x_comparison, + } + }) + .map(|(_, (pid, _))| pid) + .copied(); + next_index + } + pub fn next_selectable_pane_id_below(&self, current_pane_id: &PaneId) -> Option { + let panes = self.panes.borrow(); + let current_pane = panes.get(current_pane_id)?; + let panes: Vec<(PaneId, &&mut Box)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + let next_index = panes + .iter() + .enumerate() + .filter(|(_, (_, c))| { + c.is_below(Box::as_ref(current_pane)) + && c.vertically_overlaps_with(Box::as_ref(current_pane)) + }) + .min_by(|(_, (_, a)), (_, (_, b))| { + let y_comparison = a.y().cmp(&b.y()); + match y_comparison { + Ordering::Equal => b.x().cmp(&a.x()), + _ => y_comparison, + } + }) + .map(|(_, (pid, _))| pid) + .copied(); + next_index + } + pub fn next_selectable_pane_id_above(&self, current_pane_id: &PaneId) -> Option { + let panes = self.panes.borrow(); + let current_pane = panes.get(current_pane_id)?; + let panes: Vec<(PaneId, &&mut Box)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + let next_index = panes + .iter() + .enumerate() + .filter(|(_, (_, c))| { + c.is_above(Box::as_ref(current_pane)) + && c.vertically_overlaps_with(Box::as_ref(current_pane)) + }) + .max_by(|(_, (_, a)), (_, (_, b))| { + let y_comparison = a.y().cmp(&b.y()); + match y_comparison { + Ordering::Equal => b.x().cmp(&a.x()), + _ => y_comparison, + } + }) + .map(|(_, (pid, _))| pid) + .copied(); + next_index + } + pub fn next_selectable_pane_id_to_the_right(&self, current_pane_id: &PaneId) -> Option { + let panes = self.panes.borrow(); + let current_pane = panes.get(current_pane_id)?; + let panes: Vec<(PaneId, &&mut Box)> = panes + .iter() + .filter(|(_, p)| p.selectable()) + .map(|(p_id, p)| (*p_id, p)) + .collect(); + let next_index = panes + .iter() + .enumerate() + .filter(|(_, (_, c))| { + c.is_right_of(Box::as_ref(current_pane)) + && c.horizontally_overlaps_with(Box::as_ref(current_pane)) + }) + .min_by(|(_, (_, a)), (_, (_, b))| { + let x_comparison = a.x().cmp(&b.x()); + match x_comparison { + Ordering::Equal => a.y().cmp(&b.y()), + _ => x_comparison, + } + }) + .map(|(_, (pid, _))| pid) + .copied(); + next_index + } + fn horizontal_borders(&self, pane_ids: &[PaneId]) -> HashSet { + pane_ids.iter().fold(HashSet::new(), |mut borders, p| { + let panes = self.panes.borrow(); + let pane = panes.get(p).unwrap(); + borders.insert(pane.y()); + borders.insert(pane.y() + pane.rows()); + borders + }) + } + fn vertical_borders(&self, pane_ids: &[PaneId]) -> HashSet { + pane_ids.iter().fold(HashSet::new(), |mut borders, p| { + let panes = self.panes.borrow(); + let pane = panes.get(p).unwrap(); + borders.insert(pane.x()); + borders.insert(pane.x() + pane.cols()); + borders + }) + } + fn panes_to_the_left_between_aligning_borders(&self, id: PaneId) -> Option> { + let panes = self.panes.borrow(); + if let Some(pane) = panes.get(&id) { + let upper_close_border = pane.y(); + let lower_close_border = pane.y() + pane.rows(); + + if let Some(panes_to_the_left) = self.pane_ids_directly_left_of(&id) { + let mut selectable_panes: Vec<_> = panes_to_the_left + .into_iter() + .filter(|pid| panes.get(pid).unwrap().selectable()) + .collect(); + let pane_borders_to_the_left = self.horizontal_borders(&selectable_panes); + if pane_borders_to_the_left.contains(&upper_close_border) + && pane_borders_to_the_left.contains(&lower_close_border) + { + selectable_panes.retain(|t| { + self.pane_is_between_horizontal_borders( + t, + upper_close_border, + lower_close_border, + ) + }); + return Some(selectable_panes); + } + } + } + None + } + fn panes_to_the_right_between_aligning_borders(&self, id: PaneId) -> Option> { + let panes = self.panes.borrow(); + if let Some(pane) = panes.get(&id) { + let upper_close_border = pane.y(); + let lower_close_border = pane.y() + pane.rows(); + + if let Some(panes_to_the_right) = self.pane_ids_directly_right_of(&id) { + let mut selectable_panes: Vec<_> = panes_to_the_right + .into_iter() + .filter(|pid| panes.get(pid).unwrap().selectable()) + .collect(); + let pane_borders_to_the_right = self.horizontal_borders(&selectable_panes); + if pane_borders_to_the_right.contains(&upper_close_border) + && pane_borders_to_the_right.contains(&lower_close_border) + { + selectable_panes.retain(|t| { + self.pane_is_between_horizontal_borders( + t, + upper_close_border, + lower_close_border, + ) + }); + return Some(selectable_panes); + } + } + } + None + } + fn panes_above_between_aligning_borders(&self, id: PaneId) -> Option> { + let panes = self.panes.borrow(); + if let Some(pane) = panes.get(&id) { + let left_close_border = pane.x(); + let right_close_border = pane.x() + pane.cols(); + + if let Some(panes_above) = self.pane_ids_directly_above(&id) { + let mut selectable_panes: Vec<_> = panes_above + .into_iter() + .filter(|pid| panes.get(pid).unwrap().selectable()) + .collect(); + let pane_borders_above = self.vertical_borders(&selectable_panes); + if pane_borders_above.contains(&left_close_border) + && pane_borders_above.contains(&right_close_border) + { + selectable_panes.retain(|t| { + self.pane_is_between_vertical_borders( + t, + left_close_border, + right_close_border, + ) + }); + return Some(selectable_panes); + } + } + } + None + } + fn panes_below_between_aligning_borders(&self, id: PaneId) -> Option> { + let panes = self.panes.borrow(); + if let Some(pane) = panes.get(&id) { + let left_close_border = pane.x(); + let right_close_border = pane.x() + pane.cols(); + + if let Some(panes_below) = self.pane_ids_directly_below(&id) { + let mut selectable_panes: Vec<_> = panes_below + .into_iter() + .filter(|pid| panes[pid].selectable()) + .collect(); + let pane_borders_below = self.vertical_borders(&selectable_panes); + if pane_borders_below.contains(&left_close_border) + && pane_borders_below.contains(&right_close_border) + { + selectable_panes.retain(|t| { + self.pane_is_between_vertical_borders( + t, + left_close_border, + right_close_border, + ) + }); + return Some(selectable_panes); + } + } + } + None + } + fn find_panes_to_grow(&self, id: PaneId) -> Option<(Vec, Direction)> { + if let Some(panes) = self + .panes_to_the_left_between_aligning_borders(id) + .or_else(|| self.panes_to_the_right_between_aligning_borders(id)) + { + return Some((panes, Direction::Horizontal)); + } + + if let Some(panes) = self + .panes_above_between_aligning_borders(id) + .or_else(|| self.panes_below_between_aligning_borders(id)) + { + return Some((panes, Direction::Vertical)); + } + + None + } + fn grow_panes(&mut self, panes: &[PaneId], direction: Direction, (width, height): (f64, f64)) { + match direction { + Direction::Horizontal => { + for pane_id in panes { + self.increase_pane_width(pane_id, width); + } + } + Direction::Vertical => { + for pane_id in panes { + self.increase_pane_height(pane_id, height); + } + } + }; + } + pub fn fill_space_over_pane(&mut self, id: PaneId) -> bool { + // true => successfully filled space over pane + // false => didn't succeed, so didn't do anything + let (freed_width, freed_height) = { + let panes = self.panes.borrow_mut(); + let pane_to_close = panes.get(&id).unwrap(); + let freed_space = pane_to_close.position_and_size(); + let freed_width = freed_space.cols.as_percent(); + let freed_height = freed_space.rows.as_percent(); + (freed_width, freed_height) + }; + if let (Some(freed_width), Some(freed_height)) = (freed_width, freed_height) { + if let Some((panes_to_grow, direction)) = self.find_panes_to_grow(id) { + self.grow_panes(&panes_to_grow, direction, (freed_width, freed_height)); + let side_length = match direction { + Direction::Vertical => self.display_area.rows, + Direction::Horizontal => self.display_area.cols, + }; + { + let mut panes = self.panes.borrow_mut(); + (*panes).remove(&id); + } + let mut pane_resizer = PaneResizer::new(self.panes.clone()); + let _ = pane_resizer.layout(direction, side_length); + return true; + } + } + false + } + pub fn find_room_for_new_pane(&self) -> Option { + let panes = self.panes.borrow(); + let pane_geoms: Vec = panes.values().map(|p| p.position_and_size()).collect(); + for offset in 0..MAX_PANES / 5 { + let half_size_middle_geom = half_size_middle_geom(&self.viewport, offset); + let half_size_top_left_geom = half_size_top_left_geom(&self.viewport, offset); + let half_size_top_right_geom = half_size_top_right_geom(&self.viewport, offset); + let half_size_bottom_left_geom = half_size_bottom_left_geom(&self.viewport, offset); + let half_size_bottom_right_geom = half_size_bottom_right_geom(&self.viewport, offset); + if pane_geom_is_big_enough(&half_size_middle_geom) + && pane_geom_is_unoccupied_and_inside_viewport( + &self.viewport, + &half_size_middle_geom, + &pane_geoms, + ) + { + return Some(half_size_middle_geom); + } else if pane_geom_is_big_enough(&half_size_top_left_geom) + && pane_geom_is_unoccupied_and_inside_viewport( + &self.viewport, + &half_size_top_left_geom, + &pane_geoms, + ) + { + return Some(half_size_top_left_geom); + } else if pane_geom_is_big_enough(&half_size_top_right_geom) + && pane_geom_is_unoccupied_and_inside_viewport( + &self.viewport, + &half_size_top_right_geom, + &pane_geoms, + ) + { + return Some(half_size_top_right_geom); + } else if pane_geom_is_big_enough(&half_size_bottom_left_geom) + && pane_geom_is_unoccupied_and_inside_viewport( + &self.viewport, + &half_size_bottom_left_geom, + &pane_geoms, + ) + { + return Some(half_size_bottom_left_geom); + } else if pane_geom_is_big_enough(&half_size_bottom_right_geom) + && pane_geom_is_unoccupied_and_inside_viewport( + &self.viewport, + &half_size_bottom_right_geom, + &pane_geoms, + ) + { + return Some(half_size_bottom_right_geom); + } + } + None + } +} + +fn half_size_middle_geom(space: &Viewport, offset: usize) -> PaneGeom { + let mut geom = PaneGeom { + x: space.x + (space.cols as f64 / 4.0).round() as usize + offset, + y: space.y + (space.rows as f64 / 4.0).round() as usize + offset, + cols: Dimension::fixed(space.cols / 2), + rows: Dimension::fixed(space.rows / 2), + }; + geom.cols.set_inner(space.cols / 2); + geom.rows.set_inner(space.rows / 2); + geom +} + +fn half_size_top_left_geom(space: &Viewport, offset: usize) -> PaneGeom { + let mut geom = PaneGeom { + x: space.x + 2 + offset, + y: space.y + 2 + offset, + cols: Dimension::fixed(space.cols / 3), + rows: Dimension::fixed(space.rows / 3), + }; + geom.cols.set_inner(space.cols / 3); + geom.rows.set_inner(space.rows / 3); + geom +} + +fn half_size_top_right_geom(space: &Viewport, offset: usize) -> PaneGeom { + let mut geom = PaneGeom { + x: ((space.x + space.cols) - (space.cols / 3) - 2).saturating_sub(offset), + y: space.y + 2 + offset, + cols: Dimension::fixed(space.cols / 3), + rows: Dimension::fixed(space.rows / 3), + }; + geom.cols.set_inner(space.cols / 3); + geom.rows.set_inner(space.rows / 3); + geom +} + +fn half_size_bottom_left_geom(space: &Viewport, offset: usize) -> PaneGeom { + let mut geom = PaneGeom { + x: space.x + 2 + offset, + y: ((space.y + space.rows) - (space.rows / 3) - 2).saturating_sub(offset), + cols: Dimension::fixed(space.cols / 3), + rows: Dimension::fixed(space.rows / 3), + }; + geom.cols.set_inner(space.cols / 3); + geom.rows.set_inner(space.rows / 3); + geom +} + +fn half_size_bottom_right_geom(space: &Viewport, offset: usize) -> PaneGeom { + let mut geom = PaneGeom { + x: ((space.x + space.cols) - (space.cols / 3) - 2).saturating_sub(offset), + y: ((space.y + space.rows) - (space.rows / 3) - 2).saturating_sub(offset), + cols: Dimension::fixed(space.cols / 3), + rows: Dimension::fixed(space.rows / 3), + }; + geom.cols.set_inner(space.cols / 3); + geom.rows.set_inner(space.rows / 3); + geom +} + +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 +} + +fn pane_geom_is_big_enough(geom: &PaneGeom) -> bool { + geom.rows.as_usize() >= MIN_TERMINAL_HEIGHT && geom.cols.as_usize() >= MIN_TERMINAL_WIDTH +} + +fn pane_geom_is_unoccupied_and_inside_viewport( + viewport: &Viewport, + geom: &PaneGeom, + existing_geoms: &[PaneGeom], +) -> bool { + pane_geom_is_inside_viewport(viewport, geom) + && !existing_geoms.iter().find(|p| *p == geom).is_some() +} diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index c91ac87a..2941ca85 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -3,8 +3,9 @@ mod clipboard; mod copy_command; -mod pane_grid; -mod pane_resizer; +pub mod floating_pane_grid; +pub mod pane_resizer; +pub mod tiled_pane_grid; use copy_command::CopyCommand; use zellij_utils::input::options::Clipboard; @@ -12,12 +13,15 @@ use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde, zellij_tile}; use crate::ui::pane_boundaries_frame::FrameParams; -use pane_grid::{split, PaneGrid}; +use tiled_pane_grid::{split, TiledPaneGrid}; +use self::clipboard::ClipboardProvider; use crate::{ os_input_output::ServerOsApi, - panes::{PaneId, PluginPane, TerminalPane}, - pty::{PtyInstruction, VteBytes}, + 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, @@ -37,17 +41,30 @@ use std::{ 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}, }; -use self::clipboard::ClipboardProvider; +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 -const MIN_TERMINAL_HEIGHT: usize = 5; -const MIN_TERMINAL_WIDTH: usize = 5; +pub const MIN_TERMINAL_HEIGHT: usize = 5; +pub const MIN_TERMINAL_WIDTH: usize = 5; const MAX_PENDING_VTE_EVENTS: usize = 7000; @@ -70,47 +87,17 @@ fn pane_content_offset(position_and_size: &PaneGeom, viewport: &Viewport) -> (us (columns_offset, rows_offset) } -#[derive(Clone, Debug, Default)] -pub struct Output { - pub client_render_instructions: HashMap, -} - -impl Output { - pub fn add_clients(&mut self, client_ids: &HashSet) { - for client_id in client_ids { - self.client_render_instructions - .insert(*client_id, String::new()); - } - } - pub fn push_str_to_multiple_clients( - &mut self, - to_push: &str, - client_ids: impl Iterator, - ) { - for client_id in client_ids { - self.client_render_instructions - .get_mut(&client_id) - .unwrap() - .push_str(to_push) - } - } - pub fn push_to_client(&mut self, client_id: ClientId, to_push: &str) { - if let Some(render_instructions) = self.client_render_instructions.get_mut(&client_id) { - render_instructions.push_str(to_push); - } - } -} - 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: Viewport, // includes all non-UI panes - display_area: Size, // includes all panes (including eg. the status bar and tab bar in the default layout) + 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, @@ -124,7 +111,8 @@ pub(crate) struct Tab { draw_pane_frames: bool, session_is_mirrored: bool, pending_vte_events: HashMap>, - selecting_with_mouse: bool, + 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` @@ -165,13 +153,16 @@ pub trait Pane { 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; + 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; + ) -> Option<(Vec, Option)>; // TODO: better fn render_fake_cursor( &mut self, cursor_color: PaletteColor, @@ -218,15 +209,27 @@ pub trait Pane { 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() } @@ -273,25 +276,19 @@ pub trait Pane { 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) {} } -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, - ); - } - }; -} - impl Tab { // FIXME: Still too many arguments for clippy to be happy... #[allow(clippy::too_many_arguments)] @@ -322,6 +319,10 @@ impl Tab { 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)), @@ -332,11 +333,12 @@ impl Tab { index, position, panes, + floating_panes, name, max_panes, panes_to_hide: HashSet::new(), active_panes: HashMap::new(), - viewport: display_area.into(), + viewport, display_area, fullscreen_is_active: false, synchronize_is_active: false, @@ -352,6 +354,7 @@ impl Tab { connected_clients_in_app, connected_clients, selecting_with_mouse: false, + link_handler: Rc::new(RefCell::new(LinkHandler::new())), clipboard_provider, focus_pane_id: None, } @@ -365,7 +368,14 @@ impl Tab { client_id: ClientId, ) { // TODO: this should be an attribute on Screen instead of full_screen_ws - let free_space = PaneGeom::default(); + 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); @@ -424,6 +434,7 @@ impl Tab { self.colors, next_terminal_position, layout.pane_name.clone().unwrap_or_default(), + self.link_handler.clone(), ); new_pane.set_borderless(layout.borderless); self.panes @@ -441,7 +452,11 @@ impl Tab { } // 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! - self.resize_whole_tab(self.display_area); + 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() @@ -505,9 +520,18 @@ impl Tab { } } pub fn add_client(&mut self, client_id: ClientId, mode_info: Option) { - match self.connected_clients.iter().next() { + 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( @@ -579,47 +603,187 @@ impl Tab { pub fn has_no_connected_clients(&self) -> bool { self.connected_clients.is_empty() } - pub fn new_pane(&mut self, pid: PaneId, client_id: Option) { - self.close_down_to_max_terminals(); - if self.fullscreen_is_active { - self.unset_fullscreen(); - } - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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) + 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 { - let new_terminal = TerminalPane::new( - term_pid, - second_winsize, - self.colors, - next_terminal_position, - String::new(), - ); - 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); + // 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); } } - if let Some(client_id) = client_id { - if self.session_is_mirrored { + } 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); } - } else { - self.active_panes.insert(client_id, pid); } } } @@ -648,6 +812,7 @@ impl Tab { 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)); @@ -691,6 +856,7 @@ impl Tab { 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)); @@ -732,9 +898,14 @@ impl Tab { } 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)) { + 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); @@ -759,12 +930,11 @@ impl Tab { } } fn process_pty_bytes(&mut self, pid: RawFd, bytes: VteBytes) { - // if we don't have the terminal in self.terminals it's probably because - // of a race condition where the terminal was created in pty but has not - // yet been created in Screen. These events are currently not buffered, so - // if you're debugging seemingly randomly missing stdout data, this is - // the reason - if let Some(terminal_output) = self.panes.get_mut(&PaneId::Terminal(pid)) { + 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 { @@ -773,19 +943,28 @@ impl Tab { } } pub fn write_to_terminals_on_current_tab(&mut self, input_bytes: Vec) { - let pane_ids = self.get_pane_ids(); + 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 = self.get_active_pane_id(client_id).unwrap(); + 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.panes.get(&pane_id).unwrap(); + 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) @@ -808,7 +987,17 @@ impl Tab { client_id: ClientId, ) -> Option<(usize, usize)> { // (x, y) - let active_terminal = &self.get_active_pane(client_id)?; + 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)| { @@ -827,7 +1016,7 @@ impl Tab { pane.set_should_render_boundaries(true); } let viewport_pane_ids: Vec<_> = self - .get_pane_ids() + .get_embedded_pane_ids() .into_iter() .filter(|id| !self.is_inside_viewport(id)) .collect(); @@ -839,7 +1028,8 @@ impl Tab { let active_terminal = self.panes.get_mut(active_pane_id).unwrap(); active_terminal.reset_size_and_position_override(); self.set_force_render(); - self.resize_whole_tab(self.display_area); + let display_area = *self.display_area.borrow(); + self.resize_whole_tab(display_area); self.toggle_fullscreen_is_active(); } } @@ -865,7 +1055,7 @@ impl Tab { // 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_pane_ids() + .get_embedded_pane_ids() .into_iter() .filter(|id| !self.is_inside_viewport(id)) .collect(); @@ -874,9 +1064,10 @@ impl Tab { 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: self.viewport.x, - y: self.viewport.y, + x: viewport.x, + y: viewport.y, ..Default::default() }; active_terminal.get_geom_override(full_screen_geom); @@ -886,7 +1077,8 @@ impl Tab { self.active_panes.insert(client_id, active_pane_id); } self.set_force_render(); - self.resize_whole_tab(self.display_area); + let display_area = *self.display_area.borrow(); + self.resize_whole_tab(display_area); self.toggle_fullscreen_is_active(); } } @@ -903,6 +1095,7 @@ impl Tab { 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 @@ -921,7 +1114,7 @@ impl Tab { 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; + let viewport = *self.viewport.borrow(); for pane in self.panes.values_mut() { if !pane.borderless() { pane.set_frame(draw_pane_frames); @@ -945,12 +1138,13 @@ impl Tab { // 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, &self.viewport); + 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 @@ -969,13 +1163,27 @@ impl Tab { return; } self.update_active_panes_in_pty_thread(); - output.add_clients(&self.connected_clients); + 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 = self.active_panes.clone(); + 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)); @@ -985,12 +1193,8 @@ impl Tab { self.colors, &active_panes, multiple_users_exist_in_session, + None, ); - if let PaneId::Terminal(..) = kind { - pane_contents_and_ui.render_pane_contents_to_multiple_clients( - self.connected_clients.iter().copied(), - ); - } for &client_id in &self.connected_clients { let client_mode = self .mode_info @@ -1009,7 +1213,7 @@ impl Tab { } else { let boundaries = client_id_to_boundaries .entry(client_id) - .or_insert_with(|| Boundaries::new(self.viewport)); + .or_insert_with(|| Boundaries::new(*self.viewport.borrow())); pane_contents_and_ui.render_pane_boundaries( client_id, client_mode, @@ -1021,28 +1225,54 @@ impl Tab { // 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 { - output.push_to_client(*client_id, &boundaries.vte_output()); + // 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.push_str_to_all_clients(overlay_vte); - output - .push_str_to_multiple_clients(overlay_vte, self.connected_clients.iter().copied()); + 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.push_str_to_multiple_clients(hide_cursor, self.connected_clients.iter().copied()); + 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.push_str_to_multiple_clients( - clear_display, + output.add_pre_vte_instruction_to_multiple_clients( self.connected_clients.iter().copied(), + clear_display, ); self.should_clear_display_before_rendering = false; } @@ -1060,12 +1290,12 @@ impl Tab { cursor_position_x + 1, change_cursor_shape ); // goto row/col - output.push_to_client(client_id, show_cursor); - output.push_to_client(client_id, goto_cursor_position); + 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.push_to_client(client_id, hide_cursor); + output.add_post_vte_instruction_to_client(client_id, hide_cursor); } } } @@ -1098,10 +1328,16 @@ impl Tab { .copied() } pub fn relayout_tab(&mut self, direction: Direction) { - let mut pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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.cols), - Direction::Vertical => pane_grid.layout(direction, self.display_area.rows), + 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); @@ -1109,44 +1345,75 @@ impl Tab { self.set_pane_frames(self.draw_pane_frames); } pub fn resize_whole_tab(&mut self, new_screen_size: Size) { - 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 = PaneGrid::new(panes, self.display_area, self.viewport); - if pane_grid.layout(Direction::Horizontal, cols).is_ok() { - let column_difference = cols as isize - self.display_area.cols as isize; - // FIXME: Should the viewport be an Offset? - self.viewport.cols = (self.viewport.cols as isize + column_difference) as usize; - self.display_area.cols = cols; - } else { - log::error!("Failed to horizontally resize the tab!!!"); + 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; } - if pane_grid.layout(Direction::Vertical, rows).is_ok() { - let row_difference = rows as isize - self.display_area.rows as isize; - self.viewport.rows = (self.viewport.rows as isize + row_difference) as usize; - self.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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); } - // TODO: can we live without the set_pane_frames we dropped here through layout_tab and in the other - // resize methods? } } 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); @@ -1154,8 +1421,21 @@ impl Tab { } } 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); @@ -1163,8 +1443,21 @@ impl Tab { } } 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); @@ -1172,8 +1465,21 @@ impl Tab { } } 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); @@ -1181,8 +1487,21 @@ impl Tab { } } 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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); @@ -1198,7 +1517,11 @@ impl Tab { return; } let current_active_pane_id = self.get_active_pane_id(client_id).unwrap(); - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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 { @@ -1213,7 +1536,11 @@ impl Tab { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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 { @@ -1228,7 +1555,11 @@ impl Tab { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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 { @@ -1237,246 +1568,282 @@ impl Tab { } // returns a boolean that indicates whether the focus moved pub fn move_focus_left(&mut self, client_id: ClientId) -> bool { - if !self.has_selectable_panes() { - return false; + 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); + } + + 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); + } + } + None => { + // TODO: can this happen? + self.active_panes.clear(); + } + } + + 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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(); + } + 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, p); + self.active_panes.insert(client_id, updated_active_pane); } } else { - self.active_panes.insert(client_id, 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); - } - } - None => { - // TODO: can this happen? - self.active_panes.clear(); - } - } - - false - } - pub fn move_focus_down(&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); - let updated_active_pane = if let Some(active_pane_id) = active_pane_id { - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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); } - } else { - self.active_panes.insert(client_id, updated_active_pane); } - } - None => { - // TODO: can this happen? - self.active_panes.clear(); + None => { + // TODO: can this happen? + self.active_panes.clear(); + } } } } pub fn move_focus_up(&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); - let updated_active_pane = if let Some(active_pane_id) = active_pane_id { - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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), - } + if self.floating_panes.panes_are_visible() { + self.floating_panes + .move_focus_up(client_id, &self.connected_clients); } 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 { + 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); + } + } else { self.active_panes.insert(client_id, updated_active_pane); } - } else { - self.active_panes.insert(client_id, updated_active_pane); } - } - None => { - // TODO: can this happen? - self.active_panes.clear(); + 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.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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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.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); + } + 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, p); + self.active_panes.insert(client_id, updated_active_pane); } } else { - self.active_panes.insert(client_id, 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); } - } else { - self.active_panes.insert(client_id, updated_active_pane); + } + None => { + // TODO: can this happen? + self.active_panes.clear(); } } - None => { - // TODO: can this happen? - self.active_panes.clear(); - } + false } - false } pub fn move_active_pane(&mut self, client_id: ClientId) { if !self.has_selectable_panes() { @@ -1486,7 +1853,11 @@ impl Tab { return; } let active_pane_id = self.get_active_pane_id(client_id).unwrap(); - let pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); + 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(); @@ -1511,152 +1882,188 @@ impl Tab { current_position.set_should_render(true); } pub fn move_active_pane_down(&mut self, client_id: ClientId) { - 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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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(); + 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 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); + 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); } - 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.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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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(); + 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 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); + 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); } - 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.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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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(); + 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 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); + 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); } - 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.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 = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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(); + 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 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); + 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); } - 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_pane_ids(); + let terminals = self.get_embedded_pane_ids(); for &pid in terminals.iter().skip(max_panes - 1) { self.senders .send_to_pty(PtyInstruction::ClosePane(pid)) @@ -1665,9 +2072,20 @@ impl Tab { } } } - pub fn get_pane_ids(&self) -> Vec { + 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); @@ -1697,33 +2115,58 @@ impl Tab { if active_pane_id == pane_id { self.active_panes.insert( client_id, - self.next_active_pane(&self.get_pane_ids()).unwrap(), + self.next_active_pane(&self.get_embedded_pane_ids()) + .unwrap(), ); } } } pub fn close_pane(&mut self, id: PaneId) -> Option> { - if self.fullscreen_is_active { - self.unset_fullscreen(); - } - let mut pane_grid = PaneGrid::new(&mut self.panes, self.display_area, self.viewport); - 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); + 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 { - 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 + 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 @@ -1732,98 +2175,210 @@ impl Tab { } } pub fn scroll_active_terminal_up(&mut self, 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(); - active_terminal.scroll_up(1, client_id); + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .map(|active_pane| active_pane.scroll_up(1, client_id)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .map(|active_pane| active_pane.scroll_up(1, client_id)); } } pub fn scroll_active_terminal_down(&mut self, 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(); - active_terminal.scroll_down(1, client_id); - if !active_terminal.is_scrolled() { - self.process_pending_vte_events(active_terminal_id); - } + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .and_then(|active_pane| { + active_pane.scroll_down(1, client_id); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .and_then(|active_pane| { + active_pane.scroll_down(1, client_id); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); } } pub fn scroll_active_terminal_up_page(&mut self, 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(); - // prevent overflow when row == 0 - let scroll_rows = active_terminal.get_content_rows(); - active_terminal.scroll_up(scroll_rows, client_id); + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .map(|active_pane| { + // prevent overflow when row == 0 + let scroll_rows = active_pane.rows().max(1) - 1; + active_pane.scroll_up(scroll_rows, client_id); + }); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .map(|active_pane| { + // prevent overflow when row == 0 + let scroll_rows = active_pane.get_content_rows(); + active_pane.scroll_up(scroll_rows, client_id); + }); } } pub fn scroll_active_terminal_down_page(&mut self, 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(); - // prevent overflow when row == 0 - let scroll_rows = active_terminal.get_content_rows(); - active_terminal.scroll_down(scroll_rows, client_id); - if !active_terminal.is_scrolled() { - self.process_pending_vte_events(active_terminal_id); - } + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .and_then(|active_pane| { + 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() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .and_then(|active_pane| { + 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() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); } } pub fn scroll_active_terminal_up_half_page(&mut self, 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(); - // prevent overflow when row == 0 - let scroll_rows = (active_terminal.rows().max(1) - 1) / 2; - active_terminal.scroll_up(scroll_rows, client_id); + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .map(|active_pane| { + // prevent overflow when row == 0 + let scroll_rows = (active_pane.rows().max(1) - 1) / 2; + active_pane.scroll_up(scroll_rows, client_id); + }); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .map(|active_pane| { + // 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_terminal_id) = self.get_active_terminal_id(client_id) { - let active_terminal = self - .panes - .get_mut(&PaneId::Terminal(active_terminal_id)) - .unwrap(); - // prevent overflow when row == 0 - let scroll_rows = (active_terminal.rows().max(1) - 1) / 2; - active_terminal.scroll_down(scroll_rows, client_id); - if !active_terminal.is_scrolled() { - self.process_pending_vte_events(active_terminal_id); - } + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .and_then(|active_pane| { + 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() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .and_then(|active_pane| { + 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() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); } } pub fn scroll_active_terminal_to_bottom(&mut self, 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(); - active_terminal.clear_scroll(); - if !active_terminal.is_scrolled() { - self.process_pending_vte_events(active_terminal_id); - } + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .and_then(|active_pane| { + active_pane.clear_scroll(); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .and_then(|active_pane| { + active_pane.clear_scroll(); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); } } pub fn clear_active_terminal_scroll(&mut self, 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(); - active_terminal.clear_scroll(); - if !active_terminal.is_scrolled() { - self.process_pending_vte_events(active_terminal_id); - } + // TODO: is this a thing? + if self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes() { + self.floating_panes + .get_active_pane_mut(client_id) + .and_then(|active_pane| { + active_pane.clear_scroll(); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); + } else { + self.active_panes + .get(&client_id) + .and_then(|active_pane_id| self.panes.get_mut(active_pane_id)) + .and_then(|active_pane| { + active_pane.clear_scroll(); + if !active_pane.is_scrolled() { + if let PaneId::Terminal(raw_fd) = active_pane.pid() { + return Some(raw_fd); + } + } + None + }) + .map(|raw_fd| self.process_pending_vte_events(raw_fd)); } } pub fn scroll_terminal_up(&mut self, point: &Position, lines: usize, client_id: ClientId) { @@ -1846,6 +2401,11 @@ impl Tab { 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 { @@ -1871,6 +2431,17 @@ impl Tab { pub fn handle_left_click(&mut self, position: &Position, client_id: ClientId) { self.focus_pane_at(position, client_id); + let show_floating_panes = self.floating_panes.panes_are_visible(); + if show_floating_panes { + let search_selectable = false; + if 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); @@ -1886,6 +2457,17 @@ impl Tab { }; } 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); + } + return; + } + } if let Some(clicked_pane) = self.get_pane_id_at(point, true) { if self.session_is_mirrored { // move all clients @@ -1897,29 +2479,72 @@ impl Tab { } else { self.active_panes.insert(client_id, 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.floating_panes.panes_are_visible() { + self.floating_panes.stop_moving_pane_with_mouse(*position); + } if !self.selecting_with_mouse { return; } - let active_pane_id = self.get_active_pane_id(client_id); - // on release, get the selected text from the active pane, and reset it's selection let mut selected_text = None; - if active_pane_id != self.get_pane_id_at(position, true) { - if let Some(active_pane_id) = active_pane_id { - if let Some(active_pane) = self.panes.get_mut(&active_pane_id) { - active_pane.end_selection(None, client_id); - selected_text = active_pane.get_selected_text(); - active_pane.reset_selection(); + if self.floating_panes.panes_are_visible() { + let active_pane_id = self + .floating_panes + .active_pane_id(client_id) + .or_else(|| self.active_panes.get(&client_id).copied()); + // on release, get the selected text from the active pane, and reset it's selection + let pane_id_at_position = self + .floating_panes + .get_pane_id_at(position, true) + .or_else(|| self.get_pane_id_at(position, true)); + if active_pane_id != pane_id_at_position { + // release happened outside of pane + if let Some(active_pane_id) = active_pane_id { + if let Some(active_pane) = self + .floating_panes + .get_mut(&active_pane_id) + .or_else(|| self.panes.get_mut(&active_pane_id)) + { + active_pane.end_selection(None, client_id); + selected_text = active_pane.get_selected_text(); + active_pane.reset_selection(); + } } + } else if let Some(pane) = pane_id_at_position.and_then(|pane_id_at_position| { + self.floating_panes + .get_mut(&pane_id_at_position) + .or_else(|| self.panes.get_mut(&pane_id_at_position)) + }) { + // release happened inside of pane + let relative_position = pane.relative_position(position); + pane.end_selection(Some(&relative_position), client_id); + selected_text = pane.get_selected_text(); + pane.reset_selection(); + } + } else { + let active_pane_id = self.active_panes.get(&client_id); + // on release, get the selected text from the active pane, and reset it's selection + if active_pane_id != self.get_pane_id_at(position, true).as_ref() { + if let Some(active_pane_id) = active_pane_id { + if let Some(active_pane) = self.panes.get_mut(&active_pane_id) { + active_pane.end_selection(None, client_id); + selected_text = active_pane.get_selected_text(); + active_pane.reset_selection(); + } + } + } else if let Some(pane) = self.get_pane_at(position, true) { + let relative_position = pane.relative_position(position); + pane.end_selection(Some(&relative_position), client_id); + selected_text = pane.get_selected_text(); + pane.reset_selection(); } - } else if let Some(pane) = self.get_pane_at(position, true) { - let relative_position = pane.relative_position(position); - pane.end_selection(Some(&relative_position), client_id); - selected_text = pane.get_selected_text(); - pane.reset_selection(); } if let Some(selected_text) = selected_text { @@ -1928,7 +2553,26 @@ impl Tab { self.selecting_with_mouse = false; } pub fn handle_mouse_hold(&mut self, position_on_screen: &Position, client_id: ClientId) { - if let Some(active_pane_id) = self.get_active_pane_id(client_id) { + let search_selectable = true; + if self + .floating_panes + .move_pane_with_mouse(*position_on_screen, search_selectable) + { + self.set_force_render(); + } else if self.floating_panes.panes_are_visible() { + let active_pane = self + .floating_panes + .get_active_pane_mut(client_id) + .or_else(|| { + self.active_panes + .get(&client_id) + .and_then(|pane_id| self.panes.get_mut(pane_id)) + }); + active_pane.map(|active_pane| { + let relative_position = active_pane.relative_position(position_on_screen); + active_pane.update_selection(&relative_position, client_id); + }); + } else if let Some(active_pane_id) = self.get_active_pane_id(client_id) { if let Some(active_pane) = self.panes.get_mut(&active_pane_id) { let relative_position = active_pane.relative_position(position_on_screen); active_pane.update_selection(&relative_position, client_id); @@ -1954,17 +2598,17 @@ impl Tab { fn write_selection_to_clipboard(&self, selection: &str) { let mut output = Output::default(); - output.add_clients(&self.connected_clients); + 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(output))) + .send_to_server(ServerInstruction::Render(Some(serialized_output))) .unwrap(); Event::CopyToClipboard(self.clipboard_provider.as_copy_destination()) } @@ -1980,31 +2624,28 @@ impl Tab { 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, self.panes.get(pane_id).unwrap()) + is_inside_viewport(&*self.viewport.borrow(), self.panes.get(pane_id).unwrap()) } fn offset_viewport(&mut self, position_and_size: &Viewport) { - if position_and_size.x == self.viewport.x - && position_and_size.x + position_and_size.cols == self.viewport.x + self.viewport.cols + 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 == self.viewport.y { - self.viewport.y += position_and_size.rows; - self.viewport.rows -= position_and_size.rows; - } else if position_and_size.y + position_and_size.rows - == self.viewport.y + self.viewport.rows - { - self.viewport.rows -= position_and_size.rows; + 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 == self.viewport.y - && position_and_size.y + position_and_size.rows == self.viewport.y + self.viewport.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 == self.viewport.x { - self.viewport.x += position_and_size.cols; - self.viewport.cols -= position_and_size.cols; - } else if position_and_size.x + position_and_size.cols - == self.viewport.x + self.viewport.cols - { - self.viewport.cols -= position_and_size.cols; + 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; } } } @@ -2048,10 +2689,11 @@ impl Tab { } = *point; let line: usize = line.try_into().unwrap(); - line >= self.viewport.y - && column >= self.viewport.x - && line <= self.viewport.y + self.viewport.rows - && column <= self.viewport.x + self.viewport.cols + let viewport = self.viewport.borrow(); + line >= viewport.y + && column >= viewport.x + && line <= viewport.y + viewport.rows + && column <= viewport.x + viewport.cols } } @@ -2063,6 +2705,17 @@ pub fn is_inside_viewport(viewport: &Viewport, pane: &Box) -> bool { <= 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; diff --git a/zellij-server/src/tab/pane_grid.rs b/zellij-server/src/tab/tiled_pane_grid.rs similarity index 99% rename from zellij-server/src/tab/pane_grid.rs rename to zellij-server/src/tab/tiled_pane_grid.rs index 8e32ce48..827c5771 100644 --- a/zellij-server/src/tab/pane_grid.rs +++ b/zellij-server/src/tab/tiled_pane_grid.rs @@ -1,5 +1,5 @@ use super::pane_resizer::PaneResizer; -use crate::tab::is_inside_viewport; +use crate::tab::{is_inside_viewport, MIN_TERMINAL_HEIGHT, MIN_TERMINAL_WIDTH}; use crate::{panes::PaneId, tab::Pane}; use std::cmp::Reverse; use std::collections::{HashMap, HashSet}; @@ -14,26 +14,22 @@ use std::rc::Rc; const RESIZE_PERCENT: f64 = 5.0; const CURSOR_HEIGHT_WIDTH_RATIO: usize = 4; // this is not accurate and kind of a magic number, TODO: look into this -// FIXME: This should be replaced by `RESIZE_PERCENT` at some point -const MIN_TERMINAL_HEIGHT: usize = 5; -const MIN_TERMINAL_WIDTH: usize = 5; type BorderAndPaneIds = (usize, Vec); -pub struct PaneGrid<'a> { - // panes: HashMap<&'a PaneId, &'a mut Box>, +pub struct TiledPaneGrid<'a> { panes: Rc>>>, display_area: Size, // includes all panes (including eg. the status bar and tab bar in the default layout) viewport: Viewport, // includes all non-UI panes } -impl<'a> PaneGrid<'a> { +impl<'a> TiledPaneGrid<'a> { pub fn new( panes: impl IntoIterator)>, display_area: Size, viewport: Viewport, ) -> Self { let panes: HashMap<_, _> = panes.into_iter().map(|(p_id, p)| (*p_id, p)).collect(); - PaneGrid { + TiledPaneGrid { panes: Rc::new(RefCell::new(panes)), display_area, viewport, @@ -1643,7 +1639,6 @@ impl<'a> PaneGrid<'a> { pub fn fill_space_over_pane(&mut self, id: PaneId) -> bool { // true => successfully filled space over pane // false => didn't succeed, so didn't do anything - log::info!("fill_space_over_pane"); let (freed_width, freed_height) = { let panes = self.panes.borrow_mut(); let pane_to_close = panes.get(&id).unwrap(); diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__cannot_float_only_embedded_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__cannot_float_only_embedded_pane.snap new file mode 100644 index 00000000..3d1ba729 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__cannot_float_only_embedded_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ I am an embedded pane │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__decrease_floating_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__decrease_floating_pane_size.snap new file mode 100644 index 00000000..65011a67 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__decrease_floating_pane_size.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ ┌ Pane #2 ─────────────────────────────────────────────┐ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ I am scratch terminal │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ └──────────────────────────────────────────────────────┘ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap new file mode 100644 index 00000000..bab72f60 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__drag_pane_with_mouse.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────────┌ Pane #2 ─────────────────────────────────────────────────┐────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ I am scratch terminal │ │ +12 (C): │ ┌ Pane #2 ─────────────── SCROLL│ │────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__embed_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__embed_floating_pane.snap new file mode 100644 index 00000000..581c2ca3 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__embed_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────┐┌ Pane #2 ─────────────────────────────────────────────────┐ +01 (C): │ ││ │ +02 (C): │ ││ │ +03 (C): │ ││ │ +04 (C): │ ││ I am scratch terminal │ +05 (C): │ ││ │ +06 (C): │ ││ │ +07 (C): │ ││ │ +08 (C): │ ││ │ +09 (C): │ ││ │ +10 (C): │ ││ │ +11 (C): │ ││ │ +12 (C): │ ││ │ +13 (C): │ ││ │ +14 (C): │ ││ │ +15 (C): │ ││ │ +16 (C): │ ││ │ +17 (C): │ ││ │ +18 (C): │ ││ │ +19 (C): └───────────────────────────────────────────────────────────┘└──────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__five_new_floating_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__five_new_floating_panes.snap new file mode 100644 index 00000000..9f4d3777 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__five_new_floating_panes.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__float_embedded_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__float_embedded_pane.snap new file mode 100644 index 00000000..ada8f530 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__float_embedded_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am an embedded pane │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__floating_panes_persist_across_toggles.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__floating_panes_persist_across_toggles.snap new file mode 100644 index 00000000..dd632a95 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__floating_panes_persist_across_toggles.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__increase_floating_pane_size.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__increase_floating_pane_size.snap new file mode 100644 index 00000000..30121d19 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__increase_floating_pane_size.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────────┐ │ +05 (C): │ │ │ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ I am scratch terminal │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ └──────────────────────────────────────────────────────────────┘ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__mark_text_inside_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__mark_text_inside_floating_pane.snap new file mode 100644 index 00000000..38417876 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__mark_text_inside_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ │────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_down.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_down.snap new file mode 100644 index 00000000..96faec6e --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_down.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ │────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└────────────────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_left.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_left.snap new file mode 100644 index 00000000..38417876 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_left.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ │────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_right.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_right.snap new file mode 100644 index 00000000..95f61793 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_right.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ───────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ └──────────────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_up.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_up.snap new file mode 100644 index 00000000..38417876 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_up.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ │────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_with_mouse.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_with_mouse.snap new file mode 100644 index 00000000..38417876 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_floating_pane_focus_with_mouse.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE┌ Pane #2 ─────────────────────────────────────────────────┐EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └───────────────────────────│ │────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SC│ │────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEE└──────────────────────────────────────────────────────────┘EEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_pane_focus_with_mouse_to_non_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_pane_focus_with_mouse_to_non_floating_pane.snap new file mode 100644 index 00000000..1f5098fb --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__move_pane_focus_with_mouse_to_non_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane.snap new file mode 100644 index 00000000..dd632a95 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__new_floating_pane.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_down.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_down.snap new file mode 100644 index 00000000..3a38a934 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_down.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ │ │ │ +15 (C): │ │ │ │ +16 (C): │ └──────────────────────────────────────────────────────────┘ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_left.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_left.snap new file mode 100644 index 00000000..f5831eb8 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_left.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ──────────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └───────────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_right.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_right.snap new file mode 100644 index 00000000..52dffcce --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_right.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ──────────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └───────────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_up.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_up.snap new file mode 100644 index 00000000..6e4b9b6d --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_floating_pane_up.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +04 (C): │ │ │ │ +05 (C): │ │ │ │ +06 (C): │ │ │ │ +07 (C): │ │ I am scratch terminal │ │ +08 (C): │ │ │ │ +09 (C): │ │ │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tab_with_floating_panes.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tab_with_floating_panes.snap new file mode 100644 index 00000000..223045ff --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__resize_tab_with_floating_panes.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ────────────────────┌ Pane #2 ─────────────────────────────────────────────────┐─────────┐ +01 (C): │ │ │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +04 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ I am scrat┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +07 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +08 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +09 (C): └─└──────────────────────────────────────┘──────────────────└──────────────────────────────────────┘ +10 (C): +11 (C): +12 (C): +13 (C): +14 (C): +15 (C): +16 (C): +17 (C): +18 (C): +19 (C): + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically.snap new file mode 100644 index 00000000..7e5a2f06 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ────────────────────┌ Pane #2 ─────────┐ +01 (C): │ │ │ +02 (C): │ ┌ Pane #┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ +03 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +04 (C): │ ┌ Pane #┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ +05 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +06 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +07 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +08 (C): │ │EEEEEEE│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ +09 (C): └─└───────└──────────────────────────────────────┘ +10 (C): +11 (C): +12 (C): +13 (C): +14 (C): +15 (C): +16 (C): +17 (C): +18 (C): +19 (C): + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_back.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_back.snap new file mode 100644 index 00000000..9f4d3777 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_back.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +03 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +04 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +05 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +06 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +07 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ ┌ Pane #2 ─────────────── SCROLL: 0/1 ┐ │ +13 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +14 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│─────────────────────────────────────│EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +15 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +16 (C): │ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │EEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEEE│ │ +17 (C): │ └──────────────────────────────────────┘ └──────────────────────────────────────┘ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_off.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_off.snap new file mode 100644 index 00000000..1f5098fb --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_off.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ │ +06 (C): │ │ +07 (C): │ │ +08 (C): │ │ +09 (C): │ │ +10 (C): │ │ +11 (C): │ │ +12 (C): │ │ +13 (C): │ │ +14 (C): │ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_on.snap b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_on.snap new file mode 100644 index 00000000..dd632a95 --- /dev/null +++ b/zellij-server/src/tab/unit/snapshots/zellij_server__tab__tab_integration_tests__toggle_floating_panes_on.snap @@ -0,0 +1,26 @@ +--- +source: zellij-server/src/tab/./unit/tab_integration_tests.rs +expression: snapshot + +--- +00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐ +01 (C): │ │ +02 (C): │ │ +03 (C): │ │ +04 (C): │ │ +05 (C): │ ┌ Pane #2 ─────────────────────────────────────────────────┐ │ +06 (C): │ │ │ │ +07 (C): │ │ │ │ +08 (C): │ │ │ │ +09 (C): │ │ I am scratch terminal │ │ +10 (C): │ │ │ │ +11 (C): │ │ │ │ +12 (C): │ │ │ │ +13 (C): │ │ │ │ +14 (C): │ └──────────────────────────────────────────────────────────┘ │ +15 (C): │ │ +16 (C): │ │ +17 (C): │ │ +18 (C): │ │ +19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘ + diff --git a/zellij-server/src/tab/unit/tab_integration_tests.rs b/zellij-server/src/tab/unit/tab_integration_tests.rs new file mode 100644 index 00000000..6312035b --- /dev/null +++ b/zellij-server/src/tab/unit/tab_integration_tests.rs @@ -0,0 +1,1058 @@ +use super::{Output, Tab}; +use crate::zellij_tile::data::{ModeInfo, Palette}; +use crate::{ + os_input_output::{AsyncReader, Pid, ServerOsApi}, + panes::PaneId, + thread_bus::ThreadSenders, + ClientId, +}; +use std::convert::TryInto; +use std::path::PathBuf; +use zellij_utils::envs::set_session_name; +use zellij_utils::input::layout::LayoutTemplate; +use zellij_utils::input::options::Clipboard; +use zellij_utils::ipc::IpcReceiverWithContext; +use zellij_utils::pane_size::Size; +use zellij_utils::position::Position; + +use std::cell::RefCell; +use std::collections::HashSet; +use std::os::unix::io::RawFd; +use std::rc::Rc; + +use zellij_utils::nix; + +use zellij_utils::{ + input::command::TerminalAction, + interprocess::local_socket::LocalSocketStream, + ipc::{ClientToServerMsg, ServerToClientMsg}, +}; + +#[derive(Clone)] +struct FakeInputOutput {} + +impl ServerOsApi for FakeInputOutput { + fn set_terminal_size_using_fd(&self, _fd: RawFd, _cols: u16, _rows: u16) { + // noop + } + fn spawn_terminal( + &self, + _file_to_open: TerminalAction, + _quit_cb: Box, + ) -> (RawFd, RawFd) { + unimplemented!() + } + fn read_from_tty_stdout(&self, _fd: RawFd, _buf: &mut [u8]) -> Result { + unimplemented!() + } + fn async_file_reader(&self, _fd: RawFd) -> Box { + unimplemented!() + } + fn write_to_tty_stdin(&self, _fd: RawFd, _buf: &[u8]) -> Result { + unimplemented!() + } + fn tcdrain(&self, _fd: RawFd) -> Result<(), nix::Error> { + unimplemented!() + } + fn kill(&self, _pid: Pid) -> Result<(), nix::Error> { + unimplemented!() + } + fn force_kill(&self, _pid: Pid) -> Result<(), nix::Error> { + unimplemented!() + } + fn box_clone(&self) -> Box { + Box::new((*self).clone()) + } + fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) { + unimplemented!() + } + fn new_client( + &mut self, + _client_id: ClientId, + _stream: LocalSocketStream, + ) -> IpcReceiverWithContext { + unimplemented!() + } + fn remove_client(&mut self, _client_id: ClientId) { + unimplemented!() + } + fn load_palette(&self) -> Palette { + unimplemented!() + } + fn get_cwd(&self, _pid: Pid) -> Option { + unimplemented!() + } +} + +// TODO: move to shared thingy with other test file +fn create_new_tab(size: Size) -> Tab { + set_session_name("test".into()); + let index = 0; + let position = 0; + let name = String::new(); + let os_api = Box::new(FakeInputOutput {}); + let senders = ThreadSenders::default().silently_fail_on_send(); + let max_panes = None; + let mode_info = ModeInfo::default(); + let colors = Palette::default(); + let draw_pane_frames = true; + let client_id = 1; + let session_is_mirrored = true; + let mut connected_clients = HashSet::new(); + connected_clients.insert(client_id); + let connected_clients = Rc::new(RefCell::new(connected_clients)); + let copy_command = None; + let clipboard = Clipboard::default(); + let mut tab = Tab::new( + index, + position, + name, + size, + os_api, + senders, + max_panes, + mode_info, + colors, + draw_pane_frames, + connected_clients, + session_is_mirrored, + client_id, + copy_command, + clipboard, + ); + tab.apply_layout( + LayoutTemplate::default().try_into().unwrap(), + vec![1], + index, + client_id, + ); + tab +} + +use crate::panes::grid::Grid; +use crate::panes::link_handler::LinkHandler; +use ::insta::assert_snapshot; +use zellij_utils::vte; + +fn take_snapshot(ansi_instructions: &str, rows: usize, columns: usize, palette: Palette) -> String { + let mut grid = Grid::new( + rows, + columns, + palette, + Rc::new(RefCell::new(LinkHandler::new())), + ); + let mut vte_parser = vte::Parser::new(); + for &byte in ansi_instructions.as_bytes() { + vte_parser.advance(&mut grid, byte); + } + format!("{:?}", grid) +} + +fn take_snapshot_and_cursor_position( + ansi_instructions: &str, + rows: usize, + columns: usize, + palette: Palette, +) -> (String, Option<(usize, usize)>) { + // snapshot, x_coordinates, y_coordinates + let mut grid = Grid::new( + rows, + columns, + palette, + Rc::new(RefCell::new(LinkHandler::new())), + ); + let mut vte_parser = vte::Parser::new(); + for &byte in ansi_instructions.as_bytes() { + vte_parser.advance(&mut grid, byte); + } + (format!("{:?}", grid), grid.cursor_coordinates()) +} + +#[test] +fn new_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn floating_panes_persist_across_toggles() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.toggle_floating_panes(client_id, None); + // here we send bytes to the pane when it's not visible to make sure they're still handled and + // we see them once we toggle the panes back + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.toggle_floating_panes(client_id, None); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn toggle_floating_panes_off() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.toggle_floating_panes(client_id, None); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn toggle_floating_panes_on() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.toggle_floating_panes(client_id, None); + tab.toggle_floating_panes(client_id, None); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn five_new_floating_panes() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn increase_floating_pane_size() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_increase(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn decrease_floating_pane_size() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_decrease(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_floating_pane_left() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_left(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_floating_pane_right() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_right(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_floating_pane_up() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_up(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn resize_floating_pane_down() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.resize_down(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn move_floating_pane_focus_left() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.move_focus_left(client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((71, 9)), + "cursor coordinates moved to the pane on the left" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn move_floating_pane_focus_right() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.move_focus_left(client_id); + tab.move_focus_right(client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((80, 3)), + "cursor coordinates moved to the pane on the right" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn move_floating_pane_focus_up() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.move_focus_up(client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((71, 9)), + "cursor coordinates moved to the pane above" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn move_floating_pane_focus_down() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.move_focus_up(client_id); + tab.move_focus_down(client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((80, 13)), + "cursor coordinates moved to the pane below" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn move_floating_pane_focus_with_mouse() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_left_click(&Position::new(9, 71), client_id); + tab.handle_mouse_release(&Position::new(9, 71), client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((71, 9)), + "cursor coordinates moved to the clicked pane" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn move_pane_focus_with_mouse_to_non_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_left_click(&Position::new(4, 71), client_id); + tab.handle_mouse_release(&Position::new(4, 71), client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((1, 1)), + "cursor coordinates moved to the clicked pane" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn drag_pane_with_mouse() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_left_click(&Position::new(5, 71), client_id); + tab.handle_mouse_release(&Position::new(7, 75), client_id); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((75, 11)), + "cursor coordinates moved to the clicked pane" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn mark_text_inside_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_left_click(&Position::new(9, 71), client_id); + assert_eq!( + tab.selecting_with_mouse, true, + "started selecting with mouse on click" + ); + tab.handle_mouse_release(&Position::new(8, 50), client_id); + assert_eq!( + tab.selecting_with_mouse, false, + "stopped selecting with mouse on release" + ); + tab.render(&mut output, None); + let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_eq!( + cursor_coordinates, + Some((71, 9)), + "cursor coordinates stayed in clicked pane" + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn resize_tab_with_floating_panes() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.resize_whole_tab(Size { + cols: 100, + rows: 10, + }); + tab.render(&mut output, None); + let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.resize_whole_tab(Size { cols: 50, rows: 10 }); + tab.render(&mut output, None); + let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn shrink_whole_tab_with_floating_panes_horizontally_and_vertically_and_expand_back() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id_1 = PaneId::Terminal(2); + let new_pane_id_2 = PaneId::Terminal(3); + let new_pane_id_3 = PaneId::Terminal(4); + let new_pane_id_4 = PaneId::Terminal(5); + let new_pane_id_5 = PaneId::Terminal(6); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id_1, Some(client_id)); + tab.new_pane(new_pane_id_2, Some(client_id)); + tab.new_pane(new_pane_id_3, Some(client_id)); + tab.new_pane(new_pane_id_4, Some(client_id)); + tab.new_pane(new_pane_id_5, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())); + tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())); + tab.resize_whole_tab(Size { cols: 50, rows: 10 }); + tab.resize_whole_tab(Size { + cols: 121, + rows: 20, + }); + tab.render(&mut output, None); + let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + + assert_snapshot!(snapshot); +} + +#[test] +fn embed_floating_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.toggle_floating_panes(client_id, None); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am scratch terminal".as_bytes()), + ); + tab.toggle_pane_embed_or_floating(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn float_embedded_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let new_pane_id = PaneId::Terminal(2); + let mut output = Output::default(); + tab.new_pane(new_pane_id, Some(client_id)); + tab.handle_pty_bytes( + 2, + Vec::from("\n\n\n I am an embedded pane".as_bytes()), + ); + tab.toggle_pane_embed_or_floating(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} + +#[test] +fn cannot_float_only_embedded_pane() { + let size = Size { + cols: 121, + rows: 20, + }; + let client_id = 1; + let mut tab = create_new_tab(size); + let mut output = Output::default(); + tab.handle_pty_bytes( + 1, + Vec::from("\n\n\n I am an embedded pane".as_bytes()), + ); + tab.toggle_pane_embed_or_floating(client_id); + tab.render(&mut output, None); + let snapshot = take_snapshot( + output.serialize().get(&client_id).unwrap(), + size.rows, + size.cols, + Palette::default(), + ); + assert_snapshot!(snapshot); +} diff --git a/zellij-server/src/ui/boundaries.rs b/zellij-server/src/ui/boundaries.rs index d26db14a..64943741 100644 --- a/zellij-server/src/ui/boundaries.rs +++ b/zellij-server/src/ui/boundaries.rs @@ -1,9 +1,10 @@ use zellij_utils::{pane_size::Viewport, zellij_tile}; +use crate::output::CharacterChunk; +use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER, RESET_STYLES}; use crate::tab::Pane; use ansi_term::Colour::{Fixed, RGB}; use std::collections::HashMap; -use std::fmt::Write; use zellij_tile::data::PaletteColor; use zellij_utils::shared::colors; @@ -43,6 +44,19 @@ impl BoundarySymbol { self.color = color; *self } + pub fn as_terminal_character(&self) -> TerminalCharacter { + if self.invisible { + EMPTY_TERMINAL_CHARACTER + } else { + let character = self.boundary_type.chars().next().unwrap(); + TerminalCharacter { + character, + width: 1, + styles: RESET_STYLES + .foreground(self.color.map(|palette_color| palette_color.into())), + } + } + } } impl Display for BoundarySymbol { @@ -511,19 +525,16 @@ impl Boundaries { } } } - pub fn vte_output(&self) -> String { - let mut vte_output = String::new(); + pub fn render(&self) -> Vec { + let mut character_chunks = vec![]; for (coordinates, boundary_character) in &self.boundary_characters { - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - coordinates.y + 1, - coordinates.x + 1, - boundary_character - ) - .unwrap(); // goto row/col + boundary character + character_chunks.push(CharacterChunk::new( + vec![boundary_character.as_terminal_character()], + coordinates.x, + coordinates.y, + )); } - vte_output + character_chunks } fn rect_right_boundary_is_before_screen_edge(&self, rect: &dyn Pane) -> bool { rect.x() + rect.cols() < self.viewport.cols diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index f2052412..c46e5534 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -1,32 +1,64 @@ +use crate::output::CharacterChunk; +use crate::panes::{AnsiCode, CharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER}; use crate::ui::boundaries::boundary_type; use crate::ClientId; -use ansi_term::Colour::{Fixed, RGB}; -use ansi_term::Style; use zellij_utils::zellij_tile::prelude::{client_id_to_colors, Palette, PaletteColor}; use zellij_utils::{envs::get_session_name, pane_size::Viewport}; use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; -use std::fmt::Write; - -fn color_string(character: &str, color: Option) -> String { - match color { - Some(PaletteColor::Rgb((r, g, b))) => RGB(r, g, b).bold().paint(character).to_string(), - Some(PaletteColor::EightBit(color)) => Fixed(color).bold().paint(character).to_string(), - None => Style::new().bold().paint(character).to_string(), +fn foreground_color(characters: &str, color: Option) -> Vec { + let mut colored_string = Vec::with_capacity(characters.chars().count()); + for character in characters.chars() { + let styles = match color { + Some(palette_color) => { + let mut styles = CharacterStyles::new(); + styles.reset_all(); + styles + .foreground(Some(AnsiCode::from(palette_color))) + .bold(Some(AnsiCode::On)) + } + None => { + let mut styles = CharacterStyles::new(); + styles.reset_all(); + styles.bold(Some(AnsiCode::On)) + } + }; + let terminal_character = TerminalCharacter { + character, + styles, + width: character.width().unwrap_or(0), + }; + colored_string.push(terminal_character); } + colored_string } -fn background_color(character: &str, color: Option) -> String { - match color { - Some(PaletteColor::Rgb((r, g, b))) => { - Style::new().on(RGB(r, g, b)).paint(character).to_string() - } - Some(PaletteColor::EightBit(color)) => { - Style::new().on(Fixed(color)).paint(character).to_string() - } - None => character.to_string(), +fn background_color(characters: &str, color: Option) -> Vec { + let mut colored_string = Vec::with_capacity(characters.chars().count()); + for character in characters.chars() { + let styles = match color { + Some(palette_color) => { + let mut styles = CharacterStyles::new(); + styles.reset_all(); + styles + .background(Some(AnsiCode::from(palette_color))) + .bold(Some(AnsiCode::On)) + } + None => { + let mut styles = CharacterStyles::new(); + styles.reset_all(); + styles + } + }; + let terminal_character = TerminalCharacter { + character, + styles, + width: character.width().unwrap_or(0), + }; + colored_string.push(terminal_character); } + colored_string } pub struct FrameParams { @@ -70,11 +102,14 @@ impl PaneFrame { other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session, } } - fn client_cursor(&self, client_id: ClientId) -> String { + fn client_cursor(&self, client_id: ClientId) -> Vec { let color = client_id_to_colors(client_id, self.colors); background_color(" ", color.map(|c| c.0)) } - fn render_title_right_side(&self, max_length: usize) -> Option<(String, usize)> { + fn render_title_right_side( + &self, + max_length: usize, + ) -> Option<(Vec, usize)> { // string and length because of color if self.scroll_position.0 > 0 || self.scroll_position.1 > 0 { let prefix = " SCROLL: "; @@ -86,17 +121,17 @@ impl PaneFrame { let prefix_len = prefix.chars().count(); if prefix_len + full_indication_len <= max_length { Some(( - color_string(&format!("{}{}", prefix, full_indication), self.color), + foreground_color(&format!("{}{}", prefix, full_indication), self.color), prefix_len + full_indication_len, )) } else if full_indication_len <= max_length { Some(( - color_string(&full_indication, self.color), + foreground_color(&full_indication, self.color), full_indication_len, )) } else if short_indication_len <= max_length { Some(( - color_string(&short_indication, self.color), + foreground_color(&short_indication, self.color), short_indication_len, )) } else { @@ -106,24 +141,24 @@ impl PaneFrame { None } } - fn render_my_focus(&self, max_length: usize) -> Option<(String, usize)> { - let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); - let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + fn render_my_focus(&self, max_length: usize) -> Option<(Vec, usize)> { + let left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color); + let right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color); let full_indication_text = "MY FOCUS"; - let full_indication = format!( - "{} {} {}", - left_separator, - color_string(full_indication_text, self.color), - right_separator - ); + let mut full_indication = vec![]; + full_indication.append(&mut left_separator.clone()); + full_indication.push(EMPTY_TERMINAL_CHARACTER); + full_indication.append(&mut foreground_color(full_indication_text, self.color)); + full_indication.push(EMPTY_TERMINAL_CHARACTER); + full_indication.append(&mut right_separator.clone()); let full_indication_len = full_indication_text.width() + 4; // 2 for separators 2 for padding let short_indication_text = "ME"; - let short_indication = format!( - "{} {} {}", - left_separator, - color_string(short_indication_text, self.color), - right_separator - ); + let mut short_indication = vec![]; + short_indication.append(&mut left_separator.clone()); + short_indication.push(EMPTY_TERMINAL_CHARACTER); + short_indication.append(&mut foreground_color(short_indication_text, self.color)); + short_indication.push(EMPTY_TERMINAL_CHARACTER); + short_indication.append(&mut right_separator.clone()); let short_indication_len = short_indication_text.width() + 4; // 2 for separators 2 for padding if full_indication_len <= max_length { Some((full_indication, full_indication_len)) @@ -133,91 +168,110 @@ impl PaneFrame { None } } - fn render_my_and_others_focus(&self, max_length: usize) -> Option<(String, usize)> { - let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); - let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + fn render_my_and_others_focus( + &self, + max_length: usize, + ) -> Option<(Vec, usize)> { + let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color); + let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color); let full_indication_text = "MY FOCUS AND:"; let short_indication_text = "+"; - let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication = foreground_color(full_indication_text, self.color); let mut full_indication_len = full_indication_text.width(); - let mut short_indication = color_string(short_indication_text, self.color); + let mut short_indication = foreground_color(short_indication_text, self.color); let mut short_indication_len = short_indication_text.width(); for client_id in &self.other_focused_clients { - let text = format!(" {}", self.client_cursor(*client_id)); + let mut text = self.client_cursor(*client_id); full_indication_len += 2; - full_indication.push_str(&text); + full_indication.push(EMPTY_TERMINAL_CHARACTER); + full_indication.append(&mut text.clone()); short_indication_len += 2; - short_indication.push_str(&text); + short_indication.append(&mut text); } if full_indication_len + 4 <= max_length { // 2 for separators, 2 for padding - Some(( - format!("{} {} {}", left_separator, full_indication, right_separator), - full_indication_len + 4, - )) + let mut ret = vec![]; + ret.append(&mut left_separator); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut full_indication); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut right_separator); + Some((ret, full_indication_len + 4)) } else if short_indication_len + 4 <= max_length { // 2 for separators, 2 for padding - Some(( - format!( - "{} {} {}", - left_separator, short_indication, right_separator - ), - short_indication_len + 4, - )) + let mut ret = vec![]; + ret.append(&mut left_separator); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut short_indication); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut right_separator); + Some((ret, short_indication_len + 4)) } else { None } } - fn render_other_focused_users(&self, max_length: usize) -> Option<(String, usize)> { - let left_separator = color_string(boundary_type::VERTICAL_LEFT, self.color); - let right_separator = color_string(boundary_type::VERTICAL_RIGHT, self.color); + fn render_other_focused_users( + &self, + max_length: usize, + ) -> Option<(Vec, usize)> { + let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color); + let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color); let full_indication_text = if self.other_focused_clients.len() == 1 { "FOCUSED USER:" } else { "FOCUSED USERS:" }; let middle_indication_text = "U:"; - let mut full_indication = color_string(full_indication_text, self.color); + let mut full_indication = foreground_color(full_indication_text, self.color); let mut full_indication_len = full_indication_text.width(); - let mut middle_indication = color_string(middle_indication_text, self.color); + let mut middle_indication = foreground_color(middle_indication_text, self.color); let mut middle_indication_len = middle_indication_text.width(); - let mut short_indication = String::from(""); + let mut short_indication = vec![]; let mut short_indication_len = 0; for client_id in &self.other_focused_clients { - let text = format!(" {}", self.client_cursor(*client_id)); + let mut text = self.client_cursor(*client_id); full_indication_len += 2; - full_indication.push_str(&text); + full_indication.push(EMPTY_TERMINAL_CHARACTER); + full_indication.append(&mut text.clone()); middle_indication_len += 2; - middle_indication.push_str(&text); + middle_indication.push(EMPTY_TERMINAL_CHARACTER); + middle_indication.append(&mut text.clone()); short_indication_len += 2; - short_indication.push_str(&text); + short_indication.push(EMPTY_TERMINAL_CHARACTER); + short_indication.append(&mut text); } if full_indication_len + 4 <= max_length { // 2 for separators, 2 for padding - Some(( - format!("{} {} {}", left_separator, full_indication, right_separator), - full_indication_len + 4, - )) + let mut ret = vec![]; + ret.append(&mut left_separator); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut full_indication); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut right_separator); + Some((ret, full_indication_len + 4)) } else if middle_indication_len + 4 <= max_length { // 2 for separators, 2 for padding - Some(( - format!( - "{} {} {}", - left_separator, middle_indication, right_separator - ), - middle_indication_len + 4, - )) + let mut ret = vec![]; + ret.append(&mut left_separator); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut middle_indication); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut right_separator); + Some((ret, middle_indication_len + 4)) } else if short_indication_len + 3 <= max_length { // 2 for separators, 1 for padding - Some(( - format!("{}{} {}", left_separator, short_indication, right_separator), - short_indication_len + 3, - )) + let mut ret = vec![]; + ret.append(&mut left_separator); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut short_indication); + ret.push(EMPTY_TERMINAL_CHARACTER); + ret.append(&mut right_separator); + Some((ret, short_indication_len + 3)) } else { None } } - fn render_title_middle(&self, max_length: usize) -> Option<(String, usize)> { + fn render_title_middle(&self, max_length: usize) -> Option<(Vec, usize)> { // string and length because of color if self.is_main_client && self.other_focused_clients.is_empty() @@ -237,7 +291,7 @@ impl PaneFrame { None } } - fn render_title_left_side(&self, max_length: usize) -> Option<(String, usize)> { + fn render_title_left_side(&self, max_length: usize) -> Option<(Vec, usize)> { let middle_truncated_sign = "[..]"; let middle_truncated_sign_long = "[...]"; let full_text = format!(" {} ", &self.title); @@ -245,7 +299,7 @@ impl PaneFrame { None } else if full_text.width() <= max_length { Some(( - color_string(&full_text, self.color), + foreground_color(&full_text, self.color), full_text.chars().count(), )) } else { @@ -269,35 +323,39 @@ impl PaneFrame { } } - let title_left_side = - if first_part.width() + middle_truncated_sign.width() + second_part.width() - < max_length - { - // this means we lost 1 character when dividing the total length into halves + let (title_left_side, title_length) = if first_part.width() + + middle_truncated_sign.width() + + second_part.width() + < max_length + { + // this means we lost 1 character when dividing the total length into halves + ( format!( "{}{}{}", first_part, middle_truncated_sign_long, second_part - ) - } else { - format!("{}{}{}", first_part, middle_truncated_sign, second_part) - }; - Some(( - color_string(&title_left_side, self.color), - title_left_side.chars().count(), - )) + ), + first_part.width() + middle_truncated_sign_long.width() + second_part.width(), + ) + } else { + ( + format!("{}{}{}", first_part, middle_truncated_sign, second_part), + first_part.width() + middle_truncated_sign.width() + second_part.width(), + ) + }; + Some((foreground_color(&title_left_side, self.color), title_length)) } } fn three_part_title_line( &self, - left_side: &str, + mut left_side: Vec, left_side_len: &usize, - middle: &str, + mut middle: Vec, middle_len: &usize, - right_side: &str, + mut right_side: Vec, right_side_len: &usize, - ) -> String { + ) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let mut title_line = String::new(); + let mut title_line = vec![]; let left_side_start_position = self.geom.x + 1; let middle_start_position = self.geom.x + (total_title_length / 2) - (middle_len / 2) + 1; let right_side_start_position = @@ -306,24 +364,23 @@ impl PaneFrame { let mut col = self.geom.x; loop { if col == self.geom.x { - title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color)); } else if col == self.geom.x + self.geom.cols - 1 { - title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color)); } else if col == left_side_start_position { - title_line.push_str(left_side); + title_line.append(&mut left_side); col += left_side_len; continue; } else if col == middle_start_position { - title_line.push_str(middle); + title_line.append(&mut middle); col += middle_len; continue; } else if col == right_side_start_position { - title_line.push_str(right_side); + title_line.append(&mut right_side); col += right_side_len; continue; } else { - title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); - // TODO: BETTER + title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color)); } if col == self.geom.x + self.geom.cols - 1 { break; @@ -334,33 +391,32 @@ impl PaneFrame { } fn left_and_middle_title_line( &self, - left_side: &str, + mut left_side: Vec, left_side_len: &usize, - middle: &str, + mut middle: Vec, middle_len: &usize, - ) -> String { + ) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let mut title_line = String::new(); + let mut title_line = vec![]; let left_side_start_position = self.geom.x + 1; let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; let mut col = self.geom.x; loop { if col == self.geom.x { - title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color)); } else if col == self.geom.x + self.geom.cols - 1 { - title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color)); } else if col == left_side_start_position { - title_line.push_str(left_side); + title_line.append(&mut left_side); col += *left_side_len; continue; } else if col == middle_start_position { - title_line.push_str(middle); + title_line.append(&mut middle); col += *middle_len; continue; } else { - title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); - // TODO: BETTER + title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color)); } if col == self.geom.x + self.geom.cols - 1 { break; @@ -369,24 +425,27 @@ impl PaneFrame { } title_line } - fn middle_only_title_line(&self, middle: &str, middle_len: &usize) -> String { + fn middle_only_title_line( + &self, + mut middle: Vec, + middle_len: &usize, + ) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - let mut title_line = String::new(); + let mut title_line = vec![]; let middle_start_position = self.geom.x + (total_title_length / 2) - (*middle_len / 2) + 1; let mut col = self.geom.x; loop { if col == self.geom.x { - title_line.push_str(&color_string(boundary_type::TOP_LEFT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_LEFT, self.color)); } else if col == self.geom.x + self.geom.cols - 1 { - title_line.push_str(&color_string(boundary_type::TOP_RIGHT, self.color)); + title_line.append(&mut foreground_color(boundary_type::TOP_RIGHT, self.color)); } else if col == middle_start_position { - title_line.push_str(middle); + title_line.append(&mut middle); col += *middle_len; continue; } else { - title_line.push_str(&color_string(boundary_type::HORIZONTAL, self.color)); - // TODO: BETTER + title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color)); } if col == self.geom.x + self.geom.cols - 1 { break; @@ -397,81 +456,86 @@ impl PaneFrame { } fn two_part_title_line( &self, - left_side: &str, + mut left_side: Vec, left_side_len: &usize, - right_side: &str, + mut right_side: Vec, right_side_len: &usize, - ) -> String { - let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); - let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + ) -> Vec { + let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color); + let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color); let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let mut middle = String::new(); for _ in (left_side_len + right_side_len)..total_title_length { middle.push_str(boundary_type::HORIZONTAL); } - format!( - "{}{}{}{}{}", - left_boundary, - left_side, - color_string(&middle, self.color), - color_string(right_side, self.color), - &right_boundary - ) + let mut ret = vec![]; + ret.append(&mut left_boundary); + ret.append(&mut left_side); + ret.append(&mut foreground_color(&middle, self.color)); + ret.append(&mut right_side); + ret.append(&mut right_boundary); + ret } - fn left_only_title_line(&self, left_side: &str, left_side_len: &usize) -> String { - let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); - let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + fn left_only_title_line( + &self, + mut left_side: Vec, + left_side_len: &usize, + ) -> Vec { + let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color); + let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color); let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let mut middle_padding = String::new(); for _ in *left_side_len..total_title_length { middle_padding.push_str(boundary_type::HORIZONTAL); } - format!( - "{}{}{}{}", - left_boundary, - left_side, - color_string(&middle_padding, self.color), - &right_boundary - ) + let mut ret = vec![]; + ret.append(&mut left_boundary); + ret.append(&mut left_side); + ret.append(&mut foreground_color(&middle_padding, self.color)); + ret.append(&mut right_boundary); + ret } - fn empty_title_line(&self) -> String { - let left_boundary = color_string(boundary_type::TOP_LEFT, self.color); - let right_boundary = color_string(boundary_type::TOP_RIGHT, self.color); + fn empty_title_line(&self) -> Vec { + let mut left_boundary = foreground_color(boundary_type::TOP_LEFT, self.color); + let mut right_boundary = foreground_color(boundary_type::TOP_RIGHT, self.color); let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let mut middle_padding = String::new(); for _ in 0..total_title_length { middle_padding.push_str(boundary_type::HORIZONTAL); } - format!( - "{}{}{}", - left_boundary, - color_string(&middle_padding, self.color), - right_boundary - ) + let mut ret = vec![]; + ret.append(&mut left_boundary); + ret.append(&mut foreground_color(&middle_padding, self.color)); + ret.append(&mut right_boundary); + ret } - fn title_line_with_middle(&self, middle: &str, middle_len: &usize) -> String { + fn title_line_with_middle( + &self, + middle: Vec, + middle_len: &usize, + ) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let length_of_each_side = total_title_length.saturating_sub(*middle_len + 2) / 2; - let mut left_side = self.render_title_left_side(length_of_each_side); - let mut right_side = self.render_title_right_side(length_of_each_side); + let left_side = self.render_title_left_side(length_of_each_side); + let right_side = self.render_title_right_side(length_of_each_side); - match (&mut left_side, &mut right_side) { + match (left_side, right_side) { (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => self .three_part_title_line( left_side, - left_side_len, + &left_side_len, middle, middle_len, right_side, - right_side_len, + &right_side_len, ), (Some((left_side, left_side_len)), None) => { - self.left_and_middle_title_line(left_side, left_side_len, middle, middle_len) + self.left_and_middle_title_line(left_side, &left_side_len, middle, middle_len) } _ => self.middle_only_title_line(middle, middle_len), } } - fn title_line_without_middle(&self) -> String { + fn title_line_without_middle(&self) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners let left_side = self.render_title_left_side(total_title_length); let right_side = left_side.as_ref().and_then(|(_left_side, left_side_len)| { @@ -480,96 +544,74 @@ impl PaneFrame { }); match (left_side, right_side) { (Some((left_side, left_side_len)), Some((right_side, right_side_len))) => { - self.two_part_title_line(&left_side, &left_side_len, &right_side, &right_side_len) + self.two_part_title_line(left_side, &left_side_len, right_side, &right_side_len) } (Some((left_side, left_side_len)), None) => { - self.left_only_title_line(&left_side, &left_side_len) + self.left_only_title_line(left_side, &left_side_len) } _ => self.empty_title_line(), } } - fn render_title(&self, vte_output: &mut String) { + fn render_title(&self) -> Vec { let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners - if let Some((middle, middle_length)) = &self.render_title_middle(total_title_length) { - let title_text = self.title_line_with_middle(middle, middle_length); - write!( - vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - self.geom.y + 1, // +1 because goto is 1 indexed - self.geom.x + 1, // +1 because goto is 1 indexed - color_string(&title_text, self.color), - ) - .unwrap(); // goto row/col + boundary character - } else { - let title_text = self.title_line_without_middle(); - write!( - vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - self.geom.y + 1, // +1 because goto is 1 indexed - self.geom.x + 1, // +1 because goto is 1 indexed - color_string(&title_text, self.color), - ) - .unwrap(); // goto row/col + boundary character - } + self.render_title_middle(total_title_length) + .map(|(middle, middle_length)| self.title_line_with_middle(middle, &middle_length)) + .or_else(|| Some(self.title_line_without_middle())) + .unwrap() } - pub fn render(&self) -> String { - let mut vte_output = String::new(); - for row in self.geom.y..(self.geom.y + self.geom.rows) { - if row == self.geom.y { + pub fn render(&self) -> (Vec, Option) { + let mut character_chunks = vec![]; + for row in 0..self.geom.rows { + if row == 0 { // top row - self.render_title(&mut vte_output); - } else if row == self.geom.y + self.geom.rows - 1 { + let title = self.render_title(); + let x = self.geom.x; + let y = self.geom.y + row; + character_chunks.push(CharacterChunk::new(title, x, y)); + } else if row == self.geom.rows - 1 { // bottom row - for col in self.geom.x..(self.geom.x + self.geom.cols) { - let boundary = if col == self.geom.x { + let mut bottom_row = vec![]; + for col in 0..self.geom.cols { + let boundary = if col == 0 { // bottom left corner boundary_type::BOTTOM_LEFT - } else if col == self.geom.x + self.geom.cols - 1 { + } else if col == self.geom.cols - 1 { // bottom right corner boundary_type::BOTTOM_RIGHT } else { boundary_type::HORIZONTAL }; - let boundary_rendered = color_string(boundary, self.color); - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - row + 1, - col + 1, - boundary_rendered - ) - .unwrap(); + let mut boundary_character = foreground_color(boundary, self.color); + bottom_row.append(&mut boundary_character); } + let x = self.geom.x; + let y = self.geom.y + row; + character_chunks.push(CharacterChunk::new(bottom_row, x, y)); } else { - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - row + 1, // +1 because goto is 1 indexed - self.geom.x + 1, - color_string(boundary_type::VERTICAL, self.color), - ) - .unwrap(); // goto row/col + boundary character - write!( - &mut vte_output, - "\u{1b}[{};{}H\u{1b}[m{}", - row + 1, // +1 because goto is 1 indexed - self.geom.x + self.geom.cols, - color_string(boundary_type::VERTICAL, self.color), - ) - .unwrap(); // goto row/col + boundary character + let boundary_character_left = foreground_color(boundary_type::VERTICAL, self.color); + let boundary_character_right = + foreground_color(boundary_type::VERTICAL, self.color); + + let x = self.geom.x; + let y = self.geom.y + row; + character_chunks.push(CharacterChunk::new(boundary_character_left, x, y)); + + let x = (self.geom.x + self.geom.cols).saturating_sub(1); + let y = self.geom.y + row; + character_chunks.push(CharacterChunk::new(boundary_character_right, x, y)); } } - if self.is_main_client { - write!( - &mut vte_output, + let vte_output = if self.is_main_client { + Some(format!( "\u{1b}]0;Zellij ({}) - {}", get_session_name().unwrap(), self.title - ) - .unwrap(); - } - vte_output + )) + } else { + None + }; + (character_chunks, vte_output) } } diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index d8967f6e..5e3ee9b9 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -1,5 +1,6 @@ +use crate::output::Output; use crate::panes::PaneId; -use crate::tab::{Output, Pane}; +use crate::tab::Pane; use crate::ui::boundaries::Boundaries; use crate::ui::pane_boundaries_frame::FrameParams; use crate::ClientId; @@ -14,6 +15,7 @@ pub struct PaneContentsAndUi<'a> { colors: Palette, focused_clients: Vec, multiple_users_exist_in_session: bool, + z_index: Option, } impl<'a> PaneContentsAndUi<'a> { @@ -23,6 +25,7 @@ impl<'a> PaneContentsAndUi<'a> { colors: Palette, active_panes: &HashMap, multiple_users_exist_in_session: bool, + z_index: Option, ) -> Self { let focused_clients: Vec = active_panes .iter() @@ -35,37 +38,48 @@ impl<'a> PaneContentsAndUi<'a> { colors, focused_clients, multiple_users_exist_in_session, + z_index, } } pub fn render_pane_contents_to_multiple_clients( &mut self, clients: impl Iterator, ) { - if let Some(vte_output) = self.pane.render(None) { - // FIXME: Use Termion for cursor and style clearing? - self.output.push_str_to_multiple_clients( - &format!( - "\u{1b}[{};{}H\u{1b}[m{}", - self.pane.y() + 1, - self.pane.x() + 1, - vte_output - ), - clients, + if let Some((character_chunks, raw_vte_output)) = self.pane.render(None) { + let clients: Vec = clients.collect(); + self.output.add_character_chunks_to_multiple_clients( + character_chunks, + clients.iter().copied(), + self.z_index, ); + if let Some(raw_vte_output) = raw_vte_output { + self.output.add_post_vte_instruction_to_multiple_clients( + clients.iter().copied(), + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + raw_vte_output + ), + ); + } } } pub fn render_pane_contents_for_client(&mut self, client_id: ClientId) { - if let Some(vte_output) = self.pane.render(Some(client_id)) { - // FIXME: Use Termion for cursor and style clearing? - self.output.push_to_client( - client_id, - &format!( - "\u{1b}[{};{}H\u{1b}[m{}", - self.pane.y() + 1, - self.pane.x() + 1, - vte_output - ), - ); + if let Some((character_chunks, raw_vte_output)) = self.pane.render(Some(client_id)) { + self.output + .add_character_chunks_to_client(client_id, character_chunks, self.z_index); + if let Some(raw_vte_output) = raw_vte_output { + self.output.add_post_vte_instruction_to_client( + client_id, + &format!( + "\u{1b}[{};{}H\u{1b}[m{}", + self.pane.y() + 1, + self.pane.x() + 1, + raw_vte_output + ), + ); + } } } pub fn render_fake_cursor_if_needed(&mut self, client_id: ClientId) { @@ -84,7 +98,7 @@ impl<'a> PaneContentsAndUi<'a> { .unwrap(); if let Some(colors) = client_id_to_colors(*fake_cursor_client_id, self.colors) { if let Some(vte_output) = self.pane.render_fake_cursor(colors.0, colors.1) { - self.output.push_to_client( + self.output.add_post_vte_instruction_to_client( client_id, &format!( "\u{1b}[{};{}H\u{1b}[m{}", @@ -139,17 +153,18 @@ impl<'a> PaneContentsAndUi<'a> { other_cursors_exist_in_session: self.multiple_users_exist_in_session, } }; - if let Some(vte_output) = self.pane.render_frame(client_id, frame_params, client_mode) { - // FIXME: Use Termion for cursor and style clearing? - self.output.push_to_client( + if let Some((frame_terminal_characters, vte_output)) = + self.pane.render_frame(client_id, frame_params, client_mode) + { + self.output.add_character_chunks_to_client( client_id, - &format!( - "\u{1b}[{};{}H\u{1b}[m{}", - self.pane.y() + 1, - self.pane.x() + 1, - vte_output - ), + frame_terminal_characters, + self.z_index, ); + if let Some(vte_output) = vte_output { + self.output + .add_post_vte_instruction_to_client(client_id, &vte_output); + } } } pub fn render_pane_boundaries( diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 97a6f351..dd88d5eb 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -204,23 +204,8 @@ pub fn close_the_middle_tab() { new_tab(&mut screen, 1); new_tab(&mut screen, 2); new_tab(&mut screen, 3); - dbg!(screen - .tabs - .values() - .map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids())) - .collect::>()); screen.switch_tab_prev(1); - dbg!(screen - .tabs - .values() - .map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids())) - .collect::>()); screen.close_tab(1); - dbg!(screen - .tabs - .values() - .map(|t| (t.index, t.position, t.name.clone(), t.get_pane_ids())) - .collect::>()); assert_eq!(screen.tabs.len(), 2, "Two tabs left"); assert_eq!( diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 607e8ea4..7123e362 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -137,6 +137,10 @@ keybinds: key: [Char: 'f',] - action: [TogglePaneFrames, SwitchToMode: Normal,] key: [Char: 'z',] + - action: [ToggleFloatingPanes, SwitchToMode: Normal,] + key: [Char: 'w'] + - action: [TogglePaneEmbedOrFloating, SwitchToMode: Normal,] + key: [Char: 'e'] - action: [NewPane: ,] key: [ Alt: 'n',] - action: [MoveFocus: Left,] @@ -438,7 +442,7 @@ plugins: # Choose the path to the default shell that zellij will use for opening new panes # Default: $SHELL -#default_shell: fish +# default_shell: fish # Toggle between having pane frames around the panes # Options: diff --git a/zellij-utils/assets/layouts/no-plugins.yaml b/zellij-utils/assets/layouts/no-plugins.yaml new file mode 100644 index 00000000..808d6860 --- /dev/null +++ b/zellij-utils/assets/layouts/no-plugins.yaml @@ -0,0 +1,8 @@ +--- +template: + direction: Horizontal + parts: + - direction: Vertical + body: true +tabs: + - direction: Vertical diff --git a/zellij-utils/src/envs.rs b/zellij-utils/src/envs.rs index 971def6e..5bae2b0c 100644 --- a/zellij-utils/src/envs.rs +++ b/zellij-utils/src/envs.rs @@ -11,9 +11,11 @@ pub fn set_zellij(v: String) { } pub const SESSION_NAME_ENV_KEY: &str = "ZELLIJ_SESSION_NAME"; + pub fn get_session_name() -> Result { Ok(var(SESSION_NAME_ENV_KEY)?) } + pub fn set_session_name(v: String) { set_var(SESSION_NAME_ENV_KEY, v); } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 88410389..ea8033e5 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -207,6 +207,8 @@ pub enum ScreenContext { HandlePtyBytes, Render, NewPane, + ToggleFloatingPanes, + TogglePaneEmbedOrFloating, HorizontalSplit, VerticalSplit, WriteCharacter, diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index d29799fb..acc86e3b 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -81,6 +81,10 @@ pub enum Action { /// Open a new pane in the specified direction (relative to focus). /// If no direction is specified, will try to use the biggest available space. NewPane(Option), + /// Embed focused pane in tab if floating or float focused pane if embedded + TogglePaneEmbedOrFloating, + /// Toggle the visibility of all floating panes (if any) in the current Tab + ToggleFloatingPanes, /// Close the focus pane. CloseFocus, PaneNameInput(Vec), diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 34572ba7..f8626e04 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -33,7 +33,6 @@ pub fn get_mode_info( ], InputMode::Pane => vec![ ("←↓↑→".to_string(), "Move focus".to_string()), - ("p".to_string(), "Next".to_string()), ("n".to_string(), "New".to_string()), ("d".to_string(), "Down split".to_string()), ("r".to_string(), "Right split".to_string()), @@ -41,6 +40,9 @@ pub fn get_mode_info( ("f".to_string(), "Fullscreen".to_string()), ("z".to_string(), "Frames".to_string()), ("c".to_string(), "Rename".to_string()), + ("w".to_string(), "Floating Toggle".to_string()), + ("e".to_string(), "Embed Pane".to_string()), + ("p".to_string(), "Next".to_string()), ], InputMode::Tab => vec![ ("←↓↑→".to_string(), "Move focus".to_string()), diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 8ae76630..f95945a4 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -4,7 +4,7 @@ use crate::position::Position; /// Contains the position and size of a [`Pane`], or more generally of any terminal, measured /// in character rows and columns. -#[derive(Clone, Copy, Default, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Clone, Copy, Default, PartialEq, Debug, Serialize, Deserialize, Eq)] pub struct PaneGeom { pub x: usize, pub y: usize, @@ -34,7 +34,7 @@ pub struct Size { pub cols: usize, } -#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] +#[derive(Eq, Clone, Copy, PartialEq, Debug, Serialize, Deserialize)] pub struct Dimension { pub constraint: Constraint, inner: usize, @@ -90,6 +90,8 @@ pub enum Constraint { Percent(f64), } +impl Eq for Constraint {} + impl PaneGeom { pub fn contains(&self, point: &Position) -> bool { let col = point.column.0 as usize; diff --git a/zellij-utils/src/position.rs b/zellij-utils/src/position.rs index 56fcd638..89b91f82 100644 --- a/zellij-utils/src/position.rs +++ b/zellij-utils/src/position.rs @@ -20,6 +20,14 @@ impl Position { column: Column(self.column.0.saturating_sub(column)), } } + + pub fn line(&self) -> isize { + self.line.0 + } + + pub fn column(&self) -> usize { + self.column.0 + } } #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize, Serialize, PartialOrd)]