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:
parent
177cd20bea
commit
c3115a428e
15 changed files with 449 additions and 19 deletions
12
Cargo.lock
generated
12
Cargo.lock
generated
|
|
@ -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",
|
||||
|
|
|
|||
79
zellij-server/src/background_jobs.rs
Normal file
79
zellij-server/src/background_jobs.rs
Normal 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
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -110,6 +110,7 @@ pub struct TerminalPane {
|
|||
// 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
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ impl Dimension {
|
|||
pub fn fixed(size: usize) -> Dimension {
|
||||
Self {
|
||||
constraint: Constraint::Fixed(size),
|
||||
inner: 1,
|
||||
inner: size,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue