use super::{Output, Tab}; use crate::panes::kitty_graphics::KittyImageStore; use crate::panes::sixel::SixelImageStore; use crate::screen::CopyOptions; use crate::Arc; use zellij_utils::input::options::PaneFrameStyle; use crate::{ os_input_output::ServerOsApi, pane_groups::PaneGroups, panes::PaneId, plugins::PluginInstruction, thread_bus::ThreadSenders, ClientId, ServerInstruction, }; use std::net::{IpAddr, Ipv4Addr}; use std::path::PathBuf; use std::sync::Mutex; use zellij_utils::channels::Receiver; use zellij_utils::data::Direction; use zellij_utils::data::Resize; use zellij_utils::data::ResizeStrategy; use zellij_utils::data::WebSharing; use zellij_utils::envs::set_session_name; use zellij_utils::errors::{prelude::*, ErrorContext}; use zellij_utils::input::layout::{ FloatingPaneLayout, Layout, PercentOrFixed, RunPluginOrAlias, SwapFloatingLayout, SwapTiledLayout, TiledPaneLayout, }; use zellij_utils::input::mouse::MouseEvent; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::position::Position; use crate::pty_writer::PtyWriteInstruction; use zellij_utils::channels::{self, ChannelWithContext, SenderWithContext}; use crate::os_input_output::AsyncReader; use std::cell::RefCell; use std::collections::{BTreeMap, HashMap}; use std::rc::Rc; use interprocess::local_socket::Stream as LocalSocketStream; use zellij_utils::{ data::{FloatingPaneCoordinates, InputMode, ModeInfo, NewPanePlacement, Palette, Style}, input::command::{RunCommand, TerminalAction}, ipc::{ClientToServerMsg, ServerToClientMsg}, }; #[derive(Clone, Default)] struct FakeInputOutput { file_dumps: Arc>>, pub tty_stdin_bytes: Arc>>>, } impl ServerOsApi for FakeInputOutput { fn set_terminal_size_using_terminal_id( &self, _id: u32, _cols: u16, _rows: u16, _width_in_pixels: Option, _height_in_pixels: Option, ) -> Result<()> { // noop Ok(()) } fn spawn_terminal( &self, _file_to_open: TerminalAction, _quit_db: Box, RunCommand) + Send>, _default_editor: Option, ) -> Result<(u32, Box, Option)> { unimplemented!() } fn write_to_tty_stdin(&self, id: u32, buf: &[u8]) -> Result { self.tty_stdin_bytes .lock() .unwrap() .entry(id) .or_insert_with(|| vec![]) .extend_from_slice(buf); Ok(buf.len()) } fn tcdrain(&self, _id: u32) -> Result<()> { unimplemented!() } fn kill(&self, _pid: u32) -> Result<()> { unimplemented!() } fn force_kill(&self, _pid: u32) -> Result<()> { unimplemented!() } fn box_clone(&self) -> Box { Box::new((*self).clone()) } fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) -> Result<()> { unimplemented!() } fn new_client( &mut self, _client_id: ClientId, _stream: LocalSocketStream, ) -> Result> { unimplemented!() } fn new_client_with_reply( &mut self, _client_id: ClientId, _stream: LocalSocketStream, _reply_stream: LocalSocketStream, ) -> Result> { unimplemented!() } fn remove_client(&mut self, _client_id: ClientId) -> Result<()> { unimplemented!() } fn load_palette(&self) -> Palette { unimplemented!() } fn get_cwd(&self, _pid: u32) -> Option { unimplemented!() } fn write_to_file(&mut self, buf: String, name: Option) -> Result<()> { let f: String = match name { Some(x) => x, None => "tmp-name".to_owned(), }; self.file_dumps.lock().to_anyhow()?.insert(f, buf); Ok(()) } fn re_run_command_in_terminal( &self, _terminal_id: u32, _run_command: RunCommand, _quit_cb: Box, RunCommand) + Send>, ) -> Result<(Box, Option)> { unimplemented!() } fn clear_terminal_id(&self, _terminal_id: u32) -> Result<()> { unimplemented!() } fn send_sigint(&self, _pid: u32) -> Result<()> { unimplemented!() } } struct MockPtyInstructionBus { output: Arc>>, pty_writer_sender: SenderWithContext, pty_writer_receiver: Arc>, handle: Option>, } impl MockPtyInstructionBus { fn new() -> Self { let output = Arc::new(Mutex::new(vec![])); let (pty_writer_sender, pty_writer_receiver): ChannelWithContext = channels::unbounded(); let pty_writer_sender = SenderWithContext::new(pty_writer_sender); let pty_writer_receiver = Arc::new(pty_writer_receiver); Self { output, pty_writer_sender, pty_writer_receiver, handle: None, } } fn start(&mut self) { let output = self.output.clone(); let pty_writer_receiver = self.pty_writer_receiver.clone(); let handle = std::thread::Builder::new() .name("pty_writer".to_string()) .spawn({ move || loop { let (event, _err_ctx) = pty_writer_receiver .recv() .expect("failed to receive event on channel"); match event { PtyWriteInstruction::Write(msg, _, _) => output .lock() .unwrap() .push(String::from_utf8_lossy(&msg).to_string()), PtyWriteInstruction::Exit => break, _ => {}, } } }) .unwrap(); self.handle = Some(handle); } fn exit(&mut self) { self.pty_writer_sender .send(PtyWriteInstruction::Exit) .unwrap(); let handle = self.handle.take().unwrap(); handle.join().unwrap(); } fn pty_write_sender(&self) -> SenderWithContext { self.pty_writer_sender.clone() } fn clone_output(&self) -> Vec { self.output.lock().unwrap().clone() } } fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_with_stacked_pane_list( size: Size, default_mode: ModeInfo, stacked_pane_list: bool, base_layout_and_ids: Option<(TiledPaneLayout, Vec<(u32, Option)>)>, ) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let stacked_pane_list = Rc::new(RefCell::new(stacked_pane_list)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, stacked_pane_list, sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, false, false, web_server_ip, web_server_port, 0, ); let (base_layout, new_terminal_ids) = base_layout_and_ids.unwrap_or_else(|| (TiledPaneLayout::default(), vec![(1, None)])); tab.apply_layout( base_layout, vec![], new_terminal_ids, vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::None; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_with_swap_layouts( size: Size, default_mode: ModeInfo, swap_layouts: (Vec, Vec), base_layout_and_ids: Option<( TiledPaneLayout, Vec, Vec<(u32, Option)>, Vec<(u32, Option)>, HashMap>, )>, draw_pane_frames: bool, stacked_resize: bool, ) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let mut senders = ThreadSenders::default().silently_fail_on_send(); let (mock_plugin_sender, _mock_plugin_receiver): ChannelWithContext = channels::unbounded(); senders.replace_to_plugin(SenderWithContext::new(mock_plugin_sender)); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(stacked_resize)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, if draw_pane_frames { PaneFrameStyle::Full } else { PaneFrameStyle::None }, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, swap_layouts, PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); let ( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, ) = base_layout_and_ids.unwrap_or_default(); let new_terminal_ids = if new_terminal_ids.is_empty() { vec![(1, None)] } else { new_terminal_ids }; tab.apply_layout( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, client_id, None, ) .unwrap(); tab } fn create_new_tab_with_os_api( size: Size, default_mode: ModeInfo, os_api: &Box, ) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = os_api.clone(); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), // swap layouts PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let layout = Layout::from_str(layout, "layout_file_name".into(), None, None).unwrap(); let (tab_layout, floating_panes_layout) = layout.new_tab(); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), // swap layouts PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); let pane_ids = tab_layout .extract_run_instructions() .iter() .enumerate() .map(|(i, _)| (i as u32, None)) .collect(); let floating_pane_ids = floating_panes_layout .iter() .enumerate() .map(|(i, _)| (i as u32, None)) .collect(); tab.apply_layout( tab_layout, floating_panes_layout, pane_ids, floating_pane_ids, HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_with_mock_pty_writer( size: Size, default_mode: ModeInfo, mock_pty_writer: SenderWithContext, ) -> Tab { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let mut senders = ThreadSenders::default().silently_fail_on_send(); senders.replace_to_pty_writer(mock_pty_writer); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), // swap layouts PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn create_new_tab_with_sixel_support( size: Size, sixel_image_store: Rc>, ) -> Tab { // this is like the create_new_tab function but includes stuff needed for sixel, // eg. character_cell_size set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let senders = ThreadSenders::default().silently_fail_on_send(); let max_panes = None; let mode_info = ModeInfo::default(); let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_size, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), // swap layouts PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); tab } fn read_fixture(fixture_name: &str) -> Vec { let mut path_to_file = std::path::PathBuf::new(); path_to_file.push("../src"); path_to_file.push("tests"); path_to_file.push("fixtures"); path_to_file.push(fixture_name); std::fs::read(path_to_file) .unwrap_or_else(|_| panic!("could not read fixture {:?}", &fixture_name)) } use crate::panes::grid::Grid; use crate::panes::link_handler::LinkHandler; use insta::assert_snapshot; use vte; fn take_snapshot(ansi_instructions: &str, rows: usize, columns: usize, palette: Palette) -> String { let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let mut grid = Grid::new( rows, columns, Rc::new(RefCell::new(palette)), terminal_emulator_color_codes, Rc::new(RefCell::new(LinkHandler::new())), character_cell_size, sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), Style::default(), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, ); let mut vte_parser = vte::Parser::new(); vte_parser.advance(&mut grid, ansi_instructions.as_bytes()); format!("{:?}", grid) } fn take_snapshot_with_sixel( ansi_instructions: &str, rows: usize, columns: usize, palette: Palette, sixel_image_store: Rc>, ) -> String { let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let mut grid = Grid::new( rows, columns, Rc::new(RefCell::new(palette)), terminal_emulator_color_codes, Rc::new(RefCell::new(LinkHandler::new())), character_cell_size, sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), Style::default(), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, ); let mut vte_parser = vte::Parser::new(); vte_parser.advance(&mut grid, ansi_instructions.as_bytes()); 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 sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let mut grid = Grid::new( rows, columns, Rc::new(RefCell::new(palette)), terminal_emulator_color_codes, Rc::new(RefCell::new(LinkHandler::new())), Rc::new(RefCell::new(None)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), Style::default(), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, ); let mut vte_parser = vte::Parser::new(); vte_parser.advance(&mut grid, ansi_instructions.as_bytes()); let coords = grid .cursor_coordinates() .and_then(|(x, y, visible)| if visible { Some((x, y)) } else { None }); (format!("{:?}", grid), coords) } #[test] fn increase_tiled_pane_sizes_with_stacked_resizes() { // this is the default resizing algorithm let size = Size { cols: 200, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); for i in 2..5 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } // first we increase until fullscreen and once more to make sure we don't increase beyond it for _ in 0..=6 { tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } // then we decrease until the original position for _ in 0..=5 { tab.resize(client_id, ResizeStrategy::new(Resize::Decrease, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } } #[test] fn increase_tiled_pane_sizes_with_stacked_resizes_into_uneven_panes() { // this is the default resizing algorithm let size = Size { cols: 200, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); for i in 2..4 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } tab.move_focus_up(client_id).unwrap(); tab.vertical_split(PaneId::Terminal(4), None, client_id, None, None) .unwrap(); tab.move_focus_right(client_id).unwrap(); tab.horizontal_split(PaneId::Terminal(5), None, client_id, None, None) .unwrap(); tab.move_focus_down(client_id).unwrap(); // increase twice, once to add the short pane into the stack and shorten the larger one, and // once to add the remaining two panes (the one we shortened and the extra one near it) for _ in 0..2 { tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } // then we decrease until the original position for _ in 0..2 { tab.resize(client_id, ResizeStrategy::new(Resize::Decrease, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } } #[test] fn split_stack_vertically() { let size = Size { cols: 200, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); for i in 2..4 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } // the below resizes will end up stacking the panes tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.vertical_split(PaneId::Terminal(4), None, client_id, None, None) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn split_stack_horizontally() { let size = Size { cols: 200, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); for i in 2..4 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } // the below resizes will end up stacking the panes tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.horizontal_split(PaneId::Terminal(4), None, client_id, None, None) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn render_stacks_without_pane_frames() { // this checks various cases and gotchas that have to do with rendering stacked panes when we // don't draw frames around panes let size = Size { cols: 100, rows: 40, }; let client_id = 1; let mut tab = create_new_tab_without_pane_frames(size, ModeInfo::default()); let mut output = Output::default(); for i in 2..4 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } // the below resizes will end up stacking the panes tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.vertical_split(PaneId::Terminal(4), None, client_id, None, None) .unwrap(); for i in 5..7 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 7..9 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); for i in 9..11 { let new_pane_id_1 = PaneId::Terminal(i); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Right)), ) .unwrap(); let _ = tab.focus_pane_with_id(PaneId::Terminal(7), false, false, client_id); let _ = tab.focus_pane_with_id(PaneId::Terminal(5), false, false, client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn dump_screen() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let map = Arc::new(Mutex::new(HashMap::new())); tab.os_api = Box::new(FakeInputOutput { file_dumps: map.clone(), ..Default::default() }); let new_pane_id = PaneId::Terminal(2); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())) .unwrap(); let file = "/tmp/log.sh"; tab.dump_active_terminal_screen(Some(file.to_string()), client_id, false) .unwrap(); assert_eq!( map.lock().unwrap().get(file).unwrap(), "scratch", "screen was dumped properly" ); } #[test] fn clear_screen() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let map = Arc::new(Mutex::new(HashMap::new())); tab.os_api = Box::new(FakeInputOutput { file_dumps: map.clone(), ..Default::default() }); let new_pane_id = PaneId::Terminal(2); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("scratch".as_bytes())) .unwrap(); let file = "/tmp/log-clear-screen.sh"; tab.clear_active_terminal_screen(client_id).unwrap(); tab.dump_active_terminal_screen(Some(file.to_string()), client_id, false) .unwrap(); assert_eq!( map.lock().unwrap().get(file).unwrap(), "", "screen was cleared properly" ); } #[test] fn new_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn new_stacked_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::Stacked { pane_id_to_stack_under: None, borderless: None, }, Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); // 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()), ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Decrease, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Left)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Right)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Up)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id_1 = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Down)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.move_focus_left(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((3, 3)), "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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.move_focus_left(client_id).unwrap(); tab.move_focus_right(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((5, 5)), "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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.move_focus_up(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((3, 3)), "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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.move_focus_up(client_id).unwrap(); tab.move_focus_down(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((5, 5)), "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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(9, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(9, 71)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((35, 10)), "cursor coordinates moved to the clicked pane" ); assert_snapshot!(snapshot); } #[test] fn move_floating_pane_focus_to_last_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.move_focus_up(client_id).unwrap(); tab.focus_last_pane(client_id); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((5, 5)), "cursor coordinates moved back to the last pane below" ); 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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(4, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(4, 71)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); 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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(6, 30)), client_id, ) .unwrap(); assert!( tab.selecting_with_mouse_in_pane.is_some(), "started selecting with mouse on click" ); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(5, 15)), client_id, ) .unwrap(); assert!( tab.selecting_with_mouse_in_pane.is_none(), "stopped selecting with mouse on release" ); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((5, 5)), "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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 100, rows: 10, }) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 50, rows: 10 }).unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().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, ModeInfo::default()); 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(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(4, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(5, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(6, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 50, rows: 10 }).unwrap(); tab.resize_whole_tab(Size { cols: 121, rows: 20, }) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, _cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am an embedded pane".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn embed_floating_pane_without_pane_frames() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.set_pane_frames(PaneFrameStyle::None); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am scratch terminal".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn float_embedded_pane_without_pane_frames() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.set_pane_frames(PaneFrameStyle::None); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am an embedded pane".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } fn setup_tab_with_focused_floating_pane( size: Size, client_id: ClientId, frame_style: PaneFrameStyle, ) -> Tab { let mut tab = create_new_tab(size, ModeInfo::default()); tab.set_pane_frames(frame_style); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( PaneId::Terminal(2), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am a floating pane".as_bytes()), ) .unwrap(); tab } #[test] fn floating_fullscreen_full_frame_style() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = setup_tab_with_focused_floating_pane(size, client_id, PaneFrameStyle::Full); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn floating_fullscreen_titles_frame_style() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = setup_tab_with_focused_floating_pane(size, client_id, PaneFrameStyle::Titles); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn floating_fullscreen_none_frame_style() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = setup_tab_with_focused_floating_pane(size, client_id, PaneFrameStyle::None); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn changing_frame_style_while_floating_fullscreen_reflows_content() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = setup_tab_with_focused_floating_pane(size, client_id, PaneFrameStyle::Full); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.set_pane_frames(PaneFrameStyle::None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn exiting_floating_fullscreen_clears_background() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( PaneId::Terminal(2), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am a floating pane".as_bytes()), ) .unwrap(); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn floating_no_ui_fullscreen_covers_tab_and_status_bar() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = setup_tab_with_focused_floating_pane(size, client_id, PaneFrameStyle::Full); let mut output = Output::default(); tab.toggle_active_pane_no_ui_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn floating_fullscreen_hides_other_floating_and_pinned_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( PaneId::Terminal(2), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( PaneId::Terminal(3), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.set_floating_pane_pinned(PaneId::Terminal(3), true); tab.handle_pty_bytes(2, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\u{1b}#8".as_bytes())) .unwrap(); let _ = tab.focus_pane_with_id(PaneId::Terminal(2), false, false, client_id); let mut output = Output::default(); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().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, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes( 1, Vec::from("\n\n\n I am an embedded pane".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn replacing_existing_wide_characters() { // this is a real world use case using ncmpcpp with wide characters and scrolling // the reason we don't break it down is that it exposes quite a few edge cases with wide // characters that we should handle properly let size = Size { cols: 238, rows: 48, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); let pane_content = read_fixture("ncmpcpp-wide-chars"); tab.handle_pty_bytes(1, pane_content).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn rename_embedded_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes( 1, Vec::from("\n\n\n I am an embedded pane".as_bytes()), ) .unwrap(); tab.update_active_pane_name("Renamed empedded pane".as_bytes().to_vec(), client_id) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn rename_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am a floating pane".as_bytes()), ) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.update_active_pane_name("Renamed floating pane".as_bytes().to_vec(), client_id) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn wide_characters_in_left_title_side() { // this test makes sure the title doesn't overflow when it has wide characters let size = Size { cols: 238, rows: 48, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); let pane_content = read_fixture("title-wide-chars"); tab.handle_pty_bytes(1, pane_content).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn save_cursor_position_across_resizes() { // the save cursor position ANSI instruction (CSI s) needs to point to the same character after we // resize the pane let size = Size { cols: 100, rows: 5 }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes( 1, Vec::from("\n\n\rI am some text\n\rI am another line of text\n\rLet's save the cursor position here \u{1b}[sI should be ovewritten".as_bytes()), ).unwrap(); // We check cursor and saved cursor are handled separately by: // 1. moving real cursor up two lines let _ = tab.handle_pty_bytes(1, Vec::from("\u{1b}[2A".as_bytes())); // 2. resizing so real cursor gets lost above the viewport, which resets it to row 0 // The saved cursor ends up on row 1, allowing detection if it (incorrectly) gets reset too tab.resize_whole_tab(Size { cols: 35, rows: 4 }).unwrap(); // Now overwrite tab.handle_pty_bytes(1, Vec::from("\u{1b}[uthis overwrote me!".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 100, rows: 3 }).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn move_floating_pane_with_sixel_image() { let new_pane_id = PaneId::Terminal(2); let size = Size { cols: 121, rows: 20, }; let client_id = 1; let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let mut tab = create_new_tab_with_sixel_support(size, sixel_image_store.clone()); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let mut output = Output::new( sixel_image_store.clone(), character_cell_size, true, true, Rc::new(RefCell::new(KittyImageStore::default())), Rc::new(RefCell::new(HashMap::new())), Rc::new(RefCell::new(HashMap::new())), ); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(2, fixture).unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot_with_sixel( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), sixel_image_store, ); assert_snapshot!(snapshot); } #[test] fn floating_pane_above_sixel_image() { let new_pane_id = PaneId::Terminal(2); let size = Size { cols: 121, rows: 20, }; let client_id = 1; let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let mut tab = create_new_tab_with_sixel_support(size, sixel_image_store.clone()); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let mut output = Output::new( sixel_image_store.clone(), character_cell_size, true, true, Rc::new(RefCell::new(KittyImageStore::default())), Rc::new(RefCell::new(HashMap::new())), Rc::new(RefCell::new(HashMap::new())), ); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let fixture = read_fixture("sixel-image-500px.six"); tab.handle_pty_bytes(1, fixture).unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot_with_sixel( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), sixel_image_store, ); assert_snapshot!(snapshot); } #[test] fn suppress_tiled_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.replace_active_pane_with_editor_pane(new_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn suppress_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let editor_pane_id = PaneId::Terminal(3); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.replace_active_pane_with_editor_pane(editor_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_suppressing_tiled_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.replace_active_pane_with_editor_pane(new_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(1, Vec::from("\n\n\nI am the original pane".as_bytes())) .unwrap(); tab.close_pane(new_pane_id, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_suppressing_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let editor_pane_id = PaneId::Terminal(3); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.replace_active_pane_with_editor_pane(editor_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am the original pane".as_bytes())) .unwrap(); tab.close_pane(editor_pane_id, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn suppress_tiled_pane_float_it_and_close() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.replace_active_pane_with_editor_pane(new_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(1, Vec::from("\n\n\nI am the original pane".as_bytes())) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.close_pane(new_pane_id, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn suppress_floating_pane_embed_it_and_close_it() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let editor_pane_id = PaneId::Terminal(3); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.replace_active_pane_with_editor_pane(editor_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am the original pane".as_bytes())) .unwrap(); tab.toggle_pane_embed_or_floating(client_id).unwrap(); tab.close_pane(editor_pane_id, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn resize_whole_tab_while_tiled_pane_is_suppressed() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.replace_active_pane_with_editor_pane(new_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 100, rows: 10, }) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn resize_whole_tab_while_floting_pane_is_suppressed() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let editor_pane_id = PaneId::Terminal(3); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.replace_active_pane_with_editor_pane(editor_pane_id, client_id) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am an editor pane".as_bytes())) .unwrap(); tab.resize_whole_tab(Size { cols: 100, rows: 10, }) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn enter_search_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mode_info = ModeInfo { mode: InputMode::Search, ..Default::default() }; let mut tab = create_new_tab(size, mode_info); let mut output = Output::default(); let pane_content = read_fixture("grid_copy"); tab.handle_pty_bytes(1, pane_content).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_tab_nothing_highlighted", snapshot); // Pane title should show 'tortor' as search term // Only lines containing 'tortor' get marked as render-targets, so // only those are updated (search-styling is not visible here). tab.update_search_term("tortor".as_bytes().to_vec(), client_id) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_tab_highlight_tortor", snapshot); // Pane title should show search modifiers tab.toggle_search_wrap(client_id); tab.toggle_search_whole_words(client_id); tab.toggle_search_case_sensitivity(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_tab_highlight_tortor_modified", snapshot); // And only the search term again tab.toggle_search_wrap(client_id); tab.toggle_search_whole_words(client_id); tab.toggle_search_case_sensitivity(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_tab_highlight_tortor", snapshot); } #[test] fn enter_search_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mode_info = ModeInfo { mode: InputMode::Search, ..Default::default() }; let mut tab = create_new_tab(size, mode_info); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let pane_content = read_fixture("grid_copy"); tab.handle_pty_bytes(2, pane_content).unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_floating_tab_nothing_highlighted", snapshot); // Only the line inside the floating tab which contain 'fring' should be in the new snapshot tab.update_search_term("fring".as_bytes().to_vec(), client_id) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!("search_floating_tab_highlight_fring", snapshot); } #[test] fn pane_in_sgr_button_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1006h"); // button event tracking (1002) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[<0;71;5M".to_string(), // SGR left click "\u{1b}[<32;72;9M".to_string(), // SGR left click (hold) "\u{1b}[<0;75;7m".to_string(), // SGR left button release "\u{1b}[<2;71;5M".to_string(), // SGR right click "\u{1b}[<34;72;9M".to_string(), // SGR right click (hold) "\u{1b}[<2;75;7m".to_string(), // SGR right button release "\u{1b}[<1;71;5M".to_string(), // SGR middle click "\u{1b}[<33;72;9M".to_string(), // SGR middle click (hold) "\u{1b}[<1;75;7m".to_string(), // SGR middle button release "\u{1b}[<64;71;5M".to_string(), // SGR scroll up "\u{1b}[<65;71;5M".to_string(), // SGR scroll down ] ); } #[test] fn pane_in_sgr_normal_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1006h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[<0;71;5M".to_string(), // SGR left click // no hold event here, as hold events are not reported in normal mode "\u{1b}[<0;75;7m".to_string(), // SGR left button release "\u{1b}[<2;71;5M".to_string(), // SGR right click // no hold event here, as hold events are not reported in normal mode "\u{1b}[<2;75;7m".to_string(), // SGR right button release "\u{1b}[<1;71;5M".to_string(), // SGR middle click // no hold event here, as hold events are not reported in normal mode "\u{1b}[<1;75;7m".to_string(), // SGR middle button release "\u{1b}[<64;71;5M".to_string(), // SGR scroll up "\u{1b}[<65;71;5M".to_string(), // SGR scroll down ] ); } #[test] fn pane_in_sgr_any_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1006h"); // any event tracking (1003) with SGR encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(9, 72)), client_id, ) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[<0;71;5M".to_string(), // SGR left click "\u{1b}[<32;72;9M".to_string(), // SGR left click (hold) "\u{1b}[<0;75;7m".to_string(), // SGR left button release "\u{1b}[<2;71;5M".to_string(), // SGR right click "\u{1b}[<34;72;9M".to_string(), // SGR right click (hold) "\u{1b}[<2;75;7m".to_string(), // SGR right button release "\u{1b}[<1;71;5M".to_string(), // SGR middle click "\u{1b}[<33;72;9M".to_string(), // SGR middle click (hold) "\u{1b}[<1;75;7m".to_string(), // SGR middle button release "\u{1b}[<64;71;5M".to_string(), // SGR scroll up "\u{1b}[<65;71;5M".to_string(), // SGR scroll down "\u{1b}[<35;72;9M".to_string(), // SGR buttonless motion ] ); } #[test] fn pane_in_utf8_button_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1002;1005h"); // button event tracking (1002) with utf8 encoding (1005) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[M g%".to_string(), // utf8 left click "\u{1b}[M@h)".to_string(), // utf8 left click (hold) "\u{1b}[M#k'".to_string(), // utf8 left button release "\u{1b}[M\"g%".to_string(), // utf8 right click "\u{1b}[MBh)".to_string(), // utf8 right click (hold) "\u{1b}[M#k'".to_string(), // utf8 right button release "\u{1b}[M!g%".to_string(), // utf8 middle click "\u{1b}[MAh)".to_string(), // utf8 middle click (hold) "\u{1b}[M#k'".to_string(), // utf8 middle click release "\u{1b}[M`g%".to_string(), // utf8 scroll up "\u{1b}[Mag%".to_string(), // utf8 scroll down ] ); } #[test] fn pane_in_utf8_normal_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1000;1005h"); // normal event tracking (1000) with sgr encoding (1006) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[M g%".to_string(), // utf8 left click // no hold event here, as hold events are not reported in normal mode "\u{1b}[M#k'".to_string(), // utf8 left button release "\u{1b}[M\"g%".to_string(), // utf8 right click // no hold event here, as hold events are not reported in normal mode "\u{1b}[M#k'".to_string(), // utf8 right button release "\u{1b}[M!g%".to_string(), // utf8 middle click // no hold event here, as hold events are not reported in normal mode "\u{1b}[M#k'".to_string(), // utf8 middle click release "\u{1b}[M`g%".to_string(), // utf8 scroll up "\u{1b}[Mag%".to_string(), // utf8 scroll down ] ); } #[test] fn pane_in_utf8_any_event_tracking_mouse_mode() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let sgr_mouse_mode_any_button = String::from("\u{1b}[?1003;1005h"); // any event tracking (1002) with utf8 encoding (1005) tab.handle_pty_bytes(1, sgr_mouse_mode_any_button.as_bytes().to_vec()) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_right_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_right_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_right_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(5, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_middle_motion_event(Position::new(9, 72)), client_id, ) .unwrap(); // tab.handle_middle_mouse_release(&Position::new(7, 75), client_id) tab.handle_mouse_event( &MouseEvent::new_middle_release_event(Position::new(7, 75)), client_id, ) .unwrap(); tab.handle_scrollwheel_up(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(5, 71), 1, client_id) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(9, 72)), client_id, ) .unwrap(); pty_instruction_bus.exit(); assert_eq!( pty_instruction_bus.clone_output(), vec![ "\u{1b}[M g%".to_string(), // utf8 left click "\u{1b}[M@h)".to_string(), // utf8 left click (hold) "\u{1b}[M#k'".to_string(), // utf8 left button release "\u{1b}[M\"g%".to_string(), // utf8 right click "\u{1b}[MBh)".to_string(), // utf8 right click (hold) "\u{1b}[M#k'".to_string(), // utf8 right button release "\u{1b}[M!g%".to_string(), // utf8 middle click "\u{1b}[MAh)".to_string(), // utf8 middle click (hold) "\u{1b}[M#k'".to_string(), // utf8 middle click release "\u{1b}[M`g%".to_string(), // utf8 scroll up "\u{1b}[Mag%".to_string(), // utf8 scroll down "\u{1b}[MCh)".to_string(), // urf8 buttonless motion ] ); } #[test] fn tab_with_basic_layout() { let layout = r#" layout { pane split_direction="Vertical" { pane pane split_direction="Horizontal" { pane pane } } } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn tab_with_layout_that_has_floating_panes() { let layout = r#" layout { pane floating_panes { pane pane } } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn tab_with_nested_layout() { let layout = r#" layout { pane_template name="top-and-vertical-sandwich" { pane vertical-sandwich { pane } } pane_template name="vertical-sandwich" split_direction="vertical" { pane children pane } pane_template name="nested-vertical-sandwich" split_direction="vertical" { pane top-and-vertical-sandwich pane } nested-vertical-sandwich } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn tab_with_nested_uneven_layout() { let layout = r#" layout { pane_template name="horizontal-with-vertical-top" { pane split_direction="Vertical" { pane children } pane } horizontal-with-vertical-top name="my tab" { pane pane } } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn pane_bracketed_paste_ignored_when_not_in_bracketed_paste_mode() { // regression test for: https://github.com/zellij-org/zellij/issues/1687 let size = Size { cols: 121, rows: 20, }; let client_id: u16 = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let bracketed_paste_start = vec![27, 91, 50, 48, 48, 126]; // \u{1b}[200~ let bracketed_paste_end = vec![27, 91, 50, 48, 49, 126]; // \u{1b}[201 tab.write_to_active_terminal(&None, bracketed_paste_start, false, client_id) .unwrap(); tab.write_to_active_terminal(&None, "test".as_bytes().to_vec(), false, client_id) .unwrap(); tab.write_to_active_terminal(&None, bracketed_paste_end, false, client_id) .unwrap(); pty_instruction_bus.exit(); assert_eq!(pty_instruction_bus.clone_output(), vec!["", "test", ""]); } #[test] fn pane_faux_scrolling_in_alternate_mode() { let size = Size { cols: 121, rows: 20, }; let client_id: u16 = 1; let lines_to_scroll = 3; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let enable_alternate_screen = String::from("\u{1b}[?1049h"); // CSI ? 1049 h -> switch to the Alternate Screen Buffer let set_application_mode = String::from("\u{1b}[?1h"); // no output since alternate scren not active yet tab.handle_scrollwheel_up(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); tab.handle_pty_bytes(1, enable_alternate_screen.as_bytes().to_vec()) .unwrap(); // CSI A * lines_to_scroll, CSI B * lines_to_scroll tab.handle_scrollwheel_up(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); tab.handle_pty_bytes(1, set_application_mode.as_bytes().to_vec()) .unwrap(); // SS3 A * lines_to_scroll, SS3 B * lines_to_scroll tab.handle_scrollwheel_up(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); tab.handle_scrollwheel_down(&Position::new(1, 1), lines_to_scroll, client_id) .unwrap(); pty_instruction_bus.exit(); let mut expected: Vec<&str> = Vec::new(); expected.append(&mut vec!["\u{1b}[A"; lines_to_scroll]); expected.append(&mut vec!["\u{1b}[B"; lines_to_scroll]); expected.append(&mut vec!["\u{1b}OA"; lines_to_scroll]); expected.append(&mut vec!["\u{1b}OB"; lines_to_scroll]); assert_eq!(pty_instruction_bus.clone_output(), expected); } #[test] fn move_pane_focus_sends_tty_csi_event() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let tty_stdin_bytes = Arc::new(Mutex::new(BTreeMap::new())); let os_api = Box::new(FakeInputOutput { tty_stdin_bytes: tty_stdin_bytes.clone(), ..Default::default() }); let mut tab = create_new_tab_with_os_api(size, ModeInfo::default(), &os_api); let new_pane_id_1 = PaneId::Terminal(2); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 1, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 2, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.move_focus_left(client_id).unwrap(); assert_snapshot!(format!("{:?}", *tty_stdin_bytes.lock().unwrap())); } #[test] fn move_floating_pane_focus_sends_tty_csi_event() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let tty_stdin_bytes = Arc::new(Mutex::new(BTreeMap::new())); let os_api = Box::new(FakeInputOutput { tty_stdin_bytes: tty_stdin_bytes.clone(), ..Default::default() }); let mut tab = create_new_tab_with_os_api(size, ModeInfo::default(), &os_api); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 1, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 2, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 3, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.move_focus_left(client_id).unwrap(); assert_snapshot!(format!("{:?}", *tty_stdin_bytes.lock().unwrap())); } #[test] fn toggle_floating_panes_on_sends_tty_csi_event() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let tty_stdin_bytes = Arc::new(Mutex::new(BTreeMap::new())); let os_api = Box::new(FakeInputOutput { tty_stdin_bytes: tty_stdin_bytes.clone(), ..Default::default() }); let mut tab = create_new_tab_with_os_api(size, ModeInfo::default(), &os_api); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.handle_pty_bytes( 1, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 2, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 3, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); assert_snapshot!(format!("{:?}", *tty_stdin_bytes.lock().unwrap())); } #[test] fn toggle_floating_panes_off_sends_tty_csi_event() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let tty_stdin_bytes = Arc::new(Mutex::new(BTreeMap::new())); let os_api = Box::new(FakeInputOutput { tty_stdin_bytes: tty_stdin_bytes.clone(), ..Default::default() }); let mut tab = create_new_tab_with_os_api(size, ModeInfo::default(), &os_api); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes( 1, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 2, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.handle_pty_bytes( 3, // subscribe to focus events Vec::from("\u{1b}[?1004h".as_bytes()), ) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); assert_snapshot!(format!("{:?}", *tty_stdin_bytes.lock().unwrap())); } #[test] fn can_swap_tiled_layout_at_runtime() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab max_panes=2 split_direction="vertical" { pane pane } } swap_tiled_layout { tab max_panes=2 { pane pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_swap_floating_layout_at_runtime() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_floating_layout { floating_panes max_panes=2 { pane pane } } swap_floating_layout { floating_panes max_panes=2 { pane { x "0%" } pane { x "100%" } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swapping_layouts_after_resize_snaps_to_current_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane } } swap_tiled_layout { tab { pane split_direction="vertical" { pane pane } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.next_swap_layout().unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_tiled_layout_with_stacked_children() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_tiled_layout_with_only_stacked_children() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane stacked=true { children; } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_tiled_layout_with_stacked_children_and_no_pane_frames() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, false, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn move_focus_up_with_stacked_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn move_focus_down_with_stacked_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_down(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn move_focus_right_into_stacked_panes() { // here we make sure that when we focus right into a stack, // we will always focus on the "main" pane of the stack // and not on one of its folds let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); for i in 0..12 { let new_pane_id = i + 2; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.move_focus_left(client_id); tab.horizontal_split(PaneId::Terminal(16), None, client_id, None, None) .unwrap(); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_right(client_id); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 12)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn move_focus_left_into_stacked_panes() { // here we make sure that when we focus left into a stack, // we will always focus on the "main" pane of the stack // and not on one of its folds let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane stacked=true { children; } pane focus=true } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); for i in 0..13 { let new_pane_id = i + 2; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.move_focus_right(client_id); tab.horizontal_split(PaneId::Terminal(1), None, client_id, None, None) .unwrap(); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_left(client_id); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((1, 12)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn move_focus_up_into_stacked_panes() { // here we make sure that when we focus up into a stack, // the main pane will become the lowest pane and the sizes // in the stack will be adjusted accordingly let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); for i in 0..4 { let new_pane_id = i + 3; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_left(client_id); let _ = tab.move_focus_down(client_id); tab.vertical_split(PaneId::Terminal(7), None, client_id, None, None) .unwrap(); let _ = tab.move_focus_up(client_id); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 9)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn move_focus_down_into_stacked_panes() { // here we make sure that when we focus down into a stack, // the main pane will become the highest pane and the sizes // in the stack will be adjusted accordingly let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); for i in 0..4 { let new_pane_id = i + 3; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let _ = tab.move_focus_left(client_id); let _ = tab.move_focus_up(client_id); tab.vertical_split(PaneId::Terminal(7), None, client_id, None, None) .unwrap(); let _ = tab.move_focus_down(client_id); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 8)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn focus_last_stacked_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.move_focus_right(client_id); tab.move_focus_up(client_id); tab.move_focus_up(client_id); tab.focus_last_pane(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_main_stacked_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.close_pane(new_pane_id_2, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_main_stacked_pane_in_mid_stack() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_up(client_id); tab.close_pane(new_pane_id_3, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_one_liner_stacked_pane_below_main_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_left(client_id); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_up(client_id); tab.close_pane(new_pane_id_2, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_one_liner_stacked_pane_above_main_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_up(client_id); tab.close_pane(new_pane_id_2, false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_of_main_pane_in_stack_horizontally() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Left)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_of_main_pane_in_stack_vertically() { let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Down)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_of_main_pane_in_stack_non_directionally() { let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = false; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_into_pane_stack_horizontally() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Right)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_into_pane_stack_vertically() { let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_right(client_id); let _ = tab.move_focus_down(client_id); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Up)), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_increase_size_into_pane_stack_non_directionally() { // note - this is not the default behavior, by default stacked_resize is enabled (set to true) let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = false; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.resize(client_id, ResizeStrategy::new(Resize::Increase, None)) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn decreasing_size_of_whole_tab_treats_stacked_panes_properly() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.resize_whole_tab(Size { cols: 100, rows: 10, }); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn increasing_size_of_whole_tab_treats_stacked_panes_properly() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.resize_whole_tab(Size { cols: 100, rows: 10, }); let _ = tab.resize_whole_tab(Size { cols: 121, rows: 20, }); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn cannot_decrease_stack_size_beyond_minimum_height() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_down(client_id); for _ in 0..6 { tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Up)), ) .unwrap(); } tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn focus_stacked_pane_over_flexible_pane_with_the_mouse() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(1, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(1, 71)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn focus_stacked_pane_under_flexible_pane_with_the_mouse() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(1, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(9, 71)), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn close_stacked_pane_with_previously_focused_other_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(2, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(2, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(1, 71)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(1, 71)), client_id, ) .unwrap(); tab.close_pane(PaneId::Terminal(4), false, None); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 2)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn close_pane_near_stacked_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.close_pane(PaneId::Terminal(6), false, None); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 4)), "cursor coordinates moved to the main pane of the stack", ); assert_snapshot!(snapshot); } #[test] fn focus_next_pane_expands_stacked_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_left(client_id); tab.focus_next_pane(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn stacked_panes_can_become_fullscreen() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane stacked=true { children; } } pane } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); 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); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_4, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_5, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); let _ = tab.move_focus_up(client_id); tab.toggle_active_pane_fullscreen(client_id); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn layout_with_plugins_and_commands_swaped_properly() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=1 borderless=true { plugin location="zellij:tab-bar" } pane split_direction="vertical" { pane command="command1" pane pane command="command2" } pane size=2 borderless=true { plugin location="zellij:status-bar" } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane command="command2" pane command="command1" pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); command_1.hold_on_close = true; let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); command_2.hold_on_close = true; let new_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_floating_terminal_ids = vec![]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn base_layout_is_included_in_swap_layouts() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=1 borderless=true { plugin location="zellij:tab-bar" } pane split_direction="vertical" { pane command="command1" pane pane command="command2" } pane size=2 borderless=true { plugin location="zellij:status-bar" } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane command="command2" pane command="command1" pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_floating_terminal_ids = vec![]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.previous_swap_layout().unwrap(); // move back to the base layout tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_layouts_including_command_panes_absent_from_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=1 borderless=true { plugin location="zellij:tab-bar" } pane split_direction="vertical" { pane pane pane } pane size=2 borderless=true { plugin location="zellij:status-bar" } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane command="command2" pane command="command1" pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_layouts_not_including_command_panes_present_in_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=1 borderless=true { plugin location="zellij:tab-bar" } pane split_direction="vertical" { pane command="command1" pane pane command="command2" } pane size=2 borderless=true { plugin location="zellij:status-bar" } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane pane pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_floating_terminal_ids = vec![]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_layouts_including_plugin_panes_absent_from_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=2 borderless=true pane split_direction="vertical" { pane pane pane } pane size=1 borderless=true } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane pane pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_layouts_not_including_plugin_panes_present_in_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane size=1 borderless=true { plugin location="zellij:tab-bar" } pane split_direction="vertical" { pane command="command1" pane pane command="command2" } pane size=2 borderless=true { plugin location="zellij:status-bar" } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_tiled_layout { tab { pane size=2 borderless=true { plugin location="zellij:status-bar" } pane pane pane pane size=1 borderless=true { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_floating_terminal_ids = vec![]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn new_pane_in_auto_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout "#; let swap_layouts = r#" layout { swap_tiled_layout { tab max_panes=5 { pane split_direction="vertical" { pane pane { children; } } } tab max_panes=8 { pane split_direction="vertical" { pane { children; } pane { pane; pane; pane; pane; } } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let mut expected_cursor_coordinates = vec![ (62, 1), (62, 11), (62, 15), (62, 16), (62, 16), (62, 16), (62, 16), ]; for i in 0..7 { let new_pane_id = i + 2; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let (expected_x, expected_y) = expected_cursor_coordinates.remove(0); assert_eq!( cursor_coordinates, Some((expected_x, expected_y)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } } #[test] fn new_pane_in_stacked_resizes() { let size = Size { cols: 200, rows: 40, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_tiled_layouts = vec![]; let swap_floating_layouts = vec![]; let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let mut expected_cursor_coordinates = vec![(101, 1), (101, 21), (151, 21), (151, 22)]; for i in 0..4 { let new_pane_id = i + 2; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let (expected_x, expected_y) = expected_cursor_coordinates.remove(0); assert_eq!( cursor_coordinates, Some((expected_x, expected_y)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } } #[test] fn when_swapping_tiled_layouts_in_a_damaged_state_layout_and_pane_focus_are_unchanged() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane pane pane } "#; let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane pane } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.move_focus_down(client_id); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Down)), ) .unwrap(); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((1, 8)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_swapping_tiled_layouts_in_an_undamaged_state_pane_focuses_on_focused_node() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane pane pane } "#; let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane focus=true pane pane } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.move_focus_down(client_id); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((1, 1)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_swapping_tiled_layouts_in_an_undamaged_state_with_no_focus_node_pane_focuses_on_deepest_node( ) { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane pane pane } "#; let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane pane } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.move_focus_down(client_id); let _ = tab.move_focus_down(client_id); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((82, 1)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_closing_a_pane_in_auto_layout_the_focus_goes_to_last_focused_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { pane pane pane } "#; let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane pane } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.move_focus_down(client_id); let _ = tab.move_focus_down(client_id); tab.close_pane(PaneId::Terminal(3), false, None); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((62, 1)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn floating_layout_with_plugins_and_commands_swaped_properly() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane x=0 y=0 { plugin location="zellij:tab-bar" } pane x=0 y=10 command="command1" pane pane x=50 y=10 command="command2" pane x=50 y=0 { plugin location="zellij:status-bar" } } } "#; // this swap layout swaps between the location of the plugins and the commands let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane x=0 y=0 { plugin location="zellij:status-bar" } pane x=0 y=10 command="command2" pane pane x=50 y=10 command="command1" pane x=50 y=0 { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_floating_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_terminal_ids = vec![(4, None)]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn base_floating_layout_is_included_in_swap_layouts() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane x=0 y=0 { plugin location="zellij:tab-bar" } pane x=0 y=10 command="command1" pane pane x=50 y=10 command="command2" pane x=50 y=0 { plugin location="zellij:status-bar" } } } "#; // this swap layout swaps between the location of the plugins and the commands let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane x=0 y=0 { plugin location="zellij:status-bar" } pane x=0 y=10 command="command2" pane pane x=50 y=10 command="command1" pane x=50 y=0 { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_floating_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_terminal_ids = vec![(4, None)]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.previous_swap_layout().unwrap(); // move back to the base layout tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_floating_layouts_including_command_panes_absent_from_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane { plugin location="zellij:tab-bar" } pane pane pane pane { plugin location="zellij:status-bar" } } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane { plugin location="zellij:status-bar" } pane x=0 y=0 command="command1" pane x=10 y=10 command="command2" pane pane { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_floating_layouts_not_including_command_panes_present_in_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane { plugin location="zellij:tab-bar" } pane command="command1" pane pane command="command2" pane { plugin location="zellij:status-bar" } } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane { plugin location="zellij:status-bar" } pane pane pane pane { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_floating_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_terminal_ids = vec![(4, None)]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_floating_layouts_including_plugin_panes_absent_from_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane pane pane } } "#; let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane { plugin location="zellij:status-bar" } pane pane { plugin location="zellij:tab-bar" } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn swap_floating_layouts_not_including_plugin_panes_present_in_existing_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane { plugin location="zellij:tab-bar" } pane pane { plugin location="zellij:status-bar" } } } "#; // this swap layout changes both the split direction of the two command panes and the location // of the plugins - we want to make sure that they are all placed properly and not switched // around let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane pane pane } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let mut command_1 = RunCommand::default(); command_1.command = PathBuf::from("command1"); let mut command_2 = RunCommand::default(); command_2.command = PathBuf::from("command2"); let new_floating_terminal_ids = vec![(1, Some(command_1)), (2, None), (3, Some(command_2))]; let new_terminal_ids = vec![(4, None)]; let mut new_plugin_ids = HashMap::new(); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:tab-bar", &None, None, None).unwrap(), vec![1], ); new_plugin_ids.insert( RunPluginOrAlias::from_url("zellij:status-bar", &None, None, None).unwrap(), vec![2], ); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.handle_plugin_bytes(1, 1, "I am a tab bar".as_bytes().to_vec()); let _ = tab.handle_plugin_bytes(2, 1, "I am a\n\rstatus bar".as_bytes().to_vec()); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn new_floating_pane_in_auto_layout() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout "#; let swap_layouts = r#" layout { swap_floating_layout name="spread" { floating_panes max_panes=1 { pane {y "50%"; x "50%"; } } floating_panes max_panes=2 { pane { x "1%"; y "25%"; width "45%"; } pane { x "50%"; y "25%"; width "45%"; } } floating_panes max_panes=3 { pane { x "1%"; y "1%"; width "45%"; } pane { x "50%"; y "1%"; width "45%"; } pane { y "55%"; width "45%"; height "45%"; } } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![(1, None)]; let new_floating_terminal_ids = vec![]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let mut expected_cursor_coordinates = vec![(61, 11), (61, 6), (34, 12)]; for i in 0..3 { let new_pane_id = i + 2; tab.new_pane( PaneId::Terminal(new_pane_id), None, None, false, true, NewPanePlacement::Floating(None), Some(client_id), None, ) .unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let (expected_x, expected_y) = expected_cursor_coordinates.remove(0); assert_eq!( cursor_coordinates, Some((expected_x, expected_y)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } } #[test] fn when_swapping_floating_layouts_in_a_damaged_state_layout_and_pane_focus_are_unchanged() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane x=0 y=0 pane pane } } "#; let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane pane pane } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); tab.resize( client_id, ResizeStrategy::new(Resize::Increase, Some(Direction::Down)), ) .unwrap(); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((33, 8)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_swapping_floating_layouts_in_an_undamaged_state_pane_focuses_on_focused_node() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane x=0 y=0 pane pane } } "#; let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane pane pane } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((35, 10)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_swapping_floating_layouts_in_an_undamaged_state_with_no_focus_node_pane_focuses_on_deepest_node( ) { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane pane pane } } "#; let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane pane pane } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); tab.next_swap_layout().unwrap(); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((35, 10)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_closing_a_floating_pane_in_auto_layout_the_focus_goes_to_last_focused_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane pane pane } } "#; let swap_layouts = r#" layout { swap_floating_layout { floating_panes { pane pane pane } } } "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let _ = tab.move_focus_up(client_id); let _ = tab.move_focus_up(client_id); tab.close_pane(PaneId::Terminal(1), false, None); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((31, 6)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_resizing_whole_tab_with_auto_layout_and_floating_panes_the_layout_is_maintained() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut output = Output::default(); let base_layout = r#" layout { floating_panes { pane pane pane } } "#; let swap_layouts = r#" layout "#; let (base_layout, base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_floating_terminal_ids = vec![(1, None), (2, None), (3, None)]; let new_terminal_ids = vec![(4, None)]; let new_plugin_ids = HashMap::new(); let swap_layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = swap_layout.swap_tiled_layouts.clone(); let swap_floating_layouts = swap_layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), Some(( base_layout, base_floating_layout, new_terminal_ids, new_floating_terminal_ids, new_plugin_ids, )), true, stacked_resize, ); let new_size = Size { cols: 150, rows: 30, }; let _ = tab.resize_whole_tab(new_size); tab.render(&mut output, None).unwrap(); let (snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), new_size.rows, new_size.cols, Palette::default(), ); assert_eq!( cursor_coordinates, Some((43, 13)), "cursor coordinates moved to the new pane", ); assert_snapshot!(snapshot); } #[test] fn when_applying_a_truncated_swap_layout_child_attributes_are_not_ignored() { // here we want to make sure that the nested borderless is preserved on resize (when the layout // is reapplied, and thus is truncated to just one pane rather than a logical container pane // and an actual pane as it is described here) let layout = r#" layout { pane { pane borderless=true } } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); let new_size = Size { cols: 122, rows: 20, }; let _ = tab.resize_whole_tab(new_size); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), new_size.rows, new_size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn can_define_expanded_pane_in_stack() { let layout = r#" layout { pane split_direction="Vertical" { pane pane stacked=true { pane pane expanded=true pane pane } } } "#; let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn borderless_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); // Create borderless floating pane let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(50)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(true), }; tab.new_floating_pane( new_pane_id, None, None, false, true, Some(coordinates), None, ) .unwrap(); tab.handle_pty_bytes( 2, Vec::from("\n\n\n I am borderless".as_bytes()), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn borderless_pane_content_fills_edges() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(5)), pinned: None, borderless: Some(true), }; tab.new_floating_pane( new_pane_id, None, None, false, true, Some(coordinates), None, ) .unwrap(); // Fill with X's to verify content reaches edges tab.handle_pty_bytes( 2, Vec::from("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX\r\nXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX".as_bytes()), ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn borderless_pinned_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let borderless_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(30)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(60)), height: Some(PercentOrFixed::Fixed(8)), pinned: Some(true), borderless: Some(true), }; tab.new_floating_pane( borderless_pane_id, None, None, false, true, Some(coordinates), None, ) .unwrap(); // Toggle floating panes off to test pinned behavior tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Content behind pinned pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\nPinned borderless pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn cursor_hidden_when_floating_pane_is_under_pinned_pane() { // Test that cursor is correctly hidden when focused on a floating pane that's under a pinned pane let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); // Create first floating pane (will be underneath) let bottom_pane_coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(40)), height: Some(PercentOrFixed::Fixed(10)), pinned: Some(false), borderless: Some(false), }; tab.new_floating_pane( PaneId::Terminal(2), None, None, false, true, Some(bottom_pane_coordinates), None, ) .unwrap(); // Create overlapping pinned pane on top let top_pane_coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(15)), y: Some(PercentOrFixed::Fixed(6)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(8)), pinned: Some(true), borderless: Some(false), }; tab.new_floating_pane( PaneId::Terminal(3), None, None, false, true, Some(top_pane_coordinates), None, ) .unwrap(); // Add some text to both panes so we can see them tab.handle_pty_bytes(2, Vec::from("Bottom floating pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("Top pinned pane".as_bytes())) .unwrap(); // Explicitly focus the bottom floating pane (PaneId::Terminal(2)) // Use move_focus_left to switch from pane 3 to pane 2 tab.move_focus_left(client_id).unwrap(); // Render tab.render(&mut output, None).unwrap(); let (_snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // The cursor should be hidden because the focused pane (2) is under the pinned pane (3) // and the cursor position falls within the overlapping area assert_eq!( cursor_coordinates, None, "Cursor should be hidden when focused floating pane is under a pinned pane that covers it" ); } #[test] fn cursor_visible_when_pinned_pane_is_focused() { // Test that cursor is visible when focused on a pinned pane that's on top let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); // Create first floating pane (will be underneath) let bottom_pane_coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(10)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(40)), height: Some(PercentOrFixed::Fixed(10)), pinned: Some(false), borderless: Some(false), }; tab.new_floating_pane( PaneId::Terminal(2), None, None, false, true, Some(bottom_pane_coordinates), None, ) .unwrap(); // Create overlapping pinned pane on top let top_pane_coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(15)), y: Some(PercentOrFixed::Fixed(6)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(8)), pinned: Some(true), borderless: Some(false), }; tab.new_floating_pane( PaneId::Terminal(3), None, None, false, true, Some(top_pane_coordinates), None, ) .unwrap(); // Add some text to both panes tab.handle_pty_bytes(2, Vec::from("Bottom floating pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("Top pinned pane".as_bytes())) .unwrap(); // The pinned pane should be focused by default (last created) // Render tab.render(&mut output, None).unwrap(); let (_snapshot, cursor_coordinates) = take_snapshot_and_cursor_position( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // The cursor should be visible because the focused pane is the pinned pane on top assert!( cursor_coordinates.is_some(), "Cursor should be visible when focused on pinned pane that's on top" ); } #[test] fn test_left_click_on_inactive_tiled_pane_changes_focus() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(new_pane_id)); let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); } #[test] fn test_left_release_after_selection_copies_to_clipboard() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let _output = Output::default(); // Fill the pane with some text content tab.handle_pty_bytes(1, Vec::from("Selectable text content here".as_bytes())) .unwrap(); // Start text selection with left press tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(1, 5)), client_id, ) .unwrap(); // Drag to create selection tab.handle_mouse_event( &MouseEvent::new_left_motion_event(Position::new(1, 15)), client_id, ) .unwrap(); // Release should trigger clipboard copy let release_effect = tab .handle_mouse_event( &MouseEvent::new_left_release_event(Position::new(1, 15)), client_id, ) .unwrap(); // Verify clipboard message was sent assert!(release_effect.leave_clipboard_message); } #[test] fn test_ctrl_click_on_tiled_pane_edge_starts_resize() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Click on the edge between panes with Ctrl // For a vertical split at cols=121, the edge is around column 60-61 let edge_position = Position::new(5, 60); let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); // Verify resize started assert!(effect.state_changed); } #[test] fn test_ctrl_drag_resizes_tiled_pane_horizontally() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane content".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane content".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let edge_position = Position::new(5, 60); tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); let motion_position = Position::new(5, 61); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_drag_resizes_tiled_pane_vertically() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.horizontal_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Top pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Bottom pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let edge_position = Position::new(10, 60); tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); let motion_position = Position::new(11, 60); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_drag_resizes_unfocused_tiled_pane_vertically() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.horizontal_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Top pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Bottom pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let edge_position = Position::new(10, 60); let _ = tab.focus_pane_with_id(PaneId::Terminal(1), false, false, client_id); // focus top pane tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); let motion_position = Position::new(11, 60); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let focused_pane = tab.get_active_pane_id(client_id).unwrap(); assert_ne!(focused_pane, new_pane_id, "resize did not change focus"); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_click_on_pane_body_does_nothing() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Click in the center of a pane (not on an edge) let center_position = Position::new(10, 30); let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(center_position), client_id, ) .unwrap(); // Should not trigger any state change assert!(!effect.state_changed); } #[test] fn test_ctrl_drag_resizes_floating_pane_from_edge() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(coordinates)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let edge_position = Position::new(7, 14); // Approximate right edge tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); let motion_position = Position::new(7, 15); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_drag_resizes_floating_pane_from_corner() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(coordinates)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Corner resize test".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let corner_position = Position::new(14, 14); tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(corner_position), client_id, ) .unwrap(); let motion_position = Position::new(15, 15); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(25)), height: Some(PercentOrFixed::Fixed(10)), pinned: Some(true), borderless: Some(false), }; tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(coordinates)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Corner resize test".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); eprintln!("{}", snapshot_before); let corner_position = Position::new(14, 29); tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(corner_position), client_id, ) .unwrap(); let motion_position = Position::new(15, 30); let motion_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(motion_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); eprintln!("{}", snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_click_on_floating_pin_button_toggles_pin() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); // Toggle floating panes mode and create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Pin toggle test".as_bytes())) .unwrap(); // Render before pin toggle tab.render(&mut output, None).unwrap(); // Calculate pin button position (3 cells left of top-right corner of floating pane) // Default floating pane is roughly at y=5, right edge around x=90 // Pin button should be at approximately (5, 87) let pin_position = Position::new(5, 87); let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(pin_position), client_id, ) .unwrap(); // Verify state changed (pin toggle should trigger this) assert!(effect.state_changed); // Render after pin toggle let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshot should show pin indicator change assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_click_on_floating_frame_not_on_pin_starts_resize() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Toggle floating panes mode and create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Frame click test".as_bytes())) .unwrap(); // Click on frame but NOT on pin button location // Use left edge of frame instead let frame_position = Position::new(5, 30); let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(frame_position), client_id, ) .unwrap(); // Should start resize (if on edge), state_changed should be true assert!(effect.state_changed); } #[test] fn test_left_drag_moves_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); // Toggle floating panes mode and create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Moving pane".as_bytes())) .unwrap(); // Render before move tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Click on frame (not ctrl, not pin location) to start move let frame_position = Position::new(5, 30); let press_effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) .unwrap(); // Drag to move the pane let motion_position = Position::new(6, 31); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(motion_position), client_id, ) .unwrap(); // Release tab.handle_mouse_event( &MouseEvent::new_left_release_event(motion_position), client_id, ) .unwrap(); // Verify state changed assert!(press_effect.state_changed); // Render after move let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshots should be different assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_left_click_on_floating_pane_changes_focus() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id_1 = PaneId::Terminal(2); let floating_pane_id_2 = PaneId::Terminal(3); let coordinates_1 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; let coordinates_2 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(20)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id_1, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_1)), Some(client_id), None, ) .unwrap(); tab.new_pane( floating_pane_id_2, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_2)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Pane 1".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("Pane 2".as_bytes())) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_2)); let pane1_position = Position::new(7, 7); let effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(pane1_position), client_id) .unwrap(); assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_1)); } #[test] fn test_left_click_on_pinned_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id_1 = PaneId::Terminal(2); let floating_pane_id_2 = PaneId::Terminal(3); let mut output = Output::default(); let coordinates_1 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; let coordinates_2 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(60)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); // will be pinned tab.new_pane( floating_pane_id_1, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_1)), Some(client_id), None, ) .unwrap(); // will not be pinned tab.new_pane( floating_pane_id_2, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_2)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Pinned pane".as_bytes())) .unwrap(); // render once so that the client's frame will be registered tab.render(&mut output, None).unwrap(); let pin_position = Position::new(5, 31); tab.handle_mouse_event(&MouseEvent::new_left_press_event(pin_position), client_id) .unwrap(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); let pane_position = Position::new(6, 25); let _effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(pane_position), client_id) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Verify pinned pane is still visible in snapshot assert_snapshot!(format!("{}", snapshot)); } #[test] fn test_alt_left_click_toggles_pane_group() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Alt+click on a pane to toggle group let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_alt_event(Position::new(5, 30)), client_id, ) .unwrap(); // Verify group_toggle effect returned assert!(effect.group_toggle.is_some()); assert_eq!(effect.group_toggle, Some(PaneId::Terminal(1))); } #[test] fn test_alt_left_drag_adds_panes_to_group() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id_2 = PaneId::Terminal(2); let new_pane_id_3 = PaneId::Terminal(3); let _output = Output::default(); // Create two vertical splits (3 panes) tab.vertical_split(new_pane_id_2, None, client_id, None, None) .unwrap(); tab.vertical_split(new_pane_id_3, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Pane 1".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Pane 2".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("Pane 3".as_bytes())) .unwrap(); // Alt+press on first pane let press_effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_alt_event(Position::new(5, 10)), client_id, ) .unwrap(); assert!(press_effect.group_toggle.is_some()); // Alt+drag to second pane let motion_effect_1 = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_alt_event(Position::new(5, 40)), client_id, ) .unwrap(); // Verify group_add effect assert!(motion_effect_1.group_add.is_some()); // Alt+drag to third pane let motion_effect_2 = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_alt_event(Position::new(5, 80)), client_id, ) .unwrap(); // Verify group_add effect assert!(motion_effect_2.group_add.is_some()); } #[test] fn test_right_alt_click_ungroups_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // First, create a group with Alt+click tab.handle_mouse_event( &MouseEvent::new_left_press_with_alt_event(Position::new(5, 30)), client_id, ) .unwrap(); // Right+Alt click to ungroup let effect = tab .handle_mouse_event( &MouseEvent::new_right_press_with_alt_event(Position::new(5, 30)), client_id, ) .unwrap(); // Verify ungroup effect returned assert!(effect.ungroup); } #[test] fn test_scroll_wheel_up_scrolls_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); // Fill the pane with many lines to enable scrolling let mut content = String::new(); for i in 0..50 { content.push_str(&format!("Line {}\r\n", i)); } tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) .unwrap(); // Render before scroll tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Scroll up let _effect = tab .handle_mouse_event( &MouseEvent::new_scroll_up_event(Position::new(10, 60)), client_id, ) .unwrap(); // Render after scroll let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshots should be different (viewport moved) assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_scroll_wheel_down_scrolls_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let _output = Output::default(); // Fill the pane with many lines to enable scrolling let mut content = String::new(); for i in 0..50 { content.push_str(&format!("Line {}\r\n", i)); } tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) .unwrap(); // First scroll up to have content above viewport tab.handle_mouse_event( &MouseEvent::new_scroll_up_event(Position::new(10, 60)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_scroll_up_event(Position::new(10, 60)), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_scroll_up_event(Position::new(10, 60)), client_id, ) .unwrap(); // Render before scrolling down let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Scroll down let _effect = tab .handle_mouse_event( &MouseEvent::new_scroll_down_event(Position::new(10, 60)), client_id, ) .unwrap(); // Render after scroll let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshots should be different (viewport moved) assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_scroll_on_inactive_pane_scrolls_that_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Fill both panes with scrollable content let mut content = String::new(); for i in 0..50 { content.push_str(&format!("Left Line {}\r\n", i)); } tab.handle_pty_bytes(1, Vec::from(content.as_bytes())) .unwrap(); let mut content = String::new(); for i in 0..50 { content.push_str(&format!("Right Line {}\r\n", i)); } tab.handle_pty_bytes(2, Vec::from(content.as_bytes())) .unwrap(); // Active pane is pane 2 (right), scroll on pane 1 (left) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Render before scroll tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Scroll up on the left (inactive) pane let _effect = tab .handle_mouse_event( &MouseEvent::new_scroll_up_event(Position::new(10, 30)), client_id, ) .unwrap(); // Render after scroll let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshots should be different (left pane scrolled) assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_right_click_forwards_to_active_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let _output = Output::default(); tab.handle_pty_bytes(1, Vec::from("Active pane".as_bytes())) .unwrap(); // Right click on the active pane let effect = tab .handle_mouse_event( &MouseEvent::new_right_press_event(Position::new(10, 60)), client_id, ) .unwrap(); // Event should be forwarded (verified via MouseEffect or no error) // The effect may not indicate state change, but should succeed assert!(effect.group_toggle.is_none()); } #[test] fn test_middle_click_forwards_to_active_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let _output = Output::default(); tab.handle_pty_bytes(1, Vec::from("Active pane".as_bytes())) .unwrap(); // Middle click on the active pane let effect = tab .handle_mouse_event( &MouseEvent::new_middle_press_event(Position::new(10, 60)), client_id, ) .unwrap(); // Event should be forwarded (verified via MouseEffect or no error) assert!(effect.group_toggle.is_none()); } #[test] fn test_hover_over_inactive_pane_sets_hover_state() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Hover over inactive pane (left) with buttonless motion let effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); let hover_panes = tab.query_mouse_hover_pane_id(); assert_snapshot!(format!("{:?}", hover_panes)); assert!(effect.state_changed); } #[test] fn test_hover_over_active_pane_unsets_hover() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Hover over inactive pane (left) with buttonless motion let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); // Hover over active pane (right) with buttonless motion let effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 90)), client_id, ) .unwrap(); let hover_panes = tab.query_mouse_hover_pane_id(); assert_snapshot!(format!("{:?}", hover_panes)); assert!(effect.state_changed); } #[test] fn test_resize_tiled_then_move_floating_sequence() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let tiled_pane_id = PaneId::Terminal(2); let floating_pane_id = PaneId::Terminal(3); let _output = Output::default(); // Create tiled split tab.vertical_split(tiled_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Tiled Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Tiled Right".as_bytes())) .unwrap(); // Resize tiled pane with Ctrl+drag let edge_position = Position::new(5, 60); tab.handle_mouse_event( &MouseEvent::new_left_press_with_ctrl_event(edge_position), client_id, ) .unwrap(); let motion_position = Position::new(5, 70); let resize_effect = tab .handle_mouse_event( &MouseEvent::new_left_motion_with_ctrl_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_with_ctrl_event(motion_position), client_id, ) .unwrap(); assert!(resize_effect.state_changed); // Now toggle floating and create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("Floating".as_bytes())) .unwrap(); // Move floating pane let frame_position = Position::new(5, 30); let move_effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) .unwrap(); let motion_position = Position::new(8, 40); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(motion_position), client_id, ) .unwrap(); // Both operations should have succeeded assert!(move_effect.state_changed); // Render final state let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(format!("{}", snapshot)); } #[test] fn test_alt_click_works_on_floating_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Toggle floating panes mode and create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); // Alt+click on floating pane to toggle group let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_with_alt_event(Position::new(10, 60)), client_id, ) .unwrap(); // Verify group_toggle effect returned assert!(effect.group_toggle.is_some()); assert_eq!(effect.group_toggle, Some(PaneId::Terminal(2))); } #[test] fn test_left_click_on_tiled_frame_edge_starts_resize() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let _output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right".as_bytes())) .unwrap(); // Click on the edge between panes (without Ctrl) let edge_position = Position::new(5, 60); let effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(edge_position), client_id) .unwrap(); // Edge detection should start resize even without Ctrl for tiled panes assert!(effect.state_changed); } #[test] fn test_left_click_on_floating_frame_not_on_pin_moves_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Movable pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let frame_position = Position::new(5, 30); let press_effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(frame_position), client_id) .unwrap(); let motion_position = Position::new(6, 31); tab.handle_mouse_event( &MouseEvent::new_left_motion_event(motion_position), client_id, ) .unwrap(); tab.handle_mouse_event( &MouseEvent::new_left_release_event(motion_position), client_id, ) .unwrap(); assert!(press_effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_up_on_active_tiled_pane_increases_pane_size() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let active_pane_position = Position::new(5, 70); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(active_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_down_on_active_tiled_pane_decreases_pane_size() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let active_pane_position = Position::new(5, 70); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(active_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_up_on_inactive_tiled_pane_increases_active_tiled_pane_size() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let inactive_pane_position = Position::new(5, 10); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(inactive_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_down_on_inactive_tiled_pane_decreases_active_tiled_pane_size() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Left pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Right pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let inactive_pane_position = Position::new(5, 10); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(inactive_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_up_increases_floating_pane_size() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let floating_pane_position = Position::new(5, 50); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(floating_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_down_decreases_floating_pane_size() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let floating_pane_position = Position::new(5, 50); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(floating_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_hidden() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(10)), pinned: Some(true), borderless: Some(false), }; tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(coordinates)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let floating_pane_position = Position::new(10, 10); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(floating_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes_hidden() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); let coordinates = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(30)), height: Some(PercentOrFixed::Fixed(10)), pinned: Some(true), borderless: Some(false), }; tab.new_pane( new_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(coordinates)), Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(1, Vec::from("Tiled pane".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("Floating pane".as_bytes())) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_before = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); let floating_pane_position = Position::new(6, 10); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(floating_pane_position), client_id, ) .unwrap(); assert!(effect.state_changed); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot_after = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_ne!(snapshot_before, snapshot_after); eprintln!("{}", snapshot_before); eprintln!("{}", snapshot_after); assert_snapshot!(format!("{}", snapshot_after)); } #[test] fn test_ctrl_scroll_up_does_not_resize_pane_when_mouse_scroll_resize_disabled() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.update_mouse_scroll_resize(false); let pane_geom_before = tab.get_active_pane(client_id).unwrap().position_and_size(); let active_pane_position = Position::new(5, 70); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(active_pane_position), client_id, ) .unwrap(); let pane_geom_after = tab.get_active_pane(client_id).unwrap().position_and_size(); assert!(!effect.state_changed); assert_eq!(pane_geom_before, pane_geom_after); } #[test] fn test_ctrl_scroll_down_does_not_resize_pane_when_mouse_scroll_resize_disabled() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.update_mouse_scroll_resize(false); let pane_geom_before = tab.get_active_pane(client_id).unwrap().position_and_size(); let active_pane_position = Position::new(5, 70); let effect = tab .handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(active_pane_position), client_id, ) .unwrap(); let pane_geom_after = tab.get_active_pane(client_id).unwrap().position_and_size(); assert!(!effect.state_changed); assert_eq!(pane_geom_before, pane_geom_after); } fn two_adjacent_stacks_layout() -> (TiledPaneLayout, Vec<(u32, Option)>) { let base_layout = r#" layout { pane split_direction="horizontal" { pane stacked=true { pane focus=true pane pane } pane stacked=true { pane pane pane } } } "#; let (base_layout, _base_floating_layout) = Layout::from_kdl(base_layout, Some("file_name.kdl".into()), None, None) .unwrap() .template .unwrap(); let new_terminal_ids = vec![ (1, None), (2, None), (3, None), (4, None), (5, None), (6, None), ]; (base_layout, new_terminal_ids) } fn stack_member_pane_count(tab: &Tab) -> usize { tab.get_all_pane_ids() .into_iter() .filter(|id| tab.pane_is_stack_list_member(id)) .count() } #[test] fn test_ctrl_scroll_up_merging_stacks_preserves_all_panes() { let size = Size { cols: 150, rows: 40, }; let client_id = 1; let mut output = Output::default(); let mut tab = create_new_tab_with_stacked_pane_list( size, ModeInfo::default(), true, Some(two_adjacent_stacks_layout()), ); tab.render(&mut output, None).unwrap(); let mut pane_ids_before = tab.get_all_pane_ids(); pane_ids_before.sort(); assert_eq!( pane_ids_before.len(), 6, "all six panes exist before the merge" ); assert_eq!(stack_member_pane_count(&tab), 6); let active_pane_position = Position::new(5, 40); tab.handle_mouse_event( &MouseEvent::new_ctrl_scroll_up_event(active_pane_position), client_id, ) .unwrap(); let dissolved_into_grid = !tab.has_stack_lists(); assert!( dissolved_into_grid, "stack lists must be dissolved into in-grid stacks before the classic merge mutation" ); let mut in_grid_ids = tab.get_static_and_floating_pane_ids(); in_grid_ids.sort(); assert_eq!( in_grid_ids, pane_ids_before, "all panes must be present in the grid after the merge, none orphaned" ); let mut pane_ids_after = tab.get_all_pane_ids(); pane_ids_after.sort(); assert_eq!( pane_ids_before, pane_ids_after, "no panes are lost when merging stacks via ctrl+scroll resize" ); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let mut pane_ids_final = tab.get_all_pane_ids(); pane_ids_final.sort(); assert_eq!(pane_ids_before, pane_ids_final); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn test_ctrl_scroll_down_in_stack_dissolves_stack_lists_before_mutation() { let size = Size { cols: 150, rows: 40, }; let client_id = 1; let mut output = Output::default(); let mut tab = create_new_tab_with_stacked_pane_list( size, ModeInfo::default(), true, Some(two_adjacent_stacks_layout()), ); tab.render(&mut output, None).unwrap(); let mut pane_ids_before = tab.get_all_pane_ids(); pane_ids_before.sort(); assert_eq!(pane_ids_before.len(), 6); assert_eq!(stack_member_pane_count(&tab), 6); tab.handle_mouse_event( &MouseEvent::new_ctrl_scroll_down_event(Position::new(5, 40)), client_id, ) .unwrap(); assert!( !tab.has_stack_lists(), "stack lists must be dissolved into in-grid stacks before the classic merge mutation" ); let mut in_grid_ids = tab.get_static_and_floating_pane_ids(); in_grid_ids.sort(); assert_eq!( in_grid_ids, pane_ids_before, "all panes must be present in the grid after the resize, none orphaned" ); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let mut pane_ids_final = tab.get_all_pane_ids(); pane_ids_final.sort(); assert_eq!(pane_ids_before, pane_ids_final); } #[test] fn resize_hint_text_tracks_mouse_scroll_resize_updates() { let size = Size { cols: 120, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); tab.vertical_split(PaneId::Terminal(2), None, client_id, None, None) .unwrap(); tab.mouse_help_text_visible.insert(client_id, true); tab.update_mouse_scroll_resize(false); let disabled_hints = tab.resolve_hint_text(client_id); assert!(!disabled_hints.is_empty()); assert!(disabled_hints .values() .all(|hint| !hint.text.contains("MouseScroll"))); tab.update_mouse_scroll_resize(true); let enabled_hints = tab.resolve_hint_text(client_id); assert!(!enabled_hints.is_empty()); assert!(enabled_hints .values() .all(|hint| hint.text.contains("MouseScroll"))); } #[test] fn in_place_pane_with_close_replaced_pane_false_restores_original() { // When an in-place pane is closed and close_replaced_pane=false (the default), // the pane that was replaced is restored to its original position. let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); // Create a second tiled pane so closing the in-place pane doesn't leave an empty tab tab.new_pane( PaneId::Terminal(2), None, None, false, false, NewPanePlacement::Tiled { direction: None, borderless: None, }, Some(client_id), None, ) .unwrap(); // Write distinguishing content to each pane tab.handle_pty_bytes(1, Vec::from("\n\n\nI am pane one".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am pane two".as_bytes())) .unwrap(); // Open pane 3 in-place of pane 2 without closing the replaced pane (suppress it) tab.new_in_place_pane( PaneId::Terminal(3), None, None, Some(PaneId::Terminal(2)), false, // close_replaced_pane Some(client_id), None, None, ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am the in-place pane".as_bytes())) .unwrap(); // Close the in-place pane — pane 2 should be restored tab.close_pane(PaneId::Terminal(3), false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshot shows both pane 1 and the restored pane 2 assert_snapshot!(snapshot); } #[test] fn in_place_pane_with_close_replaced_pane_true_closes_original() { // When an in-place pane is closed and close_replaced_pane=true, // the replaced pane is permanently destroyed rather than restored. let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); // Create a second tiled pane tab.new_pane( PaneId::Terminal(2), None, None, false, false, NewPanePlacement::Tiled { direction: None, borderless: None, }, Some(client_id), None, ) .unwrap(); tab.handle_pty_bytes(1, Vec::from("\n\n\nI am pane one".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("\n\n\nI am pane two".as_bytes())) .unwrap(); // Open pane 3 in-place of pane 2, closing the replaced pane permanently tab.new_in_place_pane( PaneId::Terminal(3), None, None, Some(PaneId::Terminal(2)), true, // close_replaced_pane Some(client_id), None, None, ) .unwrap(); tab.handle_pty_bytes(3, Vec::from("\n\n\nI am the in-place pane".as_bytes())) .unwrap(); // Close the in-place pane — pane 2 should NOT be restored (it was closed) tab.close_pane(PaneId::Terminal(3), false, None); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Snapshot shows only pane 1 (pane 2 was permanently closed, not restored) assert_snapshot!(snapshot); } // ===================================================================== // Plugin Highlight Mouse Integration Tests // ===================================================================== use zellij_utils::data::{HighlightLayer, HighlightStyle, RegexHighlight}; fn create_new_tab_with_plugin_receiver( size: Size, default_mode: ModeInfo, ) -> (Tab, Receiver<(PluginInstruction, ErrorContext)>) { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); let mut senders = ThreadSenders::default().silently_fail_on_send(); let (mock_plugin_sender, mock_plugin_receiver): ChannelWithContext = channels::unbounded(); senders.replace_to_plugin(SenderWithContext::new(mock_plugin_sender)); let max_panes = None; let mode_info = default_mode; let style = Style::default(); let draw_pane_frames = PaneFrameStyle::Full; let auto_layout = true; let client_id = 1; let session_is_mirrored = true; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let debug = false; let arrow_fonts = true; let styled_underlines = true; let osc8_hyperlinks = true; let explicitly_disable_kitty_keyboard_protocol = false; let advanced_mouse_actions = true; let mouse_scroll_resize = true; let web_sharing = WebSharing::Off; let web_server_ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let web_server_port = 8080; let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, style, mode_info, draw_pane_frames, auto_layout, connected_clients, session_is_mirrored, Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), PathBuf::from("my_default_shell"), debug, arrow_fonts, styled_underlines, osc8_hyperlinks, explicitly_disable_kitty_keyboard_protocol, None, false, web_sharing, current_group, currently_marking_pane_group, advanced_mouse_actions, mouse_scroll_resize, true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through web_server_ip, web_server_port, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); (tab, mock_plugin_receiver) } #[test] fn click_on_plugin_highlight_sends_highlight_clicked() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); // Feed PTY bytes to pane 1 tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes())) .unwrap(); // Set plugin highlight on pane 1 for pattern "foo" let highlights = vec![RegexHighlight { pattern: "foo".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane( PaneId::Terminal(1), 42, highlights, &Style::default(), ); // Alt+Click at position where "foo" appears // With draw_pane_frames=true, content starts at row 1, col 1 // "click here foo bar" -> "foo" starts at offset 11, so col = 1 + 11 = 12 let click_position = Position::new(1, 12); let mut alt_click = MouseEvent::new_left_press_event(click_position); alt_click.alt = true; let effect = tab.handle_mouse_event(&alt_click, client_id).unwrap(); assert!(effect.state_changed); // Check that HighlightClicked was sent to the plugin channel let mut found_highlight_clicked = false; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if let PluginInstruction::HighlightClicked { plugin_id, pane_id, pattern, matched_string, .. } = instruction { assert_eq!(plugin_id, 42); assert_eq!(pane_id, PaneId::Terminal(1)); assert_eq!(pattern, "foo"); assert_eq!(matched_string, "foo"); found_highlight_clicked = true; break; } } assert!( found_highlight_clicked, "Expected HighlightClicked instruction to be sent" ); } #[test] fn click_outside_highlight_starts_normal_selection() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes())) .unwrap(); let highlights = vec![RegexHighlight { pattern: "foo".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane( PaneId::Terminal(1), 42, highlights, &Style::default(), ); // Click on "click" (col 1), not on "foo" let click_position = Position::new(1, 1); let _effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id) .unwrap(); // No HighlightClicked should be sent let mut found_highlight_clicked = false; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if matches!(instruction, PluginInstruction::HighlightClicked { .. }) { found_highlight_clicked = true; } } assert!( !found_highlight_clicked, "HighlightClicked should NOT be sent for miss" ); // Selection should have started assert!(tab.selecting_with_mouse_in_pane.is_some()); } #[test] fn hover_over_highlight_shows_styling() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes(1, Vec::from("hello link_text bar".as_bytes())) .unwrap(); let highlights = vec![RegexHighlight { pattern: "link_text".into(), style: HighlightStyle::None, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: true, bold: false, italic: true, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default()); // Hover over the match (link_text starts at offset 6, with frame offset col = 7) let hover_position = Position::new(1, 9); let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(hover_position), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_with_hover = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot_with_hover); // Move hover away from the match let mut output2 = Output::default(); let far_position = Position::new(1, 1); let _effect2 = tab .handle_mouse_event(&MouseEvent::new_buttonless_motion(far_position), client_id) .unwrap(); tab.render(&mut output2, None).unwrap(); let snapshot_without_hover = take_snapshot( output2.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot_without_hover); } #[test] fn hover_on_unfocused_pane_no_highlight() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); let mut output = Output::default(); // Create a vertical split tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); tab.handle_pty_bytes(1, Vec::from("link_text".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("right pane".as_bytes())) .unwrap(); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Set on_hover highlight on pane 1 (left, unfocused) let highlights = vec![RegexHighlight { pattern: "link_text".into(), style: HighlightStyle::None, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: true, bold: false, italic: true, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default()); // Hover over left pane (unfocused) let hover_position = Position::new(1, 5); let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(hover_position), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // Hover highlight should NOT be visible on the unfocused pane assert_snapshot!(snapshot); } #[test] fn highlight_click_in_mouse_mode_pane_suppressed() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); // Feed PTY bytes that enable mouse tracking (SGR mode + any-event tracking) tab.handle_pty_bytes(1, Vec::from("\x1b[?1006h\x1b[?1003h".as_bytes())) .unwrap(); // Then feed text with matchable content tab.handle_pty_bytes(1, Vec::from("foo bar baz".as_bytes())) .unwrap(); // Set plugin highlight let highlights = vec![RegexHighlight { pattern: "foo".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane( PaneId::Terminal(1), 42, highlights, &Style::default(), ); // Left-click on the highlight position // With mouse tracking, the click goes through SendToTerminal, not StartSelection let click_position = Position::new(1, 1); let _effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id) .unwrap(); // No HighlightClicked should be sent (the click is forwarded to the terminal) let mut found_highlight_clicked = false; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if matches!(instruction, PluginInstruction::HighlightClicked { .. }) { found_highlight_clicked = true; } } assert!( !found_highlight_clicked, "HighlightClicked should NOT be sent when terminal has mouse tracking" ); } #[test] fn set_and_clear_highlights_across_tiled_and_floating() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let pane2 = PaneId::Terminal(2); let pane3 = PaneId::Terminal(3); // Create a second tiled pane tab.vertical_split(pane2, None, client_id, None, None) .unwrap(); // Create a floating pane tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( pane3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); // Feed content to all panes tab.handle_pty_bytes(1, Vec::from("aaa bbb".as_bytes())) .unwrap(); tab.handle_pty_bytes(2, Vec::from("aaa bbb".as_bytes())) .unwrap(); tab.handle_pty_bytes(3, Vec::from("aaa bbb".as_bytes())) .unwrap(); // Set highlights from plugin 1 (pattern "aaa") on all panes let h1 = vec![RegexHighlight { pattern: "aaa".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, h1.clone(), &Style::default()); tab.set_plugin_regex_highlights_for_pane(pane2, 1, h1.clone(), &Style::default()); tab.set_plugin_regex_highlights_for_pane(pane3, 1, h1.clone(), &Style::default()); // Set highlights from plugin 2 (pattern "bbb") on all panes let h2 = vec![RegexHighlight { pattern: "bbb".into(), style: HighlightStyle::Emphasis1, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: false, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 2, h2.clone(), &Style::default()); tab.set_plugin_regex_highlights_for_pane(pane2, 2, h2.clone(), &Style::default()); tab.set_plugin_regex_highlights_for_pane(pane3, 2, h2.clone(), &Style::default()); // Clear plugin 1 highlights across all panes tab.clear_all_plugin_highlights(1); // Verify plugin 2 highlights remain — we can use the tab render to snapshot, // or directly check that plugin 2 pattern still matches on pane 1 // Since clear_all_plugin_highlights iterates all panes, we test via // the set_plugin_regex_highlights_for_pane / clear flow: // Plugin 2 "bbb" highlights should still be active. let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot); } #[test] fn plain_click_on_highlight_does_not_send_highlight_clicked() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes())) .unwrap(); let highlights = vec![RegexHighlight { pattern: "foo".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane( PaneId::Terminal(1), 42, highlights, &Style::default(), ); // Plain left-click on "foo" — should start selection, NOT send HighlightClicked let click_position = Position::new(1, 12); let _effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id) .unwrap(); let mut found_highlight_clicked = false; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if matches!(instruction, PluginInstruction::HighlightClicked { .. }) { found_highlight_clicked = true; } } assert!( !found_highlight_clicked, "Plain click should NOT send HighlightClicked after Alt+Click change" ); // Selection should have started instead assert!(tab.selecting_with_mouse_in_pane.is_some()); } #[test] fn alt_click_outside_highlight_falls_through_to_group_toggle() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("click here foo bar".as_bytes())) .unwrap(); let highlights = vec![RegexHighlight { pattern: "foo".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane( PaneId::Terminal(1), 42, highlights, &Style::default(), ); // Alt+Click on "click" (col 1), not on "foo" — should fall through to group toggle let click_position = Position::new(1, 1); let mut alt_click = MouseEvent::new_left_press_event(click_position); alt_click.alt = true; let effect = tab.handle_mouse_event(&alt_click, client_id).unwrap(); // No HighlightClicked should be sent let mut found_highlight_clicked = false; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if matches!(instruction, PluginInstruction::HighlightClicked { .. }) { found_highlight_clicked = true; } } assert!( !found_highlight_clicked, "Alt+Click outside highlight should NOT send HighlightClicked" ); // Group toggle effect should have been produced assert_eq!(effect.group_toggle, Some(PaneId::Terminal(1))); } #[test] fn hover_over_highlight_with_tooltip_caches_tooltip() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes(1, Vec::from("hello link_text bar".as_bytes())) .unwrap(); let highlights = vec![RegexHighlight { pattern: "link_text".into(), style: HighlightStyle::None, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: true, bold: false, italic: true, underline: true, tooltip_text: Some("Open".to_string()), }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 1, highlights, &Style::default()); // Hover over the match (link_text starts at offset 6, with frame offset col = 7) let hover_position = Position::new(1, 9); let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(hover_position), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot_with_tooltip = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // The bottom frame row should contain the tooltip text assert_snapshot!(snapshot_with_tooltip); // Move hover away — tooltip should disappear let mut output2 = Output::default(); let far_position = Position::new(1, 1); let _effect2 = tab .handle_mouse_event(&MouseEvent::new_buttonless_motion(far_position), client_id) .unwrap(); tab.render(&mut output2, None).unwrap(); let snapshot_without_tooltip = take_snapshot( output2.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); assert_snapshot!(snapshot_without_tooltip); } // ===================================================================== // Highlight Layer Priority Integration Tests // ===================================================================== #[test] fn higher_layer_highlight_style_wins_in_rendered_output() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes(1, Vec::from("overlap_text here\n".as_bytes())) .unwrap(); // Plugin 1 (id: 10): Hint layer, underline only let h1 = vec![RegexHighlight { pattern: "overlap_text".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; // Plugin 2 (id: 20): Tool layer, bold only let h2 = vec![RegexHighlight { pattern: "overlap_text".into(), style: HighlightStyle::Emphasis1, layer: HighlightLayer::Tool, context: BTreeMap::new(), on_hover: false, bold: true, italic: false, underline: false, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 10, h1, &Style::default()); tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 20, h2, &Style::default()); tab.render(&mut output, None).unwrap(); let raw_output = output.serialize().unwrap(); let raw_str = raw_output.get(&client_id).unwrap(); // The renderer emits a series of individual SGR codes before the // highlighted text. Extract the full escape prefix preceding // "overlap_text" and verify that bold (\x1b[1m) is present and // underline (\x1b[4m) is absent. let idx = raw_str .find("overlap_text") .expect("overlap_text should appear in raw output"); let preceding = &raw_str[..idx]; // Walk backwards to find the reset (\x1b[m) that starts this cell's styling let reset_pos = preceding .rfind("\x1b[m") .expect("should find a reset before overlap_text"); let sgr_block = &raw_str[reset_pos..idx]; assert!( sgr_block.contains("\x1b[1m"), "Tool layer bold styling (\\x1b[1m) should be present before overlap_text, got: {:?}", sgr_block ); assert!( !sgr_block.contains("\x1b[4m"), "Hint layer underline styling (\\x1b[4m) should NOT be present before overlap_text, got: {:?}", sgr_block ); } #[test] fn click_dispatches_to_higher_layer_plugin() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let (mut tab, mock_plugin_receiver) = create_new_tab_with_plugin_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("click here shared_text bar\n".as_bytes())) .unwrap(); // Plugin 1 (id: 10): Hint layer let h1 = vec![RegexHighlight { pattern: "shared_text".into(), style: HighlightStyle::Emphasis0, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; // Plugin 2 (id: 20): Tool layer let h2 = vec![RegexHighlight { pattern: "shared_text".into(), style: HighlightStyle::Emphasis1, layer: HighlightLayer::Tool, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 10, h1, &Style::default()); tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 20, h2, &Style::default()); // Alt+Click on "shared_text" (starts at offset 11, frame adds +1 => col 12) let click_position = Position::new(1, 12); let mut alt_click = MouseEvent::new_left_press_event(click_position); alt_click.alt = true; let _effect = tab.handle_mouse_event(&alt_click, client_id).unwrap(); // Drain the plugin receiver and find the HighlightClicked instruction let mut found_plugin_id = None; while let Ok((instruction, _ctx)) = mock_plugin_receiver.try_recv() { if let PluginInstruction::HighlightClicked { plugin_id, .. } = instruction { found_plugin_id = Some(plugin_id); } } assert_eq!( found_plugin_id, Some(20), "HighlightClicked should be dispatched to the Tool layer plugin (id: 20)" ); } #[test] fn hover_tooltip_shows_higher_layer_tooltip() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes(1, Vec::from("hover here tooltipped bar\n".as_bytes())) .unwrap(); // Plugin 1 (id: 10): Hint layer with tooltip let h1 = vec![RegexHighlight { pattern: "tooltipped".into(), style: HighlightStyle::None, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: true, bold: false, italic: true, underline: true, tooltip_text: Some("Hint Tooltip".to_string()), }]; // Plugin 2 (id: 20): Tool layer with tooltip let h2 = vec![RegexHighlight { pattern: "tooltipped".into(), style: HighlightStyle::None, layer: HighlightLayer::Tool, context: BTreeMap::new(), on_hover: true, bold: false, italic: true, underline: true, tooltip_text: Some("Tool Tooltip".to_string()), }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 10, h1, &Style::default()); tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 20, h2, &Style::default()); // Hover over "tooltipped" (starts at offset 11, frame adds +1 => col 12) let hover_position = Position::new(1, 12); let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(hover_position), client_id, ) .unwrap(); tab.render(&mut output, None).unwrap(); let snapshot = take_snapshot( output.serialize().unwrap().get(&client_id).unwrap(), size.rows, size.cols, Palette::default(), ); // The bottom frame row should contain "Tool Tooltip" from the Tool layer assert_snapshot!(snapshot); } #[test] fn non_overlapping_highlights_from_different_layers_coexist() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut output = Output::default(); tab.handle_pty_bytes(1, Vec::from("aaa bbb ccc\n".as_bytes())) .unwrap(); // Plugin 1 (id: 10): Hint layer, underline on "aaa" let h1 = vec![RegexHighlight { pattern: "aaa".into(), style: HighlightStyle::None, layer: HighlightLayer::Hint, context: BTreeMap::new(), on_hover: false, bold: false, italic: false, underline: true, tooltip_text: None, }]; // Plugin 2 (id: 20): Tool layer, bold on "bbb" let h2 = vec![RegexHighlight { pattern: "bbb".into(), style: HighlightStyle::None, layer: HighlightLayer::Tool, context: BTreeMap::new(), on_hover: false, bold: true, italic: false, underline: false, tooltip_text: None, }]; // Plugin 3 (id: 30): ActionFeedback layer, italic on "ccc" let h3 = vec![RegexHighlight { pattern: "ccc".into(), style: HighlightStyle::None, layer: HighlightLayer::ActionFeedback, context: BTreeMap::new(), on_hover: false, bold: false, italic: true, underline: false, tooltip_text: None, }]; tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 10, h1, &Style::default()); tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 20, h2, &Style::default()); tab.set_plugin_regex_highlights_for_pane(PaneId::Terminal(1), 30, h3, &Style::default()); tab.render(&mut output, None).unwrap(); let raw_output = output.serialize().unwrap(); let raw_str = raw_output.get(&client_id).unwrap(); // The renderer emits individual SGR codes in sequence before each // highlighted word. Extract the escape prefix block before each word // and verify the expected attribute is present. // Helper: extract the SGR block between the last \x1b[m reset and the text let sgr_before = |text: &str| -> String { let idx = raw_str .find(text) .unwrap_or_else(|| panic!("{} not found in raw output", text)); let preceding = &raw_str[..idx]; let reset_pos = preceding .rfind("\x1b[m") .unwrap_or_else(|| panic!("no reset before {}", text)); raw_str[reset_pos..idx].to_string() }; let aaa_sgr = sgr_before("aaa"); let bbb_sgr = sgr_before("bbb"); let ccc_sgr = sgr_before("ccc"); assert!( aaa_sgr.contains("\x1b[4m"), "Hint layer underline (\\x1b[4m) should be applied to 'aaa', got: {:?}", aaa_sgr ); assert!( bbb_sgr.contains("\x1b[1m"), "Tool layer bold (\\x1b[1m) should be applied to 'bbb', got: {:?}", bbb_sgr ); assert!( ccc_sgr.contains("\x1b[3m"), "ActionFeedback layer italic (\\x1b[3m) should be applied to 'ccc', got: {:?}", ccc_sgr ); } #[test] fn focus_follows_mouse_focuses_tiled_pane_on_hover() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); // Create a vertical split: pane 1 (left), pane 2 (right) tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Active pane is pane 2 (right) after split assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over the left pane (pane 1) — column 30 is in the left half of 121 cols let effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); // Focus should have changed to pane 1 assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); } #[test] fn focus_follows_mouse_disabled_does_not_change_focus() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Active pane is pane 2 (right) after split assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // focus_follows_mouse is false by default — do NOT enable it // Hover over the left pane (pane 1) let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); // Focus should remain on pane 2 assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); } #[test] fn focus_follows_mouse_focuses_floating_pane_on_hover() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let floating_pane_id_1 = PaneId::Terminal(2); let floating_pane_id_2 = PaneId::Terminal(3); let coordinates_1 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; let coordinates_2 = FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(20)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), }; tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id_1, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_1)), Some(client_id), None, ) .unwrap(); tab.new_pane( floating_pane_id_2, None, None, false, true, NewPanePlacement::Floating(Some(coordinates_2)), Some(client_id), None, ) .unwrap(); // Active pane is floating_pane_id_2 (last created) assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_2)); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over floating pane 1 (at coordinates x=5, y=5, so position row=7, col=7 is inside it) let effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(7, 7)), client_id, ) .unwrap(); // Focus should have changed to floating pane 1 assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_1)); } #[test] fn focus_follows_mouse_skips_unselectable_panes() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Mark pane 1 (left) as unselectable tab.set_pane_selectable(PaneId::Terminal(1), false); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over the unselectable left pane (pane 1) let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); // Focus should remain on pane 2 — unselectable panes are skipped assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); } #[test] fn focus_follows_mouse_no_op_on_active_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over the already-active right pane (pane 2) — column 90 is in the right half let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 90)), client_id, ) .unwrap(); // Focus should remain on pane 2 assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); } #[test] fn focus_follows_mouse_respects_live_toggle() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Active pane is pane 2 (right) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable focus follows mouse and hover left — should switch focus tab.update_focus_follows_mouse(true); tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Disable focus follows mouse (simulating live config reload) tab.update_focus_follows_mouse(false); // Hover back to the right pane — should NOT switch focus tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 90)), client_id, ) .unwrap(); // Focus should remain on pane 1 (where it was left) assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); } #[test] fn focus_follows_mouse_repeated_hover_same_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); tab.update_focus_follows_mouse(true); // First hover over left pane — switches focus tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 30)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Second hover over same left pane at a different position — focus stays tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(5, 25)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); } #[test] fn focus_follows_mouse_ignores_tiled_pane_when_floating_visible() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); // Create a vertical split so there are two tiled panes tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Open floating layer with a floating pane let floating_pane_id = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id, None, None, false, true, NewPanePlacement::Floating(Some(FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(50)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(20)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), })), Some(client_id), None, ) .unwrap(); // Active pane is the floating pane assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id)); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over a tiled pane (left side, column 10) — should NOT change focus tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(10, 10)), client_id, ) .unwrap(); // Focus should remain on the floating pane assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id)); } #[test] fn focus_follows_mouse_focuses_floating_pane_when_floating_visible() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); // Open floating layer with two floating panes let floating_pane_id_1 = PaneId::Terminal(2); let floating_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id_1, None, None, false, true, NewPanePlacement::Floating(Some(FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(5)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), })), Some(client_id), None, ) .unwrap(); tab.new_pane( floating_pane_id_2, None, None, false, true, NewPanePlacement::Floating(Some(FloatingPaneCoordinates { x: Some(PercentOrFixed::Fixed(50)), y: Some(PercentOrFixed::Fixed(5)), width: Some(PercentOrFixed::Fixed(10)), height: Some(PercentOrFixed::Fixed(10)), pinned: None, borderless: Some(false), })), Some(client_id), None, ) .unwrap(); // Active pane is floating_pane_id_2 (last created) assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_2)); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Hover over floating pane 1 (at x=5, y=5 — position row=7, col=7 is inside it) let effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(7, 7)), client_id, ) .unwrap(); // Focus should have changed to floating pane 1 assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_1)); } #[test] fn focus_follows_mouse_skips_stacked_one_liner_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let swap_layouts = r#" layout { swap_tiled_layout { tab { pane split_direction="vertical" { pane pane stacked=true { children; } } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let stacked_resize = true; let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, stacked_resize, ); // Add panes so the stack has one-liners let new_pane_id_1 = PaneId::Terminal(2); let new_pane_id_2 = PaneId::Terminal(3); let new_pane_id_3 = PaneId::Terminal(4); tab.new_pane( new_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( new_pane_id_3, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); // Focus the left (non-stacked) tiled pane let _ = tab.move_focus_left(client_id); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Enable focus follows mouse tab.update_focus_follows_mouse(true); // Find a one-liner pane dynamically by inspecting geometries let one_liner_geom = tab .get_all_pane_ids() .iter() .filter_map(|pid| { let pane = tab.get_pane_with_id(*pid)?; let geom = pane.current_geom(); if geom.is_stacked() && geom.rows.is_fixed() { Some(geom) } else { None } }) .next() .expect("Expected at least one stacked one-liner pane"); // Hover in the middle of the one-liner pane let hover_row = one_liner_geom.y as i32; let hover_col = (one_liner_geom.x + one_liner_geom.cols.as_usize() / 2) as u16; let _effect = tab .handle_mouse_event( &MouseEvent::new_buttonless_motion(Position::new(hover_row, hover_col)), client_id, ) .unwrap(); // Focus should remain on the left tiled pane — the one-liner should be skipped assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); } #[test] fn mouse_click_through_sends_click_to_newly_focused_tiled_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Active pane is pane 2 (right) after split assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable SGR mouse mode on pane 1 (left, inactive) let sgr_mouse_mode = String::from("\u{1b}[?1000;1006h"); tab.handle_pty_bytes(1, sgr_mouse_mode.as_bytes().to_vec()) .unwrap(); // Enable click-through tab.update_mouse_click_through(true); // Click on pane 1 (left half) — column 30 is in the left pane of a 121-col split let click_position = Position::new(5, 30); let effect = tab .handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id) .unwrap(); assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); pty_instruction_bus.exit(); // Verify the SGR left-click escape sequence was sent to the terminal. let output = pty_instruction_bus.clone_output(); assert!( !output.is_empty(), "Expected click to be forwarded to terminal" ); // The output should contain an SGR mouse press sequence: \x1b[<0;COL;ROWM assert!( output[0].starts_with("\u{1b}[<0;"), "Expected SGR left-click sequence, got: {:?}", output[0] ); } #[test] fn mouse_click_through_disabled_does_not_send_click_to_focused_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Enable SGR mouse mode on pane 1 (inactive) let sgr_mouse_mode = String::from("\u{1b}[?1000;1006h"); tab.handle_pty_bytes(1, sgr_mouse_mode.as_bytes().to_vec()) .unwrap(); // mouse_click_through is false by default — do NOT enable it // Click on pane 1 let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); // Focus changed assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); pty_instruction_bus.exit(); // No click was forwarded let output = pty_instruction_bus.clone_output(); assert!( output.is_empty(), "Expected no click forwarded, got: {:?}", output ); } #[test] fn mouse_click_through_sends_click_to_newly_focused_floating_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); // Create two floating panes let floating_pane_id_1 = PaneId::Terminal(2); let floating_pane_id_2 = PaneId::Terminal(3); tab.toggle_floating_panes(Some(client_id), None, None) .unwrap(); tab.new_pane( floating_pane_id_1, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.new_pane( floating_pane_id_2, None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); // Enable SGR mouse mode on floating pane 1 let sgr_mouse_mode = String::from("\u{1b}[?1000;1006h"); tab.handle_pty_bytes(2, sgr_mouse_mode.as_bytes().to_vec()) .unwrap(); tab.update_mouse_click_through(true); // Click on floating pane 1 (need to find its position — render first) let mut output = Output::default(); tab.render(&mut output, None).unwrap(); // Focus floating pane 2 first to make pane 1 inactive tab.focus_pane_with_id(floating_pane_id_2, false, false, client_id) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(floating_pane_id_2)); // Now click on floating pane 1's area let _effect = tab .handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); // Verify focus changed and click was forwarded if tab.get_active_pane_id(client_id) == Some(floating_pane_id_1) { pty_instruction_bus.exit(); let pty_output = pty_instruction_bus.clone_output(); assert!( !pty_output.is_empty(), "Expected click forwarded to floating pane terminal" ); } else { pty_instruction_bus.exit(); // Position may not have landed on floating pane 1 — this is a layout-dependent test. // Adjust the position if this branch is hit. } } #[test] fn mouse_click_through_does_not_apply_when_focus_follows_mouse_is_enabled() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Enable SGR mouse mode on pane 1 let sgr_mouse_mode = String::from("\u{1b}[?1000;1006h"); tab.handle_pty_bytes(1, sgr_mouse_mode.as_bytes().to_vec()) .unwrap(); // Enable BOTH features tab.update_focus_follows_mouse(true); tab.update_mouse_click_through(true); // Step 1: Hover over pane 1 — this focuses it via focus-follows-mouse let hover_position = Position::new(5, 30); tab.handle_mouse_event( &MouseEvent::new_buttonless_motion(hover_position), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Step 2: Click on pane 1 — now this is a click on the ACTIVE pane // It should go through the normal SendToTerminal path let click_position = Position::new(5, 30); tab.handle_mouse_event(&MouseEvent::new_left_press_event(click_position), client_id) .unwrap(); pty_instruction_bus.exit(); // Click was sent to terminal via the normal path (not click-through) let output = pty_instruction_bus.clone_output(); assert!( !output.is_empty(), "Expected click sent to active pane terminal" ); } #[test] fn mouse_click_through_starts_selection_on_non_mouse_mode_pane() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Pane 1 is NOT in mouse mode — no SGR escape sent tab.update_mouse_click_through(true); // Click on pane 1 let effect = tab .handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); assert!(effect.state_changed); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Selection should have started assert_eq!( tab.selecting_with_mouse_in_pane, Some(PaneId::Terminal(1)), "Expected text selection to start on the newly focused pane" ); } #[test] fn mouse_click_through_respects_live_toggle() { let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut pty_instruction_bus = MockPtyInstructionBus::new(); let mut tab = create_new_tab_with_mock_pty_writer( size, ModeInfo::default(), pty_instruction_bus.pty_write_sender(), ); pty_instruction_bus.start(); let new_pane_id = PaneId::Terminal(2); tab.vertical_split(new_pane_id, None, client_id, None, None) .unwrap(); // Enable SGR mouse mode on pane 1 let sgr_mouse_mode = String::from("\u{1b}[?1000;1006h"); tab.handle_pty_bytes(1, sgr_mouse_mode.as_bytes().to_vec()) .unwrap(); // mouse_click_through starts disabled // Click pane 1 — should only focus, no forwarding tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); // Click pane 2 to refocus it tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 90)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(2))); // Now enable click-through tab.update_mouse_click_through(true); // Click pane 1 again — this time the click should be forwarded tab.handle_mouse_event( &MouseEvent::new_left_press_event(Position::new(5, 30)), client_id, ) .unwrap(); assert_eq!(tab.get_active_pane_id(client_id), Some(PaneId::Terminal(1))); pty_instruction_bus.exit(); // Only the LAST click (after enabling) should have produced terminal output let output = pty_instruction_bus.clone_output(); assert_eq!( output.len(), 1, "Expected exactly one forwarded click, got: {:?}", output ); assert!( output[0].starts_with("\u{1b}[<0;"), "Expected SGR left-click sequence, got: {:?}", output[0] ); } // ======================================================================== // OSC 99 Desktop Notification Integration Tests // ======================================================================== /// Creates a Tab with a real `to_server` sender so that `ServerInstruction::Render` /// calls from `forward_desktop_notifications` can be captured on the receiver. fn create_new_tab_with_server_receiver( size: Size, default_mode: ModeInfo, ) -> (Tab, Receiver<(ServerInstruction, ErrorContext)>) { set_session_name("test".into()); let index = 0; let position = 0; let name = String::new(); let os_api = Box::new(FakeInputOutput::default()); // Set up real server channel to capture ServerInstruction::Render let (server_sender, server_receiver) = channels::unbounded(); let server_sender = SenderWithContext::new(server_sender); let mut senders = ThreadSenders::default().silently_fail_on_send(); senders.to_server = Some(server_sender); let max_panes = None; let client_id = 1; let mut connected_clients = HashMap::new(); connected_clients.insert(client_id, false); let connected_clients = Rc::new(RefCell::new(connected_clients)); let character_cell_info = Rc::new(RefCell::new(None)); let stacked_resize = Rc::new(RefCell::new(true)); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let copy_options = CopyOptions::default(); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let current_group = Rc::new(RefCell::new(PaneGroups::new(ThreadSenders::default()))); let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new())); let mut tab = Tab::new( index, position, name, size, character_cell_info, stacked_resize, Rc::new(RefCell::new(false)), sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), os_api, senders, max_panes, Style::default(), default_mode, PaneFrameStyle::Full, true, // auto_layout connected_clients, true, // session_is_mirrored Some(client_id), copy_options, terminal_emulator_colors, terminal_emulator_color_codes, (vec![], vec![]), PathBuf::from("my_default_shell"), false, // debug true, // arrow_fonts true, // styled_underlines true, // osc8_hyperlinks false, // explicitly_disable_kitty_keyboard_protocol None, false, WebSharing::Off, current_group, currently_marking_pane_group, true, // advanced_mouse_actions true, // mouse_scroll_resize true, // mouse_hover_effects false, // focus_follows_mouse false, // mouse_click_through IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 8080, 0, // mobile_tab_count ); tab.apply_layout( TiledPaneLayout::default(), vec![], vec![(1, None)], vec![], HashMap::new(), client_id, None, ) .unwrap(); (tab, server_receiver) } /// Helper: collect all ServerInstruction::Render messages from the receiver, /// concatenate the per-client strings, and return the combined output. fn collect_render_output(receiver: &Receiver<(ServerInstruction, ErrorContext)>) -> String { let mut output = String::new(); while let Ok((instruction, _)) = receiver.try_recv() { if let ServerInstruction::Render(Some(client_map)) = instruction { for (_client_id, content) in client_map { output.push_str(&content); } } } output } #[test] fn osc99_notification_forwarded_through_tab_to_server_render() { // Integration test: OSC 99 sequence emitted by a pane's PTY is parsed by Grid, // drained by Tab, namespaced, and forwarded via ServerInstruction::Render. let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); // Feed OSC 99 sequence to pane (pid=1) — BEL terminated tab.handle_pty_bytes(1, Vec::from("\x1b]99;i=test1:p=title;Hello World\x07")) .unwrap(); let output = collect_render_output(&server_receiver); // The forwarded output should contain a namespaced OSC 99 sequence assert!( output.contains("\x1b]99;"), "Render output should contain OSC 99 prefix, got: {:?}", output ); // The identifier should be namespaced with pane ID (pid=1) assert!( output.contains("i=p1.test1"), "Identifier should be namespaced as i=p1.test1, got: {:?}", output ); // The payload should be preserved assert!( output.contains("Hello World"), "Payload 'Hello World' should be forwarded, got: {:?}", output ); } #[test] fn osc99_notification_st_terminator_forwarded() { // Same as above but with ESC \ (ST) terminator let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("\x1b]99;i=test2:p=title;Build done\x1b\\")) .unwrap(); let output = collect_render_output(&server_receiver); assert!( output.contains("i=p1.test2"), "Identifier should be namespaced, got: {:?}", output ); assert!( output.contains("Build done"), "Payload should be forwarded, got: {:?}", output ); } #[test] fn osc99_notification_without_identifier_gets_default() { // When no i= key is present, namespace_notification_id adds i=p.0 let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("\x1b]99;p=title;No ID here\x07")) .unwrap(); let output = collect_render_output(&server_receiver); assert!( output.contains("i=p1.:"), "Missing i= should get default i=p1. (empty original id), got: {:?}", output ); assert!( output.contains("No ID here"), "Payload should be forwarded, got: {:?}", output ); } #[test] fn osc99_multiple_notifications_forwarded() { // Multiple OSC 99 sequences in one PTY write should all be forwarded let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); tab.handle_pty_bytes( 1, Vec::from("\x1b]99;i=n1:p=title;First\x07\x1b]99;i=n2:p=title;Second\x07"), ) .unwrap(); let output = collect_render_output(&server_receiver); assert!( output.contains("i=p1.n1"), "First notification should be namespaced, got: {:?}", output ); assert!( output.contains("i=p1.n2"), "Second notification should be namespaced, got: {:?}", output ); assert!( output.contains("First") && output.contains("Second"), "Both payloads should be forwarded, got: {:?}", output ); } #[test] fn osc99_notification_mixed_with_regular_output() { // OSC 99 embedded in regular terminal output should be extracted and forwarded let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); tab.handle_pty_bytes( 1, Vec::from("hello\x1b]99;i=mid:p=title;Inline notification\x07world"), ) .unwrap(); let output = collect_render_output(&server_receiver); assert!( output.contains("i=p1.mid"), "Notification should be forwarded, got: {:?}", output ); assert!( output.contains("Inline notification"), "Notification payload should be present, got: {:?}", output ); } #[test] fn osc99_notification_preserves_metadata_keys() { // All metadata keys (p=, a=, u=, etc.) should be preserved through forwarding let size = Size { cols: 80, rows: 24 }; let (mut tab, server_receiver) = create_new_tab_with_server_receiver(size, ModeInfo::default()); tab.handle_pty_bytes( 1, Vec::from("\x1b]99;i=rich:p=title:a=report:u=2;Important!\x07"), ) .unwrap(); let output = collect_render_output(&server_receiver); assert!( output.contains("i=p1r.rich"), "Identifier should be namespaced with 'r' flag (a=report present), got: {:?}", output ); assert!( output.contains("p=title"), "p=title metadata should be preserved, got: {:?}", output ); assert!( output.contains("a=report"), "a=report metadata should be preserved, got: {:?}", output ); assert!( output.contains("u=2"), "u=2 metadata should be preserved, got: {:?}", output ); } #[test] fn osc99_grid_parses_and_stores_notification() { // Direct Grid-level test: feed OSC 99 bytes through vte parser, // verify pending_desktop_notifications is populated correctly. use crate::panes::grid::Grid; use crate::panes::link_handler::LinkHandler; let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let character_cell_size = Rc::new(RefCell::new(Some(SizeInPixels { width: 8, height: 21, }))); let mut grid = Grid::new( 24, 80, Rc::new(RefCell::new(Palette::default())), terminal_emulator_color_codes, Rc::new(RefCell::new(LinkHandler::new())), character_cell_size, sixel_image_store, Rc::new(RefCell::new(KittyImageStore::default())), Style::default(), false, // debug true, // arrow_fonts true, // styled_underlines true, // osc8_hyperlinks false, // explicitly_disable_kitty_keyboard_protocol ); // Feed OSC 99 through vte parser let mut vte_parser = vte::Parser::new(); vte_parser.advance( &mut grid, b"\x1b]99;i=gridtest:p=title;Grid notification\x07", ); assert_eq!( grid.pending_desktop_notifications.len(), 1, "Should have one pending notification" ); let (ref payload, ref _terminator) = grid.pending_desktop_notifications.first().unwrap(); assert!( payload.contains("i=gridtest"), "Payload should contain i=gridtest, got: {:?}", payload ); } #[test] fn osc99_namespace_denormalize_roundtrip() { // Test that namespace_notification_id and denormalize_notification_response // are inverse operations. use crate::panes::grid::namespace_notification_id; use crate::screen::denormalize_notification_response; let original_metadata = "i=mynotif:p=title:a=report"; let pane_id: u32 = 42; // Namespace — a=report means 'r' flag is set let namespaced = namespace_notification_id(original_metadata, pane_id); assert!( namespaced.contains("i=p42r.mynotif"), "Should namespace to i=p42r.mynotif (a=report → 'r' flag), got: {:?}", namespaced ); // Simulate a response with the namespaced ID let response_payload = format!("i=p42r.mynotif:p=close;activated"); let result = denormalize_notification_response(response_payload.as_bytes()); assert!(result.is_some(), "Should successfully denormalize"); let (terminal_id, app_wants_report, is_query, response_bytes) = result.unwrap(); assert_eq!(terminal_id, 42, "Should extract pane_id 42"); assert!( app_wants_report, "'r' flag in i=p42r.mynotif means app_wants_report should be true" ); assert!(!is_query, "No 'q' flag means is_query should be false"); let response_str = String::from_utf8_lossy(&response_bytes); assert!( response_str.contains("i=mynotif"), "Should restore original i=mynotif, got: {:?}", response_str ); assert!( !response_str.contains("p42"), "Should not contain namespaced prefix, got: {:?}", response_str ); } #[test] fn osc99_denormalize_with_no_namespace_returns_none() { use crate::screen::denormalize_notification_response; // A response without the p. namespace pattern should return None let result = denormalize_notification_response(b"i=plain_id;data"); assert!( result.is_none(), "Should return None for non-namespaced response" ); } #[test] fn osc99_namespace_without_identifier_adds_default() { use crate::panes::grid::namespace_notification_id; // With a=report → 'r' flag let metadata = "p=title:a=report"; let namespaced = namespace_notification_id(metadata, 7); assert!( namespaced.contains("i=p7r.:"), "Should add default i=p7r. when no i= present (a=report → 'r' flag), got: {:?}", namespaced ); // Without a=report → no 'r' flag let metadata = "p=title"; let namespaced = namespace_notification_id(metadata, 7); assert!( namespaced.contains("i=p7.:"), "Should add default i=p7. when no i= and no a=report, got: {:?}", namespaced ); } #[test] fn osc99_namespace_ensures_report_action() { use crate::panes::grid::namespace_notification_id; // a=focus → a=focus,report let result = namespace_notification_id("i=test:p=title:a=focus", 1); assert!( result.contains("a=focus,report"), "a=focus should be augmented with report, got: {:?}", result ); // a=report → unchanged let result = namespace_notification_id("i=test:p=title:a=report", 1); assert!( result.contains("a=report"), "a=report should be preserved, got: {:?}", result ); assert!( !result.contains("a=report,report"), "Should not duplicate report, got: {:?}", result ); // a=focus,report → unchanged let result = namespace_notification_id("i=test:p=title:a=focus,report", 1); assert!( result.contains("a=focus,report"), "a=focus,report should be preserved, got: {:?}", result ); // No a= key → a=report added let result = namespace_notification_id("i=test:p=title", 1); assert!( result.contains("a=report"), "Missing a= should get a=report appended, got: {:?}", result ); } #[test] fn osc99_report_flag_roundtrip() { use crate::panes::grid::namespace_notification_id; use crate::screen::denormalize_notification_response; // App sends a=report → namespaced with 'r' flag → denormalize returns app_wants_report=true let namespaced = namespace_notification_id("i=myid:p=title:a=report", 5); assert!( namespaced.contains("i=p5r.myid"), "a=report should produce 'r' flag in namespace, got: {:?}", namespaced ); let response = format!("i=p5r.myid;activated"); let (pane_id, wants_report, _is_query, _bytes) = denormalize_notification_response(response.as_bytes()).unwrap(); assert_eq!(pane_id, 5); assert!(wants_report, "Should detect 'r' flag as app_wants_report"); // App sends a=focus (no report) → namespaced without 'r' flag → denormalize returns false let namespaced = namespace_notification_id("i=myid:p=title:a=focus", 5); assert!( namespaced.contains("i=p5.myid"), "a=focus should NOT produce 'r' flag, got: {:?}", namespaced ); assert!( namespaced.contains("a=focus,report"), "a=focus should be augmented with report for the host terminal, got: {:?}", namespaced ); let response = format!("i=p5.myid;activated"); let (pane_id, wants_report, _is_query, _bytes) = denormalize_notification_response(response.as_bytes()).unwrap(); assert_eq!(pane_id, 5); assert!(!wants_report, "No 'r' flag means app did not want report"); } #[test] fn osc99_query_flag_roundtrip() { use crate::panes::grid::namespace_notification_id; use crate::screen::denormalize_notification_response; // Capability query (p=?) gets 'q' flag let namespaced = namespace_notification_id("i=qid:p=?", 3); assert!( namespaced.contains("i=p3q.qid"), "p=? should produce 'q' flag, got: {:?}", namespaced ); let response = format!("i=p3q.qid;p=title,body"); let (pane_id, wants_report, is_query, _bytes) = denormalize_notification_response(response.as_bytes()).unwrap(); assert_eq!(pane_id, 3); assert!(!wants_report); assert!(is_query, "Should detect 'q' flag"); // Both flags: a=report + p=? (unlikely but valid) let namespaced = namespace_notification_id("i=both:p=?:a=report", 3); assert!( namespaced.contains("i=p3rq.both"), "Both flags should be present, got: {:?}", namespaced ); let response = format!("i=p3rq.both;p=title,body"); let (pane_id, wants_report, is_query, _bytes) = denormalize_notification_response(response.as_bytes()).unwrap(); assert_eq!(pane_id, 3); assert!(wants_report); assert!(is_query); // Regular notification (no p=?, no a=report) — no flags let namespaced = namespace_notification_id("i=plain:p=title:a=focus", 3); assert!( namespaced.contains("i=p3.plain"), "No flags expected, got: {:?}", namespaced ); let response = format!("i=p3.plain;activated"); let (pane_id, wants_report, is_query, _bytes) = denormalize_notification_response(response.as_bytes()).unwrap(); assert_eq!(pane_id, 3); assert!(!wants_report); assert!(!is_query); } #[test] fn hidden_cursor_still_emits_cup_for_host_terminal_positioning() { let size = Size { cols: 80, rows: 24 }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); tab.handle_pty_bytes(1, Vec::from("\u{1b}[10;20H\u{1b}[?25l".as_bytes())) .unwrap(); let mut output = Output::default(); tab.render(&mut output, None).unwrap(); let serialized = output.serialize().unwrap(); let client_output = serialized.get(&client_id).unwrap(); assert!( client_output.contains("\u{1b}[?25l"), "Expected hide-cursor sequence in output" ); assert!( client_output.contains("\u{1b}[11;21H"), "Expected CUP sequence for hidden cursor positioning, got: {:?}", client_output ); assert!( !client_output.contains("\u{1b}[?25h"), "Show-cursor sequence must not be present when app has hidden the cursor" ); } #[test] fn sl0p_spike_pane_order_is_stable_across_a_stacked_swap_round_trip() { // SPIKE for size-responsive layouts: if panes do NOT return to their // original slots after going stacked and back, then "collapse to a stack on // a small terminal and pop back out when it grows" cannot work, because // every resize would reshuffle the user's arrangement. // // This asserts the round trip only. It says nothing about resize triggering. let size = Size { cols: 121, rows: 20, }; let client_id = 1; let swap_layouts = r#" layout { swap_tiled_layout name="tiled" { tab { pane split_direction="vertical" { pane pane pane pane } } } swap_tiled_layout name="stacked" { tab min_panes=2 { pane stacked=true { children; } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let swap_tiled_layouts = layout.swap_tiled_layouts.clone(); let swap_floating_layouts = layout.swap_floating_layouts.clone(); let mut tab = create_new_tab_with_swap_layouts( size, ModeInfo::default(), (swap_tiled_layouts, swap_floating_layouts), None, true, true, ); for pid in [2, 3, 4] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let geoms = |tab: &Tab| -> Vec<(PaneId, usize, usize, usize, usize)> { let mut v: Vec<_> = tab .tiled_panes .panes .iter() .map(|(id, p)| { let g = p.position_and_size(); (*id, g.x, g.y, g.cols.as_usize(), g.rows.as_usize()) }) .collect(); v.sort_by_key(|e| format!("{:?}", e.0)); v }; let before = geoms(&tab); eprintln!("BEFORE: {:?}", before); // to the stacked layout and back tab.next_swap_layout().unwrap(); let stacked = geoms(&tab); eprintln!("STACKED: {:?}", stacked); tab.next_swap_layout().unwrap(); let after = geoms(&tab); eprintln!("AFTER: {:?}", after); assert_ne!( before, stacked, "the stacked layout should actually change the geometry, or this spike proves nothing" ); assert_eq!( before, after, "panes must return to their original slots after a stacked round trip" ); } #[test] fn sl0p_responsive_layout_collapses_when_narrow_and_restores_when_wide() { let wide = Size { cols: 121, rows: 20, }; let client_id = 1; let swap_layouts = r#" layout { swap_tiled_layout name="tiled" { tab { pane split_direction="vertical" { pane pane pane } } } swap_tiled_layout name="stacked" { tab min_panes=2 { pane stacked=true { children; } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let mut tab = create_new_tab_with_swap_layouts( wide, ModeInfo::default(), ( layout.swap_tiled_layouts.clone(), layout.swap_floating_layouts.clone(), ), None, true, true, ); tab.update_responsive_layout(80, String::from("stacked")); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let widths = |tab: &Tab| -> Vec { let mut v: Vec = tab .tiled_panes .panes .values() .map(|p| p.position_and_size().cols.as_usize()) .collect(); v.sort_unstable(); v }; let all_full_width = |tab: &Tab, cols: usize| widths(tab).iter().all(|w| *w == cols); assert!( !all_full_width(&tab, wide.cols), "precondition: panes should be side by side while wide, got {:?}", widths(&tab) ); // shrink below the threshold -> collapse to the stacked layout let narrow = Size { cols: 70, rows: 20 }; tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "narrow terminal should collapse to a stack, got widths {:?}", widths(&tab) ); // just over the threshold is inside the hysteresis band -> stay collapsed let borderline = Size { cols: 85, rows: 20 }; tab.resize_whole_tab(borderline).unwrap(); assert!( all_full_width(&tab, borderline.cols), "inside the hysteresis band it must NOT flip back, got widths {:?}", widths(&tab) ); // clearly wide again -> restore the previous layout tab.resize_whole_tab(wide).unwrap(); assert!( !all_full_width(&tab, wide.cols), "a wide terminal should restore the side-by-side layout, got widths {:?}", widths(&tab) ); } #[test] fn sl0p_responsive_layout_restores_even_if_the_layout_is_damaged_while_collapsed() { // REGRESSION: the collapse/restore switch backs off when the user has // rearranged panes by hand (a "damaged" swap layout). But the damaged flag // is also set by resize_whole_tab's own auto_layout pass, which only clears // it again if it found a layout that fits -- and on a very narrow terminal // it can find none. // // If restore consulted that flag, a tab that had already collapsed would be // stranded in the narrow layout for the rest of the session: every // subsequent resize would see "damaged" and refuse to restore. Growing the // terminal back must always get the wide layout back. let wide = Size { cols: 121, rows: 20, }; let client_id = 1; let swap_layouts = r#" layout { swap_tiled_layout name="tiled" { tab { pane split_direction="vertical" { pane pane pane } } } swap_tiled_layout name="stacked" { tab min_panes=2 { pane stacked=true { children; } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let mut tab = create_new_tab_with_swap_layouts( wide, ModeInfo::default(), ( layout.swap_tiled_layouts.clone(), layout.swap_floating_layouts.clone(), ), None, true, true, ); tab.update_responsive_layout(80, String::from("stacked")); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let widths = |tab: &Tab| -> Vec { let mut v: Vec = tab .tiled_panes .panes .values() .map(|p| p.position_and_size().cols.as_usize()) .collect(); v.sort_unstable(); v }; let all_full_width = |tab: &Tab, cols: usize| widths(tab).iter().all(|w| *w == cols); // collapse let narrow = Size { cols: 70, rows: 20 }; tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "precondition: should have collapsed, got {:?}", widths(&tab) ); // now damage the layout WHILE collapsed, exactly as a failed auto_layout // pass on a narrow terminal would tab.swap_layouts.set_is_tiled_damaged(); // growing back must still restore, despite the damage tab.resize_whole_tab(wide).unwrap(); assert!( !all_full_width(&tab, wide.cols), "a collapsed tab must still restore when it grows back even if the \ layout was damaged while collapsed, got widths {:?}", widths(&tab) ); } #[test] fn sl0p_stack_list_entry_keeps_a_label_when_default_pane_names_are_hidden() { // `hide_default_pane_names` blanks the "Pane #N" placeholder, which is what // we want for a frame: the pane is still visible and positioned, so an empty // title just looks clean. // // The stack list is the opposite case: the label is the entire affordance, // so blanking it leaves rows that cannot be told apart -- which is exactly // what happens to freshly created panes, since nothing has named them yet. // The stack list must therefore keep a label regardless of the option. let size = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } tab.update_frame_layout( zellij_utils::data::PaneTitleAlignment::Left, 0, 0, zellij_utils::data::PanePadding::default(), 0, true, // hide_default_pane_names ); for (pane_id, pane) in tab.tiled_panes.panes.iter() { // precondition: the option really is blanking the frame title, or this // test would pass for the wrong reason assert_eq!( pane.current_title(), "", "precondition: {:?} should have a blank FRAME title while \ hide_default_pane_names is on", pane_id ); let label = pane.stack_list_entry_label(); assert!( !label.trim().is_empty(), "{:?} must still get a stack list label while \ hide_default_pane_names is on, got {:?}", pane_id, label ); assert!( label.contains("Pane #"), "the stack list label should fall back to the placeholder, got {:?}", label ); } } #[test] fn sl0p_stack_list_caps_its_rows_and_windows_around_the_focused_entry() { // The picker reserves one row per member plus a padding row, so a deep // stack eats the screen. `stack_list_max_rows` caps it and slides the // window to keep the focused member visible. let size = Size { cols: 121, rows: 40, }; let mut tab = create_new_tab(size, ModeInfo::default()); tab.update_stack_list_max_rows(3); // (member_count, visible_rank) -> (window_start, shown_count) let cases = vec![ // fewer members than the cap: show everything, no window ((2, 0), (0, 2)), ((3, 2), (0, 3)), // capped: window centred on the focused entry where it can be ((10, 0), (0, 3)), ((10, 1), (0, 3)), ((10, 2), (1, 3)), ((10, 5), (4, 3)), // clamped at the end, so the window never runs off the bottom ((10, 9), (7, 3)), ((10, 8), (7, 3)), ]; for ((member_count, visible_rank), expected) in cases { assert_eq!( { let (start, shown, _rows) = super::stack_list_window_core(member_count, visible_rank, 1, 3); (start, shown) }, expected, "window for {} members focused on rank {}", member_count, visible_rank ); } // a window must never exceed the cap, run past the end, or lose the focus for member_count in 1..30usize { for visible_rank in 0..member_count { let (start, shown, _rows) = super::stack_list_window_core(member_count, visible_rank, 1, 3); assert!(shown <= 3, "{} members: shown {} exceeds cap", member_count, shown); assert!( start + shown <= member_count, "{} members rank {}: window {}..{} runs past the end", member_count, visible_rank, start, start + shown ); assert!( visible_rank >= start && visible_rank < start + shown, "{} members rank {}: focused entry outside window {}..{}", member_count, visible_rank, start, start + shown ); } } } #[test] fn sl0p_stack_list_uncapped_by_default_shows_every_member() { let size = Size { cols: 121, rows: 40, }; let tab = create_new_tab(size, ModeInfo::default()); // 0 = uncapped, the historical behaviour for member_count in [1usize, 5, 12, 40] { assert_eq!( { let (start, shown, _rows) = super::stack_list_window_core(member_count, 0, 1, 0); (start, shown) }, (0, member_count), "uncapped should show all {} members", member_count ); } } #[test] fn sl0p_clicking_a_scrolled_stack_entry_selects_the_pane_it_shows() { // REGRESSION: the picker's row -> member mapping exists in TWO places -- // the renderer, and this hit-test used to resolve mouse events. Capping the // list made the renderer draw a WINDOW of members, so screen row 0 is no // longer members[0]. A hit-test that still assumes row N == member N picks // the wrong pane the moment the window scrolls. let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut members = vec![PaneId::Terminal(1)]; for pid in 2..=8 { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); members.push(PaneId::Terminal(pid)); } // register the stack list directly: we are testing the row arithmetic, not // the stack machinery that would normally build this let visible = members[7]; tab.stack_lists.insert( 0, super::StackList { members: members.clone(), visible, }, ); for member in &members { tab.stack_list_of_member.insert(*member, 0); } tab.update_stack_list_max_rows(3); // 8 members, cap 3, focused on rank 7 -> window clamps to ranks 5,6,7 let (window_start, shown, _rows) = super::stack_list_window_core(members.len(), 7, 1, 3); assert_eq!((window_start, shown), (5, 3), "precondition: window position"); let geom = tab.tiled_panes.get_pane(visible).unwrap().current_geom(); for row in 0..shown { let point = Position::new((geom.y + row) as i32, 0); assert_eq!( tab.stack_list_member_at_point(visible, &point), members[window_start + row], "row {} shows member {}, so clicking it must select that pane", row, window_start + row ); } } #[test] fn sl0p_clicking_the_indicator_row_expands_and_collapses_the_stack_list() { let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut members = vec![PaneId::Terminal(1)]; for pid in 2..=8 { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); members.push(PaneId::Terminal(pid)); } let visible = members[7]; tab.stack_lists.insert( 0, super::StackList { members: members.clone(), visible, }, ); for member in &members { tab.stack_list_of_member.insert(*member, 0); } tab.update_stack_list_max_rows(3); let geom = tab.tiled_panes.get_pane(visible).unwrap().current_geom(); let row_of = |row: usize| Position::new((geom.y + row) as i32, 0); // collapsed: 3 of 8 shown, indicator on the row after them let layout = tab.stack_list_layout_of(0).unwrap().2; assert_eq!((layout.rows, layout.hidden), (3, 5)); assert_eq!( tab.stack_list_indicator(0, layout.hidden), Some(String::from("+5 more")) ); // clicking a member row must NOT toggle assert!( !tab.toggle_stack_list_expansion_at_point(visible, &row_of(1)), "a click on a member row must not toggle the expansion" ); assert_eq!(tab.stack_list_layout_of(0).unwrap().2.rows, 3); // clicking the indicator row expands to every member assert!( tab.toggle_stack_list_expansion_at_point(visible, &row_of(3)), "a click on the indicator row should be consumed" ); let layout = tab.stack_list_layout_of(0).unwrap().2; assert_eq!( (layout.rows, layout.hidden, layout.cells.len()), (8, 0, 8), "expanded should show every member" ); assert_eq!( tab.stack_list_indicator(0, layout.hidden), Some(String::from("- fewer")), "expanded needs a visible way back, or the toggle is one-way" ); // the reserved rows must follow, or the list draws over the pane below it assert_eq!( tab.reserved_top_rows.borrow().get(&visible).copied(), Some(9), "expanded reserves one row per member plus the indicator" ); // and clicking it again collapses (indicator is now one row lower) assert!(tab.toggle_stack_list_expansion_at_point(visible, &row_of(8))); let layout = tab.stack_list_layout_of(0).unwrap().2; assert_eq!((layout.rows, layout.hidden), (3, 5)); assert_eq!( tab.reserved_top_rows.borrow().get(&visible).copied(), Some(4), "collapsed reserves the cap plus the indicator" ); } #[test] fn sl0p_stack_list_grid_packs_entries_into_fewer_rows() { let size = Size { cols: 121, rows: 40, }; let mut tab = create_new_tab(size, ModeInfo::default()); tab.update_stack_list_max_rows(3); // rows mode: 8 members, 3 rows, 5 hidden let rows_layout = tab.stack_list_layout(0, 8, 7, 120, 6); assert_eq!((rows_layout.rows, rows_layout.hidden), (3, 5)); assert!( rows_layout.cells.iter().all(|c| c.width == 120 && c.x_offset == 0), "rows mode must keep full-width entries, got {:?}", rows_layout.cells ); // grid mode: label 6 + 6 decoration = 12 wide cells, so 120 cols -> 10 // columns and all 8 members fit on ONE row within the same budget tab.update_stack_list_grid(true); let grid_layout = tab.stack_list_layout(0, 8, 7, 120, 6); assert_eq!( (grid_layout.rows, grid_layout.hidden, grid_layout.cells.len()), (1, 0, 8), "the grid should fit every member in one row here" ); // cells must tile: same row -> no overlap, and never past the edge for cell in &grid_layout.cells { assert!( cell.x_offset + cell.width <= 120, "cell {:?} runs past the available width", cell ); } for a in &grid_layout.cells { for b in &grid_layout.cells { if a.member_index >= b.member_index || a.row != b.row { continue; } assert!( a.x_offset + a.width <= b.x_offset || b.x_offset + b.width <= a.x_offset, "cells {:?} and {:?} overlap", a, b ); } } } #[test] fn sl0p_stack_list_grid_respects_the_row_budget_and_stays_addressable() { let size = Size { cols: 121, rows: 40, }; let mut tab = create_new_tab(size, ModeInfo::default()); tab.update_stack_list_grid(true); for max_rows in [1usize, 2, 3, 5] { tab.update_stack_list_max_rows(max_rows); for member_count in 1..40usize { for visible_rank in 0..member_count { let layout = tab.stack_list_layout(0, member_count, visible_rank, 120, 6); assert!( layout.rows <= max_rows, "{} members cap {}: used {} rows", member_count, max_rows, layout.rows ); assert_eq!( layout.cells.len() + layout.hidden, member_count, "every member must be either shown or counted as hidden" ); // the focused member must always be reachable without scrolling assert!( layout .cells .iter() .any(|c| c.member_index == visible_rank), "{} members cap {} rank {}: focused member not in the layout", member_count, max_rows, visible_rank ); } } } } #[test] fn sl0p_clicking_a_grid_cell_selects_the_member_that_cell_shows() { // The grid gives the row -> member mapping a COLUMN axis. That mapping // already selected the wrong pane once when the renderer and the hit-test // disagreed, so assert the hit-test against the very layout that was drawn. let size = Size { cols: 121, rows: 40, }; let client_id = 1; let mut tab = create_new_tab(size, ModeInfo::default()); let mut members = vec![PaneId::Terminal(1)]; for pid in 2..=8 { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); members.push(PaneId::Terminal(pid)); } let visible = members[7]; tab.stack_lists.insert( 0, super::StackList { members: members.clone(), visible, }, ); for member in &members { tab.stack_list_of_member.insert(*member, 0); } tab.update_stack_list_max_rows(3); tab.update_stack_list_grid(true); let (_list, entry_x, layout) = tab.stack_list_layout_of(0).unwrap(); assert!( layout.cells.iter().any(|c| c.x_offset > 0), "precondition: the grid should have produced more than one column, got {:?}", layout.cells ); let geom = tab.tiled_panes.get_pane(visible).unwrap().position_and_size(); for cell in &layout.cells { // click the middle of the cell, where the label actually is let column = entry_x + cell.x_offset + cell.width / 2; let point = Position::new((geom.y + cell.row) as i32, column as u16); assert_eq!( tab.stack_list_member_at_point(visible, &point), members[cell.member_index], "cell {:?} shows member {}, so clicking it must select that pane", cell, cell.member_index ); } } #[test] fn sl0p_stack_list_gaps_cover_every_column_no_cell_draws_to() { // REGRESSION: the picker draws each entry as its own chunk, so the space // between and around them was never painted and kept the previous frame's // contents. Adding a pane re-flows the grid (an unnamed pane's "Pane #N" // label is far wider than a named one), which left old borders on screen. // // The fix must cover EVERY column no cell draws to, and must not overlap a // cell -- blanking a cell's own columns erases the entry instead. let size = Size { cols: 121, rows: 40, }; let mut tab = create_new_tab(size, ModeInfo::default()); tab.update_stack_list_grid(true); let available_cols = 120; for max_rows in [1usize, 3] { tab.update_stack_list_max_rows(max_rows); for widest in [4usize, 10, 30] { for member_count in 1..20usize { let layout = tab.stack_list_layout(0, member_count, 0, available_cols, widest); for row in 0..layout.rows { let gaps = super::stack_list_row_gaps(&layout, row, available_cols); let cells: Vec<(usize, usize)> = layout .cells .iter() .filter(|c| c.row == row) .map(|c| (c.x_offset, c.x_offset + c.width)) .collect(); // no gap may overlap a cell for (gs, ge) in &gaps { for (cs, ce) in &cells { assert!( ge <= cs || ce <= gs, "gap {:?} overlaps cell {:?} (widest {}, {} members)", (gs, ge), (cs, ce), widest, member_count ); } } // gaps + cells must tile the row exactly let mut covered: Vec<(usize, usize)> = gaps.iter().copied().chain(cells.iter().copied()).collect(); covered.sort_unstable(); let mut cursor = 0; for (start, end) in covered { assert_eq!( start, cursor, "column {} uncovered on row {} (widest {}, {} members)", cursor, row, widest, member_count ); cursor = end; } assert_eq!( cursor, available_cols, "row {} stops short at {} (widest {}, {} members)", row, cursor, widest, member_count ); } } } } } #[test] fn sl0p_responsive_collapse_synthesizes_a_stack_when_the_layout_has_no_swap_layouts() { // The common case, and the one that made this look broken: a custom layout // file only gets swap layouts from a hand-written sibling // ".swap.kdl", so most layouts have NONE and the collapse had // nothing to select by name. It must synthesize one instead. let wide = Size { cols: 121, rows: 20, }; let client_id = 1; // deliberately NO swap layouts, exactly like a bare layout file let mut tab = create_new_tab_with_swap_layouts( wide, ModeInfo::default(), (vec![], vec![]), None, true, true, ); tab.update_responsive_layout(80, String::from("stacked")); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let widths = |tab: &Tab| -> Vec { let mut v: Vec = tab .tiled_panes .panes .values() .map(|p| p.position_and_size().cols.as_usize()) .collect(); v.sort_unstable(); v }; let all_full_width = |tab: &Tab, cols: usize| widths(tab).iter().all(|w| *w == cols); assert!( !all_full_width(&tab, wide.cols), "precondition: panes should be side by side while wide, got {:?}", widths(&tab) ); let narrow = Size { cols: 70, rows: 20 }; tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "a layout with no swap layouts should still collapse, got widths {:?}", widths(&tab) ); // Synthesized layouts must be created once and reused. They are appended // lazily in BOTH directions now, so a bug here grows the layout list without // bound, one entry per resize, for as long as the session lives. let wide = Size { cols: 121, rows: 20, }; tab.resize_whole_tab(wide).unwrap(); let after_first_cycle = tab.swap_layouts.tiled_layout_count(); for _ in 0..5 { tab.resize_whole_tab(narrow).unwrap(); tab.resize_whole_tab(wide).unwrap(); } assert_eq!( tab.swap_layouts.tiled_layout_count(), after_first_cycle, "synthesized layouts must be reused, not appended on every resize" ); } #[test] fn sl0p_responsive_expands_again_after_a_pane_is_added_while_collapsed() { // REGRESSION: the tab restores into whatever layout was active before the // collapse, which for a bare layout is BASE -- and BASE is pinned to // ExactPanes of the count it was written with. Add one pane and it can never // apply again, so the tab stayed stacked on every subsequent widening. // // Worse, the restore position was cleared regardless of whether anything was // applied, so the tab believed it had expanded while visibly stacked and // never retried. let wide = Size { cols: 121, rows: 20, }; let client_id = 1; let mut tab = create_new_tab_with_swap_layouts( wide, ModeInfo::default(), (vec![], vec![]), None, true, true, ); tab.update_responsive_layout(80, String::from("stacked")); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let widths = |tab: &Tab| -> Vec { let mut v: Vec = tab .tiled_panes .panes .values() .map(|p| p.position_and_size().cols.as_usize()) .collect(); v.sort_unstable(); v }; let all_full_width = |tab: &Tab, cols: usize| widths(tab).iter().all(|w| *w == cols); let narrow = Size { cols: 70, rows: 20 }; tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "precondition: should have collapsed, got {:?}", widths(&tab) ); // add a pane WHILE collapsed, which is what makes BASE inapplicable tab.new_pane( PaneId::Terminal(4), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); tab.resize_whole_tab(wide).unwrap(); assert!( !all_full_width(&tab, wide.cols), "a tab must still expand after a pane was added while collapsed, got widths {:?}", widths(&tab) ); // and it must still work a second time round tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "should collapse again after expanding, got {:?}", widths(&tab) ); tab.resize_whole_tab(wide).unwrap(); assert!( !all_full_width(&tab, wide.cols), "should expand again on the second round trip, got {:?}", widths(&tab) ); } #[test] fn sl0p_responsive_collapses_even_when_the_named_layout_never_applies() { // A swap layout can exist under the right name and still never apply: this // one wants min_panes=4 and the tab has three. Resolving the collapse target // fresh means falling back to a synthesized stack, where keying off the // configured name alone would find the layout, fail its constraint, and give // up -- looking exactly like the feature being broken. let wide = Size { cols: 121, rows: 20, }; let client_id = 1; let swap_layouts = r#" layout { swap_tiled_layout name="stacked" { tab min_panes=4 { pane stacked=true { children; } } } } "#; let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap(); let mut tab = create_new_tab_with_swap_layouts( wide, ModeInfo::default(), ( layout.swap_tiled_layouts.clone(), layout.swap_floating_layouts.clone(), ), None, true, true, ); tab.update_responsive_layout(80, String::from("stacked")); for pid in [2, 3] { tab.new_pane( PaneId::Terminal(pid), None, None, false, true, NewPanePlacement::default(), Some(client_id), None, ) .unwrap(); } let widths = |tab: &Tab| -> Vec { let mut v: Vec = tab .tiled_panes .panes .values() .map(|p| p.position_and_size().cols.as_usize()) .collect(); v.sort_unstable(); v }; let all_full_width = |tab: &Tab, cols: usize| widths(tab).iter().all(|w| *w == cols); // precondition: three panes, so the named layout's min_panes=4 cannot match assert_eq!(tab.tiled_panes.panes.len(), 3); let narrow = Size { cols: 70, rows: 20 }; tab.resize_whole_tab(narrow).unwrap(); assert!( all_full_width(&tab, narrow.cols), "should fall back to a synthesized stack when the named layout cannot apply, \ got widths {:?}", widths(&tab) ); tab.resize_whole_tab(wide).unwrap(); assert!( !all_full_width(&tab, wide.cols), "and still expand again afterwards, got widths {:?}", widths(&tab) ); }