From c3115a428ed5c990cc5ead5629dabb624ae90453 Mon Sep 17 00:00:00 2001 From: Aram Drevekenin Date: Wed, 14 Dec 2022 22:26:48 +0100 Subject: [PATCH] fix(panes): show visual error when unable to split panes vertically/horizontally (#2025) * fix(panes): show visual error when failing to split pane vertically/horizontally * fix: lockfile --- Cargo.lock | 12 +-- zellij-server/src/background_jobs.rs | 79 +++++++++++++++++ zellij-server/src/lib.rs | 34 ++++++++ zellij-server/src/panes/plugin_pane.rs | 26 +++++- zellij-server/src/panes/terminal_pane.rs | 26 +++++- zellij-server/src/panes/tiled_panes/mod.rs | 39 +++++++++ zellij-server/src/screen.rs | 28 +++++++ zellij-server/src/tab/mod.rs | 80 +++++++++++++++++- zellij-server/src/tab/unit/tab_tests.rs | 84 ++++++++++++++++++- zellij-server/src/thread_bus.rs | 26 +++++- zellij-server/src/ui/pane_boundaries_frame.rs | 3 + zellij-server/src/ui/pane_contents_and_ui.rs | 4 +- zellij-server/src/unit/screen_tests.rs | 15 +++- zellij-utils/src/errors.rs | 10 +++ zellij-utils/src/pane_size.rs | 2 +- 15 files changed, 449 insertions(+), 19 deletions(-) create mode 100644 zellij-server/src/background_jobs.rs diff --git a/Cargo.lock b/Cargo.lock index 72e0cfc5..8d96ef7b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3889,7 +3889,7 @@ dependencies = [ [[package]] name = "zellij" -version = "0.34.4" +version = "0.34.5" dependencies = [ "anyhow", "dialoguer", @@ -3908,7 +3908,7 @@ dependencies = [ [[package]] name = "zellij-client" -version = "0.34.4" +version = "0.34.5" dependencies = [ "insta", "log", @@ -3921,7 +3921,7 @@ dependencies = [ [[package]] name = "zellij-server" -version = "0.34.4" +version = "0.34.5" dependencies = [ "ansi_term", "arrayvec 0.7.2", @@ -3951,7 +3951,7 @@ dependencies = [ [[package]] name = "zellij-tile" -version = "0.34.4" +version = "0.34.5" dependencies = [ "clap", "serde", @@ -3963,14 +3963,14 @@ dependencies = [ [[package]] name = "zellij-tile-utils" -version = "0.34.4" +version = "0.34.5" dependencies = [ "ansi_term", ] [[package]] name = "zellij-utils" -version = "0.34.4" +version = "0.34.5" dependencies = [ "anyhow", "async-std", diff --git a/zellij-server/src/background_jobs.rs b/zellij-server/src/background_jobs.rs new file mode 100644 index 00000000..3c89ed06 --- /dev/null +++ b/zellij-server/src/background_jobs.rs @@ -0,0 +1,79 @@ +use zellij_utils::async_std::task; +use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +use crate::panes::PaneId; +use crate::screen::ScreenInstruction; +use crate::thread_bus::Bus; + +#[derive(Debug, Clone, Eq, PartialEq, Hash)] +pub enum BackgroundJob { + DisplayPaneError(PaneId, String), + Exit, +} + +impl From<&BackgroundJob> for BackgroundJobContext { + fn from(background_job: &BackgroundJob) -> Self { + match *background_job { + BackgroundJob::DisplayPaneError(..) => BackgroundJobContext::DisplayPaneError, + BackgroundJob::Exit => BackgroundJobContext::Exit, + } + } +} + +static FLASH_DURATION_MS: u64 = 1000; + +pub(crate) fn background_jobs_main(bus: Bus) -> Result<()> { + let err_context = || "failed to write to pty".to_string(); + let mut running_jobs: HashMap = HashMap::new(); + + loop { + let (event, mut err_ctx) = bus.recv().with_context(err_context)?; + err_ctx.add_call(ContextType::BackgroundJob((&event).into())); + let job = event.clone(); + match event { + BackgroundJob::DisplayPaneError(pane_id, text) => { + if job_already_running(job, &mut running_jobs) { + continue; + } + task::spawn({ + let senders = bus.senders.clone(); + async move { + let _ = senders.send_to_screen( + ScreenInstruction::AddRedPaneFrameColorOverride(pane_id, Some(text)), + ); + task::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)).await; + let _ = senders.send_to_screen( + ScreenInstruction::ClearPaneFrameColorOverride(pane_id), + ); + } + }); + }, + BackgroundJob::Exit => { + return Ok(()); + }, + } + } +} + +fn job_already_running( + job: BackgroundJob, + running_jobs: &mut HashMap, +) -> bool { + match running_jobs.get_mut(&job) { + Some(current_running_job_start_time) => { + if current_running_job_start_time.elapsed() > Duration::from_millis(FLASH_DURATION_MS) { + *current_running_job_start_time = Instant::now(); + false + } else { + true + } + }, + None => { + running_jobs.insert(job.clone(), Instant::now()); + false + }, + } +} diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index c024a03d..40c2edc2 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -3,6 +3,7 @@ pub mod output; pub mod panes; pub mod tab; +mod background_jobs; mod logging_pipe; mod plugins; mod pty; @@ -13,6 +14,7 @@ mod terminal_bytes; mod thread_bus; mod ui; +use background_jobs::{background_jobs_main, BackgroundJob}; use log::info; use pty_writer::{pty_writer_main, PtyWriteInstruction}; use std::collections::{HashMap, HashSet}; @@ -110,6 +112,7 @@ pub(crate) struct SessionMetaData { pty_thread: Option>, plugin_thread: Option>, pty_writer_thread: Option>, + background_jobs_thread: Option>, } impl Drop for SessionMetaData { @@ -118,6 +121,7 @@ impl Drop for SessionMetaData { let _ = self.senders.send_to_screen(ScreenInstruction::Exit); let _ = self.senders.send_to_plugin(PluginInstruction::Exit); let _ = self.senders.send_to_pty_writer(PtyWriteInstruction::Exit); + let _ = self.senders.send_to_background_jobs(BackgroundJob::Exit); if let Some(screen_thread) = self.screen_thread.take() { let _ = screen_thread.join(); } @@ -130,6 +134,9 @@ impl Drop for SessionMetaData { if let Some(pty_writer_thread) = self.pty_writer_thread.take() { let _ = pty_writer_thread.join(); } + if let Some(background_jobs_thread) = self.background_jobs_thread.take() { + let _ = background_jobs_thread.join(); + } } } @@ -638,6 +645,10 @@ fn init_session( channels::unbounded(); let to_pty_writer = SenderWithContext::new(to_pty_writer); + let (to_background_jobs, background_jobs_receiver): ChannelWithContext = + channels::unbounded(); + let to_background_jobs = SenderWithContext::new(to_background_jobs); + // Determine and initialize the data directory let data_dir = opts.data_dir.unwrap_or_else(get_default_data_dir); @@ -664,6 +675,7 @@ fn init_session( Some(&to_plugin), Some(&to_server), Some(&to_pty_writer), + Some(&to_background_jobs), Some(os_input.clone()), ), opts.debug, @@ -684,6 +696,7 @@ fn init_session( Some(&to_plugin), Some(&to_server), Some(&to_pty_writer), + Some(&to_background_jobs), Some(os_input.clone()), ); let max_panes = opts.max_panes; @@ -711,6 +724,7 @@ fn init_session( Some(&to_plugin), None, Some(&to_pty_writer), + Some(&to_background_jobs), None, ); let store = Store::default(); @@ -738,18 +752,37 @@ fn init_session( Some(&to_plugin), Some(&to_server), None, + Some(&to_background_jobs), Some(os_input.clone()), ); || pty_writer_main(pty_writer_bus).fatal() }) .unwrap(); + let background_jobs_thread = thread::Builder::new() + .name("background_jobs".to_string()) + .spawn({ + let background_jobs_bus = Bus::new( + vec![background_jobs_receiver], + Some(&to_screen), + Some(&to_pty), + Some(&to_plugin), + Some(&to_server), + Some(&to_pty_writer), + None, + Some(os_input.clone()), + ); + || background_jobs_main(background_jobs_bus).fatal() + }) + .unwrap(); + SessionMetaData { senders: ThreadSenders { to_screen: Some(to_screen), to_pty: Some(to_pty), to_plugin: Some(to_plugin), to_pty_writer: Some(to_pty_writer), + to_background_jobs: Some(to_background_jobs), to_server: None, should_silently_fail: false, }, @@ -760,5 +793,6 @@ fn init_session( pty_thread: Some(pty_thread), plugin_thread: Some(plugin_thread), pty_writer_thread: Some(pty_writer_thread), + background_jobs_thread: Some(background_jobs_thread), } } diff --git a/zellij-server/src/panes/plugin_pane.rs b/zellij-server/src/panes/plugin_pane.rs index a530b20b..3a0ec9c8 100644 --- a/zellij-server/src/panes/plugin_pane.rs +++ b/zellij-server/src/panes/plugin_pane.rs @@ -64,6 +64,7 @@ pub(crate) struct PluginPane { prev_pane_name: String, frame: HashMap, borderless: bool, + pane_frame_color_override: Option<(PaletteColor, Option)>, } impl PluginPane { @@ -102,6 +103,7 @@ impl PluginPane { vte_parsers: HashMap::new(), grids: HashMap::new(), style, + pane_frame_color_override: None, } } } @@ -244,7 +246,13 @@ impl Pane for PluginPane { } if let Some(grid) = self.grids.get(&client_id) { let err_context = || format!("failed to render frame for client {client_id}"); - let pane_title = if self.pane_name.is_empty() + let pane_title = if let Some(text_color_override) = self + .pane_frame_color_override + .as_ref() + .and_then(|(_color, text)| text.as_ref()) + { + text_color_override.into() + } else if self.pane_name.is_empty() && input_mode == InputMode::RenamePane && frame_params.is_main_client { @@ -257,12 +265,15 @@ impl Pane for PluginPane { self.pane_name.clone() }; - let frame = PaneFrame::new( + let mut frame = PaneFrame::new( self.current_geom().into(), grid.scrollback_position_and_length(), pane_title, frame_params, ); + if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() { + frame.override_color(*frame_color_override); + } let res = match self.frame.get(&client_id) { // TODO: use and_then or something? @@ -469,6 +480,17 @@ impl Pane for PluginPane { )])) .unwrap(); } + fn add_red_pane_frame_color_override(&mut self, error_text: Option) { + self.pane_frame_color_override = Some((self.style.colors.red, error_text)); + } + fn clear_pane_frame_color_override(&mut self) { + self.pane_frame_color_override = None; + } + fn frame_color_override(&self) -> Option { + self.pane_frame_color_override + .as_ref() + .map(|(color, _text)| *color) + } } impl PluginPane { diff --git a/zellij-server/src/panes/terminal_pane.rs b/zellij-server/src/panes/terminal_pane.rs index b5c51c65..4d5ed840 100644 --- a/zellij-server/src/panes/terminal_pane.rs +++ b/zellij-server/src/panes/terminal_pane.rs @@ -109,7 +109,8 @@ pub struct TerminalPane { is_held: Option<(Option, IsFirstRun, RunCommand)>, // a "held" pane means that its command has either exited and the pane is waiting for a // possible user instruction to be re-run, or that the command has not yet been run banner: Option, // a banner to be rendered inside this TerminalPane, used for panes - // held on startup and can possibly be used to display some errors + // held on startup and can possibly be used to display some errors + pane_frame_color_override: Option<(PaletteColor, Option)>, } impl Pane for TerminalPane { @@ -301,7 +302,13 @@ impl Pane for TerminalPane { ) -> Result, Option)>> { let err_context = || format!("failed to render frame for client {client_id}"); // TODO: remove the cursor stuff from here - let pane_title = if self.pane_name.is_empty() + let pane_title = if let Some(text_color_override) = self + .pane_frame_color_override + .as_ref() + .and_then(|(_color, text)| text.as_ref()) + { + text_color_override.into() + } else if self.pane_name.is_empty() && input_mode == InputMode::RenamePane && frame_params.is_main_client { @@ -353,6 +360,9 @@ impl Pane for TerminalPane { frame.add_exit_status(exit_status.as_ref().copied()); } } + if let Some((frame_color_override, _text)) = self.pane_frame_color_override.as_ref() { + frame.override_color(*frame_color_override); + } let res = match self.frame.get(&client_id) { // TODO: use and_then or something? @@ -669,6 +679,17 @@ impl Pane for TerminalPane { } self.set_should_render(true); } + fn add_red_pane_frame_color_override(&mut self, error_text: Option) { + self.pane_frame_color_override = Some((self.style.colors.red, error_text)); + } + fn clear_pane_frame_color_override(&mut self) { + self.pane_frame_color_override = None; + } + fn frame_color_override(&self) -> Option { + self.pane_frame_color_override + .as_ref() + .map(|(color, _text)| *color) + } } impl TerminalPane { @@ -717,6 +738,7 @@ impl TerminalPane { search_term: String::new(), is_held: None, banner: None, + pane_frame_color_override: None, } } pub fn get_x(&self) -> usize { diff --git a/zellij-server/src/panes/tiled_panes/mod.rs b/zellij-server/src/panes/tiled_panes/mod.rs index b5caa591..ee752ea1 100644 --- a/zellij-server/src/panes/tiled_panes/mod.rs +++ b/zellij-server/src/panes/tiled_panes/mod.rs @@ -205,6 +205,19 @@ impl TiledPanes { }) .collect() } + pub fn borderless_pane_geoms(&self) -> Vec { + self.panes + .values() + .filter_map(|p| { + let geom = p.position_and_size(); + if p.borderless() { + Some(geom.into()) + } else { + None + } + }) + .collect() + } pub fn first_selectable_pane_id(&self) -> Option { self.panes .iter() @@ -295,6 +308,19 @@ impl TiledPanes { } false } + pub fn can_split_active_pane_horizontally(&self, client_id: ClientId) -> bool { + let active_pane_id = &self.active_panes.get(&client_id).unwrap(); + let active_pane = self.panes.get(active_pane_id).unwrap(); + let full_pane_size = active_pane.position_and_size(); + if full_pane_size.rows.is_fixed() { + return false; + } + if split(SplitDirection::Horizontal, &full_pane_size).is_some() { + true + } else { + false + } + } pub fn split_pane_horizontally( &mut self, pid: PaneId, @@ -313,6 +339,19 @@ impl TiledPanes { self.relayout(SplitDirection::Vertical); } } + pub fn can_split_active_pane_vertically(&self, client_id: ClientId) -> bool { + let active_pane_id = &self.active_panes.get(&client_id).unwrap(); + let active_pane = self.panes.get(active_pane_id).unwrap(); + let full_pane_size = active_pane.position_and_size(); + if full_pane_size.cols.is_fixed() { + return false; + } + if split(SplitDirection::Vertical, &full_pane_size).is_some() { + true + } else { + false + } + } pub fn split_pane_vertically( &mut self, pid: PaneId, diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs index f304383b..9ed803bb 100644 --- a/zellij-server/src/screen.rs +++ b/zellij-server/src/screen.rs @@ -224,6 +224,8 @@ pub enum ScreenInstruction { SearchToggleCaseSensitivity(ClientId), SearchToggleWholeWord(ClientId), SearchToggleWrap(ClientId), + AddRedPaneFrameColorOverride(PaneId, Option), // Option => optional error text + ClearPaneFrameColorOverride(PaneId), } impl From<&ScreenInstruction> for ScreenContext { @@ -355,6 +357,12 @@ impl From<&ScreenInstruction> for ScreenContext { }, ScreenInstruction::SearchToggleWholeWord(..) => ScreenContext::SearchToggleWholeWord, ScreenInstruction::SearchToggleWrap(..) => ScreenContext::SearchToggleWrap, + ScreenInstruction::AddRedPaneFrameColorOverride(..) => { + ScreenContext::AddRedPaneFrameColorOverride + }, + ScreenInstruction::ClearPaneFrameColorOverride(..) => { + ScreenContext::ClearPaneFrameColorOverride + }, } } } @@ -2112,6 +2120,26 @@ pub(crate) fn screen_thread_main( screen.render()?; screen.unblock_input()?; }, + ScreenInstruction::AddRedPaneFrameColorOverride(pane_id, error_text) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.add_red_pane_frame_color_override(pane_id, error_text); + break; + } + } + screen.render()?; + }, + ScreenInstruction::ClearPaneFrameColorOverride(pane_id) => { + let all_tabs = screen.get_tabs_mut(); + for tab in all_tabs.values_mut() { + if tab.has_pane_with_pid(&pane_id) { + tab.clear_pane_frame_color_override(pane_id); + break; + } + } + screen.render()?; + }, } } Ok(()) diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs index 63573a08..a841b39a 100644 --- a/zellij-server/src/tab/mod.rs +++ b/zellij-server/src/tab/mod.rs @@ -13,6 +13,7 @@ use zellij_utils::input::command::RunCommand; use zellij_utils::position::{Column, Line}; use zellij_utils::{position::Position, serde}; +use crate::background_jobs::BackgroundJob; use crate::pty_writer::PtyWriteInstruction; use crate::screen::CopyOptions; use crate::ui::pane_boundaries_frame::FrameParams; @@ -386,6 +387,9 @@ pub trait Pane { fn hold(&mut self, _exit_status: Option, _is_first_run: bool, _run_command: RunCommand) { // No-op by default, only terminal panes support holding } + fn add_red_pane_frame_color_override(&mut self, _error_text: Option); + fn clear_pane_frame_color_override(&mut self); + fn frame_color_override(&self) -> Option; } #[derive(Clone, Debug)] @@ -618,14 +622,18 @@ impl Tab { .send_to_pty(PtyInstruction::ClosePane(PaneId::Terminal(*unused_pid))) .with_context(err_context)?; } - // FIXME: This is another hack to crop the viewport to fixed-size panes. Once you can have - // non-fixed panes that are part of the viewport, get rid of this! + + // here we offset the viewport from borderless panes that are on the edges of the + // screen, this is so that when we don't have pane boundaries (eg. when they were + // disabled by the user) boundaries won't be drawn around these panes + // geometrically, we can only do this with panes that are on the edges of the + // screen - so it's mostly a best-effort thing let display_area = { let display_area = self.display_area.borrow(); *display_area }; self.resize_whole_tab(display_area); - let boundary_geoms = self.tiled_panes.fixed_pane_geoms(); + let boundary_geoms = self.tiled_panes.borderless_pane_geoms(); for geom in boundary_geoms { self.offset_viewport(&geom) } @@ -1035,6 +1043,20 @@ impl Tab { self.should_clear_display_before_rendering = true; self.tiled_panes.focus_pane(pid, client_id); } + } else { + log::error!("No room to split pane horizontally"); + if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { + self.senders + .send_to_background_jobs(BackgroundJob::DisplayPaneError( + active_pane_id, + "TOO SMALL!".into(), + )) + .with_context(err_context)?; + } + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) + .with_context(err_context)?; + return Ok(()); } Ok(()) } @@ -1075,6 +1097,20 @@ impl Tab { self.should_clear_display_before_rendering = true; self.tiled_panes.focus_pane(pid, client_id); } + } else { + log::error!("No room to split pane vertically"); + if let Some(active_pane_id) = self.tiled_panes.get_active_pane_id(client_id) { + self.senders + .send_to_background_jobs(BackgroundJob::DisplayPaneError( + active_pane_id, + "TOO SMALL!".into(), + )) + .with_context(err_context)?; + } + self.senders + .send_to_pty(PtyInstruction::ClosePane(pid)) + .with_context(err_context)?; + return Ok(()); } Ok(()) } @@ -1139,6 +1175,11 @@ impl Tab { .values() .any(|s_p| s_p.pid() == PaneId::Plugin(plugin_id)) } + pub fn has_pane_with_pid(&self, pid: &PaneId) -> bool { + self.tiled_panes.panes_contain(pid) + || self.floating_panes.panes_contain(pid) + || self.suppressed_panes.values().any(|s_p| s_p.pid() == *pid) + } pub fn handle_pty_bytes(&mut self, pid: u32, bytes: VteBytes) -> Result<()> { if self.is_pending { self.pending_instructions @@ -2809,6 +2850,39 @@ impl Tab { self.is_pending } + pub fn add_red_pane_frame_color_override( + &mut self, + pane_id: PaneId, + error_text: Option, + ) { + if let Some(pane) = self + .tiled_panes + .get_pane_mut(pane_id) + .or_else(|| self.floating_panes.get_pane_mut(pane_id)) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == pane_id) + }) + { + pane.add_red_pane_frame_color_override(error_text); + } + } + pub fn clear_pane_frame_color_override(&mut self, pane_id: PaneId) { + if let Some(pane) = self + .tiled_panes + .get_pane_mut(pane_id) + .or_else(|| self.floating_panes.get_pane_mut(pane_id)) + .or_else(|| { + self.suppressed_panes + .values_mut() + .find(|s_p| s_p.pid() == pane_id) + }) + { + pane.clear_pane_frame_color_override(); + } + } + fn show_floating_panes(&mut self) { // this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true) self.floating_panes.toggle_show_panes(true); diff --git a/zellij-server/src/tab/unit/tab_tests.rs b/zellij-server/src/tab/unit/tab_tests.rs index a676f3c6..192c55e3 100644 --- a/zellij-server/src/tab/unit/tab_tests.rs +++ b/zellij-server/src/tab/unit/tab_tests.rs @@ -10,7 +10,7 @@ use crate::{ use std::path::PathBuf; use zellij_utils::data::{Direction, Resize, ResizeStrategy}; use zellij_utils::errors::prelude::*; -use zellij_utils::input::layout::PaneLayout; +use zellij_utils::input::layout::{PaneLayout, SplitDirection, SplitSize}; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; @@ -187,6 +187,62 @@ fn create_new_tab(size: Size) -> Tab { tab } +fn create_new_tab_with_layout(size: Size, layout: PaneLayout) -> Tab { + let index = 0; + let position = 0; + let name = String::new(); + let os_api = Box::new(FakeInputOutput {}); + let senders = ThreadSenders::default().silently_fail_on_send(); + let max_panes = None; + let mode_info = ModeInfo::default(); + let style = Style::default(); + let draw_pane_frames = true; + let client_id = 1; + let session_is_mirrored = true; + let mut connected_clients = HashSet::new(); + let character_cell_info = Rc::new(RefCell::new(None)); + connected_clients.insert(client_id); + let connected_clients = Rc::new(RefCell::new(connected_clients)); + let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); + let copy_options = CopyOptions::default(); + let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); + let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); + let mut tab = Tab::new( + index, + position, + name, + size, + character_cell_info, + sixel_image_store, + os_api, + senders, + max_panes, + style, + mode_info, + draw_pane_frames, + connected_clients, + session_is_mirrored, + client_id, + copy_options, + terminal_emulator_colors, + terminal_emulator_color_codes, + ); + let mut new_terminal_ids = vec![]; + for i in 0..layout.extract_run_instructions().len() { + new_terminal_ids.push((i as u32, None)); + } + tab.apply_layout( + layout, + // vec![(1, None), (2, None)], + new_terminal_ids, + HashMap::new(), + index, + client_id, + ) + .unwrap(); + tab +} + fn create_new_tab_with_cell_size( size: Size, character_cell_size: Rc>>, @@ -677,6 +733,32 @@ pub fn cannot_split_largest_pane_when_there_is_no_room() { ); } +#[test] +pub fn cannot_split_panes_vertically_when_active_pane_has_fixed_columns() { + let size = Size { cols: 50, rows: 20 }; + let mut initial_layout = PaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Vertical; + let mut fixed_child = PaneLayout::default(); + fixed_child.split_size = Some(SplitSize::Fixed(30)); + initial_layout.children = vec![fixed_child, PaneLayout::default()]; + let mut tab = create_new_tab_with_layout(size, initial_layout); + tab.vertical_split(PaneId::Terminal(3), None, 1).unwrap(); + assert_eq!(tab.tiled_panes.panes.len(), 2, "Tab still has two panes"); +} + +#[test] +pub fn cannot_split_panes_horizontally_when_active_pane_has_fixed_rows() { + let size = Size { cols: 50, rows: 20 }; + let mut initial_layout = PaneLayout::default(); + initial_layout.children_split_direction = SplitDirection::Horizontal; + let mut fixed_child = PaneLayout::default(); + fixed_child.split_size = Some(SplitSize::Fixed(12)); + initial_layout.children = vec![fixed_child, PaneLayout::default()]; + let mut tab = create_new_tab_with_layout(size, initial_layout); + tab.horizontal_split(PaneId::Terminal(3), None, 1).unwrap(); + assert_eq!(tab.tiled_panes.panes.len(), 2, "Tab still has two panes"); +} + #[test] pub fn toggle_focused_pane_fullscreen() { let size = Size { diff --git a/zellij-server/src/thread_bus.rs b/zellij-server/src/thread_bus.rs index 8d172242..7f831d6b 100644 --- a/zellij-server/src/thread_bus.rs +++ b/zellij-server/src/thread_bus.rs @@ -1,8 +1,9 @@ //! Definitions and helpers for sending and receiving messages between threads. use crate::{ - os_input_output::ServerOsApi, plugins::PluginInstruction, pty::PtyInstruction, - pty_writer::PtyWriteInstruction, screen::ScreenInstruction, ServerInstruction, + background_jobs::BackgroundJob, os_input_output::ServerOsApi, plugins::PluginInstruction, + pty::PtyInstruction, pty_writer::PtyWriteInstruction, screen::ScreenInstruction, + ServerInstruction, }; use zellij_utils::errors::prelude::*; use zellij_utils::{channels, channels::SenderWithContext, errors::ErrorContext}; @@ -15,6 +16,7 @@ pub struct ThreadSenders { pub to_plugin: Option>, pub to_server: Option>, pub to_pty_writer: Option>, + pub to_background_jobs: Option>, // this is a convenience for the unit tests // it's not advisable to set it to true in production code pub should_silently_fail: bool, @@ -109,6 +111,23 @@ impl ThreadSenders { .context("failed to send message to pty writer") } } + pub fn send_to_background_jobs(&self, background_job: BackgroundJob) -> Result<()> { + if self.should_silently_fail { + let _ = self + .to_background_jobs + .as_ref() + .map(|sender| sender.send(background_job)) + .unwrap_or_else(|| Ok(())); + Ok(()) + } else { + self.to_background_jobs + .as_ref() + .context("failed to get background jobs sender")? + .send(background_job) + .to_anyhow() + .context("failed to send message to background jobs") + } + } #[allow(unused)] pub fn silently_fail_on_send(mut self) -> Self { @@ -142,6 +161,7 @@ impl Bus { to_plugin: Option<&SenderWithContext>, to_server: Option<&SenderWithContext>, to_pty_writer: Option<&SenderWithContext>, + to_background_jobs: Option<&SenderWithContext>, os_input: Option>, ) -> Self { Bus { @@ -152,6 +172,7 @@ impl Bus { to_plugin: to_plugin.cloned(), to_server: to_server.cloned(), to_pty_writer: to_pty_writer.cloned(), + to_background_jobs: to_background_jobs.cloned(), should_silently_fail: false, }, os_input: os_input.clone(), @@ -174,6 +195,7 @@ impl Bus { to_plugin: None, to_server: None, to_pty_writer: None, + to_background_jobs: None, should_silently_fail: true, }, os_input: None, diff --git a/zellij-server/src/ui/pane_boundaries_frame.rs b/zellij-server/src/ui/pane_boundaries_frame.rs index 6209466c..537a80c4 100644 --- a/zellij-server/src/ui/pane_boundaries_frame.rs +++ b/zellij-server/src/ui/pane_boundaries_frame.rs @@ -122,6 +122,9 @@ impl PaneFrame { pub fn indicate_first_run(&mut self) { self.is_first_run = true; } + pub fn override_color(&mut self, color: PaletteColor) { + self.color = Some(color); + } fn client_cursor(&self, client_id: ClientId) -> Vec { let color = client_id_to_colors(client_id, self.style.colors); background_color(" ", color.map(|c| c.0)) diff --git a/zellij-server/src/ui/pane_contents_and_ui.rs b/zellij-server/src/ui/pane_contents_and_ui.rs index 1cc715d8..7f718d0b 100644 --- a/zellij-server/src/ui/pane_contents_and_ui.rs +++ b/zellij-server/src/ui/pane_contents_and_ui.rs @@ -238,7 +238,9 @@ impl<'a> PaneContentsAndUi<'a> { session_is_mirrored: bool, ) -> Option { let pane_focused_for_client_id = self.focused_clients.contains(&client_id); - if pane_focused_for_client_id { + if let Some(override_color) = self.pane.frame_color_override() { + Some(override_color) + } else if pane_focused_for_client_id { match mode { InputMode::Normal | InputMode::Locked => { if session_is_mirrored || !self.multiple_users_exist_in_session { diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index 2c512bd8..f6f993c7 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -14,11 +14,12 @@ use zellij_utils::data::Resize; use zellij_utils::errors::{prelude::*, ErrorContext}; use zellij_utils::input::actions::Action; use zellij_utils::input::command::{RunCommand, TerminalAction}; -use zellij_utils::input::layout::{PaneLayout, SplitDirection}; +use zellij_utils::input::layout::{PaneLayout, SplitDirection, SplitSize}; use zellij_utils::input::options::Options; use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::pane_size::{Size, SizeInPixels}; +use crate::background_jobs::BackgroundJob; use crate::pty_writer::PtyWriteInstruction; use std::env::set_var; use std::os::unix::io::RawFd; @@ -238,6 +239,7 @@ struct MockScreen { pub main_client_id: u16, pub pty_receiver: Option>, pub pty_writer_receiver: Option>, + pub background_jobs_receiver: Option>, pub screen_receiver: Option>, pub server_receiver: Option>, pub plugin_receiver: Option>, @@ -246,6 +248,7 @@ struct MockScreen { pub to_plugin: SenderWithContext, pub to_server: SenderWithContext, pub to_pty_writer: SenderWithContext, + pub to_background_jobs: SenderWithContext, pub os_input: FakeInputOutput, pub client_attributes: ClientAttributes, pub config_options: Options, @@ -264,6 +267,7 @@ impl MockScreen { Some(&self.to_plugin.clone()), Some(&self.to_server.clone()), Some(&self.to_pty_writer.clone()), + Some(&self.to_background_jobs.clone()), Some(Box::new(self.os_input.clone())), ) .should_silently_fail(); @@ -352,6 +356,7 @@ impl MockScreen { pty_thread: None, plugin_thread: None, pty_writer_thread: None, + background_jobs_thread: None, } } } @@ -376,6 +381,10 @@ impl MockScreen { channels::unbounded(); let to_pty_writer = SenderWithContext::new(to_pty_writer); + let (to_background_jobs, background_jobs_receiver): ChannelWithContext = + channels::unbounded(); + let to_background_jobs = SenderWithContext::new(to_background_jobs); + let client_attributes = ClientAttributes { size, ..Default::default() @@ -390,6 +399,7 @@ impl MockScreen { to_pty: Some(to_pty.clone()), to_plugin: Some(to_plugin.clone()), to_pty_writer: Some(to_pty_writer.clone()), + to_background_jobs: Some(to_background_jobs.clone()), to_server: Some(to_server.clone()), should_silently_fail: true, }, @@ -400,6 +410,7 @@ impl MockScreen { pty_thread: None, plugin_thread: None, pty_writer_thread: None, + background_jobs_thread: None, }; let os_input = FakeInputOutput::default(); @@ -409,6 +420,7 @@ impl MockScreen { main_client_id, pty_receiver: Some(pty_receiver), pty_writer_receiver: Some(pty_writer_receiver), + background_jobs_receiver: Some(background_jobs_receiver), screen_receiver: Some(screen_receiver), server_receiver: Some(server_receiver), plugin_receiver: Some(plugin_receiver), @@ -417,6 +429,7 @@ impl MockScreen { to_plugin, to_server, to_pty_writer, + to_background_jobs, os_input, client_attributes, config_options, diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index cbd0bdf1..4b7149ec 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -215,6 +215,7 @@ pub enum ContextType { StdinHandler, AsyncTask, PtyWrite(PtyWriteContext), + BackgroundJob(BackgroundJobContext), /// An empty, placeholder call. This should be thought of as representing no call at all. /// A call stack representation filled with these is the representation of an empty call stack. Empty, @@ -231,6 +232,7 @@ impl Display for ContextType { ContextType::StdinHandler => Some(("stdin_handler_thread:", "AcceptInput".to_string())), ContextType::AsyncTask => Some(("stream_terminal_bytes:", "AsyncTask".to_string())), ContextType::PtyWrite(c) => Some(("pty_writer_thread:", format!("{:?}", c))), + ContextType::BackgroundJob(c) => Some(("background_jobs_thread:", format!("{:?}", c))), ContextType::Empty => None, } { write!(f, "{} {}", left.purple(), right.green()) @@ -350,6 +352,8 @@ pub enum ScreenContext { SearchToggleCaseSensitivity, SearchToggleWholeWord, SearchToggleWrap, + AddRedPaneFrameColorOverride, + ClearPaneFrameColorOverride, } /// Stack call representations corresponding to the different types of [`PtyInstruction`]s. @@ -420,6 +424,12 @@ pub enum PtyWriteContext { Exit, } +#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +pub enum BackgroundJobContext { + DisplayPaneError, + Exit, +} + use thiserror::Error; #[derive(Debug, Error)] pub enum ZellijError { diff --git a/zellij-utils/src/pane_size.rs b/zellij-utils/src/pane_size.rs index 9c64080d..048ae3d6 100644 --- a/zellij-utils/src/pane_size.rs +++ b/zellij-utils/src/pane_size.rs @@ -56,7 +56,7 @@ impl Dimension { pub fn fixed(size: usize) -> Dimension { Self { constraint: Constraint::Fixed(size), - inner: 1, + inner: size, } }