mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
Yazelix fixes (sixel/kitty correctness, explicit-swap-layout change rather than toggle, border styling) (#5630)
* fix: sixel image reaping/id-reuse * fix: kitty scaling issues * fix: stacked pane ordering * feat: apply tiled/floating swap layout by name * border styling * fix tests * fix: complete Yazelix feature validation (#5631) * fix: retire client-ids instead of reusing them * distributions * rustfmt * fix tests * update changelog * fix tests and race --------- Co-authored-by: Lucca Huguet <[email protected]>
This commit is contained in:
co-authored by
Lucca Huguet
parent
f19b2ce822
commit
81f56e1aed
@@ -7,6 +7,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
## [Unreleased]
|
||||
* fix: forward OSC7 (cwd changes) to host terminal (https://github.com/zellij-org/zellij/pull/4867)
|
||||
* performance: reduce server memory usage by not copying the builtin plugins onto the heap (https://github.com/zellij-org/zellij/pull/5622)
|
||||
* feat: upstream Yazelix graphics and stacked-pane fixes, named swap-layout selection and configurable pane-border styles (https://github.com/zellij-org/zellij/pull/5630)
|
||||
* feat: distributions -> allow using Zellij as a library (https://github.com/zellij-org/zellij/pull/5630)
|
||||
* fix: do not reuse client ids (https://github.com/zellij-org/zellij/pull/5630)
|
||||
|
||||
## [0.45.1] - 2026-08-28
|
||||
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
|
||||
|
||||
@@ -60,7 +60,7 @@ struct State {
|
||||
persist: bool,
|
||||
is_first_run: bool,
|
||||
own_tab_index: Option<usize>,
|
||||
own_client_id: u16,
|
||||
own_client_id: ClientId,
|
||||
|
||||
// Keybinding cache
|
||||
cached_keybinds: KeybindsVec,
|
||||
@@ -593,7 +593,7 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_toggle_key_config(toggle_key: &str, client_id: u16) -> String {
|
||||
fn bind_toggle_key_config(toggle_key: &str, client_id: ClientId) -> String {
|
||||
format!(
|
||||
r#"
|
||||
keybinds {{
|
||||
|
||||
@@ -5,14 +5,14 @@ use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::style;
|
||||
|
||||
fn cursors<'a>(
|
||||
focused_clients: &'a [ClientId],
|
||||
focused_client_slots: &'a [usize],
|
||||
colors: MultiplayerColors,
|
||||
) -> (Vec<ANSIString<'a>>, usize) {
|
||||
// cursor section, text length
|
||||
let mut len = 0;
|
||||
let mut cursors = vec![];
|
||||
for client_id in focused_clients.iter() {
|
||||
if let Some(color) = client_id_to_colors(*client_id, colors) {
|
||||
for display_slot in focused_client_slots.iter() {
|
||||
if let Some(color) = client_slot_to_colors(*display_slot, colors) {
|
||||
cursors.push(style!(color.1, color.0).paint(" "));
|
||||
len += 1;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ pub fn render_tab(
|
||||
separator: &str,
|
||||
dimmed: bool,
|
||||
) -> LinePart {
|
||||
let focused_clients = tab.other_focused_clients.as_slice();
|
||||
let focused_clients = tab.other_focused_client_slots.as_slice();
|
||||
let separator_width = separator.width();
|
||||
let alternate_tab_color = if is_alternate_tab {
|
||||
palette.ribbon_unselected.emphasis_1
|
||||
|
||||
@@ -5,14 +5,14 @@ use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::style;
|
||||
|
||||
fn cursors<'a>(
|
||||
focused_clients: &'a [ClientId],
|
||||
focused_client_slots: &'a [usize],
|
||||
multiplayer_colors: MultiplayerColors,
|
||||
) -> (Vec<ANSIString<'a>>, usize) {
|
||||
// cursor section, text length
|
||||
let mut len = 0;
|
||||
let mut cursors = vec![];
|
||||
for client_id in focused_clients.iter() {
|
||||
if let Some(color) = client_id_to_colors(*client_id, multiplayer_colors) {
|
||||
for display_slot in focused_client_slots.iter() {
|
||||
if let Some(color) = client_slot_to_colors(*display_slot, multiplayer_colors) {
|
||||
cursors.push(style!(color.1, color.0).paint(" "));
|
||||
len += 1;
|
||||
}
|
||||
@@ -29,7 +29,7 @@ pub fn render_tab(
|
||||
separator: &str,
|
||||
dimmed: bool,
|
||||
) -> LinePart {
|
||||
let focused_clients = tab.other_focused_clients.as_slice();
|
||||
let focused_clients = tab.other_focused_client_slots.as_slice();
|
||||
let separator_width = separator.width();
|
||||
|
||||
let alternate_tab_color = if is_alternate_tab {
|
||||
|
||||
@@ -293,6 +293,35 @@ load_plugins {
|
||||
//
|
||||
// pane_frames true
|
||||
|
||||
// pane_frame_style "titles"
|
||||
|
||||
// Configure the look of the pane frame borders
|
||||
//
|
||||
// ui {
|
||||
// pane_frames {
|
||||
// // round the corners of single-line borders
|
||||
// rounded_corners true
|
||||
// // line style for all four sides of tiled pane frames
|
||||
// // one of: single, double, heavy, dashed, heavy_dashed
|
||||
// border_style "single"
|
||||
// // per-side overrides, eg. a double top border with single sides
|
||||
// border_top "double"
|
||||
// border_right "single"
|
||||
// border_bottom "single"
|
||||
// border_left "single"
|
||||
// // when two neighbouring panes disagree about a shared grid line
|
||||
// // (pane frames off), that line falls back to border_style above
|
||||
// // the same keys prefixed with "floating_" apply to floating panes,
|
||||
// // they default to the values above
|
||||
// floating_border_style "double"
|
||||
// floating_border_top "double"
|
||||
// floating_border_right "double"
|
||||
// floating_border_bottom "double"
|
||||
// floating_border_left "double"
|
||||
// floating_border_rounded_corners false
|
||||
// }
|
||||
// }
|
||||
|
||||
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
|
||||
// Options:
|
||||
// - true (default)
|
||||
|
||||
Generated
+4573
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "example-distribution"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
zellij = { path = "../.." }
|
||||
|
||||
[workspace]
|
||||
members = ["plugin"]
|
||||
@@ -0,0 +1,17 @@
|
||||
plugins {
|
||||
example-plugin location="zellij:example-plugin"
|
||||
}
|
||||
|
||||
load_plugins {
|
||||
"zellij:example-plugin"
|
||||
}
|
||||
|
||||
keybinds {
|
||||
session {
|
||||
unbind "s"
|
||||
}
|
||||
}
|
||||
|
||||
default_layout "example"
|
||||
|
||||
theme "gruvbox-dark"
|
||||
@@ -0,0 +1,14 @@
|
||||
layout {
|
||||
pane size=1 borderless=true {
|
||||
plugin location="tab-bar"
|
||||
}
|
||||
pane split_direction="vertical" {
|
||||
pane
|
||||
pane size="45%" {
|
||||
plugin location="example-plugin"
|
||||
}
|
||||
}
|
||||
pane size=1 borderless=true {
|
||||
plugin location="status-bar"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
use std::path::PathBuf;
|
||||
use std::process::Command;
|
||||
|
||||
const PLUGIN: &str = "example-distribution-plugin";
|
||||
const TARGET: &str = "wasm32-wasip1";
|
||||
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-changed=plugin/src");
|
||||
println!("cargo:rerun-if-changed=plugin/Cargo.toml");
|
||||
println!("cargo:rerun-if-env-changed=EXAMPLE_DISTRIBUTION_PLUGIN_WASM");
|
||||
|
||||
if let Ok(prebuilt) = std::env::var("EXAMPLE_DISTRIBUTION_PLUGIN_WASM") {
|
||||
println!("cargo:rustc-env=EXAMPLE_DISTRIBUTION_PLUGIN_WASM={}", prebuilt);
|
||||
return;
|
||||
}
|
||||
|
||||
let out_dir = PathBuf::from(std::env::var("OUT_DIR").expect("OUT_DIR is set by cargo"));
|
||||
let target_dir = out_dir.join("plugin-target");
|
||||
let cargo = std::env::var("CARGO").unwrap_or_else(|_| "cargo".to_owned());
|
||||
|
||||
let mut command = Command::new(cargo);
|
||||
command
|
||||
.arg("build")
|
||||
.arg("--release")
|
||||
.args(["--target", TARGET])
|
||||
.args(["--package", PLUGIN])
|
||||
.arg("--target-dir")
|
||||
.arg(&target_dir)
|
||||
.env_remove("CARGO_MAKEFLAGS")
|
||||
.env_remove("CARGO_ENCODED_RUSTFLAGS")
|
||||
.env_remove("RUSTFLAGS")
|
||||
.env_remove("CARGO_TARGET_DIR")
|
||||
.env_remove("CARGO_BUILD_TARGET");
|
||||
|
||||
let status = command
|
||||
.status()
|
||||
.unwrap_or_else(|e| panic!("failed to run cargo to build '{}': {}", PLUGIN, e));
|
||||
|
||||
if !status.success() {
|
||||
panic!(
|
||||
"failed to build '{}' for {}. If the target is missing, install it with:\n \
|
||||
rustup target add {}",
|
||||
PLUGIN, TARGET, TARGET
|
||||
);
|
||||
}
|
||||
|
||||
let wasm = target_dir
|
||||
.join(TARGET)
|
||||
.join("release")
|
||||
.join(format!("{}.wasm", PLUGIN.replace('-', "_")));
|
||||
let wasm = if wasm.exists() {
|
||||
wasm
|
||||
} else {
|
||||
target_dir
|
||||
.join(TARGET)
|
||||
.join("release")
|
||||
.join(format!("{}.wasm", PLUGIN))
|
||||
};
|
||||
|
||||
assert!(
|
||||
wasm.exists(),
|
||||
"'{}' built successfully but no wasm artifact was found at '{}'",
|
||||
PLUGIN,
|
||||
wasm.display()
|
||||
);
|
||||
|
||||
println!(
|
||||
"cargo:rustc-env=EXAMPLE_DISTRIBUTION_PLUGIN_WASM={}",
|
||||
wasm.display()
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "example-distribution-plugin"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
license = "MIT"
|
||||
publish = false
|
||||
|
||||
[dependencies]
|
||||
zellij-tile = { path = "../../../zellij-tile" }
|
||||
@@ -0,0 +1,103 @@
|
||||
use std::collections::BTreeMap;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
session_name: String,
|
||||
connected_clients: usize,
|
||||
tabs: usize,
|
||||
panes: usize,
|
||||
updates_received: usize,
|
||||
permission_prompts_seen: usize,
|
||||
}
|
||||
|
||||
register_plugin!(State);
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, _configuration: BTreeMap<String, String>) {
|
||||
subscribe(&[
|
||||
EventType::SessionUpdate,
|
||||
EventType::PermissionRequestResult,
|
||||
]);
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
match event {
|
||||
Event::SessionUpdate(sessions, _) => {
|
||||
if let Some(session) = sessions.iter().find(|s| s.is_current_session) {
|
||||
self.session_name = session.name.clone();
|
||||
self.connected_clients = session.connected_clients;
|
||||
self.tabs = session.tabs.len();
|
||||
self.panes = session.panes.panes.values().map(|p| p.len()).sum();
|
||||
}
|
||||
self.updates_received += 1;
|
||||
true
|
||||
},
|
||||
Event::PermissionRequestResult(_) => {
|
||||
self.permission_prompts_seen += 1;
|
||||
true
|
||||
},
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, _rows: usize, cols: usize) {
|
||||
let mut row = 0;
|
||||
let line = |text: Text, row: &mut usize| {
|
||||
print_text_with_coordinates(text, 0, *row, Some(cols), None);
|
||||
*row += 1;
|
||||
};
|
||||
|
||||
line(
|
||||
Text::new("Example Distribution plugin").color_range(2, ..),
|
||||
&mut row,
|
||||
);
|
||||
row += 1;
|
||||
|
||||
line(Text::new("bundled as: zellij:example-plugin"), &mut row);
|
||||
line(
|
||||
Text::new(format!(
|
||||
"permission prompts: {}",
|
||||
self.permission_prompts_seen
|
||||
))
|
||||
.color_range(
|
||||
if self.permission_prompts_seen == 0 { 3 } else { 1 },
|
||||
..,
|
||||
),
|
||||
&mut row,
|
||||
);
|
||||
row += 1;
|
||||
|
||||
if self.updates_received == 0 {
|
||||
line(Text::new("waiting for SessionUpdate..."), &mut row);
|
||||
} else {
|
||||
line(
|
||||
Text::new(format!("session: {}", self.session_name)),
|
||||
&mut row,
|
||||
);
|
||||
line(
|
||||
Text::new(format!(
|
||||
"clients: {} tabs: {} panes: {}",
|
||||
self.connected_clients, self.tabs, self.panes
|
||||
)),
|
||||
&mut row,
|
||||
);
|
||||
line(
|
||||
Text::new(format!("SessionUpdate events: {}", self.updates_received)),
|
||||
&mut row,
|
||||
);
|
||||
}
|
||||
row += 1;
|
||||
|
||||
for note in [
|
||||
"SessionUpdate is gated behind the",
|
||||
"ReadApplicationState permission.",
|
||||
"This plugin never called",
|
||||
"request_permission() and was never",
|
||||
"prompted, because the distribution",
|
||||
"bundles it into the executable.",
|
||||
] {
|
||||
line(Text::new(note), &mut row);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
use zellij::{Distribution, DistributionLayout};
|
||||
|
||||
fn main() {
|
||||
zellij::run(
|
||||
Distribution::new("example-distribution", env!("CARGO_PKG_VERSION"))
|
||||
.with_display_name("Example Distribution")
|
||||
.with_plugin(
|
||||
"example-plugin",
|
||||
include_bytes!(env!("EXAMPLE_DISTRIBUTION_PLUGIN_WASM")),
|
||||
)
|
||||
.without_plugin("share")
|
||||
.with_config(include_str!("../assets/config.kdl"))
|
||||
.with_layout(DistributionLayout::new(
|
||||
"example",
|
||||
include_str!("../assets/layout.kdl"),
|
||||
)),
|
||||
)
|
||||
}
|
||||
+442
@@ -0,0 +1,442 @@
|
||||
pub mod commands;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use zellij_utils::{
|
||||
cli::{CliAction, CliArgs, Command, Sessions},
|
||||
consts::{create_config_and_cache_folders, VERSION},
|
||||
data::UnblockCondition,
|
||||
distribution::set_distribution,
|
||||
envs,
|
||||
input::config::Config,
|
||||
logging::*,
|
||||
setup::Setup,
|
||||
shared::web_server_base_url_from_config,
|
||||
};
|
||||
|
||||
pub use zellij_utils::distribution::{Distribution, DistributionLayout, DistributionPlugin};
|
||||
|
||||
/// Run Zellij as the given distribution.
|
||||
///
|
||||
/// A distribution bundles its own plugins, configuration and layouts into the executable. Its
|
||||
/// plugins are referenced as `zellij:<name>`, are loaded from memory rather than from disk, and
|
||||
/// are not subject to the plugin permission system, exactly as Zellij's own builtin plugins are.
|
||||
///
|
||||
/// Must be called at most once, before anything else in the process reads configuration.
|
||||
pub fn run(distribution: Distribution) {
|
||||
if let Err(e) = set_distribution(distribution) {
|
||||
eprintln!("Failed to start: {}", e);
|
||||
std::process::exit(2);
|
||||
}
|
||||
main()
|
||||
}
|
||||
|
||||
/// Run stock Zellij
|
||||
pub fn run_zellij() {
|
||||
main()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
configure_logger();
|
||||
create_config_and_cache_folders();
|
||||
let opts = CliArgs::parse_for_distribution();
|
||||
|
||||
{
|
||||
let config = Config::try_from(&opts).ok();
|
||||
if let Some(Command::Action(cli_action)) = opts.command {
|
||||
commands::send_action_to_session(*cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Subscribe(subscribe_cli)) = opts.command {
|
||||
commands::subscribe_to_session(subscribe_cli, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Run {
|
||||
command,
|
||||
direction,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
name,
|
||||
close_on_exit,
|
||||
start_suspended,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success,
|
||||
block_until_exit_failure,
|
||||
block_until_exit,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let cwd = cwd.or_else(|| std::env::current_dir().ok());
|
||||
let skip_plugin_cache = false; // N/A for this action
|
||||
|
||||
// Compute the unblock condition
|
||||
let unblock_condition = if block_until_exit_success {
|
||||
Some(UnblockCondition::OnExitSuccess)
|
||||
} else if block_until_exit_failure {
|
||||
Some(UnblockCondition::OnExitFailure)
|
||||
} else if block_until_exit {
|
||||
Some(UnblockCondition::OnAnyExit)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command_cli_action = CliAction::NewPane {
|
||||
command,
|
||||
plugin: None,
|
||||
direction,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
pane_id: None,
|
||||
name,
|
||||
close_on_exit,
|
||||
start_suspended,
|
||||
configuration: None,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
unblock_condition,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Plugin {
|
||||
url,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
configuration,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let cwd = None;
|
||||
let stacked = false;
|
||||
let blocking = false;
|
||||
let unblock_condition = None;
|
||||
let command_cli_action = CliAction::NewPane {
|
||||
command: vec![],
|
||||
plugin: Some(url),
|
||||
direction: None,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
pane_id: None,
|
||||
name: None,
|
||||
close_on_exit: false,
|
||||
start_suspended: false,
|
||||
configuration,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
unblock_condition,
|
||||
near_current_pane: false,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Edit {
|
||||
file,
|
||||
direction,
|
||||
line_number,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
cwd,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let mut file = file;
|
||||
let cwd = cwd.or_else(|| std::env::current_dir().ok());
|
||||
if file.is_relative() {
|
||||
if let Some(cwd) = cwd.as_ref() {
|
||||
file = cwd.join(file);
|
||||
}
|
||||
}
|
||||
let command_cli_action = CliAction::Edit {
|
||||
file,
|
||||
direction,
|
||||
line_number,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
cwd,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Pipe {
|
||||
name,
|
||||
payload,
|
||||
args,
|
||||
plugin,
|
||||
plugin_configuration,
|
||||
})) = opts.command
|
||||
{
|
||||
let command_cli_action = CliAction::Pipe {
|
||||
name,
|
||||
payload,
|
||||
args,
|
||||
plugin,
|
||||
plugin_configuration,
|
||||
|
||||
force_launch_plugin: false,
|
||||
skip_plugin_cache: false,
|
||||
floating_plugin: None,
|
||||
in_place_plugin: None,
|
||||
plugin_cwd: None,
|
||||
plugin_title: None,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Command::Sessions(Sessions::ListSessions {
|
||||
no_formatting,
|
||||
short,
|
||||
reverse,
|
||||
})) = opts.command
|
||||
{
|
||||
commands::list_sessions(no_formatting, short, reverse);
|
||||
} else if let Some(Command::Sessions(Sessions::ListAliases)) = opts.command {
|
||||
commands::list_aliases(opts);
|
||||
} else if let Some(Command::Sessions(Sessions::Watch { ref session_name })) = opts.command {
|
||||
commands::watch_session(session_name.clone(), opts);
|
||||
} else if let Some(Command::Sessions(Sessions::KillAllSessions { yes })) = opts.command {
|
||||
commands::kill_all_sessions(yes);
|
||||
} else if let Some(Command::Sessions(Sessions::KillSession { ref target_session })) =
|
||||
opts.command
|
||||
{
|
||||
commands::kill_session(target_session);
|
||||
} else if let Some(Command::Sessions(Sessions::DeleteAllSessions { yes, force })) = opts.command
|
||||
{
|
||||
commands::delete_all_sessions(yes, force);
|
||||
} else if let Some(Command::Sessions(Sessions::DeleteSession {
|
||||
ref target_session,
|
||||
force,
|
||||
})) = opts.command
|
||||
{
|
||||
commands::delete_session(target_session, force);
|
||||
} else if let Some(path) = opts.server {
|
||||
commands::start_server(path, opts.debug);
|
||||
} else if opts.layout.is_some() || opts.layout_string.is_some() {
|
||||
if let Some(session_name) = opts
|
||||
.session
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| envs::get_session_name().ok())
|
||||
{
|
||||
let config = Config::try_from(&opts).ok();
|
||||
let options = Setup::from_cli_args(&opts).ok().map(|r| r.2);
|
||||
let new_layout_cli_action = CliAction::NewTab {
|
||||
layout: opts.layout.clone(),
|
||||
layout_string: opts.layout_string.clone(),
|
||||
layout_dir: options.as_ref().and_then(|o| o.layout_dir.clone()),
|
||||
name: None,
|
||||
cwd: options.as_ref().and_then(|o| o.default_cwd.clone()),
|
||||
initial_command: vec![],
|
||||
initial_plugin: None,
|
||||
close_on_exit: Default::default(),
|
||||
start_suspended: Default::default(),
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
no_focus: false,
|
||||
};
|
||||
commands::send_action_to_session(new_layout_cli_action, Some(session_name), config);
|
||||
} else {
|
||||
commands::start_client(opts);
|
||||
}
|
||||
} else if let Some(layout_for_new_session) = &opts.new_session_with_layout {
|
||||
let mut opts = opts.clone();
|
||||
opts.new_session_with_layout = None;
|
||||
opts.layout = Some(layout_for_new_session.clone());
|
||||
commands::start_client(opts);
|
||||
} else if let Some(Command::Web(web_opts)) = &opts.command {
|
||||
if web_opts.get_start() {
|
||||
let daemonize = web_opts.daemonize;
|
||||
commands::start_web_server(
|
||||
opts.clone(),
|
||||
daemonize,
|
||||
web_opts.ip,
|
||||
web_opts.port,
|
||||
web_opts.cert.clone(),
|
||||
web_opts.key.clone(),
|
||||
web_opts.server_startup_timeout,
|
||||
);
|
||||
} else if web_opts.stop {
|
||||
match commands::stop_web_server() {
|
||||
Ok(()) => {
|
||||
println!("Stopped web server.");
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to stop web server: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.status {
|
||||
let mut config_options = commands::get_config_options_from_cli_args(&opts)
|
||||
.expect("Can't find config options");
|
||||
if let Some(ip) = web_opts.ip {
|
||||
config_options.web_server_ip = Some(ip);
|
||||
}
|
||||
if let Some(port) = web_opts.port {
|
||||
config_options.web_server_port = Some(port);
|
||||
}
|
||||
let web_server_base_url = web_server_base_url_from_config(config_options);
|
||||
match commands::web_server_status(&web_server_base_url, web_opts.timeout) {
|
||||
Ok(version) => {
|
||||
let version = version.trim();
|
||||
println!(
|
||||
"Web server online with version: {}. Checked: {}",
|
||||
version, web_server_base_url
|
||||
);
|
||||
if version != VERSION {
|
||||
println!("");
|
||||
println!(
|
||||
"Note: this version differs from the current Zellij version: {}.",
|
||||
VERSION
|
||||
);
|
||||
println!("Consider stopping the server with: zellij web --stop");
|
||||
println!("And then restarting it with: zellij web --start");
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
println!("Web server is offline, checked: {}", web_server_base_url);
|
||||
},
|
||||
}
|
||||
} else if web_opts.create_token {
|
||||
let read_only = false;
|
||||
match commands::create_auth_token(web_opts.token_name.clone(), read_only) {
|
||||
Ok(token_and_name) => {
|
||||
println!("Created token successfully");
|
||||
println!("");
|
||||
println!("{}", token_and_name);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.create_read_only_token {
|
||||
let read_only = true;
|
||||
match commands::create_auth_token(web_opts.token_name.clone(), read_only) {
|
||||
Ok(token_and_name) => {
|
||||
println!("Created token successfully");
|
||||
println!("");
|
||||
println!("{}", token_and_name);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if let Some(token_name_to_revoke) = &web_opts.revoke_token {
|
||||
match commands::revoke_auth_token(token_name_to_revoke) {
|
||||
Ok(revoked) => {
|
||||
if revoked {
|
||||
println!("Successfully revoked token.");
|
||||
} else {
|
||||
eprintln!("Token by that name does not exist.");
|
||||
std::process::exit(2)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to revoke token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.revoke_all_tokens {
|
||||
match commands::revoke_all_auth_tokens() {
|
||||
Ok(_) => {
|
||||
println!("Successfully revoked all auth tokens");
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to revoke all auth tokens: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.list_tokens {
|
||||
match commands::list_auth_tokens() {
|
||||
Ok(token_list) => {
|
||||
for item in token_list {
|
||||
println!("{}", item);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to list tokens: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
commands::start_client(opts);
|
||||
}
|
||||
}
|
||||
+1
-412
@@ -1,414 +1,3 @@
|
||||
mod commands;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
use clap::Parser;
|
||||
use zellij_utils::{
|
||||
cli::{CliAction, CliArgs, Command, Sessions},
|
||||
consts::{create_config_and_cache_folders, VERSION},
|
||||
data::UnblockCondition,
|
||||
envs,
|
||||
input::config::Config,
|
||||
logging::*,
|
||||
setup::Setup,
|
||||
shared::web_server_base_url_from_config,
|
||||
};
|
||||
|
||||
fn main() {
|
||||
configure_logger();
|
||||
create_config_and_cache_folders();
|
||||
let opts = CliArgs::parse();
|
||||
|
||||
{
|
||||
let config = Config::try_from(&opts).ok();
|
||||
if let Some(Command::Action(cli_action)) = opts.command {
|
||||
commands::send_action_to_session(*cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Subscribe(subscribe_cli)) = opts.command {
|
||||
commands::subscribe_to_session(subscribe_cli, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Run {
|
||||
command,
|
||||
direction,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
name,
|
||||
close_on_exit,
|
||||
start_suspended,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success,
|
||||
block_until_exit_failure,
|
||||
block_until_exit,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let cwd = cwd.or_else(|| std::env::current_dir().ok());
|
||||
let skip_plugin_cache = false; // N/A for this action
|
||||
|
||||
// Compute the unblock condition
|
||||
let unblock_condition = if block_until_exit_success {
|
||||
Some(UnblockCondition::OnExitSuccess)
|
||||
} else if block_until_exit_failure {
|
||||
Some(UnblockCondition::OnExitFailure)
|
||||
} else if block_until_exit {
|
||||
Some(UnblockCondition::OnAnyExit)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let command_cli_action = CliAction::NewPane {
|
||||
command,
|
||||
plugin: None,
|
||||
direction,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
pane_id: None,
|
||||
name,
|
||||
close_on_exit,
|
||||
start_suspended,
|
||||
configuration: None,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
unblock_condition,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Plugin {
|
||||
url,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
configuration,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let cwd = None;
|
||||
let stacked = false;
|
||||
let blocking = false;
|
||||
let unblock_condition = None;
|
||||
let command_cli_action = CliAction::NewPane {
|
||||
command: vec![],
|
||||
plugin: Some(url),
|
||||
direction: None,
|
||||
cwd,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
pane_id: None,
|
||||
name: None,
|
||||
close_on_exit: false,
|
||||
start_suspended: false,
|
||||
configuration,
|
||||
skip_plugin_cache,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
stacked,
|
||||
blocking,
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
unblock_condition,
|
||||
near_current_pane: false,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Edit {
|
||||
file,
|
||||
direction,
|
||||
line_number,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
cwd,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
})) = opts.command
|
||||
{
|
||||
let mut file = file;
|
||||
let cwd = cwd.or_else(|| std::env::current_dir().ok());
|
||||
if file.is_relative() {
|
||||
if let Some(cwd) = cwd.as_ref() {
|
||||
file = cwd.join(file);
|
||||
}
|
||||
}
|
||||
let command_cli_action = CliAction::Edit {
|
||||
file,
|
||||
direction,
|
||||
line_number,
|
||||
floating,
|
||||
in_place,
|
||||
close_replaced_pane,
|
||||
cwd,
|
||||
x,
|
||||
y,
|
||||
width,
|
||||
height,
|
||||
pinned,
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
tab_id,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
if let Some(Command::Sessions(Sessions::Pipe {
|
||||
name,
|
||||
payload,
|
||||
args,
|
||||
plugin,
|
||||
plugin_configuration,
|
||||
})) = opts.command
|
||||
{
|
||||
let command_cli_action = CliAction::Pipe {
|
||||
name,
|
||||
payload,
|
||||
args,
|
||||
plugin,
|
||||
plugin_configuration,
|
||||
|
||||
force_launch_plugin: false,
|
||||
skip_plugin_cache: false,
|
||||
floating_plugin: None,
|
||||
in_place_plugin: None,
|
||||
plugin_cwd: None,
|
||||
plugin_title: None,
|
||||
};
|
||||
commands::send_action_to_session(command_cli_action, opts.session, config);
|
||||
std::process::exit(0);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(Command::Sessions(Sessions::ListSessions {
|
||||
no_formatting,
|
||||
short,
|
||||
reverse,
|
||||
})) = opts.command
|
||||
{
|
||||
commands::list_sessions(no_formatting, short, reverse);
|
||||
} else if let Some(Command::Sessions(Sessions::ListAliases)) = opts.command {
|
||||
commands::list_aliases(opts);
|
||||
} else if let Some(Command::Sessions(Sessions::Watch { ref session_name })) = opts.command {
|
||||
commands::watch_session(session_name.clone(), opts);
|
||||
} else if let Some(Command::Sessions(Sessions::KillAllSessions { yes })) = opts.command {
|
||||
commands::kill_all_sessions(yes);
|
||||
} else if let Some(Command::Sessions(Sessions::KillSession { ref target_session })) =
|
||||
opts.command
|
||||
{
|
||||
commands::kill_session(target_session);
|
||||
} else if let Some(Command::Sessions(Sessions::DeleteAllSessions { yes, force })) = opts.command
|
||||
{
|
||||
commands::delete_all_sessions(yes, force);
|
||||
} else if let Some(Command::Sessions(Sessions::DeleteSession {
|
||||
ref target_session,
|
||||
force,
|
||||
})) = opts.command
|
||||
{
|
||||
commands::delete_session(target_session, force);
|
||||
} else if let Some(path) = opts.server {
|
||||
commands::start_server(path, opts.debug);
|
||||
} else if opts.layout.is_some() || opts.layout_string.is_some() {
|
||||
if let Some(session_name) = opts
|
||||
.session
|
||||
.as_ref()
|
||||
.cloned()
|
||||
.or_else(|| envs::get_session_name().ok())
|
||||
{
|
||||
let config = Config::try_from(&opts).ok();
|
||||
let options = Setup::from_cli_args(&opts).ok().map(|r| r.2);
|
||||
let new_layout_cli_action = CliAction::NewTab {
|
||||
layout: opts.layout.clone(),
|
||||
layout_string: opts.layout_string.clone(),
|
||||
layout_dir: options.as_ref().and_then(|o| o.layout_dir.clone()),
|
||||
name: None,
|
||||
cwd: options.as_ref().and_then(|o| o.default_cwd.clone()),
|
||||
initial_command: vec![],
|
||||
initial_plugin: None,
|
||||
close_on_exit: Default::default(),
|
||||
start_suspended: Default::default(),
|
||||
block_until_exit_success: false,
|
||||
block_until_exit_failure: false,
|
||||
block_until_exit: false,
|
||||
no_focus: false,
|
||||
};
|
||||
commands::send_action_to_session(new_layout_cli_action, Some(session_name), config);
|
||||
} else {
|
||||
commands::start_client(opts);
|
||||
}
|
||||
} else if let Some(layout_for_new_session) = &opts.new_session_with_layout {
|
||||
let mut opts = opts.clone();
|
||||
opts.new_session_with_layout = None;
|
||||
opts.layout = Some(layout_for_new_session.clone());
|
||||
commands::start_client(opts);
|
||||
} else if let Some(Command::Web(web_opts)) = &opts.command {
|
||||
if web_opts.get_start() {
|
||||
let daemonize = web_opts.daemonize;
|
||||
commands::start_web_server(
|
||||
opts.clone(),
|
||||
daemonize,
|
||||
web_opts.ip,
|
||||
web_opts.port,
|
||||
web_opts.cert.clone(),
|
||||
web_opts.key.clone(),
|
||||
web_opts.server_startup_timeout,
|
||||
);
|
||||
} else if web_opts.stop {
|
||||
match commands::stop_web_server() {
|
||||
Ok(()) => {
|
||||
println!("Stopped web server.");
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to stop web server: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.status {
|
||||
let mut config_options = commands::get_config_options_from_cli_args(&opts)
|
||||
.expect("Can't find config options");
|
||||
if let Some(ip) = web_opts.ip {
|
||||
config_options.web_server_ip = Some(ip);
|
||||
}
|
||||
if let Some(port) = web_opts.port {
|
||||
config_options.web_server_port = Some(port);
|
||||
}
|
||||
let web_server_base_url = web_server_base_url_from_config(config_options);
|
||||
match commands::web_server_status(&web_server_base_url, web_opts.timeout) {
|
||||
Ok(version) => {
|
||||
let version = version.trim();
|
||||
println!(
|
||||
"Web server online with version: {}. Checked: {}",
|
||||
version, web_server_base_url
|
||||
);
|
||||
if version != VERSION {
|
||||
println!("");
|
||||
println!(
|
||||
"Note: this version differs from the current Zellij version: {}.",
|
||||
VERSION
|
||||
);
|
||||
println!("Consider stopping the server with: zellij web --stop");
|
||||
println!("And then restarting it with: zellij web --start");
|
||||
}
|
||||
},
|
||||
Err(_e) => {
|
||||
println!("Web server is offline, checked: {}", web_server_base_url);
|
||||
},
|
||||
}
|
||||
} else if web_opts.create_token {
|
||||
let read_only = false;
|
||||
match commands::create_auth_token(web_opts.token_name.clone(), read_only) {
|
||||
Ok(token_and_name) => {
|
||||
println!("Created token successfully");
|
||||
println!("");
|
||||
println!("{}", token_and_name);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.create_read_only_token {
|
||||
let read_only = true;
|
||||
match commands::create_auth_token(web_opts.token_name.clone(), read_only) {
|
||||
Ok(token_and_name) => {
|
||||
println!("Created token successfully");
|
||||
println!("");
|
||||
println!("{}", token_and_name);
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to create token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if let Some(token_name_to_revoke) = &web_opts.revoke_token {
|
||||
match commands::revoke_auth_token(token_name_to_revoke) {
|
||||
Ok(revoked) => {
|
||||
if revoked {
|
||||
println!("Successfully revoked token.");
|
||||
} else {
|
||||
eprintln!("Token by that name does not exist.");
|
||||
std::process::exit(2)
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to revoke token: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.revoke_all_tokens {
|
||||
match commands::revoke_all_auth_tokens() {
|
||||
Ok(_) => {
|
||||
println!("Successfully revoked all auth tokens");
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to revoke all auth tokens: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
} else if web_opts.list_tokens {
|
||||
match commands::list_auth_tokens() {
|
||||
Ok(token_list) => {
|
||||
for item in token_list {
|
||||
println!("{}", item);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!("Failed to list tokens: {}", e);
|
||||
std::process::exit(2)
|
||||
},
|
||||
}
|
||||
}
|
||||
} else {
|
||||
commands::start_client(opts);
|
||||
}
|
||||
zellij::run_zellij()
|
||||
}
|
||||
|
||||
+26
-10
@@ -3,14 +3,30 @@ use std::net::{IpAddr, Ipv4Addr};
|
||||
use clap::{CommandFactory, Parser};
|
||||
use zellij_utils::cli::{CliArgs, Command};
|
||||
|
||||
const CLI_PARSE_TEST_STACK_SIZE: usize = 16 * 1024 * 1024;
|
||||
|
||||
fn on_large_stack<T: Send + 'static>(f: impl FnOnce() -> T + Send + 'static) -> T {
|
||||
std::thread::Builder::new()
|
||||
.stack_size(CLI_PARSE_TEST_STACK_SIZE)
|
||||
.spawn(f)
|
||||
.expect("failed to spawn cli parse test thread")
|
||||
.join()
|
||||
.expect("cli parse test thread panicked")
|
||||
}
|
||||
|
||||
fn try_parse(args: &[&str]) -> Result<CliArgs, clap::Error> {
|
||||
let args: Vec<String> = args.iter().map(|arg| arg.to_string()).collect();
|
||||
on_large_stack(move || CliArgs::try_parse_from(args))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn verify_cli() {
|
||||
CliArgs::command().debug_assert();
|
||||
on_large_stack(|| CliArgs::command().debug_assert());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_alone_works() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status"]);
|
||||
let args = try_parse(&["zellij", "web", "--status"]);
|
||||
assert!(args.is_ok());
|
||||
if let Ok(CliArgs {
|
||||
command: Some(Command::Web(web)),
|
||||
@@ -26,7 +42,7 @@ fn web_cli_status_alone_works() {
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_timeout_works() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status", "--timeout", "5"]);
|
||||
let args = try_parse(&["zellij", "web", "--status", "--timeout", "5"]);
|
||||
assert!(args.is_ok());
|
||||
if let Ok(CliArgs {
|
||||
command: Some(Command::Web(web)),
|
||||
@@ -43,7 +59,7 @@ fn web_cli_status_with_timeout_works() {
|
||||
#[test]
|
||||
fn web_cli_timeout_with_status_works() {
|
||||
// Test with --timeout before --status (order shouldn't matter)
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--timeout", "10", "--status"]);
|
||||
let args = try_parse(&["zellij", "web", "--timeout", "10", "--status"]);
|
||||
assert!(args.is_ok());
|
||||
if let Ok(CliArgs {
|
||||
command: Some(Command::Web(web)),
|
||||
@@ -59,25 +75,25 @@ fn web_cli_timeout_with_status_works() {
|
||||
|
||||
#[test]
|
||||
fn web_cli_timeout_without_status_fails() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--timeout", "5"]);
|
||||
let args = try_parse(&["zellij", "web", "--timeout", "5"]);
|
||||
assert!(args.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_start_fails() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status", "--start"]);
|
||||
let args = try_parse(&["zellij", "web", "--status", "--start"]);
|
||||
assert!(args.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_stop_fails() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status", "--stop"]);
|
||||
let args = try_parse(&["zellij", "web", "--status", "--stop"]);
|
||||
assert!(args.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_ip_works() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status", "--ip", "127.0.0.1"]);
|
||||
let args = try_parse(&["zellij", "web", "--status", "--ip", "127.0.0.1"]);
|
||||
assert!(args.is_ok());
|
||||
if let Ok(CliArgs {
|
||||
command: Some(Command::Web(web)),
|
||||
@@ -93,7 +109,7 @@ fn web_cli_status_with_ip_works() {
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_port_works() {
|
||||
let args = CliArgs::try_parse_from(["zellij", "web", "--status", "--port", "9000"]);
|
||||
let args = try_parse(&["zellij", "web", "--status", "--port", "9000"]);
|
||||
assert!(args.is_ok());
|
||||
if let Ok(CliArgs {
|
||||
command: Some(Command::Web(web)),
|
||||
@@ -109,7 +125,7 @@ fn web_cli_status_with_port_works() {
|
||||
|
||||
#[test]
|
||||
fn web_cli_status_with_ip_and_port_works() {
|
||||
let args = CliArgs::try_parse_from([
|
||||
let args = try_parse(&[
|
||||
"zellij", "web", "--status", "--ip", "0.0.0.0", "--port", "9000",
|
||||
]);
|
||||
assert!(args.is_ok());
|
||||
|
||||
+26
-13
@@ -362,10 +362,12 @@ fn check_ipc_pipe_length(ipc_pipe: &Path) {
|
||||
"Error: the IPC socket path is too long ({} bytes, max {}):\n {}\n\n\
|
||||
This is usually caused by a long $TMPDIR path.\n\
|
||||
To fix this, set a shorter socket directory, eg.:\n \
|
||||
ZELLIJ_SOCKET_DIR=/tmp/zellij zellij",
|
||||
ZELLIJ_SOCKET_DIR=/tmp/{} {}",
|
||||
path_len,
|
||||
ZELLIJ_SOCK_MAX_LENGTH - 1,
|
||||
ipc_pipe.display()
|
||||
ipc_pipe.display(),
|
||||
zellij_utils::distribution::name(),
|
||||
zellij_utils::distribution::name()
|
||||
);
|
||||
std::process::exit(1);
|
||||
}
|
||||
@@ -408,14 +410,18 @@ fn exit_after_startup_error(teardown: Option<TerminalTeardown>, message: String)
|
||||
|
||||
fn spawn_server_error_message(e: io::Error) -> String {
|
||||
format!(
|
||||
"Error: failed to start the Zellij server process:\n\n\
|
||||
"Error: failed to start the {} server process:\n\n\
|
||||
Reason: {}\n\n\
|
||||
This can happen if the Zellij binary cannot be executed, or if the server \
|
||||
This can happen if the {} binary cannot be executed, or if the server \
|
||||
could not create its session socket - for example due to a permission issue \
|
||||
in the socket directory.\n\
|
||||
To fix a socket directory issue, set a writable socket directory, eg.:\n \
|
||||
ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij",
|
||||
e
|
||||
ZELLIJ_SOCKET_DIR=/tmp/{}-$USER {}",
|
||||
zellij_utils::distribution::display_name(),
|
||||
e,
|
||||
zellij_utils::distribution::display_name(),
|
||||
zellij_utils::distribution::name(),
|
||||
zellij_utils::distribution::name()
|
||||
)
|
||||
}
|
||||
|
||||
@@ -425,15 +431,19 @@ fn create_ipc_pipe(teardown: Option<TerminalTeardown>) -> PathBuf {
|
||||
exit_after_startup_error(
|
||||
teardown,
|
||||
format!(
|
||||
"Error: failed to create the Zellij socket directory:\n {}\n\n\
|
||||
"Error: failed to create the {} socket directory:\n {}\n\n\
|
||||
Reason: {}\n\n\
|
||||
This usually means the directory (or one of its parents) is owned by \
|
||||
another user or is not writable - for example if Zellij was previously \
|
||||
another user or is not writable - for example if {} was previously \
|
||||
run with `sudo`, or if $XDG_RUNTIME_DIR points to a directory you do not \
|
||||
own.\nTo fix this, remove or correct the offending directory, or set a \
|
||||
writable socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij",
|
||||
writable socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/{}-$USER {}",
|
||||
zellij_utils::distribution::display_name(),
|
||||
sock_dir.display(),
|
||||
e
|
||||
e,
|
||||
zellij_utils::distribution::display_name(),
|
||||
zellij_utils::distribution::name(),
|
||||
zellij_utils::distribution::name()
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -441,13 +451,16 @@ fn create_ipc_pipe(teardown: Option<TerminalTeardown>) -> PathBuf {
|
||||
exit_after_startup_error(
|
||||
teardown,
|
||||
format!(
|
||||
"Error: failed to set permissions (0700) on the Zellij socket directory:\n {}\n\n\
|
||||
"Error: failed to set permissions (0700) on the {} socket directory:\n {}\n\n\
|
||||
Reason: {}\n\n\
|
||||
This usually means the directory is owned by another user.\n\
|
||||
To fix this, remove or correct the offending directory, or set a writable \
|
||||
socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/zellij-$USER zellij",
|
||||
socket directory, eg.:\n ZELLIJ_SOCKET_DIR=/tmp/{}-$USER {}",
|
||||
zellij_utils::distribution::display_name(),
|
||||
sock_dir.display(),
|
||||
e
|
||||
e,
|
||||
zellij_utils::distribution::name(),
|
||||
zellij_utils::distribution::name()
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -97,6 +97,11 @@ pub fn zellij_server_listener(
|
||||
colors: palette,
|
||||
rounded_corners: config.ui.pane_frames.rounded_corners,
|
||||
hide_session_name: config.ui.pane_frames.hide_session_name,
|
||||
border_style: config.ui.pane_frames.resolved_border_style(),
|
||||
floating_border_style: config
|
||||
.ui
|
||||
.pane_frames
|
||||
.resolved_floating_border_style(),
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -368,6 +368,7 @@ fn control_payload_to_server_msg(
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
},
|
||||
terminal_id: None,
|
||||
client_id: None,
|
||||
|
||||
@@ -164,25 +164,23 @@ impl ServerOsApi for FakeServerOsApi {
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
let ipc_receiver = IpcReceiverWithContext::new(stream);
|
||||
let non_blocking_client_sender = NonBlockingClientSender::new(ipc_receiver.get_sender());
|
||||
receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
let non_blocking_client_sender = NonBlockingClientSender::new(receiver.get_sender());
|
||||
self.non_blocking_client_senders
|
||||
.lock()
|
||||
.unwrap()
|
||||
.insert(client_id, non_blocking_client_sender);
|
||||
Ok(ipc_receiver)
|
||||
Ok(())
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!("windows dual-pipe IPC is not used by the test harness")
|
||||
}
|
||||
fn remove_client(&mut self, client_id: ClientId) -> Result<()> {
|
||||
|
||||
@@ -320,6 +320,7 @@ fn new_pane_cli_action(
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
#![cfg(unix)]
|
||||
|
||||
use std::sync::Once;
|
||||
|
||||
use zellij_integration_tests::{normalized, LayoutInfo, TestRunner, TERMINAL_SIZE};
|
||||
use zellij_utils::distribution::{self, Distribution, DistributionLayout};
|
||||
|
||||
const FIXTURE_PLUGIN: &[u8] = include_bytes!(concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm"
|
||||
));
|
||||
|
||||
const FIXTURE_PLUGIN_PATH: &str = concat!(
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"/../zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm"
|
||||
);
|
||||
|
||||
const PERMISSION_PROMPT: &str = "Allow? (y/n)";
|
||||
|
||||
const BUNDLED_LAYOUT: &str = r#"
|
||||
layout {
|
||||
pane size=1 borderless=true {
|
||||
plugin location="tab-bar"
|
||||
}
|
||||
pane
|
||||
pane size=10 borderless=true {
|
||||
plugin location="zellij:bundled-plugin"
|
||||
}
|
||||
pane size=2 borderless=true {
|
||||
plugin location="status-bar"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
|
||||
static INSTALL: Once = Once::new();
|
||||
|
||||
fn install_distribution() {
|
||||
INSTALL.call_once(|| {
|
||||
distribution::set_distribution(
|
||||
Distribution::new("testdist", "9.9.9")
|
||||
.with_display_name("Test Distribution")
|
||||
.with_plugin("bundled-plugin", FIXTURE_PLUGIN)
|
||||
.with_layout(DistributionLayout::new("bundled", BUNDLED_LAYOUT)),
|
||||
)
|
||||
.expect("the test distribution should install");
|
||||
});
|
||||
}
|
||||
|
||||
fn start_with_bundled_layout() -> zellij_integration_tests::TestSession {
|
||||
install_distribution();
|
||||
TestRunner::new(TERMINAL_SIZE)
|
||||
.with_layout(LayoutInfo::BuiltIn("bundled".to_owned()))
|
||||
.start()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bundled_plugin_runs_without_asking_for_permissions() {
|
||||
let mut zellij = start_with_bundled_layout();
|
||||
let terminal = zellij.expect_pty_spawn();
|
||||
terminal.output(b"$ ");
|
||||
|
||||
let grid_snapshot = zellij.wait_until("bundled plugin rendered its own output", |grid| {
|
||||
grid.contains("Received events:") && grid.tab_bar_appears() && grid.status_bar_appears()
|
||||
});
|
||||
assert!(
|
||||
!grid_snapshot.contains(PERMISSION_PROMPT),
|
||||
"a bundled plugin must not ask for permissions, got:\n{}",
|
||||
grid_snapshot.text
|
||||
);
|
||||
zellij.quit();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_same_plugin_loaded_from_a_file_still_asks_for_permissions() {
|
||||
install_distribution();
|
||||
let file_plugin_layout = format!(
|
||||
r#"
|
||||
layout {{
|
||||
pane size=1 borderless=true {{
|
||||
plugin location="tab-bar"
|
||||
}}
|
||||
pane
|
||||
pane size=10 borderless=true {{
|
||||
plugin location="file:{}"
|
||||
}}
|
||||
pane size=2 borderless=true {{
|
||||
plugin location="status-bar"
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
FIXTURE_PLUGIN_PATH
|
||||
);
|
||||
let mut zellij = TestRunner::new(TERMINAL_SIZE)
|
||||
.with_layout(LayoutInfo::Stringified(file_plugin_layout))
|
||||
.start();
|
||||
let terminal = zellij.expect_pty_spawn();
|
||||
terminal.output(b"$ ");
|
||||
|
||||
zellij.wait_until("file plugin stops on the permission prompt", |grid| {
|
||||
grid.contains(PERMISSION_PROMPT)
|
||||
});
|
||||
zellij.quit();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_bundled_layout_is_used_to_lay_out_the_session() {
|
||||
let mut zellij = start_with_bundled_layout();
|
||||
let terminal = zellij.expect_pty_spawn();
|
||||
terminal.output(b"$ ");
|
||||
|
||||
let grid_snapshot = zellij.wait_until("bundled layout rendered", |grid| {
|
||||
grid.contains("Received events:") && grid.tab_bar_appears() && grid.status_bar_appears()
|
||||
});
|
||||
insta::assert_snapshot!(normalized(&grid_snapshot));
|
||||
zellij.quit();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_session_runs_under_the_distributions_own_directories() {
|
||||
let mut zellij = start_with_bundled_layout();
|
||||
let terminal = zellij.expect_pty_spawn();
|
||||
terminal.output(b"$ ");
|
||||
zellij.wait_until("app loaded", |grid| {
|
||||
grid.tab_bar_appears() && grid.status_bar_appears()
|
||||
});
|
||||
|
||||
let cache_dir = zellij_utils::consts::ZELLIJ_CACHE_DIR.display().to_string();
|
||||
assert!(
|
||||
cache_dir.contains("testdist"),
|
||||
"the running session should use the distribution's cache dir, got '{}'",
|
||||
cache_dir
|
||||
);
|
||||
zellij.quit();
|
||||
}
|
||||
@@ -36,6 +36,7 @@ fn no_focus_new_pane_action(command: &[&str]) -> CliAction {
|
||||
no_focus: true,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ fn replace_pane_in_place_with_held_command(
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
});
|
||||
zellij.expect_pty_spawn()
|
||||
}
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
---
|
||||
source: zellij-integration-tests/tests/distribution.rs
|
||||
expression: normalized(&grid_snapshot)
|
||||
---
|
||||
Zellij (test) Tab #1 +
|
||||
$ █
|
||||
────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
Rows: 10, Cols: 120, Received events: []
|
||||
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT BASE
|
||||
+1
-1
@@ -4,4 +4,4 @@ expression: normalized(&grid_snapshot)
|
||||
---
|
||||
Zellij (test) Tab #1 Tab #2 +
|
||||
$ █
|
||||
|
||||
Ctrl + <g> LOCK <p> PANE <t> TAB <n> RESIZE <h> MOVE <s> SEARCH <o> SESSION <q> QUIT ...
|
||||
|
||||
@@ -151,7 +151,10 @@ fn mouse_click_new_tab_button_in_tab_bar_opens_a_new_tab() {
|
||||
new_tab_terminal.output(PROMPT);
|
||||
|
||||
let grid_snapshot = zellij.wait_until("a second tab opened", |grid_snapshot| {
|
||||
grid_snapshot.contains("Tab #1") && grid_snapshot.contains("Tab #2")
|
||||
grid_snapshot.contains("Tab #1")
|
||||
&& grid_snapshot.contains("Tab #2")
|
||||
&& grid_snapshot.contains("$ ")
|
||||
&& grid_snapshot.status_bar_appears()
|
||||
});
|
||||
assert_snapshot!(normalized(&grid_snapshot));
|
||||
zellij.quit();
|
||||
|
||||
+142
-85
@@ -75,11 +75,17 @@ use zellij_utils::{
|
||||
options::Options,
|
||||
plugins::PluginAliases,
|
||||
},
|
||||
ipc::{ClientAttributes, ExitReason, ServerToClientMsg},
|
||||
ipc::{
|
||||
ClientAttributes, ClientToServerMsg, ExitReason, IpcReceiverWithContext, ServerToClientMsg,
|
||||
},
|
||||
shared::{default_palette, web_server_base_url},
|
||||
};
|
||||
|
||||
pub type ClientId = u16;
|
||||
pub use zellij_utils::data::ClientId;
|
||||
|
||||
const ACCEPT_ERROR_BACKOFF: std::time::Duration = std::time::Duration::from_millis(100);
|
||||
|
||||
const PRE_HANDSHAKE_READ_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(30);
|
||||
|
||||
/// Instructions related to server-side application
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -464,6 +470,11 @@ impl SessionMetaData {
|
||||
auto_layout: new_config.options.auto_layout.unwrap_or(true),
|
||||
rounded_corners: new_config.ui.pane_frames.rounded_corners,
|
||||
hide_session_name: new_config.ui.pane_frames.hide_session_name,
|
||||
border_style: new_config.ui.pane_frames.resolved_border_style(),
|
||||
floating_border_style: new_config
|
||||
.ui
|
||||
.pane_frames
|
||||
.resolved_floating_border_style(),
|
||||
stacked_resize: new_config.options.stacked_resize.unwrap_or(true),
|
||||
stacked_pane_list: new_config.options.stacked_pane_list.unwrap_or(true),
|
||||
default_editor: new_config.options.scrollback_editor.clone(),
|
||||
@@ -640,6 +651,7 @@ pub(crate) struct SessionState {
|
||||
/// empty synthetic reply so `Screen`'s `forward_in_flight` slot
|
||||
/// releases and the queued forwards keep moving.
|
||||
forwards_in_flight: HashMap<u32, ClientId>,
|
||||
next_client_id: ClientId,
|
||||
}
|
||||
|
||||
impl SessionState {
|
||||
@@ -650,26 +662,33 @@ impl SessionState {
|
||||
watchers: HashMap::new(),
|
||||
last_active_client: None,
|
||||
forwards_in_flight: HashMap::new(),
|
||||
next_client_id: 1,
|
||||
}
|
||||
}
|
||||
pub fn new_client(&mut self) -> ClientId {
|
||||
let all_ids: HashSet<ClientId> = self
|
||||
let client_id = match self.next_client_id.checked_add(1) {
|
||||
Some(following_client_id) => {
|
||||
let client_id = self.next_client_id;
|
||||
self.next_client_id = following_client_id;
|
||||
client_id
|
||||
},
|
||||
None => self.lowest_unused_client_id(),
|
||||
};
|
||||
self.clients.insert(client_id, None);
|
||||
client_id
|
||||
}
|
||||
fn lowest_unused_client_id(&self) -> ClientId {
|
||||
let taken: HashSet<ClientId> = self
|
||||
.clients
|
||||
.keys()
|
||||
.copied()
|
||||
.chain(self.watchers.keys().copied())
|
||||
.collect();
|
||||
|
||||
let mut next_client_id = 1;
|
||||
loop {
|
||||
if all_ids.contains(&next_client_id) {
|
||||
next_client_id += 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
let mut client_id = 1;
|
||||
while taken.contains(&client_id) {
|
||||
client_id += 1;
|
||||
}
|
||||
self.clients.insert(next_client_id, None);
|
||||
next_client_id
|
||||
client_id
|
||||
}
|
||||
pub fn associate_pipe_with_client(&mut self, pipe_id: String, client_id: ClientId) {
|
||||
self.pipes.insert(pipe_id, client_id);
|
||||
@@ -893,6 +912,54 @@ pub fn start_server(os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
|
||||
start_server_impl(os_input, socket_path, true);
|
||||
}
|
||||
|
||||
fn handle_new_connection(
|
||||
session_data: Arc<RwLock<Option<SessionMetaData>>>,
|
||||
session_state: Arc<RwLock<SessionState>>,
|
||||
mut os_input: Box<dyn ServerOsApi>,
|
||||
to_server: SenderWithContext<ServerInstruction>,
|
||||
stream: interprocess::local_socket::Stream,
|
||||
#[cfg(windows)] reply_stream: interprocess::local_socket::Stream,
|
||||
) -> Result<()> {
|
||||
let mut receiver: IpcReceiverWithContext<ClientToServerMsg> =
|
||||
IpcReceiverWithContext::new(stream);
|
||||
|
||||
let _ = receiver.set_read_timeout(Some(PRE_HANDSHAKE_READ_TIMEOUT));
|
||||
let first_instruction = match receiver.try_recv_client_msg() {
|
||||
Ok((instruction, _err_ctx)) => instruction,
|
||||
Err(_) => return Ok(()),
|
||||
};
|
||||
let _ = receiver.set_read_timeout(None);
|
||||
|
||||
if let ClientToServerMsg::ConnStatus = first_instruction {
|
||||
let mut sender = receiver.get_sender::<ServerToClientMsg>();
|
||||
let _ = sender.send_server_msg(ServerToClientMsg::Connected);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let client_id = session_state.write().unwrap().new_client();
|
||||
|
||||
#[cfg(windows)]
|
||||
let registered = os_input.register_client_with_reply(client_id, reply_stream);
|
||||
#[cfg(not(windows))]
|
||||
let registered = os_input.register_client(client_id, &receiver);
|
||||
|
||||
if let Err(err) = registered {
|
||||
log::error!("Failed to register client {}: {:?}", client_id, err);
|
||||
let _ = session_state.write().unwrap().remove_client(client_id);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
route_thread_main(
|
||||
session_data,
|
||||
session_state,
|
||||
os_input,
|
||||
to_server,
|
||||
receiver,
|
||||
client_id,
|
||||
Some(first_instruction),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn start_server_impl(
|
||||
mut os_input: Box<dyn ServerOsApi>,
|
||||
socket_path: PathBuf,
|
||||
@@ -946,41 +1013,41 @@ pub fn start_server_impl(
|
||||
for stream in listener.incoming() {
|
||||
match stream {
|
||||
Ok(stream) => {
|
||||
let mut os_input = os_input.clone();
|
||||
let client_id = session_state.write().unwrap().new_client();
|
||||
let os_input = os_input.clone();
|
||||
|
||||
#[cfg(windows)]
|
||||
let reply_stream = reply_listener
|
||||
.accept()
|
||||
.expect("failed to accept reply connection");
|
||||
|
||||
#[cfg(windows)]
|
||||
let receiver = os_input
|
||||
.new_client_with_reply(client_id, stream, reply_stream)
|
||||
.unwrap();
|
||||
#[cfg(not(windows))]
|
||||
let receiver = os_input.new_client(client_id, stream).unwrap();
|
||||
let reply_stream = match reply_listener.accept() {
|
||||
Ok(reply_stream) => reply_stream,
|
||||
Err(err) => {
|
||||
log::error!("Failed to accept reply connection: {:?}", err);
|
||||
continue;
|
||||
},
|
||||
};
|
||||
|
||||
let session_data = session_data.clone();
|
||||
let session_state = session_state.clone();
|
||||
let to_server = to_server.clone();
|
||||
thread::Builder::new()
|
||||
let spawn_result = thread::Builder::new()
|
||||
.name("server_router".to_string())
|
||||
.spawn(move || {
|
||||
route_thread_main(
|
||||
handle_new_connection(
|
||||
session_data,
|
||||
session_state,
|
||||
os_input,
|
||||
to_server,
|
||||
receiver,
|
||||
client_id,
|
||||
stream,
|
||||
#[cfg(windows)]
|
||||
reply_stream,
|
||||
)
|
||||
.fatal()
|
||||
})
|
||||
.unwrap();
|
||||
});
|
||||
if let Err(err) = spawn_result {
|
||||
log::error!("Failed to spawn router thread: {:?}", err);
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
panic!("err {:?}", err);
|
||||
log::error!("Failed to accept connection: {:?}", err);
|
||||
thread::sleep(ACCEPT_ERROR_BACKOFF);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -999,10 +1066,15 @@ pub fn start_server_impl(
|
||||
== Some(LayoutInfo::BuiltIn("welcome".to_owned()))
|
||||
|| config.options.default_layout == Some(PathBuf::from("welcome"));
|
||||
|
||||
let successfully_written_config = Config::write_config_to_disk_if_it_does_not_exist(
|
||||
config.to_string(true),
|
||||
&cli_assets.config_file_path,
|
||||
);
|
||||
let successfully_written_config =
|
||||
if zellij_utils::distribution::bundled_config().is_some() {
|
||||
false
|
||||
} else {
|
||||
Config::write_config_to_disk_if_it_does_not_exist(
|
||||
config.to_string(true),
|
||||
&cli_assets.config_file_path,
|
||||
)
|
||||
};
|
||||
// if we successfully wrote the config to disk, it means two things:
|
||||
// 1. It did not exist beforehand
|
||||
// 2. The config folder is writeable
|
||||
@@ -1026,6 +1098,11 @@ pub fn start_server_impl(
|
||||
.unwrap_or_else(|| default_palette().into()),
|
||||
rounded_corners: config.ui.pane_frames.rounded_corners,
|
||||
hide_session_name: config.ui.pane_frames.hide_session_name,
|
||||
border_style: config.ui.pane_frames.resolved_border_style(),
|
||||
floating_border_style: config
|
||||
.ui
|
||||
.pane_frames
|
||||
.resolved_floating_border_style(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1207,6 +1284,11 @@ pub fn start_server_impl(
|
||||
.unwrap_or_else(|| default_palette().into()),
|
||||
rounded_corners: config.ui.pane_frames.rounded_corners,
|
||||
hide_session_name: config.ui.pane_frames.hide_session_name,
|
||||
border_style: config.ui.pane_frames.resolved_border_style(),
|
||||
floating_border_style: config
|
||||
.ui
|
||||
.pane_frames
|
||||
.resolved_floating_border_style(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1389,22 +1471,14 @@ pub fn start_server_impl(
|
||||
// Handle regular client removal
|
||||
remove_client!(client_id, os_input, session_state, session_data);
|
||||
drop(completion_tx); // prevent deadlock with route thread
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id));
|
||||
}
|
||||
if !session_state.read().unwrap().active_clients_are_connected() {
|
||||
*session_data.write().unwrap() = None;
|
||||
let client_ids_to_cleanup: Vec<ClientId> = session_state
|
||||
@@ -1455,22 +1529,14 @@ pub fn start_server_impl(
|
||||
}
|
||||
// Handle regular client removal
|
||||
remove_client!(client_id, os_input, session_state, session_data);
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id));
|
||||
}
|
||||
}
|
||||
},
|
||||
ServerInstruction::SendWebClientsForbidden(client_id) => {
|
||||
@@ -1535,22 +1601,14 @@ pub fn start_server_impl(
|
||||
// by us having to wait for session_data to send cleanup
|
||||
// signals to the various threads
|
||||
for client_id in client_ids {
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id))
|
||||
.unwrap();
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::RemoveClient(client_id));
|
||||
}
|
||||
}
|
||||
},
|
||||
ServerInstruction::Render(serialized_output) => {
|
||||
@@ -1616,7 +1674,6 @@ pub fn start_server_impl(
|
||||
},
|
||||
ServerInstruction::ConnStatus(client_id) => {
|
||||
let _ = os_input.send_to_client(client_id, ServerToClientMsg::Connected);
|
||||
remove_client!(client_id, os_input, session_state, session_data);
|
||||
},
|
||||
ServerInstruction::Log(
|
||||
lines_to_log,
|
||||
|
||||
@@ -332,18 +332,16 @@ pub trait ServerOsApi: Send + Sync {
|
||||
/// Returns a [`Box`] pointer to this [`ServerOsApi`] struct.
|
||||
fn box_clone(&self) -> Box<dyn ServerOsApi>;
|
||||
fn send_to_client(&self, client_id: ClientId, msg: ServerToClientMsg) -> Result<()>;
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>>;
|
||||
/// Create a new client with a separate reply stream (Windows dual-pipe IPC).
|
||||
fn new_client_with_reply(
|
||||
receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()>;
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>>;
|
||||
) -> Result<()>;
|
||||
fn remove_client(&mut self, client_id: ClientId) -> Result<()>;
|
||||
fn load_palette(&self) -> Palette;
|
||||
/// Returns the current working directory for a given pid
|
||||
@@ -461,35 +459,32 @@ impl ServerOsApi for ServerOsInputOutput {
|
||||
}
|
||||
}
|
||||
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
let receiver = IpcReceiverWithContext::new(stream);
|
||||
receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
let sender = ClientSender::new(client_id, receiver.get_sender());
|
||||
self.client_senders
|
||||
.lock()
|
||||
.to_anyhow()
|
||||
.with_context(|| format!("failed to create new client {client_id}"))?
|
||||
.insert(client_id, sender);
|
||||
Ok(receiver)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
stream: LocalSocketStream,
|
||||
reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
let receiver = IpcReceiverWithContext::new(stream);
|
||||
) -> Result<()> {
|
||||
let sender = ClientSender::new(client_id, IpcSenderWithContext::new(reply_stream));
|
||||
self.client_senders
|
||||
.lock()
|
||||
.to_anyhow()
|
||||
.with_context(|| format!("failed to create new client {client_id}"))?
|
||||
.insert(client_id, sender);
|
||||
Ok(receiver)
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn remove_client(&mut self, client_id: ClientId) -> Result<()> {
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::panes::Row;
|
||||
|
||||
use crate::panes::Selection;
|
||||
use crate::{
|
||||
panes::kitty_graphics::store::{InternalImageId, KittyImageStore},
|
||||
panes::kitty_graphics::store::{InternalImageId, KittyImageStore, ScaledImageKey},
|
||||
panes::sixel::SixelImageStore,
|
||||
panes::terminal_character::{AnsiCode, CharacterStyles},
|
||||
panes::{LinkHandler, PaneId, TerminalCharacter, DEFAULT_STYLES, EMPTY_TERMINAL_CHARACTER},
|
||||
@@ -371,13 +371,13 @@ pub struct KittyChunkKey {
|
||||
pub struct HostPlacementRecord {
|
||||
pub host_image_id: u32,
|
||||
pub host_placement_id: u32,
|
||||
pub image_key: (InternalImageId, Option<(u16, u16)>),
|
||||
pub image_key: (InternalImageId, Option<ScaledImageKey>),
|
||||
pub geometry: KittyImageChunk,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct HostKittyState {
|
||||
pub transmitted: HashMap<(InternalImageId, Option<(u16, u16)>), u32>,
|
||||
pub transmitted: HashMap<(InternalImageId, Option<ScaledImageKey>), u32>,
|
||||
pub live_placements: HashMap<KittyChunkKey, HostPlacementRecord>,
|
||||
next_host_image_id: u32,
|
||||
next_host_placement_id: u32,
|
||||
@@ -469,10 +469,13 @@ fn serialize_kitty_frame(kitty_input: KittyFrameInput) -> Result<String> {
|
||||
host_state.live_placements.clear();
|
||||
host_state.transmitted.clear();
|
||||
}
|
||||
let mut freed: Vec<((InternalImageId, Option<(u16, u16)>), u32)> = host_state
|
||||
let mut freed: Vec<((InternalImageId, Option<ScaledImageKey>), u32)> = host_state
|
||||
.transmitted
|
||||
.iter()
|
||||
.filter(|(image_key, _)| kitty_image_store.get(image_key.0).is_none())
|
||||
.filter(|(image_key, _)| match image_key.1 {
|
||||
Some(key) => kitty_image_store.scaled_variant(image_key.0, key).is_none(),
|
||||
None => kitty_image_store.get(image_key.0).is_none(),
|
||||
})
|
||||
.map(|(image_key, host_id)| (*image_key, *host_id))
|
||||
.collect();
|
||||
freed.sort_by_key(|(_, host_id)| *host_id);
|
||||
@@ -542,19 +545,15 @@ fn serialize_kitty_frame(kitty_input: KittyFrameInput) -> Result<String> {
|
||||
host_state.live_placements.remove(&key);
|
||||
}
|
||||
for (key, chunk) in current {
|
||||
let variant = if chunk.scaled_px.is_some() {
|
||||
Some(chunk.dest_cells)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let variant = chunk.scaled_image;
|
||||
let image_key = (chunk.internal_image_id, variant);
|
||||
let host_image_id = match host_state.transmitted.get(&image_key) {
|
||||
Some(host_image_id) => *host_image_id,
|
||||
None => {
|
||||
let raster_dims = match variant {
|
||||
Some(cells) => {
|
||||
match kitty_image_store.scaled_variant(chunk.internal_image_id, cells) {
|
||||
Some(_) => chunk.scaled_px,
|
||||
Some(key) => {
|
||||
match kitty_image_store.scaled_variant(chunk.internal_image_id, key) {
|
||||
Some(_) => Some(key.size),
|
||||
None => None,
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1188,8 +1188,7 @@ fn kitty_chunk(
|
||||
cell_offset_x: 0,
|
||||
cell_offset_y: 0,
|
||||
z_index: 0,
|
||||
dest_cells: (3, 2),
|
||||
scaled_px: None,
|
||||
scaled_image: None,
|
||||
placement_uid,
|
||||
}
|
||||
}
|
||||
@@ -1433,7 +1432,6 @@ fn kitty_occlusion_crops_exclude_covered_quarter() {
|
||||
let mut chunk = kitty_chunk(internal, 1, 0, 0);
|
||||
chunk.source_px_width = 40;
|
||||
chunk.source_px_height = 80;
|
||||
chunk.dest_cells = (4, 4);
|
||||
let output = run_kitty_frame(&parts, vec![chunk], Some(floating_panes_stack));
|
||||
let crops = parse_kitty_placement_crops(&output);
|
||||
let crop_set: HashSet<(usize, usize, usize, usize, usize, usize)> =
|
||||
@@ -1559,7 +1557,6 @@ fn kitty_emitted_bytes_roundtrip_through_our_parser() {
|
||||
chunk.cell_offset_x = 3;
|
||||
chunk.cell_offset_y = 4;
|
||||
chunk.z_index = -1;
|
||||
chunk.dest_cells = (2, 2);
|
||||
let output = run_kitty_frame(&parts, vec![chunk.clone()], None);
|
||||
let commands = extract_kitty_commands(&output);
|
||||
assert!(
|
||||
@@ -1635,3 +1632,120 @@ fn kitty_host_ids_stay_within_signed_32_bit_range() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kitty_cropped_placements_preserve_pixels_after_cell_resize() {
|
||||
use crate::panes::kitty_graphics::grid_state::{KittyGrid, KittyVerticalAnchor};
|
||||
use crate::panes::kitty_graphics::parser::KittyCommand;
|
||||
|
||||
let colors = [
|
||||
[255, 0, 0, 255],
|
||||
[0, 255, 0, 255],
|
||||
[0, 0, 255, 255],
|
||||
[255; 4],
|
||||
];
|
||||
for scaled in [false, true] {
|
||||
let parts = create_test_kitty_parts();
|
||||
let cell = Rc::new(RefCell::new(Some(SizeInPixels {
|
||||
width: 10,
|
||||
height: 20,
|
||||
})));
|
||||
let mut grid = KittyGrid::new(cell.clone(), parts.0.clone());
|
||||
let image = DecodedImage {
|
||||
bytes: (0..40)
|
||||
.flat_map(|y| (0..20).flat_map(move |x| colors[y / 20 * 2 + x / 10]))
|
||||
.collect(),
|
||||
width: 20,
|
||||
height: 40,
|
||||
format: KittyFormat::Rgba32,
|
||||
};
|
||||
let image_id = grid
|
||||
.transmit(
|
||||
&KittyCommand {
|
||||
image_id: Some(1),
|
||||
..Default::default()
|
||||
},
|
||||
image,
|
||||
)
|
||||
.unwrap();
|
||||
let internal = grid.pane_image_id_map()[&image_id];
|
||||
for i in 0..4 {
|
||||
let command = KittyCommand {
|
||||
image_id: Some(image_id),
|
||||
placement_id: Some(i as u32 + 1),
|
||||
source_x: (i % 2 * 10) as u32,
|
||||
source_y: (i / 2 * 20) as u32,
|
||||
source_w: 10,
|
||||
source_h: 20,
|
||||
columns: u32::from(scaled),
|
||||
rows: u32::from(scaled),
|
||||
..Default::default()
|
||||
};
|
||||
grid.place(
|
||||
image_id,
|
||||
internal,
|
||||
&command,
|
||||
(i * 20, 0),
|
||||
cell.borrow().unwrap(),
|
||||
KittyVerticalAnchor {
|
||||
canonical_line: 0,
|
||||
offset_px_from_line_start: 0,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
let mut host_images = HashMap::new();
|
||||
let mut host_placements = std::collections::BTreeMap::<u32, KittyCommand>::new();
|
||||
for size in [(10, 20), (15, 30)] {
|
||||
*cell.borrow_mut() = Some(SizeInPixels {
|
||||
width: size.0,
|
||||
height: size.1,
|
||||
});
|
||||
grid.character_cell_size_possibly_changed();
|
||||
let chunks = grid.viewport_kitty_chunks(20, 0, 50, 0, 0);
|
||||
let output = run_kitty_frame(&parts, chunks, None);
|
||||
let mut parser = KittyCommandParser::new();
|
||||
for raw in extract_kitty_commands(&output) {
|
||||
if let Some(command) = parser.parse(&raw) {
|
||||
let command = command.unwrap();
|
||||
match command.action {
|
||||
KittyAction::Transmit => {
|
||||
host_images.insert(command.image_id.unwrap(), command.image.unwrap());
|
||||
},
|
||||
KittyAction::Display => {
|
||||
host_placements.insert(command.placement_id.unwrap(), command);
|
||||
},
|
||||
KittyAction::Delete => {
|
||||
let id = command.image_id.unwrap();
|
||||
host_images.remove(&id);
|
||||
host_placements.retain(|_, placement| placement.image_id != Some(id));
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(host_placements.len(), 4);
|
||||
assert_eq!(host_images.len(), if scaled { 4 } else { 1 });
|
||||
for (i, command) in host_placements.values().enumerate() {
|
||||
let image = &host_images[&command.image_id.unwrap()];
|
||||
let expected_size = if scaled { size } else { (10, 20) };
|
||||
assert_eq!(
|
||||
(command.source_w as usize, command.source_h as usize),
|
||||
expected_size
|
||||
);
|
||||
for y in command.source_y..command.source_y + command.source_h {
|
||||
for x in command.source_x..command.source_x + command.source_w {
|
||||
let offset = ((y * image.width + x) * 4) as usize;
|
||||
assert_eq!(
|
||||
&image.bytes[offset..offset + 4],
|
||||
&colors[i],
|
||||
"scaled={scaled}, cell={size:?}, placement={i}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
let scaled_bytes = if scaled { 4 * size.0 * size.1 * 4 } else { 0 };
|
||||
assert_eq!(parts.0.borrow().total_bytes(), 20 * 40 * 4 + scaled_bytes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +169,7 @@ impl PaneGroups {
|
||||
height: Some(PercentOrFixed::Fixed(height)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
let _ = self.senders.send_to_pty(PtyInstruction::FillPluginCwd(
|
||||
should_float,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pub mod floating_pane_grid;
|
||||
use zellij_utils::{
|
||||
data::{Direction, FloatingPaneCoordinates, PaneInfo, ResizeStrategy},
|
||||
data::{BorderStyle, Direction, FloatingPaneCoordinates, PaneInfo, ResizeStrategy},
|
||||
position::Position,
|
||||
};
|
||||
|
||||
@@ -41,6 +41,7 @@ pub struct FloatingPanes {
|
||||
viewport: Rc<RefCell<Viewport>>,
|
||||
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
default_mode_info: ModeInfo,
|
||||
@@ -70,6 +71,7 @@ impl FloatingPanes {
|
||||
viewport: Rc<RefCell<Viewport>>,
|
||||
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
fullscreen_covers_ui: Rc<RefCell<bool>>,
|
||||
@@ -86,6 +88,7 @@ impl FloatingPanes {
|
||||
viewport,
|
||||
connected_clients,
|
||||
connected_clients_in_app,
|
||||
client_display_slots,
|
||||
mode_info,
|
||||
character_cell_size,
|
||||
session_is_mirrored,
|
||||
@@ -598,6 +601,7 @@ impl FloatingPanes {
|
||||
mouse_scroll_resize,
|
||||
mouse_hover_tips,
|
||||
self.dimmed_clients.clone(),
|
||||
&self.client_display_slots.borrow(),
|
||||
);
|
||||
for client_id in &connected_clients {
|
||||
let client_mode = self
|
||||
@@ -1710,6 +1714,17 @@ impl FloatingPanes {
|
||||
pane.update_rounded_corners(rounded_corners);
|
||||
}
|
||||
}
|
||||
pub fn update_border_styles(
|
||||
&mut self,
|
||||
border_style: BorderStyle,
|
||||
floating_border_style: BorderStyle,
|
||||
) {
|
||||
self.style.border_style = border_style;
|
||||
self.style.floating_border_style = floating_border_style;
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.invalidate_frame_cache();
|
||||
}
|
||||
}
|
||||
pub fn next_selectable_pane_id_above(&mut self, pane_id: &PaneId) -> Option<PaneId> {
|
||||
let display_area = *self.display_area.borrow();
|
||||
let viewport = *self.viewport.borrow();
|
||||
|
||||
@@ -1922,6 +1922,9 @@ impl Grid {
|
||||
let changed_rects = self
|
||||
.output_buffer
|
||||
.changed_rects_in_viewport(self.viewport.len());
|
||||
if let Some(image_ids_to_reap) = self.sixel_grid.drain_image_ids_to_reap() {
|
||||
self.sixel_grid.reap_images(image_ids_to_reap);
|
||||
}
|
||||
let changed_sixel_image_chunks = self.sixel_grid.changed_sixel_chunks_in_viewport(
|
||||
changed_rects,
|
||||
self.lines_above.len(),
|
||||
@@ -1929,9 +1932,6 @@ impl Grid {
|
||||
x_offset,
|
||||
y_offset,
|
||||
);
|
||||
if let Some(image_ids_to_reap) = self.sixel_grid.drain_image_ids_to_reap() {
|
||||
self.sixel_grid.reap_images(image_ids_to_reap);
|
||||
}
|
||||
if self.kitty_settle_placements_below_the_viewport() {
|
||||
self.kitty_reanchor_all_from_pixels();
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::parser::{DecodedImage, KittyCommand, KittyError, KittyErrorCode};
|
||||
use super::store::{InternalImageId, KittyImageStore};
|
||||
use super::store::{InternalImageId, KittyImageStore, ScaledImageKey};
|
||||
use crate::panes::sixel::PixelRect;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
@@ -18,8 +18,7 @@ pub struct KittyImageChunk {
|
||||
pub cell_offset_x: u32,
|
||||
pub cell_offset_y: u32,
|
||||
pub z_index: i32,
|
||||
pub dest_cells: (u16, u16),
|
||||
pub scaled_px: Option<(usize, usize)>,
|
||||
pub scaled_image: Option<ScaledImageKey>,
|
||||
pub placement_uid: u64,
|
||||
}
|
||||
|
||||
@@ -46,6 +45,15 @@ pub struct KittyPlacement {
|
||||
pub vertical_anchor: KittyVerticalAnchor,
|
||||
}
|
||||
|
||||
impl KittyPlacement {
|
||||
pub fn scaled_image(&self) -> Option<ScaledImageKey> {
|
||||
self.scaled_px.map(|size| ScaledImageKey {
|
||||
source: self.source_rect,
|
||||
size,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct KittyReplyData {
|
||||
pub image_id: Option<u32>,
|
||||
@@ -329,6 +337,12 @@ impl KittyGrid {
|
||||
(c, r) => (c * cell.width, r * cell.height, c, r, true),
|
||||
};
|
||||
let dest_cells = (cols as u16, rows as u16);
|
||||
let source_rect = PixelRect {
|
||||
x: source_x,
|
||||
y: source_y as isize,
|
||||
width: source_w,
|
||||
height: source_h,
|
||||
};
|
||||
if is_scaled {
|
||||
let scaled_bytes = {
|
||||
let store = self.kitty_image_store.borrow();
|
||||
@@ -348,7 +362,10 @@ impl KittyGrid {
|
||||
};
|
||||
self.kitty_image_store.borrow_mut().add_scaled_variant(
|
||||
internal,
|
||||
dest_cells,
|
||||
ScaledImageKey {
|
||||
source: source_rect,
|
||||
size: (dst_w, dst_h),
|
||||
},
|
||||
scaled_bytes,
|
||||
);
|
||||
}
|
||||
@@ -386,12 +403,7 @@ impl KittyGrid {
|
||||
placement_uid,
|
||||
internal_id: internal,
|
||||
display_rect,
|
||||
source_rect: PixelRect {
|
||||
x: source_x,
|
||||
y: source_y as isize,
|
||||
width: source_w,
|
||||
height: source_h,
|
||||
},
|
||||
source_rect,
|
||||
emit_x: 0,
|
||||
emit_y: 0,
|
||||
scaled_px: if is_scaled {
|
||||
@@ -830,7 +842,12 @@ impl KittyGrid {
|
||||
(self.previous_cell_size, *self.character_cell_size.borrow())
|
||||
{
|
||||
if previous_cell_size != character_cell_size {
|
||||
let mut regenerations: Vec<(InternalImageId, (u16, u16), PixelRect)> = Vec::new();
|
||||
for internal in self.image_ids.values() {
|
||||
self.kitty_image_store
|
||||
.borrow_mut()
|
||||
.clear_scaled_variants(*internal);
|
||||
}
|
||||
let mut regenerations: Vec<(InternalImageId, ScaledImageKey)> = Vec::new();
|
||||
for placement in self.placements.iter_mut() {
|
||||
placement.display_rect.x = (placement.display_rect.x
|
||||
/ previous_cell_size.width)
|
||||
@@ -846,14 +863,12 @@ impl KittyGrid {
|
||||
Some((placement.display_rect.width, placement.display_rect.height));
|
||||
placement.emit_x = 0;
|
||||
placement.emit_y = 0;
|
||||
regenerations.push((
|
||||
placement.internal_id,
|
||||
placement.dest_cells,
|
||||
placement.source_rect,
|
||||
));
|
||||
regenerations
|
||||
.push((placement.internal_id, placement.scaled_image().unwrap()));
|
||||
}
|
||||
}
|
||||
for (internal, dest_cells, source_rect) in regenerations {
|
||||
for (internal, key) in regenerations {
|
||||
let source_rect = key.source;
|
||||
let scaled_bytes = {
|
||||
let store = self.kitty_image_store.borrow();
|
||||
match store.get(internal) {
|
||||
@@ -871,8 +886,8 @@ impl KittyGrid {
|
||||
&cropped,
|
||||
source_rect.width,
|
||||
source_rect.height,
|
||||
dest_cells.0 as usize * character_cell_size.width,
|
||||
dest_cells.1 as usize * character_cell_size.height,
|
||||
key.size.0,
|
||||
key.size.1,
|
||||
))
|
||||
},
|
||||
None => None,
|
||||
@@ -881,7 +896,7 @@ impl KittyGrid {
|
||||
if let Some(scaled_bytes) = scaled_bytes {
|
||||
self.kitty_image_store.borrow_mut().add_scaled_variant(
|
||||
internal,
|
||||
dest_cells,
|
||||
key,
|
||||
scaled_bytes,
|
||||
);
|
||||
}
|
||||
@@ -1034,8 +1049,14 @@ impl KittyGrid {
|
||||
.saturating_sub(line_index + scrollback_size_in_lines);
|
||||
let cell_y =
|
||||
viewport_y_offset + line_index + image_cell_distance_from_changed_rect_top;
|
||||
let source_px_x = placement.emit_x;
|
||||
let source_px_y = placement.emit_y
|
||||
let (source_x, source_y) = if placement.scaled_px.is_some() {
|
||||
(0, 0)
|
||||
} else {
|
||||
(placement.source_rect.x, placement.source_rect.y as usize)
|
||||
};
|
||||
let source_px_x = source_x + placement.emit_x;
|
||||
let source_px_y = source_y
|
||||
+ placement.emit_y
|
||||
+ std::cmp::max(changed_rect_top_edge - image_top_edge, 0) as usize;
|
||||
let source_px_height = std::cmp::min(
|
||||
(std::cmp::min(changed_rect_bottom_edge, image_bottom_edge)
|
||||
@@ -1062,8 +1083,7 @@ impl KittyGrid {
|
||||
cell_offset_x: placement.cell_offset.0,
|
||||
cell_offset_y: placement.cell_offset.1,
|
||||
z_index: placement.z_index,
|
||||
dest_cells: placement.dest_cells,
|
||||
scaled_px: placement.scaled_px,
|
||||
scaled_image: placement.scaled_image(),
|
||||
placement_uid: placement.placement_uid,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2,18 +2,25 @@ use base64::engine::general_purpose::STANDARD as BASE64_ENCODER;
|
||||
use base64::engine::Engine as _;
|
||||
|
||||
use super::parser::{DecodedImage, KittyError, KittyErrorCode, KittyFormat};
|
||||
use crate::panes::sixel::PixelRect;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const DEFAULT_KITTY_STORE_QUOTA_BYTES: usize = 335_544_320;
|
||||
|
||||
pub type InternalImageId = u64;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
pub struct ScaledImageKey {
|
||||
pub source: PixelRect,
|
||||
pub size: (usize, usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct KittyImage {
|
||||
pub rgba: Vec<u8>,
|
||||
pub width: u32,
|
||||
pub height: u32,
|
||||
pub scaled_variants: HashMap<(u16, u16), Vec<u8>>,
|
||||
pub scaled_variants: HashMap<ScaledImageKey, Vec<u8>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -21,7 +28,7 @@ struct StoredImage {
|
||||
image: KittyImage,
|
||||
refcount: usize,
|
||||
lru_stamp: u64,
|
||||
base64_cache: HashMap<Option<(u16, u16)>, String>,
|
||||
base64_cache: HashMap<Option<ScaledImageKey>, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -93,10 +100,10 @@ impl KittyImageStore {
|
||||
pub fn get(&self, id: InternalImageId) -> Option<&KittyImage> {
|
||||
self.images.get(&id).map(|stored| &stored.image)
|
||||
}
|
||||
pub fn scaled_variant(&self, id: InternalImageId, cells: (u16, u16)) -> Option<&[u8]> {
|
||||
pub fn scaled_variant(&self, id: InternalImageId, key: ScaledImageKey) -> Option<&[u8]> {
|
||||
self.images
|
||||
.get(&id)
|
||||
.and_then(|stored| stored.image.scaled_variants.get(&cells))
|
||||
.and_then(|stored| stored.image.scaled_variants.get(&key))
|
||||
.map(|bytes| bytes.as_slice())
|
||||
}
|
||||
pub fn touch(&mut self, id: InternalImageId) {
|
||||
@@ -132,21 +139,21 @@ impl KittyImageStore {
|
||||
pub fn base64_for(
|
||||
&mut self,
|
||||
id: InternalImageId,
|
||||
variant: Option<(u16, u16)>,
|
||||
variant: Option<ScaledImageKey>,
|
||||
) -> Option<String> {
|
||||
let stored = self.images.get_mut(&id)?;
|
||||
if let Some(cached) = stored.base64_cache.get(&variant) {
|
||||
return Some(cached.clone());
|
||||
}
|
||||
let bytes = match variant {
|
||||
Some(cells) => stored.image.scaled_variants.get(&cells)?,
|
||||
Some(key) => stored.image.scaled_variants.get(&key)?,
|
||||
None => &stored.image.rgba,
|
||||
};
|
||||
let encoded = BASE64_ENCODER.encode(bytes);
|
||||
stored.base64_cache.insert(variant, encoded.clone());
|
||||
Some(encoded)
|
||||
}
|
||||
pub fn add_scaled_variant(&mut self, id: InternalImageId, cells: (u16, u16), bytes: Vec<u8>) {
|
||||
pub fn add_scaled_variant(&mut self, id: InternalImageId, key: ScaledImageKey, bytes: Vec<u8>) {
|
||||
let lru_stamp = self.bump_lru();
|
||||
let inserted = match self.images.get_mut(&id) {
|
||||
Some(stored) => {
|
||||
@@ -154,10 +161,10 @@ impl KittyImageStore {
|
||||
let old_len = stored
|
||||
.image
|
||||
.scaled_variants
|
||||
.insert(cells, bytes)
|
||||
.insert(key, bytes)
|
||||
.map(|old| old.len())
|
||||
.unwrap_or(0);
|
||||
stored.base64_cache.remove(&Some(cells));
|
||||
stored.base64_cache.remove(&Some(key));
|
||||
stored.lru_stamp = lru_stamp;
|
||||
Some((old_len, new_len))
|
||||
},
|
||||
@@ -170,6 +177,17 @@ impl KittyImageStore {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn clear_scaled_variants(&mut self, id: InternalImageId) {
|
||||
if let Some(stored) = self.images.get_mut(&id) {
|
||||
self.total_bytes -= stored
|
||||
.image
|
||||
.scaled_variants
|
||||
.drain()
|
||||
.map(|(_, bytes)| bytes.len())
|
||||
.sum::<usize>();
|
||||
stored.base64_cache.retain(|key, _| key.is_none());
|
||||
}
|
||||
}
|
||||
pub fn refcount(&self, id: InternalImageId) -> Option<usize> {
|
||||
self.images.get(&id).map(|stored| stored.refcount)
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ fn changed_chunks_for_placement_straddling_changed_rect() {
|
||||
assert_eq!(chunk.source_px_y, 0);
|
||||
assert_eq!(chunk.source_px_width, 20);
|
||||
assert_eq!(chunk.source_px_height, 10);
|
||||
assert_eq!(chunk.scaled_px, None);
|
||||
assert_eq!(chunk.scaled_image, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -188,8 +188,7 @@ fn changed_chunks_for_scaled_placement_use_variant_space() {
|
||||
assert_eq!(chunk.cell_y, 1);
|
||||
assert_eq!(chunk.source_px_y, 20);
|
||||
assert_eq!(chunk.source_px_height, 20);
|
||||
assert_eq!(chunk.scaled_px, Some((20, 40)));
|
||||
assert_eq!(chunk.dest_cells, (2, 2));
|
||||
assert_eq!(chunk.scaled_image.unwrap().size, (20, 40));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -98,11 +98,19 @@ fn byte_accounting_matches_hand_computed_total_with_scaled_variants() {
|
||||
let mut store = KittyImageStore::default();
|
||||
let id_a = store.store_image(rgba_image(10, 10)).unwrap();
|
||||
let id_b = store.store_image(rgba_image(4, 5)).unwrap();
|
||||
store.add_scaled_variant(id_a, (3, 2), vec![0; 120]);
|
||||
store.add_scaled_variant(id_a, (5, 4), vec![0; 60]);
|
||||
store.add_scaled_variant(id_a, (3, 2), vec![0; 100]);
|
||||
let first = super::ScaledImageKey {
|
||||
source: crate::panes::sixel::PixelRect::new(0, 0, 10, 10),
|
||||
size: (3, 2),
|
||||
};
|
||||
let second = super::ScaledImageKey {
|
||||
size: (5, 4),
|
||||
..first
|
||||
};
|
||||
store.add_scaled_variant(id_a, first, vec![0; 120]);
|
||||
store.add_scaled_variant(id_a, second, vec![0; 60]);
|
||||
store.add_scaled_variant(id_a, first, vec![0; 100]);
|
||||
assert_eq!(store.total_bytes(), 640);
|
||||
assert_eq!(store.scaled_variant(id_a, (3, 2)).unwrap().len(), 100);
|
||||
assert_eq!(store.scaled_variant(id_a, first).unwrap().len(), 100);
|
||||
store.free(id_a);
|
||||
assert_eq!(store.total_bytes(), 80);
|
||||
store.free(id_b);
|
||||
|
||||
@@ -22,7 +22,8 @@ use std::rc::Rc;
|
||||
use vte;
|
||||
use zellij_utils::data::PaneContents;
|
||||
use zellij_utils::data::{
|
||||
BareKey, KeyWithModifier, PermissionStatus, PermissionType, PluginPermission,
|
||||
BareKey, BorderStyleOverride, KeyWithModifier, PermissionStatus, PermissionType,
|
||||
PluginPermission,
|
||||
};
|
||||
use zellij_utils::pane_size::{Offset, SizeInPixels};
|
||||
use zellij_utils::position::Position;
|
||||
@@ -99,6 +100,7 @@ pub(crate) struct PluginPane {
|
||||
prev_pane_name: String,
|
||||
frame: HashMap<ClientId, PaneFrame>,
|
||||
borderless: bool,
|
||||
border_style_override: BorderStyleOverride,
|
||||
exclude_from_sync: bool,
|
||||
pane_frame_color_override: Option<(PaletteColor, Option<String>)>,
|
||||
invoked_with: Option<Run>,
|
||||
@@ -146,6 +148,7 @@ impl PluginPane {
|
||||
content_offset: Offset::default(),
|
||||
pane_title: title,
|
||||
borderless: false,
|
||||
border_style_override: BorderStyleOverride::default(),
|
||||
pane_name: pane_name.clone(),
|
||||
prev_pane_name: pane_name,
|
||||
terminal_emulator_colors,
|
||||
@@ -747,6 +750,14 @@ impl Pane for PluginPane {
|
||||
fn borderless(&self) -> bool {
|
||||
self.borderless
|
||||
}
|
||||
fn set_border_style_override(&mut self, border_style_override: BorderStyleOverride) {
|
||||
self.border_style_override = border_style_override;
|
||||
self.frame.clear();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
fn border_style_override(&self) -> BorderStyleOverride {
|
||||
self.border_style_override
|
||||
}
|
||||
fn set_exclude_from_sync(&mut self, exclude_from_sync: bool) {
|
||||
self.exclude_from_sync = exclude_from_sync;
|
||||
}
|
||||
@@ -847,6 +858,10 @@ impl Pane for PluginPane {
|
||||
self.style.rounded_corners = rounded_corners;
|
||||
self.frame.clear();
|
||||
}
|
||||
fn invalidate_frame_cache(&mut self) {
|
||||
self.frame.clear();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
fn set_should_be_suppressed(&mut self, should_be_suppressed: bool) {
|
||||
self.should_be_suppressed = should_be_suppressed;
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::output::SixelImageChunk;
|
||||
use std::cell::RefCell;
|
||||
use std::collections::HashMap;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
|
||||
use sixel_image::{SixelDeserializer, SixelImage};
|
||||
@@ -66,6 +66,7 @@ pub struct SixelGrid {
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
currently_parsing: Option<SixelDeserializer>,
|
||||
image_ids_to_reap: Vec<usize>,
|
||||
images_to_check_for_emptiness: HashSet<usize>,
|
||||
sixel_parser: Option<sixel_tokenizer::Parser>,
|
||||
pub sixel_image_store: Rc<RefCell<SixelImageStore>>,
|
||||
}
|
||||
@@ -167,8 +168,8 @@ impl SixelGrid {
|
||||
if let Some(intersecting_rect) =
|
||||
pixel_rect.intersecting_rect(&image_size_and_coordinates)
|
||||
{
|
||||
if intersecting_rect.x == pixel_rect.x
|
||||
&& intersecting_rect.y == pixel_rect.y
|
||||
if intersecting_rect.x == 0
|
||||
&& intersecting_rect.y == 0
|
||||
&& intersecting_rect.height == pixel_rect.height
|
||||
&& intersecting_rect.width == pixel_rect.width
|
||||
{
|
||||
@@ -225,6 +226,21 @@ impl SixelGrid {
|
||||
}
|
||||
}
|
||||
pub fn drain_image_ids_to_reap(&mut self) -> Option<Vec<usize>> {
|
||||
if !self.images_to_check_for_emptiness.is_empty() {
|
||||
let store = self.sixel_image_store.borrow();
|
||||
for image_id in self.images_to_check_for_emptiness.drain() {
|
||||
let has_lit_pixels = store
|
||||
.sixel_images
|
||||
.get(&image_id)
|
||||
.map_or(false, |(sixel_image, _)| {
|
||||
sixel_image.pixels.iter().flatten().any(|pixel| pixel.on)
|
||||
});
|
||||
if !has_lit_pixels {
|
||||
self.sixel_image_locations.remove(&image_id);
|
||||
self.image_ids_to_reap.push(image_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let images_to_reap = self.image_ids_to_reap.drain(..);
|
||||
if images_to_reap.len() > 0 {
|
||||
Some(images_to_reap.collect())
|
||||
@@ -255,6 +271,7 @@ impl SixelGrid {
|
||||
.map(|(image_id, _image_rect)| image_id)
|
||||
.collect();
|
||||
image_ids.append(&mut self.image_ids_to_reap);
|
||||
self.images_to_check_for_emptiness.clear();
|
||||
if !image_ids.is_empty() {
|
||||
Some(image_ids)
|
||||
} else {
|
||||
@@ -262,13 +279,17 @@ impl SixelGrid {
|
||||
}
|
||||
}
|
||||
pub fn next_image_id(&self) -> usize {
|
||||
self.sixel_image_store.borrow().sixel_images.keys().len()
|
||||
let mut store = self.sixel_image_store.borrow_mut();
|
||||
let id = store.next_image_id;
|
||||
store.next_image_id += 1;
|
||||
id
|
||||
}
|
||||
pub fn new_sixel_image(&mut self, sixel_image_id: usize, sixel_image: SixelImage) {
|
||||
self.sixel_image_store
|
||||
.borrow_mut()
|
||||
.sixel_images
|
||||
.insert(sixel_image_id, (sixel_image, HashMap::new()));
|
||||
self.images_to_check_for_emptiness.insert(sixel_image_id);
|
||||
}
|
||||
pub fn remove_pixels_from_image(&mut self, image_id: usize, pixel_rect: PixelRect) {
|
||||
if let Some((sixel_image, sixel_image_cache)) = self
|
||||
@@ -285,6 +306,7 @@ impl SixelGrid {
|
||||
);
|
||||
sixel_image_cache.clear(); // TODO: more intelligent cache clearing
|
||||
}
|
||||
self.images_to_check_for_emptiness.insert(image_id);
|
||||
}
|
||||
pub fn reap_images(&mut self, ids_to_reap: Vec<usize>) {
|
||||
for id in ids_to_reap {
|
||||
@@ -428,6 +450,7 @@ type SixelImageCache = HashMap<PixelRect, String>;
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SixelImageStore {
|
||||
sixel_images: HashMap<usize, (SixelImage, SixelImageCache)>,
|
||||
next_image_id: usize,
|
||||
}
|
||||
|
||||
impl SixelImageStore {
|
||||
|
||||
@@ -25,8 +25,8 @@ use zellij_utils::input::mouse::{MouseEvent, MouseEventType};
|
||||
use zellij_utils::pane_size::Offset;
|
||||
use zellij_utils::{
|
||||
data::{
|
||||
BareKey, InputMode, KeyWithModifier, Palette, PaletteColor, PaneId as ZellijUtilsPaneId,
|
||||
RegexHighlight, Style, Styling,
|
||||
BareKey, BorderStyleOverride, InputMode, KeyWithModifier, Palette, PaletteColor,
|
||||
PaneId as ZellijUtilsPaneId, RegexHighlight, Style, Styling,
|
||||
},
|
||||
errors::prelude::*,
|
||||
input::layout::Run,
|
||||
@@ -145,6 +145,7 @@ pub struct TerminalPane {
|
||||
prev_pane_name: String,
|
||||
frame: HashMap<ClientId, PaneFrame>,
|
||||
borderless: bool,
|
||||
border_style_override: BorderStyleOverride,
|
||||
exclude_from_sync: bool,
|
||||
fake_cursor_locations: HashSet<(usize, usize)>, // (x, y) - these hold a record of previous fake cursors which we need to clear on render
|
||||
search_term: String,
|
||||
@@ -990,6 +991,14 @@ impl Pane for TerminalPane {
|
||||
self.set_content_offset(Offset::frame(1));
|
||||
}
|
||||
}
|
||||
fn set_border_style_override(&mut self, border_style_override: BorderStyleOverride) {
|
||||
self.border_style_override = border_style_override;
|
||||
self.frame.clear();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
fn border_style_override(&self) -> BorderStyleOverride {
|
||||
self.border_style_override
|
||||
}
|
||||
|
||||
fn set_exclude_from_sync(&mut self, exclude_from_sync: bool) {
|
||||
self.exclude_from_sync = exclude_from_sync;
|
||||
@@ -1232,6 +1241,10 @@ impl Pane for TerminalPane {
|
||||
self.style.rounded_corners = rounded_corners;
|
||||
self.frame.clear();
|
||||
}
|
||||
fn invalidate_frame_cache(&mut self) {
|
||||
self.frame.clear();
|
||||
self.set_should_render(true);
|
||||
}
|
||||
fn drain_fake_cursors(&mut self) -> Option<HashSet<(usize, usize)>> {
|
||||
if !self.fake_cursor_locations.is_empty() {
|
||||
for (y, _x) in &self.fake_cursor_locations {
|
||||
@@ -1418,6 +1431,7 @@ impl TerminalPane {
|
||||
pane_name: pane_name.clone(),
|
||||
prev_pane_name: pane_name,
|
||||
borderless: false,
|
||||
border_style_override: BorderStyleOverride::default(),
|
||||
exclude_from_sync: false,
|
||||
fake_cursor_locations: HashSet::new(),
|
||||
search_term: String::new(),
|
||||
|
||||
@@ -28,7 +28,10 @@ use crate::{
|
||||
};
|
||||
use stacked_panes::StackedPanes;
|
||||
use zellij_utils::{
|
||||
data::{Direction, ModeInfo, PaneInfo, Resize, ResizeStrategy, Style, Styling},
|
||||
data::{
|
||||
BorderStyle, BorderStyleOverride, Direction, LineStyle, ModeInfo, PaneInfo, Resize,
|
||||
ResizeStrategy, Style, Styling,
|
||||
},
|
||||
errors::prelude::*,
|
||||
input::{
|
||||
command::RunCommand,
|
||||
@@ -70,6 +73,7 @@ pub struct TiledPanes {
|
||||
viewport: Rc<RefCell<Viewport>>,
|
||||
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
stacked_resize: Rc<RefCell<bool>>,
|
||||
@@ -97,6 +101,7 @@ impl TiledPanes {
|
||||
viewport: Rc<RefCell<Viewport>>,
|
||||
connected_clients: Rc<RefCell<HashSet<ClientId>>>,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
mode_info: Rc<RefCell<HashMap<ClientId, ModeInfo>>>,
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
stacked_resize: Rc<RefCell<bool>>,
|
||||
@@ -115,6 +120,7 @@ impl TiledPanes {
|
||||
viewport,
|
||||
connected_clients,
|
||||
connected_clients_in_app,
|
||||
client_display_slots,
|
||||
mode_info,
|
||||
character_cell_size,
|
||||
stacked_resize,
|
||||
@@ -1161,6 +1167,11 @@ impl TiledPanes {
|
||||
let connected_clients: Vec<ClientId> = connected_clients.into_iter().collect();
|
||||
let multiple_users_exist_in_session = { self.connected_clients_in_app.borrow().len() > 1 };
|
||||
let mut client_id_to_boundaries: HashMap<ClientId, Boundaries> = HashMap::new();
|
||||
let fallback_line_style = self
|
||||
.style
|
||||
.border_style
|
||||
.uniform_style()
|
||||
.unwrap_or(LineStyle::Single);
|
||||
let active_panes = if floating_panes_are_visible {
|
||||
HashMap::new()
|
||||
} else {
|
||||
@@ -1274,6 +1285,7 @@ impl TiledPanes {
|
||||
mouse_scroll_resize,
|
||||
mouse_hover_tips,
|
||||
self.dimmed_clients.clone(),
|
||||
&self.client_display_slots.borrow(),
|
||||
);
|
||||
pane_contents_and_ui.set_frame_geom_override(visible_member_frame_override);
|
||||
pane_contents_and_ui.set_blank_title(reserved_rows_for_pane > 0);
|
||||
@@ -1317,9 +1329,12 @@ impl TiledPanes {
|
||||
pane_is_selectable,
|
||||
)
|
||||
.with_context(err_context)?;
|
||||
let boundaries = client_id_to_boundaries
|
||||
.entry(*client_id)
|
||||
.or_insert_with(|| Boundaries::new(*self.viewport.borrow()));
|
||||
let boundaries =
|
||||
client_id_to_boundaries
|
||||
.entry(*client_id)
|
||||
.or_insert_with(|| {
|
||||
Boundaries::new(*self.viewport.borrow(), fallback_line_style)
|
||||
});
|
||||
pane_contents_and_ui.render_pane_boundaries(
|
||||
*client_id,
|
||||
client_mode,
|
||||
@@ -1329,9 +1344,12 @@ impl TiledPanes {
|
||||
pane_is_on_bottom_of_stack,
|
||||
);
|
||||
} else {
|
||||
let boundaries = client_id_to_boundaries
|
||||
.entry(*client_id)
|
||||
.or_insert_with(|| Boundaries::new(*self.viewport.borrow()));
|
||||
let boundaries =
|
||||
client_id_to_boundaries
|
||||
.entry(*client_id)
|
||||
.or_insert_with(|| {
|
||||
Boundaries::new(*self.viewport.borrow(), fallback_line_style)
|
||||
});
|
||||
pane_contents_and_ui.render_pane_boundaries(
|
||||
*client_id,
|
||||
client_mode,
|
||||
@@ -1415,6 +1433,7 @@ impl TiledPanes {
|
||||
run: Option<Run>,
|
||||
geom: PaneGeom,
|
||||
should_be_borderless: Option<bool>,
|
||||
border_style: Option<BorderStyleOverride>,
|
||||
) {
|
||||
match self
|
||||
.panes
|
||||
@@ -1423,6 +1442,9 @@ impl TiledPanes {
|
||||
{
|
||||
Some((_, pane)) => {
|
||||
pane.set_geom(geom);
|
||||
if let Some(border_style) = border_style {
|
||||
pane.set_border_style_override(border_style);
|
||||
}
|
||||
|
||||
if let Some(should_be_borderless) = should_be_borderless {
|
||||
pane.set_borderless(should_be_borderless);
|
||||
@@ -2738,13 +2760,17 @@ impl TiledPanes {
|
||||
self.panes.remove(&pane_id)
|
||||
}
|
||||
pub fn remove_pane(&mut self, pane_id: PaneId) -> Option<Box<dyn Pane>> {
|
||||
let closed_logical_position = self
|
||||
.panes
|
||||
.get(&pane_id)
|
||||
.and_then(|pane| pane.position_and_size().logical_position);
|
||||
let mut pane_grid = TiledPaneGrid::new(
|
||||
&mut self.panes,
|
||||
&self.panes_to_hide,
|
||||
*self.display_area.borrow(),
|
||||
*self.viewport.borrow(),
|
||||
);
|
||||
if pane_grid.fill_space_over_pane(pane_id) {
|
||||
let closed_pane = if pane_grid.fill_space_over_pane(pane_id) {
|
||||
// successfully filled space over pane
|
||||
let closed_pane = self.panes.remove(&pane_id);
|
||||
self.move_clients_out_of_pane(pane_id);
|
||||
@@ -2762,7 +2788,20 @@ impl TiledPanes {
|
||||
self.active_panes.clear(&mut self.panes);
|
||||
}
|
||||
closed_pane
|
||||
};
|
||||
if let Some(closed_logical_position) = closed_logical_position {
|
||||
for pane in self.panes.values_mut() {
|
||||
let mut geom = pane.position_and_size();
|
||||
if geom
|
||||
.logical_position
|
||||
.is_some_and(|position| position > closed_logical_position)
|
||||
{
|
||||
geom.logical_position = geom.logical_position.map(|position| position - 1);
|
||||
pane.set_geom(geom);
|
||||
}
|
||||
}
|
||||
}
|
||||
closed_pane
|
||||
}
|
||||
pub fn hold_pane(
|
||||
&mut self,
|
||||
@@ -3113,6 +3152,17 @@ impl TiledPanes {
|
||||
pane.update_rounded_corners(rounded_corners);
|
||||
}
|
||||
}
|
||||
pub fn update_border_styles(
|
||||
&mut self,
|
||||
border_style: BorderStyle,
|
||||
floating_border_style: BorderStyle,
|
||||
) {
|
||||
self.style.border_style = border_style;
|
||||
self.style.floating_border_style = floating_border_style;
|
||||
for pane in self.panes.values_mut() {
|
||||
pane.invalidate_frame_cache();
|
||||
}
|
||||
}
|
||||
pub fn stack_panes(
|
||||
&mut self,
|
||||
root_pane_id: PaneId,
|
||||
|
||||
@@ -61,6 +61,8 @@ impl<'a> StackedPanes<'a> {
|
||||
let destination_pane = panes
|
||||
.get_mut(&destination_pane_id)
|
||||
.with_context(err_context)?;
|
||||
destination_pane_geom.logical_position =
|
||||
destination_pane.position_and_size().logical_position;
|
||||
destination_pane.set_geom(destination_pane_geom);
|
||||
} else if destination_pane_stack_id.is_some() {
|
||||
// we're moving down to the highest pane in the stack, we need to expand it and shrink the
|
||||
@@ -97,6 +99,8 @@ impl<'a> StackedPanes<'a> {
|
||||
let destination_pane = panes
|
||||
.get_mut(&destination_pane_id)
|
||||
.with_context(err_context)?;
|
||||
destination_pane_geom.logical_position =
|
||||
destination_pane.position_and_size().logical_position;
|
||||
destination_pane.set_geom(destination_pane_geom);
|
||||
} else if destination_pane_stack_id.is_some() {
|
||||
// we're moving up to the lowest pane in the stack, we need to expand it and shrink the
|
||||
@@ -1053,6 +1057,8 @@ impl<'a> StackedPanes<'a> {
|
||||
pane_to_close_position_and_size.stacked = None;
|
||||
}
|
||||
let pane_below = panes.get_mut(&pane_id_below).with_context(err_context)?;
|
||||
pane_to_close_position_and_size.logical_position =
|
||||
pane_below.position_and_size().logical_position;
|
||||
pane_below.set_geom(pane_to_close_position_and_size);
|
||||
return Ok(true);
|
||||
} else if position_of_current_pane > 0 {
|
||||
@@ -1070,6 +1076,8 @@ impl<'a> StackedPanes<'a> {
|
||||
pane_to_close_position_and_size.stacked = None;
|
||||
}
|
||||
let pane_above = panes.get_mut(&pane_id_above).with_context(err_context)?;
|
||||
pane_to_close_position_and_size.logical_position =
|
||||
pane_above.position_and_size().logical_position;
|
||||
pane_above.set_geom(pane_to_close_position_and_size);
|
||||
return Ok(true);
|
||||
} else {
|
||||
|
||||
@@ -3303,6 +3303,51 @@ pub fn sixel_images_are_reaped_when_scrolled_off() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn sixel_replacement_reaps_covered_images_without_reusing_live_ids() {
|
||||
let (mut grid, _) = new_kitty_grid(10, 25);
|
||||
let mut parser = vte::Parser::new();
|
||||
for step in 0..18u8 {
|
||||
let width = if step < 12 { 20 } else { 10 + (step % 3) * 10 };
|
||||
if step >= 12 {
|
||||
parser.advance(&mut grid, b"\x1b[3;4H ");
|
||||
}
|
||||
let bytes = format!(
|
||||
"\u{1b}[3;4H\u{1b}P9;1q\"1;1;{};12#1;2;{};0;0#1!{}~-!{}~\u{1b}\\",
|
||||
width,
|
||||
step * 5,
|
||||
width,
|
||||
width
|
||||
);
|
||||
parser.advance(&mut grid, bytes.as_bytes());
|
||||
grid.read_changes(0, 0);
|
||||
let coordinates: Vec<_> = grid.sixel_grid.image_coordinates().collect();
|
||||
assert_eq!(coordinates.len(), 1, "obsolete image at step {}", step);
|
||||
let (id, rect) = coordinates[0];
|
||||
let mut store = grid.sixel_grid.sixel_image_store.borrow_mut();
|
||||
assert_eq!(store.image_count(), 1, "storage after step {}", step);
|
||||
let serialized = store
|
||||
.serialize_image(id, 0, 0, rect.width, rect.height)
|
||||
.expect("replacement image survives reaping");
|
||||
let decoded = sixel_image::SixelImage::new(serialized.as_bytes()).unwrap();
|
||||
assert!(decoded.pixels.iter().flatten().all(|pixel| {
|
||||
pixel.on
|
||||
&& decoded.color_registers[&pixel.color]
|
||||
== sixel_image::SixelColor::Rgb(step * 5, 0, 0)
|
||||
}));
|
||||
}
|
||||
parser.advance(&mut grid, b"\x1b[3;4H ");
|
||||
let (_, sixel_chunks, _) = grid.read_changes(0, 0);
|
||||
assert!(
|
||||
sixel_chunks.is_empty(),
|
||||
"erased rasters must not be replayed"
|
||||
);
|
||||
assert_eq!(grid.sixel_grid.sixel_image_store.borrow().image_count(), 0);
|
||||
parser.advance(&mut grid, b"\x1b[3;4H\x1bP0;0q\"1;1;20;12?\x1b\\");
|
||||
let (_, sixel_chunks, _) = grid.read_changes(0, 0);
|
||||
assert_eq!(sixel_chunks.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn sixel_images_are_reaped_when_resetting() {
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
@@ -7250,7 +7295,7 @@ fn kitty_c_r_scaling_produces_exact_cell_rect_and_variant() {
|
||||
assert_eq!(
|
||||
kitty_image_store
|
||||
.borrow()
|
||||
.scaled_variant(internal_id, (3, 2))
|
||||
.scaled_variant(internal_id, placement.scaled_image().unwrap())
|
||||
.unwrap()
|
||||
.len(),
|
||||
30 * 40 * 4
|
||||
|
||||
@@ -1080,7 +1080,7 @@ fn create_guest_modal_pane() -> TerminalPane {
|
||||
fn press_key(
|
||||
pane: &mut TerminalPane,
|
||||
bare_key: zellij_utils::data::BareKey,
|
||||
client_id: u16,
|
||||
client_id: crate::ClientId,
|
||||
) -> Option<crate::tab::AdjustedInput> {
|
||||
let key = zellij_utils::data::KeyWithModifier::new(bare_key);
|
||||
pane.adjust_input_to_terminal(&Some(key), vec![], false, Some(client_id))
|
||||
|
||||
@@ -88,6 +88,7 @@ pub enum PluginInstruction {
|
||||
Resize(PluginId, usize, usize), // plugin_id, columns, rows
|
||||
AddClient(ClientId),
|
||||
RemoveClient(ClientId),
|
||||
UpdatePluginTabIndices(Vec<(PluginId, usize)>),
|
||||
NewTab(
|
||||
Option<PathBuf>,
|
||||
Option<TerminalAction>,
|
||||
@@ -236,6 +237,7 @@ impl From<&PluginInstruction> for PluginContext {
|
||||
PluginInstruction::Exit => PluginContext::Exit,
|
||||
PluginInstruction::AddClient(_) => PluginContext::AddClient,
|
||||
PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient,
|
||||
PluginInstruction::UpdatePluginTabIndices(..) => PluginContext::UpdatePluginTabIndices,
|
||||
PluginInstruction::NewTab(..) => PluginContext::NewTab,
|
||||
PluginInstruction::OverrideLayout(..) => PluginContext::OverrideLayout,
|
||||
PluginInstruction::ApplyCachedEvents { .. } => PluginContext::ApplyCachedEvents,
|
||||
@@ -510,6 +512,9 @@ pub(crate) fn plugin_thread_main(
|
||||
PluginInstruction::RemoveClient(client_id) => {
|
||||
wasm_bridge.remove_client(client_id);
|
||||
},
|
||||
PluginInstruction::UpdatePluginTabIndices(tab_indices) => {
|
||||
wasm_bridge.update_plugin_tab_indices(tab_indices);
|
||||
},
|
||||
PluginInstruction::NewTab(
|
||||
cwd,
|
||||
terminal_action,
|
||||
|
||||
@@ -82,6 +82,24 @@ impl PendingPipes {
|
||||
}
|
||||
pipe_names_to_unblock
|
||||
}
|
||||
pub fn unload_plugin_client(
|
||||
&mut self,
|
||||
plugin_id: &PluginId,
|
||||
client_id: &ClientId,
|
||||
) -> Vec<String> {
|
||||
let mut pipe_names_to_unblock = vec![];
|
||||
for (pipe_name, pending_pipe_info) in self.pipes.iter_mut() {
|
||||
let should_unblock_this_pipe =
|
||||
pending_pipe_info.unload_plugin_client(plugin_id, client_id);
|
||||
if should_unblock_this_pipe {
|
||||
pipe_names_to_unblock.push(pipe_name.to_owned());
|
||||
}
|
||||
}
|
||||
for pipe_name in &pipe_names_to_unblock {
|
||||
self.pipes.remove(pipe_name);
|
||||
}
|
||||
pipe_names_to_unblock
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
@@ -135,6 +153,17 @@ impl PendingPipeInfo {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn unload_plugin_client(
|
||||
&mut self,
|
||||
plugin_id_to_unload: &PluginId,
|
||||
client_id_to_unload: &ClientId,
|
||||
) -> bool {
|
||||
self.currently_being_processed_by
|
||||
.retain(|(plugin_id, client_id)| {
|
||||
plugin_id != plugin_id_to_unload || client_id != client_id_to_unload
|
||||
});
|
||||
self.currently_being_processed_by.is_empty() && !self.is_explicitly_blocked
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pipe_message_to_plugin(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::plugins::plugin_map::{
|
||||
PluginEnv, PluginMap, RunningPlugin, VecDequeInputStream, WriteOutputStream,
|
||||
PluginEnv, PluginMap, PluginMetadata, RunningPlugin, VecDequeInputStream, WriteOutputStream,
|
||||
};
|
||||
use crate::plugins::plugin_worker::{plugin_worker, RunningWorker};
|
||||
use crate::plugins::wasm_bridge::{LoadingContext, PluginCache};
|
||||
@@ -132,6 +132,7 @@ impl<'a> PluginLoader<'a> {
|
||||
self
|
||||
}
|
||||
pub fn start_plugin(&mut self) -> Result<()> {
|
||||
self.record_plugin_metadata();
|
||||
let module = if self.skip_cache {
|
||||
self.interpret_module()?
|
||||
} else {
|
||||
@@ -143,6 +144,19 @@ impl<'a> PluginLoader<'a> {
|
||||
self.clone_instance_for_other_clients()?;
|
||||
Ok(())
|
||||
}
|
||||
fn record_plugin_metadata(&mut self) {
|
||||
self.plugin_map.insert_metadata(
|
||||
self.plugin_id,
|
||||
PluginMetadata {
|
||||
plugin_config: self.plugin_config.clone(),
|
||||
tab_index: self.tab_index,
|
||||
rows: self.size.rows,
|
||||
columns: self.size.cols,
|
||||
cwd: self.plugin_cwd.clone(),
|
||||
is_background: self.tab_index.is_none(),
|
||||
},
|
||||
);
|
||||
}
|
||||
fn interpret_module(&mut self) -> Result<Module> {
|
||||
self.loading_indication.override_previous_error();
|
||||
let wasm_bytes = self.plugin_config.resolve_wasm_bytes(&self.plugin_dir)?;
|
||||
@@ -266,7 +280,6 @@ impl<'a> PluginLoader<'a> {
|
||||
wasi_ctx,
|
||||
plugin_own_data_dir: self.plugin_own_data_dir.clone(),
|
||||
plugin_own_cache_dir: self.plugin_own_cache_dir.clone(),
|
||||
tab_index: self.tab_index,
|
||||
path_to_default_shell: self.path_to_default_shell.clone(),
|
||||
default_shell: self.default_shell.clone(),
|
||||
plugin_cwd: self.plugin_cwd.clone(),
|
||||
@@ -378,7 +391,6 @@ impl<'a> PluginLoader<'a> {
|
||||
wasi_ctx,
|
||||
plugin_own_data_dir: self.plugin_own_data_dir.clone(),
|
||||
plugin_own_cache_dir: self.plugin_own_cache_dir.clone(),
|
||||
tab_index: self.tab_index,
|
||||
path_to_default_shell: self.path_to_default_shell.clone(),
|
||||
default_shell: self.default_shell.clone(),
|
||||
plugin_cwd: self.plugin_cwd.clone(),
|
||||
|
||||
@@ -27,33 +27,35 @@ use zellij_utils::{data::PermissionType, errors::prelude::*};
|
||||
// so when adding/removing from the map - everything is halted, that's life
|
||||
// but when cloning the internal RunningPlugin and Subscriptions atomics, we can call methods on
|
||||
// them without blocking other instances
|
||||
pub type PluginKey = (PluginId, ClientId);
|
||||
pub type PluginAssets = (
|
||||
Arc<Mutex<RunningPlugin>>,
|
||||
Arc<Mutex<Subscriptions>>,
|
||||
HashMap<String, UnboundedSender<MessageToWorker>>,
|
||||
);
|
||||
pub type RemovedPluginAssets = HashMap<PluginKey, PluginAssets>;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct PluginMetadata {
|
||||
pub plugin_config: PluginConfig,
|
||||
pub tab_index: Option<usize>,
|
||||
pub rows: usize,
|
||||
pub columns: usize,
|
||||
pub cwd: PathBuf,
|
||||
pub is_background: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PluginMap {
|
||||
plugin_assets: HashMap<
|
||||
(PluginId, ClientId),
|
||||
(
|
||||
Arc<Mutex<RunningPlugin>>,
|
||||
Arc<Mutex<Subscriptions>>,
|
||||
HashMap<String, UnboundedSender<MessageToWorker>>,
|
||||
),
|
||||
>,
|
||||
plugin_assets: HashMap<PluginKey, PluginAssets>,
|
||||
plugin_metadata: HashMap<PluginId, PluginMetadata>,
|
||||
}
|
||||
|
||||
impl PluginMap {
|
||||
pub fn remove_plugins(
|
||||
&mut self,
|
||||
pid: PluginId,
|
||||
) -> HashMap<
|
||||
(PluginId, ClientId),
|
||||
(
|
||||
Arc<Mutex<RunningPlugin>>,
|
||||
Arc<Mutex<Subscriptions>>,
|
||||
HashMap<String, UnboundedSender<MessageToWorker>>,
|
||||
),
|
||||
> {
|
||||
pub fn remove_plugins(&mut self, pid: PluginId) -> RemovedPluginAssets {
|
||||
self.plugin_metadata.remove(&pid);
|
||||
let mut removed = HashMap::new();
|
||||
let ids_in_plugin_map: Vec<(PluginId, ClientId)> =
|
||||
self.plugin_assets.keys().copied().collect();
|
||||
let ids_in_plugin_map: Vec<PluginKey> = self.plugin_assets.keys().copied().collect();
|
||||
for (plugin_id, client_id) in ids_in_plugin_map {
|
||||
if pid == plugin_id {
|
||||
if let Some(plugin_asset) = self.plugin_assets.remove(&(plugin_id, client_id)) {
|
||||
@@ -63,13 +65,44 @@ impl PluginMap {
|
||||
}
|
||||
removed
|
||||
}
|
||||
pub fn remove_plugin_clients(&mut self, client_id: ClientId) -> RemovedPluginAssets {
|
||||
let mut removed = HashMap::new();
|
||||
let ids_in_plugin_map: Vec<PluginKey> = self.plugin_assets.keys().copied().collect();
|
||||
for key in ids_in_plugin_map {
|
||||
if key.1 == client_id {
|
||||
if let Some(plugin_asset) = self.plugin_assets.remove(&key) {
|
||||
removed.insert(key, plugin_asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
pub fn insert_metadata(&mut self, plugin_id: PluginId, metadata: PluginMetadata) {
|
||||
self.plugin_metadata.insert(plugin_id, metadata);
|
||||
}
|
||||
pub fn metadata(&self, plugin_id: PluginId) -> Option<&PluginMetadata> {
|
||||
self.plugin_metadata.get(&plugin_id)
|
||||
}
|
||||
pub fn set_tab_index(&mut self, plugin_id: PluginId, tab_index: usize) {
|
||||
if let Some(metadata) = self.plugin_metadata.get_mut(&plugin_id) {
|
||||
if !metadata.is_background {
|
||||
metadata.tab_index = Some(tab_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn set_size(&mut self, plugin_id: PluginId, rows: usize, columns: usize) {
|
||||
if let Some(metadata) = self.plugin_metadata.get_mut(&plugin_id) {
|
||||
metadata.rows = rows;
|
||||
metadata.columns = columns;
|
||||
}
|
||||
}
|
||||
pub fn set_cwd(&mut self, plugin_id: PluginId, cwd: PathBuf) {
|
||||
if let Some(metadata) = self.plugin_metadata.get_mut(&plugin_id) {
|
||||
metadata.cwd = cwd;
|
||||
}
|
||||
}
|
||||
pub fn plugin_ids(&self) -> Vec<PluginId> {
|
||||
let mut unique_plugins: HashSet<PluginId> = self
|
||||
.plugin_assets
|
||||
.keys()
|
||||
.map(|(plugin_id, _client_id)| *plugin_id)
|
||||
.collect();
|
||||
unique_plugins.drain().into_iter().collect()
|
||||
self.plugin_metadata.keys().copied().collect()
|
||||
}
|
||||
pub fn running_plugins(&mut self) -> Vec<(PluginId, ClientId, Arc<Mutex<RunningPlugin>>)> {
|
||||
self.plugin_assets
|
||||
@@ -154,17 +187,14 @@ impl PluginMap {
|
||||
) -> Result<Vec<PluginId>> {
|
||||
let err_context = || format!("Failed to get plugin ids for location {plugin_location}");
|
||||
let plugin_ids: Vec<PluginId> = self
|
||||
.plugin_assets
|
||||
.plugin_metadata
|
||||
.iter()
|
||||
.filter(|(_, (running_plugin, _subscriptions, _workers))| {
|
||||
let running_plugin = running_plugin.lock().unwrap();
|
||||
let plugin_config = &running_plugin.store.data().plugin;
|
||||
let running_plugin_location = &plugin_config.location;
|
||||
let running_plugin_configuration = &plugin_config.initial_userspace_configuration;
|
||||
running_plugin_location == plugin_location
|
||||
&& running_plugin_configuration == plugin_configuration
|
||||
.filter(|(_plugin_id, metadata)| {
|
||||
&metadata.plugin_config.location == plugin_location
|
||||
&& &metadata.plugin_config.initial_userspace_configuration
|
||||
== plugin_configuration
|
||||
})
|
||||
.map(|((plugin_id, _client_id), _)| *plugin_id)
|
||||
.map(|(plugin_id, _metadata)| *plugin_id)
|
||||
.collect();
|
||||
if plugin_ids.is_empty() {
|
||||
return Err(ZellijError::PluginDoesNotExist).with_context(err_context);
|
||||
@@ -231,39 +261,25 @@ impl PluginMap {
|
||||
);
|
||||
}
|
||||
pub fn run_plugin_of_plugin_id(&self, plugin_id: PluginId) -> Option<RunPlugin> {
|
||||
self.plugin_assets
|
||||
.iter()
|
||||
.find_map(|((p_id, _), (running_plugin, _, _))| {
|
||||
if *p_id == plugin_id {
|
||||
let running_plugin = running_plugin.lock().unwrap();
|
||||
let plugin_config = &running_plugin.store.data().plugin;
|
||||
let run_plugin_location = plugin_config.location.clone();
|
||||
let run_plugin_configuration =
|
||||
plugin_config.initial_userspace_configuration.clone();
|
||||
let initial_cwd = plugin_config.initial_cwd.clone();
|
||||
Some(RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: run_plugin_location,
|
||||
configuration: run_plugin_configuration,
|
||||
initial_cwd,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
self.plugin_metadata
|
||||
.get(&plugin_id)
|
||||
.map(|metadata| RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: metadata.plugin_config.location.clone(),
|
||||
configuration: metadata
|
||||
.plugin_config
|
||||
.initial_userspace_configuration
|
||||
.clone(),
|
||||
initial_cwd: metadata.plugin_config.initial_cwd.clone(),
|
||||
})
|
||||
}
|
||||
pub fn list_plugins(&self) -> BTreeMap<PluginId, RunPlugin> {
|
||||
let all_plugin_ids: HashSet<PluginId> = self
|
||||
.all_plugin_ids()
|
||||
.into_iter()
|
||||
.map(|(plugin_id, _client_id)| plugin_id)
|
||||
.collect();
|
||||
let mut plugin_ids_to_cmds: BTreeMap<u32, RunPlugin> = BTreeMap::new();
|
||||
for plugin_id in all_plugin_ids {
|
||||
let plugin_cmd = self.run_plugin_of_plugin_id(plugin_id);
|
||||
for plugin_id in self.plugin_metadata.keys() {
|
||||
let plugin_cmd = self.run_plugin_of_plugin_id(*plugin_id);
|
||||
match plugin_cmd {
|
||||
Some(plugin_cmd) => {
|
||||
plugin_ids_to_cmds.insert(plugin_id, plugin_cmd.clone());
|
||||
plugin_ids_to_cmds.insert(*plugin_id, plugin_cmd.clone());
|
||||
},
|
||||
None => log::error!("Plugin with id: {plugin_id} not found"),
|
||||
}
|
||||
@@ -280,7 +296,6 @@ pub struct PluginEnv {
|
||||
pub permissions: Arc<Mutex<Option<HashSet<PermissionType>>>>,
|
||||
pub senders: ThreadSenders,
|
||||
pub wasi_ctx: WasiCtx,
|
||||
pub tab_index: Option<usize>,
|
||||
pub client_id: ClientId,
|
||||
#[allow(dead_code)]
|
||||
pub plugin_own_data_dir: PathBuf,
|
||||
|
||||
+1
@@ -8,6 +8,7 @@ Some(
|
||||
None,
|
||||
NoPreference {
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
ClientId(
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -36,6 +37,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -51,6 +53,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+3
@@ -18,6 +18,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -36,6 +37,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -51,6 +53,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+1
@@ -31,6 +31,7 @@ Some(
|
||||
None,
|
||||
NoPreference {
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
true,
|
||||
ClientId(
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ Some(
|
||||
Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
ClientId(
|
||||
|
||||
+1
@@ -28,6 +28,7 @@ Some(
|
||||
Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
ClientId(
|
||||
|
||||
+1
@@ -30,6 +30,7 @@ Some(
|
||||
Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
ClientId(
|
||||
|
||||
+1
@@ -23,6 +23,7 @@ Some(
|
||||
Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
ClientId(
|
||||
|
||||
+94
@@ -21,6 +21,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -60,6 +61,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -75,6 +77,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -112,6 +115,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -130,6 +134,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -147,6 +152,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -162,6 +168,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -201,6 +208,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -216,6 +224,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -248,6 +257,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -272,6 +282,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -290,6 +301,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -308,6 +320,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -326,6 +339,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -341,6 +355,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -356,6 +371,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -371,6 +387,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -410,6 +427,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -425,6 +443,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -457,6 +476,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -481,6 +501,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -499,6 +520,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -517,6 +539,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -535,6 +558,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -550,6 +574,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -572,6 +597,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -590,6 +616,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -608,6 +635,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -626,6 +654,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -641,6 +670,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -656,6 +686,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -671,6 +702,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -710,6 +742,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -725,6 +758,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -760,6 +794,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -778,6 +813,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -793,6 +829,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -832,6 +869,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -847,6 +885,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -879,6 +918,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -903,6 +943,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -921,6 +962,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -939,6 +981,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -957,6 +1000,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -972,6 +1016,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -987,6 +1032,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1002,6 +1048,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1041,6 +1088,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1056,6 +1104,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1088,6 +1137,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -1112,6 +1162,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1130,6 +1181,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1148,6 +1200,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1166,6 +1219,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1181,6 +1235,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1203,6 +1258,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1221,6 +1277,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1239,6 +1296,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1257,6 +1315,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1272,6 +1331,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1287,6 +1347,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1302,6 +1363,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1341,6 +1403,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1356,6 +1419,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1391,6 +1455,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -1408,6 +1473,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1447,6 +1513,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1462,6 +1529,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1501,6 +1569,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1519,6 +1588,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: Some(
|
||||
0,
|
||||
@@ -1536,6 +1606,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1551,6 +1622,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1590,6 +1662,7 @@ Some(
|
||||
borderless: Some(
|
||||
true,
|
||||
),
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1605,6 +1678,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -1662,6 +1736,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1694,6 +1769,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1726,6 +1802,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1758,6 +1835,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1790,6 +1868,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1822,6 +1901,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1854,6 +1934,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1886,6 +1967,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1918,6 +2000,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1950,6 +2033,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -1985,6 +2069,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2017,6 +2102,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2045,6 +2131,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2077,6 +2164,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2105,6 +2193,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2133,6 +2222,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2169,6 +2259,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2201,6 +2292,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2233,6 +2325,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
@@ -2265,6 +2358,7 @@ Some(
|
||||
),
|
||||
pinned: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
run: None,
|
||||
focus: None,
|
||||
already_running: false,
|
||||
|
||||
@@ -4,7 +4,9 @@ use crate::plugins::pipes::{
|
||||
apply_pipe_message_to_plugin, pipes_to_block_or_unblock, PendingPipes, PipeStateChange,
|
||||
};
|
||||
use crate::plugins::plugin_loader::PluginLoader;
|
||||
use crate::plugins::plugin_map::{AtomicEvent, PluginEnv, PluginMap, RunningPlugin, Subscriptions};
|
||||
use crate::plugins::plugin_map::{
|
||||
AtomicEvent, PluginEnv, PluginMap, RemovedPluginAssets, RunningPlugin, Subscriptions,
|
||||
};
|
||||
|
||||
use crate::plugins::plugin_worker::MessageToWorker;
|
||||
use crate::plugins::watch_filesystem::watch_filesystem;
|
||||
@@ -688,10 +690,11 @@ impl WasmBridge {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut new_plugins = HashSet::new();
|
||||
for plugin_id in self.plugin_map.lock().unwrap().plugin_ids() {
|
||||
new_plugins.insert(plugin_id);
|
||||
}
|
||||
let new_plugins: HashSet<PluginId> = {
|
||||
let plugin_map = self.plugin_map.lock().unwrap();
|
||||
self.connected_clients.lock().unwrap().push(client_id);
|
||||
plugin_map.plugin_ids().into_iter().collect()
|
||||
};
|
||||
for plugin_id in new_plugins {
|
||||
if self
|
||||
.plugin_map
|
||||
@@ -783,7 +786,6 @@ impl WasmBridge {
|
||||
},
|
||||
)
|
||||
}
|
||||
self.connected_clients.lock().unwrap().push(client_id);
|
||||
Ok(())
|
||||
}
|
||||
pub fn resize_plugin(
|
||||
@@ -795,6 +797,11 @@ impl WasmBridge {
|
||||
) -> Result<()> {
|
||||
let err_context = move || format!("failed to resize plugin {pid}");
|
||||
|
||||
self.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_size(pid, new_rows, new_columns);
|
||||
|
||||
let plugins_to_resize: Vec<(PluginId, ClientId, Arc<Mutex<RunningPlugin>>)> = self
|
||||
.plugin_map
|
||||
.lock()
|
||||
@@ -1028,7 +1035,7 @@ impl WasmBridge {
|
||||
// Execute directly on pinned thread (no async I/O needed for directory check/change)
|
||||
self.plugin_executor
|
||||
.execute_for_plugin(plugin_id_to_update, {
|
||||
move |senders, _plugin_map, _connected_clients, _plugin_cache, _engine| {
|
||||
move |senders, plugin_map, _connected_clients, _plugin_cache, _engine| {
|
||||
match new_host_dir.try_exists() {
|
||||
Ok(false) => {
|
||||
log::error!(
|
||||
@@ -1081,6 +1088,11 @@ impl WasmBridge {
|
||||
Ok(wasi_ctx) => {
|
||||
drop(std::mem::replace(&mut plugin_env.wasi_ctx, wasi_ctx));
|
||||
plugin_env.plugin_cwd = new_host_dir.clone();
|
||||
drop(running_plugin);
|
||||
plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.set_cwd(*plugin_id, new_host_dir.clone());
|
||||
|
||||
let _ =
|
||||
senders.send_to_plugin(PluginInstruction::Update(vec![(
|
||||
@@ -1258,6 +1270,78 @@ impl WasmBridge {
|
||||
if let Some(ref mut prev_report) = self.previous_pane_render_report {
|
||||
prev_report.all_pane_contents.remove(&client_id);
|
||||
}
|
||||
|
||||
let instances_to_clean_up = self
|
||||
.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.remove_plugin_clients(client_id);
|
||||
self.clean_up_plugin_instances(instances_to_clean_up);
|
||||
|
||||
for messages in self.cached_worker_messages.values_mut() {
|
||||
messages.retain(|(c_id, _, _, _)| c_id != &client_id);
|
||||
}
|
||||
self.cached_worker_messages
|
||||
.retain(|_plugin_id, messages| !messages.is_empty());
|
||||
}
|
||||
|
||||
pub fn update_plugin_tab_indices(&mut self, tab_indices: Vec<(PluginId, usize)>) {
|
||||
let mut plugin_map = self.plugin_map.lock().unwrap();
|
||||
for (plugin_id, tab_index) in tab_indices {
|
||||
plugin_map.set_tab_index(plugin_id, tab_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn clean_up_plugin_instances(&mut self, instances: RemovedPluginAssets) {
|
||||
if instances.is_empty() {
|
||||
return;
|
||||
}
|
||||
for ((plugin_id, client_id), (running_plugin, subscriptions, workers)) in instances {
|
||||
if running_plugin.lock().unwrap().intercepting_key_presses() {
|
||||
let _ = self
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(client_id));
|
||||
}
|
||||
let _ = self.senders.send_to_screen(
|
||||
ScreenInstruction::UpdateBackgroundPluginSubscriptions(
|
||||
plugin_id,
|
||||
client_id,
|
||||
HashSet::new(),
|
||||
),
|
||||
);
|
||||
|
||||
for (_worker_name, worker_sender) in workers {
|
||||
drop(worker_sender.send(MessageToWorker::Exit));
|
||||
}
|
||||
|
||||
self.plugin_executor.execute_for_plugin(plugin_id, {
|
||||
move |_senders, _plugin_map, _connected_clients, _plugin_cache, _engine| {
|
||||
let cache_dir = running_plugin
|
||||
.lock()
|
||||
.unwrap()
|
||||
.store
|
||||
.data()
|
||||
.plugin_own_data_dir
|
||||
.clone();
|
||||
if let Err(e) = std::fs::remove_dir_all(&cache_dir) {
|
||||
log::error!("Failed to remove data dir for plugin instance: {:?}", e);
|
||||
}
|
||||
drop(running_plugin);
|
||||
drop(subscriptions);
|
||||
}
|
||||
});
|
||||
|
||||
let mut pipes_to_unblock = self
|
||||
.pending_pipes
|
||||
.unload_plugin_client(&plugin_id, &client_id);
|
||||
for pipe_name in pipes_to_unblock.drain(..) {
|
||||
let _ = self
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::UnblockCliPipeInput(pipe_name))
|
||||
.context("failed to unblock input pipe");
|
||||
}
|
||||
}
|
||||
self.cached_plugin_map.clear();
|
||||
}
|
||||
|
||||
fn get_changed_panes_per_client(
|
||||
@@ -1329,17 +1413,13 @@ impl WasmBridge {
|
||||
client_id: ClientId,
|
||||
events: HashSet<EventType>,
|
||||
) {
|
||||
// Check if this plugin is a background plugin (tab_index == None)
|
||||
let is_background = {
|
||||
let mut plugin_map = self.plugin_map.lock().unwrap();
|
||||
plugin_map
|
||||
.running_plugins_and_subscriptions()
|
||||
.iter()
|
||||
.any(|(pid, cid, rp, _)| {
|
||||
*pid == plugin_id
|
||||
&& *cid == client_id
|
||||
&& rp.lock().unwrap().store.data().tab_index.is_none()
|
||||
})
|
||||
let plugin_map = self.plugin_map.lock().unwrap();
|
||||
plugin_map.contains(plugin_id, client_id)
|
||||
&& plugin_map
|
||||
.metadata(plugin_id)
|
||||
.map(|metadata| metadata.is_background)
|
||||
.unwrap_or(false)
|
||||
};
|
||||
if is_background {
|
||||
let _ = self.senders.send_to_screen(
|
||||
@@ -1689,41 +1769,29 @@ impl WasmBridge {
|
||||
self.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_running_plugin(plugin_id, None)
|
||||
.map(|r| {
|
||||
let r = r.lock().unwrap();
|
||||
(r.rows, r.columns)
|
||||
})
|
||||
.metadata(plugin_id)
|
||||
.map(|metadata| (metadata.rows, metadata.columns))
|
||||
}
|
||||
fn cwd_of_plugin_id(&self, plugin_id: PluginId) -> Option<PathBuf> {
|
||||
self.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_running_plugin(plugin_id, None)
|
||||
.map(|r| {
|
||||
let r = r.lock().unwrap();
|
||||
r.store.data().plugin_cwd.clone()
|
||||
})
|
||||
.metadata(plugin_id)
|
||||
.map(|metadata| metadata.cwd.clone())
|
||||
}
|
||||
fn plugin_config_of_plugin_id(&self, plugin_id: PluginId) -> Option<PluginConfig> {
|
||||
self.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_running_plugin(plugin_id, None)
|
||||
.map(|r| {
|
||||
let r = r.lock().unwrap();
|
||||
r.store.data().plugin.clone()
|
||||
})
|
||||
.metadata(plugin_id)
|
||||
.map(|metadata| metadata.plugin_config.clone())
|
||||
}
|
||||
fn tab_index_of_plugin_id(&self, plugin_id: PluginId) -> Option<usize> {
|
||||
self.plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.get_running_plugin(plugin_id, None)
|
||||
.and_then(|r| {
|
||||
let r = r.lock().unwrap();
|
||||
r.store.data().tab_index
|
||||
})
|
||||
.metadata(plugin_id)
|
||||
.and_then(|metadata| metadata.tab_index)
|
||||
}
|
||||
fn start_plugin_loading_indication(
|
||||
&self,
|
||||
@@ -1815,6 +1883,10 @@ impl WasmBridge {
|
||||
.data_mut()
|
||||
.set_permissions(HashSet::from_iter(permissions.clone()));
|
||||
|
||||
if running_plugin.store.data().plugin.is_builtin() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let mut permission_cache = PermissionCache::from_path_or_default(cache_path);
|
||||
permission_cache.cache(
|
||||
running_plugin.store.data().plugin.location.to_string(),
|
||||
|
||||
@@ -21,13 +21,14 @@ use tokio::sync::oneshot;
|
||||
use wasmi::{Caller, Linker};
|
||||
use zellij_utils::consts::ipc_connect;
|
||||
use zellij_utils::data::{
|
||||
BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse, BreakPanesToTabWithIndexResponse,
|
||||
CommandType, ConnectToSession, DeleteAllDeadSessionsResponse, DeleteDeadSessionResponse,
|
||||
DeleteLayoutResponse, EditLayoutResponse, Event, FloatingPaneCoordinates,
|
||||
FocusOrCreateTabResponse, GetFocusedPaneInfoResponse, GetPaneCwdResponse, GetPanePidResponse,
|
||||
GetPaneRunningCommandResponse, HttpVerb, KeyWithModifier, KillSessionsResponse, LayoutInfo,
|
||||
LayoutMetadata, LayoutParsingError, MessageToPlugin, NewPanePlacement, NewTabResponse,
|
||||
NewTabUnfocusedResponse, NewTiledPaneInTabResponse, OpenCommandPaneBackgroundResponse,
|
||||
BorderStyleOverride, BreakPanesToNewTabResponse, BreakPanesToTabWithIdResponse,
|
||||
BreakPanesToTabWithIndexResponse, CommandType, ConnectToSession, DeleteAllDeadSessionsResponse,
|
||||
DeleteDeadSessionResponse, DeleteLayoutResponse, EditLayoutResponse, Event,
|
||||
FloatingPaneCoordinates, FocusOrCreateTabResponse, GetFocusedPaneInfoResponse,
|
||||
GetPaneCwdResponse, GetPanePidResponse, GetPaneRunningCommandResponse, HttpVerb,
|
||||
KeyWithModifier, KillSessionsResponse, LayoutInfo, LayoutMetadata, LayoutParsingError,
|
||||
MessageToPlugin, NewPanePlacement, NewTabResponse, NewTabUnfocusedResponse,
|
||||
NewTiledPaneInTabResponse, OpenCommandPaneBackgroundResponse,
|
||||
OpenCommandPaneFloatingNearPluginResponse, OpenCommandPaneFloatingResponse,
|
||||
OpenCommandPaneInPlaceOfPaneIdResponse, OpenCommandPaneInPlaceOfPluginResponse,
|
||||
OpenCommandPaneInPlaceResponse, OpenCommandPaneNearPluginResponse, OpenCommandPaneResponse,
|
||||
@@ -199,21 +200,35 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
floating_pane_coordinates,
|
||||
context,
|
||||
) => open_file_floating(env, file_to_open, floating_pane_coordinates, context),
|
||||
PluginCommand::OpenTerminal(cwd) => open_terminal(env, cwd.path.try_into()?),
|
||||
PluginCommand::OpenTerminal(cwd) => {
|
||||
let border_style = cwd.border_style;
|
||||
open_terminal(env, cwd.path.try_into()?, border_style)
|
||||
},
|
||||
PluginCommand::OpenTerminalNearPlugin(cwd) => {
|
||||
open_terminal_near_plugin(env, cwd.path.try_into()?)
|
||||
let border_style = cwd.border_style;
|
||||
open_terminal_near_plugin(env, cwd.path.try_into()?, border_style)
|
||||
},
|
||||
PluginCommand::OpenTerminalFloating(cwd, floating_pane_coordinates) => {
|
||||
let floating_pane_coordinates = FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
cwd.border_style,
|
||||
);
|
||||
open_terminal_floating(env, cwd.path.try_into()?, floating_pane_coordinates)
|
||||
},
|
||||
PluginCommand::OpenTerminalFloatingNearPlugin(
|
||||
cwd,
|
||||
floating_pane_coordinates,
|
||||
) => open_terminal_floating_near_plugin(
|
||||
env,
|
||||
cwd.path.try_into()?,
|
||||
floating_pane_coordinates,
|
||||
),
|
||||
) => {
|
||||
let floating_pane_coordinates = FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
cwd.border_style,
|
||||
);
|
||||
open_terminal_floating_near_plugin(
|
||||
env,
|
||||
cwd.path.try_into()?,
|
||||
floating_pane_coordinates,
|
||||
)
|
||||
},
|
||||
PluginCommand::OpenCommandPane(command_to_run, context) => {
|
||||
open_command_pane(env, command_to_run, context)
|
||||
},
|
||||
@@ -351,6 +366,12 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
PluginCommand::QuitZellij => quit_zellij(env),
|
||||
PluginCommand::PreviousSwapLayout => previous_swap_layout(env),
|
||||
PluginCommand::NextSwapLayout => next_swap_layout(env),
|
||||
PluginCommand::ApplyTiledSwapLayout(layout_name) => {
|
||||
apply_tiled_swap_layout(env, layout_name)
|
||||
},
|
||||
PluginCommand::ApplyFloatingSwapLayout(layout_name) => {
|
||||
apply_floating_swap_layout(env, layout_name)
|
||||
},
|
||||
PluginCommand::GoToTabName(tab_name) => go_to_tab_name(env, tab_name),
|
||||
PluginCommand::FocusOrCreateTab(tab_name) => focus_or_create_tab(env, tab_name),
|
||||
PluginCommand::GoToTab(tab_index) => go_to_tab(env, tab_index),
|
||||
@@ -622,6 +643,9 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
PluginCommand::TogglePaneBorderless(pane_id) => {
|
||||
toggle_pane_borderless(env, pane_id.into())
|
||||
},
|
||||
PluginCommand::SetPaneBorderStyle(pane_id, border_style) => {
|
||||
set_pane_border_style(env, pane_id.into(), border_style)
|
||||
},
|
||||
PluginCommand::SetPaneBorderless(pane_id, borderless) => {
|
||||
set_pane_borderless(env, pane_id.into(), borderless)
|
||||
},
|
||||
@@ -901,8 +925,9 @@ fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) {
|
||||
}
|
||||
|
||||
fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Result<()> {
|
||||
if PermissionCache::from_path_or_default(None)
|
||||
.check_permissions(env.plugin.location.to_string(), &permissions)
|
||||
if env.plugin.is_builtin()
|
||||
|| PermissionCache::from_path_or_default(None)
|
||||
.check_permissions(env.plugin.location.to_string(), &permissions)
|
||||
{
|
||||
return env
|
||||
.senders
|
||||
@@ -1208,6 +1233,7 @@ fn open_editor_pane_in_new_tab(
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let file_to_open = FileToOpen {
|
||||
border_style: None,
|
||||
path,
|
||||
cwd,
|
||||
..file_to_open
|
||||
@@ -1407,6 +1433,7 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String
|
||||
let floating = false;
|
||||
let in_place = false;
|
||||
let start_suppressed = false;
|
||||
let border_style = file_to_open.border_style;
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
@@ -1421,7 +1448,7 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String
|
||||
in_place,
|
||||
close_replaced_pane: false,
|
||||
start_suppressed,
|
||||
coordinates: None,
|
||||
coordinates: FloatingPaneCoordinates::merge_border_style(None, border_style),
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
@@ -1503,6 +1530,7 @@ fn open_file_floating(
|
||||
let floating = true;
|
||||
let in_place = false;
|
||||
let start_suppressed = false;
|
||||
let border_style = file_to_open.border_style;
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
@@ -1517,7 +1545,10 @@ fn open_file_floating(
|
||||
in_place,
|
||||
close_replaced_pane: false,
|
||||
start_suppressed,
|
||||
coordinates: floating_pane_coordinates,
|
||||
coordinates: FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
border_style,
|
||||
),
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
@@ -1582,6 +1613,7 @@ fn open_file_near_plugin(
|
||||
file_to_open: FileToOpen,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let border_style = file_to_open.border_style;
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
@@ -1600,7 +1632,7 @@ fn open_file_near_plugin(
|
||||
let pty_instr = PtyInstruction::SpawnTerminal(
|
||||
Some(open_file),
|
||||
Some(title),
|
||||
NewPanePlacement::default(),
|
||||
NewPanePlacement::default().with_border_style(border_style),
|
||||
start_suppressed,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
@@ -1625,6 +1657,10 @@ fn open_file_floating_near_plugin(
|
||||
floating_pane_coordinates: Option<FloatingPaneCoordinates>,
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let floating_pane_coordinates = FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
file_to_open.border_style,
|
||||
);
|
||||
let cwd = file_to_open
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd))
|
||||
@@ -1702,7 +1738,7 @@ fn open_file_in_place_of_plugin(
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
|
||||
fn open_terminal(env: &PluginEnv, cwd: PathBuf, border_style: Option<BorderStyleOverride>) {
|
||||
let error_msg = || format!("failed to open file in plugin {}", env.name());
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
@@ -1724,6 +1760,7 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
border_style,
|
||||
tab_id: None,
|
||||
};
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
@@ -1738,7 +1775,11 @@ fn open_terminal(env: &PluginEnv, cwd: PathBuf) {
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) {
|
||||
fn open_terminal_near_plugin(
|
||||
env: &PluginEnv,
|
||||
cwd: PathBuf,
|
||||
border_style: Option<BorderStyleOverride>,
|
||||
) {
|
||||
let cwd = translate_plugin_path(env, cwd);
|
||||
let mut default_shell = env.default_shell.clone().unwrap_or_else(|| {
|
||||
TerminalAction::RunCommand(RunCommand {
|
||||
@@ -1758,6 +1799,7 @@ fn open_terminal_near_plugin(env: &PluginEnv, cwd: PathBuf) {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style,
|
||||
},
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
@@ -1940,6 +1982,7 @@ fn open_command_pane_in_place_of_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let _border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2039,6 +2082,7 @@ fn open_command_pane_in_place_of_pane_id(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let _border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2134,6 +2178,7 @@ fn open_command_pane(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2164,6 +2209,7 @@ fn open_command_pane(
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
border_style,
|
||||
tab_id: None,
|
||||
};
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
@@ -2185,6 +2231,7 @@ fn open_command_pane_near_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2218,6 +2265,7 @@ fn open_command_pane_near_plugin(
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style,
|
||||
},
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
@@ -2244,6 +2292,7 @@ fn open_command_pane_floating(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2270,7 +2319,10 @@ fn open_command_pane_floating(
|
||||
let action = Action::NewFloatingPane {
|
||||
command: Some(run_command_action),
|
||||
pane_name: name,
|
||||
coordinates: floating_pane_coordinates,
|
||||
coordinates: FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
border_style,
|
||||
),
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
@@ -2295,6 +2347,7 @@ fn open_command_pane_floating_near_plugin(
|
||||
context: BTreeMap<String, String>,
|
||||
) {
|
||||
let command = command_to_run.path;
|
||||
let border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2325,7 +2378,10 @@ fn open_command_pane_floating_near_plugin(
|
||||
let _ = env.senders.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||
Some(run_cmd),
|
||||
name,
|
||||
NewPanePlacement::Floating(floating_pane_coordinates),
|
||||
NewPanePlacement::Floating(FloatingPaneCoordinates::merge_border_style(
|
||||
floating_pane_coordinates,
|
||||
border_style,
|
||||
)),
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
@@ -2355,6 +2411,7 @@ fn open_command_pane_in_place(
|
||||
) {
|
||||
let error_msg = || format!("failed to open command in plugin {}", env.name());
|
||||
let command = command_to_run.path;
|
||||
let _border_style = command_to_run.border_style;
|
||||
let cwd = command_to_run
|
||||
.cwd
|
||||
.map(|cwd| translate_plugin_path(env, cwd));
|
||||
@@ -2869,6 +2926,7 @@ fn new_tiled_pane_in_tab(env: &PluginEnv, tab_position: usize) {
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
tab_id: Some(tab_position),
|
||||
};
|
||||
let result = apply_action!(action, error_msg, env);
|
||||
@@ -3251,6 +3309,23 @@ fn next_swap_layout(env: &PluginEnv) {
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn apply_tiled_swap_layout(env: &PluginEnv, layout_name: String) {
|
||||
let error_msg = || format!("failed to apply tiled swap layout in plugin {}", env.name());
|
||||
let action = Action::ApplyTiledSwapLayout { name: layout_name };
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn apply_floating_swap_layout(env: &PluginEnv, layout_name: String) {
|
||||
let error_msg = || {
|
||||
format!(
|
||||
"failed to apply floating swap layout in plugin {}",
|
||||
env.name()
|
||||
)
|
||||
};
|
||||
let action = Action::ApplyFloatingSwapLayout { name: layout_name };
|
||||
apply_action!(action, error_msg, env);
|
||||
}
|
||||
|
||||
fn go_to_tab_name(env: &PluginEnv, tab_name: String) {
|
||||
let error_msg = || format!("failed to change tab in plugin {}", env.name());
|
||||
let create = false;
|
||||
@@ -3945,6 +4020,16 @@ fn set_pane_borderless(env: &PluginEnv, pane_id: PaneId, borderless: bool) {
|
||||
));
|
||||
}
|
||||
|
||||
fn set_pane_border_style(env: &PluginEnv, pane_id: PaneId, border_style: BorderStyleOverride) {
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::SetPaneBorderStyle(
|
||||
pane_id,
|
||||
border_style,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
fn set_pane_color(env: &PluginEnv, pane_id: PaneId, fg: Option<String>, bg: Option<String>) {
|
||||
let _ = env
|
||||
.senders
|
||||
@@ -4685,6 +4770,7 @@ fn try_edit_layout(
|
||||
|
||||
// Create FileToOpen for the layout file
|
||||
let file_to_open = FileToOpen {
|
||||
border_style: None,
|
||||
path: file_path,
|
||||
line_number: None,
|
||||
cwd: Some(layout_dir.clone()),
|
||||
@@ -5520,6 +5606,8 @@ fn check_command_permission(
|
||||
| PluginCommand::QuitZellij
|
||||
| PluginCommand::PreviousSwapLayout
|
||||
| PluginCommand::NextSwapLayout
|
||||
| PluginCommand::ApplyTiledSwapLayout(..)
|
||||
| PluginCommand::ApplyFloatingSwapLayout(..)
|
||||
| PluginCommand::GoToTabName(..)
|
||||
| PluginCommand::FocusOrCreateTab(..)
|
||||
| PluginCommand::GoToTab(..)
|
||||
@@ -5554,6 +5642,7 @@ fn check_command_permission(
|
||||
| PluginCommand::ChangeFloatingPanesCoordinates(..)
|
||||
| PluginCommand::TogglePaneBorderless(..)
|
||||
| PluginCommand::SetPaneBorderless(..)
|
||||
| PluginCommand::SetPaneBorderStyle(..)
|
||||
| PluginCommand::SetPaneColor(..)
|
||||
| PluginCommand::GroupAndUngroupPanes(..)
|
||||
| PluginCommand::HighlightAndUnhighlightPanes(..)
|
||||
|
||||
@@ -23,7 +23,7 @@ use zellij_utils::{
|
||||
UnblockCondition,
|
||||
},
|
||||
envs,
|
||||
errors::prelude::*,
|
||||
errors::{prelude::*, ErrorContext},
|
||||
input::{
|
||||
actions::{Action, SearchDirection, SearchOption},
|
||||
command::TerminalAction,
|
||||
@@ -678,8 +678,12 @@ pub(crate) fn route_action(
|
||||
Some(direction) => NewPanePlacement::Tiled {
|
||||
direction: Some(direction),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
None => NewPanePlacement::NoPreference {
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
None => NewPanePlacement::NoPreference { borderless: None },
|
||||
};
|
||||
senders
|
||||
.send_to_pty(PtyInstruction::SpawnTerminal(
|
||||
@@ -782,7 +786,12 @@ pub(crate) fn route_action(
|
||||
} else {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: split_direction,
|
||||
borderless: None,
|
||||
borderless: floating_pane_coordinates
|
||||
.as_ref()
|
||||
.and_then(|c| c.borderless),
|
||||
border_style: floating_pane_coordinates
|
||||
.as_ref()
|
||||
.and_then(|c| c.border_style),
|
||||
}
|
||||
},
|
||||
start_suppressed,
|
||||
@@ -889,6 +898,7 @@ pub(crate) fn route_action(
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
if no_focus {
|
||||
ClientTabIndexOrPaneId::TabIndexNoFocus(tab_id)
|
||||
@@ -902,6 +912,7 @@ pub(crate) fn route_action(
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: Some(pane_id.into()),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
ClientTabIndexOrPaneId::PaneId(pane_id),
|
||||
)
|
||||
@@ -910,6 +921,7 @@ pub(crate) fn route_action(
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
if no_focus {
|
||||
ClientTabIndexOrPaneId::ClientIdNoFocus(client_id)
|
||||
@@ -937,6 +949,7 @@ pub(crate) fn route_action(
|
||||
near_current_pane,
|
||||
no_focus,
|
||||
borderless,
|
||||
border_style,
|
||||
tab_id,
|
||||
} => {
|
||||
let run_cmd = run_command
|
||||
@@ -951,6 +964,7 @@ pub(crate) fn route_action(
|
||||
NewPanePlacement::Tiled {
|
||||
direction,
|
||||
borderless,
|
||||
border_style,
|
||||
},
|
||||
false,
|
||||
client_tab_index_or_paneid,
|
||||
@@ -1008,6 +1022,7 @@ pub(crate) fn route_action(
|
||||
NewPanePlacement::Tiled {
|
||||
direction: command.direction,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
false,
|
||||
client_tab_index_or_paneid,
|
||||
@@ -1337,6 +1352,24 @@ pub(crate) fn route_action(
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::ApplyTiledSwapLayout { name } => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ApplyTiledSwapLayout(
|
||||
client_id,
|
||||
name,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::ApplyFloatingSwapLayout { name } => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ApplyFloatingSwapLayout(
|
||||
client_id,
|
||||
name,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::OverrideLayout {
|
||||
tabs,
|
||||
retain_existing_terminal_panes,
|
||||
@@ -1897,6 +1930,18 @@ pub(crate) fn route_action(
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::SetPaneBorderStyle {
|
||||
pane_id,
|
||||
border_style,
|
||||
} => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::SetPaneBorderStyle(
|
||||
pane_id.into(),
|
||||
border_style,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::TogglePaneInGroup => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::TogglePaneInGroup(
|
||||
@@ -2160,6 +2205,24 @@ pub(crate) fn route_action(
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::ApplyTiledSwapLayoutByTabId { id, name } => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ApplyTiledSwapLayoutWithTabId(
|
||||
id as usize,
|
||||
name,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::ApplyFloatingSwapLayoutByTabId { id, name } => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::ApplyFloatingSwapLayoutWithTabId(
|
||||
id as usize,
|
||||
name,
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
))
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
Action::MoveTabByTabId { id, direction } => {
|
||||
senders
|
||||
.send_to_screen(ScreenInstruction::MoveTabWithTabId(
|
||||
@@ -2257,13 +2320,21 @@ pub(crate) fn route_thread_main(
|
||||
to_server: SenderWithContext<ServerInstruction>,
|
||||
mut receiver: IpcReceiverWithContext<ClientToServerMsg>,
|
||||
client_id: ClientId,
|
||||
first_instruction: Option<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
let mut retry_queue = VecDeque::new();
|
||||
let err_context = || format!("failed to handle instruction for client {client_id}");
|
||||
let mut seen_cli_pipes = HashSet::new();
|
||||
let mut consecutive_unknown_messages_received = 0;
|
||||
let mut first_instruction =
|
||||
first_instruction.map(|instruction| (instruction, ErrorContext::default()));
|
||||
let mut cleanup_requested = false;
|
||||
'route_loop: loop {
|
||||
match receiver.try_recv_client_msg() {
|
||||
let received = match first_instruction.take() {
|
||||
Some(instruction) => Ok(instruction),
|
||||
None => receiver.try_recv_client_msg(),
|
||||
};
|
||||
match received {
|
||||
Ok((instruction, err_ctx)) => {
|
||||
consecutive_unknown_messages_received = 0;
|
||||
err_ctx.update_thread_ctx();
|
||||
@@ -2674,7 +2745,6 @@ pub(crate) fn route_thread_main(
|
||||
}
|
||||
},
|
||||
ClientToServerMsg::ClientExited => {
|
||||
let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
|
||||
return Ok(true);
|
||||
},
|
||||
ClientToServerMsg::KillSession => {
|
||||
@@ -2927,6 +2997,7 @@ pub(crate) fn route_thread_main(
|
||||
},
|
||||
);
|
||||
let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
|
||||
cleanup_requested = true;
|
||||
break 'route_loop;
|
||||
}
|
||||
},
|
||||
@@ -2938,7 +3009,9 @@ pub(crate) fn route_thread_main(
|
||||
let _ = os_input.send_to_client(client_id, ServerToClientMsg::UnblockInputThread);
|
||||
}
|
||||
// route thread exited, make sure we clean up
|
||||
let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
|
||||
if !cleanup_requested {
|
||||
let _ = to_server.send(ServerInstruction::RemoveClient(client_id));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
+266
-12
@@ -40,11 +40,12 @@ use crate::route::NotificationEnd;
|
||||
|
||||
use log::{debug, warn};
|
||||
use zellij_utils::data::{
|
||||
CommandOrPlugin, Direction, EventType, FloatingPaneCoordinates, GetFocusedPaneInfoResponse,
|
||||
HostTerminalThemeMode, KeyWithModifier, LayoutInfo, LayoutWithError, ListPanesResponse,
|
||||
ListTabsResponse, NewPanePlacement, PaneContents, PaneInfo, PaneListEntry, PaneManifest,
|
||||
PaneRenderReport, PaneScrollbackResponse, PluginPermission, RegexHighlight, Resize,
|
||||
ResizeStrategy, SessionInfo, Styling, TabInfo, ThemeHue, WebSharing,
|
||||
BorderStyle, BorderStyleOverride, CommandOrPlugin, Direction, EventType,
|
||||
FloatingPaneCoordinates, GetFocusedPaneInfoResponse, HostTerminalThemeMode, KeyWithModifier,
|
||||
LayoutInfo, LayoutWithError, ListPanesResponse, ListTabsResponse, NewPanePlacement,
|
||||
PaneContents, PaneInfo, PaneListEntry, PaneManifest, PaneRenderReport, PaneScrollbackResponse,
|
||||
PluginPermission, RegexHighlight, Resize, ResizeStrategy, SessionInfo, Styling, TabInfo,
|
||||
ThemeHue, WebSharing,
|
||||
};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::actions::Action;
|
||||
@@ -647,6 +648,8 @@ pub enum ScreenInstruction {
|
||||
},
|
||||
PreviousSwapLayout(ClientId, Option<NotificationEnd>),
|
||||
NextSwapLayout(ClientId, Option<NotificationEnd>),
|
||||
ApplyTiledSwapLayout(ClientId, String, Option<NotificationEnd>),
|
||||
ApplyFloatingSwapLayout(ClientId, String, Option<NotificationEnd>),
|
||||
OverrideLayout(
|
||||
Option<PathBuf>, // cwd (applies to all tabs)
|
||||
Option<TerminalAction>, // default_shell (applies to all tabs)
|
||||
@@ -812,6 +815,8 @@ pub enum ScreenInstruction {
|
||||
auto_layout: bool,
|
||||
rounded_corners: bool,
|
||||
hide_session_name: bool,
|
||||
border_style: BorderStyle,
|
||||
floating_border_style: BorderStyle,
|
||||
stacked_resize: bool,
|
||||
stacked_pane_list: bool,
|
||||
default_editor: Option<PathBuf>,
|
||||
@@ -890,6 +895,7 @@ pub enum ScreenInstruction {
|
||||
),
|
||||
TogglePaneBorderless(PaneId, Option<NotificationEnd>),
|
||||
SetPaneBorderless(PaneId, bool, Option<NotificationEnd>),
|
||||
SetPaneBorderStyle(PaneId, BorderStyleOverride, Option<NotificationEnd>),
|
||||
AddHighlightPaneFrameColorOverride(Vec<PaneId>, Option<String>), // Option<String> => optional
|
||||
// message
|
||||
GroupAndUngroupPanes(Vec<PaneId>, Vec<PaneId>, bool, ClientId), // panes_to_group, panes_to_ungroup, bool -> for all clients
|
||||
@@ -959,6 +965,8 @@ pub enum ScreenInstruction {
|
||||
ToggleFloatingPanesWithTabId(usize, Option<TerminalAction>, Option<NotificationEnd>),
|
||||
PreviousSwapLayoutWithTabId(usize, Option<NotificationEnd>),
|
||||
NextSwapLayoutWithTabId(usize, Option<NotificationEnd>),
|
||||
ApplyTiledSwapLayoutWithTabId(usize, String, Option<NotificationEnd>),
|
||||
ApplyFloatingSwapLayoutWithTabId(usize, String, Option<NotificationEnd>),
|
||||
MoveTabWithTabId(usize, Direction, Option<NotificationEnd>),
|
||||
SetSoftKeyboard {
|
||||
client_id: ClientId,
|
||||
@@ -1165,6 +1173,10 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||
},
|
||||
ScreenInstruction::PreviousSwapLayout(..) => ScreenContext::PreviousSwapLayout,
|
||||
ScreenInstruction::NextSwapLayout(..) => ScreenContext::NextSwapLayout,
|
||||
ScreenInstruction::ApplyTiledSwapLayout(..) => ScreenContext::ApplyTiledSwapLayout,
|
||||
ScreenInstruction::ApplyFloatingSwapLayout(..) => {
|
||||
ScreenContext::ApplyFloatingSwapLayout
|
||||
},
|
||||
ScreenInstruction::OverrideLayout(..) => ScreenContext::OverrideLayout,
|
||||
ScreenInstruction::OverrideLayoutComplete(..) => ScreenContext::OverrideLayoutComplete,
|
||||
ScreenInstruction::QueryTabNames(..) => ScreenContext::QueryTabNames,
|
||||
@@ -1255,6 +1267,7 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||
},
|
||||
ScreenInstruction::TogglePaneBorderless(..) => ScreenContext::TogglePaneBorderless,
|
||||
ScreenInstruction::SetPaneBorderless(..) => ScreenContext::SetPaneBorderless,
|
||||
ScreenInstruction::SetPaneBorderStyle(..) => ScreenContext::SetPaneBorderStyle,
|
||||
ScreenInstruction::AddHighlightPaneFrameColorOverride(..) => {
|
||||
ScreenContext::AddHighlightPaneFrameColorOverride
|
||||
},
|
||||
@@ -1361,6 +1374,12 @@ impl From<&ScreenInstruction> for ScreenContext {
|
||||
ScreenInstruction::NextSwapLayoutWithTabId(..) => {
|
||||
ScreenContext::NextSwapLayoutWithTabId
|
||||
},
|
||||
ScreenInstruction::ApplyTiledSwapLayoutWithTabId(..) => {
|
||||
ScreenContext::ApplyTiledSwapLayoutWithTabId
|
||||
},
|
||||
ScreenInstruction::ApplyFloatingSwapLayoutWithTabId(..) => {
|
||||
ScreenContext::ApplyFloatingSwapLayoutWithTabId
|
||||
},
|
||||
ScreenInstruction::MoveTabWithTabId(..) => ScreenContext::MoveTabWithTabId,
|
||||
ScreenInstruction::SetSoftKeyboard { .. } => ScreenContext::SetSoftKeyboard,
|
||||
ScreenInstruction::FocusHostSession(..) => ScreenContext::FocusHostSession,
|
||||
@@ -1538,6 +1557,8 @@ pub(crate) struct Screen {
|
||||
terminal_emulator_colors: Rc<RefCell<Palette>>,
|
||||
terminal_emulator_color_codes: Rc<RefCell<HashMap<usize, String>>>,
|
||||
connected_clients: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
highest_client_id_seen: ClientId,
|
||||
/// The indices of this [`Screen`]'s active [`Tab`]s.
|
||||
active_tab_ids: BTreeMap<ClientId, usize>,
|
||||
client_sizes: HashMap<ClientId, Size>,
|
||||
@@ -1597,6 +1618,8 @@ pub(crate) struct Screen {
|
||||
cached_layout_errors: Vec<LayoutWithError>,
|
||||
pane_render_subscribers: HashMap<ClientId, PaneRenderSubscription>,
|
||||
background_plugin_subscriptions: HashMap<(PluginId, ClientId), HashSet<EventType>>,
|
||||
last_reported_plugin_tab_indices: HashMap<PluginId, usize>,
|
||||
state_report_target: Option<(PluginId, ClientId)>,
|
||||
next_forward_token: u32,
|
||||
pending_forwarded_queries: HashMap<u32, PendingForwardEntry>,
|
||||
forward_queue: VecDeque<PendingForward>,
|
||||
@@ -1750,6 +1773,8 @@ impl Screen {
|
||||
client_kitty_host_state: Rc::new(RefCell::new(HashMap::new())),
|
||||
style: client_attributes.style,
|
||||
connected_clients: Rc::new(RefCell::new(HashMap::new())),
|
||||
client_display_slots: Rc::new(RefCell::new(HashMap::new())),
|
||||
highest_client_id_seen: 0,
|
||||
active_tab_ids: BTreeMap::new(),
|
||||
client_sizes: HashMap::new(),
|
||||
global_last_active_tab_id: 0,
|
||||
@@ -1805,6 +1830,8 @@ impl Screen {
|
||||
cached_layout_errors: vec![],
|
||||
pane_render_subscribers: HashMap::new(),
|
||||
background_plugin_subscriptions: HashMap::new(),
|
||||
last_reported_plugin_tab_indices: HashMap::new(),
|
||||
state_report_target: None,
|
||||
next_forward_token: 1, // 0 is reserved as the startup sentinel
|
||||
pending_forwarded_queries: HashMap::new(),
|
||||
forward_queue: VecDeque::new(),
|
||||
@@ -4849,6 +4876,7 @@ impl Screen {
|
||||
self.pane_frame_style,
|
||||
self.auto_layout,
|
||||
self.connected_clients.clone(),
|
||||
self.client_display_slots.clone(),
|
||||
self.session_is_mirrored,
|
||||
client_id,
|
||||
self.copy_options.clone(),
|
||||
@@ -5032,8 +5060,8 @@ impl Screen {
|
||||
})
|
||||
.with_context(err_context)?;
|
||||
|
||||
if !self.active_tab_ids.contains_key(&client_id) {
|
||||
// this means this is a new client and we need to add it to our state properly
|
||||
let client_has_since_disconnected = client_id <= self.highest_client_id_seen;
|
||||
if !self.active_tab_ids.contains_key(&client_id) && !client_has_since_disconnected {
|
||||
self.add_client(client_id, is_web_client)
|
||||
.with_context(err_context)?;
|
||||
}
|
||||
@@ -5061,8 +5089,16 @@ impl Screen {
|
||||
format!("failed to attach client {client_id} to tab with index {tab_index}")
|
||||
};
|
||||
|
||||
// Set followed_client_id to the first regular client if not already set
|
||||
if self.followed_client_id.is_none() && !self.watcher_clients.contains_key(&client_id) {
|
||||
let followed_client_is_gone = self
|
||||
.followed_client_id
|
||||
.map(|followed_client_id| {
|
||||
!self
|
||||
.connected_clients
|
||||
.borrow()
|
||||
.contains_key(&followed_client_id)
|
||||
})
|
||||
.unwrap_or(true);
|
||||
if followed_client_is_gone && !self.watcher_clients.contains_key(&client_id) {
|
||||
self.followed_client_id = Some(client_id);
|
||||
}
|
||||
|
||||
@@ -5097,11 +5133,13 @@ impl Screen {
|
||||
bail!("Can't find a valid tab to attach client to!");
|
||||
};
|
||||
|
||||
self.highest_client_id_seen = self.highest_client_id_seen.max(client_id);
|
||||
self.active_tab_ids.insert(client_id, tab_index);
|
||||
self.client_kitty_host_state.borrow_mut().remove(&client_id);
|
||||
self.connected_clients
|
||||
.borrow_mut()
|
||||
.insert(client_id, is_web_client);
|
||||
self.assign_display_slot(client_id);
|
||||
if is_web_client {
|
||||
self.kitty_host_capabilities
|
||||
.borrow_mut()
|
||||
@@ -5136,6 +5174,7 @@ impl Screen {
|
||||
|
||||
pub fn remove_client(&mut self, client_id: ClientId) -> Result<()> {
|
||||
let err_context = || format!("failed to remove client {client_id}");
|
||||
self.highest_client_id_seen = self.highest_client_id_seen.max(client_id);
|
||||
|
||||
self.set_client_dimmed(client_id, false, None);
|
||||
let passthrough_panes: Vec<PaneId> = self
|
||||
@@ -5218,7 +5257,10 @@ impl Screen {
|
||||
self.push_sixel_host_support_to_tabs();
|
||||
}
|
||||
self.client_sizes.remove(&client_id);
|
||||
self.client_display_slots.borrow_mut().remove(&client_id);
|
||||
self.pane_render_subscribers.remove(&client_id);
|
||||
self.background_plugin_subscriptions
|
||||
.retain(|(_plugin_id, c_id), _subscriptions| c_id != &client_id);
|
||||
self.last_forwarded_osc7.remove(&client_id);
|
||||
self.client_host_focused.remove(&client_id);
|
||||
self.client_notification_protocols.remove(&client_id);
|
||||
@@ -5297,6 +5339,7 @@ impl Screen {
|
||||
is_fullscreen_active: tab.is_fullscreen_active(),
|
||||
is_sync_panes_active: tab.is_sync_panes_active(),
|
||||
are_floating_panes_visible: tab.are_floating_panes_visible(),
|
||||
other_focused_client_slots: self.display_slots_of_clients(&all_focused_clients),
|
||||
other_focused_clients: all_focused_clients,
|
||||
active_swap_layout_name,
|
||||
is_swap_layout_dirty,
|
||||
@@ -5344,6 +5387,8 @@ impl Screen {
|
||||
is_fullscreen_active: tab.is_fullscreen_active(),
|
||||
is_sync_panes_active: tab.is_sync_panes_active(),
|
||||
are_floating_panes_visible: tab.are_floating_panes_visible(),
|
||||
other_focused_client_slots: self
|
||||
.display_slots_of_clients(&other_focused_clients),
|
||||
other_focused_clients,
|
||||
active_swap_layout_name,
|
||||
is_swap_layout_dirty,
|
||||
@@ -5402,6 +5447,20 @@ impl Screen {
|
||||
Ok(pane_manifest)
|
||||
}
|
||||
|
||||
fn report_pane_and_tab_state_to_plugin(
|
||||
&mut self,
|
||||
plugin_id: PluginId,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
self.state_report_target = Some((plugin_id, client_id));
|
||||
let pane_state = self.generate_and_report_pane_state();
|
||||
let tab_state = self.generate_and_report_tab_state();
|
||||
self.state_report_target = None;
|
||||
pane_state?;
|
||||
tab_state?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn collect_pane_list(&self, show_all: bool) -> Result<ListPanesResponse> {
|
||||
fn should_include_pane(pane_info: &PaneInfo, show_all: bool) -> bool {
|
||||
pane_info.is_selectable || show_all
|
||||
@@ -5473,6 +5532,45 @@ impl Screen {
|
||||
false
|
||||
}
|
||||
}
|
||||
fn assign_display_slot(&mut self, client_id: ClientId) {
|
||||
let mut display_slots = self.client_display_slots.borrow_mut();
|
||||
if display_slots.contains_key(&client_id) {
|
||||
return;
|
||||
}
|
||||
let taken: HashSet<usize> = display_slots.values().copied().collect();
|
||||
let mut slot = 1;
|
||||
while taken.contains(&slot) {
|
||||
slot += 1;
|
||||
}
|
||||
display_slots.insert(client_id, slot);
|
||||
}
|
||||
fn display_slots_of_clients(&self, client_ids: &[ClientId]) -> Vec<usize> {
|
||||
let display_slots = self.client_display_slots.borrow();
|
||||
client_ids
|
||||
.iter()
|
||||
.map(|client_id| display_slots.get(client_id).copied().unwrap_or(0))
|
||||
.collect()
|
||||
}
|
||||
fn report_plugin_tab_indices(&mut self) {
|
||||
let mut current: HashMap<PluginId, usize> = HashMap::new();
|
||||
for tab in self.tabs.values() {
|
||||
for plugin_id in tab.get_plugin_ids() {
|
||||
current.insert(plugin_id, tab.position);
|
||||
}
|
||||
}
|
||||
if current == self.last_reported_plugin_tab_indices {
|
||||
return;
|
||||
}
|
||||
let tab_indices: Vec<(PluginId, usize)> = current
|
||||
.iter()
|
||||
.map(|(plugin_id, tab_index)| (*plugin_id, *tab_index))
|
||||
.collect();
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::UpdatePluginTabIndices(tab_indices));
|
||||
self.last_reported_plugin_tab_indices = current;
|
||||
}
|
||||
fn log_and_report_session_state(&mut self) -> Result<()> {
|
||||
let err_context = || format!("Failed to log and report session state");
|
||||
|
||||
@@ -5480,6 +5578,7 @@ impl Screen {
|
||||
self.revert_fit_disabled_without_reference_client()
|
||||
.with_context(err_context)?;
|
||||
self.reconcile_all_single_pane_focus();
|
||||
self.report_plugin_tab_indices();
|
||||
// generate own session info
|
||||
let pane_manifest = self.generate_and_report_pane_state()?;
|
||||
let tab_infos = self.generate_and_report_tab_state()?;
|
||||
@@ -5880,7 +5979,7 @@ impl Screen {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn client_id(&mut self, client_id: ClientId) -> Option<u16> {
|
||||
fn client_id(&mut self, client_id: ClientId) -> Option<ClientId> {
|
||||
if self.get_active_tab(client_id).is_ok() {
|
||||
Some(client_id)
|
||||
} else {
|
||||
@@ -6171,6 +6270,13 @@ impl Screen {
|
||||
/// Returns plugin IDs from the client's active tab plus background plugins
|
||||
/// subscribed to the given event type.
|
||||
fn targeted_plugin_ids(&self, client_id: ClientId, event_type: EventType) -> Vec<PluginId> {
|
||||
if let Some((target_plugin_id, target_client_id)) = self.state_report_target {
|
||||
return if target_client_id == client_id {
|
||||
vec![target_plugin_id]
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
}
|
||||
let mut plugin_ids = Vec::new();
|
||||
// Active-tab plugins
|
||||
if let Some(active_tab_id) = self.active_tab_ids.get(&client_id) {
|
||||
@@ -6712,6 +6818,7 @@ impl Screen {
|
||||
height: Some(PercentOrFixed::Fixed(pane.rows())),
|
||||
pinned: Some(pane.current_geom().is_pinned),
|
||||
borderless: Some(pane.borderless()),
|
||||
border_style: None,
|
||||
};
|
||||
new_active_tab.add_floating_pane(
|
||||
pane,
|
||||
@@ -6880,6 +6987,8 @@ impl Screen {
|
||||
auto_layout: bool,
|
||||
rounded_corners: bool,
|
||||
hide_session_name: bool,
|
||||
border_style: BorderStyle,
|
||||
floating_border_style: BorderStyle,
|
||||
stacked_resize: bool,
|
||||
stacked_pane_list: bool,
|
||||
default_editor: Option<PathBuf>,
|
||||
@@ -6906,6 +7015,10 @@ impl Screen {
|
||||
self.default_mode_info.update_theme(theme);
|
||||
self.default_mode_info
|
||||
.update_rounded_corners(rounded_corners);
|
||||
self.default_mode_info
|
||||
.update_border_styles(border_style, floating_border_style);
|
||||
self.style.border_style = border_style;
|
||||
self.style.floating_border_style = floating_border_style;
|
||||
// `default_mode_info` is the fallback used by `change_mode` for
|
||||
// clients that don't yet have a per-client `mode_info` entry, so its
|
||||
// keybinds and base mode must be kept in sync with reconfigures.
|
||||
@@ -6946,6 +7059,7 @@ impl Screen {
|
||||
for tab in self.tabs.values_mut() {
|
||||
tab.update_theme(theme);
|
||||
tab.update_rounded_corners(rounded_corners);
|
||||
tab.update_border_styles(border_style, floating_border_style);
|
||||
tab.update_default_shell(default_shell.clone());
|
||||
tab.update_default_editor(self.default_editor.clone());
|
||||
tab.update_auto_layout(auto_layout);
|
||||
@@ -7294,6 +7408,18 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
}
|
||||
pub fn set_pane_border_style(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
border_style: BorderStyleOverride,
|
||||
) -> bool {
|
||||
for (_tab_id, tab) in self.tabs.iter_mut() {
|
||||
if tab.has_pane_with_pid(&pane_id) {
|
||||
return tab.set_pane_border_style(pane_id, border_style);
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn handle_mouse_event(&mut self, event: MouseEvent, client_id: ClientId) {
|
||||
let is_bare_motion = event.event_type == MouseEventType::Motion
|
||||
&& !event.left
|
||||
@@ -7510,6 +7636,7 @@ impl Screen {
|
||||
focused_clients,
|
||||
default_fg,
|
||||
default_bg,
|
||||
p.border_style_override(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -7539,7 +7666,7 @@ impl Screen {
|
||||
PaneLayoutMetadata::new(
|
||||
pane_id,
|
||||
p.position_and_size(),
|
||||
false, // floating panes are never borderless
|
||||
p.borderless(),
|
||||
p.invoked_with().clone(),
|
||||
p.custom_title(),
|
||||
!focused_clients.is_empty(),
|
||||
@@ -7551,6 +7678,7 @@ impl Screen {
|
||||
focused_clients,
|
||||
default_fg,
|
||||
default_bg,
|
||||
p.border_style_override(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
@@ -7668,6 +7796,7 @@ impl Screen {
|
||||
is_fullscreen_active: tab.is_fullscreen_active(),
|
||||
is_sync_panes_active: tab.is_sync_panes_active(),
|
||||
are_floating_panes_visible: tab.are_floating_panes_visible(),
|
||||
other_focused_client_slots: self.display_slots_of_clients(&all_focused_clients),
|
||||
other_focused_clients: all_focused_clients,
|
||||
active_swap_layout_name,
|
||||
is_swap_layout_dirty,
|
||||
@@ -8415,6 +8544,7 @@ pub(crate) fn screen_thread_main(
|
||||
NewPanePlacement::Tiled {
|
||||
direction: Some(direction),
|
||||
borderless,
|
||||
border_style: None,
|
||||
} => {
|
||||
if direction == Direction::Left
|
||||
|| direction == Direction::Right
|
||||
@@ -10429,6 +10559,58 @@ pub(crate) fn screen_thread_main(
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::ApplyTiledSwapLayout(client_id, layout_name, mut completion_tx) => {
|
||||
let mut applied = false;
|
||||
active_tab_and_connected_client_id!(
|
||||
screen,
|
||||
client_id,
|
||||
|tab: &mut Tab, _client_id: ClientId| {
|
||||
applied = tab.apply_tiled_swap_layout(&layout_name)?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
},
|
||||
?
|
||||
);
|
||||
if !applied {
|
||||
log::error!("Tiled swap layout not found or incompatible: {layout_name}");
|
||||
if let Some(ref mut c) = completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!(
|
||||
"Tiled swap layout not found or incompatible: {layout_name}"
|
||||
));
|
||||
}
|
||||
}
|
||||
drop(completion_tx);
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::ApplyFloatingSwapLayout(
|
||||
client_id,
|
||||
layout_name,
|
||||
mut completion_tx,
|
||||
) => {
|
||||
let mut applied = false;
|
||||
active_tab_and_connected_client_id!(
|
||||
screen,
|
||||
client_id,
|
||||
|tab: &mut Tab, _client_id: ClientId| {
|
||||
applied = tab.apply_floating_swap_layout(&layout_name)?;
|
||||
Ok::<(), anyhow::Error>(())
|
||||
},
|
||||
?
|
||||
);
|
||||
if !applied {
|
||||
log::error!("Floating swap layout not found or incompatible: {layout_name}");
|
||||
if let Some(ref mut c) = completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!(
|
||||
"Floating swap layout not found or incompatible: {layout_name}"
|
||||
));
|
||||
}
|
||||
}
|
||||
drop(completion_tx);
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::OverrideLayout(
|
||||
cwd,
|
||||
default_shell,
|
||||
@@ -10828,6 +11010,7 @@ pub(crate) fn screen_thread_main(
|
||||
new_pane_placement = NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
};
|
||||
}
|
||||
if should_be_in_place {
|
||||
@@ -11603,6 +11786,8 @@ pub(crate) fn screen_thread_main(
|
||||
auto_layout,
|
||||
rounded_corners,
|
||||
hide_session_name,
|
||||
border_style,
|
||||
floating_border_style,
|
||||
stacked_resize,
|
||||
stacked_pane_list,
|
||||
default_editor,
|
||||
@@ -11636,6 +11821,8 @@ pub(crate) fn screen_thread_main(
|
||||
auto_layout,
|
||||
rounded_corners,
|
||||
hide_session_name,
|
||||
border_style,
|
||||
floating_border_style,
|
||||
stacked_resize,
|
||||
stacked_pane_list,
|
||||
default_editor,
|
||||
@@ -12062,6 +12249,16 @@ pub(crate) fn screen_thread_main(
|
||||
screen.set_pane_borderless(pane_id, borderless);
|
||||
let _ = screen.render(None);
|
||||
},
|
||||
ScreenInstruction::SetPaneBorderStyle(pane_id, border_style, mut completion_tx) => {
|
||||
if !screen.set_pane_border_style(pane_id, border_style) {
|
||||
log::error!("Pane with id {:?} not found", pane_id);
|
||||
if let Some(c) = completion_tx.as_mut() {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!("Pane with id {:?} not found", pane_id));
|
||||
}
|
||||
}
|
||||
let _ = screen.render(None);
|
||||
},
|
||||
ScreenInstruction::GroupAndUngroupPanes(
|
||||
pane_ids_to_group,
|
||||
pane_ids_to_ungroup,
|
||||
@@ -12322,7 +12519,10 @@ pub(crate) fn screen_thread_main(
|
||||
} else {
|
||||
screen
|
||||
.background_plugin_subscriptions
|
||||
.insert((plugin_id, client_id), subscriptions);
|
||||
.entry((plugin_id, client_id))
|
||||
.or_default()
|
||||
.extend(subscriptions);
|
||||
screen.report_pane_and_tab_state_to_plugin(plugin_id, client_id)?;
|
||||
}
|
||||
},
|
||||
ScreenInstruction::ClearHintTextCache => {
|
||||
@@ -12806,6 +13006,60 @@ pub(crate) fn screen_thread_main(
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::ApplyTiledSwapLayoutWithTabId(
|
||||
tab_id,
|
||||
layout_name,
|
||||
mut _completion_tx,
|
||||
) => {
|
||||
if let Some(tab) = screen.tabs.get_mut(&tab_id) {
|
||||
let applied = tab.apply_tiled_swap_layout(&layout_name)?;
|
||||
if !applied {
|
||||
log::error!("Tiled swap layout not found or incompatible: {layout_name}");
|
||||
if let Some(ref mut c) = _completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!(
|
||||
"Tiled swap layout not found or incompatible: {layout_name}"
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Tab with id {} not found", tab_id);
|
||||
if let Some(ref mut c) = _completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!("Tab with id {} not found", tab_id));
|
||||
}
|
||||
}
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::ApplyFloatingSwapLayoutWithTabId(
|
||||
tab_id,
|
||||
layout_name,
|
||||
mut _completion_tx,
|
||||
) => {
|
||||
if let Some(tab) = screen.tabs.get_mut(&tab_id) {
|
||||
let applied = tab.apply_floating_swap_layout(&layout_name)?;
|
||||
if !applied {
|
||||
log::error!(
|
||||
"Floating swap layout not found or incompatible: {layout_name}"
|
||||
);
|
||||
if let Some(ref mut c) = _completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!(
|
||||
"Floating swap layout not found or incompatible: {layout_name}"
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
log::error!("Tab with id {} not found", tab_id);
|
||||
if let Some(ref mut c) = _completion_tx {
|
||||
c.set_exit_status(1);
|
||||
c.set_error_message(format!("Tab with id {} not found", tab_id));
|
||||
}
|
||||
}
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
},
|
||||
ScreenInstruction::MoveTabWithTabId(tab_id, direction, _completion_tx) => {
|
||||
if pending_tab_ids.is_empty() {
|
||||
screen.move_tab_by_id(tab_id, direction)?;
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use zellij_utils::common_path::common_path_all;
|
||||
use zellij_utils::pane_size::PaneGeom;
|
||||
use zellij_utils::{
|
||||
data::{LayoutMetadata, PaneMetadata, TabMetadata},
|
||||
data::{BorderStyleOverride, LayoutMetadata, PaneMetadata, TabMetadata},
|
||||
input::command::RunCommand,
|
||||
input::layout::{Layout, Run, RunPlugin, RunPluginOrAlias},
|
||||
input::plugins::PluginAliases,
|
||||
@@ -535,6 +535,7 @@ impl Into<PaneLayoutManifest> for PaneLayoutMetadata {
|
||||
pane_contents: self.pane_contents,
|
||||
default_fg: self.default_fg,
|
||||
default_bg: self.default_bg,
|
||||
border_style: self.border_style.none_if_empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -561,6 +562,7 @@ pub struct PaneLayoutMetadata {
|
||||
focused_clients: Vec<ClientId>,
|
||||
default_fg: Option<String>,
|
||||
default_bg: Option<String>,
|
||||
border_style: BorderStyleOverride,
|
||||
}
|
||||
|
||||
impl PaneLayoutMetadata {
|
||||
@@ -575,6 +577,7 @@ impl PaneLayoutMetadata {
|
||||
focused_clients: Vec<ClientId>,
|
||||
default_fg: Option<String>,
|
||||
default_bg: Option<String>,
|
||||
border_style: BorderStyleOverride,
|
||||
) -> Self {
|
||||
PaneLayoutMetadata {
|
||||
id,
|
||||
@@ -588,6 +591,7 @@ impl PaneLayoutMetadata {
|
||||
focused_clients,
|
||||
default_fg,
|
||||
default_bg,
|
||||
border_style,
|
||||
}
|
||||
}
|
||||
fn to_pane_metadata(&self) -> PaneMetadata {
|
||||
@@ -664,13 +668,15 @@ impl ClientMetadata {
|
||||
default_editor: &Option<PathBuf>,
|
||||
) -> String {
|
||||
let mut lines = vec![];
|
||||
lines.push(String::from("CLIENT_ID ZELLIJ_PANE_ID RUNNING_COMMAND"));
|
||||
lines.push(format!(
|
||||
"{0: <10} {1: <14} {2}",
|
||||
"CLIENT_ID", "ZELLIJ_PANE_ID", "RUNNING_COMMAND"
|
||||
));
|
||||
|
||||
for (client_id, client_metadata) in clients_metadata.iter() {
|
||||
// 9 - CLIENT_ID, 14 - ZELLIJ_PANE_ID, 15 - RUNNING_COMMAND
|
||||
lines.push(format!(
|
||||
"{} {} {}",
|
||||
format!("{0: <9}", client_id),
|
||||
format!("{0: <10}", client_id),
|
||||
format!("{0: <14}", client_metadata.stringify_pane_id()),
|
||||
format!(
|
||||
"{0: <15}",
|
||||
@@ -701,6 +707,7 @@ mod tests {
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -720,6 +727,7 @@ mod tests {
|
||||
vec![],
|
||||
None,
|
||||
None,
|
||||
Default::default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -552,6 +552,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
}
|
||||
|
||||
new_plugin.set_borderless(layout.borderless.unwrap_or(false));
|
||||
new_plugin.set_border_style_override(layout.border_style.unwrap_or_default());
|
||||
if let Some(exclude_from_sync) = layout.exclude_from_sync {
|
||||
new_plugin.set_exclude_from_sync(exclude_from_sync);
|
||||
}
|
||||
@@ -605,6 +606,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
} else {
|
||||
new_pane.set_borderless(false);
|
||||
}
|
||||
new_pane.set_border_style_override(floating_pane_layout.border_style.unwrap_or_default());
|
||||
resize_pty!(
|
||||
new_pane,
|
||||
self.os_api,
|
||||
@@ -662,6 +664,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
} else {
|
||||
new_pane.set_borderless(false);
|
||||
}
|
||||
new_pane.set_border_style_override(floating_pane_layout.border_style.unwrap_or_default());
|
||||
if floating_pane_layout.default_fg.is_some() || floating_pane_layout.default_bg.is_some() {
|
||||
new_pane.set_pane_default_colors(
|
||||
floating_pane_layout.default_fg.clone(),
|
||||
@@ -735,6 +738,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
new_pane.handle_pty_bytes("\n\r".as_bytes().into());
|
||||
}
|
||||
new_pane.set_borderless(layout.borderless.unwrap_or(false));
|
||||
new_pane.set_border_style_override(layout.border_style.unwrap_or_default());
|
||||
if let Some(exclude_from_sync) = layout.exclude_from_sync {
|
||||
new_pane.set_exclude_from_sync(exclude_from_sync);
|
||||
}
|
||||
@@ -764,6 +768,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
layout.run,
|
||||
position_and_size,
|
||||
layout.borderless,
|
||||
layout.border_style,
|
||||
);
|
||||
found_exact_match = true;
|
||||
}
|
||||
@@ -784,6 +789,7 @@ impl<'a> LayoutApplier<'a> {
|
||||
run_instruction.clone(),
|
||||
position_and_size,
|
||||
layout.borderless,
|
||||
layout.border_style,
|
||||
);
|
||||
found_empty_location = true;
|
||||
}
|
||||
@@ -1331,6 +1337,9 @@ impl<'a> PaneApplier<'a> {
|
||||
if let Some(should_be_borderless) = floating_panes_layout.borderless {
|
||||
pane.set_borderless(should_be_borderless);
|
||||
}
|
||||
if let Some(border_style) = floating_panes_layout.border_style {
|
||||
pane.set_border_style_override(border_style);
|
||||
}
|
||||
self.apply_position_and_size_to_floating_pane(pane, position_and_size);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1410,6 +1419,9 @@ impl<'a> PaneApplier<'a> {
|
||||
if let Some(should_be_borderless) = layout.borderless {
|
||||
pane.set_borderless(should_be_borderless);
|
||||
}
|
||||
if let Some(border_style) = layout.border_style {
|
||||
pane.set_border_style_override(border_style);
|
||||
}
|
||||
if let Some(pane_title) = layout.name.as_ref() {
|
||||
pane.set_title(pane_title.into());
|
||||
}
|
||||
|
||||
+128
-11
@@ -17,8 +17,9 @@ use unicode_width::UnicodeWidthStr;
|
||||
use uuid::Uuid;
|
||||
use zellij_utils::data::PaneContents;
|
||||
use zellij_utils::data::{
|
||||
Direction, KeyWithModifier, NewPanePlacement, PaneInfo, PermissionStatus, PermissionType,
|
||||
PluginPermission, RegexHighlight, ResizeStrategy, Style, StyledText, WebSharing,
|
||||
BorderStyle, BorderStyleOverride, Direction, KeyWithModifier, NewPanePlacement, PaneInfo,
|
||||
PermissionStatus, PermissionType, PluginPermission, RegexHighlight, ResizeStrategy, Style,
|
||||
StyledText, WebSharing,
|
||||
};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::command::RunCommand;
|
||||
@@ -267,6 +268,7 @@ pub(crate) struct Tab {
|
||||
word_separators: String,
|
||||
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
// the below are the configured values - the ones that will be set if and when the web server
|
||||
// is brought online
|
||||
web_server_ip: IpAddr,
|
||||
@@ -627,6 +629,10 @@ pub trait Pane {
|
||||
fn load_pane_name(&mut self);
|
||||
fn set_borderless(&mut self, borderless: bool);
|
||||
fn borderless(&self) -> bool;
|
||||
fn set_border_style_override(&mut self, _border_style: BorderStyleOverride) {}
|
||||
fn border_style_override(&self) -> BorderStyleOverride {
|
||||
BorderStyleOverride::default()
|
||||
}
|
||||
fn set_exclude_from_sync(&mut self, exclude_from_sync: bool);
|
||||
fn exclude_from_sync(&self) -> bool;
|
||||
|
||||
@@ -754,6 +760,7 @@ pub trait Pane {
|
||||
fn update_kitty_host_support(&mut self, _supported: KittyHostSupport) {}
|
||||
fn update_sixel_host_support(&mut self, _supported: bool) {}
|
||||
fn update_rounded_corners(&mut self, _rounded_corners: bool) {}
|
||||
fn invalidate_frame_cache(&mut self) {}
|
||||
fn set_should_be_suppressed(&mut self, _should_be_suppressed: bool) {}
|
||||
fn query_should_be_suppressed(&self) -> bool {
|
||||
false
|
||||
@@ -865,6 +872,7 @@ impl Tab {
|
||||
pane_frame_style: PaneFrameStyle,
|
||||
auto_layout: bool,
|
||||
connected_clients_in_app: Rc<RefCell<HashMap<ClientId, bool>>>, // bool -> is_web_client
|
||||
client_display_slots: Rc<RefCell<HashMap<ClientId, usize>>>,
|
||||
session_is_mirrored: bool,
|
||||
client_id: Option<ClientId>,
|
||||
copy_options: CopyOptions,
|
||||
@@ -921,6 +929,7 @@ impl Tab {
|
||||
viewport.clone(),
|
||||
connected_clients.clone(),
|
||||
connected_clients_in_app.clone(),
|
||||
client_display_slots.clone(),
|
||||
mode_info.clone(),
|
||||
character_cell_size.clone(),
|
||||
stacked_resize.clone(),
|
||||
@@ -938,6 +947,7 @@ impl Tab {
|
||||
viewport.clone(),
|
||||
connected_clients.clone(),
|
||||
connected_clients_in_app.clone(),
|
||||
client_display_slots.clone(),
|
||||
mode_info.clone(),
|
||||
character_cell_size.clone(),
|
||||
fullscreen_covers_ui.clone(),
|
||||
@@ -1031,6 +1041,7 @@ impl Tab {
|
||||
osc133_command_selection: true,
|
||||
word_separators: DEFAULT_WORD_SEPARATORS.to_owned(),
|
||||
connected_clients_in_app,
|
||||
client_display_slots,
|
||||
web_server_ip,
|
||||
web_server_port,
|
||||
panes_with_pending_bell: HashSet::new(),
|
||||
@@ -1684,6 +1695,7 @@ impl Tab {
|
||||
self.mouse_scroll_resize,
|
||||
self.mouse_hover_tips,
|
||||
self.dimmed_clients.clone(),
|
||||
&self.client_display_slots.borrow(),
|
||||
);
|
||||
pane_contents_and_ui.set_frame_geom_override(Some(header_geom));
|
||||
pane_contents_and_ui.set_stack_list_entry(
|
||||
@@ -1907,10 +1919,16 @@ impl Tab {
|
||||
}
|
||||
}
|
||||
fn relayout_floating_panes(&mut self, search_backwards: bool) -> Result<()> {
|
||||
if let Some(layout_candidate) = self
|
||||
let layout_candidate = self
|
||||
.swap_layouts
|
||||
.swap_floating_panes(&self.floating_panes, search_backwards)
|
||||
{
|
||||
.swap_floating_panes(&self.floating_panes, search_backwards);
|
||||
self.apply_floating_layout_candidate(layout_candidate)
|
||||
}
|
||||
fn apply_floating_layout_candidate(
|
||||
&mut self,
|
||||
layout_candidate: Option<Vec<FloatingPaneLayout>>,
|
||||
) -> Result<()> {
|
||||
if let Some(layout_candidate) = layout_candidate {
|
||||
LayoutApplier::new(
|
||||
&self.viewport,
|
||||
&self.senders,
|
||||
@@ -1949,10 +1967,16 @@ impl Tab {
|
||||
self.tiled_panes.unset_fullscreen();
|
||||
}
|
||||
self.dissolve_stack_lists_for_classic_mutation();
|
||||
if let Some(layout_candidate) = self
|
||||
let layout_candidate = self
|
||||
.swap_layouts
|
||||
.swap_tiled_panes(&self.tiled_panes, search_backwards)
|
||||
{
|
||||
.swap_tiled_panes(&self.tiled_panes, search_backwards);
|
||||
self.apply_tiled_layout_candidate(layout_candidate)
|
||||
}
|
||||
fn apply_tiled_layout_candidate(
|
||||
&mut self,
|
||||
layout_candidate: Option<TiledPaneLayout>,
|
||||
) -> Result<()> {
|
||||
if let Some(layout_candidate) = layout_candidate {
|
||||
let application_res = LayoutApplier::new(
|
||||
&self.viewport,
|
||||
&self.senders,
|
||||
@@ -1995,6 +2019,46 @@ impl Tab {
|
||||
.with_context(|| format!("failed to apply cached resizes"))?;
|
||||
Ok(())
|
||||
}
|
||||
fn settled_tiled_pane_count(&self) -> usize {
|
||||
let mut pane_count = self.tiled_panes.visible_panes_count();
|
||||
if self.tiled_panes.fullscreen_is_active() {
|
||||
pane_count += self.tiled_panes.panes_to_hide_count();
|
||||
}
|
||||
if self.stacked_pane_list_is_active() {
|
||||
pane_count += self.suppressed_stack_list_members().count();
|
||||
}
|
||||
pane_count
|
||||
}
|
||||
pub fn apply_tiled_swap_layout(&mut self, layout_name: &str) -> Result<bool> {
|
||||
let Some((position, layout_candidate)) = self
|
||||
.swap_layouts
|
||||
.tiled_layout_candidate_by_name(layout_name, self.settled_tiled_pane_count())
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
if self.tiled_panes.fullscreen_is_active() {
|
||||
self.tiled_panes.unset_fullscreen();
|
||||
}
|
||||
self.dissolve_stack_lists_for_classic_mutation();
|
||||
self.swap_layouts
|
||||
.set_current_tiled_layout_position(position);
|
||||
self.apply_tiled_layout_candidate(Some(layout_candidate))?;
|
||||
Ok(true)
|
||||
}
|
||||
pub fn apply_floating_swap_layout(&mut self, layout_name: &str) -> Result<bool> {
|
||||
let Some((position, layout_candidate)) =
|
||||
self.swap_layouts.floating_layout_candidate_by_name(
|
||||
layout_name,
|
||||
self.floating_panes.visible_panes_count(),
|
||||
)
|
||||
else {
|
||||
return Ok(false);
|
||||
};
|
||||
self.swap_layouts
|
||||
.set_current_floating_layout_position(position);
|
||||
self.apply_floating_layout_candidate(Some(layout_candidate))?;
|
||||
Ok(true)
|
||||
}
|
||||
pub fn previous_swap_layout(&mut self) -> Result<()> {
|
||||
let search_backwards = true;
|
||||
if self.floating_panes.panes_are_visible() {
|
||||
@@ -2283,7 +2347,15 @@ impl Tab {
|
||||
Ok(())
|
||||
}
|
||||
pub fn remove_client(&mut self, client_id: ClientId) {
|
||||
self.focus_pane_id = None;
|
||||
let is_connected_to_this_tab = self.connected_clients.borrow().contains(&client_id);
|
||||
if is_connected_to_this_tab {
|
||||
let is_last_connected_client = self.connected_clients.borrow().len() == 1;
|
||||
self.focus_pane_id = if is_last_connected_client {
|
||||
self.tiled_panes.focused_pane_id(client_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
}
|
||||
self.mode_info
|
||||
.borrow_mut()
|
||||
.get_mut(&client_id)
|
||||
@@ -2483,8 +2555,9 @@ impl Tab {
|
||||
blocking_notification: Option<NotificationEnd>,
|
||||
) -> Result<()> {
|
||||
let invoked_with = self.normalize_invoked_with_for_default_shell(invoked_with);
|
||||
match new_pane_placement {
|
||||
NewPanePlacement::NoPreference { borderless } => self.new_no_preference_pane(
|
||||
let border_style = new_pane_placement.get_border_style();
|
||||
let result = match new_pane_placement {
|
||||
NewPanePlacement::NoPreference { borderless, .. } => self.new_no_preference_pane(
|
||||
pid,
|
||||
initial_pane_title,
|
||||
invoked_with,
|
||||
@@ -2497,6 +2570,7 @@ impl Tab {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless,
|
||||
..
|
||||
} => self.new_tiled_pane(
|
||||
pid,
|
||||
initial_pane_title,
|
||||
@@ -2510,6 +2584,7 @@ impl Tab {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: Some(direction),
|
||||
borderless,
|
||||
..
|
||||
} => {
|
||||
let is_vertical = direction == Direction::Left || direction == Direction::Right;
|
||||
let focused_client_id = client_id.filter(|_| should_focus_pane);
|
||||
@@ -2588,6 +2663,7 @@ impl Tab {
|
||||
pane_id_to_replace,
|
||||
close_replaced_pane,
|
||||
borderless,
|
||||
..
|
||||
} => self.new_in_place_pane(
|
||||
pid,
|
||||
initial_pane_title,
|
||||
@@ -2601,6 +2677,7 @@ impl Tab {
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under,
|
||||
borderless,
|
||||
..
|
||||
} => self.new_stacked_pane(
|
||||
pid,
|
||||
initial_pane_title,
|
||||
@@ -2612,7 +2689,11 @@ impl Tab {
|
||||
blocking_notification,
|
||||
borderless,
|
||||
),
|
||||
};
|
||||
if let Some(border_style) = border_style {
|
||||
self.set_pane_border_style(pid, border_style);
|
||||
}
|
||||
result
|
||||
}
|
||||
pub fn new_no_preference_pane(
|
||||
&mut self,
|
||||
@@ -7558,6 +7639,21 @@ impl Tab {
|
||||
pane.update_rounded_corners(rounded_corners);
|
||||
}
|
||||
}
|
||||
pub fn update_border_styles(
|
||||
&mut self,
|
||||
border_style: BorderStyle,
|
||||
floating_border_style: BorderStyle,
|
||||
) {
|
||||
self.style.border_style = border_style;
|
||||
self.style.floating_border_style = floating_border_style;
|
||||
self.floating_panes
|
||||
.update_border_styles(border_style, floating_border_style);
|
||||
self.tiled_panes
|
||||
.update_border_styles(border_style, floating_border_style);
|
||||
for (_, pane) in self.suppressed_panes.values_mut() {
|
||||
pane.invalidate_frame_cache();
|
||||
}
|
||||
}
|
||||
pub fn update_arrow_fonts(&mut self, should_support_arrow_fonts: bool) {
|
||||
self.arrow_fonts = should_support_arrow_fonts;
|
||||
self.floating_panes
|
||||
@@ -7833,6 +7929,27 @@ impl Tab {
|
||||
log::error!("Pane with id {:?} not found", pane_id);
|
||||
Ok(())
|
||||
}
|
||||
pub fn set_pane_border_style(
|
||||
&mut self,
|
||||
pane_id: PaneId,
|
||||
border_style: BorderStyleOverride,
|
||||
) -> bool {
|
||||
if let Some(pane) = self.floating_panes.get_pane_mut(pane_id) {
|
||||
pane.set_border_style_override(border_style);
|
||||
self.set_force_render();
|
||||
return true;
|
||||
}
|
||||
if let Some(pane) = self.tiled_panes.get_pane_mut(pane_id) {
|
||||
pane.set_border_style_override(border_style);
|
||||
self.set_force_render();
|
||||
return true;
|
||||
}
|
||||
if let Some(pane) = self.suppressed_panes.get_mut(&pane_id) {
|
||||
pane.1.set_border_style_override(border_style);
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
pub fn get_viewport(&self) -> Viewport {
|
||||
self.viewport.borrow().clone()
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ impl SwapLayouts {
|
||||
if self.swap_floating_layouts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let pane_count = floating_panes.visible_panes_count();
|
||||
let initial_position = self.current_floating_layout_position;
|
||||
|
||||
macro_rules! progress_layout {
|
||||
@@ -161,10 +162,8 @@ impl SwapLayouts {
|
||||
.nth(self.current_floating_layout_position)
|
||||
{
|
||||
Some(swap_layout) => {
|
||||
for (constraint, layout) in swap_layout.0.iter() {
|
||||
if self.state_fits_floating_panes_constraint(constraint, floating_panes) {
|
||||
return Some(layout.clone());
|
||||
};
|
||||
if let Some(layout) = self.floating_layout_candidate(swap_layout, pane_count) {
|
||||
return Some(layout);
|
||||
}
|
||||
progress_layout!();
|
||||
},
|
||||
@@ -178,41 +177,87 @@ impl SwapLayouts {
|
||||
}
|
||||
None
|
||||
}
|
||||
fn state_fits_tiled_panes_constraint(
|
||||
&self,
|
||||
constraint: &LayoutConstraint,
|
||||
tiled_panes: &TiledPanes,
|
||||
) -> bool {
|
||||
fn state_fits_pane_count_constraint(constraint: &LayoutConstraint, pane_count: usize) -> bool {
|
||||
match constraint {
|
||||
LayoutConstraint::MaxPanes(max_panes) => {
|
||||
tiled_panes.visible_panes_count() <= *max_panes
|
||||
},
|
||||
LayoutConstraint::MinPanes(min_panes) => {
|
||||
tiled_panes.visible_panes_count() >= *min_panes
|
||||
},
|
||||
LayoutConstraint::ExactPanes(pane_count) => {
|
||||
tiled_panes.visible_panes_count() == *pane_count
|
||||
},
|
||||
LayoutConstraint::MaxPanes(max_panes) => pane_count <= *max_panes,
|
||||
LayoutConstraint::MinPanes(min_panes) => pane_count >= *min_panes,
|
||||
LayoutConstraint::ExactPanes(exact_panes) => pane_count == *exact_panes,
|
||||
LayoutConstraint::NoConstraint => true,
|
||||
}
|
||||
}
|
||||
fn state_fits_floating_panes_constraint(
|
||||
fn tiled_layout_candidate(
|
||||
&self,
|
||||
constraint: &LayoutConstraint,
|
||||
floating_panes: &FloatingPanes,
|
||||
) -> bool {
|
||||
match constraint {
|
||||
LayoutConstraint::MaxPanes(max_panes) => {
|
||||
floating_panes.visible_panes_count() <= *max_panes
|
||||
},
|
||||
LayoutConstraint::MinPanes(min_panes) => {
|
||||
floating_panes.visible_panes_count() >= *min_panes
|
||||
},
|
||||
LayoutConstraint::ExactPanes(pane_count) => {
|
||||
floating_panes.visible_panes_count() == *pane_count
|
||||
},
|
||||
LayoutConstraint::NoConstraint => true,
|
||||
swap_layout: &SwapTiledLayout,
|
||||
pane_count: usize,
|
||||
) -> Option<TiledPaneLayout> {
|
||||
for (constraint, layout) in swap_layout.0.iter() {
|
||||
if Self::state_fits_pane_count_constraint(constraint, pane_count) {
|
||||
let focus_layout_if_not_focused = true;
|
||||
let display_area = self.display_area.borrow();
|
||||
// TODO: reuse the assets from position_panes_in_space here?
|
||||
let display_area = PaneGeom::from(&*display_area);
|
||||
if layout
|
||||
.position_panes_in_space(
|
||||
&display_area,
|
||||
Some(pane_count),
|
||||
false,
|
||||
focus_layout_if_not_focused,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some(layout.clone());
|
||||
}
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
fn floating_layout_candidate(
|
||||
&self,
|
||||
swap_layout: &SwapFloatingLayout,
|
||||
pane_count: usize,
|
||||
) -> Option<Vec<FloatingPaneLayout>> {
|
||||
for (constraint, layout) in swap_layout.0.iter() {
|
||||
if Self::state_fits_pane_count_constraint(constraint, pane_count) {
|
||||
return Some(layout.clone());
|
||||
};
|
||||
}
|
||||
None
|
||||
}
|
||||
pub fn tiled_layout_candidate_by_name(
|
||||
&self,
|
||||
layout_name: &str,
|
||||
pane_count: usize,
|
||||
) -> Option<(usize, TiledPaneLayout)> {
|
||||
self.swap_tiled_layouts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, layout)| layout.1.as_deref() == Some(layout_name))
|
||||
.find_map(|(position, layout)| {
|
||||
self.tiled_layout_candidate(layout, pane_count)
|
||||
.map(|layout| (position, layout))
|
||||
})
|
||||
}
|
||||
pub fn floating_layout_candidate_by_name(
|
||||
&self,
|
||||
layout_name: &str,
|
||||
pane_count: usize,
|
||||
) -> Option<(usize, Vec<FloatingPaneLayout>)> {
|
||||
self.swap_floating_layouts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, layout)| layout.1.as_deref() == Some(layout_name))
|
||||
.find_map(|(position, layout)| {
|
||||
self.floating_layout_candidate(layout, pane_count)
|
||||
.map(|layout| (position, layout))
|
||||
})
|
||||
}
|
||||
pub fn set_current_tiled_layout_position(&mut self, position: usize) {
|
||||
self.current_tiled_layout_position = position;
|
||||
self.is_tiled_damaged = false;
|
||||
}
|
||||
pub fn set_current_floating_layout_position(&mut self, position: usize) {
|
||||
self.current_floating_layout_position = position;
|
||||
self.is_floating_damaged = false;
|
||||
}
|
||||
pub fn swap_tiled_panes(
|
||||
&mut self,
|
||||
@@ -222,6 +267,7 @@ impl SwapLayouts {
|
||||
if self.swap_tiled_layouts.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let pane_count = tiled_panes.visible_panes_count();
|
||||
|
||||
macro_rules! progress_layout {
|
||||
() => {{
|
||||
@@ -256,25 +302,8 @@ impl SwapLayouts {
|
||||
.nth(self.current_tiled_layout_position)
|
||||
{
|
||||
Some(swap_layout) => {
|
||||
for (constraint, layout) in swap_layout.0.iter() {
|
||||
if self.state_fits_tiled_panes_constraint(constraint, tiled_panes) {
|
||||
let focus_layout_if_not_focused = true;
|
||||
let display_area = self.display_area.borrow();
|
||||
// TODO: reuse the assets from position_panes_in_space here?
|
||||
let pane_count = tiled_panes.visible_panes_count();
|
||||
let display_area = PaneGeom::from(&*display_area);
|
||||
if layout
|
||||
.position_panes_in_space(
|
||||
&display_area,
|
||||
Some(pane_count),
|
||||
false,
|
||||
focus_layout_if_not_focused,
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some(layout.clone());
|
||||
}
|
||||
};
|
||||
if let Some(layout) = self.tiled_layout_candidate(swap_layout, pane_count) {
|
||||
return Some(layout);
|
||||
}
|
||||
progress_layout!();
|
||||
},
|
||||
|
||||
@@ -75,19 +75,18 @@ impl ServerOsApi for FakeInputOutput {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -199,6 +198,7 @@ fn create_layout_applier_fixtures(
|
||||
viewport.clone(),
|
||||
connected_clients_set.clone(),
|
||||
connected_clients.clone(),
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
mode_info.clone(),
|
||||
character_cell_size.clone(),
|
||||
stacked_resize,
|
||||
@@ -218,6 +218,7 @@ fn create_layout_applier_fixtures(
|
||||
viewport.clone(),
|
||||
connected_clients_set,
|
||||
connected_clients.clone(),
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
mode_info,
|
||||
character_cell_size.clone(),
|
||||
fullscreen_covers_ui,
|
||||
@@ -335,6 +336,7 @@ fn create_layout_applier_fixtures_with_receivers(
|
||||
viewport.clone(),
|
||||
connected_clients_set.clone(),
|
||||
connected_clients.clone(),
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
mode_info.clone(),
|
||||
character_cell_size.clone(),
|
||||
stacked_resize,
|
||||
@@ -354,6 +356,7 @@ fn create_layout_applier_fixtures_with_receivers(
|
||||
viewport.clone(),
|
||||
connected_clients_set,
|
||||
connected_clients.clone(),
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
mode_info,
|
||||
character_cell_size.clone(),
|
||||
fullscreen_covers_ui,
|
||||
|
||||
+3
-4
@@ -1,12 +1,12 @@
|
||||
---
|
||||
source: zellij-server/src/tab/./unit/tab_integration_tests.rs
|
||||
assertion_line: 3253
|
||||
assertion_line: 5784
|
||||
expression: snapshot
|
||||
---
|
||||
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
01 (C): ┌ Pane #2 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
02 (C): ┌ Pane #3 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
03 (C): ┌ Pane #4 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
02 (C): ┌ Pane #4 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
03 (C): ┌ Pane #5 ──────────────────────────────────────────────────────────────────────────────────────────────────────────────┐
|
||||
04 (C): │ │
|
||||
05 (C): │ │
|
||||
06 (C): │ │
|
||||
@@ -23,4 +23,3 @@ expression: snapshot
|
||||
17 (C): │ │
|
||||
18 (C): │ │
|
||||
19 (C): └───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
|
||||
|
||||
|
||||
@@ -14,10 +14,10 @@ use std::path::PathBuf;
|
||||
use std::sync::Mutex;
|
||||
|
||||
use zellij_utils::channels::Receiver;
|
||||
use zellij_utils::data::Direction;
|
||||
use zellij_utils::data::Resize;
|
||||
use zellij_utils::data::ResizeStrategy;
|
||||
use zellij_utils::data::WebSharing;
|
||||
use zellij_utils::data::{BorderStyle, BorderStyleOverride, Direction, LineStyle};
|
||||
use zellij_utils::envs::set_session_name;
|
||||
use zellij_utils::errors::{prelude::*, ErrorContext};
|
||||
use zellij_utils::input::layout::{
|
||||
@@ -94,19 +94,18 @@ impl ServerOsApi for FakeInputOutput {
|
||||
fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client(&mut self, _client_id: ClientId) -> Result<()> {
|
||||
@@ -259,6 +258,7 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -357,6 +357,7 @@ fn create_new_tab_with_stacked_pane_list(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -451,6 +452,7 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -562,6 +564,7 @@ fn create_new_tab_with_swap_layouts(
|
||||
},
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -670,6 +673,7 @@ fn create_new_tab_with_os_api(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -764,6 +768,7 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -872,6 +877,7 @@ fn create_new_tab_with_mock_pty_writer(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -971,6 +977,7 @@ fn create_new_tab_with_sixel_support(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -1558,6 +1565,7 @@ fn new_stacked_pane() {
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -1597,6 +1605,7 @@ fn kitty_visible_panes_exclude_collapsed_stack_members() {
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -5157,7 +5166,7 @@ fn pane_bracketed_paste_ignored_when_not_in_bracketed_paste_mode() {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let client_id: u16 = 1;
|
||||
let client_id: ClientId = 1;
|
||||
|
||||
let mut pty_instruction_bus = MockPtyInstructionBus::new();
|
||||
let mut tab = create_new_tab_with_mock_pty_writer(
|
||||
@@ -5187,7 +5196,7 @@ fn pane_faux_scrolling_in_alternate_mode() {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let client_id: u16 = 1;
|
||||
let client_id: ClientId = 1;
|
||||
let lines_to_scroll = 3;
|
||||
|
||||
let mut pty_instruction_bus = MockPtyInstructionBus::new();
|
||||
@@ -5525,6 +5534,180 @@ fn can_swap_tiled_layout_at_runtime() {
|
||||
assert_snapshot!(snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_apply_named_tiled_layout_while_floating_pane_is_visible() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let client_id = 1;
|
||||
let swap_layouts = r#"
|
||||
layout {
|
||||
swap_tiled_layout name="vertical" {
|
||||
tab max_panes=2 split_direction="vertical" {
|
||||
pane
|
||||
pane
|
||||
}
|
||||
}
|
||||
swap_tiled_layout name="horizontal" {
|
||||
tab max_panes=2 {
|
||||
pane
|
||||
pane
|
||||
}
|
||||
}
|
||||
swap_tiled_layout name="one-pane" {
|
||||
tab max_panes=1 {
|
||||
pane
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap();
|
||||
let mut tab = create_new_tab_with_swap_layouts(
|
||||
size,
|
||||
ModeInfo::default(),
|
||||
(
|
||||
layout.swap_tiled_layouts.clone(),
|
||||
layout.swap_floating_layouts.clone(),
|
||||
),
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
tab.new_pane(
|
||||
PaneId::Terminal(2),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
PaneId::Terminal(3),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let focused_floating_pane = tab.get_active_pane_id(client_id);
|
||||
let initial_layout = tab.swap_layout_info();
|
||||
|
||||
for unavailable_layout in ["missing", "one-pane"] {
|
||||
assert!(!tab.apply_tiled_swap_layout(unavailable_layout).unwrap());
|
||||
assert_eq!(tab.swap_layout_info(), initial_layout);
|
||||
assert!(tab.are_floating_panes_visible());
|
||||
assert_eq!(tab.get_active_pane_id(client_id), focused_floating_pane);
|
||||
}
|
||||
|
||||
assert!(tab.apply_tiled_swap_layout("horizontal").unwrap());
|
||||
assert!(tab.are_floating_panes_visible());
|
||||
assert_eq!(tab.get_active_pane_id(client_id), focused_floating_pane);
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
tab.swap_layout_info(),
|
||||
(Some("horizontal".to_owned()), false)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_apply_named_floating_layout_while_floating_panes_are_hidden() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let client_id = 1;
|
||||
let swap_layouts = r#"
|
||||
layout {
|
||||
swap_floating_layout name="staggered" {
|
||||
floating_panes max_panes=2 {
|
||||
pane
|
||||
pane
|
||||
}
|
||||
}
|
||||
swap_floating_layout name="centered" {
|
||||
floating_panes max_panes=2 {
|
||||
pane x="10%" y="10%"
|
||||
pane x="20%" y="20%"
|
||||
}
|
||||
}
|
||||
swap_floating_layout name="one-pane" {
|
||||
floating_panes max_panes=1 {
|
||||
pane
|
||||
}
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let layout = Layout::from_kdl(swap_layouts, Some("file_name.kdl".into()), None, None).unwrap();
|
||||
let mut tab = create_new_tab_with_swap_layouts(
|
||||
size,
|
||||
ModeInfo::default(),
|
||||
(
|
||||
layout.swap_tiled_layouts.clone(),
|
||||
layout.swap_floating_layouts.clone(),
|
||||
),
|
||||
None,
|
||||
true,
|
||||
true,
|
||||
);
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
PaneId::Terminal(2),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
PaneId::Terminal(3),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
let focused_tiled_pane = tab.get_active_pane_id(client_id);
|
||||
let initial_layout = tab.swap_layout_info();
|
||||
|
||||
for unavailable_layout in ["missing", "one-pane"] {
|
||||
assert!(!tab.apply_floating_swap_layout(unavailable_layout).unwrap());
|
||||
assert_eq!(tab.swap_layout_info(), initial_layout);
|
||||
assert!(!tab.are_floating_panes_visible());
|
||||
assert_eq!(tab.get_active_pane_id(client_id), focused_tiled_pane);
|
||||
}
|
||||
|
||||
assert!(tab.apply_floating_swap_layout("centered").unwrap());
|
||||
assert!(!tab.are_floating_panes_visible());
|
||||
assert_eq!(tab.swap_layout_info(), initial_layout);
|
||||
assert_eq!(tab.get_active_pane_id(client_id), focused_tiled_pane);
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
assert_eq!(tab.swap_layout_info(), (Some("centered".to_owned()), false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn can_swap_floating_layout_at_runtime() {
|
||||
let size = Size {
|
||||
@@ -5776,43 +5959,49 @@ fn swap_tiled_layout_with_only_stacked_children() {
|
||||
true,
|
||||
stacked_resize,
|
||||
);
|
||||
let new_pane_id_1 = PaneId::Terminal(2);
|
||||
let new_pane_id_2 = PaneId::Terminal(3);
|
||||
let new_pane_id_3 = PaneId::Terminal(4);
|
||||
let new_pane_ids = [2, 3, 4, 5].map(PaneId::Terminal);
|
||||
let open_pane = |tab: &mut Tab, pane_id| {
|
||||
tab.new_pane(
|
||||
pane_id,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
};
|
||||
let stack_order = |tab: &Tab| {
|
||||
let mut panes = tab
|
||||
.tiled_panes
|
||||
.get_panes()
|
||||
.filter_map(|(pane_id, pane)| {
|
||||
let geom = pane.current_geom();
|
||||
geom.is_stacked().then_some((geom.y, *pane_id))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
panes.sort_unstable();
|
||||
panes
|
||||
.into_iter()
|
||||
.map(|(_, pane_id)| pane_id)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
tab.new_pane(
|
||||
new_pane_id_1,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
new_pane_id_2,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
new_pane_id_3,
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
for pane_id in &new_pane_ids[..3] {
|
||||
open_pane(&mut tab, *pane_id);
|
||||
}
|
||||
tab.move_focus_up(client_id).unwrap();
|
||||
open_pane(&mut tab, new_pane_ids[3]);
|
||||
let mut expected_order = vec![PaneId::Terminal(1)];
|
||||
expected_order.extend(new_pane_ids);
|
||||
assert_eq!(stack_order(&tab), expected_order);
|
||||
tab.close_pane(new_pane_ids[1], false, None);
|
||||
expected_order.remove(2);
|
||||
assert_eq!(stack_order(&tab), expected_order);
|
||||
tab.next_swap_layout().unwrap();
|
||||
assert_eq!(stack_order(&tab), expected_order);
|
||||
tab.render(&mut output, None).unwrap();
|
||||
let snapshot = take_snapshot(
|
||||
output.serialize().unwrap().get(&client_id).unwrap(),
|
||||
@@ -10663,6 +10852,7 @@ fn borderless_floating_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(true),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10710,6 +10900,7 @@ fn borderless_pane_content_fills_edges() {
|
||||
height: Some(PercentOrFixed::Fixed(5)),
|
||||
pinned: None,
|
||||
borderless: Some(true),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10758,6 +10949,7 @@ fn borderless_pinned_floating_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(8)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(true),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10810,6 +11002,7 @@ fn cursor_hidden_when_floating_pane_is_under_pinned_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: Some(false),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10831,6 +11024,7 @@ fn cursor_hidden_when_floating_pane_is_under_pinned_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(8)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10890,6 +11084,7 @@ fn cursor_visible_when_pinned_pane_is_focused() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: Some(false),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -10911,6 +11106,7 @@ fn cursor_visible_when_pinned_pane_is_focused() {
|
||||
height: Some(PercentOrFixed::Fixed(8)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_floating_pane(
|
||||
@@ -11352,6 +11548,7 @@ fn test_ctrl_drag_resizes_floating_pane_from_edge() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
@@ -11433,6 +11630,7 @@ fn test_ctrl_drag_resizes_floating_pane_from_corner() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
@@ -11514,6 +11712,7 @@ fn test_ctrl_drag_resizes_pinned_floating_pane_when_floating_panes_not_shown() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_pane(
|
||||
@@ -11777,6 +11976,7 @@ fn test_left_click_on_floating_pane_changes_focus() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
let coordinates_2 = FloatingPaneCoordinates {
|
||||
@@ -11786,6 +11986,7 @@ fn test_left_click_on_floating_pane_changes_focus() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
@@ -11848,6 +12049,7 @@ fn test_left_click_on_pinned_floating_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
let coordinates_2 = FloatingPaneCoordinates {
|
||||
@@ -11857,6 +12059,7 @@ fn test_left_click_on_pinned_floating_pane() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
@@ -13186,6 +13389,7 @@ fn test_ctrl_scroll_up_increases_pinned_floating_pane_size_when_floating_panes_h
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_pane(
|
||||
@@ -13254,6 +13458,7 @@ fn test_ctrl_scroll_down_decreases_pinned_floating_pane_size_when_floating_panes
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: Some(true),
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.new_pane(
|
||||
@@ -13526,6 +13731,7 @@ fn adding_stacked_pane_in_stack_list_mode_moves_existing_pane_to_suppressed_imme
|
||||
NewPanePlacement::Stacked {
|
||||
pane_id_to_stack_under: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -13753,6 +13959,7 @@ fn in_place_pane_with_close_replaced_pane_false_restores_original() {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -13816,6 +14023,7 @@ fn in_place_pane_with_close_replaced_pane_true_closes_original() {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -13921,6 +14129,7 @@ fn create_new_tab_with_plugin_receiver(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -14880,6 +15089,7 @@ fn focus_follows_mouse_focuses_floating_pane_on_hover() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
let coordinates_2 = FloatingPaneCoordinates {
|
||||
@@ -14889,6 +15099,7 @@ fn focus_follows_mouse_focuses_floating_pane_on_hover() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
@@ -15104,6 +15315,7 @@ fn focus_follows_mouse_ignores_tiled_pane_when_floating_visible() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -15154,6 +15366,7 @@ fn focus_follows_mouse_focuses_floating_pane_when_floating_visible() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -15172,6 +15385,7 @@ fn focus_follows_mouse_focuses_floating_pane_when_floating_visible() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -15706,6 +15920,7 @@ fn create_new_tab_with_server_receiver(
|
||||
PaneFrameStyle::Full,
|
||||
true, // auto_layout
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
true, // session_is_mirrored
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -16365,3 +16580,202 @@ fn host_focus_events_only_reach_panes_that_asked_for_them() {
|
||||
.map(|bytes| String::from_utf8_lossy(bytes).to_string());
|
||||
assert_eq!(written, Some("\u{1b}[O\u{1b}[I".to_owned()));
|
||||
}
|
||||
|
||||
fn border_style_override(all: LineStyle) -> BorderStyleOverride {
|
||||
BorderStyleOverride {
|
||||
all: Some(all),
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
fn uniform_border_style(line_style: LineStyle) -> BorderStyle {
|
||||
BorderStyle {
|
||||
top: line_style,
|
||||
right: line_style,
|
||||
bottom: line_style,
|
||||
left: line_style,
|
||||
rounded_corners: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tab(tab: &mut Tab, size: Size, client_id: ClientId) -> String {
|
||||
let mut output = Output::default();
|
||||
tab.render(&mut output, None).unwrap();
|
||||
take_snapshot(
|
||||
output.serialize().unwrap().get(&client_id).unwrap(),
|
||||
size.rows,
|
||||
size.cols,
|
||||
Palette::default(),
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_per_pane_border_style_is_drawn_on_the_pane_frame() {
|
||||
let size = Size { cols: 40, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.set_pane_border_style(
|
||||
PaneId::Terminal(1),
|
||||
border_style_override(LineStyle::Double),
|
||||
);
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('╔'), "{}", snapshot);
|
||||
assert!(snapshot.contains('║'), "{}", snapshot);
|
||||
assert!(snapshot.contains('╝'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_per_pane_border_style_reaches_a_floating_pane() {
|
||||
let size = Size { cols: 60, rows: 20 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.toggle_floating_panes(Some(client_id), None, None)
|
||||
.unwrap();
|
||||
tab.new_pane(
|
||||
PaneId::Terminal(2),
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
true,
|
||||
NewPanePlacement::default(),
|
||||
Some(client_id),
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
tab.set_pane_border_style(PaneId::Terminal(2), border_style_override(LineStyle::Heavy));
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('┏'), "{}", snapshot);
|
||||
assert!(snapshot.contains('┃'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn setting_a_border_style_on_an_unknown_pane_reports_failure() {
|
||||
let size = Size { cols: 40, rows: 10 };
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
assert!(tab.set_pane_border_style(
|
||||
PaneId::Terminal(1),
|
||||
border_style_override(LineStyle::Double)
|
||||
));
|
||||
assert!(!tab.set_pane_border_style(
|
||||
PaneId::Terminal(99),
|
||||
border_style_override(LineStyle::Double)
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_empty_border_style_override_restores_the_configured_default() {
|
||||
let size = Size { cols: 40, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.set_pane_border_style(
|
||||
PaneId::Terminal(1),
|
||||
border_style_override(LineStyle::Double),
|
||||
);
|
||||
assert!(render_tab(&mut tab, size, client_id).contains('╔'));
|
||||
tab.set_pane_border_style(PaneId::Terminal(1), BorderStyleOverride::default());
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(!snapshot.contains('╔'), "{}", snapshot);
|
||||
assert!(snapshot.contains('┌'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconfiguring_the_global_border_style_repaints_open_panes() {
|
||||
let size = Size { cols: 40, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
assert!(render_tab(&mut tab, size, client_id).contains('┌'));
|
||||
let heavy = uniform_border_style(LineStyle::Heavy);
|
||||
tab.update_border_styles(heavy, heavy);
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('┏'), "{}", snapshot);
|
||||
assert!(snapshot.contains('┛'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_per_pane_border_style_wins_over_the_global_one() {
|
||||
let size = Size { cols: 40, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
let heavy = uniform_border_style(LineStyle::Heavy);
|
||||
tab.update_border_styles(heavy, heavy);
|
||||
tab.set_pane_border_style(
|
||||
PaneId::Terminal(1),
|
||||
border_style_override(LineStyle::Double),
|
||||
);
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('╔'), "{}", snapshot);
|
||||
assert!(!snapshot.contains('┏'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn border_styles_from_a_layout_reach_the_panes() {
|
||||
let size = Size { cols: 60, rows: 20 };
|
||||
let client_id = 1;
|
||||
let layout = r#"
|
||||
layout {
|
||||
pane border_style="double"
|
||||
floating_panes {
|
||||
pane x=2 y=2 width=20 height=6 border_style="heavy"
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let mut tab = create_new_tab_with_layout(size, ModeInfo::default(), layout);
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('╔'), "{}", snapshot);
|
||||
assert!(snapshot.contains('┏'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_neighbours_share_a_styled_grid_line() {
|
||||
let size = Size { cols: 60, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.set_pane_frames(PaneFrameStyle::None);
|
||||
tab.vertical_split(PaneId::Terminal(2), None, client_id, None, None)
|
||||
.unwrap();
|
||||
tab.set_pane_border_style(
|
||||
PaneId::Terminal(1),
|
||||
border_style_override(LineStyle::Double),
|
||||
);
|
||||
tab.set_pane_border_style(
|
||||
PaneId::Terminal(2),
|
||||
border_style_override(LineStyle::Double),
|
||||
);
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('║'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_neighbours_fall_back_to_the_ambient_grid_line() {
|
||||
let size = Size { cols: 60, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.set_pane_frames(PaneFrameStyle::None);
|
||||
tab.vertical_split(PaneId::Terminal(2), None, client_id, None, None)
|
||||
.unwrap();
|
||||
let double = uniform_border_style(LineStyle::Double);
|
||||
tab.update_border_styles(double, double);
|
||||
tab.set_pane_border_style(PaneId::Terminal(2), border_style_override(LineStyle::Heavy));
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('║'), "{}", snapshot);
|
||||
assert!(!snapshot.contains('┃'), "{}", snapshot);
|
||||
assert!(!snapshot.contains('│'), "{}", snapshot);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mismatched_neighbours_fall_back_to_single_when_the_ambient_style_is_mixed() {
|
||||
let size = Size { cols: 60, rows: 10 };
|
||||
let client_id = 1;
|
||||
let mut tab = create_new_tab(size, ModeInfo::default());
|
||||
tab.set_pane_frames(PaneFrameStyle::None);
|
||||
tab.vertical_split(PaneId::Terminal(2), None, client_id, None, None)
|
||||
.unwrap();
|
||||
let mixed = BorderStyle {
|
||||
top: LineStyle::Double,
|
||||
..Default::default()
|
||||
};
|
||||
tab.update_border_styles(mixed, mixed);
|
||||
tab.set_pane_border_style(PaneId::Terminal(2), border_style_override(LineStyle::Heavy));
|
||||
let snapshot = render_tab(&mut tab, size, client_id);
|
||||
assert!(snapshot.contains('│'), "{}", snapshot);
|
||||
}
|
||||
|
||||
@@ -70,19 +70,18 @@ impl ServerOsApi for FakeInputOutput {
|
||||
fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client(&mut self, _client_id: ClientId) -> Result<()> {
|
||||
@@ -212,6 +211,7 @@ fn create_new_tab_with_plugin_receiver(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -303,6 +303,7 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab {
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -400,6 +401,7 @@ fn create_new_tab_with_cell_size(
|
||||
draw_pane_frames,
|
||||
auto_layout,
|
||||
connected_clients,
|
||||
Rc::new(RefCell::new(HashMap::new())),
|
||||
session_is_mirrored,
|
||||
Some(client_id),
|
||||
copy_options,
|
||||
@@ -910,6 +912,7 @@ pub fn split_in_direction_without_a_connected_client_still_creates_the_pane() {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: Some(Direction::Down),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
@@ -940,6 +943,7 @@ pub fn split_in_direction_without_a_connected_client_splits_the_existing_pane()
|
||||
NewPanePlacement::Tiled {
|
||||
direction: Some(Direction::Right),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
None,
|
||||
None,
|
||||
|
||||
@@ -0,0 +1,379 @@
|
||||
use crate::ui::boundaries::boundary_type;
|
||||
use zellij_utils::data::LineStyle;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub enum Corner {
|
||||
TopLeft,
|
||||
TopRight,
|
||||
BottomLeft,
|
||||
BottomRight,
|
||||
}
|
||||
|
||||
pub fn horizontal(line_style: LineStyle) -> &'static str {
|
||||
match line_style {
|
||||
LineStyle::Single => "─",
|
||||
LineStyle::Double => "═",
|
||||
LineStyle::Heavy => "━",
|
||||
LineStyle::Dashed => "┄",
|
||||
LineStyle::HeavyDashed => "┅",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn vertical(line_style: LineStyle) -> &'static str {
|
||||
match line_style {
|
||||
LineStyle::Single => "│",
|
||||
LineStyle::Double => "║",
|
||||
LineStyle::Heavy => "┃",
|
||||
LineStyle::Dashed => "┆",
|
||||
LineStyle::HeavyDashed => "┇",
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Weight {
|
||||
Light,
|
||||
Double,
|
||||
Heavy,
|
||||
}
|
||||
|
||||
fn weight(line_style: LineStyle) -> Weight {
|
||||
match line_style {
|
||||
LineStyle::Single | LineStyle::Dashed => Weight::Light,
|
||||
LineStyle::Double => Weight::Double,
|
||||
LineStyle::Heavy | LineStyle::HeavyDashed => Weight::Heavy,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn corner(
|
||||
corner: Corner,
|
||||
horizontal_style: LineStyle,
|
||||
vertical_style: LineStyle,
|
||||
rounded: bool,
|
||||
) -> &'static str {
|
||||
let horizontal_weight = weight(horizontal_style);
|
||||
let mut vertical_weight = weight(vertical_style);
|
||||
// there are no glyphs mixing a double arm with a heavy arm, in this case we render both arms
|
||||
// in the weight of the horizontal arm
|
||||
if (horizontal_weight == Weight::Double && vertical_weight == Weight::Heavy)
|
||||
|| (horizontal_weight == Weight::Heavy && vertical_weight == Weight::Double)
|
||||
{
|
||||
vertical_weight = horizontal_weight;
|
||||
}
|
||||
if rounded && horizontal_weight == Weight::Light && vertical_weight == Weight::Light {
|
||||
return match corner {
|
||||
Corner::TopLeft => boundary_type::TOP_LEFT_ROUND,
|
||||
Corner::TopRight => boundary_type::TOP_RIGHT_ROUND,
|
||||
Corner::BottomLeft => boundary_type::BOTTOM_LEFT_ROUND,
|
||||
Corner::BottomRight => boundary_type::BOTTOM_RIGHT_ROUND,
|
||||
};
|
||||
}
|
||||
match (corner, horizontal_weight, vertical_weight) {
|
||||
(Corner::TopLeft, Weight::Light, Weight::Light) => "┌",
|
||||
(Corner::TopLeft, Weight::Double, Weight::Light) => "╒",
|
||||
(Corner::TopLeft, Weight::Light, Weight::Double) => "╓",
|
||||
(Corner::TopLeft, Weight::Double, Weight::Double) => "╔",
|
||||
(Corner::TopLeft, Weight::Heavy, Weight::Light) => "┍",
|
||||
(Corner::TopLeft, Weight::Light, Weight::Heavy) => "┎",
|
||||
(Corner::TopLeft, Weight::Heavy, Weight::Heavy) => "┏",
|
||||
|
||||
(Corner::TopRight, Weight::Light, Weight::Light) => "┐",
|
||||
(Corner::TopRight, Weight::Double, Weight::Light) => "╕",
|
||||
(Corner::TopRight, Weight::Light, Weight::Double) => "╖",
|
||||
(Corner::TopRight, Weight::Double, Weight::Double) => "╗",
|
||||
(Corner::TopRight, Weight::Heavy, Weight::Light) => "┑",
|
||||
(Corner::TopRight, Weight::Light, Weight::Heavy) => "┒",
|
||||
(Corner::TopRight, Weight::Heavy, Weight::Heavy) => "┓",
|
||||
|
||||
(Corner::BottomLeft, Weight::Light, Weight::Light) => "└",
|
||||
(Corner::BottomLeft, Weight::Double, Weight::Light) => "╘",
|
||||
(Corner::BottomLeft, Weight::Light, Weight::Double) => "╙",
|
||||
(Corner::BottomLeft, Weight::Double, Weight::Double) => "╚",
|
||||
(Corner::BottomLeft, Weight::Heavy, Weight::Light) => "┕",
|
||||
(Corner::BottomLeft, Weight::Light, Weight::Heavy) => "┖",
|
||||
(Corner::BottomLeft, Weight::Heavy, Weight::Heavy) => "┗",
|
||||
|
||||
(Corner::BottomRight, Weight::Light, Weight::Light) => "┘",
|
||||
(Corner::BottomRight, Weight::Double, Weight::Light) => "╛",
|
||||
(Corner::BottomRight, Weight::Light, Weight::Double) => "╜",
|
||||
(Corner::BottomRight, Weight::Double, Weight::Double) => "╝",
|
||||
(Corner::BottomRight, Weight::Heavy, Weight::Light) => "┙",
|
||||
(Corner::BottomRight, Weight::Light, Weight::Heavy) => "┚",
|
||||
(Corner::BottomRight, Weight::Heavy, Weight::Heavy) => "┛",
|
||||
|
||||
(_, Weight::Double, Weight::Heavy) | (_, Weight::Heavy, Weight::Double) => {
|
||||
unreachable!("double/heavy mixes are normalized above")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title_separator_left(horizontal_style: LineStyle) -> &'static str {
|
||||
match weight(horizontal_style) {
|
||||
Weight::Light => boundary_type::VERTICAL_LEFT,
|
||||
Weight::Double => "╡",
|
||||
Weight::Heavy => "┥",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title_separator_right(horizontal_style: LineStyle) -> &'static str {
|
||||
match weight(horizontal_style) {
|
||||
Weight::Light => boundary_type::VERTICAL_RIGHT,
|
||||
Weight::Double => "╞",
|
||||
Weight::Heavy => "┝",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remap_light_glyph(glyph: &'static str, line_style: LineStyle) -> &'static str {
|
||||
match line_style {
|
||||
LineStyle::Single => glyph,
|
||||
LineStyle::Dashed => match glyph {
|
||||
boundary_type::HORIZONTAL => "┄",
|
||||
boundary_type::VERTICAL => "┆",
|
||||
_ => glyph,
|
||||
},
|
||||
LineStyle::Double => match glyph {
|
||||
boundary_type::HORIZONTAL => "═",
|
||||
boundary_type::VERTICAL => "║",
|
||||
boundary_type::TOP_LEFT => "╔",
|
||||
boundary_type::TOP_LEFT_ROUND => "╔",
|
||||
boundary_type::TOP_RIGHT => "╗",
|
||||
boundary_type::TOP_RIGHT_ROUND => "╗",
|
||||
boundary_type::BOTTOM_LEFT => "╚",
|
||||
boundary_type::BOTTOM_LEFT_ROUND => "╚",
|
||||
boundary_type::BOTTOM_RIGHT => "╝",
|
||||
boundary_type::BOTTOM_RIGHT_ROUND => "╝",
|
||||
boundary_type::VERTICAL_LEFT => "╣",
|
||||
boundary_type::VERTICAL_RIGHT => "╠",
|
||||
boundary_type::HORIZONTAL_DOWN => "╦",
|
||||
boundary_type::HORIZONTAL_UP => "╩",
|
||||
boundary_type::CROSS => "╬",
|
||||
_ => glyph,
|
||||
},
|
||||
LineStyle::Heavy | LineStyle::HeavyDashed => {
|
||||
let heavy_dashed = line_style == LineStyle::HeavyDashed;
|
||||
match glyph {
|
||||
boundary_type::HORIZONTAL => {
|
||||
if heavy_dashed {
|
||||
"┅"
|
||||
} else {
|
||||
"━"
|
||||
}
|
||||
},
|
||||
boundary_type::VERTICAL => {
|
||||
if heavy_dashed {
|
||||
"┇"
|
||||
} else {
|
||||
"┃"
|
||||
}
|
||||
},
|
||||
boundary_type::TOP_LEFT => "┏",
|
||||
boundary_type::TOP_LEFT_ROUND => "┏",
|
||||
boundary_type::TOP_RIGHT => "┓",
|
||||
boundary_type::TOP_RIGHT_ROUND => "┓",
|
||||
boundary_type::BOTTOM_LEFT => "┗",
|
||||
boundary_type::BOTTOM_LEFT_ROUND => "┗",
|
||||
boundary_type::BOTTOM_RIGHT => "┛",
|
||||
boundary_type::BOTTOM_RIGHT_ROUND => "┛",
|
||||
boundary_type::VERTICAL_LEFT => "┫",
|
||||
boundary_type::VERTICAL_RIGHT => "┣",
|
||||
boundary_type::HORIZONTAL_DOWN => "┳",
|
||||
boundary_type::HORIZONTAL_UP => "┻",
|
||||
boundary_type::CROSS => "╋",
|
||||
_ => glyph,
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
const ALL: [LineStyle; 5] = [
|
||||
LineStyle::Single,
|
||||
LineStyle::Double,
|
||||
LineStyle::Heavy,
|
||||
LineStyle::Dashed,
|
||||
LineStyle::HeavyDashed,
|
||||
];
|
||||
|
||||
const CORNERS: [Corner; 4] = [
|
||||
Corner::TopLeft,
|
||||
Corner::TopRight,
|
||||
Corner::BottomLeft,
|
||||
Corner::BottomRight,
|
||||
];
|
||||
|
||||
#[test]
|
||||
fn every_edge_style_has_its_own_glyph() {
|
||||
let horizontals: Vec<&str> = ALL.iter().map(|s| horizontal(*s)).collect();
|
||||
let verticals: Vec<&str> = ALL.iter().map(|s| vertical(*s)).collect();
|
||||
assert_eq!(horizontals, vec!["─", "═", "━", "┄", "┅"]);
|
||||
assert_eq!(verticals, vec!["│", "║", "┃", "┆", "┇"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_corner_combination_resolves_to_a_single_width_glyph() {
|
||||
for corner in CORNERS {
|
||||
for horizontal_style in ALL {
|
||||
for vertical_style in ALL {
|
||||
for rounded in [false, true] {
|
||||
let glyph =
|
||||
super::corner(corner, horizontal_style, vertical_style, rounded);
|
||||
assert_eq!(
|
||||
glyph.chars().count(),
|
||||
1,
|
||||
"{:?} {:?}/{:?} rounded={} produced {:?}",
|
||||
corner,
|
||||
horizontal_style,
|
||||
vertical_style,
|
||||
rounded,
|
||||
glyph
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn corners_only_round_when_both_arms_are_single_width() {
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Single, LineStyle::Single, true),
|
||||
"╭"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Dashed, LineStyle::Dashed, true),
|
||||
"╭"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Double, LineStyle::Single, true),
|
||||
"╒"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Single, LineStyle::Heavy, true),
|
||||
"┎"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mixed_weight_corners_use_the_dedicated_glyphs() {
|
||||
assert_eq!(
|
||||
corner(
|
||||
Corner::BottomRight,
|
||||
LineStyle::Double,
|
||||
LineStyle::Single,
|
||||
false
|
||||
),
|
||||
"╛"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(
|
||||
Corner::BottomRight,
|
||||
LineStyle::Single,
|
||||
LineStyle::Double,
|
||||
false
|
||||
),
|
||||
"╜"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(
|
||||
Corner::BottomRight,
|
||||
LineStyle::Heavy,
|
||||
LineStyle::Single,
|
||||
false
|
||||
),
|
||||
"┙"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(
|
||||
Corner::BottomRight,
|
||||
LineStyle::Single,
|
||||
LineStyle::Heavy,
|
||||
false
|
||||
),
|
||||
"┚"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_double_and_heavy_corner_uses_the_horizontal_arm_for_both() {
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Double, LineStyle::Heavy, false),
|
||||
"╔"
|
||||
);
|
||||
assert_eq!(
|
||||
corner(Corner::TopLeft, LineStyle::Heavy, LineStyle::Double, false),
|
||||
"┏"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn title_separators_follow_the_horizontal_weight() {
|
||||
assert_eq!(title_separator_left(LineStyle::Single), "┤");
|
||||
assert_eq!(title_separator_right(LineStyle::Single), "├");
|
||||
assert_eq!(title_separator_left(LineStyle::Double), "╡");
|
||||
assert_eq!(title_separator_right(LineStyle::Double), "╞");
|
||||
assert_eq!(title_separator_left(LineStyle::HeavyDashed), "┥");
|
||||
assert_eq!(title_separator_right(LineStyle::HeavyDashed), "┝");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remapping_covers_every_light_glyph_for_double_and_heavy() {
|
||||
let light = [
|
||||
boundary_type::HORIZONTAL,
|
||||
boundary_type::VERTICAL,
|
||||
boundary_type::TOP_LEFT,
|
||||
boundary_type::TOP_RIGHT,
|
||||
boundary_type::BOTTOM_LEFT,
|
||||
boundary_type::BOTTOM_RIGHT,
|
||||
boundary_type::VERTICAL_LEFT,
|
||||
boundary_type::VERTICAL_RIGHT,
|
||||
boundary_type::HORIZONTAL_DOWN,
|
||||
boundary_type::HORIZONTAL_UP,
|
||||
boundary_type::CROSS,
|
||||
];
|
||||
for glyph in light {
|
||||
for line_style in [LineStyle::Double, LineStyle::Heavy] {
|
||||
assert_ne!(
|
||||
remap_light_glyph(glyph, line_style),
|
||||
glyph,
|
||||
"{:?} left {} unmapped",
|
||||
line_style,
|
||||
glyph
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashed_remapping_only_touches_the_straight_runs() {
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::HORIZONTAL, LineStyle::Dashed),
|
||||
"┄"
|
||||
);
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::VERTICAL, LineStyle::Dashed),
|
||||
"┆"
|
||||
);
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::CROSS, LineStyle::Dashed),
|
||||
boundary_type::CROSS
|
||||
);
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::HORIZONTAL, LineStyle::HeavyDashed),
|
||||
"┅"
|
||||
);
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::CROSS, LineStyle::HeavyDashed),
|
||||
"╋"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_style_remap_is_the_identity() {
|
||||
assert_eq!(
|
||||
remap_light_glyph(boundary_type::CROSS, LineStyle::Single),
|
||||
boundary_type::CROSS
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,14 @@ use zellij_utils::pane_size::{Offset, Viewport};
|
||||
use crate::output::CharacterChunk;
|
||||
use crate::panes::terminal_character::{TerminalCharacter, EMPTY_TERMINAL_CHARACTER, RESET_STYLES};
|
||||
use crate::tab::Pane;
|
||||
use crate::ui::border_glyphs::remap_light_glyph;
|
||||
use ansi_term::Colour::{Fixed, RGB};
|
||||
use std::collections::HashMap;
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::{data::PaletteColor, shared::colors};
|
||||
use zellij_utils::{
|
||||
data::{BorderStyle, LineStyle, PaletteColor},
|
||||
shared::colors,
|
||||
};
|
||||
|
||||
use std::fmt::{Display, Error, Formatter};
|
||||
pub mod boundary_type {
|
||||
@@ -34,6 +38,7 @@ pub struct BoundarySymbol {
|
||||
boundary_type: BoundaryType,
|
||||
invisible: bool,
|
||||
color: Option<(PaletteColor, usize)>, // (color, color_precedence)
|
||||
line_style: Option<LineStyle>,
|
||||
}
|
||||
|
||||
impl BoundarySymbol {
|
||||
@@ -42,25 +47,39 @@ impl BoundarySymbol {
|
||||
boundary_type,
|
||||
invisible: false,
|
||||
color: Some((PaletteColor::EightBit(colors::GRAY), 0)),
|
||||
line_style: Some(LineStyle::Single),
|
||||
}
|
||||
}
|
||||
pub fn color(&mut self, color: Option<(PaletteColor, usize)>) -> Self {
|
||||
self.color = color;
|
||||
*self
|
||||
}
|
||||
pub fn as_terminal_character(&self) -> Result<TerminalCharacter> {
|
||||
pub fn line_style(&mut self, line_style: LineStyle) -> Self {
|
||||
self.line_style = Some(line_style);
|
||||
*self
|
||||
}
|
||||
pub fn glyph(&self, fallback_line_style: LineStyle) -> BoundaryType {
|
||||
remap_light_glyph(
|
||||
self.boundary_type,
|
||||
self.line_style.unwrap_or(fallback_line_style),
|
||||
)
|
||||
}
|
||||
pub fn as_terminal_character(
|
||||
&self,
|
||||
fallback_line_style: LineStyle,
|
||||
) -> Result<TerminalCharacter> {
|
||||
let tc = if self.invisible {
|
||||
EMPTY_TERMINAL_CHARACTER
|
||||
} else {
|
||||
let character = self
|
||||
.boundary_type
|
||||
let glyph = self.glyph(fallback_line_style);
|
||||
let character = glyph
|
||||
.chars()
|
||||
.next()
|
||||
.context("no boundary symbols defined")
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to convert boundary symbol {} into terminal character",
|
||||
self.boundary_type
|
||||
glyph
|
||||
)
|
||||
})?;
|
||||
TerminalCharacter::new_singlewidth_styled(
|
||||
@@ -76,18 +95,15 @@ impl BoundarySymbol {
|
||||
|
||||
impl Display for BoundarySymbol {
|
||||
fn fmt(&self, f: &mut Formatter) -> Result<(), Error> {
|
||||
let glyph = self.glyph(LineStyle::Single);
|
||||
match self.invisible {
|
||||
true => write!(f, " "),
|
||||
false => match self.color {
|
||||
Some(color) => match color.0 {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
write!(f, "{}", RGB(r, g, b).paint(self.boundary_type))
|
||||
},
|
||||
PaletteColor::EightBit(color) => {
|
||||
write!(f, "{}", Fixed(color).paint(self.boundary_type))
|
||||
},
|
||||
PaletteColor::Rgb((r, g, b)) => write!(f, "{}", RGB(r, g, b).paint(glyph)),
|
||||
PaletteColor::EightBit(color) => write!(f, "{}", Fixed(color).paint(glyph)),
|
||||
},
|
||||
None => write!(f, "{}", self.boundary_type),
|
||||
None => write!(f, "{}", glyph),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -110,6 +126,16 @@ fn combine_symbols(
|
||||
},
|
||||
_ => current_symbol.color.or(next_symbol.color),
|
||||
};
|
||||
let line_style = match (current_symbol.line_style, next_symbol.line_style) {
|
||||
(Some(current_line_style), Some(next_line_style)) => {
|
||||
if current_line_style == next_line_style {
|
||||
Some(current_line_style)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
match (current_symbol.boundary_type, next_symbol.boundary_type) {
|
||||
(CROSS, _) | (_, CROSS) => {
|
||||
// (┼, *) or (*, ┼) => Some(┼)
|
||||
@@ -118,6 +144,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_RIGHT, TOP_RIGHT) => {
|
||||
@@ -127,6 +154,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_RIGHT, VERTICAL) | (TOP_RIGHT, BOTTOM_RIGHT) | (TOP_RIGHT, VERTICAL_LEFT) => {
|
||||
@@ -138,6 +166,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_RIGHT, HORIZONTAL) | (TOP_RIGHT, TOP_LEFT) | (TOP_RIGHT, HORIZONTAL_DOWN) => {
|
||||
@@ -149,6 +178,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_RIGHT, BOTTOM_LEFT) | (TOP_RIGHT, VERTICAL_RIGHT) | (TOP_RIGHT, HORIZONTAL_UP) => {
|
||||
@@ -160,6 +190,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL, HORIZONTAL) => {
|
||||
@@ -169,6 +200,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL, VERTICAL) | (HORIZONTAL, VERTICAL_LEFT) | (HORIZONTAL, VERTICAL_RIGHT) => {
|
||||
@@ -180,6 +212,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL, TOP_LEFT) | (HORIZONTAL, HORIZONTAL_DOWN) => {
|
||||
@@ -190,6 +223,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL, BOTTOM_RIGHT) | (HORIZONTAL, BOTTOM_LEFT) | (HORIZONTAL, HORIZONTAL_UP) => {
|
||||
@@ -201,6 +235,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL, VERTICAL) => {
|
||||
@@ -210,6 +245,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL, TOP_LEFT) | (VERTICAL, BOTTOM_LEFT) | (VERTICAL, VERTICAL_RIGHT) => {
|
||||
@@ -221,6 +257,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL, BOTTOM_RIGHT) | (VERTICAL, VERTICAL_LEFT) => {
|
||||
@@ -231,6 +268,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL, HORIZONTAL_DOWN) | (VERTICAL, HORIZONTAL_UP) => {
|
||||
@@ -241,6 +279,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_LEFT, TOP_LEFT) => {
|
||||
@@ -250,6 +289,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_LEFT, BOTTOM_RIGHT) | (TOP_LEFT, VERTICAL_LEFT) | (TOP_LEFT, HORIZONTAL_UP) => {
|
||||
@@ -261,6 +301,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_LEFT, BOTTOM_LEFT) | (TOP_LEFT, VERTICAL_RIGHT) => {
|
||||
@@ -271,6 +312,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(TOP_LEFT, HORIZONTAL_DOWN) => {
|
||||
@@ -280,6 +322,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_RIGHT, BOTTOM_RIGHT) => {
|
||||
@@ -289,6 +332,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_RIGHT, BOTTOM_LEFT) | (BOTTOM_RIGHT, HORIZONTAL_UP) => {
|
||||
@@ -299,6 +343,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_RIGHT, VERTICAL_LEFT) => {
|
||||
@@ -308,6 +353,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_RIGHT, VERTICAL_RIGHT) | (BOTTOM_RIGHT, HORIZONTAL_DOWN) => {
|
||||
@@ -318,6 +364,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_LEFT, BOTTOM_LEFT) => {
|
||||
@@ -327,6 +374,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_LEFT, VERTICAL_LEFT) | (BOTTOM_LEFT, HORIZONTAL_DOWN) => {
|
||||
@@ -337,6 +385,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_LEFT, VERTICAL_RIGHT) => {
|
||||
@@ -346,6 +395,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(BOTTOM_LEFT, HORIZONTAL_UP) => {
|
||||
@@ -355,6 +405,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL_LEFT, VERTICAL_LEFT) => {
|
||||
@@ -364,6 +415,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL_LEFT, VERTICAL_RIGHT)
|
||||
@@ -377,6 +429,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL_RIGHT, VERTICAL_RIGHT) => {
|
||||
@@ -386,6 +439,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(VERTICAL_RIGHT, HORIZONTAL_DOWN) | (VERTICAL_RIGHT, HORIZONTAL_UP) => {
|
||||
@@ -396,6 +450,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL_DOWN, HORIZONTAL_DOWN) => {
|
||||
@@ -405,6 +460,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL_DOWN, HORIZONTAL_UP) => {
|
||||
@@ -414,6 +470,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(HORIZONTAL_UP, HORIZONTAL_UP) => {
|
||||
@@ -423,6 +480,7 @@ fn combine_symbols(
|
||||
boundary_type,
|
||||
invisible,
|
||||
color,
|
||||
line_style,
|
||||
})
|
||||
},
|
||||
(_, _) => combine_symbols(next_symbol, current_symbol),
|
||||
@@ -443,14 +501,16 @@ impl Coordinates {
|
||||
|
||||
pub struct Boundaries {
|
||||
viewport: Viewport,
|
||||
fallback_line_style: LineStyle,
|
||||
pub boundary_characters: HashMap<Coordinates, BoundarySymbol>,
|
||||
}
|
||||
|
||||
#[allow(clippy::if_same_then_else)]
|
||||
impl Boundaries {
|
||||
pub fn new(viewport: Viewport) -> Self {
|
||||
pub fn new(viewport: Viewport, fallback_line_style: LineStyle) -> Self {
|
||||
Boundaries {
|
||||
viewport,
|
||||
fallback_line_style,
|
||||
boundary_characters: HashMap::new(),
|
||||
}
|
||||
}
|
||||
@@ -458,6 +518,7 @@ impl Boundaries {
|
||||
&mut self,
|
||||
rect: &dyn Pane,
|
||||
color: Option<(PaletteColor, usize)>, // (color, color_precedence)
|
||||
border_style: BorderStyle,
|
||||
pane_is_on_top_of_stack: bool,
|
||||
pane_is_on_bottom_of_stack: bool,
|
||||
pane_is_stacked_under: bool,
|
||||
@@ -479,19 +540,29 @@ impl Boundaries {
|
||||
let coordinates = Coordinates::new(boundary_x_coords, row);
|
||||
let symbol_to_add = if row == first_row_coordinates && row != self.viewport.y {
|
||||
if pane_is_stacked {
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_RIGHT).color(color)
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_RIGHT)
|
||||
.color(color)
|
||||
.line_style(border_style.left)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.left)
|
||||
}
|
||||
} else if row == first_row_coordinates && pane_is_stacked {
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.left)
|
||||
} else if row == last_row_coordinates - 1
|
||||
&& row != self.viewport.y + self.viewport.rows - 1
|
||||
&& content_offset.bottom > 0
|
||||
{
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.left)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::VERTICAL).color(color)
|
||||
BoundarySymbol::new(boundary_type::VERTICAL)
|
||||
.color(color)
|
||||
.line_style(border_style.left)
|
||||
};
|
||||
let next_symbol = self
|
||||
.boundary_characters
|
||||
@@ -509,11 +580,17 @@ impl Boundaries {
|
||||
for col in first_col_coordinates..last_col_coordinates {
|
||||
let coordinates = Coordinates::new(col, boundary_y_coords);
|
||||
let symbol_to_add = if col == first_col_coordinates && col != self.viewport.x {
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::TOP_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.top)
|
||||
} else if col == last_col_coordinates - 1 && col != self.viewport.cols - 1 {
|
||||
BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color)
|
||||
BoundarySymbol::new(boundary_type::TOP_RIGHT)
|
||||
.color(color)
|
||||
.line_style(border_style.top)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::HORIZONTAL).color(color)
|
||||
BoundarySymbol::new(boundary_type::HORIZONTAL)
|
||||
.color(color)
|
||||
.line_style(border_style.top)
|
||||
};
|
||||
let next_symbol = self
|
||||
.boundary_characters
|
||||
@@ -532,20 +609,30 @@ impl Boundaries {
|
||||
for row in first_row_coordinates..last_row_coordinates {
|
||||
let coordinates = Coordinates::new(boundary_x_coords, row);
|
||||
let symbol_to_add = if row == first_row_coordinates && pane_is_stacked {
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.right)
|
||||
} else if row == first_row_coordinates && row != self.viewport.y {
|
||||
if pane_is_stacked {
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::VERTICAL_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.right)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::TOP_RIGHT).color(color)
|
||||
BoundarySymbol::new(boundary_type::TOP_RIGHT)
|
||||
.color(color)
|
||||
.line_style(border_style.right)
|
||||
}
|
||||
} else if row == last_row_coordinates - 1
|
||||
&& row != self.viewport.y + self.viewport.rows - 1
|
||||
&& content_offset.bottom > 0
|
||||
{
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color)
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_RIGHT)
|
||||
.color(color)
|
||||
.line_style(border_style.right)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::VERTICAL).color(color)
|
||||
BoundarySymbol::new(boundary_type::VERTICAL)
|
||||
.color(color)
|
||||
.line_style(border_style.right)
|
||||
};
|
||||
let next_symbol = self
|
||||
.boundary_characters
|
||||
@@ -563,11 +650,17 @@ impl Boundaries {
|
||||
for col in first_col_coordinates..last_col_coordinates {
|
||||
let coordinates = Coordinates::new(col, boundary_y_coords);
|
||||
let symbol_to_add = if col == first_col_coordinates && col != self.viewport.x {
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_LEFT).color(color)
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_LEFT)
|
||||
.color(color)
|
||||
.line_style(border_style.bottom)
|
||||
} else if col == last_col_coordinates - 1 && col != self.viewport.cols - 1 {
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_RIGHT).color(color)
|
||||
BoundarySymbol::new(boundary_type::BOTTOM_RIGHT)
|
||||
.color(color)
|
||||
.line_style(border_style.bottom)
|
||||
} else {
|
||||
BoundarySymbol::new(boundary_type::HORIZONTAL).color(color)
|
||||
BoundarySymbol::new(boundary_type::HORIZONTAL)
|
||||
.color(color)
|
||||
.line_style(border_style.bottom)
|
||||
};
|
||||
let next_symbol = self
|
||||
.boundary_characters
|
||||
@@ -584,16 +677,25 @@ impl Boundaries {
|
||||
) -> Result<Vec<CharacterChunk>> {
|
||||
let mut character_chunks = vec![];
|
||||
for (coordinates, boundary_character) in &self.boundary_characters {
|
||||
let glyph = boundary_character.glyph(self.fallback_line_style);
|
||||
let already_on_screen = existing_boundaries_on_screen
|
||||
.and_then(|e| e.boundary_characters.get(coordinates))
|
||||
.map(|e| e == boundary_character)
|
||||
.map(|e| {
|
||||
e.color == boundary_character.color
|
||||
&& e.invisible == boundary_character.invisible
|
||||
&& e.glyph(
|
||||
existing_boundaries_on_screen
|
||||
.map(|b| b.fallback_line_style)
|
||||
.unwrap_or(LineStyle::Single),
|
||||
) == glyph
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if already_on_screen {
|
||||
continue;
|
||||
}
|
||||
character_chunks.push(CharacterChunk::new(
|
||||
vec![boundary_character
|
||||
.as_terminal_character()
|
||||
.as_terminal_character(self.fallback_line_style)
|
||||
.context("failed to render as terminal character")?],
|
||||
coordinates.x,
|
||||
coordinates.y,
|
||||
@@ -653,3 +755,57 @@ impl Boundaries {
|
||||
&& rect.y() + rect.rows() <= self.viewport.y + self.viewport.rows
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn symbol(boundary_type: BoundaryType, line_style: LineStyle) -> BoundarySymbol {
|
||||
BoundarySymbol::new(boundary_type).line_style(line_style)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn matching_line_styles_are_kept_through_a_junction() {
|
||||
let combined = combine_symbols(
|
||||
symbol(boundary_type::HORIZONTAL, LineStyle::Double),
|
||||
symbol(boundary_type::VERTICAL, LineStyle::Double),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(combined.glyph(LineStyle::Single), "╬");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn conflicting_line_styles_fall_back_to_the_ambient_style() {
|
||||
let combined = combine_symbols(
|
||||
symbol(boundary_type::HORIZONTAL, LineStyle::Double),
|
||||
symbol(boundary_type::VERTICAL, LineStyle::Heavy),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(combined.glyph(LineStyle::Single), "┼");
|
||||
assert_eq!(combined.glyph(LineStyle::Double), "╬");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_style_without_a_glyph_for_the_junction_falls_back_to_single() {
|
||||
let combined = combine_symbols(
|
||||
symbol(boundary_type::HORIZONTAL, LineStyle::Dashed),
|
||||
symbol(boundary_type::VERTICAL, LineStyle::Dashed),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(combined.glyph(LineStyle::Single), "┼");
|
||||
assert_eq!(
|
||||
symbol(boundary_type::HORIZONTAL, LineStyle::Dashed).glyph(LineStyle::Single),
|
||||
"┄"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heavy_junctions_are_kept() {
|
||||
let combined = combine_symbols(
|
||||
symbol(boundary_type::HORIZONTAL, LineStyle::Heavy),
|
||||
symbol(boundary_type::TOP_LEFT, LineStyle::Heavy),
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(combined.glyph(LineStyle::Single), "┳");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
pub mod border_glyphs;
|
||||
pub mod boundaries;
|
||||
pub mod components;
|
||||
pub mod hint_text;
|
||||
|
||||
@@ -3,13 +3,14 @@ use crate::panes::{
|
||||
AnsiCode, CharacterStyles, RcCharacterStyles, TerminalCharacter, EMPTY_TERMINAL_CHARACTER,
|
||||
};
|
||||
use crate::tab::GuestChoiceIndicator;
|
||||
use crate::ui::border_glyphs::{self, Corner};
|
||||
use crate::ui::boundaries::boundary_type;
|
||||
use crate::ui::hint_text::{
|
||||
exit_code_segments, hover_segments, rerun_segments, resize_segments, HintExitStatus, HintLevel,
|
||||
HintSegment, HintTier,
|
||||
};
|
||||
use crate::ClientId;
|
||||
use zellij_utils::data::{client_id_to_colors, PaletteColor, Style};
|
||||
use zellij_utils::data::{client_slot_to_colors, BorderStyle, LineStyle, PaletteColor, Style};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::pane_size::{Offset, PaneGeom, Viewport};
|
||||
use zellij_utils::position::Position;
|
||||
@@ -79,7 +80,9 @@ pub struct FrameParams {
|
||||
pub focused_client: Option<ClientId>,
|
||||
pub is_main_client: bool, // more accurately: is_focused_for_main_client
|
||||
pub other_focused_clients: Vec<ClientId>,
|
||||
pub other_focused_client_slots: Vec<usize>,
|
||||
pub style: Style,
|
||||
pub border_style: BorderStyle,
|
||||
pub color: Option<PaletteColor>,
|
||||
pub other_cursors_exist_in_session: bool,
|
||||
pub pane_is_stacked_under: bool,
|
||||
@@ -108,11 +111,13 @@ pub struct PaneFrame {
|
||||
pub title: String,
|
||||
pub scroll_position: (usize, usize), // (position, length)
|
||||
pub style: Style,
|
||||
pub border_style: BorderStyle,
|
||||
pub color: Option<PaletteColor>,
|
||||
pub focused_client: Option<ClientId>,
|
||||
pub is_main_client: bool,
|
||||
pub other_cursors_exist_in_session: bool,
|
||||
pub other_focused_clients: Vec<ClientId>,
|
||||
pub other_focused_client_slots: Vec<usize>,
|
||||
exit_status: Option<ExitStatus>,
|
||||
is_first_run: bool,
|
||||
pane_is_stacked_over: bool,
|
||||
@@ -147,10 +152,12 @@ impl PaneFrame {
|
||||
title: main_title,
|
||||
scroll_position,
|
||||
style: frame_params.style,
|
||||
border_style: frame_params.border_style,
|
||||
color: frame_params.color,
|
||||
focused_client: frame_params.focused_client,
|
||||
is_main_client: frame_params.is_main_client,
|
||||
other_focused_clients: frame_params.other_focused_clients,
|
||||
other_focused_client_slots: frame_params.other_focused_client_slots,
|
||||
other_cursors_exist_in_session: frame_params.other_cursors_exist_in_session,
|
||||
exit_status: None,
|
||||
is_first_run: false,
|
||||
@@ -191,33 +198,67 @@ impl PaneFrame {
|
||||
self.color = Some(color);
|
||||
self.color_override = Some(color);
|
||||
}
|
||||
fn client_cursor(&self, client_id: ClientId) -> Vec<TerminalCharacter> {
|
||||
let color = client_id_to_colors(client_id, self.style.colors.multiplayer_user_colors);
|
||||
fn client_cursor(&self, display_slot: usize) -> Vec<TerminalCharacter> {
|
||||
let color = client_slot_to_colors(display_slot, self.style.colors.multiplayer_user_colors);
|
||||
background_color(" ", color.map(|c| c.0))
|
||||
}
|
||||
fn top_horizontal(&self) -> &'static str {
|
||||
border_glyphs::horizontal(self.border_style.top)
|
||||
}
|
||||
fn bottom_horizontal(&self) -> &'static str {
|
||||
border_glyphs::horizontal(self.border_style.bottom)
|
||||
}
|
||||
fn left_vertical(&self) -> &'static str {
|
||||
border_glyphs::vertical(self.border_style.left)
|
||||
}
|
||||
fn right_vertical(&self) -> &'static str {
|
||||
border_glyphs::vertical(self.border_style.right)
|
||||
}
|
||||
fn title_separator_left(&self) -> &'static str {
|
||||
border_glyphs::title_separator_left(self.border_style.top)
|
||||
}
|
||||
fn title_separator_right(&self) -> &'static str {
|
||||
border_glyphs::title_separator_right(self.border_style.top)
|
||||
}
|
||||
fn horizontal_style_of_corner(&self, corner: Corner) -> LineStyle {
|
||||
match corner {
|
||||
Corner::TopLeft | Corner::TopRight => self.border_style.top,
|
||||
Corner::BottomLeft | Corner::BottomRight => self.border_style.bottom,
|
||||
}
|
||||
}
|
||||
fn vertical_style_of_corner(&self, corner: Corner) -> LineStyle {
|
||||
match corner {
|
||||
Corner::TopLeft | Corner::BottomLeft => self.border_style.left,
|
||||
Corner::TopRight | Corner::BottomRight => self.border_style.right,
|
||||
}
|
||||
}
|
||||
fn get_corner(&self, corner: &'static str) -> &'static str {
|
||||
let corner = if !self.should_draw_pane_frames
|
||||
if !self.should_draw_pane_frames
|
||||
&& (corner == boundary_type::TOP_LEFT || corner == boundary_type::TOP_RIGHT)
|
||||
{
|
||||
boundary_type::HORIZONTAL
|
||||
} else if self.pane_is_stacked_under && corner == boundary_type::TOP_RIGHT {
|
||||
boundary_type::BOTTOM_RIGHT
|
||||
} else if self.pane_is_stacked_under && corner == boundary_type::TOP_LEFT {
|
||||
boundary_type::BOTTOM_LEFT
|
||||
} else {
|
||||
corner
|
||||
return self.top_horizontal();
|
||||
}
|
||||
let style_corner = match corner {
|
||||
boundary_type::TOP_LEFT => Corner::TopLeft,
|
||||
boundary_type::TOP_RIGHT => Corner::TopRight,
|
||||
boundary_type::BOTTOM_LEFT => Corner::BottomLeft,
|
||||
_ => Corner::BottomRight,
|
||||
};
|
||||
if self.style.rounded_corners {
|
||||
match corner {
|
||||
boundary_type::TOP_RIGHT => boundary_type::TOP_RIGHT_ROUND,
|
||||
boundary_type::TOP_LEFT => boundary_type::TOP_LEFT_ROUND,
|
||||
boundary_type::BOTTOM_RIGHT => boundary_type::BOTTOM_RIGHT_ROUND,
|
||||
boundary_type::BOTTOM_LEFT => boundary_type::BOTTOM_LEFT_ROUND,
|
||||
_ => corner,
|
||||
let shape_corner = if self.pane_is_stacked_under {
|
||||
match style_corner {
|
||||
Corner::TopRight => Corner::BottomRight,
|
||||
Corner::TopLeft => Corner::BottomLeft,
|
||||
other => other,
|
||||
}
|
||||
} else {
|
||||
corner
|
||||
}
|
||||
style_corner
|
||||
};
|
||||
border_glyphs::corner(
|
||||
shape_corner,
|
||||
self.horizontal_style_of_corner(style_corner),
|
||||
self.vertical_style_of_corner(style_corner),
|
||||
self.border_style.rounded_corners,
|
||||
)
|
||||
}
|
||||
fn render_title_right_side(
|
||||
&self,
|
||||
@@ -310,8 +351,8 @@ impl PaneFrame {
|
||||
}
|
||||
}
|
||||
fn render_my_focus(&self, max_length: usize) -> Option<(Vec<TerminalCharacter>, usize)> {
|
||||
let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
|
||||
let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
|
||||
let mut left_separator = foreground_color(self.title_separator_left(), self.color);
|
||||
let mut right_separator = foreground_color(self.title_separator_right(), self.color);
|
||||
let full_indication_text = "MY FOCUS";
|
||||
let mut full_indication = vec![];
|
||||
full_indication.append(&mut left_separator);
|
||||
@@ -340,16 +381,16 @@ impl PaneFrame {
|
||||
&self,
|
||||
max_length: usize,
|
||||
) -> Option<(Vec<TerminalCharacter>, usize)> {
|
||||
let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
|
||||
let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
|
||||
let mut left_separator = foreground_color(self.title_separator_left(), self.color);
|
||||
let mut right_separator = foreground_color(self.title_separator_right(), self.color);
|
||||
let full_indication_text = "MY FOCUS AND:";
|
||||
let short_indication_text = "+";
|
||||
let mut full_indication = foreground_color(full_indication_text, self.color);
|
||||
let mut full_indication_len = full_indication_text.width();
|
||||
let mut short_indication = foreground_color(short_indication_text, self.color);
|
||||
let mut short_indication_len = short_indication_text.width();
|
||||
for client_id in &self.other_focused_clients {
|
||||
let mut text = self.client_cursor(*client_id);
|
||||
for display_slot in &self.other_focused_client_slots {
|
||||
let mut text = self.client_cursor(*display_slot);
|
||||
full_indication_len += 2;
|
||||
full_indication.push(EMPTY_TERMINAL_CHARACTER);
|
||||
full_indication.append(&mut text.clone());
|
||||
@@ -383,8 +424,8 @@ impl PaneFrame {
|
||||
&self,
|
||||
max_length: usize,
|
||||
) -> Option<(Vec<TerminalCharacter>, usize)> {
|
||||
let mut left_separator = foreground_color(boundary_type::VERTICAL_LEFT, self.color);
|
||||
let mut right_separator = foreground_color(boundary_type::VERTICAL_RIGHT, self.color);
|
||||
let mut left_separator = foreground_color(self.title_separator_left(), self.color);
|
||||
let mut right_separator = foreground_color(self.title_separator_right(), self.color);
|
||||
let full_indication_text = if self.other_focused_clients.len() == 1 {
|
||||
"FOCUSED USER:"
|
||||
} else {
|
||||
@@ -397,8 +438,8 @@ impl PaneFrame {
|
||||
let mut middle_indication_len = middle_indication_text.width();
|
||||
let mut short_indication = vec![];
|
||||
let mut short_indication_len = 0;
|
||||
for client_id in &self.other_focused_clients {
|
||||
let mut text = self.client_cursor(*client_id);
|
||||
for display_slot in &self.other_focused_client_slots {
|
||||
let mut text = self.client_cursor(*display_slot);
|
||||
full_indication_len += 2;
|
||||
full_indication.push(EMPTY_TERMINAL_CHARACTER);
|
||||
full_indication.append(&mut text.clone());
|
||||
@@ -563,7 +604,7 @@ impl PaneFrame {
|
||||
col += right_side_len;
|
||||
continue;
|
||||
} else {
|
||||
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
|
||||
title_line.append(&mut foreground_color(self.top_horizontal(), self.color));
|
||||
}
|
||||
if col == self.geom.x + self.geom.cols - 1 {
|
||||
break;
|
||||
@@ -605,7 +646,7 @@ impl PaneFrame {
|
||||
col += *middle_len;
|
||||
continue;
|
||||
} else {
|
||||
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
|
||||
title_line.append(&mut foreground_color(self.top_horizontal(), self.color));
|
||||
}
|
||||
if col == self.geom.x + self.geom.cols - 1 {
|
||||
break;
|
||||
@@ -640,7 +681,7 @@ impl PaneFrame {
|
||||
col += *middle_len;
|
||||
continue;
|
||||
} else {
|
||||
title_line.append(&mut foreground_color(boundary_type::HORIZONTAL, self.color));
|
||||
title_line.append(&mut foreground_color(self.top_horizontal(), self.color));
|
||||
}
|
||||
if col == self.geom.x + self.geom.cols - 1 {
|
||||
break;
|
||||
@@ -663,7 +704,7 @@ impl PaneFrame {
|
||||
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
|
||||
let mut middle = String::new();
|
||||
for _ in (left_side_len + right_side_len)..total_title_length {
|
||||
middle.push_str(boundary_type::HORIZONTAL);
|
||||
middle.push_str(self.top_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -685,7 +726,7 @@ impl PaneFrame {
|
||||
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
|
||||
let mut middle_padding = String::new();
|
||||
for _ in *left_side_len..total_title_length {
|
||||
middle_padding.push_str(boundary_type::HORIZONTAL);
|
||||
middle_padding.push_str(self.top_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -702,7 +743,7 @@ impl PaneFrame {
|
||||
let total_title_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
|
||||
let mut middle_padding = String::new();
|
||||
for _ in 0..total_title_length {
|
||||
middle_padding.push_str(boundary_type::HORIZONTAL);
|
||||
middle_padding.push_str(self.top_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -812,7 +853,7 @@ impl PaneFrame {
|
||||
line.push(EMPTY_TERMINAL_CHARACTER);
|
||||
}
|
||||
}
|
||||
line.append(&mut foreground_color(boundary_type::VERTICAL, None));
|
||||
line.append(&mut foreground_color(self.left_vertical(), None));
|
||||
for _ in 0..corner_padding {
|
||||
line.push(EMPTY_TERMINAL_CHARACTER);
|
||||
}
|
||||
@@ -824,7 +865,7 @@ impl PaneFrame {
|
||||
for _ in 0..corner_padding {
|
||||
line.push(EMPTY_TERMINAL_CHARACTER);
|
||||
}
|
||||
line.append(&mut foreground_color(boundary_type::VERTICAL, None));
|
||||
line.append(&mut foreground_color(self.right_vertical(), None));
|
||||
let mut occupied_columns = entry_start + full_width_entry_length;
|
||||
let (mut scroll_part, scroll_length) = self
|
||||
.bracketed_scroll_indicator(usable_cols.saturating_sub(occupied_columns))
|
||||
@@ -973,9 +1014,9 @@ impl PaneFrame {
|
||||
fn focused_users_part(&self, label: &str) -> (Vec<TerminalCharacter>, usize) {
|
||||
let mut content = foreground_color(label, self.color);
|
||||
let mut content_length = label.width();
|
||||
for client_id in &self.other_focused_clients {
|
||||
for display_slot in &self.other_focused_client_slots {
|
||||
content.push(EMPTY_TERMINAL_CHARACTER);
|
||||
content.append(&mut self.client_cursor(*client_id));
|
||||
content.append(&mut self.client_cursor(*display_slot));
|
||||
content_length += 2;
|
||||
}
|
||||
self.bracketed_title_part_from_characters(content, content_length)
|
||||
@@ -983,12 +1024,12 @@ impl PaneFrame {
|
||||
fn focused_users_cursors_part(&self) -> (Vec<TerminalCharacter>, usize) {
|
||||
let mut content = vec![];
|
||||
let mut content_length = 0;
|
||||
for client_id in &self.other_focused_clients {
|
||||
for display_slot in &self.other_focused_client_slots {
|
||||
if content_length > 0 {
|
||||
content.push(EMPTY_TERMINAL_CHARACTER);
|
||||
content_length += 1;
|
||||
}
|
||||
content.append(&mut self.client_cursor(*client_id));
|
||||
content.append(&mut self.client_cursor(*display_slot));
|
||||
content_length += 1;
|
||||
}
|
||||
self.bracketed_title_part_from_characters(content, content_length)
|
||||
@@ -1045,7 +1086,7 @@ impl PaneFrame {
|
||||
};
|
||||
let right_start = width.saturating_sub(right_length).max(middle_end);
|
||||
let fill_character = if self.pane_is_stacked {
|
||||
foreground_color(boundary_type::HORIZONTAL, self.color)
|
||||
foreground_color(self.bottom_horizontal(), self.color)
|
||||
.into_iter()
|
||||
.next()
|
||||
.unwrap_or(EMPTY_TERMINAL_CHARACTER)
|
||||
@@ -1096,7 +1137,7 @@ impl PaneFrame {
|
||||
let padding_len = max_undertitle_length.saturating_sub(text_len);
|
||||
let mut padding = String::new();
|
||||
for _ in 0..padding_len {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
|
||||
let mut ret = vec![];
|
||||
@@ -1130,7 +1171,7 @@ impl PaneFrame {
|
||||
let padding_len = max_undertitle_length.saturating_sub(help_text_len);
|
||||
let mut padding = String::new();
|
||||
for _ in 0..padding_len {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
|
||||
let mut ret = vec![];
|
||||
@@ -1183,7 +1224,7 @@ impl PaneFrame {
|
||||
// render exit status and tips
|
||||
let mut padding = String::new();
|
||||
for _ in full_text_len..max_undertitle_length {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -1196,7 +1237,7 @@ impl PaneFrame {
|
||||
// render only exit status
|
||||
let mut padding = String::new();
|
||||
for _ in first_part_len..max_undertitle_length {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -1213,7 +1254,7 @@ impl PaneFrame {
|
||||
let full_text_len = first_part_len;
|
||||
let mut padding = String::new();
|
||||
for _ in full_text_len..max_undertitle_length {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -1241,7 +1282,7 @@ impl PaneFrame {
|
||||
// render exit status and tips
|
||||
let mut padding = String::new();
|
||||
for _ in hover_shortcuts_len..max_undertitle_length {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
let mut ret = vec![];
|
||||
ret.append(&mut left_boundary);
|
||||
@@ -1369,7 +1410,7 @@ impl PaneFrame {
|
||||
// bottom right corner
|
||||
self.get_corner(boundary_type::BOTTOM_RIGHT)
|
||||
} else {
|
||||
boundary_type::HORIZONTAL
|
||||
self.bottom_horizontal()
|
||||
};
|
||||
|
||||
let mut boundary_character = foreground_color(boundary, self.color);
|
||||
@@ -1381,9 +1422,9 @@ impl PaneFrame {
|
||||
}
|
||||
} else {
|
||||
let boundary_character_left =
|
||||
foreground_color(boundary_type::VERTICAL, self.color);
|
||||
foreground_color(self.left_vertical(), self.color);
|
||||
let boundary_character_right =
|
||||
foreground_color(boundary_type::VERTICAL, self.color);
|
||||
foreground_color(self.right_vertical(), self.color);
|
||||
|
||||
let x = self.geom.x;
|
||||
let y = self.geom.y + row;
|
||||
@@ -1421,7 +1462,7 @@ impl PaneFrame {
|
||||
Some(ExitStatus::Exited) => {
|
||||
self.render_hint_segments(&exit_code_segments(HintExitStatus::Exited))
|
||||
},
|
||||
None => (foreground_color(boundary_type::HORIZONTAL, self.color), 1),
|
||||
None => (foreground_color(self.bottom_horizontal(), self.color), 1),
|
||||
}
|
||||
}
|
||||
fn second_held_title_part_full(&self) -> (Vec<TerminalCharacter>, usize) {
|
||||
@@ -1438,7 +1479,7 @@ impl PaneFrame {
|
||||
let mut ret = vec![];
|
||||
let mut padding = String::new();
|
||||
for _ in 0..max_undertitle_length {
|
||||
padding.push_str(boundary_type::HORIZONTAL);
|
||||
padding.push_str(self.bottom_horizontal());
|
||||
}
|
||||
ret.append(&mut left_boundary);
|
||||
ret.append(&mut foreground_color(&padding, self.color));
|
||||
@@ -1467,7 +1508,9 @@ mod tests {
|
||||
focused_client: None,
|
||||
is_main_client: true,
|
||||
other_focused_clients: vec![],
|
||||
other_focused_client_slots: vec![],
|
||||
style: Style::default(),
|
||||
border_style: BorderStyle::default(),
|
||||
color: None,
|
||||
other_cursors_exist_in_session: false,
|
||||
pane_is_stacked_over: false,
|
||||
@@ -1496,6 +1539,94 @@ mod tests {
|
||||
chars.iter().map(|c| c.character).collect()
|
||||
}
|
||||
|
||||
fn frame_rows(border_style: BorderStyle) -> Vec<String> {
|
||||
let mut frame = pane_frame_with(false, false, 10);
|
||||
frame.border_style = border_style;
|
||||
frame.geom.rows = 3;
|
||||
frame.mouse_hover_tips = false;
|
||||
frame.show_help_text = false;
|
||||
let (chunks, _) = frame.render().unwrap();
|
||||
let mut rows: Vec<(usize, usize, String)> = chunks
|
||||
.iter()
|
||||
.map(|c| (c.y, c.x, characters_to_string(&c.terminal_characters)))
|
||||
.collect();
|
||||
rows.sort();
|
||||
let mut by_row: Vec<String> = vec![String::new(); 3];
|
||||
for (y, _x, text) in rows {
|
||||
by_row[y].push_str(&text);
|
||||
}
|
||||
by_row
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn single_line_frame_with_rounded_corners() {
|
||||
let rows = frame_rows(BorderStyle {
|
||||
rounded_corners: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(rows[0].starts_with('╭'), "{}", rows[0]);
|
||||
assert!(rows[0].ends_with('╮'), "{}", rows[0]);
|
||||
assert_eq!(rows[1], "││");
|
||||
assert_eq!(rows[2], "╰────────╯");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn double_top_border_with_single_sides() {
|
||||
let rows = frame_rows(BorderStyle {
|
||||
top: LineStyle::Double,
|
||||
rounded_corners: true,
|
||||
..Default::default()
|
||||
});
|
||||
assert!(rows[0].starts_with('╒'), "{}", rows[0]);
|
||||
assert!(rows[0].ends_with('╕'), "{}", rows[0]);
|
||||
assert_eq!(rows[1], "││");
|
||||
assert_eq!(rows[2], "╰────────╯");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn all_double_border() {
|
||||
let rows = frame_rows(BorderStyle {
|
||||
top: LineStyle::Double,
|
||||
right: LineStyle::Double,
|
||||
bottom: LineStyle::Double,
|
||||
left: LineStyle::Double,
|
||||
rounded_corners: true,
|
||||
});
|
||||
assert!(rows[0].starts_with('╔'), "{}", rows[0]);
|
||||
assert!(rows[0].ends_with('╗'), "{}", rows[0]);
|
||||
assert_eq!(rows[1], "║║");
|
||||
assert_eq!(rows[2], "╚════════╝");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn heavy_and_double_corner_falls_back_to_the_horizontal_arm() {
|
||||
let rows = frame_rows(BorderStyle {
|
||||
top: LineStyle::Double,
|
||||
right: LineStyle::Heavy,
|
||||
bottom: LineStyle::Heavy,
|
||||
left: LineStyle::Heavy,
|
||||
rounded_corners: false,
|
||||
});
|
||||
assert!(rows[0].starts_with('╔'), "{}", rows[0]);
|
||||
assert!(rows[0].ends_with('╗'), "{}", rows[0]);
|
||||
assert_eq!(rows[1], "┃┃");
|
||||
assert_eq!(rows[2], "┗━━━━━━━━┛");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dashed_border_uses_light_corners() {
|
||||
let rows = frame_rows(BorderStyle {
|
||||
top: LineStyle::Dashed,
|
||||
right: LineStyle::Dashed,
|
||||
bottom: LineStyle::Dashed,
|
||||
left: LineStyle::Dashed,
|
||||
rounded_corners: false,
|
||||
});
|
||||
assert!(rows[0].starts_with('┌'), "{}", rows[0]);
|
||||
assert_eq!(rows[1], "┆┆");
|
||||
assert_eq!(rows[2], "└┄┄┄┄┄┄┄┄┘");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_text_full_includes_ctrl_scroll_when_enabled_for_tiled_pane() {
|
||||
let frame = pane_frame_with(true, false, 80);
|
||||
|
||||
@@ -6,7 +6,7 @@ use crate::ui::boundaries::Boundaries;
|
||||
use crate::ui::pane_boundaries_frame::{FrameParams, StackListEntry};
|
||||
use crate::ClientId;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style};
|
||||
use zellij_utils::data::{client_slot_to_colors, BorderStyle, InputMode, PaletteColor, Style};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::pane_size::PaneGeom;
|
||||
|
||||
@@ -25,6 +25,7 @@ pub struct PaneContentsAndUi<'a> {
|
||||
output: &'a mut Output,
|
||||
style: Style,
|
||||
focused_clients: Vec<ClientId>,
|
||||
client_display_slots: HashMap<ClientId, usize>,
|
||||
multiple_users_exist_in_session: bool,
|
||||
z_index: Option<usize>,
|
||||
pane_is_stacked_under: bool,
|
||||
@@ -62,6 +63,7 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
mouse_scroll_resize: bool,
|
||||
mouse_hover_tips: bool,
|
||||
dimmed_for_clients: HashSet<ClientId>,
|
||||
client_display_slots: &HashMap<ClientId, usize>,
|
||||
) -> Self {
|
||||
let mut focused_clients: Vec<ClientId> = active_panes
|
||||
.iter()
|
||||
@@ -69,6 +71,7 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
.map(|(c_id, _p_id)| *c_id)
|
||||
.collect();
|
||||
focused_clients.sort_unstable();
|
||||
let client_display_slots = client_display_slots.clone();
|
||||
let mouse_is_hovering_over_pane_for_clients = mouse_hover_pane_id
|
||||
.iter()
|
||||
.filter_map(|(client_id, pane_id)| {
|
||||
@@ -84,6 +87,7 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
output,
|
||||
style,
|
||||
focused_clients,
|
||||
client_display_slots,
|
||||
multiple_users_exist_in_session,
|
||||
z_index,
|
||||
pane_is_stacked_under,
|
||||
@@ -103,6 +107,12 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
dimmed_for_clients,
|
||||
}
|
||||
}
|
||||
fn display_slot_of(&self, client_id: ClientId) -> usize {
|
||||
self.client_display_slots
|
||||
.get(&client_id)
|
||||
.copied()
|
||||
.unwrap_or(0)
|
||||
}
|
||||
fn frame_is_dimmed_for_client(&self, client_id: ClientId) -> bool {
|
||||
self.dimmed_for_clients.contains(&client_id) && !self.focused_clients.contains(&client_id)
|
||||
}
|
||||
@@ -256,8 +266,8 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
.with_context(|| {
|
||||
format!("failed to render fake cursor if needed for client {client_id}")
|
||||
})?;
|
||||
if let Some(colors) = client_id_to_colors(
|
||||
*fake_cursor_client_id,
|
||||
if let Some(colors) = client_slot_to_colors(
|
||||
self.display_slot_of(*fake_cursor_client_id),
|
||||
self.style.colors.multiplayer_user_colors,
|
||||
) {
|
||||
let cursor_is_visible = self
|
||||
@@ -358,12 +368,15 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
});
|
||||
let frame_is_dimmed = self.frame_is_dimmed_for_client(client_id);
|
||||
let guest_choice_indicator = self.pane.guest_choice_indicator(client_id);
|
||||
let border_style = self.border_style(pane_is_floating);
|
||||
let frame_params = if session_is_mirrored {
|
||||
FrameParams {
|
||||
focused_client,
|
||||
is_main_client: pane_focused_for_client_id,
|
||||
other_focused_clients: vec![],
|
||||
other_focused_client_slots: vec![],
|
||||
style: self.style,
|
||||
border_style,
|
||||
color: frame_color.map(|c| c.0),
|
||||
other_cursors_exist_in_session: false,
|
||||
pane_is_stacked_over: self.pane_is_stacked_over,
|
||||
@@ -391,8 +404,13 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
FrameParams {
|
||||
focused_client,
|
||||
is_main_client: pane_focused_for_client_id,
|
||||
other_focused_client_slots: other_focused_clients
|
||||
.iter()
|
||||
.map(|c_id| self.display_slot_of(*c_id))
|
||||
.collect(),
|
||||
other_focused_clients,
|
||||
style: self.style,
|
||||
border_style,
|
||||
color: frame_color.map(|c| c.0),
|
||||
other_cursors_exist_in_session: self.multiple_users_exist_in_session,
|
||||
pane_is_stacked_over: self.pane_is_stacked_over,
|
||||
@@ -437,6 +455,14 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
pub fn border_style(&self, pane_is_floating: bool) -> BorderStyle {
|
||||
let base = if pane_is_floating {
|
||||
self.style.floating_border_style
|
||||
} else {
|
||||
self.style.border_style
|
||||
};
|
||||
self.pane.border_style_override().apply_to(base)
|
||||
}
|
||||
pub fn render_pane_boundaries(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
@@ -447,9 +473,11 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
pane_is_on_bottom_of_stack: bool,
|
||||
) {
|
||||
let color = self.frame_color(client_id, client_mode, session_is_mirrored);
|
||||
let pane_is_floating = false;
|
||||
boundaries.add_rect(
|
||||
self.pane.as_ref(),
|
||||
color,
|
||||
self.border_style(pane_is_floating),
|
||||
pane_is_on_top_of_stack,
|
||||
pane_is_on_bottom_of_stack,
|
||||
self.pane_is_stacked_under,
|
||||
@@ -483,8 +511,8 @@ impl<'a> PaneContentsAndUi<'a> {
|
||||
if session_is_mirrored || !self.multiple_users_exist_in_session {
|
||||
Some((self.style.colors.frame_selected.base, 3))
|
||||
} else {
|
||||
let colors = client_id_to_colors(
|
||||
client_id,
|
||||
let colors = client_slot_to_colors(
|
||||
self.display_slot_of(client_id),
|
||||
self.style.colors.multiplayer_user_colors,
|
||||
);
|
||||
colors.map(|colors| (colors.0, 3))
|
||||
|
||||
@@ -85,19 +85,18 @@ impl ServerOsApi for MockOsApi {
|
||||
fn send_to_client(&self, _: ClientId, _: ServerToClientMsg) -> anyhow::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_: ClientId,
|
||||
_: LocalSocketStream,
|
||||
) -> anyhow::Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_: ClientId,
|
||||
_: LocalSocketStream,
|
||||
_: LocalSocketStream,
|
||||
) -> anyhow::Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> anyhow::Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client(&mut self, _: ClientId) -> anyhow::Result<()> {
|
||||
|
||||
@@ -129,19 +129,18 @@ impl ServerOsApi for MockServerOsApi {
|
||||
fn send_to_client(&self, _client_id: ClientId, _msg: ServerToClientMsg) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client(&mut self, _client_id: ClientId) -> Result<()> {
|
||||
|
||||
@@ -236,19 +236,18 @@ impl ServerOsApi for FakeInputOutput {
|
||||
.push(msg);
|
||||
Ok(())
|
||||
}
|
||||
fn new_client(
|
||||
fn register_client(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
_receiver: &IpcReceiverWithContext<ClientToServerMsg>,
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn new_client_with_reply(
|
||||
fn register_client_with_reply(
|
||||
&mut self,
|
||||
_client_id: ClientId,
|
||||
_stream: LocalSocketStream,
|
||||
_reply_stream: LocalSocketStream,
|
||||
) -> Result<IpcReceiverWithContext<ClientToServerMsg>> {
|
||||
) -> Result<()> {
|
||||
unimplemented!()
|
||||
}
|
||||
fn remove_client(&mut self, _client_id: ClientId) -> Result<()> {
|
||||
@@ -408,7 +407,7 @@ fn seed_first_client_size(mut screen: Screen, size: Size) -> Screen {
|
||||
}
|
||||
|
||||
struct MockScreen {
|
||||
pub main_client_id: u16,
|
||||
pub main_client_id: ClientId,
|
||||
pub pty_receiver: Option<Receiver<(PtyInstruction, ErrorContext)>>,
|
||||
pub pty_writer_receiver: Option<Receiver<(PtyWriteInstruction, ErrorContext)>>,
|
||||
#[allow(dead_code)]
|
||||
@@ -1744,6 +1743,7 @@ fn open_new_floating_pane_with_custom_coordinates() {
|
||||
height: Some(PercentOrFixed::Fixed(2)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1780,6 +1780,7 @@ fn open_new_floating_pane_with_custom_coordinates_exceeding_viewport() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1816,6 +1817,7 @@ fn floating_pane_auto_centers_horizontally_with_only_width() {
|
||||
height: Some(PercentOrFixed::Fixed(10)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1852,6 +1854,7 @@ fn floating_pane_auto_centers_vertically_with_only_height() {
|
||||
height: Some(PercentOrFixed::Fixed(20)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1888,6 +1891,7 @@ fn floating_pane_auto_centers_both_axes_with_only_size() {
|
||||
height: Some(PercentOrFixed::Fixed(30)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1924,6 +1928,7 @@ fn floating_pane_respects_explicit_coordinates_with_size() {
|
||||
height: Some(PercentOrFixed::Fixed(30)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -1960,6 +1965,7 @@ fn floating_pane_centers_with_percentage_width() {
|
||||
height: Some(PercentOrFixed::Fixed(20)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -2001,6 +2007,7 @@ fn floating_pane_centers_large_pane_safely() {
|
||||
height: Some(PercentOrFixed::Fixed(50)),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
})),
|
||||
Some(1),
|
||||
None,
|
||||
@@ -2413,6 +2420,7 @@ fn group_panes_following_focus() {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -2474,6 +2482,7 @@ fn break_group_with_mouse() {
|
||||
NewPanePlacement::Tiled {
|
||||
direction: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
},
|
||||
Some(client_id),
|
||||
None,
|
||||
@@ -3572,6 +3581,7 @@ pub fn send_cli_new_pane_action_with_default_parameters() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3607,6 +3617,7 @@ pub fn web_new_pane_in_tab_action_targets_requested_tab() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
route_arbitrary_action_to_server(&session_metadata, action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -3682,6 +3693,7 @@ pub fn send_cli_new_pane_action_with_split_direction() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3738,6 +3750,7 @@ pub fn send_cli_new_pane_action_with_command_and_cwd() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3805,6 +3818,7 @@ pub fn send_cli_new_pane_action_with_floating_pane_and_coordinates() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3849,6 +3863,7 @@ pub fn send_cli_edit_action_with_default_parameters() {
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_edit_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3893,6 +3908,7 @@ pub fn send_cli_edit_action_with_line_number() {
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_edit_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -3937,6 +3953,7 @@ pub fn send_cli_edit_action_with_split_direction() {
|
||||
near_current_pane: false,
|
||||
no_focus: false,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_edit_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100)); // give time for actions to be
|
||||
@@ -5310,6 +5327,7 @@ pub fn send_cli_change_floating_pane_coordinates_action() {
|
||||
height: Some("10".to_owned()),
|
||||
pinned: None,
|
||||
borderless: Some(false),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(
|
||||
&session_metadata,
|
||||
@@ -5329,6 +5347,80 @@ pub fn send_cli_change_floating_pane_coordinates_action() {
|
||||
assert_snapshot!(format!("{}", snapshot_count));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn send_cli_set_pane_border_style_action() {
|
||||
let size = Size { cols: 80, rows: 10 };
|
||||
let client_id = 10;
|
||||
let mut mock_screen = MockScreen::new(size);
|
||||
let session_metadata = mock_screen.clone_session_metadata();
|
||||
let screen_thread = mock_screen.run(Some(TiledPaneLayout::default()), vec![]);
|
||||
|
||||
let received_server_instructions = Arc::new(Mutex::new(vec![]));
|
||||
let server_receiver = mock_screen.server_receiver.take().unwrap();
|
||||
let server_thread = log_actions_in_thread!(
|
||||
received_server_instructions,
|
||||
ServerInstruction::KillSession,
|
||||
server_receiver
|
||||
);
|
||||
let set_pane_border_style_action = CliAction::SetPaneBorderStyle {
|
||||
pane_id: "0".to_owned(),
|
||||
border_style: Some("double,rounded:false".to_owned()),
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, set_pane_border_style_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
mock_screen.teardown(vec![server_thread, screen_thread]);
|
||||
let snapshots = take_snapshots_and_cursor_coordinates_from_render_events(
|
||||
received_server_instructions.lock().unwrap().iter(),
|
||||
size,
|
||||
);
|
||||
let snapshot_count = snapshots.len();
|
||||
for (_cursor_coordinates, snapshot) in snapshots {
|
||||
assert_snapshot!(format!("{}", snapshot));
|
||||
}
|
||||
assert_snapshot!(format!("{}", snapshot_count));
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn set_pane_border_style_reports_an_unknown_pane() {
|
||||
let size = Size { cols: 80, rows: 10 };
|
||||
let client_id = 10;
|
||||
let mut mock_screen = MockScreen::new(size);
|
||||
let session_metadata = mock_screen.clone_session_metadata();
|
||||
let screen_thread = mock_screen.run(Some(TiledPaneLayout::default()), vec![]);
|
||||
let server_receiver = mock_screen.server_receiver.take().unwrap();
|
||||
let server_thread = std::thread::spawn(move || {
|
||||
while let Ok(instruction) = server_receiver.recv() {
|
||||
if matches!(instruction, (ServerInstruction::KillSession, _)) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let (_, completion) = route_action(
|
||||
Action::SetPaneBorderStyle {
|
||||
pane_id: PaneId::Terminal(999).into(),
|
||||
border_style: Default::default(),
|
||||
},
|
||||
client_id,
|
||||
None,
|
||||
None,
|
||||
session_metadata.senders.clone(),
|
||||
None,
|
||||
None,
|
||||
InputMode::Normal,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
let completion = completion.unwrap();
|
||||
assert_eq!(completion.exit_status, Some(1));
|
||||
assert_eq!(
|
||||
completion.error_message.as_deref(),
|
||||
Some("Pane with id Terminal(999) not found")
|
||||
);
|
||||
|
||||
mock_screen.teardown(vec![server_thread, screen_thread]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn go_to_tab_by_id_verifies_screen_state() {
|
||||
let size = Size {
|
||||
@@ -5633,6 +5725,7 @@ pub fn send_cli_new_pane_in_place_with_close_replaced_pane() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -5685,6 +5778,7 @@ pub fn send_cli_edit_in_place_with_close_replaced_pane() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: None,
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -8424,6 +8518,7 @@ pub fn send_cli_new_pane_action_with_tab_id() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -8487,6 +8582,7 @@ pub fn send_cli_new_floating_pane_action_with_tab_id() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -8537,6 +8633,7 @@ pub fn send_cli_edit_action_with_tab_id() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_edit_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -8599,6 +8696,7 @@ pub fn send_cli_new_pane_action_with_tab_id_and_direction() {
|
||||
no_focus: false,
|
||||
borderless: Some(false),
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
@@ -8661,6 +8759,7 @@ pub fn send_cli_new_pane_action_with_tab_id_and_stacked() {
|
||||
no_focus: false,
|
||||
borderless: None,
|
||||
tab_id: Some(0),
|
||||
border_style: None,
|
||||
};
|
||||
send_cli_action_to_server(&session_metadata, cli_new_pane_action, client_id);
|
||||
std::thread::sleep(std::time::Duration::from_millis(100));
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: None, borderless: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: None, borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: None, borderless: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: Some(100), cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: None, borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: Some(Down), borderless: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(OpenFile(OpenFilePayload { path: "/file/to/edit", line_number: None, cwd: Some("."), originating_plugin: None })), Some("Editing: /file/to/edit"), Tiled { direction: Some(Down), borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", new_pane_instruction)"
|
||||
---
|
||||
Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled { direction: Some(Right), borderless: Some(false) }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false))
|
||||
Some(SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Tiled { direction: Some(Right), borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false))
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled { direction: None, borderless: Some(false) }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled { direction: None, borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None, borderless: Some(false) })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(Some(RunCommand(RunCommand { command: "htop", args: [], cwd: Some("/some/folder"), hold_on_close: true, hold_on_start: false, originating_plugin: None, use_terminal_title: false })), None, Floating(Some(FloatingPaneCoordinates { x: Some(Fixed(10)), y: None, width: Some(Percent(20)), height: None, pinned: None, borderless: Some(false), border_style: None })), false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+1
-1
@@ -2,4 +2,4 @@
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
|
||||
---
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled { direction: Some(Right), borderless: Some(false) }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
[UpdateActivePane(Some(Terminal(0)), 1), UpdateActivePane(Some(Terminal(0)), 1), SpawnTerminal(None, None, Tiled { direction: Some(Right), borderless: Some(false), border_style: None }, false, ClientId(10), Some(NotificationEnd { channel: None, exit_status: None, unblock_condition: None, affected_pane_id: None, affected_tab_id: None, error_message: None, stdout_message: None }), false), Exit]
|
||||
|
||||
+3
-1
@@ -1,6 +1,5 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
assertion_line: 3515
|
||||
expression: "format!(\"{:#?}\", new_tab_action)"
|
||||
---
|
||||
Some(
|
||||
@@ -19,6 +18,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -37,6 +37,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -52,6 +53,7 @@ Some(
|
||||
split_size: None,
|
||||
run: None,
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:#?}\", new_tab_instruction)"
|
||||
expression: output
|
||||
---
|
||||
NewTab(
|
||||
None,
|
||||
@@ -21,6 +21,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -43,6 +44,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -65,6 +67,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -84,6 +87,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+5
-1
@@ -1,6 +1,6 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{:#?}\", new_tab_instruction)"
|
||||
expression: output
|
||||
---
|
||||
NewTab(
|
||||
None,
|
||||
@@ -21,6 +21,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -43,6 +44,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -65,6 +67,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -84,6 +87,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+4
@@ -21,6 +21,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -43,6 +44,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -65,6 +67,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
@@ -84,6 +87,7 @@ NewTab(
|
||||
),
|
||||
),
|
||||
borderless: None,
|
||||
border_style: None,
|
||||
focus: None,
|
||||
external_children_index: None,
|
||||
children_are_stacked: false,
|
||||
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{}\", snapshot)"
|
||||
---
|
||||
00 (C): ╔ Pane #1 ═════════════════════════════════════════════════════════════════════╗
|
||||
01 (C): ║ ║
|
||||
02 (C): ║ ║
|
||||
03 (C): ║ ║
|
||||
04 (C): ║ ║
|
||||
05 (C): ║ ║
|
||||
06 (C): ║ ║
|
||||
07 (C): ║ ║
|
||||
08 (C): ║ ║
|
||||
09 (C): ╚══════════════════════════════════════════════════════════════════════════════╝
|
||||
+5
@@ -0,0 +1,5 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{}\", snapshot_count)"
|
||||
---
|
||||
2
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
---
|
||||
source: zellij-server/src/./unit/screen_tests.rs
|
||||
expression: "format!(\"{}\", snapshot)"
|
||||
---
|
||||
00 (C): ┌ Pane #1 ─────────────────────────────────────────────────────────────────────┐
|
||||
01 (C): │ │
|
||||
02 (C): │ │
|
||||
03 (C): │ │
|
||||
04 (C): │ │
|
||||
05 (C): │ │
|
||||
06 (C): │ │
|
||||
07 (C): │ │
|
||||
08 (C): │ │
|
||||
09 (C): └──────────────────────────────────────────────────────────────────────────────┘
|
||||
@@ -1404,6 +1404,24 @@ pub fn next_swap_layout() {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Applies the named tiled [swap layout](https://zellij.dev/documentation/swap-layouts.html)
|
||||
/// without changing floating pane visibility or focus. Unknown or incompatible names are a no-op.
|
||||
pub fn apply_tiled_swap_layout(layout_name: &str) {
|
||||
let plugin_command = PluginCommand::ApplyTiledSwapLayout(layout_name.to_owned());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Applies the named floating [swap layout](https://zellij.dev/documentation/swap-layouts.html)
|
||||
/// without changing floating pane visibility or focus. Unknown or incompatible names are a no-op.
|
||||
pub fn apply_floating_swap_layout(layout_name: &str) {
|
||||
let plugin_command = PluginCommand::ApplyFloatingSwapLayout(layout_name.to_owned());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Change focus to the tab with the specified name
|
||||
pub fn go_to_tab_name(tab_name: &str) {
|
||||
let plugin_command = PluginCommand::GoToTabName(tab_name.to_owned());
|
||||
@@ -2585,6 +2603,13 @@ pub fn set_pane_borderless(pane_id: PaneId, borderless: bool) {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
pub fn set_pane_border_style(pane_id: PaneId, border_style: BorderStyleOverride) {
|
||||
let plugin_command = PluginCommand::SetPaneBorderStyle(pane_id, border_style);
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
/// Set the default foreground and/or background color of a pane
|
||||
///
|
||||
/// # Arguments
|
||||
|
||||
@@ -312,6 +312,33 @@ load_plugins {
|
||||
//
|
||||
// pane_frame_style "titles"
|
||||
|
||||
// Configure the look of the pane frame borders
|
||||
//
|
||||
// ui {
|
||||
// pane_frames {
|
||||
// // round the corners of single-line borders
|
||||
// rounded_corners true
|
||||
// // line style for all four sides of tiled pane frames
|
||||
// // one of: single, double, heavy, dashed, heavy_dashed
|
||||
// border_style "single"
|
||||
// // per-side overrides, eg. a double top border with single sides
|
||||
// border_top "double"
|
||||
// border_right "single"
|
||||
// border_bottom "single"
|
||||
// border_left "single"
|
||||
// // when two neighbouring panes disagree about a shared grid line
|
||||
// // (pane frames off), that line falls back to border_style above
|
||||
// // the same keys prefixed with "floating_" apply to floating panes,
|
||||
// // they default to the values above
|
||||
// floating_border_style "double"
|
||||
// floating_border_top "double"
|
||||
// floating_border_right "double"
|
||||
// floating_border_bottom "double"
|
||||
// floating_border_left "double"
|
||||
// floating_border_rounded_corners false
|
||||
// }
|
||||
// }
|
||||
|
||||
// Toggle between having Zellij lay out panes according to a predefined set of layouts whenever possible
|
||||
// Options:
|
||||
// - true (default)
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user