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
This commit is contained in:
Aram Drevekenin 2022-12-14 22:26:48 +01:00 committed by GitHub
parent 177cd20bea
commit c3115a428e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 449 additions and 19 deletions

12
Cargo.lock generated
View file

@ -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",

View file

@ -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<BackgroundJob>) -> Result<()> {
let err_context = || "failed to write to pty".to_string();
let mut running_jobs: HashMap<BackgroundJob, Instant> = 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<BackgroundJob, Instant>,
) -> 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
},
}
}

View file

@ -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<thread::JoinHandle<()>>,
plugin_thread: Option<thread::JoinHandle<()>>,
pty_writer_thread: Option<thread::JoinHandle<()>>,
background_jobs_thread: Option<thread::JoinHandle<()>>,
}
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<BackgroundJob> =
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),
}
}

View file

@ -64,6 +64,7 @@ pub(crate) struct PluginPane {
prev_pane_name: String,
frame: HashMap<ClientId, PaneFrame>,
borderless: bool,
pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
}
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<String>) {
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<PaletteColor> {
self.pane_frame_color_override
.as_ref()
.map(|(color, _text)| *color)
}
}
impl PluginPane {

View file

@ -109,7 +109,8 @@ pub struct TerminalPane {
is_held: Option<(Option<i32>, 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<String>, // 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<String>)>,
}
impl Pane for TerminalPane {
@ -301,7 +302,13 @@ impl Pane for TerminalPane {
) -> Result<Option<(Vec<CharacterChunk>, Option<String>)>> {
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<String>) {
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<PaletteColor> {
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 {

View file

@ -205,6 +205,19 @@ impl TiledPanes {
})
.collect()
}
pub fn borderless_pane_geoms(&self) -> Vec<Viewport> {
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<PaneId> {
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,

View file

@ -224,6 +224,8 @@ pub enum ScreenInstruction {
SearchToggleCaseSensitivity(ClientId),
SearchToggleWholeWord(ClientId),
SearchToggleWrap(ClientId),
AddRedPaneFrameColorOverride(PaneId, Option<String>), // Option<String> => 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(())

View file

@ -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<i32>, _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<String>);
fn clear_pane_frame_color_override(&mut self);
fn frame_color_override(&self) -> Option<PaletteColor>;
}
#[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<String>,
) {
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);

View file

@ -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<RefCell<Option<SizeInPixels>>>,
@ -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 {

View file

@ -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<SenderWithContext<PluginInstruction>>,
pub to_server: Option<SenderWithContext<ServerInstruction>>,
pub to_pty_writer: Option<SenderWithContext<PtyWriteInstruction>>,
pub to_background_jobs: Option<SenderWithContext<BackgroundJob>>,
// 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<T> Bus<T> {
to_plugin: Option<&SenderWithContext<PluginInstruction>>,
to_server: Option<&SenderWithContext<ServerInstruction>>,
to_pty_writer: Option<&SenderWithContext<PtyWriteInstruction>>,
to_background_jobs: Option<&SenderWithContext<BackgroundJob>>,
os_input: Option<Box<dyn ServerOsApi>>,
) -> Self {
Bus {
@ -152,6 +172,7 @@ impl<T> Bus<T> {
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<T> Bus<T> {
to_plugin: None,
to_server: None,
to_pty_writer: None,
to_background_jobs: None,
should_silently_fail: true,
},
os_input: None,

View file

@ -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<TerminalCharacter> {
let color = client_id_to_colors(client_id, self.style.colors);
background_color(" ", color.map(|c| c.0))

View file

@ -238,7 +238,9 @@ impl<'a> PaneContentsAndUi<'a> {
session_is_mirrored: bool,
) -> Option<PaletteColor> {
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 {

View file

@ -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<Receiver<(PtyInstruction, ErrorContext)>>,
pub pty_writer_receiver: Option<Receiver<(PtyWriteInstruction, ErrorContext)>>,
pub background_jobs_receiver: Option<Receiver<(BackgroundJob, ErrorContext)>>,
pub screen_receiver: Option<Receiver<(ScreenInstruction, ErrorContext)>>,
pub server_receiver: Option<Receiver<(ServerInstruction, ErrorContext)>>,
pub plugin_receiver: Option<Receiver<(PluginInstruction, ErrorContext)>>,
@ -246,6 +248,7 @@ struct MockScreen {
pub to_plugin: SenderWithContext<PluginInstruction>,
pub to_server: SenderWithContext<ServerInstruction>,
pub to_pty_writer: SenderWithContext<PtyWriteInstruction>,
pub to_background_jobs: SenderWithContext<BackgroundJob>,
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<BackgroundJob> =
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,

View file

@ -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 {

View file

@ -56,7 +56,7 @@ impl Dimension {
pub fn fixed(size: usize) -> Dimension {
Self {
constraint: Constraint::Fixed(size),
inner: 1,
inner: size,
}
}