mirror of
https://github.com/zellij-org/zellij.git
synced 2026-10-11 22:50:07 +00:00
Resource usage optimizations (#5662)
* shared instance plugins * reduce needless allocations and share memory * fix: read buffer bug * idle plugin fixes and optimizations * trim allocator for glibc on pane close * fix flaky integration tests * refactor: naming * rustfmt * add pr
This commit is contained in:
@@ -11,3 +11,6 @@ linker = "x86_64-w64-mingw32-gcc"
|
||||
|
||||
[build]
|
||||
target-dir = "target"
|
||||
|
||||
[target.wasm32-wasip1]
|
||||
rustflags = ["-C", "link-arg=-zstack-size=262144"]
|
||||
|
||||
@@ -16,6 +16,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
|
||||
* fix: resize state and correctness (https://github.com/zellij-org/zellij/pull/5656)
|
||||
* fix: compress kitty images if the host terminal supports it (https://github.com/zellij-org/zellij/pull/5608)
|
||||
* fix: add websocket heartbeat to webserver to prevernt idle disconnect through proxies (https://github.com/zellij-org/zellij/pull/4633)
|
||||
* fix: memory-usage optimizations and multiple-pane-single-instance-plugins (https://github.com/zellij-org/zellij/pull/5662)
|
||||
|
||||
## [0.45.1] - 2026-08-28
|
||||
* fix: nested-session detection over SSH (https://github.com/zellij-org/zellij/pull/5522)
|
||||
|
||||
+3
-3
@@ -71,7 +71,7 @@ unaffected, since they embed the plugins from `zellij-utils/assets/plugins`.
|
||||
|
||||
## Packaging Zellij for a distribution
|
||||
|
||||
Builtin plugins (`status-bar`, `tab-bar`, ...) are `.wasm` files that are embedded into
|
||||
Builtin plugins (`bars`, `strider`, ...) are `.wasm` files that are embedded into
|
||||
the Zellij binary at compile time. Pre-built copies live in `zellij-utils/assets/plugins`
|
||||
so that `cargo install zellij` works without further tooling. Distributions that forbid
|
||||
pre-built binaries in source packages can strip that folder and build the plugins from
|
||||
@@ -80,9 +80,9 @@ source instead:
|
||||
```sh
|
||||
# 1. build the plugins from source (repeat for every plugin, or use `cargo x build --release`)
|
||||
cargo build --release --target wasm32-wasip1 \
|
||||
-p status-bar -p tab-bar -p compact-bar -p strider -p session-manager \
|
||||
-p bars -p strider -p session-manager \
|
||||
-p configuration -p plugin-manager -p about -p share -p multiple-select \
|
||||
-p layout-manager -p link
|
||||
-p layout-manager
|
||||
|
||||
# 2. install the resulting artifacts from <target-dir>/wasm32-wasip1/release/*.wasm
|
||||
# into $PREFIX/share/zellij/plugins/
|
||||
|
||||
Generated
+32
-47
@@ -382,6 +382,22 @@ dependencies = [
|
||||
"backtrace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bars"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"lazy_static",
|
||||
"rand 0.10.2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.19",
|
||||
"unicode-width 0.2.2",
|
||||
"zellij-tile",
|
||||
"zellij-tile-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.22.1"
|
||||
@@ -656,16 +672,6 @@ version = "0.6.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4dfdf9179d546b55ff3f88c9d93ecfaa3e9760163da5a1080af5243230dbbb70"
|
||||
|
||||
[[package]]
|
||||
name = "compact-bar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"unicode-width 0.2.2",
|
||||
"zellij-tile",
|
||||
"zellij-tile-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "concurrent-queue"
|
||||
version = "2.2.0"
|
||||
@@ -2121,13 +2127,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zellij-tile",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linux-raw-sys"
|
||||
version = "0.4.15"
|
||||
@@ -2938,9 +2937,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.8.1"
|
||||
version = "1.13.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370"
|
||||
checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "regex-automata"
|
||||
version = "0.4.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
@@ -2949,9 +2960,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex-syntax"
|
||||
version = "0.7.1"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c"
|
||||
checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
|
||||
|
||||
[[package]]
|
||||
name = "ring"
|
||||
@@ -3460,22 +3471,6 @@ version = "1.2.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
|
||||
|
||||
[[package]]
|
||||
name = "status-bar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"lazy_static",
|
||||
"rand 0.10.2",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.19",
|
||||
"unicode-width 0.2.2",
|
||||
"zellij-tile",
|
||||
"zellij-tile-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strider"
|
||||
version = "0.2.0"
|
||||
@@ -3645,16 +3640,6 @@ dependencies = [
|
||||
"winx",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tab-bar"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"unicode-width 0.2.2",
|
||||
"zellij-tile",
|
||||
"zellij-tile-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tempfile"
|
||||
version = "3.10.1"
|
||||
|
||||
+1
-4
@@ -42,10 +42,8 @@ vte = { workspace = true }
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"default-plugins/compact-bar",
|
||||
"default-plugins/status-bar",
|
||||
"default-plugins/bars",
|
||||
"default-plugins/strider",
|
||||
"default-plugins/tab-bar",
|
||||
"default-plugins/fixture-plugin-for-tests",
|
||||
"default-plugins/session-manager",
|
||||
"default-plugins/configuration",
|
||||
@@ -54,7 +52,6 @@ members = [
|
||||
"default-plugins/share",
|
||||
"default-plugins/multiple-select",
|
||||
"default-plugins/layout-manager",
|
||||
"default-plugins/link",
|
||||
"zellij-client",
|
||||
"zellij-integration-tests",
|
||||
"zellij-server",
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
[package]
|
||||
name = "status-bar"
|
||||
name = "bars"
|
||||
version = "0.1.0"
|
||||
authors = ["Brooks J Rady <[email protected]>"]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@ pub enum ActionType {
|
||||
Detach,
|
||||
Quit,
|
||||
NewStackedPane,
|
||||
Other(String), // Fallback for unhandled actions
|
||||
Other(String),
|
||||
}
|
||||
|
||||
impl ActionType {
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
use crate::LinePart;
|
||||
use crate::compact_bar::LinePart;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart {
|
||||
@@ -6,7 +6,7 @@ pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart {
|
||||
CopyDestination::Command => "Text piped to external command",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
CopyDestination::Primary => "Text copied to system primary selection",
|
||||
#[cfg(target_os = "macos")] // primary selection does not exist on macos
|
||||
#[cfg(target_os = "macos")]
|
||||
CopyDestination::Primary => "Text copied to system clipboard",
|
||||
CopyDestination::System => "Text copied to system clipboard",
|
||||
};
|
||||
+1
-19
@@ -1,4 +1,4 @@
|
||||
use crate::action_types::ActionType;
|
||||
use crate::compact_bar::action_types::ActionType;
|
||||
use std::collections::HashSet;
|
||||
use zellij_tile::prelude::actions::Action;
|
||||
use zellij_tile::prelude::*;
|
||||
@@ -6,7 +6,6 @@ use zellij_tile::prelude::*;
|
||||
pub struct KeybindProcessor;
|
||||
|
||||
impl KeybindProcessor {
|
||||
/// Find predetermined actions based on predicates while maintaining order
|
||||
pub fn find_predetermined_actions<F>(
|
||||
mode_info: &ModeInfo,
|
||||
mode: InputMode,
|
||||
@@ -19,16 +18,13 @@ impl KeybindProcessor {
|
||||
let keybinds = mode_info.get_keybinds_for_mode(mode);
|
||||
let mut processed_action_types = HashSet::new();
|
||||
|
||||
// Iterate through predicates in order to maintain the desired sequence
|
||||
for predicate in predicates {
|
||||
// Find the first matching action for this predicate
|
||||
let mut found_match = false;
|
||||
for (_key, actions) in &keybinds {
|
||||
if let Some(first_action) = actions.first() {
|
||||
if predicate(first_action) {
|
||||
let action_type = ActionType::from_action(first_action);
|
||||
|
||||
// Skip if we've already processed this action type
|
||||
if processed_action_types.contains(&action_type) {
|
||||
found_match = true;
|
||||
break;
|
||||
@@ -36,7 +32,6 @@ impl KeybindProcessor {
|
||||
|
||||
let mut matching_keys = Vec::new();
|
||||
|
||||
// Find all keys that match this action type (including different directions)
|
||||
for (inner_key, inner_actions) in &keybinds {
|
||||
if let Some(inner_first_action) = inner_actions.first() {
|
||||
if ActionType::from_action(inner_first_action) == action_type {
|
||||
@@ -49,7 +44,6 @@ impl KeybindProcessor {
|
||||
let description = action_type.description();
|
||||
let should_add_brackets_to_keys = mode != InputMode::Normal;
|
||||
|
||||
// Check if this is switching to normal mode
|
||||
let is_switching_to_locked = matches!(
|
||||
first_action,
|
||||
Action::SwitchToMode {
|
||||
@@ -72,7 +66,6 @@ impl KeybindProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
// If we found a match for this predicate, we've processed it
|
||||
if found_match {
|
||||
continue;
|
||||
}
|
||||
@@ -81,7 +74,6 @@ impl KeybindProcessor {
|
||||
result
|
||||
}
|
||||
|
||||
/// Group keys into sets and separate different key types with '|'
|
||||
fn group_key_sets(
|
||||
keys: &[String],
|
||||
should_add_brackets_to_keys: bool,
|
||||
@@ -91,7 +83,6 @@ impl KeybindProcessor {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Filter out Esc and Enter keys when switching to normal mode, but only if other keys exist
|
||||
let filtered_keys: Vec<String> = if is_switching_to_locked {
|
||||
let non_esc_enter_keys: Vec<String> = keys
|
||||
.iter()
|
||||
@@ -100,10 +91,8 @@ impl KeybindProcessor {
|
||||
.collect();
|
||||
|
||||
if non_esc_enter_keys.is_empty() {
|
||||
// If no other keys exist, keep the original keys
|
||||
keys.to_vec()
|
||||
} else {
|
||||
// Use filtered keys (without Esc/Enter)
|
||||
non_esc_enter_keys
|
||||
}
|
||||
} else {
|
||||
@@ -118,7 +107,6 @@ impl KeybindProcessor {
|
||||
};
|
||||
}
|
||||
|
||||
// Group keys by type
|
||||
let mut arrow_keys = Vec::new();
|
||||
let mut hjkl_lower = Vec::new();
|
||||
let mut hjkl_upper = Vec::new();
|
||||
@@ -160,7 +148,6 @@ impl KeybindProcessor {
|
||||
|
||||
let mut groups = Vec::new();
|
||||
|
||||
// Add hjkl group if present (prioritize hjkl over arrows)
|
||||
if !hjkl_lower.is_empty() {
|
||||
Self::sort_hjkl(&mut hjkl_lower);
|
||||
groups.push(Self::format_key_group(
|
||||
@@ -170,7 +157,6 @@ impl KeybindProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
// Add HJKL group if present
|
||||
if !hjkl_upper.is_empty() {
|
||||
Self::sort_hjkl_upper(&mut hjkl_upper);
|
||||
groups.push(Self::format_key_group(
|
||||
@@ -180,7 +166,6 @@ impl KeybindProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
// Add arrow keys group if present
|
||||
if !arrow_keys.is_empty() {
|
||||
Self::sort_arrows(&mut arrow_keys);
|
||||
groups.push(Self::format_key_group(
|
||||
@@ -217,7 +202,6 @@ impl KeybindProcessor {
|
||||
));
|
||||
}
|
||||
|
||||
// Add other keys with / separator
|
||||
if !other_keys.is_empty() {
|
||||
groups.push(other_keys.join("/"));
|
||||
}
|
||||
@@ -270,7 +254,6 @@ impl KeybindProcessor {
|
||||
let pos_b = order.iter().position(|&x| &x == b).unwrap_or(usize::MAX);
|
||||
pos_a.cmp(&pos_b)
|
||||
});
|
||||
// Remove "=" if both "+" and "=" are present
|
||||
if keys.contains(&"+") && keys.contains(&"=") {
|
||||
keys.retain(|k| k != &"=");
|
||||
}
|
||||
@@ -300,7 +283,6 @@ impl KeybindProcessor {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get predetermined actions for a specific mode
|
||||
pub fn get_predetermined_actions(
|
||||
mode_info: &ModeInfo,
|
||||
mode: InputMode,
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use ansi_term::ANSIStrings;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{LinePart, TabRenderData, ARROW_SEPARATOR};
|
||||
use crate::compact_bar::{LinePart, TabRenderData, ARROW_SEPARATOR};
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::style;
|
||||
|
||||
@@ -0,0 +1,857 @@
|
||||
mod action_types;
|
||||
mod clipboard_utils;
|
||||
mod keybind_utils;
|
||||
mod line;
|
||||
mod tab;
|
||||
mod tooltip;
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use tab::get_tab_to_focus;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::keybinds::KeybindStore;
|
||||
use crate::ClientSeed;
|
||||
use clipboard_utils::{system_clipboard_error, text_copied_hint};
|
||||
use line::tab_line;
|
||||
use tab::tab_style;
|
||||
use tooltip::TooltipRenderer;
|
||||
|
||||
static ARROW_SEPARATOR: &str = "";
|
||||
|
||||
const CONFIG_IS_TOOLTIP: &str = "is_tooltip";
|
||||
const CONFIG_TOGGLE_TOOLTIP_KEY: &str = "tooltip";
|
||||
const MSG_TOGGLE_TOOLTIP: &str = "toggle_tooltip";
|
||||
const MSG_TOGGLE_PERSISTED_TOOLTIP: &str = "toggle_persisted_tooltip";
|
||||
const MSG_LAUNCH_TOOLTIP: &str = "launch_tooltip_if_not_launched";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinePart {
|
||||
part: String,
|
||||
len: usize,
|
||||
tab_index: Option<usize>,
|
||||
}
|
||||
|
||||
struct TabRenderData {
|
||||
tabs: Vec<LinePart>,
|
||||
active_tab_index: usize,
|
||||
active_swap_layout_name: Option<String>,
|
||||
is_swap_layout_dirty: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SlotConfig {
|
||||
config: BTreeMap<String, String>,
|
||||
is_tooltip: bool,
|
||||
toggle_tooltip_key: Option<String>,
|
||||
persist: bool,
|
||||
is_first_run: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
tabs: Vec<TabInfo>,
|
||||
active_tab_idx: usize,
|
||||
mode_info: ModeInfo,
|
||||
display_area_rows: usize,
|
||||
display_area_cols: usize,
|
||||
text_copy_destination: Option<CopyDestination>,
|
||||
display_system_clipboard_failure: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct SlotClientState {
|
||||
tab_line: Vec<LinePart>,
|
||||
breadcrumb_range: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CompactBar {
|
||||
slots: BTreeMap<SlotId, SlotConfig>,
|
||||
slot_tabs: BTreeMap<SlotId, usize>,
|
||||
clients: BTreeMap<ClientId, ClientState>,
|
||||
slot_clients: BTreeMap<(SlotId, ClientId), SlotClientState>,
|
||||
tooltip_is_active: bool,
|
||||
configured_toggle_keys: BTreeSet<(String, ClientId)>,
|
||||
}
|
||||
|
||||
impl CompactBar {
|
||||
pub fn has_slots(&self) -> bool {
|
||||
!self.slots.is_empty()
|
||||
}
|
||||
|
||||
pub fn slot_added(&mut self, slot: &Slot) {
|
||||
let config = slot.configuration.clone();
|
||||
let is_tooltip = config
|
||||
.get(CONFIG_IS_TOOLTIP)
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(false);
|
||||
let toggle_tooltip_key = if is_tooltip {
|
||||
None
|
||||
} else {
|
||||
config.get(CONFIG_TOGGLE_TOOLTIP_KEY).cloned()
|
||||
};
|
||||
let events = if is_tooltip {
|
||||
vec![
|
||||
EventType::ModeUpdate,
|
||||
EventType::TabUpdate,
|
||||
EventType::InitialKeybinds,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
EventType::TabUpdate,
|
||||
EventType::PaneUpdate,
|
||||
EventType::ModeUpdate,
|
||||
EventType::Mouse,
|
||||
EventType::CopyToClipboard,
|
||||
EventType::InputReceived,
|
||||
EventType::SystemClipboardFailure,
|
||||
EventType::InitialKeybinds,
|
||||
]
|
||||
};
|
||||
subscribe(&events);
|
||||
self.slots.insert(
|
||||
slot.id,
|
||||
SlotConfig {
|
||||
config,
|
||||
is_tooltip,
|
||||
toggle_tooltip_key,
|
||||
persist: false,
|
||||
is_first_run: is_tooltip,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
self.slots.remove(&slot_id);
|
||||
self.slot_tabs.remove(&slot_id);
|
||||
self.slot_clients.retain(|(s, _), _| *s != slot_id);
|
||||
}
|
||||
|
||||
pub fn client_connected(&mut self, client_id: ClientId) {
|
||||
self.clients.entry(client_id).or_default();
|
||||
}
|
||||
|
||||
pub fn client_disconnected(&mut self, client_id: ClientId) {
|
||||
self.clients.remove(&client_id);
|
||||
self.slot_clients.retain(|(_, c), _| *c != client_id);
|
||||
self.configured_toggle_keys.retain(|(_, c)| *c != client_id);
|
||||
}
|
||||
|
||||
pub fn client_ids(&self) -> impl Iterator<Item = ClientId> + '_ {
|
||||
self.clients.keys().copied()
|
||||
}
|
||||
|
||||
fn swap_keybinds(&mut self, client_id: ClientId, keybinds: &mut KeybindStore) -> bool {
|
||||
match self.clients.get_mut(&client_id) {
|
||||
Some(client) => keybinds.swap(client_id, &mut client.mode_info.keybinds),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.clients.clear();
|
||||
self.slot_clients.clear();
|
||||
self.slot_tabs.clear();
|
||||
self.tooltip_is_active = false;
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> BTreeMap<ClientId, ClientSeed> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(client_id, client)| {
|
||||
(
|
||||
*client_id,
|
||||
ClientSeed {
|
||||
mode_info: client.mode_info.clone(),
|
||||
tabs: client.tabs.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn seed(&mut self, seeds: &BTreeMap<ClientId, ClientSeed>) {
|
||||
for (client_id, seed) in seeds {
|
||||
let client = self.clients.entry(*client_id).or_default();
|
||||
client.mode_info = seed.mode_info.clone();
|
||||
update_display_area(client, &seed.tabs);
|
||||
if let Some(active_tab_index) = seed.tabs.iter().position(|t| t.active) {
|
||||
client.active_tab_idx = active_tab_index + 1;
|
||||
client.tabs = seed.tabs.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_toggle_keybinds(&mut self, client_id: Option<ClientId>) {
|
||||
let Some(client_id) = client_id else {
|
||||
return;
|
||||
};
|
||||
for slot in self.slots.values() {
|
||||
if slot.is_tooltip {
|
||||
continue;
|
||||
}
|
||||
if let Some(toggle_key) = &slot.toggle_tooltip_key {
|
||||
let entry = (toggle_key.clone(), client_id);
|
||||
if !self.configured_toggle_keys.contains(&entry) {
|
||||
reconfigure(bind_toggle_key_config(toggle_key, client_id), false);
|
||||
self.configured_toggle_keys.insert(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn has_toggle_key(&self) -> bool {
|
||||
self.slots
|
||||
.values()
|
||||
.any(|s| !s.is_tooltip && s.toggle_tooltip_key.is_some())
|
||||
}
|
||||
|
||||
fn main_slot_ids(&self) -> Vec<SlotId> {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|(_, s)| !s.is_tooltip)
|
||||
.map(|(id, _)| *id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tooltip_slot_ids(&self) -> Vec<SlotId> {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|(_, s)| s.is_tooltip)
|
||||
.map(|(id, _)| *id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn active_tab_index_of(&self, client_id: ClientId) -> Option<usize> {
|
||||
self.clients
|
||||
.get(&client_id)
|
||||
.filter(|c| c.active_tab_idx > 0)
|
||||
.map(|c| c.active_tab_idx - 1)
|
||||
}
|
||||
|
||||
fn tooltip_slots_in_tab(&self, tab_index: Option<usize>) -> Vec<SlotId> {
|
||||
self.tooltip_slot_ids()
|
||||
.into_iter()
|
||||
.filter(|id| match (tab_index, self.slot_tabs.get(id)) {
|
||||
(Some(tab_index), Some(slot_tab)) => *slot_tab == tab_index,
|
||||
_ => true,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn tooltip_slots_for_client(&self, client_id: ClientId) -> Vec<SlotId> {
|
||||
self.tooltip_slots_in_tab(self.active_tab_index_of(client_id))
|
||||
}
|
||||
|
||||
fn main_slot_for_client(&self, client_id: ClientId) -> Option<SlotId> {
|
||||
let active_tab_index = self.active_tab_index_of(client_id);
|
||||
let candidates: Vec<SlotId> = self
|
||||
.slots
|
||||
.iter()
|
||||
.filter(|(_, s)| !s.is_tooltip && s.toggle_tooltip_key.is_some())
|
||||
.map(|(id, _)| *id)
|
||||
.collect();
|
||||
candidates
|
||||
.iter()
|
||||
.find(|id| {
|
||||
active_tab_index.is_some() && self.slot_tabs.get(*id) == active_tab_index.as_ref()
|
||||
})
|
||||
.or_else(|| candidates.first())
|
||||
.copied()
|
||||
}
|
||||
|
||||
fn main_slot_in_active_tab(&self, client_id: ClientId) -> Option<SlotId> {
|
||||
let active_tab_index = self
|
||||
.clients
|
||||
.get(&client_id)
|
||||
.map(|c| c.active_tab_idx.saturating_sub(1))?;
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|(_, s)| !s.is_tooltip && s.toggle_tooltip_key.is_some())
|
||||
.map(|(id, _)| *id)
|
||||
.find(|id| self.slot_tabs.get(id) == Some(&active_tab_index))
|
||||
}
|
||||
|
||||
pub fn update(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
context: EventContext,
|
||||
keybinds: &mut KeybindStore,
|
||||
) -> RenderResponse {
|
||||
match event {
|
||||
Event::ModeUpdate(_) | Event::TabUpdate(_) | Event::InitialKeybinds(_) => {
|
||||
for slot in self.slots.values_mut() {
|
||||
slot.is_first_run = false;
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
match event {
|
||||
Event::PaneUpdate(pane_manifest) => {
|
||||
if self.handle_pane_update(pane_manifest) {
|
||||
RenderResponse::Slots(self.main_slot_ids())
|
||||
} else {
|
||||
RenderResponse::Nothing
|
||||
}
|
||||
},
|
||||
Event::Mouse(mouse_event) => {
|
||||
if let (Some(slot_id), Some(client_id)) = (context.slot_id, context.client_id) {
|
||||
self.handle_mouse_event(slot_id, client_id, mouse_event);
|
||||
}
|
||||
RenderResponse::Nothing
|
||||
},
|
||||
_ => {
|
||||
let target_clients: Vec<ClientId> = match context.client_id {
|
||||
Some(client_id) => vec![client_id],
|
||||
None => self.clients.keys().copied().collect(),
|
||||
};
|
||||
let mut should_render = false;
|
||||
for client_id in target_clients {
|
||||
if self.update_client(event, client_id, keybinds) {
|
||||
should_render = true;
|
||||
}
|
||||
}
|
||||
if !should_render {
|
||||
RenderResponse::Nothing
|
||||
} else if let Some(client_id) = context.client_id {
|
||||
RenderResponse::Client(client_id)
|
||||
} else {
|
||||
RenderResponse::Slots(self.slots.keys().copied().collect())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn update_client(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
client_id: ClientId,
|
||||
keybinds: &mut KeybindStore,
|
||||
) -> bool {
|
||||
match event {
|
||||
Event::InitialKeybinds(_) => {
|
||||
self.clients.entry(client_id).or_default();
|
||||
true
|
||||
},
|
||||
Event::ModeUpdate(mode_info) => {
|
||||
self.handle_mode_update(client_id, mode_info.clone(), keybinds)
|
||||
},
|
||||
Event::TabUpdate(tabs) => self.handle_tab_update(client_id, tabs),
|
||||
Event::CopyToClipboard(copy_destination) => {
|
||||
self.handle_clipboard_copy(client_id, *copy_destination)
|
||||
},
|
||||
Event::SystemClipboardFailure => self.handle_clipboard_failure(client_id),
|
||||
Event::InputReceived => self.handle_input_received(client_id),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mode_update(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
mode_info: ModeInfo,
|
||||
keybinds: &mut KeybindStore,
|
||||
) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
let should_render = client.mode_info != mode_info;
|
||||
let old_mode = client.mode_info.mode;
|
||||
let new_mode = mode_info.mode;
|
||||
let base_mode = mode_info.base_mode.unwrap_or(InputMode::Normal);
|
||||
|
||||
client.mode_info = mode_info;
|
||||
|
||||
let lent = self.swap_keybinds(client_id, keybinds);
|
||||
for slot_id in self.tooltip_slots_for_client(client_id) {
|
||||
self.handle_tooltip_mode_update(slot_id, client_id, old_mode, new_mode, base_mode);
|
||||
}
|
||||
self.handle_main_mode_update(client_id, new_mode, base_mode);
|
||||
if lent {
|
||||
self.swap_keybinds(client_id, keybinds);
|
||||
}
|
||||
|
||||
should_render
|
||||
}
|
||||
|
||||
fn handle_main_mode_update(
|
||||
&self,
|
||||
client_id: ClientId,
|
||||
new_mode: InputMode,
|
||||
base_mode: InputMode,
|
||||
) {
|
||||
if self.has_toggle_key() && new_mode != base_mode && !is_restricted_mode(new_mode) {
|
||||
if let Some(slot_id) = self.main_slot_for_client(client_id) {
|
||||
self.launch_tooltip_if_not_launched(slot_id, client_id, new_mode);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tooltip_mode_update(
|
||||
&mut self,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
old_mode: InputMode,
|
||||
new_mode: InputMode,
|
||||
base_mode: InputMode,
|
||||
) {
|
||||
let persist = self.slots.get(&slot_id).map(|s| s.persist).unwrap_or(false);
|
||||
if !persist && (new_mode == base_mode || is_restricted_mode(new_mode)) {
|
||||
let _ = close_slot(slot_id);
|
||||
} else if new_mode != old_mode || persist {
|
||||
self.update_tooltip_for_mode_change(slot_id, client_id, new_mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tab_update(&mut self, client_id: ClientId, tabs: &[TabInfo]) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
update_display_area(client, tabs);
|
||||
|
||||
if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
|
||||
let active_tab_idx = active_tab_index + 1;
|
||||
let previous_active_tab_idx = client.active_tab_idx;
|
||||
let should_render =
|
||||
client.active_tab_idx != active_tab_idx || client.tabs.as_slice() != tabs;
|
||||
|
||||
client.active_tab_idx = active_tab_idx;
|
||||
client.tabs = tabs.to_vec();
|
||||
|
||||
if previous_active_tab_idx != 0 && previous_active_tab_idx != active_tab_idx {
|
||||
let tooltips = self.tooltip_slots_in_tab(Some(previous_active_tab_idx - 1));
|
||||
for slot_id in tooltips {
|
||||
self.move_tooltip_to_new_tab(slot_id, active_tab_idx);
|
||||
}
|
||||
}
|
||||
|
||||
should_render
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pane_update(&mut self, pane_manifest: &PaneManifest) -> bool {
|
||||
for (tab_index, panes) in &pane_manifest.panes {
|
||||
for pane in panes {
|
||||
if pane.is_plugin && self.slots.contains_key(&pane.id) {
|
||||
self.slot_tabs.insert(pane.id, *tab_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.has_toggle_key() {
|
||||
let previous_tooltip_state = self.tooltip_is_active;
|
||||
self.tooltip_is_active = self.detect_tooltip_presence(pane_manifest);
|
||||
previous_tooltip_state != self.tooltip_is_active
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, slot_id: SlotId, client_id: ClientId, mouse_event: &Mouse) {
|
||||
if self
|
||||
.slots
|
||||
.get(&slot_id)
|
||||
.map(|s| s.is_tooltip)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
let Some(client) = self.clients.get(&client_id) else {
|
||||
return;
|
||||
};
|
||||
let empty_slot_client = SlotClientState::default();
|
||||
let slot_client = self
|
||||
.slot_clients
|
||||
.get(&(slot_id, client_id))
|
||||
.unwrap_or(&empty_slot_client);
|
||||
|
||||
match mouse_event {
|
||||
Mouse::LeftClick(_, col) => {
|
||||
let col = *col;
|
||||
if matches!(slot_client.breadcrumb_range, Some((start, end)) if col >= start && col < end)
|
||||
{
|
||||
focus_host_session();
|
||||
} else if let Some(tab_idx) =
|
||||
get_tab_to_focus(&slot_client.tab_line, client.active_tab_idx, col)
|
||||
{
|
||||
switch_tab_to(tab_idx.try_into().unwrap());
|
||||
}
|
||||
},
|
||||
Mouse::ScrollUp(_) => {
|
||||
let next_tab = min(client.active_tab_idx + 1, client.tabs.len());
|
||||
switch_tab_to(next_tab as u32);
|
||||
},
|
||||
Mouse::ScrollDown(_) => {
|
||||
let prev_tab = max(client.active_tab_idx.saturating_sub(1), 1);
|
||||
switch_tab_to(prev_tab as u32);
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_clipboard_copy(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
copy_destination: CopyDestination,
|
||||
) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
let should_render = match client.text_copy_destination {
|
||||
Some(current) => current != copy_destination,
|
||||
None => true,
|
||||
};
|
||||
|
||||
client.text_copy_destination = Some(copy_destination);
|
||||
should_render
|
||||
}
|
||||
|
||||
fn handle_clipboard_failure(&mut self, client_id: ClientId) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
client.display_system_clipboard_failure = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_input_received(&mut self, client_id: ClientId) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
let should_render =
|
||||
client.text_copy_destination.is_some() || client.display_system_clipboard_failure;
|
||||
client.text_copy_destination = None;
|
||||
client.display_system_clipboard_failure = false;
|
||||
should_render
|
||||
}
|
||||
|
||||
pub fn pipe(
|
||||
&mut self,
|
||||
message: PipeMessage,
|
||||
context: EventContext,
|
||||
keybinds: &mut KeybindStore,
|
||||
) -> RenderResponse {
|
||||
if let Some(slot_id) = context.slot_id {
|
||||
if self
|
||||
.slots
|
||||
.get(&slot_id)
|
||||
.map(|s| s.is_tooltip)
|
||||
.unwrap_or(false)
|
||||
{
|
||||
if message.is_private {
|
||||
self.handle_tooltip_pipe(slot_id, &message);
|
||||
}
|
||||
return RenderResponse::Nothing;
|
||||
}
|
||||
}
|
||||
if message.name == MSG_TOGGLE_TOOLTIP && message.is_private {
|
||||
let client_id = message
|
||||
.payload
|
||||
.as_ref()
|
||||
.and_then(|p| p.trim().parse::<ClientId>().ok());
|
||||
if let Some(client_id) = client_id {
|
||||
if let Some(slot_id) = self.main_slot_in_active_tab(client_id) {
|
||||
let mode = self
|
||||
.clients
|
||||
.get(&client_id)
|
||||
.map(|c| c.mode_info.mode)
|
||||
.unwrap_or(InputMode::Normal);
|
||||
let lent = self.swap_keybinds(client_id, keybinds);
|
||||
self.toggle_persisted_tooltip(slot_id, client_id, mode);
|
||||
if lent {
|
||||
self.swap_keybinds(client_id, keybinds);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if message.is_private
|
||||
&& (message.name == MSG_TOGGLE_PERSISTED_TOOLTIP || message.name == MSG_LAUNCH_TOOLTIP)
|
||||
{
|
||||
let targets = match context.client_id {
|
||||
Some(client_id) => self.tooltip_slots_for_client(client_id),
|
||||
None => self.tooltip_slot_ids(),
|
||||
};
|
||||
for slot_id in targets {
|
||||
self.handle_tooltip_pipe(slot_id, &message);
|
||||
}
|
||||
}
|
||||
RenderResponse::Nothing
|
||||
}
|
||||
|
||||
fn handle_tooltip_pipe(&mut self, slot_id: SlotId, message: &PipeMessage) {
|
||||
if message.name == MSG_TOGGLE_PERSISTED_TOOLTIP {
|
||||
let Some(slot) = self.slots.get_mut(&slot_id) else {
|
||||
return;
|
||||
};
|
||||
if slot.is_first_run {
|
||||
slot.persist = true;
|
||||
} else {
|
||||
#[cfg(target_family = "wasm")]
|
||||
let _ = close_slot(slot_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_persisted_tooltip(&self, slot_id: SlotId, client_id: ClientId, new_mode: InputMode) {
|
||||
#[allow(unused_variables)]
|
||||
let message = self
|
||||
.create_tooltip_message(slot_id, client_id, MSG_TOGGLE_PERSISTED_TOOLTIP, new_mode)
|
||||
.with_args(create_persist_args());
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pipe_message_to_plugin(message);
|
||||
}
|
||||
|
||||
fn launch_tooltip_if_not_launched(
|
||||
&self,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
new_mode: InputMode,
|
||||
) {
|
||||
let message = self.create_tooltip_message(slot_id, client_id, MSG_LAUNCH_TOOLTIP, new_mode);
|
||||
pipe_message_to_plugin(message);
|
||||
}
|
||||
|
||||
fn create_tooltip_message(
|
||||
&self,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
name: &str,
|
||||
mode: InputMode,
|
||||
) -> MessageToPlugin {
|
||||
let mut tooltip_config = self
|
||||
.slots
|
||||
.get(&slot_id)
|
||||
.map(|s| s.config.clone())
|
||||
.unwrap_or_default();
|
||||
tooltip_config.insert(CONFIG_IS_TOOLTIP.to_string(), "true".to_string());
|
||||
|
||||
MessageToPlugin::new(name)
|
||||
.with_plugin_url("zellij:OWN_URL")
|
||||
.with_plugin_config(tooltip_config)
|
||||
.with_floating_pane_coordinates(self.calculate_tooltip_coordinates(client_id))
|
||||
.new_plugin_instance_should_have_pane_title(format!("{:?}", mode))
|
||||
}
|
||||
|
||||
fn update_tooltip_for_mode_change(
|
||||
&self,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
new_mode: InputMode,
|
||||
) {
|
||||
let coordinates = self.calculate_tooltip_coordinates(client_id);
|
||||
change_floating_panes_coordinates(vec![(PaneId::Plugin(slot_id), coordinates)]);
|
||||
rename_plugin_pane(slot_id, format!("{:?}", new_mode));
|
||||
}
|
||||
|
||||
fn move_tooltip_to_new_tab(&mut self, slot_id: SlotId, new_tab_index: usize) {
|
||||
break_panes_to_tab_with_index(
|
||||
&[PaneId::Plugin(slot_id)],
|
||||
new_tab_index.saturating_sub(1),
|
||||
false,
|
||||
);
|
||||
self.slot_tabs
|
||||
.insert(slot_id, new_tab_index.saturating_sub(1));
|
||||
}
|
||||
|
||||
fn calculate_tooltip_coordinates(&self, client_id: ClientId) -> FloatingPaneCoordinates {
|
||||
let default_client = ClientState::default();
|
||||
let client = self.clients.get(&client_id).unwrap_or(&default_client);
|
||||
let tooltip_renderer = TooltipRenderer::new(&client.mode_info);
|
||||
let (tooltip_rows, tooltip_cols) =
|
||||
tooltip_renderer.calculate_dimensions(client.mode_info.mode);
|
||||
|
||||
let width = tooltip_cols + 4;
|
||||
let height = tooltip_rows + 2;
|
||||
let x_position = 2;
|
||||
let y_position = client.display_area_rows.saturating_sub(height + 2);
|
||||
|
||||
FloatingPaneCoordinates::new(
|
||||
Some(x_position.to_string()),
|
||||
Some(y_position.to_string()),
|
||||
Some(width.to_string()),
|
||||
Some(height.to_string()),
|
||||
Some(true),
|
||||
Some(false),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn detect_tooltip_presence(&self, pane_manifest: &PaneManifest) -> bool {
|
||||
for (_tab_index, panes) in &pane_manifest.panes {
|
||||
for pane in panes {
|
||||
if pane.is_plugin
|
||||
&& self
|
||||
.slots
|
||||
.get(&pane.id)
|
||||
.map(|s| s.is_tooltip)
|
||||
.unwrap_or(false)
|
||||
&& pane.pane_x != pane.pane_content_x
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
keybinds: &mut KeybindStore,
|
||||
) {
|
||||
let lent = self.swap_keybinds(client_id, keybinds);
|
||||
self.render_client(rows, cols, slot_id, client_id);
|
||||
if lent {
|
||||
self.swap_keybinds(client_id, keybinds);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_client(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
|
||||
let Some(slot) = self.slots.get(&slot_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(client) = self.clients.get(&client_id) else {
|
||||
return;
|
||||
};
|
||||
if slot.is_tooltip {
|
||||
let tooltip_renderer = TooltipRenderer::new(&client.mode_info);
|
||||
tooltip_renderer.render(rows, cols);
|
||||
return;
|
||||
}
|
||||
if let Some(copy_destination) = client.text_copy_destination {
|
||||
let hint = text_copied_hint(copy_destination).part;
|
||||
render_background_with_text(&client.mode_info, &hint);
|
||||
} else if client.display_system_clipboard_failure {
|
||||
let hint = system_clipboard_error().part;
|
||||
render_background_with_text(&client.mode_info, &hint);
|
||||
} else {
|
||||
if client.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tab_data = prepare_tab_data(client);
|
||||
let tab_line_output = tab_line(
|
||||
&client.mode_info,
|
||||
tab_data,
|
||||
cols,
|
||||
slot.toggle_tooltip_key.clone(),
|
||||
self.tooltip_is_active,
|
||||
);
|
||||
let slot_client = self.slot_clients.entry((slot_id, client_id)).or_default();
|
||||
slot_client.tab_line = tab_line_output.parts;
|
||||
slot_client.breadcrumb_range = tab_line_output.breadcrumb_range;
|
||||
|
||||
let output = slot_client
|
||||
.tab_line
|
||||
.iter()
|
||||
.fold(String::new(), |acc, part| acc + &part.part);
|
||||
|
||||
render_background_with_text(&client.mode_info, &output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn update_display_area(client: &mut ClientState, tabs: &[TabInfo]) {
|
||||
for tab in tabs {
|
||||
if tab.active {
|
||||
client.display_area_rows = tab.display_area_rows;
|
||||
client.display_area_cols = tab.display_area_columns;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_restricted_mode(mode: InputMode) -> bool {
|
||||
matches!(
|
||||
mode,
|
||||
InputMode::Locked
|
||||
| InputMode::EnterSearch
|
||||
| InputMode::RenameTab
|
||||
| InputMode::RenamePane
|
||||
| InputMode::Prompt
|
||||
| InputMode::Tmux
|
||||
)
|
||||
}
|
||||
|
||||
fn create_persist_args() -> BTreeMap<String, String> {
|
||||
let mut args = BTreeMap::new();
|
||||
args.insert("persist".to_string(), String::new());
|
||||
args
|
||||
}
|
||||
|
||||
fn render_background_with_text(mode_info: &ModeInfo, text: &str) {
|
||||
let background = mode_info.style.colors.text_unselected.background;
|
||||
match background {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", text, r, g, b);
|
||||
},
|
||||
PaletteColor::EightBit(color) => {
|
||||
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", text, color);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn prepare_tab_data(client: &ClientState) -> TabRenderData {
|
||||
let mut all_tabs = Vec::new();
|
||||
let mut active_tab_index = 0;
|
||||
let mut active_swap_layout_name = None;
|
||||
let mut is_swap_layout_dirty = false;
|
||||
let mut is_alternate_tab = false;
|
||||
let dimmed = client.mode_info.session_ascended == Some(true)
|
||||
|| client.mode_info.session_dimmed == Some(true);
|
||||
|
||||
for tab in &client.tabs {
|
||||
let tab_name = get_tab_display_name(client, tab);
|
||||
|
||||
if tab.active {
|
||||
active_tab_index = tab.position;
|
||||
if client.mode_info.mode != InputMode::RenameTab {
|
||||
is_swap_layout_dirty = tab.is_swap_layout_dirty;
|
||||
active_swap_layout_name = tab.active_swap_layout_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let styled_tab = tab_style(
|
||||
tab_name,
|
||||
tab,
|
||||
is_alternate_tab,
|
||||
client.mode_info.style.colors,
|
||||
client.mode_info.capabilities,
|
||||
dimmed,
|
||||
);
|
||||
|
||||
is_alternate_tab = !is_alternate_tab;
|
||||
all_tabs.push(styled_tab);
|
||||
}
|
||||
|
||||
TabRenderData {
|
||||
tabs: all_tabs,
|
||||
active_tab_index,
|
||||
active_swap_layout_name,
|
||||
is_swap_layout_dirty,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tab_display_name(client: &ClientState, tab: &TabInfo) -> String {
|
||||
let mut tab_name = tab.name.clone();
|
||||
if tab.active && client.mode_info.mode == InputMode::RenameTab && tab_name.is_empty() {
|
||||
tab_name = "Enter name...".to_string();
|
||||
}
|
||||
tab_name
|
||||
}
|
||||
|
||||
fn bind_toggle_key_config(toggle_key: &str, client_id: ClientId) -> String {
|
||||
format!(
|
||||
r#"
|
||||
keybinds {{
|
||||
shared {{
|
||||
bind "{}" {{
|
||||
MessagePlugin "compact-bar" {{
|
||||
name "toggle_tooltip"
|
||||
tooltip "{}"
|
||||
payload "{}"
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
toggle_key, toggle_key, client_id
|
||||
)
|
||||
}
|
||||
+3
-6
@@ -1,4 +1,4 @@
|
||||
use crate::{line::tab_separator, LinePart};
|
||||
use crate::compact_bar::{line::tab_separator, LinePart};
|
||||
use ansi_term::{ANSIString, ANSIStrings};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use zellij_tile::prelude::*;
|
||||
@@ -8,7 +8,6 @@ fn cursors<'a>(
|
||||
focused_client_slots: &'a [usize],
|
||||
colors: MultiplayerColors,
|
||||
) -> (Vec<ANSIString<'a>>, usize) {
|
||||
// cursor section, text length
|
||||
let mut len = 0;
|
||||
let mut cursors = vec![];
|
||||
for display_slot in focused_client_slots.iter() {
|
||||
@@ -17,7 +16,7 @@ fn cursors<'a>(
|
||||
len += 1;
|
||||
}
|
||||
}
|
||||
len += 2; // 2 for the brackets: [ and ]
|
||||
len += 2;
|
||||
(cursors, len)
|
||||
}
|
||||
|
||||
@@ -66,7 +65,7 @@ pub fn render_tab(
|
||||
style!(foreground_color, background_color).bold()
|
||||
};
|
||||
let left_separator = style!(separator_fill_color, background_color).paint(separator);
|
||||
let mut tab_text_len = text.width() + (separator_width * 2) + 2; // + 2 for padding
|
||||
let mut tab_text_len = text.width() + (separator_width * 2) + 2;
|
||||
|
||||
let tab_styled_text = text_style.paint(format!(" {} ", text));
|
||||
|
||||
@@ -115,7 +114,6 @@ pub fn tab_style(
|
||||
if tab.has_bell_notification || tab.is_flashing_bell {
|
||||
tabname.push_str(" [!]");
|
||||
}
|
||||
// we only color alternate tabs differently if we can't use the arrow fonts to separate them
|
||||
if !capabilities.arrow_fonts {
|
||||
is_alternate_tab = false;
|
||||
}
|
||||
@@ -130,7 +128,6 @@ pub(crate) fn get_tab_to_focus(
|
||||
) -> Option<usize> {
|
||||
let clicked_line_part = get_clicked_line_part(tab_line, mouse_click_col)?;
|
||||
let clicked_tab_idx = clicked_line_part.tab_index?;
|
||||
// tabs are indexed starting from 1 so we need to add 1
|
||||
let clicked_tab_idx = clicked_tab_idx + 1;
|
||||
if clicked_tab_idx != active_tab_idx {
|
||||
return Some(clicked_tab_idx);
|
||||
+4
-6
@@ -1,4 +1,4 @@
|
||||
use crate::keybind_utils::KeybindProcessor;
|
||||
use crate::compact_bar::keybind_utils::KeybindProcessor;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub struct TooltipRenderer<'a> {
|
||||
@@ -25,8 +25,6 @@ impl<'a> TooltipRenderer<'a> {
|
||||
let ribbon_total_width = ribbon_content_width + 4;
|
||||
let total_element_width = text_width + ribbon_total_width + 1;
|
||||
|
||||
// Check if this element would exceed the available columns and render an ellipses
|
||||
// if it does
|
||||
if base_x + x + total_element_width > cols {
|
||||
let remaining_space = cols.saturating_sub(base_x + x);
|
||||
let ellipsis = Text::new("...").opaque();
|
||||
@@ -65,7 +63,7 @@ impl<'a> TooltipRenderer<'a> {
|
||||
},
|
||||
_ => {
|
||||
let (_, tooltip_rows, tooltip_cols) = self.other_mode_tooltip(current_mode);
|
||||
(tooltip_rows + 1, tooltip_cols) // + 1 for the invisible table title
|
||||
(tooltip_rows + 1, tooltip_cols)
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -110,7 +108,7 @@ impl<'a> TooltipRenderer<'a> {
|
||||
let actions_vec: Vec<_> = actions.into_iter().collect();
|
||||
|
||||
let mut table = Table::new().add_row(vec![" ".to_owned(); 2]);
|
||||
let mut row_count = 1; // Start with header row
|
||||
let mut row_count = 1;
|
||||
|
||||
if actions_vec.is_empty() {
|
||||
let tooltip_text = match self.mode_info.mode {
|
||||
@@ -139,7 +137,7 @@ impl<'a> TooltipRenderer<'a> {
|
||||
row_count += 1;
|
||||
}
|
||||
|
||||
let total_width = key_width + action_width + 1; // +1 for separator
|
||||
let total_width = key_width + action_width + 1;
|
||||
(table, row_count, total_width)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
use std::collections::BTreeMap;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
type TableId = usize;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct KeybindStore {
|
||||
tables: BTreeMap<TableId, KeybindsVec>,
|
||||
clients: BTreeMap<ClientId, TableId>,
|
||||
next_table_id: TableId,
|
||||
}
|
||||
|
||||
impl KeybindStore {
|
||||
pub fn set(&mut self, client_id: ClientId, keybinds: KeybindsVec) {
|
||||
let existing = self
|
||||
.tables
|
||||
.iter()
|
||||
.find(|(_, table)| **table == keybinds)
|
||||
.map(|(table_id, _)| *table_id);
|
||||
let table_id = match existing {
|
||||
Some(table_id) => table_id,
|
||||
None => {
|
||||
let table_id = self.next_table_id;
|
||||
self.next_table_id += 1;
|
||||
self.tables.insert(table_id, keybinds);
|
||||
table_id
|
||||
},
|
||||
};
|
||||
if let Some(previous) = self.clients.insert(client_id, table_id) {
|
||||
self.release(previous);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove(&mut self, client_id: ClientId) {
|
||||
if let Some(table_id) = self.clients.remove(&client_id) {
|
||||
self.release(table_id);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn client_ids(&self) -> impl Iterator<Item = ClientId> + '_ {
|
||||
self.clients.keys().copied()
|
||||
}
|
||||
|
||||
pub fn swap(&mut self, client_id: ClientId, keybinds: &mut KeybindsVec) -> bool {
|
||||
let table = self
|
||||
.clients
|
||||
.get(&client_id)
|
||||
.and_then(|table_id| self.tables.get_mut(table_id));
|
||||
match table {
|
||||
Some(table) => {
|
||||
std::mem::swap(table, keybinds);
|
||||
true
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn release(&mut self, table_id: TableId) {
|
||||
if !self.clients.values().any(|id| *id == table_id) {
|
||||
self.tables.remove(&table_id);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn table_count(&self) -> usize {
|
||||
self.tables.len()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn take_keybinds(event: &mut Event) -> Option<KeybindsVec> {
|
||||
match event {
|
||||
Event::InitialKeybinds(keybinds) => Some(std::mem::take(keybinds)),
|
||||
Event::ModeUpdate(mode_info) if !mode_info.keybinds.is_empty() => {
|
||||
Some(std::mem::take(&mut mode_info.keybinds))
|
||||
},
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use zellij_tile::prelude::actions::Action;
|
||||
|
||||
fn table(c: char) -> KeybindsVec {
|
||||
vec![(
|
||||
InputMode::Normal,
|
||||
vec![(KeyWithModifier::new(BareKey::Char(c)), vec![Action::Quit])],
|
||||
)]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clients_with_the_same_keybinds_share_one_table() {
|
||||
let mut store = KeybindStore::default();
|
||||
store.set(1, table('q'));
|
||||
store.set(2, table('q'));
|
||||
assert_eq!(store.table_count(), 1);
|
||||
store.set(2, table('x'));
|
||||
assert_eq!(store.table_count(), 2);
|
||||
store.remove(1);
|
||||
assert_eq!(store.table_count(), 1);
|
||||
store.remove(2);
|
||||
assert_eq!(store.table_count(), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swapping_twice_restores_the_table() {
|
||||
let mut store = KeybindStore::default();
|
||||
store.set(1, table('q'));
|
||||
store.set(2, table('q'));
|
||||
let mut lent = KeybindsVec::new();
|
||||
assert!(store.swap(1, &mut lent));
|
||||
assert_eq!(lent, table('q'));
|
||||
assert!(store.swap(1, &mut lent));
|
||||
assert!(lent.is_empty());
|
||||
let mut other = KeybindsVec::new();
|
||||
assert!(store.swap(2, &mut other));
|
||||
assert_eq!(other, table('q'));
|
||||
assert!(!store.swap(3, &mut other));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn take_keybinds_empties_the_event() {
|
||||
let mut event = Event::InitialKeybinds(table('q'));
|
||||
assert_eq!(take_keybinds(&mut event), Some(table('q')));
|
||||
assert_eq!(event, Event::InitialKeybinds(vec![]));
|
||||
let mut event = Event::ModeUpdate(ModeInfo::default());
|
||||
assert_eq!(take_keybinds(&mut event), None);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
|
||||
use std::path::{Path, PathBuf};
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
@@ -7,61 +7,60 @@ const FILE_PATH_REGEX: &str = r#"(?:^|\s)((?:(?:\./|\.\./|/)[A-Za-z0-9_./\-+@%,#
|
||||
const CWD_CONTEXT_KEY: &str = "cwd";
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
pub struct Link {
|
||||
slots: BTreeSet<SlotId>,
|
||||
initialized: bool,
|
||||
known_terminal_panes: HashSet<PaneId>,
|
||||
/// Tracks the current CWD for each terminal pane.
|
||||
pane_cwds: HashMap<PaneId, PathBuf>,
|
||||
/// Tracks the directory entry names highlighted for each pane,
|
||||
/// so they can be removed when the CWD changes.
|
||||
pane_dir_entries: HashMap<PaneId, Vec<String>>,
|
||||
/// Session environment variables, fetched once on load.
|
||||
/// Used for `~` and `$VAR` expansion in clicked paths.
|
||||
env_vars: BTreeMap<String, String>,
|
||||
}
|
||||
|
||||
register_plugin!(State);
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, _configuration: BTreeMap<String, String>) {
|
||||
subscribe(&[
|
||||
EventType::PaneUpdate,
|
||||
EventType::HighlightClicked,
|
||||
EventType::CwdChanged,
|
||||
]);
|
||||
// Set host folder to "/" so that /host maps to the real filesystem root,
|
||||
// allowing std::fs operations on /host/<absolute_path>.
|
||||
change_host_folder(PathBuf::from("/"));
|
||||
self.env_vars = get_session_environment_variables();
|
||||
impl Link {
|
||||
pub fn has_slots(&self) -> bool {
|
||||
!self.slots.is_empty()
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
pub fn slot_added(&mut self, slot: &Slot) {
|
||||
self.slots.insert(slot.id);
|
||||
if !self.initialized {
|
||||
self.initialized = true;
|
||||
subscribe(&[
|
||||
EventType::PaneUpdate,
|
||||
EventType::HighlightClicked,
|
||||
EventType::CwdChanged,
|
||||
]);
|
||||
change_host_folder(PathBuf::from("/"));
|
||||
self.env_vars = get_session_environment_variables();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
self.slots.remove(&slot_id);
|
||||
}
|
||||
|
||||
pub fn update(&mut self, event: &Event) {
|
||||
match event {
|
||||
Event::PaneUpdate(pane_manifest) => {
|
||||
self.handle_pane_update(pane_manifest);
|
||||
},
|
||||
Event::HighlightClicked {
|
||||
pane_id: _,
|
||||
pattern: _,
|
||||
matched_string,
|
||||
context,
|
||||
..
|
||||
} => {
|
||||
self.handle_highlight_clicked(matched_string, context);
|
||||
self.handle_highlight_clicked(matched_string.clone(), context.clone());
|
||||
},
|
||||
Event::CwdChanged(pane_id, new_cwd, _focused_client_ids) => {
|
||||
self.handle_cwd_changed(pane_id, new_cwd);
|
||||
self.handle_cwd_changed(*pane_id, new_cwd.clone());
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
false // never render — background-only plugin
|
||||
}
|
||||
|
||||
fn render(&mut self, _rows: usize, _cols: usize) {
|
||||
// Background-only plugin. Never rendered. Intentionally empty.
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn handle_pane_update(&mut self, pane_manifest: PaneManifest) {
|
||||
impl Link {
|
||||
fn handle_pane_update(&mut self, pane_manifest: &PaneManifest) {
|
||||
let mut current_panes: HashSet<PaneId> = HashSet::new();
|
||||
|
||||
for (_tab_index, panes) in &pane_manifest.panes {
|
||||
@@ -73,10 +72,8 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
// Set highlights on newly appeared terminal panes
|
||||
for &pane_id in ¤t_panes {
|
||||
if !self.known_terminal_panes.contains(&pane_id) {
|
||||
// Fetch the pane's current CWD and scan its directory
|
||||
if let Ok(cwd) = get_pane_cwd(pane_id) {
|
||||
self.scan_and_store_dir_entries(pane_id, &cwd);
|
||||
self.pane_cwds.insert(pane_id, cwd);
|
||||
@@ -85,7 +82,6 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up tracking state for panes that no longer exist
|
||||
for &pane_id in &self.known_terminal_panes {
|
||||
if !current_panes.contains(&pane_id) {
|
||||
self.pane_cwds.remove(&pane_id);
|
||||
@@ -105,8 +101,6 @@ impl State {
|
||||
self.pane_cwds.insert(pane_id, new_cwd.clone());
|
||||
self.scan_and_store_dir_entries(pane_id, &new_cwd);
|
||||
|
||||
// clear_pane_highlights removes all highlights, then re-set everything
|
||||
// (file-path regex + directory entry patterns)
|
||||
clear_pane_highlights(pane_id);
|
||||
self.set_all_highlights_for_pane(pane_id);
|
||||
}
|
||||
@@ -117,8 +111,6 @@ impl State {
|
||||
let expanded = expand_path(path_str, &self.env_vars);
|
||||
let path_str = expanded.as_str();
|
||||
|
||||
// Resolve to a fully qualified path: if relative, join with the
|
||||
// pane CWD stored in the highlight context.
|
||||
let absolute_path = if path_str.starts_with('/') {
|
||||
PathBuf::from(path_str)
|
||||
} else if let Some(cwd) = context.get(CWD_CONTEXT_KEY) {
|
||||
@@ -127,14 +119,11 @@ impl State {
|
||||
PathBuf::from(path_str)
|
||||
};
|
||||
|
||||
// Validate the path exists via the /host/ filesystem mapping
|
||||
// established at load time. This guards against regex false
|
||||
// positives that match non-path text in terminal output.
|
||||
let host_path =
|
||||
Path::new("/host").join(absolute_path.strip_prefix("/").unwrap_or(&absolute_path));
|
||||
let metadata = match std::fs::metadata(&host_path) {
|
||||
Ok(m) => m,
|
||||
Err(_) => return, // path does not exist — silently ignore
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
if metadata.is_dir() {
|
||||
@@ -163,47 +152,19 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// (Re-)set all regex highlights for a pane: the general file-path regex
|
||||
/// plus any directory-entry patterns derived from the pane's CWD.
|
||||
fn set_all_highlights_for_pane(&self, pane_id: PaneId) {
|
||||
let mut highlights = Vec::new();
|
||||
|
||||
// Build context map containing the pane CWD (echoed back on click)
|
||||
let context = self.cwd_context_for_pane(pane_id);
|
||||
|
||||
// General file-path regex (always present)
|
||||
highlights.push(RegexHighlight {
|
||||
pattern: FILE_PATH_REGEX.to_owned(),
|
||||
style: HighlightStyle::None,
|
||||
layer: HighlightLayer::Hint,
|
||||
context: context.clone(),
|
||||
on_hover: true,
|
||||
bold: false,
|
||||
italic: true,
|
||||
underline: true,
|
||||
tooltip_text: Some("Open".to_string()),
|
||||
});
|
||||
highlights.push(link_highlight(FILE_PATH_REGEX.to_owned(), context.clone()));
|
||||
|
||||
// Directory-entry patterns for the pane's current CWD
|
||||
if let Some(entries) = self.pane_dir_entries.get(&pane_id) {
|
||||
for entry_name in entries {
|
||||
let path_chars = r#"[A-Za-z0-9_./\-+@%,#=~!\$\{\}\[\]]"#;
|
||||
let pattern = format!(
|
||||
"(?:^|\\s)({}(?:/{path_chars}+)?(?::\\d+(?::\\d+)?)?)(?::|\\s|$)",
|
||||
regex_escape(entry_name),
|
||||
);
|
||||
highlights.push(RegexHighlight {
|
||||
pattern,
|
||||
style: HighlightStyle::None,
|
||||
layer: HighlightLayer::Hint,
|
||||
context: context.clone(),
|
||||
on_hover: true,
|
||||
bold: false,
|
||||
italic: true,
|
||||
underline: true,
|
||||
tooltip_text: Some("Open".to_string()),
|
||||
});
|
||||
}
|
||||
if let Some(pattern) = self
|
||||
.pane_dir_entries
|
||||
.get(&pane_id)
|
||||
.and_then(|entries| dir_entries_pattern(entries))
|
||||
{
|
||||
highlights.push(link_highlight(pattern, context));
|
||||
}
|
||||
|
||||
set_pane_regex_highlights(pane_id, highlights);
|
||||
@@ -224,12 +185,41 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// Scan a directory for first-level file and folder names.
|
||||
/// Returns an empty vec on any error.
|
||||
/// Maximum number of directory entries to scan. Directories with more entries
|
||||
/// than this are skipped entirely to avoid excessive regex pattern count.
|
||||
const MAX_DIR_ENTRIES: usize = 500;
|
||||
|
||||
const PATH_CHARS: &str = r#"[A-Za-z0-9_./\-+@%,#=~!\$\{\}\[\]]"#;
|
||||
|
||||
fn link_highlight(pattern: String, context: BTreeMap<String, String>) -> RegexHighlight {
|
||||
RegexHighlight {
|
||||
pattern,
|
||||
style: HighlightStyle::None,
|
||||
layer: HighlightLayer::Hint,
|
||||
context,
|
||||
on_hover: true,
|
||||
bold: false,
|
||||
italic: true,
|
||||
underline: true,
|
||||
tooltip_text: Some("Open".to_string()),
|
||||
}
|
||||
}
|
||||
|
||||
fn dir_entries_pattern(entries: &[String]) -> Option<String> {
|
||||
if entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let mut names: Vec<&str> = entries.iter().map(|e| e.as_str()).collect();
|
||||
names.sort_by(|a, b| b.len().cmp(&a.len()).then_with(|| a.cmp(b)));
|
||||
names.dedup();
|
||||
let alternatives = names
|
||||
.iter()
|
||||
.map(|name| regex_escape(name))
|
||||
.collect::<Vec<_>>()
|
||||
.join("|");
|
||||
Some(format!(
|
||||
"(?:^|\\s)((?:{alternatives})(?:/{PATH_CHARS}+)?(?::\\d+(?::\\d+)?)?)(?::|\\s|$)"
|
||||
))
|
||||
}
|
||||
|
||||
fn scan_directory(path: &Path) -> Vec<String> {
|
||||
let mut entries = Vec::new();
|
||||
let read_dir = match std::fs::read_dir(path) {
|
||||
@@ -249,7 +239,6 @@ fn scan_directory(path: &Path) -> Vec<String> {
|
||||
entries
|
||||
}
|
||||
|
||||
/// Escape a string so it is treated as a literal in a regex pattern.
|
||||
fn regex_escape(s: &str) -> String {
|
||||
let mut escaped = String::with_capacity(s.len() + 8);
|
||||
for c in s.chars() {
|
||||
@@ -264,15 +253,7 @@ fn regex_escape(s: &str) -> String {
|
||||
escaped
|
||||
}
|
||||
|
||||
/// Expand `~` and `$VAR` / `${VAR}` references in a path string.
|
||||
///
|
||||
/// - `~` at the start (followed by `/` or at end-of-string) is replaced with
|
||||
/// the value of `HOME` from `env_vars`.
|
||||
/// - `$VARNAME` and `${VARNAME}` anywhere in the string are replaced with the
|
||||
/// corresponding value from `env_vars`.
|
||||
/// - Unrecognized variables and a missing `HOME` are left as-is.
|
||||
fn expand_path(path: &str, env_vars: &BTreeMap<String, String>) -> String {
|
||||
// Step 1: tilde expansion (only leading ~)
|
||||
let after_tilde = if path == "~" {
|
||||
match env_vars.get("HOME") {
|
||||
Some(home) => home.clone(),
|
||||
@@ -287,7 +268,6 @@ fn expand_path(path: &str, env_vars: &BTreeMap<String, String>) -> String {
|
||||
path.to_owned()
|
||||
};
|
||||
|
||||
// Step 2: environment variable expansion ($VAR and ${VAR})
|
||||
let bytes = after_tilde.as_bytes();
|
||||
let len = bytes.len();
|
||||
let mut result = String::with_capacity(len);
|
||||
@@ -296,18 +276,15 @@ fn expand_path(path: &str, env_vars: &BTreeMap<String, String>) -> String {
|
||||
while i < len {
|
||||
if bytes[i] == b'$' && i + 1 < len {
|
||||
let (var_name, end_idx) = if bytes[i + 1] == b'{' {
|
||||
// ${VAR} form
|
||||
if let Some(close) = after_tilde[i + 2..].find('}') {
|
||||
let name = &after_tilde[i + 2..i + 2 + close];
|
||||
(name, i + 2 + close + 1)
|
||||
} else {
|
||||
// No closing brace — not a valid variable reference
|
||||
result.push('$');
|
||||
i += 1;
|
||||
continue;
|
||||
}
|
||||
} else {
|
||||
// $VAR form — variable name is [A-Za-z_][A-Za-z0-9_]*
|
||||
let start = i + 1;
|
||||
if start < len && (bytes[start].is_ascii_alphabetic() || bytes[start] == b'_') {
|
||||
let mut end = start + 1;
|
||||
@@ -325,7 +302,6 @@ fn expand_path(path: &str, env_vars: &BTreeMap<String, String>) -> String {
|
||||
if let Some(value) = env_vars.get(var_name) {
|
||||
result.push_str(value);
|
||||
} else {
|
||||
// Unknown variable — preserve the original text
|
||||
result.push_str(&after_tilde[i..end_idx]);
|
||||
}
|
||||
i = end_idx;
|
||||
@@ -375,8 +351,6 @@ fn parse_path_and_line(matched_string: &str) -> (&str, Option<usize>) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// --- parse_path_and_line tests ---
|
||||
|
||||
#[test]
|
||||
fn parse_path_and_line_simple_path() {
|
||||
let (path, line) = parse_path_and_line("src/main.rs");
|
||||
@@ -405,8 +379,6 @@ mod tests {
|
||||
assert_eq!(line, None);
|
||||
}
|
||||
|
||||
// --- expand_path tests ---
|
||||
|
||||
#[test]
|
||||
fn expand_path_tilde() {
|
||||
let mut env = BTreeMap::new();
|
||||
@@ -440,8 +412,6 @@ mod tests {
|
||||
assert_eq!(expand_path("/absolute/path", &env), "/absolute/path");
|
||||
}
|
||||
|
||||
// --- regex_escape tests ---
|
||||
|
||||
#[test]
|
||||
fn regex_escape_special_chars() {
|
||||
assert_eq!(regex_escape("file.txt"), r"file\.txt");
|
||||
@@ -455,7 +425,40 @@ mod tests {
|
||||
assert_eq!(regex_escape("hello_world"), "hello_world");
|
||||
}
|
||||
|
||||
// --- scan_directory tests ---
|
||||
#[test]
|
||||
fn dir_entries_pattern_empty_is_none() {
|
||||
assert_eq!(dir_entries_pattern(&[]), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_entries_pattern_combines_and_escapes() {
|
||||
let entries = vec!["a.rs".to_owned(), "Cargo.toml".to_owned(), "(x)".to_owned()];
|
||||
let pattern = dir_entries_pattern(&entries).unwrap();
|
||||
assert_eq!(
|
||||
pattern,
|
||||
format!(
|
||||
"(?:^|\\s)((?:Cargo\\.toml|a\\.rs|\\(x\\))(?:/{PATH_CHARS}+)?(?::\\d+(?::\\d+)?)?)(?::|\\s|$)"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dir_entries_pattern_matches_space_separated_names() {
|
||||
let entries = vec!["src".to_owned(), "Cargo.toml".to_owned()];
|
||||
let regex = regex::Regex::new(&dir_entries_pattern(&entries).unwrap()).unwrap();
|
||||
let text = "Cargo.toml src src/main.rs:12 other";
|
||||
let mut found = vec![];
|
||||
let mut pos = 0;
|
||||
while pos <= text.len() {
|
||||
let Some(captures) = regex.captures_at(text, pos) else {
|
||||
break;
|
||||
};
|
||||
let m = captures.get(1).unwrap();
|
||||
found.push(m.as_str().to_owned());
|
||||
pos = m.end().max(pos + 1);
|
||||
}
|
||||
assert_eq!(found, vec!["Cargo.toml", "src", "src/main.rs:12"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scan_directory_returns_entries() {
|
||||
@@ -0,0 +1,274 @@
|
||||
mod compact_bar;
|
||||
mod keybinds;
|
||||
mod link;
|
||||
mod status_bar;
|
||||
mod tab_bar;
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use compact_bar::CompactBar;
|
||||
use keybinds::{take_keybinds, KeybindStore};
|
||||
use link::Link;
|
||||
use status_bar::StatusBar;
|
||||
use tab_bar::TabBar;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Role {
|
||||
TabBar,
|
||||
StatusBar,
|
||||
CompactBar,
|
||||
Link,
|
||||
}
|
||||
|
||||
impl Role {
|
||||
fn from_configuration(configuration: &BTreeMap<String, String>) -> Role {
|
||||
match configuration.get("role").map(|r| r.as_str()) {
|
||||
Some("status-bar") => Role::StatusBar,
|
||||
Some("compact-bar") => Role::CompactBar,
|
||||
Some("link") => Role::Link,
|
||||
_ => Role::TabBar,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ClientSeed {
|
||||
pub mode_info: ModeInfo,
|
||||
pub tabs: Vec<TabInfo>,
|
||||
}
|
||||
|
||||
impl ClientSeed {
|
||||
fn apply(&mut self, event: &Event) {
|
||||
match event {
|
||||
Event::ModeUpdate(mode_info) => {
|
||||
self.mode_info = mode_info.clone();
|
||||
},
|
||||
Event::TabUpdate(tabs) => {
|
||||
if tabs.iter().any(|t| t.active) {
|
||||
self.tabs = tabs.clone();
|
||||
}
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
slots: BTreeMap<SlotId, Role>,
|
||||
pending: BTreeMap<ClientId, ClientSeed>,
|
||||
tab_bar: TabBar,
|
||||
status_bar: StatusBar,
|
||||
compact_bar: CompactBar,
|
||||
link: Link,
|
||||
keybinds: KeybindStore,
|
||||
}
|
||||
|
||||
register_shared_plugin!(State);
|
||||
|
||||
impl State {
|
||||
fn role_has_slots(&self, role: Role) -> bool {
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.has_slots(),
|
||||
Role::StatusBar => self.status_bar.has_slots(),
|
||||
Role::CompactBar => self.compact_bar.has_slots(),
|
||||
Role::Link => self.link.has_slots(),
|
||||
}
|
||||
}
|
||||
|
||||
fn any_bar_has_slots(&self) -> bool {
|
||||
self.tab_bar.has_slots() || self.status_bar.has_slots() || self.compact_bar.has_slots()
|
||||
}
|
||||
|
||||
fn snapshot_for(&self, role: Role) -> BTreeMap<ClientId, ClientSeed> {
|
||||
if role != Role::TabBar && self.tab_bar.has_slots() {
|
||||
self.tab_bar.snapshot()
|
||||
} else if role != Role::StatusBar && self.status_bar.has_slots() {
|
||||
self.status_bar.snapshot()
|
||||
} else if role != Role::CompactBar && self.compact_bar.has_slots() {
|
||||
self.compact_bar.snapshot()
|
||||
} else {
|
||||
self.pending.clone()
|
||||
}
|
||||
}
|
||||
|
||||
fn store_keybinds(&mut self, event: &mut Event, client_id: Option<ClientId>) {
|
||||
let Some(keybinds) = take_keybinds(event) else {
|
||||
return;
|
||||
};
|
||||
match client_id {
|
||||
Some(client_id) => self.keybinds.set(client_id, keybinds),
|
||||
None => {
|
||||
let mut client_ids: Vec<ClientId> = self.keybinds.client_ids().collect();
|
||||
client_ids.extend(self.pending.keys().copied());
|
||||
client_ids.extend(self.tab_bar.client_ids());
|
||||
client_ids.extend(self.status_bar.client_ids());
|
||||
client_ids.extend(self.compact_bar.client_ids());
|
||||
client_ids.sort_unstable();
|
||||
client_ids.dedup();
|
||||
for client_id in client_ids {
|
||||
self.keybinds.set(client_id, keybinds.clone());
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn snapshot_of(&self, role: Role) -> BTreeMap<ClientId, ClientSeed> {
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.snapshot(),
|
||||
Role::StatusBar => self.status_bar.snapshot(),
|
||||
Role::CompactBar => self.compact_bar.snapshot(),
|
||||
Role::Link => BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ZellijSharedPlugin for State {
|
||||
fn slot_added(&mut self, slot: Slot) {
|
||||
let role = Role::from_configuration(&slot.configuration);
|
||||
if slot.kind == SlotKind::Pane {
|
||||
set_selectable(false);
|
||||
}
|
||||
if role != Role::Link && !self.role_has_slots(role) {
|
||||
let seeds = self.snapshot_for(role);
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.seed(&seeds),
|
||||
Role::StatusBar => self.status_bar.seed(&seeds),
|
||||
Role::CompactBar => self.compact_bar.seed(&seeds),
|
||||
Role::Link => {},
|
||||
}
|
||||
}
|
||||
self.slots.insert(slot.id, role);
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.slot_added(&slot),
|
||||
Role::StatusBar => self.status_bar.slot_added(&slot),
|
||||
Role::CompactBar => self.compact_bar.slot_added(&slot),
|
||||
Role::Link => self.link.slot_added(&slot),
|
||||
}
|
||||
if self.any_bar_has_slots() {
|
||||
self.pending.clear();
|
||||
}
|
||||
}
|
||||
|
||||
fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
let Some(role) = self.slots.remove(&slot_id) else {
|
||||
return;
|
||||
};
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.slot_removed(slot_id),
|
||||
Role::StatusBar => self.status_bar.slot_removed(slot_id),
|
||||
Role::CompactBar => self.compact_bar.slot_removed(slot_id),
|
||||
Role::Link => self.link.slot_removed(slot_id),
|
||||
}
|
||||
if role != Role::Link && !self.role_has_slots(role) {
|
||||
if !self.any_bar_has_slots() {
|
||||
self.pending = self.snapshot_of(role);
|
||||
}
|
||||
match role {
|
||||
Role::TabBar => self.tab_bar.reset(),
|
||||
Role::StatusBar => self.status_bar.reset(),
|
||||
Role::CompactBar => self.compact_bar.reset(),
|
||||
Role::Link => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn client_connected(&mut self, client_id: ClientId) {
|
||||
if self.tab_bar.has_slots() {
|
||||
self.tab_bar.client_connected(client_id);
|
||||
}
|
||||
if self.status_bar.has_slots() {
|
||||
self.status_bar.client_connected(client_id);
|
||||
}
|
||||
if self.compact_bar.has_slots() {
|
||||
self.compact_bar.client_connected(client_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn client_disconnected(&mut self, client_id: ClientId) {
|
||||
self.pending.remove(&client_id);
|
||||
self.keybinds.remove(client_id);
|
||||
self.tab_bar.client_disconnected(client_id);
|
||||
self.status_bar.client_disconnected(client_id);
|
||||
self.compact_bar.client_disconnected(client_id);
|
||||
}
|
||||
|
||||
fn update(&mut self, mut event: Event, context: EventContext) -> RenderResponse {
|
||||
self.compact_bar.ensure_toggle_keybinds(context.client_id);
|
||||
self.store_keybinds(&mut event, context.client_id);
|
||||
match &event {
|
||||
Event::Mouse(_) | Event::Key(_) => {
|
||||
let role = context
|
||||
.slot_id
|
||||
.and_then(|slot_id| self.slots.get(&slot_id).copied());
|
||||
match role {
|
||||
Some(Role::TabBar) => self.tab_bar.update(&event, context),
|
||||
Some(Role::StatusBar) => self.status_bar.update(&event, context),
|
||||
Some(Role::CompactBar) => {
|
||||
self.compact_bar.update(&event, context, &mut self.keybinds)
|
||||
},
|
||||
_ => RenderResponse::Nothing,
|
||||
}
|
||||
},
|
||||
_ => {
|
||||
let mut render = RenderResponse::Nothing;
|
||||
if !self.any_bar_has_slots() {
|
||||
if let Some(client_id) = context.client_id {
|
||||
if matches!(
|
||||
event,
|
||||
Event::ModeUpdate(_) | Event::TabUpdate(_) | Event::InitialKeybinds(_)
|
||||
) {
|
||||
self.pending.entry(client_id).or_default().apply(&event);
|
||||
}
|
||||
}
|
||||
}
|
||||
if self.tab_bar.has_slots() || matches!(event, Event::Timer(_)) {
|
||||
let tab_bar_render = self.tab_bar.update(&event, context);
|
||||
if self.tab_bar.has_slots() {
|
||||
render = render.merge(tab_bar_render);
|
||||
}
|
||||
}
|
||||
if self.status_bar.has_slots() {
|
||||
render = render.merge(self.status_bar.update(&event, context));
|
||||
}
|
||||
if self.compact_bar.has_slots() {
|
||||
render =
|
||||
render.merge(self.compact_bar.update(&event, context, &mut self.keybinds));
|
||||
}
|
||||
if self.link.has_slots() {
|
||||
self.link.update(&event);
|
||||
}
|
||||
render
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe(&mut self, pipe_message: PipeMessage, context: EventContext) -> RenderResponse {
|
||||
self.compact_bar.ensure_toggle_keybinds(context.client_id);
|
||||
if self.compact_bar.has_slots() {
|
||||
self.compact_bar
|
||||
.pipe(pipe_message, context, &mut self.keybinds)
|
||||
} else {
|
||||
RenderResponse::Nothing
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
|
||||
match self.slots.get(&slot_id) {
|
||||
Some(Role::TabBar) => {
|
||||
self.tab_bar
|
||||
.render(rows, cols, slot_id, client_id, &mut self.keybinds)
|
||||
},
|
||||
Some(Role::StatusBar) => {
|
||||
self.status_bar
|
||||
.render(rows, cols, slot_id, client_id, &mut self.keybinds)
|
||||
},
|
||||
Some(Role::CompactBar) => {
|
||||
self.compact_bar
|
||||
.render(rows, cols, slot_id, client_id, &mut self.keybinds)
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
+26
-124
@@ -2,11 +2,11 @@ use ansi_term::{unstyled_len, ANSIStrings};
|
||||
use zellij_tile::prelude::actions::Action;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::color_elements;
|
||||
use crate::{
|
||||
use crate::status_bar::color_elements;
|
||||
use crate::status_bar::{
|
||||
action_key, action_key_group, get_common_modifiers, style_key_with_modifier, TO_NORMAL,
|
||||
};
|
||||
use crate::{ColoredElements, LinePart};
|
||||
use crate::status_bar::{ColoredElements, LinePart};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct KeyShortcut {
|
||||
@@ -116,25 +116,6 @@ impl KeyShortcut {
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate long mode shortcut tile.
|
||||
///
|
||||
/// A long mode shortcut tile consists of a leading and trailing `separator`, a keybinding enclosed
|
||||
/// in `<>` brackets and the name of the mode displayed in capitalized letters next to it. For
|
||||
/// example, the default long mode shortcut tile for "Locked" mode is: ` <g> LOCK `.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `key`: A [`KeyShortcut`] that defines how the tile is displayed (active/disabled/...), what
|
||||
/// action it belongs to (roughly equivalent to [`InputMode`]s) and the keybinding to trigger
|
||||
/// this action.
|
||||
/// - `palette`: A structure holding styling information.
|
||||
/// - `separator`: The separator printed before and after the mode shortcut tile. The default is an
|
||||
/// arrow head-like separator.
|
||||
/// - `shared_super`: If set to true, all mode shortcut keybindings share a common modifier (see
|
||||
/// [`get_common_modifier`]) and the modifier belonging to the keybinding is **not** printed in
|
||||
/// the shortcut tile.
|
||||
/// - `first_tile`: If set to true, the leading separator for this tile will be omitted so no gap
|
||||
/// appears on the screen.
|
||||
fn long_mode_shortcut(
|
||||
key: &KeyShortcut,
|
||||
palette: ColoredElements,
|
||||
@@ -177,13 +158,13 @@ fn long_mode_shortcut(
|
||||
suffix_separator,
|
||||
])
|
||||
.to_string(),
|
||||
len: start_separator.chars().count() // Separator
|
||||
+ 2 // " <"
|
||||
+ key_binding.chars().count() // Key binding
|
||||
+ 2 // "> "
|
||||
+ key_hint.chars().count() // Key hint (mode)
|
||||
+ 1 // " "
|
||||
+ separator.chars().count(), // Separator
|
||||
len: start_separator.chars().count()
|
||||
+ 2
|
||||
+ key_binding.chars().count()
|
||||
+ 2
|
||||
+ key_hint.chars().count()
|
||||
+ 1
|
||||
+ separator.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,34 +210,16 @@ fn shortened_modifier_shortcut(
|
||||
suffix_separator,
|
||||
])
|
||||
.to_string(),
|
||||
len: start_separator.chars().count() // Separator
|
||||
+ 2 // " <"
|
||||
+ key_binding.chars().count() // Key binding
|
||||
+ 2 // "> "
|
||||
+ key_hint.chars().count() // Key hint (mode)
|
||||
+ 1 // " "
|
||||
+ separator.chars().count(), // Separator
|
||||
len: start_separator.chars().count()
|
||||
+ 2
|
||||
+ key_binding.chars().count()
|
||||
+ 2
|
||||
+ key_hint.chars().count()
|
||||
+ 1
|
||||
+ separator.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate short mode shortcut tile.
|
||||
///
|
||||
/// A short mode shortcut tile consists of a leading and trailing `separator` and a keybinding. For
|
||||
/// example, the default short mode shortcut tile for "Locked" mode is: ` g `.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// - `key`: A [`KeyShortcut`] that defines how the tile is displayed (active/disabled/...), what
|
||||
/// action it belongs to (roughly equivalent to [`InputMode`]s) and the keybinding to trigger
|
||||
/// this action.
|
||||
/// - `palette`: A structure holding styling information.
|
||||
/// - `separator`: The separator printed before and after the mode shortcut tile. The default is an
|
||||
/// arrow head-like separator.
|
||||
/// - `shared_super`: If set to true, all mode shortcut keybindings share a common modifier (see
|
||||
/// [`get_common_modifier`]) and the modifier belonging to the keybinding is **not** printed in
|
||||
/// the shortcut tile.
|
||||
/// - `first_tile`: If set to true, the leading separator for this tile will be omitted so no gap
|
||||
/// appears on the screen.
|
||||
fn short_mode_shortcut(
|
||||
key: &KeyShortcut,
|
||||
palette: ColoredElements,
|
||||
@@ -287,11 +250,11 @@ fn short_mode_shortcut(
|
||||
let suffix_separator = colors.suffix_separator.paint(separator);
|
||||
LinePart {
|
||||
part: ANSIStrings(&[prefix_separator, char_shortcut, suffix_separator]).to_string(),
|
||||
len: separator.chars().count() // Separator
|
||||
+ 1 // " "
|
||||
+ key_binding.chars().count() // Key binding
|
||||
+ 1 // " "
|
||||
+ separator.chars().count(), // Separator
|
||||
len: separator.chars().count()
|
||||
+ 1
|
||||
+ key_binding.chars().count()
|
||||
+ 1
|
||||
+ separator.chars().count(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -302,7 +265,6 @@ fn key_indicators(
|
||||
separator: &str,
|
||||
mode_info: &ModeInfo,
|
||||
) -> LinePart {
|
||||
// Print full-width hints
|
||||
let (shared_modifiers, mut line_part) = superkey(palette, separator, mode_info);
|
||||
for key in keys {
|
||||
let line_empty = line_part.len == 0;
|
||||
@@ -314,7 +276,6 @@ fn key_indicators(
|
||||
return line_part;
|
||||
}
|
||||
|
||||
// Full-width doesn't fit, try shortened modifiers (eg. "^C" instead of "Ctrl")
|
||||
line_part = superkey(palette, separator, mode_info).1;
|
||||
for key in keys {
|
||||
let line_empty = line_part.len == 0;
|
||||
@@ -327,7 +288,6 @@ fn key_indicators(
|
||||
return line_part;
|
||||
}
|
||||
|
||||
// Full-width doesn't fit, try shortened hints (just keybindings, no meanings/actions)
|
||||
line_part = superkey(palette, separator, mode_info).1;
|
||||
for key in keys {
|
||||
let line_empty = line_part.len == 0;
|
||||
@@ -339,7 +299,6 @@ fn key_indicators(
|
||||
return line_part;
|
||||
}
|
||||
|
||||
// Shortened doesn't fit, print nothing
|
||||
line_part = LinePart::default();
|
||||
line_part
|
||||
}
|
||||
@@ -374,8 +333,7 @@ fn swap_layout_status(
|
||||
let mut swap_layout_name = format!(" {} ", swap_layout_name);
|
||||
swap_layout_name.make_ascii_uppercase();
|
||||
let keycode = swap_layout_keycode(mode_info);
|
||||
let swap_layout_name_len = swap_layout_name.len() + 3; // 2 for the arrow separators, one for the screen end buffer
|
||||
//
|
||||
let swap_layout_name_len = swap_layout_name.len() + 3;
|
||||
macro_rules! style_swap_layout_indicator {
|
||||
($style_name:ident) => {{
|
||||
(
|
||||
@@ -407,10 +365,7 @@ fn swap_layout_status(
|
||||
prefix_separator, swap_layout_name, suffix_separator
|
||||
);
|
||||
let (part, full_len) = if mode_info.mode == InputMode::Locked {
|
||||
(
|
||||
format!("{}", swap_layout_indicator),
|
||||
swap_layout_name_len, // 1 is the space between
|
||||
)
|
||||
(format!("{}", swap_layout_indicator), swap_layout_name_len)
|
||||
} else {
|
||||
(
|
||||
format!(
|
||||
@@ -420,10 +375,10 @@ fn swap_layout_status(
|
||||
swap_layout_indicator,
|
||||
colored_elements.superkey_prefix.paint(" ")
|
||||
),
|
||||
keycode.len + swap_layout_name_len + 1, // 1 is the space between
|
||||
keycode.len + swap_layout_name_len + 1,
|
||||
)
|
||||
};
|
||||
let short_len = swap_layout_name_len + 1; // 1 is the space between
|
||||
let short_len = swap_layout_name_len + 1;
|
||||
if full_len <= max_len {
|
||||
Some(LinePart {
|
||||
part,
|
||||
@@ -442,29 +397,13 @@ fn swap_layout_status(
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the keybindings for switching `InputMode`s and `Quit` visible in status bar.
|
||||
///
|
||||
/// Return a Vector of `Key`s where each `Key` is a shortcut to switch to some `InputMode` or Quit
|
||||
/// zellij. Given the vast amount of things a user can configure in their zellij config, this
|
||||
/// function has some limitations to keep in mind:
|
||||
///
|
||||
/// - The vector is not deduplicated: If switching to a certain `InputMode` is bound to multiple
|
||||
/// `Key`s, all of these bindings will be part of the returned vector. There is also no
|
||||
/// guaranteed sort order. Which key ends up in the status bar in such a situation isn't defined.
|
||||
/// - The vector will **not** contain the ' ', '\n' and 'Esc' keys: These are the default bindings
|
||||
/// to get back to normal mode from any input mode, but they aren't of interest when searching
|
||||
/// for the super key. If for any input mode the user has bound only these keys to switching back
|
||||
/// to `InputMode::Normal`, a '?' will be displayed as keybinding instead.
|
||||
pub fn mode_switch_keys(mode_info: &ModeInfo) -> Vec<KeyWithModifier> {
|
||||
mode_info
|
||||
.get_mode_keybinds()
|
||||
.iter()
|
||||
.filter_map(|(key, vac)| match vac.first() {
|
||||
// No actions defined, ignore
|
||||
None => None,
|
||||
Some(vac) => {
|
||||
// We ignore certain "default" keybindings that switch back to normal InputMode.
|
||||
// These include: ' ', '\n', 'Esc'
|
||||
if matches!(
|
||||
key,
|
||||
KeyWithModifier {
|
||||
@@ -482,7 +421,6 @@ pub fn mode_switch_keys(mode_info: &ModeInfo) -> Vec<KeyWithModifier> {
|
||||
}
|
||||
if let actions::Action::SwitchToMode { input_mode: mode } = vac {
|
||||
return match mode {
|
||||
// Store the keys that switch to displayed modes
|
||||
InputMode::Normal
|
||||
| InputMode::Locked
|
||||
| InputMode::Pane
|
||||
@@ -497,7 +435,6 @@ pub fn mode_switch_keys(mode_info: &ModeInfo) -> Vec<KeyWithModifier> {
|
||||
if let actions::Action::Quit = vac {
|
||||
return Some(key.clone());
|
||||
}
|
||||
// Not a `SwitchToMode` or `Quit` action, ignore
|
||||
None
|
||||
},
|
||||
})
|
||||
@@ -509,14 +446,12 @@ pub fn superkey(
|
||||
separator: &str,
|
||||
mode_info: &ModeInfo,
|
||||
) -> (Vec<KeyModifier>, LinePart) {
|
||||
// Find a common modifier if any
|
||||
let common_modifiers = get_common_modifiers(mode_switch_keys(mode_info).iter().collect());
|
||||
if common_modifiers.is_empty() {
|
||||
return (common_modifiers, LinePart::default());
|
||||
}
|
||||
|
||||
let prefix_text = if mode_info.capabilities.arrow_fonts {
|
||||
// Add extra space in simplified ui
|
||||
format!(
|
||||
" {} + ",
|
||||
common_modifiers
|
||||
@@ -551,7 +486,6 @@ pub fn to_char(kv: Vec<KeyWithModifier>) -> Option<KeyWithModifier> {
|
||||
let key = kv
|
||||
.iter()
|
||||
.filter(|key| {
|
||||
// These are general "keybindings" to get back to normal, they aren't interesting here.
|
||||
!matches!(
|
||||
key,
|
||||
KeyWithModifier {
|
||||
@@ -569,21 +503,12 @@ pub fn to_char(kv: Vec<KeyWithModifier>) -> Option<KeyWithModifier> {
|
||||
.collect::<Vec<&KeyWithModifier>>()
|
||||
.into_iter()
|
||||
.next();
|
||||
// Maybe the user bound one of the ignored keys?
|
||||
if key.is_none() {
|
||||
return kv.first().cloned();
|
||||
}
|
||||
key.cloned()
|
||||
}
|
||||
|
||||
/// Get the [`KeyShortcut`] for a specific [`InputMode`].
|
||||
///
|
||||
/// Iterates over the contents of `shortcuts` to find the [`KeyShortcut`] with the [`KeyAction`]
|
||||
/// matching the [`InputMode`]. Returns a mutable reference to the entry in `shortcuts` if a match
|
||||
/// is found or `None` otherwise.
|
||||
///
|
||||
/// In case multiple entries in `shortcuts` match `mode` (which shouldn't happen), the first match
|
||||
/// is returned.
|
||||
fn get_key_shortcut_for_mode<'a>(
|
||||
shortcuts: &'a mut [KeyShortcut],
|
||||
mode: &InputMode,
|
||||
@@ -615,7 +540,6 @@ pub fn first_line(
|
||||
let supports_arrow_fonts = !help.capabilities.arrow_fonts;
|
||||
let colored_elements = color_elements(help.style.colors, !supports_arrow_fonts, false);
|
||||
let binds = &help.get_mode_keybinds();
|
||||
// Unselect all by default
|
||||
let mut default_keys = vec![
|
||||
KeyShortcut::new(
|
||||
KeyMode::Unselected,
|
||||
@@ -699,7 +623,6 @@ pub fn first_line(
|
||||
key_shortcut.key = to_char(action_key(binds, &[TO_NORMAL]));
|
||||
}
|
||||
|
||||
// In locked mode we must disable all other mode keybindings
|
||||
if help.mode == InputMode::Locked {
|
||||
for key in default_keys.iter_mut().skip(1) {
|
||||
key.mode = KeyMode::Disabled;
|
||||
@@ -707,7 +630,6 @@ pub fn first_line(
|
||||
}
|
||||
|
||||
if help.mode == InputMode::Tmux {
|
||||
// Tmux tile is hidden by default
|
||||
default_keys.push(KeyShortcut::new(
|
||||
KeyMode::Selected,
|
||||
KeyAction::Tmux,
|
||||
@@ -743,13 +665,6 @@ pub fn first_line(
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Unit tests.
|
||||
///
|
||||
/// Note that we cheat a little here, because the number of things one may want to test is endless,
|
||||
/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than
|
||||
/// torture. Hence, we test the most atomic units thoroughly ([`long_mode_shortcut`] and
|
||||
/// [`short_mode_shortcut`]) and then test the public API ([`first_line`]) to ensure correct
|
||||
/// operation.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -758,7 +673,6 @@ mod tests {
|
||||
color_elements(palette, false, false)
|
||||
}
|
||||
|
||||
// Strip style information from `LinePart` and return a raw String instead
|
||||
fn unstyle(line_part: LinePart) -> String {
|
||||
let string = line_part.to_string();
|
||||
|
||||
@@ -784,7 +698,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Displayed like selected(alternate), but different styling
|
||||
fn long_mode_shortcut_unselected_with_binding() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Unselected,
|
||||
@@ -800,7 +713,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Treat exactly like "unselected" variant
|
||||
fn long_mode_shortcut_unselected_alternate_with_binding() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::UnselectedAlternate,
|
||||
@@ -816,7 +728,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// KeyShortcuts without binding are only displayed when "disabled" (for locked mode indications)
|
||||
fn long_mode_shortcut_selected_without_binding() {
|
||||
let key = KeyShortcut::new(KeyMode::Selected, KeyAction::Session, None);
|
||||
let color = colored_elements();
|
||||
@@ -828,7 +739,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// First tile doesn't print a starting separator
|
||||
fn long_mode_shortcut_selected_with_binding_first_tile() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Selected,
|
||||
@@ -844,7 +754,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Modifier is the superkey, mustn't appear in angled brackets
|
||||
fn long_mode_shortcut_selected_with_ctrl_binding_shared_superkey() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Selected,
|
||||
@@ -860,7 +769,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Modifier must be in the angled brackets
|
||||
fn long_mode_shortcut_selected_with_ctrl_binding_no_shared_superkey() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Selected,
|
||||
@@ -876,7 +784,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Must be displayed as usual, but it is styled to be greyed out which we don't test here
|
||||
fn long_mode_shortcut_disabled_with_binding() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Disabled,
|
||||
@@ -892,7 +799,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Must be displayed but without keybinding
|
||||
fn long_mode_shortcut_disabled_without_binding() {
|
||||
let key = KeyShortcut::new(KeyMode::Disabled, KeyAction::Session, None);
|
||||
let color = colored_elements();
|
||||
@@ -904,9 +810,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Test all at once
|
||||
// Note that when "shared_super" is true, the tile **cannot** be the first on the line, so we
|
||||
// ignore **first** here.
|
||||
fn long_mode_shortcut_selected_with_ctrl_binding_and_shared_super_and_first_tile() {
|
||||
let key = KeyShortcut::new(
|
||||
KeyMode::Selected,
|
||||
@@ -1071,7 +974,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Observe: Modes missing in between aren't displayed!
|
||||
fn first_line_default_layout_shared_super() {
|
||||
#[rustfmt::skip]
|
||||
let mode_info = ModeInfo{
|
||||
+230
-148
@@ -24,31 +24,44 @@ use second_line::{
|
||||
};
|
||||
use tip::utils::get_cached_tip_name;
|
||||
|
||||
// for more of these, copy paste from: https://en.wikipedia.org/wiki/Box-drawing_character
|
||||
use crate::keybinds::KeybindStore;
|
||||
use crate::ClientSeed;
|
||||
|
||||
static ARROW_SEPARATOR: &str = "";
|
||||
static MORE_MSG: &str = " ... ";
|
||||
/// Shorthand for `Action::SwitchToMode{input_mode: InputMode::Normal}`.
|
||||
const TO_NORMAL: Action = Action::SwitchToMode {
|
||||
input_mode: InputMode::Normal,
|
||||
};
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
tabs: Vec<TabInfo>,
|
||||
struct SlotConfig {
|
||||
tip_name: String,
|
||||
classic_ui: bool,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ClientState {
|
||||
tabs: Vec<TabInfo>,
|
||||
mode_info: ModeInfo,
|
||||
text_copy_destination: Option<CopyDestination>,
|
||||
display_system_clipboard_failure: bool,
|
||||
classic_ui: bool,
|
||||
base_mode_is_locked: bool,
|
||||
cached_keybinds: KeybindsVec,
|
||||
new_pane_ribbon_range: Option<(usize, usize)>,
|
||||
floating_ribbon_range: Option<(usize, usize)>,
|
||||
new_pane_ribbon_hovered: bool,
|
||||
floating_ribbon_hovered: bool,
|
||||
}
|
||||
|
||||
register_plugin!(State);
|
||||
#[derive(Default)]
|
||||
struct SlotClientState {
|
||||
new_pane_ribbon_range: Option<(usize, usize)>,
|
||||
floating_ribbon_range: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct StatusBar {
|
||||
slots: BTreeMap<SlotId, SlotConfig>,
|
||||
clients: BTreeMap<ClientId, ClientState>,
|
||||
slot_clients: BTreeMap<(SlotId, ClientId), SlotClientState>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct LinePart {
|
||||
@@ -75,7 +88,6 @@ pub struct ColoredElements {
|
||||
pub unselected: SegmentStyle,
|
||||
pub unselected_alternate: SegmentStyle,
|
||||
pub disabled: SegmentStyle,
|
||||
// superkey
|
||||
pub superkey_prefix: Style,
|
||||
pub superkey_suffix_separator: Style,
|
||||
}
|
||||
@@ -90,10 +102,6 @@ pub struct SegmentStyle {
|
||||
pub suffix_separator: Style,
|
||||
}
|
||||
|
||||
// I really hate this, but I can't come up with a good solution for this,
|
||||
// we need different colors from palette for the default theme
|
||||
// plus here we can add new sources in the future, like Theme
|
||||
// that can be defined in the config perhaps
|
||||
fn color_elements(
|
||||
palette: Styling,
|
||||
different_color_alternates: bool,
|
||||
@@ -229,15 +237,25 @@ fn col_in_range(range: Option<(usize, usize)>, col: usize) -> bool {
|
||||
}
|
||||
}
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
// TODO: Should be able to choose whether to use the cache through config.
|
||||
self.tip_name = get_cached_tip_name();
|
||||
self.classic_ui = configuration
|
||||
impl StatusBar {
|
||||
pub fn has_slots(&self) -> bool {
|
||||
!self.slots.is_empty()
|
||||
}
|
||||
|
||||
pub fn slot_added(&mut self, slot: &Slot) {
|
||||
let tip_name = get_cached_tip_name();
|
||||
let classic_ui = slot
|
||||
.configuration
|
||||
.get("classic")
|
||||
.map(|c| c == "true")
|
||||
.unwrap_or(false);
|
||||
set_selectable(false);
|
||||
self.slots.insert(
|
||||
slot.id,
|
||||
SlotConfig {
|
||||
tip_name,
|
||||
classic_ui,
|
||||
},
|
||||
);
|
||||
subscribe(&[
|
||||
EventType::ModeUpdate,
|
||||
EventType::TabUpdate,
|
||||
@@ -250,36 +268,105 @@ impl ZellijPlugin for State {
|
||||
]);
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
pub fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
self.slots.remove(&slot_id);
|
||||
self.slot_clients.retain(|(s, _), _| *s != slot_id);
|
||||
}
|
||||
|
||||
pub fn client_connected(&mut self, client_id: ClientId) {
|
||||
self.clients.entry(client_id).or_default();
|
||||
}
|
||||
|
||||
pub fn client_disconnected(&mut self, client_id: ClientId) {
|
||||
self.clients.remove(&client_id);
|
||||
self.slot_clients.retain(|(_, c), _| *c != client_id);
|
||||
}
|
||||
|
||||
pub fn client_ids(&self) -> impl Iterator<Item = ClientId> + '_ {
|
||||
self.clients.keys().copied()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.clients.clear();
|
||||
self.slot_clients.clear();
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> BTreeMap<ClientId, ClientSeed> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(client_id, client)| {
|
||||
(
|
||||
*client_id,
|
||||
ClientSeed {
|
||||
mode_info: client.mode_info.clone(),
|
||||
tabs: client.tabs.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn seed(&mut self, seeds: &BTreeMap<ClientId, ClientSeed>) {
|
||||
for (client_id, seed) in seeds {
|
||||
let client = self.clients.entry(*client_id).or_default();
|
||||
client.mode_info = seed.mode_info.clone();
|
||||
client.base_mode_is_locked = client.mode_info.base_mode == Some(InputMode::Locked);
|
||||
client.tabs = seed.tabs.clone();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, event: &Event, context: EventContext) -> RenderResponse {
|
||||
let target_clients: Vec<ClientId> = match context.client_id {
|
||||
Some(client_id) => vec![client_id],
|
||||
None => self.clients.keys().copied().collect(),
|
||||
};
|
||||
let mut should_render = false;
|
||||
for client_id in target_clients {
|
||||
if self.update_client(event, client_id, context.slot_id) {
|
||||
should_render = true;
|
||||
}
|
||||
}
|
||||
if !should_render {
|
||||
RenderResponse::Nothing
|
||||
} else if let Some(client_id) = context.client_id {
|
||||
RenderResponse::Client(client_id)
|
||||
} else {
|
||||
RenderResponse::Slots(self.slots.keys().copied().collect())
|
||||
}
|
||||
}
|
||||
|
||||
fn update_client(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
client_id: ClientId,
|
||||
slot_id: Option<SlotId>,
|
||||
) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
let empty_slot_client = SlotClientState::default();
|
||||
let slot_client = slot_id
|
||||
.and_then(|slot_id| self.slot_clients.get(&(slot_id, client_id)))
|
||||
.unwrap_or(&empty_slot_client);
|
||||
let mut should_render = false;
|
||||
match event {
|
||||
Event::InitialKeybinds(keybinds) => {
|
||||
self.cached_keybinds = keybinds;
|
||||
if !self.cached_keybinds.is_empty() {
|
||||
self.mode_info.keybinds = self.cached_keybinds.clone();
|
||||
}
|
||||
Event::InitialKeybinds(_) => {
|
||||
should_render = true;
|
||||
},
|
||||
Event::ModeUpdate(mut mode_info) => {
|
||||
if mode_info.keybinds.is_empty() && !self.cached_keybinds.is_empty() {
|
||||
mode_info.keybinds = self.cached_keybinds.clone();
|
||||
} else if !mode_info.keybinds.is_empty() {
|
||||
self.cached_keybinds = mode_info.keybinds.clone();
|
||||
}
|
||||
if self.mode_info != mode_info {
|
||||
Event::ModeUpdate(mode_info) => {
|
||||
if &client.mode_info != mode_info {
|
||||
should_render = true;
|
||||
}
|
||||
self.mode_info = mode_info;
|
||||
self.base_mode_is_locked = self.mode_info.base_mode == Some(InputMode::Locked);
|
||||
client.mode_info = mode_info.clone();
|
||||
client.base_mode_is_locked = client.mode_info.base_mode == Some(InputMode::Locked);
|
||||
},
|
||||
Event::TabUpdate(tabs) => {
|
||||
if self.tabs != tabs {
|
||||
if &client.tabs != tabs {
|
||||
should_render = true;
|
||||
}
|
||||
self.tabs = tabs;
|
||||
client.tabs = tabs.clone();
|
||||
},
|
||||
Event::CopyToClipboard(copy_destination) => {
|
||||
match self.text_copy_destination {
|
||||
let copy_destination = *copy_destination;
|
||||
match client.text_copy_destination {
|
||||
Some(text_copy_destination) => {
|
||||
if text_copy_destination != copy_destination {
|
||||
should_render = true;
|
||||
@@ -289,38 +376,40 @@ impl ZellijPlugin for State {
|
||||
should_render = true;
|
||||
},
|
||||
}
|
||||
self.text_copy_destination = Some(copy_destination);
|
||||
client.text_copy_destination = Some(copy_destination);
|
||||
},
|
||||
Event::SystemClipboardFailure => {
|
||||
should_render = true;
|
||||
self.display_system_clipboard_failure = true;
|
||||
client.display_system_clipboard_failure = true;
|
||||
},
|
||||
Event::InputReceived => {
|
||||
if self.text_copy_destination.is_some()
|
||||
|| self.display_system_clipboard_failure == true
|
||||
if client.text_copy_destination.is_some()
|
||||
|| client.display_system_clipboard_failure == true
|
||||
{
|
||||
should_render = true;
|
||||
}
|
||||
self.text_copy_destination = None;
|
||||
self.display_system_clipboard_failure = false;
|
||||
client.text_copy_destination = None;
|
||||
client.display_system_clipboard_failure = false;
|
||||
},
|
||||
Event::Mouse(mouse_event) => match mouse_event {
|
||||
Mouse::LeftClick(_, col) => {
|
||||
if col_in_range(self.new_pane_ribbon_range, col) {
|
||||
let col = *col;
|
||||
if col_in_range(slot_client.new_pane_ribbon_range, col) {
|
||||
new_pane();
|
||||
} else if col_in_range(self.floating_ribbon_range, col) {
|
||||
} else if col_in_range(slot_client.floating_ribbon_range, col) {
|
||||
toggle_floating_panes(None);
|
||||
}
|
||||
},
|
||||
Mouse::Hover(_, col) => {
|
||||
let new_pane_hovered = col_in_range(self.new_pane_ribbon_range, col);
|
||||
let col = *col;
|
||||
let new_pane_hovered = col_in_range(slot_client.new_pane_ribbon_range, col);
|
||||
let floating_hovered =
|
||||
!new_pane_hovered && col_in_range(self.floating_ribbon_range, col);
|
||||
if self.new_pane_ribbon_hovered != new_pane_hovered
|
||||
|| self.floating_ribbon_hovered != floating_hovered
|
||||
!new_pane_hovered && col_in_range(slot_client.floating_ribbon_range, col);
|
||||
if client.new_pane_ribbon_hovered != new_pane_hovered
|
||||
|| client.floating_ribbon_hovered != floating_hovered
|
||||
{
|
||||
self.new_pane_ribbon_hovered = new_pane_hovered;
|
||||
self.floating_ribbon_hovered = floating_hovered;
|
||||
client.new_pane_ribbon_hovered = new_pane_hovered;
|
||||
client.floating_ribbon_hovered = floating_hovered;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
@@ -331,50 +420,77 @@ impl ZellijPlugin for State {
|
||||
should_render
|
||||
}
|
||||
|
||||
fn render(&mut self, rows: usize, cols: usize) {
|
||||
let supports_arrow_fonts = !self.mode_info.capabilities.arrow_fonts;
|
||||
pub fn render(
|
||||
&mut self,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
keybinds: &mut KeybindStore,
|
||||
) {
|
||||
let lent = self.swap_keybinds(client_id, keybinds);
|
||||
self.render_client(rows, cols, slot_id, client_id);
|
||||
if lent {
|
||||
self.swap_keybinds(client_id, keybinds);
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_keybinds(&mut self, client_id: ClientId, keybinds: &mut KeybindStore) -> bool {
|
||||
match self.clients.get_mut(&client_id) {
|
||||
Some(client) => keybinds.swap(client_id, &mut client.mode_info.keybinds),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_client(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
|
||||
let Some(client) = self.clients.get(&client_id) else {
|
||||
return;
|
||||
};
|
||||
let Some(slot) = self.slots.get(&slot_id) else {
|
||||
return;
|
||||
};
|
||||
let supports_arrow_fonts = !client.mode_info.capabilities.arrow_fonts;
|
||||
let separator = if supports_arrow_fonts {
|
||||
ARROW_SEPARATOR
|
||||
} else {
|
||||
""
|
||||
};
|
||||
|
||||
let background = self.mode_info.style.colors.text_unselected.background;
|
||||
let background = client.mode_info.style.colors.text_unselected.background;
|
||||
|
||||
if rows == 1 && !self.classic_ui {
|
||||
if rows == 1 && !slot.classic_ui {
|
||||
let fill_bg = match background {
|
||||
PaletteColor::Rgb((r, g, b)) => format!("\u{1b}[48;2;{};{};{}m\u{1b}[0K", r, g, b),
|
||||
PaletteColor::EightBit(color) => format!("\u{1b}[48;5;{}m\u{1b}[0K", color),
|
||||
};
|
||||
let active_tab = self.tabs.iter().find(|t| t.active);
|
||||
let active_tab = client.tabs.iter().find(|t| t.active);
|
||||
let (line, new_pane_ribbon_range, floating_ribbon_range) = one_line_ui(
|
||||
&self.mode_info,
|
||||
&client.mode_info,
|
||||
active_tab,
|
||||
cols,
|
||||
separator,
|
||||
self.base_mode_is_locked,
|
||||
self.text_copy_destination,
|
||||
self.display_system_clipboard_failure,
|
||||
self.new_pane_ribbon_hovered,
|
||||
self.floating_ribbon_hovered,
|
||||
client.base_mode_is_locked,
|
||||
client.text_copy_destination,
|
||||
client.display_system_clipboard_failure,
|
||||
client.new_pane_ribbon_hovered,
|
||||
client.floating_ribbon_hovered,
|
||||
);
|
||||
self.new_pane_ribbon_range = new_pane_ribbon_range;
|
||||
self.floating_ribbon_range = floating_ribbon_range;
|
||||
let slot_client = self.slot_clients.entry((slot_id, client_id)).or_default();
|
||||
slot_client.new_pane_ribbon_range = new_pane_ribbon_range;
|
||||
slot_client.floating_ribbon_range = floating_ribbon_range;
|
||||
print!("{}{}", line, fill_bg);
|
||||
return;
|
||||
}
|
||||
self.new_pane_ribbon_range = None;
|
||||
self.floating_ribbon_range = None;
|
||||
let slot_client = self.slot_clients.entry((slot_id, client_id)).or_default();
|
||||
slot_client.new_pane_ribbon_range = None;
|
||||
slot_client.floating_ribbon_range = None;
|
||||
|
||||
//TODO: Switch to UI components here
|
||||
let active_tab = self.tabs.iter().find(|t| t.active);
|
||||
let first_line = first_line(&self.mode_info, active_tab, cols, separator);
|
||||
let second_line = self.second_line(cols);
|
||||
let show_nested_session_hint = self.mode_info.session_dimmed.unwrap_or(false)
|
||||
|| self.mode_info.session_ascended.unwrap_or(false);
|
||||
let active_tab = client.tabs.iter().find(|t| t.active);
|
||||
let first_line = first_line(&client.mode_info, active_tab, cols, separator);
|
||||
let second_line = second_line(client, &slot.tip_name, cols);
|
||||
let show_nested_session_hint = client.mode_info.session_dimmed.unwrap_or(false)
|
||||
|| client.mode_info.session_ascended.unwrap_or(false);
|
||||
|
||||
// [48;5;238m is white background, [0K is so that it fills the rest of the line
|
||||
// [m is background reset, [0K is so that it clears the rest of the line
|
||||
match background {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
if rows > 1 {
|
||||
@@ -382,7 +498,7 @@ impl ZellijPlugin for State {
|
||||
} else {
|
||||
if show_nested_session_hint {
|
||||
print!("\u{1b}[m{}\u{1b}[0K", second_line);
|
||||
} else if self.mode_info.mode == InputMode::Normal {
|
||||
} else if client.mode_info.mode == InputMode::Normal {
|
||||
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", first_line, r, g, b);
|
||||
} else {
|
||||
print!("\u{1b}[m{}\u{1b}[0K", second_line);
|
||||
@@ -395,7 +511,7 @@ impl ZellijPlugin for State {
|
||||
} else {
|
||||
if show_nested_session_hint {
|
||||
print!("\u{1b}[m{}\u{1b}[0K", second_line);
|
||||
} else if self.mode_info.mode == InputMode::Normal {
|
||||
} else if client.mode_info.mode == InputMode::Normal {
|
||||
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", first_line, color);
|
||||
} else {
|
||||
print!("\u{1b}[m{}\u{1b}[0K", second_line);
|
||||
@@ -410,47 +526,45 @@ impl ZellijPlugin for State {
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn second_line(&self, cols: usize) -> LinePart {
|
||||
let active_tab = self.tabs.iter().find(|t| t.active);
|
||||
fn second_line(client: &ClientState, tip_name: &str, cols: usize) -> LinePart {
|
||||
let active_tab = client.tabs.iter().find(|t| t.active);
|
||||
|
||||
if self.mode_info.session_dimmed.unwrap_or(false) {
|
||||
return descended_into_nested_session_hint(&self.mode_info, cols);
|
||||
}
|
||||
if self.mode_info.session_ascended.unwrap_or(false) {
|
||||
return ascended_to_host_session_hint(&self.mode_info, cols);
|
||||
}
|
||||
if let Some(copy_destination) = self.text_copy_destination {
|
||||
text_copied_hint(copy_destination)
|
||||
} else if self.display_system_clipboard_failure {
|
||||
system_clipboard_error(&self.mode_info.style.colors)
|
||||
} else if let Some(active_tab) = active_tab {
|
||||
if active_tab.is_fullscreen_active {
|
||||
match self.mode_info.mode {
|
||||
InputMode::Normal => fullscreen_panes_to_hide(
|
||||
&self.mode_info.style.colors,
|
||||
active_tab.panes_to_hide,
|
||||
),
|
||||
InputMode::Locked => locked_fullscreen_panes_to_hide(
|
||||
&self.mode_info.style.colors,
|
||||
active_tab.panes_to_hide,
|
||||
),
|
||||
_ => keybinds(&self.mode_info, &self.tip_name, cols),
|
||||
}
|
||||
} else if active_tab.are_floating_panes_visible {
|
||||
match self.mode_info.mode {
|
||||
InputMode::Normal => floating_panes_are_visible(&self.mode_info),
|
||||
InputMode::Locked => {
|
||||
locked_floating_panes_are_visible(&self.mode_info.style.colors)
|
||||
},
|
||||
_ => keybinds(&self.mode_info, &self.tip_name, cols),
|
||||
}
|
||||
} else {
|
||||
keybinds(&self.mode_info, &self.tip_name, cols)
|
||||
if client.mode_info.session_dimmed.unwrap_or(false) {
|
||||
return descended_into_nested_session_hint(&client.mode_info, cols);
|
||||
}
|
||||
if client.mode_info.session_ascended.unwrap_or(false) {
|
||||
return ascended_to_host_session_hint(&client.mode_info, cols);
|
||||
}
|
||||
if let Some(copy_destination) = client.text_copy_destination {
|
||||
text_copied_hint(copy_destination)
|
||||
} else if client.display_system_clipboard_failure {
|
||||
system_clipboard_error(&client.mode_info.style.colors)
|
||||
} else if let Some(active_tab) = active_tab {
|
||||
if active_tab.is_fullscreen_active {
|
||||
match client.mode_info.mode {
|
||||
InputMode::Normal => fullscreen_panes_to_hide(
|
||||
&client.mode_info.style.colors,
|
||||
active_tab.panes_to_hide,
|
||||
),
|
||||
InputMode::Locked => locked_fullscreen_panes_to_hide(
|
||||
&client.mode_info.style.colors,
|
||||
active_tab.panes_to_hide,
|
||||
),
|
||||
_ => keybinds(&client.mode_info, tip_name, cols),
|
||||
}
|
||||
} else if active_tab.are_floating_panes_visible {
|
||||
match client.mode_info.mode {
|
||||
InputMode::Normal => floating_panes_are_visible(&client.mode_info),
|
||||
InputMode::Locked => {
|
||||
locked_floating_panes_are_visible(&client.mode_info.style.colors)
|
||||
},
|
||||
_ => keybinds(&client.mode_info, tip_name, cols),
|
||||
}
|
||||
} else {
|
||||
LinePart::default()
|
||||
keybinds(&client.mode_info, tip_name, cols)
|
||||
}
|
||||
} else {
|
||||
LinePart::default()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -468,12 +582,6 @@ pub fn get_common_modifiers(mut keyvec: Vec<&KeyWithModifier>) -> Vec<KeyModifie
|
||||
common_modifiers.into_iter().collect()
|
||||
}
|
||||
|
||||
/// Get key from action pattern(s).
|
||||
///
|
||||
/// This function takes as arguments a `keymap` that is a `Vec<(Key, Vec<Action>)>` and contains
|
||||
/// all keybindings for the current mode and one or more `p` patterns which match a sequence of
|
||||
/// actions to search for. If within the keymap a sequence of actions matching `p` is found, all
|
||||
/// keys that trigger the action pattern are returned as vector of `Vec<Key>`.
|
||||
pub fn action_key(
|
||||
keymap: &[(KeyWithModifier, Vec<Action>)],
|
||||
action: &[Action],
|
||||
@@ -496,9 +604,6 @@ pub fn action_key(
|
||||
.collect::<Vec<KeyWithModifier>>()
|
||||
}
|
||||
|
||||
/// Get multiple keys for multiple actions.
|
||||
///
|
||||
/// An extension of [`action_key`] that iterates over all action tuples and collects the results.
|
||||
pub fn action_key_group(
|
||||
keymap: &[(KeyWithModifier, Vec<Action>)],
|
||||
actions: &[&[Action]],
|
||||
@@ -510,25 +615,6 @@ pub fn action_key_group(
|
||||
ret
|
||||
}
|
||||
|
||||
/// Style a vector of [`Key`]s with the given [`Palette`].
|
||||
///
|
||||
/// Creates a line segment of style `<KEYS>`, with correct theming applied: The brackets have the
|
||||
/// regular text color, the enclosed keys are painted green and bold. If the keys share a common
|
||||
/// modifier (See [`get_common_modifier`]), it is printed in front of the keys, painted green and
|
||||
/// bold, separated with a `+`: `MOD + <KEYS>`.
|
||||
///
|
||||
/// If multiple [`Key`]s are given, the individual keys are separated with a `|` char. This does
|
||||
/// not apply to the following groups of keys which are treated specially and don't have a
|
||||
/// separator between them:
|
||||
///
|
||||
/// - "hjkl"
|
||||
/// - "HJKL"
|
||||
/// - "←↓↑→"
|
||||
/// - "←→"
|
||||
/// - "↓↑"
|
||||
///
|
||||
/// The returned Vector of [`ANSIString`] is suitable for transformation into an [`ANSIStrings`]
|
||||
/// type.
|
||||
pub fn style_key_with_modifier(
|
||||
keyvec: &[KeyWithModifier],
|
||||
palette: &Styling,
|
||||
@@ -567,7 +653,6 @@ pub fn style_key_with_modifier(
|
||||
};
|
||||
ret.push(painted_modifier);
|
||||
|
||||
// Prints key group start
|
||||
let group_start_str = if no_common_modifier { "<" } else { " + <" };
|
||||
if let Some(background) = background {
|
||||
let background = palette_match!(background);
|
||||
@@ -581,7 +666,6 @@ pub fn style_key_with_modifier(
|
||||
ret.push(Style::new().fg(text_color).paint(group_start_str));
|
||||
}
|
||||
|
||||
// Prints the keys
|
||||
let key = keyvec
|
||||
.iter()
|
||||
.map(|key| {
|
||||
@@ -604,7 +688,6 @@ pub fn style_key_with_modifier(
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Special handling of some pre-defined keygroups
|
||||
let key_string = key.join("");
|
||||
let key_separator = match &key_string[..] {
|
||||
"HJKL" => "",
|
||||
@@ -728,7 +811,7 @@ pub mod tests {
|
||||
KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier(),
|
||||
];
|
||||
let ret = get_common_modifiers(keyvec.iter().collect());
|
||||
assert_eq!(ret, vec![]); // no common modifiers
|
||||
assert_eq!(ret, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -739,7 +822,7 @@ pub mod tests {
|
||||
KeyWithModifier::new(BareKey::Char('z')).with_alt_modifier(),
|
||||
];
|
||||
let ret = get_common_modifiers(keyvec.iter().collect());
|
||||
assert_eq!(ret, vec![]); // no common modifiers
|
||||
assert_eq!(ret, vec![]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -791,7 +874,6 @@ pub mod tests {
|
||||
fn action_key_group_two_patterns() {
|
||||
let keymap = big_keymap();
|
||||
let ret = action_key_group(&keymap, &[&[Action::ScrollDown], &[Action::ScrollUp]]);
|
||||
// Mind the order!
|
||||
assert_eq!(
|
||||
ret,
|
||||
vec![
|
||||
+16
-40
@@ -8,13 +8,13 @@ use zellij_tile::prelude::actions::Action;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::{palette_match, style};
|
||||
|
||||
use crate::first_line::{to_char, KeyAction, KeyMode, KeyShortcut};
|
||||
use crate::second_line::{
|
||||
use crate::status_bar::first_line::{to_char, KeyAction, KeyMode, KeyShortcut};
|
||||
use crate::status_bar::second_line::{
|
||||
ascended_to_host_session_hint, descended_into_nested_session_hint, system_clipboard_error,
|
||||
text_copied_hint,
|
||||
};
|
||||
use crate::{action_key, action_key_group, color_elements, MORE_MSG, TO_NORMAL};
|
||||
use crate::{ColoredElements, LinePart};
|
||||
use crate::status_bar::{action_key, action_key_group, color_elements, MORE_MSG, TO_NORMAL};
|
||||
use crate::status_bar::{ColoredElements, LinePart};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
pub fn one_line_ui(
|
||||
@@ -739,7 +739,6 @@ fn render_common_modifiers(
|
||||
separator: &str,
|
||||
) {
|
||||
let prefix_text = if mode_info.capabilities.arrow_fonts {
|
||||
// Add extra space in simplified ui
|
||||
format!(
|
||||
" {} + ",
|
||||
common_modifiers
|
||||
@@ -787,7 +786,7 @@ fn render_secondary_info(
|
||||
let (secondary_keybinds, new_pane_range, floating_range) =
|
||||
secondary_keybinds(&help, tab_info, max_len, new_pane_hovered, floating_hovered);
|
||||
secondary_info.append(&secondary_keybinds);
|
||||
let remaining_space = max_len.saturating_sub(secondary_info.len).saturating_sub(1); // 1 for the end padding of the line
|
||||
let remaining_space = max_len.saturating_sub(secondary_info.len).saturating_sub(1);
|
||||
let mut padding = String::new();
|
||||
let mut padding_len = 0;
|
||||
for _ in 0..remaining_space {
|
||||
@@ -830,7 +829,6 @@ fn secondary_keybinds(
|
||||
let mut floating_range;
|
||||
let binds = &help.get_mode_keybinds();
|
||||
let should_show_focus_and_resize_shortcuts = should_show_focus_and_resize_shortcuts(tab_info);
|
||||
// New Pane
|
||||
let new_pane_action_key = action_key(
|
||||
binds,
|
||||
&[Action::NewPane {
|
||||
@@ -850,7 +848,6 @@ fn secondary_keybinds(
|
||||
vec![]
|
||||
};
|
||||
|
||||
// Resize
|
||||
let resize_increase_action_key = action_key(
|
||||
binds,
|
||||
&[Action::Resize {
|
||||
@@ -881,10 +878,8 @@ fn secondary_keybinds(
|
||||
resize_shortcuts.push(resize_decrease_key.clone());
|
||||
}
|
||||
|
||||
// Move focus
|
||||
let mut move_focus_shortcuts: Vec<KeyWithModifier> = vec![];
|
||||
|
||||
// Left
|
||||
let move_focus_left_action_key = action_key(
|
||||
binds,
|
||||
&[Action::MoveFocusOrTab {
|
||||
@@ -898,7 +893,6 @@ fn secondary_keybinds(
|
||||
if let Some(move_focus_left_key) = move_focus_left_key {
|
||||
move_focus_shortcuts.push(move_focus_left_key.clone());
|
||||
}
|
||||
// Down
|
||||
let move_focus_left_action_key = action_key(
|
||||
binds,
|
||||
&[Action::MoveFocus {
|
||||
@@ -912,7 +906,6 @@ fn secondary_keybinds(
|
||||
if let Some(move_focus_left_key) = move_focus_left_key {
|
||||
move_focus_shortcuts.push(move_focus_left_key.clone());
|
||||
}
|
||||
// Up
|
||||
let move_focus_left_action_key = action_key(
|
||||
binds,
|
||||
&[Action::MoveFocus {
|
||||
@@ -926,7 +919,6 @@ fn secondary_keybinds(
|
||||
if let Some(move_focus_left_key) = move_focus_left_key {
|
||||
move_focus_shortcuts.push(move_focus_left_key.clone());
|
||||
}
|
||||
// Right
|
||||
let move_focus_left_action_key = action_key(
|
||||
binds,
|
||||
&[Action::MoveFocusOrTab {
|
||||
@@ -1293,7 +1285,7 @@ fn hovered_ribbon_wrap(body: String, palette: Styling, supports_arrow_fonts: boo
|
||||
let ribbon_bg = palette.ribbon_unselected.emphasis_1;
|
||||
let outer_bg = palette.text_unselected.background;
|
||||
let arrow = if supports_arrow_fonts {
|
||||
crate::ARROW_SEPARATOR
|
||||
crate::status_bar::ARROW_SEPARATOR
|
||||
} else {
|
||||
""
|
||||
};
|
||||
@@ -1401,8 +1393,7 @@ fn add_shortcut(
|
||||
return ret;
|
||||
}
|
||||
|
||||
ret.append(&style_key_with_modifier(&keys, key_color_index, dimmed)); // TODO: alternate
|
||||
//
|
||||
ret.append(&style_key_with_modifier(&keys, key_color_index, dimmed));
|
||||
let ribbon = if dimmed {
|
||||
serialize_ribbon(&Text::new(format!("{}", text)).disabled())
|
||||
} else if selected {
|
||||
@@ -1413,9 +1404,9 @@ fn add_shortcut(
|
||||
ret.part = format!("{}{}", ret.part, ribbon);
|
||||
let supports_arrow_fonts = !help.capabilities.arrow_fonts;
|
||||
ret.len += if supports_arrow_fonts {
|
||||
text.width() + 4 // padding and arrow fonts
|
||||
text.width() + 4
|
||||
} else {
|
||||
text.width() + 2 // padding
|
||||
text.width() + 2
|
||||
};
|
||||
ret
|
||||
}
|
||||
@@ -1475,9 +1466,9 @@ fn add_shortcut_with_inline_key(
|
||||
ret.part = ribbon;
|
||||
let supports_arrow_fonts = !capabilities.arrow_fonts;
|
||||
ret.len += if supports_arrow_fonts {
|
||||
text.width() + key_string.width() + 7 // padding, group boundaries and arrow fonts
|
||||
text.width() + key_string.width() + 7
|
||||
} else {
|
||||
text.width() + key_string.width() + 5 // padding and group boundaries
|
||||
text.width() + key_string.width() + 5
|
||||
};
|
||||
|
||||
ret
|
||||
@@ -1515,9 +1506,9 @@ fn add_shortcut_with_key_only(
|
||||
ret.part = ribbon;
|
||||
let supports_arrow_fonts = !help.capabilities.arrow_fonts;
|
||||
ret.len += if supports_arrow_fonts {
|
||||
key_string.width() + 4 // 4 => arrow fonts + padding
|
||||
key_string.width() + 4
|
||||
} else {
|
||||
key_string.width() + 2 // 2 => padding
|
||||
key_string.width() + 2
|
||||
};
|
||||
ret
|
||||
}
|
||||
@@ -1525,7 +1516,7 @@ fn add_shortcut_with_key_only(
|
||||
fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
|
||||
let supports_arrow_fonts = !help.capabilities.arrow_fonts;
|
||||
let separator = if supports_arrow_fonts {
|
||||
crate::ARROW_SEPARATOR
|
||||
crate::status_bar::ARROW_SEPARATOR
|
||||
} else {
|
||||
" "
|
||||
};
|
||||
@@ -1582,7 +1573,7 @@ fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
|
||||
.bold()
|
||||
.paint(format!(" {} ", mode_help_text)),
|
||||
);
|
||||
ret.len += mode_help_text.width() + 2; // 2 => padding
|
||||
ret.len += mode_help_text.width() + 2;
|
||||
}
|
||||
bits.push(
|
||||
Style::new()
|
||||
@@ -1606,7 +1597,7 @@ fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
|
||||
.paint(format!("{}", separator)),
|
||||
);
|
||||
ret.part = format!("{}{}", ret.part, ANSIStrings(&bits));
|
||||
ret.len += 3; // padding and arrow fonts
|
||||
ret.len += 3;
|
||||
|
||||
if ret.len <= max_len {
|
||||
Some(ret)
|
||||
@@ -1644,27 +1635,20 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
|
||||
let mut old_keymap = mi.get_mode_keybinds();
|
||||
let s = |string: &str| string.to_string();
|
||||
|
||||
// Find a keybinding to get back to "Normal" input mode. In this case we prefer '\n' over other
|
||||
// choices. Do it here before we dedupe the keymap below!
|
||||
let base_mode = mi.base_mode;
|
||||
let to_basemode_keys = base_mode.map(|b| action_key(&old_keymap, &[to_base_mode(b)])).unwrap_or_else(|| action_key(&old_keymap, &[TO_NORMAL]));
|
||||
let to_basemode_key = if to_basemode_keys.contains(&KeyWithModifier::new(BareKey::Enter)) {
|
||||
vec![KeyWithModifier::new(BareKey::Enter)]
|
||||
} else {
|
||||
// Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise.
|
||||
to_basemode_keys.into_iter().take(1).collect()
|
||||
};
|
||||
|
||||
// Sort and deduplicate the keybindings first. We sort after the `Key`s, and deduplicate by
|
||||
// their `Action` vectors. An unstable sort is fine here because if the user maps anything to
|
||||
// the same key again, anything will happen...
|
||||
old_keymap.sort_unstable_by(|(keya, _), (keyb, _)| keya.partial_cmp(keyb).unwrap());
|
||||
|
||||
let mut known_actions: Vec<Vec<Action>> = vec![];
|
||||
let mut km = vec![];
|
||||
for (key, acvec) in old_keymap {
|
||||
if known_actions.contains(&acvec) {
|
||||
// This action is known already
|
||||
continue;
|
||||
} else {
|
||||
known_actions.push(acvec.to_vec());
|
||||
@@ -1689,12 +1673,6 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
|
||||
(s("Stack"), s("Stack"), single_action_key(&km, &[A::NewStackedPane{command: None, pane_name: None, near_current_pane: false, no_focus: false, tab_id: None}, TO_NORMAL])),
|
||||
(s("Select pane"), s("Select"), to_basemode_key),
|
||||
]} else if mi.mode == IM::Tab {
|
||||
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
|
||||
// to moving tabs focus (left/up go left, right/down go right). Since we sort the keys
|
||||
// above and then dedpulicate based on the actions, we will end up with LeftArrow for
|
||||
// "left" and DownArrow for "right". What we really expect is to see LeftArrow and
|
||||
// RightArrow.
|
||||
// FIXME: So for lack of a better idea we just check this case manually here.
|
||||
let old_keymap = mi.get_mode_keybinds();
|
||||
let focus_keys_full: Vec<KeyWithModifier> = action_key_group(&old_keymap,
|
||||
&[&[A::GoToPreviousTab], &[A::GoToNextTab]]);
|
||||
@@ -2007,7 +1985,6 @@ fn style_key_with_modifier(
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
|
||||
// Prints the keys
|
||||
let key = keyvec
|
||||
.iter()
|
||||
.map(|key| {
|
||||
@@ -2019,7 +1996,6 @@ fn style_key_with_modifier(
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Special handling of some pre-defined keygroups
|
||||
let key_string = key.join("");
|
||||
let key_separator = match &key_string[..] {
|
||||
"HJKL" => "",
|
||||
+6
-54
@@ -7,7 +7,7 @@ use zellij_tile::prelude::actions::Action;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
use crate::{
|
||||
use crate::status_bar::{
|
||||
action_key, action_key_group, style_key_with_modifier,
|
||||
tip::{data::TIPS, TipFn},
|
||||
LinePart, MORE_MSG, TO_NORMAL,
|
||||
@@ -81,29 +81,6 @@ fn full_shortcut_list_nonstandard_mode(help: &ModeInfo) -> LinePart {
|
||||
line_part
|
||||
}
|
||||
|
||||
/// Collect all relevant keybindings and hints to display.
|
||||
///
|
||||
/// Creates a vector with tuples containing the following entries:
|
||||
///
|
||||
/// - A String to display for this keybinding when there are no size restrictions,
|
||||
/// - A shortened String (where sensible) to display if the whole second line becomes too long,
|
||||
/// - A `Vec<Key>` of the keys that map to this keyhint
|
||||
///
|
||||
/// This vector is created by iterating over the keybindings for the current [`InputMode`] and
|
||||
/// storing all Keybindings that match pre-defined patterns of `Action`s. For example, the
|
||||
/// `InputMode::Pane` input mode determines which keys to display for the "Move focus" hint by
|
||||
/// searching the keybindings for anything that matches the `Action::MoveFocus(_)` action. Since by
|
||||
/// default multiple keybindings map to some action patterns (e.g. `Action::MoveFocus(_)` is bound
|
||||
/// to "hjkl", the arrow keys and "Alt + <hjkl>"), we deduplicate the vector of all keybindings
|
||||
/// before processing it.
|
||||
///
|
||||
/// Therefore we sort it by the [`Key`]s of the current keymap and deduplicate the resulting sorted
|
||||
/// vector by the `Vec<Action>` action vectors bound to the keys. As such, when multiple keys map
|
||||
/// to the same sequence of actions, the keys that appear first in the [`Key`] structure will be
|
||||
/// displayed.
|
||||
// Please don't let rustfmt play with the formatting. It will stretch out the function to about
|
||||
// three times the length and all the keybinding vectors we generate become virtually unreadable
|
||||
// for humans.
|
||||
#[rustfmt::skip]
|
||||
fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier>)> {
|
||||
use Action as A;
|
||||
@@ -115,26 +92,19 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
|
||||
let mut old_keymap = mi.get_mode_keybinds();
|
||||
let s = |string: &str| string.to_string();
|
||||
|
||||
// Find a keybinding to get back to "Normal" input mode. In this case we prefer '\n' over other
|
||||
// choices. Do it here before we dedupe the keymap below!
|
||||
let to_normal_keys = action_key(&old_keymap, &[TO_NORMAL]);
|
||||
let to_normal_key = if to_normal_keys.contains(&KeyWithModifier::new(BareKey::Enter)) {
|
||||
vec![KeyWithModifier::new(BareKey::Enter)]
|
||||
} else {
|
||||
// Yield `vec![key]` if `to_normal_keys` has at least one key, or an empty vec otherwise.
|
||||
to_normal_keys.into_iter().take(1).collect()
|
||||
};
|
||||
|
||||
// Sort and deduplicate the keybindings first. We sort after the `Key`s, and deduplicate by
|
||||
// their `Action` vectors. An unstable sort is fine here because if the user maps anything to
|
||||
// the same key again, anything will happen...
|
||||
old_keymap.sort_unstable_by(|(keya, _), (keyb, _)| keya.partial_cmp(keyb).unwrap());
|
||||
|
||||
let mut known_actions: Vec<Vec<Action>> = vec![];
|
||||
let mut km = vec![];
|
||||
for (key, acvec) in old_keymap {
|
||||
if known_actions.contains(&acvec) {
|
||||
// This action is known already
|
||||
continue;
|
||||
} else {
|
||||
known_actions.push(acvec.to_vec());
|
||||
@@ -156,12 +126,6 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
|
||||
(s("Toggle Embed"), s("Embed"), action_key(&km, &[A::TogglePaneEmbedOrFloating, TO_NORMAL])),
|
||||
(s("Select pane"), s("Select"), to_normal_key),
|
||||
]} else if mi.mode == IM::Tab {
|
||||
// With the default bindings, "Move focus" for tabs is tricky: It binds all the arrow keys
|
||||
// to moving tabs focus (left/up go left, right/down go right). Since we sort the keys
|
||||
// above and then dedpulicate based on the actions, we will end up with LeftArrow for
|
||||
// "left" and DownArrow for "right". What we really expect is to see LeftArrow and
|
||||
// RightArrow.
|
||||
// FIXME: So for lack of a better idea we just check this case manually here.
|
||||
let old_keymap = mi.get_mode_keybinds();
|
||||
let focus_keys_full: Vec<KeyWithModifier> = action_key_group(&old_keymap,
|
||||
&[&[A::GoToPreviousTab], &[A::GoToNextTab]]);
|
||||
@@ -254,7 +218,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
|
||||
action_key(&km, &[A::SearchToggleOption{option: SOpt::WholeWord}])),
|
||||
]} else if mi.mode == IM::Session { vec![
|
||||
(s("Detach"), s("Detach"), action_key(&km, &[Action::Detach])),
|
||||
(s("Session Manager"), s("Manager"), action_key(&km, &[A::LaunchOrFocusPlugin{plugin: Default::default(), should_float: true, move_to_focused_tab: true, should_open_in_place: false, close_replaced_pane: false, skip_cache: false, tab_id: None}, TO_NORMAL])), // not entirely accurate
|
||||
(s("Session Manager"), s("Manager"), action_key(&km, &[A::LaunchOrFocusPlugin{plugin: Default::default(), should_float: true, move_to_focused_tab: true, should_open_in_place: false, close_replaced_pane: false, skip_cache: false, tab_id: None}, TO_NORMAL])),
|
||||
(s("Select pane"), s("Select"), to_normal_key),
|
||||
]} else if mi.mode == IM::Tmux { vec![
|
||||
(s("Move focus"), s("Move"), action_key_group(&km, &[
|
||||
@@ -352,7 +316,6 @@ fn best_effort_shortcut_list(help: &ModeInfo, tip: TipFn, max_len: usize) -> Lin
|
||||
}
|
||||
|
||||
pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart {
|
||||
// It is assumed that there is at least one TIP data in the TIPS HasMap.
|
||||
let tip_body = TIPS
|
||||
.get(tip_name)
|
||||
.unwrap_or_else(|| TIPS.get("quicknav").unwrap());
|
||||
@@ -427,7 +390,7 @@ pub fn text_copied_hint(copy_destination: CopyDestination) -> LinePart {
|
||||
CopyDestination::Command => "Text piped to external command",
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
CopyDestination::Primary => "Text copied to system primary selection",
|
||||
#[cfg(target_os = "macos")] // primary selection does not exist on macos
|
||||
#[cfg(target_os = "macos")]
|
||||
CopyDestination::Primary => "Text copied to system clipboard",
|
||||
CopyDestination::System => "Text copied to system clipboard",
|
||||
};
|
||||
@@ -460,7 +423,7 @@ pub fn fullscreen_panes_to_hide(palette: &Styling, panes_to_hide: usize) -> Line
|
||||
+ puls.chars().count()
|
||||
+ panes.chars().count()
|
||||
+ hide.chars().count()
|
||||
+ 5; // 3 for ():'s around shortcut, 2 for the space
|
||||
+ 5;
|
||||
LinePart {
|
||||
part: format!(
|
||||
"{}{}{}{}{}{}",
|
||||
@@ -518,7 +481,7 @@ pub fn floating_panes_are_visible(mode_info: &ModeInfo) -> LinePart {
|
||||
+ p.chars().count()
|
||||
+ p_right_separator.chars().count()
|
||||
+ to_hide.chars().count()
|
||||
+ 5; // 3 for ():'s around floating_panes, 2 for the space
|
||||
+ 5;
|
||||
LinePart {
|
||||
part: format!(
|
||||
"{}{}{}{}{}{}{}{}{}{}",
|
||||
@@ -553,7 +516,7 @@ pub fn locked_fullscreen_panes_to_hide(palette: &Styling, panes_to_hide: usize)
|
||||
+ puls.chars().count()
|
||||
+ panes.chars().count()
|
||||
+ hide.chars().count()
|
||||
+ 5; // 3 for ():'s around shortcut, 2 for the space
|
||||
+ 5;
|
||||
LinePart {
|
||||
part: format!(
|
||||
"{}{}{}{}{}{}{}",
|
||||
@@ -591,16 +554,9 @@ pub fn locked_floating_panes_are_visible(palette: &Styling) -> LinePart {
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
/// Unit tests.
|
||||
///
|
||||
/// Note that we cheat a little here, because the number of things one may want to test is endless,
|
||||
/// and creating a Mockup of [`ModeInfo`] by hand for all these testcases is nothing less than
|
||||
/// torture. Hence, we test the most atomic unit thoroughly ([`full_length_shortcut`] and then test
|
||||
/// the public API ([`keybinds`]) to ensure correct operation.
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Strip style information from `LinePart` and return a raw String instead
|
||||
fn unstyle(line_part: LinePart) -> String {
|
||||
let string = line_part.to_string();
|
||||
|
||||
@@ -633,7 +589,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// When there is no binding, we print no shortcut either
|
||||
fn full_length_shortcut_without_key() {
|
||||
let keyvec = vec![];
|
||||
let palette = Styling::default();
|
||||
@@ -732,10 +687,8 @@ mod tests {
|
||||
|
||||
assert_eq!(ret, " / Ctrl + <a|b|c> Foobar");
|
||||
}
|
||||
//pub fn keybinds(help: &ModeInfo, tip_name: &str, max_width: usize) -> LinePart {
|
||||
|
||||
#[test]
|
||||
// Note how it leaves out elements that don't exist!
|
||||
fn keybinds_wide() {
|
||||
let mode_info = ModeInfo {
|
||||
mode: InputMode::Pane,
|
||||
@@ -800,7 +753,6 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
// Note how "Move focus" becomes "Move"
|
||||
fn keybinds_tight_width() {
|
||||
let mode_info = ModeInfo {
|
||||
mode: InputMode::Pane,
|
||||
-3
@@ -24,13 +24,10 @@ pub type LocalCacheResult = Result<LocalCache, LocalCacheError>;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum LocalCacheError {
|
||||
// Io error
|
||||
#[error("IoError: {0}")]
|
||||
Io(#[from] io::Error),
|
||||
// Io error with path context
|
||||
#[error("IoError: {0}, File: {1}")]
|
||||
IoPath(io::Error, PathBuf),
|
||||
// Deserialization error
|
||||
#[error("Deserialization error: {0}")]
|
||||
Serde(#[from] serde_json::Error),
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub const DEFAULT_CACHE_FILE_PATH: &str = "/tmp/status-bar-tips.cache";
|
||||
pub const MAX_CACHE_HITS: usize = 20;
|
||||
+2
-8
@@ -4,8 +4,8 @@ use ansi_term::{
|
||||
Style,
|
||||
};
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::{action_key, style_key_with_modifier};
|
||||
use crate::status_bar::LinePart;
|
||||
use crate::status_bar::{action_key, style_key_with_modifier};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
@@ -23,8 +23,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn compact_layout_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: UI taking up too much space? Start Zellij with
|
||||
// zellij -l compact or remove pane frames with Ctrl + <p> + <z>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
@@ -41,8 +39,6 @@ pub fn compact_layout_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn compact_layout_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: To save screen space, start Zellij with
|
||||
// zellij -l compact or remove pane frames with Ctrl + <p> + <z>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
@@ -59,8 +55,6 @@ pub fn compact_layout_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn compact_layout_short(help: &ModeInfo) -> LinePart {
|
||||
// Save screen space, start Zellij with
|
||||
// zellij -l compact or remove pane frames with Ctrl + <p> + <z>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
+1
-7
@@ -4,7 +4,7 @@ use ansi_term::{
|
||||
Style,
|
||||
};
|
||||
|
||||
use crate::{action_key, style_key_with_modifier, LinePart};
|
||||
use crate::status_bar::{action_key, style_key_with_modifier, LinePart};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
@@ -22,8 +22,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn edit_scrollbuffer_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Search through the scrollbuffer using your default $EDITOR with
|
||||
// Ctrl + <s> + <e>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
@@ -37,8 +35,6 @@ pub fn edit_scrollbuffer_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn edit_scrollbuffer_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Search the scrollbuffer using your $EDITOR with
|
||||
// Ctrl + <s> + <e>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
@@ -52,8 +48,6 @@ pub fn edit_scrollbuffer_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn edit_scrollbuffer_short(help: &ModeInfo) -> LinePart {
|
||||
// Search using $EDITOR with
|
||||
// Ctrl + <s> + <e>
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
let mut bits = vec![
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style};
|
||||
|
||||
use crate::{action_key, style_key_with_modifier, LinePart};
|
||||
use crate::status_bar::{action_key, style_key_with_modifier, LinePart};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
|
||||
macro_rules! strings {
|
||||
@@ -17,7 +17,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn floating_panes_mouse_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Toggle floating panes with Ctrl + <p> + <w> and move them with keyboard or mouse
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("Toggle floating panes with "),
|
||||
@@ -28,7 +27,6 @@ pub fn floating_panes_mouse_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn floating_panes_mouse_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Toggle floating panes with Ctrl + <p> + <w>
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("Toggle floating panes with "),
|
||||
@@ -38,7 +36,6 @@ pub fn floating_panes_mouse_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn floating_panes_mouse_short(help: &ModeInfo) -> LinePart {
|
||||
// Ctrl + <p> + <w> => floating panes
|
||||
let mut bits = add_keybinds(help);
|
||||
bits.push(Style::new().paint(" => floating panes"));
|
||||
strings!(&bits)
|
||||
+1
-1
@@ -2,7 +2,7 @@ use std::collections::HashMap;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
use crate::tip::TipBody;
|
||||
use crate::status_bar::tip::TipBody;
|
||||
|
||||
mod compact_layout;
|
||||
mod edit_scrollbuffer;
|
||||
+1
-5
@@ -1,6 +1,6 @@
|
||||
use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style};
|
||||
|
||||
use crate::{action_key_group, style_key_with_modifier, LinePart};
|
||||
use crate::status_bar::{action_key_group, style_key_with_modifier, LinePart};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
|
||||
macro_rules! strings {
|
||||
@@ -17,7 +17,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn move_focus_hjkl_tab_switch_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: When changing focus with Alt + <←↓↑→> moving off screen left/right focuses the next tab.
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("When changing focus with "),
|
||||
@@ -28,7 +27,6 @@ pub fn move_focus_hjkl_tab_switch_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn move_focus_hjkl_tab_switch_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Changing focus with Alt + <←↓↑→> off screen focuses the next tab.
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("Changing focus with "),
|
||||
@@ -39,7 +37,6 @@ pub fn move_focus_hjkl_tab_switch_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn move_focus_hjkl_tab_switch_short(help: &ModeInfo) -> LinePart {
|
||||
// Alt + <←↓↑→> off screen edge focuses next tab.
|
||||
let mut bits = add_keybinds(help);
|
||||
bits.push(Style::new().paint(" off screen edge focuses next tab."));
|
||||
strings!(&bits)
|
||||
@@ -59,7 +56,6 @@ fn add_keybinds<'a>(help: &'a ModeInfo) -> Vec<ANSIString<'a>> {
|
||||
],
|
||||
);
|
||||
|
||||
// Let's see if we have some pretty groups in common here
|
||||
let mut arrows = vec![];
|
||||
let mut letters = vec![];
|
||||
for key in move_focus_keys.into_iter() {
|
||||
+1
-6
@@ -7,7 +7,7 @@ use ansi_term::{
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::status_bar::LinePart;
|
||||
|
||||
macro_rules! strings {
|
||||
($ANSIStrings:expr) => {{
|
||||
@@ -23,8 +23,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn move_tabs_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Wrong order of tabs? You can move them to left and right with:
|
||||
// Alt + i (left) and Alt + o (right)
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
let bits = vec![
|
||||
@@ -39,8 +37,6 @@ pub fn move_tabs_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn move_tabs_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: You can move tabs to left and right with:
|
||||
// Alt + i (left) and Alt + o (right)
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
let bits = vec![
|
||||
@@ -55,7 +51,6 @@ pub fn move_tabs_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn move_tabs_short(help: &ModeInfo) -> LinePart {
|
||||
// Move tabs with: Alt + i (left) and Alt + o (right)
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
let bits = vec![
|
||||
+1
-2
@@ -1,6 +1,6 @@
|
||||
use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style};
|
||||
|
||||
use crate::{action_key, action_key_group, style_key_with_modifier, LinePart};
|
||||
use crate::status_bar::{action_key, action_key_group, style_key_with_modifier, LinePart};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
|
||||
macro_rules! strings {
|
||||
@@ -123,7 +123,6 @@ fn add_keybinds<'a>(help: &'a ModeInfo) -> Keygroups<'a> {
|
||||
}],
|
||||
],
|
||||
);
|
||||
// Let's see if we have some pretty groups in common here
|
||||
let mut arrows = vec![];
|
||||
let mut letters = vec![];
|
||||
for key in move_focus_keys.into_iter() {
|
||||
+1
-4
@@ -4,7 +4,7 @@ use ansi_term::{
|
||||
Style,
|
||||
};
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::status_bar::LinePart;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
@@ -22,7 +22,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn mouse_click_to_terminal_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: SHIFT + <mouse-click> bypasses Zellij and sends the mouse click directly to the terminal
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
@@ -36,7 +35,6 @@ pub fn mouse_click_to_terminal_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn mouse_click_to_terminal_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: SHIFT + <mouse-click> sends the click directly to the terminal
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
strings!(&[
|
||||
@@ -49,7 +47,6 @@ pub fn mouse_click_to_terminal_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn mouse_click_to_terminal_short(help: &ModeInfo) -> LinePart {
|
||||
// Tip: SHIFT + <mouse-click> => sends click to terminal.
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
+1
-4
@@ -1,6 +1,6 @@
|
||||
use ansi_term::{unstyled_len, ANSIString, ANSIStrings, Style};
|
||||
|
||||
use crate::{action_key, style_key_with_modifier, LinePart};
|
||||
use crate::status_bar::{action_key, style_key_with_modifier, LinePart};
|
||||
use zellij_tile::prelude::{actions::Action, *};
|
||||
|
||||
macro_rules! strings {
|
||||
@@ -17,7 +17,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn sync_tab_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Sync a tab and write keyboard input to all panes with Ctrl + <t> + <s>
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("Sync a tab and write keyboard input to all its panes with "),
|
||||
@@ -27,7 +26,6 @@ pub fn sync_tab_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn sync_tab_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Sync input to panes in a tab with Ctrl + <t> + <s>
|
||||
let mut bits = vec![
|
||||
Style::new().paint(" Tip: "),
|
||||
Style::new().paint("Sync input to panes in a tab with "),
|
||||
@@ -37,7 +35,6 @@ pub fn sync_tab_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn sync_tab_short(help: &ModeInfo) -> LinePart {
|
||||
// Sync input in a tab with Ctrl + <t> + <s>
|
||||
let mut bits = vec![Style::new().paint(" Sync input in a tab with ")];
|
||||
bits.extend(add_keybinds(help));
|
||||
strings!(&bits)
|
||||
+1
-6
@@ -4,7 +4,7 @@ use ansi_term::{
|
||||
Style,
|
||||
};
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::status_bar::LinePart;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
@@ -22,8 +22,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn use_mouse_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Use the mouse to switch pane focus, scroll through the pane
|
||||
// scrollbuffer, switch or scroll through tabs
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
strings!(&[
|
||||
@@ -34,8 +32,6 @@ pub fn use_mouse_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn use_mouse_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Use the mouse to switch panes/tabs or scroll through the pane
|
||||
// scrollbuffer
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
strings!(&[
|
||||
@@ -46,7 +42,6 @@ pub fn use_mouse_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn use_mouse_short(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Use the mouse to switch panes/tabs or scroll
|
||||
let green_color = palette_match!(help.style.colors.text_unselected.emphasis_2);
|
||||
|
||||
strings!(&[
|
||||
+1
-4
@@ -4,7 +4,7 @@ use ansi_term::{
|
||||
Style,
|
||||
};
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::status_bar::LinePart;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::palette_match;
|
||||
|
||||
@@ -22,7 +22,6 @@ macro_rules! strings {
|
||||
}
|
||||
|
||||
pub fn zellij_setup_check_full(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Having issues with Zellij? Try running "zellij setup --check"
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
strings!(&[
|
||||
@@ -36,7 +35,6 @@ pub fn zellij_setup_check_full(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn zellij_setup_check_medium(help: &ModeInfo) -> LinePart {
|
||||
// Tip: Run "zellij setup --check" to find issues
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
strings!(&[
|
||||
@@ -51,7 +49,6 @@ pub fn zellij_setup_check_medium(help: &ModeInfo) -> LinePart {
|
||||
}
|
||||
|
||||
pub fn zellij_setup_check_short(help: &ModeInfo) -> LinePart {
|
||||
// Run "zellij setup --check" to find issues
|
||||
let orange_color = palette_match!(help.style.colors.text_unselected.emphasis_0);
|
||||
|
||||
strings!(&[
|
||||
+1
-1
@@ -3,7 +3,7 @@ pub mod consts;
|
||||
pub mod data;
|
||||
pub mod utils;
|
||||
|
||||
use crate::LinePart;
|
||||
use crate::status_bar::LinePart;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
pub type TipFn = fn(&ModeInfo) -> LinePart;
|
||||
-1
@@ -35,7 +35,6 @@ pub fn get_random_tip_name() -> String {
|
||||
|
||||
pub fn get_cached_tip_name() -> String {
|
||||
let mut local_cache = match LocalCache::new(PathBuf::from(DEFAULT_CACHE_FILE_PATH)) {
|
||||
// TODO: it might be a good to leave an log with warn later, if possible.
|
||||
Err(_) => return String::from("quicknav"),
|
||||
Ok(cache) => cache,
|
||||
};
|
||||
@@ -2,7 +2,7 @@ use ansi_term::ANSIStrings;
|
||||
use std::collections::BTreeMap;
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
||||
use crate::{LinePart, ARROW_SEPARATOR};
|
||||
use crate::tab_bar::{LinePart, ARROW_SEPARATOR};
|
||||
use zellij_tile::prelude::actions::Action;
|
||||
use zellij_tile::prelude::*;
|
||||
use zellij_tile_utils::style;
|
||||
@@ -20,8 +20,6 @@ fn dim_style(bg: PaletteColor) -> ansi_term::Style {
|
||||
.dimmed()
|
||||
}
|
||||
|
||||
// move elements from before_active and after_active into tabs_to_render while they fit in cols
|
||||
// adds collapsed_tabs to the left and right if there's left over tabs that don't fit
|
||||
fn populate_tabs_in_tab_line(
|
||||
tabs_before_active: &mut Vec<LinePart>,
|
||||
tabs_after_active: &mut Vec<LinePart>,
|
||||
@@ -39,7 +37,6 @@ fn populate_tabs_in_tab_line(
|
||||
let left_count = tabs_before_active.len();
|
||||
let right_count = tabs_after_active.len();
|
||||
|
||||
// left_more_tab_index is first tab to the left of the leftmost visible tab
|
||||
let left_more_tab_index = left_count.saturating_sub(1);
|
||||
let collapsed_left = left_more_message(
|
||||
left_count,
|
||||
@@ -49,7 +46,6 @@ fn populate_tabs_in_tab_line(
|
||||
dimmed,
|
||||
);
|
||||
|
||||
// right_more_tab_index is the first tab to the right of the rightmost visible tab
|
||||
let right_more_tab_index = left_count + tabs_to_render.len();
|
||||
let collapsed_right = right_more_message(
|
||||
right_count,
|
||||
@@ -62,7 +58,6 @@ fn populate_tabs_in_tab_line(
|
||||
let total_size = collapsed_left.len + middle_size + collapsed_right.len;
|
||||
|
||||
if total_size > cols {
|
||||
// break and dont add collapsed tabs to tabs_to_render, they will not fit
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -78,7 +73,6 @@ fn populate_tabs_in_tab_line(
|
||||
usize::MAX
|
||||
};
|
||||
|
||||
// total size is shortened if the next tab to be added is the last one, as that will remove the collapsed tab
|
||||
let size_by_adding_left =
|
||||
left.saturating_add(total_size)
|
||||
.saturating_sub(if left_count == 1 {
|
||||
@@ -97,22 +91,17 @@ fn populate_tabs_in_tab_line(
|
||||
|
||||
let left_fits = size_by_adding_left <= cols;
|
||||
let right_fits = size_by_adding_right <= cols;
|
||||
// active tab is kept in the middle by adding to the side that
|
||||
// has less width, or if the tab on the other side doesn't fit
|
||||
if (total_left <= total_right || !right_fits) && left_fits {
|
||||
// add left tab
|
||||
let tab = tabs_before_active.pop().unwrap();
|
||||
middle_size += tab.len;
|
||||
total_left += tab.len;
|
||||
tabs_to_render.insert(0, tab);
|
||||
} else if right_fits {
|
||||
// add right tab
|
||||
let tab = tabs_after_active.remove(0);
|
||||
middle_size += tab.len;
|
||||
total_right += tab.len;
|
||||
tabs_to_render.push(tab);
|
||||
} else {
|
||||
// there's either no space to add more tabs or no more tabs to add, so we're done
|
||||
tabs_to_render.insert(0, collapsed_left);
|
||||
tabs_to_render.push(collapsed_right);
|
||||
break;
|
||||
@@ -135,8 +124,6 @@ fn left_more_message(
|
||||
} else {
|
||||
" ← +many ".to_string()
|
||||
};
|
||||
// 238
|
||||
// chars length plus separator length on both sides
|
||||
let more_text_len = more_text.width() + 2 * separator.width();
|
||||
let (text_color, sep_color) = (
|
||||
palette.ribbon_unselected.base,
|
||||
@@ -174,7 +161,6 @@ fn right_more_message(
|
||||
} else {
|
||||
" +many → ".to_string()
|
||||
};
|
||||
// chars length plus separator length on both sides
|
||||
let more_text_len = more_text.width() + 2 * separator.width();
|
||||
let (text_color, sep_color) = (
|
||||
palette.ribbon_unselected.base,
|
||||
@@ -343,7 +329,6 @@ pub fn tab_line(
|
||||
active_pane_scroll.map(|scroll| scroll_status(scroll, palette, dimmed));
|
||||
}
|
||||
|
||||
// Drop indicator if it would cause wrapping (active tab always rendered unconditionally)
|
||||
let prefix_len = get_current_title_len(&prefix);
|
||||
let hint_budget = cols.saturating_sub(prefix_len + active_tab.len);
|
||||
if let Some(hint_part) = hint.and_then(|hint| hint_line_part(hint, hint_budget, dimmed)) {
|
||||
@@ -574,7 +559,6 @@ pub fn style_key_with_modifier(
|
||||
.collect::<Vec<_>>()
|
||||
.join("-");
|
||||
|
||||
// Prints the keys
|
||||
let key = keyvec
|
||||
.iter()
|
||||
.map(|key| {
|
||||
@@ -586,7 +570,6 @@ pub fn style_key_with_modifier(
|
||||
})
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
// Special handling of some pre-defined keygroups
|
||||
let key_string = key.join("");
|
||||
let key_separator = match &key_string[..] {
|
||||
"HJKL" => "",
|
||||
@@ -0,0 +1,427 @@
|
||||
mod line;
|
||||
mod tab;
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::{BTreeMap, VecDeque};
|
||||
use std::convert::TryInto;
|
||||
|
||||
use tab::get_tab_to_focus;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::keybinds::KeybindStore;
|
||||
use crate::ClientSeed;
|
||||
use line::tab_line;
|
||||
use tab::tab_style;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinePart {
|
||||
part: String,
|
||||
len: usize,
|
||||
tab_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl LinePart {
|
||||
pub fn append(&mut self, to_append: &LinePart) {
|
||||
self.part.push_str(&to_append.part);
|
||||
self.len += to_append.len;
|
||||
}
|
||||
}
|
||||
|
||||
static ARROW_SEPARATOR: &str = "";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SlotConfig {
|
||||
hide_swap_layout_indication: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct ClientState {
|
||||
tabs: Vec<TabInfo>,
|
||||
active_tab_idx: usize,
|
||||
mode_info: ModeInfo,
|
||||
active_pane_scroll: Option<(usize, usize)>,
|
||||
hovered_tab_idx: Option<usize>,
|
||||
hovered_new_tab_button: bool,
|
||||
hint_text: Option<BTreeMap<usize, StyledText>>,
|
||||
outstanding_hint_timeouts: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct SlotClientState {
|
||||
tab_line: Vec<LinePart>,
|
||||
new_tab_button_range: Option<(usize, usize)>,
|
||||
breadcrumb_range: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct TabBar {
|
||||
slots: BTreeMap<SlotId, SlotConfig>,
|
||||
clients: BTreeMap<ClientId, ClientState>,
|
||||
slot_clients: BTreeMap<(SlotId, ClientId), SlotClientState>,
|
||||
hint_timeout_queue: VecDeque<ClientId>,
|
||||
}
|
||||
|
||||
impl TabBar {
|
||||
pub fn has_slots(&self) -> bool {
|
||||
!self.slots.is_empty()
|
||||
}
|
||||
|
||||
pub fn slot_added(&mut self, slot: &Slot) {
|
||||
let hide_swap_layout_indication = slot
|
||||
.configuration
|
||||
.get("hide_swap_layout_indication")
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(false);
|
||||
self.slots.insert(
|
||||
slot.id,
|
||||
SlotConfig {
|
||||
hide_swap_layout_indication,
|
||||
},
|
||||
);
|
||||
subscribe(&[
|
||||
EventType::TabUpdate,
|
||||
EventType::ModeUpdate,
|
||||
EventType::Mouse,
|
||||
EventType::InitialKeybinds,
|
||||
EventType::ActivePaneScroll,
|
||||
EventType::HintText,
|
||||
EventType::Timer,
|
||||
EventType::InputReceived,
|
||||
]);
|
||||
}
|
||||
|
||||
pub fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
self.slots.remove(&slot_id);
|
||||
self.slot_clients.retain(|(s, _), _| *s != slot_id);
|
||||
}
|
||||
|
||||
pub fn client_connected(&mut self, client_id: ClientId) {
|
||||
self.clients.entry(client_id).or_default();
|
||||
}
|
||||
|
||||
pub fn client_disconnected(&mut self, client_id: ClientId) {
|
||||
self.clients.remove(&client_id);
|
||||
self.slot_clients.retain(|(_, c), _| *c != client_id);
|
||||
}
|
||||
|
||||
pub fn client_ids(&self) -> impl Iterator<Item = ClientId> + '_ {
|
||||
self.clients.keys().copied()
|
||||
}
|
||||
|
||||
pub fn reset(&mut self) {
|
||||
self.clients.clear();
|
||||
self.slot_clients.clear();
|
||||
}
|
||||
|
||||
pub fn snapshot(&self) -> BTreeMap<ClientId, ClientSeed> {
|
||||
self.clients
|
||||
.iter()
|
||||
.map(|(client_id, client)| {
|
||||
(
|
||||
*client_id,
|
||||
ClientSeed {
|
||||
mode_info: client.mode_info.clone(),
|
||||
tabs: client.tabs.clone(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn seed(&mut self, seeds: &BTreeMap<ClientId, ClientSeed>) {
|
||||
for (client_id, seed) in seeds {
|
||||
let client = self.clients.entry(*client_id).or_default();
|
||||
client.mode_info = seed.mode_info.clone();
|
||||
if let Some(active_tab_index) = seed.tabs.iter().position(|t| t.active) {
|
||||
client.active_tab_idx = active_tab_index + 1;
|
||||
client.tabs = seed.tabs.clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn target_clients(&self, context: &EventContext) -> Vec<ClientId> {
|
||||
match context.client_id {
|
||||
Some(client_id) => vec![client_id],
|
||||
None => self.clients.keys().copied().collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_request(&self, context: &EventContext, should_render: bool) -> RenderResponse {
|
||||
if !should_render {
|
||||
RenderResponse::Nothing
|
||||
} else if let Some(client_id) = context.client_id {
|
||||
RenderResponse::Client(client_id)
|
||||
} else {
|
||||
RenderResponse::Slots(self.slots.keys().copied().collect())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update(&mut self, event: &Event, context: EventContext) -> RenderResponse {
|
||||
if let Event::Timer(_) = event {
|
||||
let Some(client_id) = self.hint_timeout_queue.pop_front() else {
|
||||
return RenderResponse::Nothing;
|
||||
};
|
||||
let Some(client) = self.clients.get_mut(&client_id) else {
|
||||
return RenderResponse::Nothing;
|
||||
};
|
||||
let mut should_render = false;
|
||||
client.outstanding_hint_timeouts = client.outstanding_hint_timeouts.saturating_sub(1);
|
||||
if client.outstanding_hint_timeouts == 0 && client.hint_text.is_some() {
|
||||
client.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
if client.tabs.is_empty() {
|
||||
should_render = false;
|
||||
}
|
||||
return if should_render {
|
||||
RenderResponse::Client(client_id)
|
||||
} else {
|
||||
RenderResponse::Nothing
|
||||
};
|
||||
}
|
||||
let mut should_render = false;
|
||||
for client_id in self.target_clients(&context) {
|
||||
if self.update_client(event, client_id, context.slot_id) {
|
||||
should_render = true;
|
||||
}
|
||||
}
|
||||
self.render_request(&context, should_render)
|
||||
}
|
||||
|
||||
fn update_client(
|
||||
&mut self,
|
||||
event: &Event,
|
||||
client_id: ClientId,
|
||||
slot_id: Option<SlotId>,
|
||||
) -> bool {
|
||||
let client = self.clients.entry(client_id).or_default();
|
||||
let empty_slot_client = SlotClientState::default();
|
||||
let slot_client = slot_id
|
||||
.and_then(|slot_id| self.slot_clients.get(&(slot_id, client_id)))
|
||||
.unwrap_or(&empty_slot_client);
|
||||
let mut should_render = false;
|
||||
match event {
|
||||
Event::InitialKeybinds(_) => {
|
||||
should_render = true;
|
||||
},
|
||||
Event::ModeUpdate(mode_info) => {
|
||||
if &client.mode_info != mode_info {
|
||||
should_render = true;
|
||||
}
|
||||
client.mode_info = mode_info.clone();
|
||||
},
|
||||
Event::TabUpdate(tabs) => {
|
||||
if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
|
||||
let active_tab_idx = active_tab_index + 1;
|
||||
|
||||
if client.active_tab_idx != active_tab_idx || &client.tabs != tabs {
|
||||
should_render = true;
|
||||
}
|
||||
client.active_tab_idx = active_tab_idx;
|
||||
client.tabs = tabs.clone();
|
||||
} else {
|
||||
eprintln!("Could not find active tab.");
|
||||
}
|
||||
},
|
||||
Event::ActivePaneScroll(scroll) => {
|
||||
if client.active_pane_scroll != *scroll {
|
||||
should_render = true;
|
||||
}
|
||||
client.active_pane_scroll = *scroll;
|
||||
},
|
||||
Event::HintText(hint_variants) => {
|
||||
if hint_variants.is_empty() {
|
||||
if client.hint_text.is_some() {
|
||||
client.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
} else {
|
||||
client.hint_text = Some(hint_variants.clone());
|
||||
client.outstanding_hint_timeouts += 1;
|
||||
self.hint_timeout_queue.push_back(client_id);
|
||||
set_timeout(5.0);
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Event::InputReceived => {
|
||||
if client.hint_text.is_some() {
|
||||
client.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Event::Mouse(me) => match me {
|
||||
Mouse::LeftClick(_, col) => {
|
||||
let col = *col;
|
||||
if let Some((start, end)) = slot_client.breadcrumb_range {
|
||||
if col >= start && col < end {
|
||||
focus_host_session();
|
||||
return should_render;
|
||||
}
|
||||
}
|
||||
if let Some((start, end)) = slot_client.new_tab_button_range {
|
||||
if col >= start && col < end {
|
||||
new_tab::<&str>(None, None);
|
||||
return should_render;
|
||||
}
|
||||
}
|
||||
let tab_to_focus =
|
||||
get_tab_to_focus(&slot_client.tab_line, client.active_tab_idx, col);
|
||||
if let Some(idx) = tab_to_focus {
|
||||
switch_tab_to(idx.try_into().unwrap());
|
||||
}
|
||||
},
|
||||
Mouse::Hover(_, col) => {
|
||||
let col = *col;
|
||||
let simplified_ui = client.mode_info.capabilities.arrow_fonts;
|
||||
let mut new_hovered_new_tab_button = false;
|
||||
let mut new_hovered_tab_idx = None;
|
||||
if !simplified_ui {
|
||||
if let Some((start, end)) = slot_client.new_tab_button_range {
|
||||
if col >= start && col < end {
|
||||
new_hovered_new_tab_button = true;
|
||||
}
|
||||
}
|
||||
if !new_hovered_new_tab_button {
|
||||
new_hovered_tab_idx =
|
||||
get_tab_to_focus(&slot_client.tab_line, client.active_tab_idx, col);
|
||||
}
|
||||
}
|
||||
if client.hovered_new_tab_button != new_hovered_new_tab_button
|
||||
|| client.hovered_tab_idx != new_hovered_tab_idx
|
||||
{
|
||||
client.hovered_new_tab_button = new_hovered_new_tab_button;
|
||||
client.hovered_tab_idx = new_hovered_tab_idx;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Mouse::ScrollUp(_) => {
|
||||
switch_tab_to(min(client.active_tab_idx + 1, client.tabs.len()) as u32);
|
||||
},
|
||||
Mouse::ScrollDown(_) => {
|
||||
switch_tab_to(max(client.active_tab_idx.saturating_sub(1), 1) as u32);
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
if client.tabs.is_empty() {
|
||||
should_render = false;
|
||||
}
|
||||
should_render
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
&mut self,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
keybinds: &mut KeybindStore,
|
||||
) {
|
||||
let lent = self.swap_keybinds(client_id, keybinds);
|
||||
self.render_client(rows, cols, slot_id, client_id);
|
||||
if lent {
|
||||
self.swap_keybinds(client_id, keybinds);
|
||||
}
|
||||
}
|
||||
|
||||
fn swap_keybinds(&mut self, client_id: ClientId, keybinds: &mut KeybindStore) -> bool {
|
||||
match self.clients.get_mut(&client_id) {
|
||||
Some(client) => keybinds.swap(client_id, &mut client.mode_info.keybinds),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn render_client(&mut self, _rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
|
||||
let Some(client) = self.clients.get(&client_id) else {
|
||||
return;
|
||||
};
|
||||
if client.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let hide_swap_layout_indication = self
|
||||
.slots
|
||||
.get(&slot_id)
|
||||
.map(|s| s.hide_swap_layout_indication)
|
||||
.unwrap_or(false);
|
||||
let dimmed = client.mode_info.session_ascended == Some(true)
|
||||
|| client.mode_info.session_dimmed == Some(true);
|
||||
let mut all_tabs: Vec<LinePart> = vec![];
|
||||
let mut active_tab_index = 0;
|
||||
let mut is_alternate_tab = false;
|
||||
for t in &client.tabs {
|
||||
let mut tabname = t.name.clone();
|
||||
if t.active && client.mode_info.mode == InputMode::RenameTab {
|
||||
if tabname.is_empty() {
|
||||
tabname = String::from("Enter name...");
|
||||
}
|
||||
active_tab_index = t.position;
|
||||
} else if t.active {
|
||||
active_tab_index = t.position;
|
||||
}
|
||||
let is_hovered = client.hovered_tab_idx == Some(t.position + 1);
|
||||
let tab = tab_style(
|
||||
tabname,
|
||||
t,
|
||||
is_alternate_tab,
|
||||
is_hovered,
|
||||
client.mode_info.style.colors,
|
||||
client.mode_info.capabilities,
|
||||
dimmed,
|
||||
);
|
||||
is_alternate_tab = !is_alternate_tab;
|
||||
all_tabs.push(tab);
|
||||
}
|
||||
|
||||
let background = client.mode_info.style.colors.text_unselected.background;
|
||||
|
||||
let full_pane_frames = client.mode_info.pane_frame_style == Some(PaneFrameStyle::Full);
|
||||
let hint_text = if full_pane_frames {
|
||||
None
|
||||
} else {
|
||||
client.hint_text.as_ref()
|
||||
};
|
||||
let breadcrumb_ancestry: Vec<String> = if client.mode_info.host_fullscreen == Some(true) {
|
||||
client.mode_info.session_ancestry.clone()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let (line, new_tab_button_range, breadcrumb_range) = tab_line(
|
||||
client.mode_info.session_name.as_deref(),
|
||||
all_tabs,
|
||||
active_tab_index,
|
||||
cols.saturating_sub(1),
|
||||
client.mode_info.style.colors,
|
||||
client.mode_info.capabilities,
|
||||
client.mode_info.style.hide_session_name,
|
||||
client.tabs.iter().find(|t| t.active),
|
||||
&client.mode_info,
|
||||
hide_swap_layout_indication,
|
||||
&background,
|
||||
client.active_pane_scroll,
|
||||
hint_text,
|
||||
is_alternate_tab,
|
||||
client.hovered_new_tab_button,
|
||||
dimmed,
|
||||
&breadcrumb_ancestry,
|
||||
);
|
||||
let slot_client = self.slot_clients.entry((slot_id, client_id)).or_default();
|
||||
slot_client.tab_line = line;
|
||||
slot_client.new_tab_button_range = new_tab_button_range;
|
||||
slot_client.breadcrumb_range = breadcrumb_range;
|
||||
|
||||
let output = slot_client
|
||||
.tab_line
|
||||
.iter()
|
||||
.fold(String::new(), |output, part| output + &part.part);
|
||||
|
||||
match background {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b);
|
||||
},
|
||||
PaletteColor::EightBit(color) => {
|
||||
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{line::tab_separator, LinePart};
|
||||
use crate::tab_bar::{line::tab_separator, LinePart};
|
||||
use ansi_term::{ANSIString, ANSIStrings};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
use zellij_tile::prelude::*;
|
||||
@@ -8,7 +8,6 @@ fn cursors<'a>(
|
||||
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 display_slot in focused_client_slots.iter() {
|
||||
@@ -71,14 +70,14 @@ pub fn render_tab(
|
||||
};
|
||||
let left_separator = style!(separator_fill_color, background_color).paint(separator);
|
||||
let right_separator_style = style!(background_color, separator_fill_color);
|
||||
let mut tab_text_len = text.width() + (separator_width * 2) + 2; // +2 for padding
|
||||
let mut tab_text_len = text.width() + (separator_width * 2) + 2;
|
||||
let tab_styled_text = text_style.paint(format!(" {} ", text));
|
||||
|
||||
let right_separator = right_separator_style.paint(separator);
|
||||
let tab_styled_text = if !focused_clients.is_empty() {
|
||||
let (cursor_section, extra_length) =
|
||||
cursors(focused_clients, palette.multiplayer_user_colors);
|
||||
tab_text_len += extra_length + 2; // 2 for cursor_beginning and cursor_end
|
||||
tab_text_len += extra_length + 2;
|
||||
let mut s = String::new();
|
||||
let cursor_beginning = text_style.paint("[").to_string();
|
||||
let cursor_section = ANSIStrings(&cursor_section).to_string();
|
||||
@@ -120,7 +119,6 @@ pub fn tab_style(
|
||||
if tab.has_bell_notification || tab.is_flashing_bell {
|
||||
tabname.push_str(" [!]");
|
||||
}
|
||||
// we only color alternate tabs differently if we can't use the arrow fonts to separate them
|
||||
if !capabilities.arrow_fonts {
|
||||
is_alternate_tab = false;
|
||||
}
|
||||
@@ -143,7 +141,6 @@ pub(crate) fn get_tab_to_focus(
|
||||
) -> Option<usize> {
|
||||
let clicked_line_part = get_clicked_line_part(tab_line, mouse_click_col)?;
|
||||
let clicked_tab_idx = clicked_line_part.tab_index?;
|
||||
// tabs are indexed starting from 1 so we need to add 1
|
||||
let clicked_tab_idx = clicked_tab_idx + 1;
|
||||
if clicked_tab_idx != active_tab_idx {
|
||||
return Some(clicked_tab_idx);
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "compact-bar"
|
||||
version = "0.1.0"
|
||||
authors = ["Alexander Kenji Berthold <[email protected]>" ]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ansi_term = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
zellij-tile = { path = "../../zellij-tile" }
|
||||
zellij-tile-utils = { path = "../../zellij-tile-utils" }
|
||||
@@ -1,613 +0,0 @@
|
||||
mod action_types;
|
||||
mod clipboard_utils;
|
||||
mod keybind_utils;
|
||||
mod line;
|
||||
mod tab;
|
||||
mod tooltip;
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use tab::get_tab_to_focus;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::clipboard_utils::{system_clipboard_error, text_copied_hint};
|
||||
use crate::line::tab_line;
|
||||
use crate::tab::tab_style;
|
||||
use crate::tooltip::TooltipRenderer;
|
||||
|
||||
static ARROW_SEPARATOR: &str = "";
|
||||
|
||||
const CONFIG_IS_TOOLTIP: &str = "is_tooltip";
|
||||
const CONFIG_TOGGLE_TOOLTIP_KEY: &str = "tooltip";
|
||||
const MSG_TOGGLE_TOOLTIP: &str = "toggle_tooltip";
|
||||
const MSG_TOGGLE_PERSISTED_TOOLTIP: &str = "toggle_persisted_tooltip";
|
||||
const MSG_LAUNCH_TOOLTIP: &str = "launch_tooltip_if_not_launched";
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinePart {
|
||||
part: String,
|
||||
len: usize,
|
||||
tab_index: Option<usize>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
// Tab state
|
||||
tabs: Vec<TabInfo>,
|
||||
active_tab_idx: usize,
|
||||
|
||||
// Display state
|
||||
mode_info: ModeInfo,
|
||||
tab_line: Vec<LinePart>,
|
||||
breadcrumb_range: Option<(usize, usize)>,
|
||||
display_area_rows: usize,
|
||||
display_area_cols: usize,
|
||||
|
||||
// Clipboard state
|
||||
text_copy_destination: Option<CopyDestination>,
|
||||
display_system_clipboard_failure: bool,
|
||||
|
||||
// Plugin configuration
|
||||
config: BTreeMap<String, String>,
|
||||
own_plugin_id: Option<u32>,
|
||||
toggle_tooltip_key: Option<String>,
|
||||
|
||||
// Tooltip state
|
||||
is_tooltip: bool,
|
||||
tooltip_is_active: bool,
|
||||
persist: bool,
|
||||
is_first_run: bool,
|
||||
own_tab_index: Option<usize>,
|
||||
own_client_id: ClientId,
|
||||
|
||||
// Keybinding cache
|
||||
cached_keybinds: KeybindsVec,
|
||||
}
|
||||
|
||||
struct TabRenderData {
|
||||
tabs: Vec<LinePart>,
|
||||
active_tab_index: usize,
|
||||
active_swap_layout_name: Option<String>,
|
||||
is_swap_layout_dirty: bool,
|
||||
}
|
||||
|
||||
register_plugin!(State);
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
let plugin_ids = get_plugin_ids();
|
||||
self.own_plugin_id = Some(plugin_ids.plugin_id);
|
||||
self.own_client_id = plugin_ids.client_id;
|
||||
self.initialize_configuration(configuration);
|
||||
self.setup_subscriptions();
|
||||
self.configure_keybinds();
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
self.is_first_run = false;
|
||||
|
||||
match event {
|
||||
Event::InitialKeybinds(keybinds) => {
|
||||
self.cached_keybinds = keybinds;
|
||||
if !self.cached_keybinds.is_empty() {
|
||||
self.mode_info.keybinds = self.cached_keybinds.clone();
|
||||
}
|
||||
true
|
||||
},
|
||||
Event::ModeUpdate(mut mode_info) => {
|
||||
if mode_info.keybinds.is_empty() && !self.cached_keybinds.is_empty() {
|
||||
mode_info.keybinds = self.cached_keybinds.clone();
|
||||
} else if !mode_info.keybinds.is_empty() {
|
||||
self.cached_keybinds = mode_info.keybinds.clone();
|
||||
}
|
||||
self.handle_mode_update(mode_info)
|
||||
},
|
||||
Event::TabUpdate(tabs) => self.handle_tab_update(tabs),
|
||||
Event::PaneUpdate(pane_manifest) => self.handle_pane_update(pane_manifest),
|
||||
Event::Mouse(mouse_event) => {
|
||||
self.handle_mouse_event(mouse_event);
|
||||
false
|
||||
},
|
||||
Event::CopyToClipboard(copy_destination) => {
|
||||
self.handle_clipboard_copy(copy_destination)
|
||||
},
|
||||
Event::SystemClipboardFailure => self.handle_clipboard_failure(),
|
||||
Event::InputReceived => self.handle_input_received(),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe(&mut self, message: PipeMessage) -> bool {
|
||||
if self.is_tooltip && message.is_private {
|
||||
self.handle_tooltip_pipe(message);
|
||||
} else if message.name == MSG_TOGGLE_TOOLTIP
|
||||
&& message.is_private
|
||||
&& self.toggle_tooltip_key.is_some()
|
||||
// only launch once per plugin instance
|
||||
&& self.own_tab_index == Some(self.active_tab_idx.saturating_sub(1))
|
||||
// only launch once per client of plugin instance
|
||||
&& Some(format!("{}", self.own_client_id)) == message.payload
|
||||
{
|
||||
self.toggle_persisted_tooltip(self.mode_info.mode);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn render(&mut self, rows: usize, cols: usize) {
|
||||
if self.is_tooltip {
|
||||
self.render_tooltip(rows, cols);
|
||||
} else {
|
||||
self.render_tab_line(cols);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl State {
|
||||
fn initialize_configuration(&mut self, configuration: BTreeMap<String, String>) {
|
||||
self.config = configuration.clone();
|
||||
self.is_tooltip = self.parse_bool_config(CONFIG_IS_TOOLTIP, false);
|
||||
|
||||
if !self.is_tooltip {
|
||||
if let Some(tooltip_toggle_key) = configuration.get(CONFIG_TOGGLE_TOOLTIP_KEY) {
|
||||
self.toggle_tooltip_key = Some(tooltip_toggle_key.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if self.is_tooltip {
|
||||
self.is_first_run = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn setup_subscriptions(&self) {
|
||||
set_selectable(false);
|
||||
|
||||
let events = if self.is_tooltip {
|
||||
vec![
|
||||
EventType::ModeUpdate,
|
||||
EventType::TabUpdate,
|
||||
EventType::InitialKeybinds,
|
||||
]
|
||||
} else {
|
||||
vec![
|
||||
EventType::TabUpdate,
|
||||
EventType::PaneUpdate,
|
||||
EventType::ModeUpdate,
|
||||
EventType::Mouse,
|
||||
EventType::CopyToClipboard,
|
||||
EventType::InputReceived,
|
||||
EventType::SystemClipboardFailure,
|
||||
EventType::InitialKeybinds,
|
||||
]
|
||||
};
|
||||
|
||||
subscribe(&events);
|
||||
}
|
||||
|
||||
fn configure_keybinds(&self) {
|
||||
if !self.is_tooltip && self.toggle_tooltip_key.is_some() {
|
||||
if let Some(toggle_key) = &self.toggle_tooltip_key {
|
||||
reconfigure(
|
||||
bind_toggle_key_config(toggle_key, self.own_client_id),
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_bool_config(&self, key: &str, default: bool) -> bool {
|
||||
self.config
|
||||
.get(key)
|
||||
.and_then(|v| v.parse().ok())
|
||||
.unwrap_or(default)
|
||||
}
|
||||
|
||||
// Event handlers
|
||||
fn handle_mode_update(&mut self, mode_info: ModeInfo) -> bool {
|
||||
let should_render = self.mode_info != mode_info;
|
||||
let old_mode = self.mode_info.mode;
|
||||
let new_mode = mode_info.mode;
|
||||
let base_mode = mode_info.base_mode.unwrap_or(InputMode::Normal);
|
||||
|
||||
self.mode_info = mode_info;
|
||||
|
||||
if self.is_tooltip {
|
||||
self.handle_tooltip_mode_update(old_mode, new_mode, base_mode);
|
||||
} else {
|
||||
self.handle_main_mode_update(new_mode, base_mode);
|
||||
}
|
||||
|
||||
should_render
|
||||
}
|
||||
|
||||
fn handle_main_mode_update(&self, new_mode: InputMode, base_mode: InputMode) {
|
||||
if self.toggle_tooltip_key.is_some()
|
||||
&& new_mode != base_mode
|
||||
&& !self.is_restricted_mode(new_mode)
|
||||
{
|
||||
self.launch_tooltip_if_not_launched(new_mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tooltip_mode_update(
|
||||
&mut self,
|
||||
old_mode: InputMode,
|
||||
new_mode: InputMode,
|
||||
base_mode: InputMode,
|
||||
) {
|
||||
if !self.persist && (new_mode == base_mode || self.is_restricted_mode(new_mode)) {
|
||||
close_self();
|
||||
} else if new_mode != old_mode || self.persist {
|
||||
self.update_tooltip_for_mode_change(new_mode);
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_tab_update(&mut self, tabs: Vec<TabInfo>) -> bool {
|
||||
self.update_display_area(&tabs);
|
||||
|
||||
if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
|
||||
let active_tab_idx = active_tab_index + 1; // Convert to 1-based indexing
|
||||
let should_render = self.active_tab_idx != active_tab_idx || self.tabs != tabs;
|
||||
|
||||
if self.is_tooltip && self.active_tab_idx != active_tab_idx {
|
||||
self.move_tooltip_to_new_tab(active_tab_idx);
|
||||
}
|
||||
|
||||
self.active_tab_idx = active_tab_idx;
|
||||
self.tabs = tabs;
|
||||
should_render
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_pane_update(&mut self, pane_manifest: PaneManifest) -> bool {
|
||||
if self.toggle_tooltip_key.is_some() {
|
||||
let previous_tooltip_state = self.tooltip_is_active;
|
||||
self.tooltip_is_active = self.detect_tooltip_presence(&pane_manifest);
|
||||
self.own_tab_index = self.find_own_tab_index(&pane_manifest);
|
||||
previous_tooltip_state != self.tooltip_is_active
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(&mut self, mouse_event: Mouse) {
|
||||
if self.is_tooltip {
|
||||
return;
|
||||
}
|
||||
|
||||
match mouse_event {
|
||||
Mouse::LeftClick(_, col) => {
|
||||
if self.click_is_on_breadcrumb(col) {
|
||||
focus_host_session();
|
||||
} else {
|
||||
self.handle_tab_click(col);
|
||||
}
|
||||
},
|
||||
Mouse::ScrollUp(_) => self.scroll_tab_up(),
|
||||
Mouse::ScrollDown(_) => self.scroll_tab_down(),
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
|
||||
fn click_is_on_breadcrumb(&self, col: usize) -> bool {
|
||||
matches!(self.breadcrumb_range, Some((start, end)) if col >= start && col < end)
|
||||
}
|
||||
|
||||
fn handle_clipboard_copy(&mut self, copy_destination: CopyDestination) -> bool {
|
||||
if self.is_tooltip {
|
||||
return false;
|
||||
}
|
||||
|
||||
let should_render = match self.text_copy_destination {
|
||||
Some(current) => current != copy_destination,
|
||||
None => true,
|
||||
};
|
||||
|
||||
self.text_copy_destination = Some(copy_destination);
|
||||
should_render
|
||||
}
|
||||
|
||||
fn handle_clipboard_failure(&mut self) -> bool {
|
||||
if self.is_tooltip {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.display_system_clipboard_failure = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn handle_input_received(&mut self) -> bool {
|
||||
if self.is_tooltip {
|
||||
return false;
|
||||
}
|
||||
|
||||
let should_render =
|
||||
self.text_copy_destination.is_some() || self.display_system_clipboard_failure;
|
||||
self.clear_clipboard_state();
|
||||
should_render
|
||||
}
|
||||
|
||||
fn handle_tooltip_pipe(&mut self, message: PipeMessage) {
|
||||
if message.name == MSG_TOGGLE_PERSISTED_TOOLTIP {
|
||||
if self.is_first_run {
|
||||
self.persist = true;
|
||||
} else {
|
||||
#[cfg(target_family = "wasm")]
|
||||
close_self();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper methods
|
||||
fn update_display_area(&mut self, tabs: &[TabInfo]) {
|
||||
for tab in tabs {
|
||||
if tab.active {
|
||||
self.display_area_rows = tab.display_area_rows;
|
||||
self.display_area_cols = tab.display_area_columns;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn detect_tooltip_presence(&self, pane_manifest: &PaneManifest) -> bool {
|
||||
for (_tab_index, panes) in &pane_manifest.panes {
|
||||
for pane in panes {
|
||||
if pane.plugin_url == Some("zellij:compact-bar".to_owned())
|
||||
&& pane.pane_x != pane.pane_content_x
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn find_own_tab_index(&self, pane_manifest: &PaneManifest) -> Option<usize> {
|
||||
for (tab_index, panes) in &pane_manifest.panes {
|
||||
for pane in panes {
|
||||
if pane.is_plugin && Some(pane.id) == self.own_plugin_id {
|
||||
return Some(*tab_index);
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn handle_tab_click(&self, col: usize) {
|
||||
if let Some(tab_idx) = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col) {
|
||||
switch_tab_to(tab_idx.try_into().unwrap());
|
||||
}
|
||||
}
|
||||
|
||||
fn scroll_tab_up(&self) {
|
||||
let next_tab = min(self.active_tab_idx + 1, self.tabs.len());
|
||||
switch_tab_to(next_tab as u32);
|
||||
}
|
||||
|
||||
fn scroll_tab_down(&self) {
|
||||
let prev_tab = max(self.active_tab_idx.saturating_sub(1), 1);
|
||||
switch_tab_to(prev_tab as u32);
|
||||
}
|
||||
|
||||
fn clear_clipboard_state(&mut self) {
|
||||
self.text_copy_destination = None;
|
||||
self.display_system_clipboard_failure = false;
|
||||
}
|
||||
|
||||
fn is_restricted_mode(&self, mode: InputMode) -> bool {
|
||||
matches!(
|
||||
mode,
|
||||
InputMode::Locked
|
||||
| InputMode::EnterSearch
|
||||
| InputMode::RenameTab
|
||||
| InputMode::RenamePane
|
||||
| InputMode::Prompt
|
||||
| InputMode::Tmux
|
||||
)
|
||||
}
|
||||
|
||||
// Tooltip operations
|
||||
fn toggle_persisted_tooltip(&self, new_mode: InputMode) {
|
||||
#[allow(unused_variables)]
|
||||
let message = self
|
||||
.create_tooltip_message(MSG_TOGGLE_PERSISTED_TOOLTIP, new_mode)
|
||||
.with_args(self.create_persist_args());
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
pipe_message_to_plugin(message);
|
||||
}
|
||||
|
||||
fn launch_tooltip_if_not_launched(&self, new_mode: InputMode) {
|
||||
let message = self.create_tooltip_message(MSG_LAUNCH_TOOLTIP, new_mode);
|
||||
pipe_message_to_plugin(message);
|
||||
}
|
||||
|
||||
fn create_tooltip_message(&self, name: &str, mode: InputMode) -> MessageToPlugin {
|
||||
let mut tooltip_config = self.config.clone();
|
||||
tooltip_config.insert(CONFIG_IS_TOOLTIP.to_string(), "true".to_string());
|
||||
|
||||
MessageToPlugin::new(name)
|
||||
.with_plugin_url("zellij:OWN_URL")
|
||||
.with_plugin_config(tooltip_config)
|
||||
.with_floating_pane_coordinates(self.calculate_tooltip_coordinates())
|
||||
.new_plugin_instance_should_have_pane_title(format!("{:?}", mode))
|
||||
}
|
||||
|
||||
fn create_persist_args(&self) -> BTreeMap<String, String> {
|
||||
let mut args = BTreeMap::new();
|
||||
args.insert("persist".to_string(), String::new());
|
||||
args
|
||||
}
|
||||
|
||||
fn update_tooltip_for_mode_change(&self, new_mode: InputMode) {
|
||||
if let Some(plugin_id) = self.own_plugin_id {
|
||||
let coordinates = self.calculate_tooltip_coordinates();
|
||||
change_floating_panes_coordinates(vec![(PaneId::Plugin(plugin_id), coordinates)]);
|
||||
rename_plugin_pane(plugin_id, format!("{:?}", new_mode));
|
||||
}
|
||||
}
|
||||
|
||||
fn move_tooltip_to_new_tab(&self, new_tab_index: usize) {
|
||||
if let Some(plugin_id) = self.own_plugin_id {
|
||||
break_panes_to_tab_with_index(
|
||||
&[PaneId::Plugin(plugin_id)],
|
||||
new_tab_index.saturating_sub(1), // Convert to 0-based indexing
|
||||
false,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn calculate_tooltip_coordinates(&self) -> FloatingPaneCoordinates {
|
||||
let tooltip_renderer = TooltipRenderer::new(&self.mode_info);
|
||||
let (tooltip_rows, tooltip_cols) =
|
||||
tooltip_renderer.calculate_dimensions(self.mode_info.mode);
|
||||
|
||||
let width = tooltip_cols + 4; // 2 for borders, 2 for padding
|
||||
let height = tooltip_rows + 2; // 2 for borders
|
||||
let x_position = 2;
|
||||
let y_position = self.display_area_rows.saturating_sub(height + 2);
|
||||
|
||||
FloatingPaneCoordinates::new(
|
||||
Some(x_position.to_string()),
|
||||
Some(y_position.to_string()),
|
||||
Some(width.to_string()),
|
||||
Some(height.to_string()),
|
||||
Some(true),
|
||||
Some(false),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
// Rendering
|
||||
fn render_tooltip(&self, rows: usize, cols: usize) {
|
||||
let tooltip_renderer = TooltipRenderer::new(&self.mode_info);
|
||||
tooltip_renderer.render(rows, cols);
|
||||
}
|
||||
|
||||
fn render_tab_line(&mut self, cols: usize) {
|
||||
if let Some(copy_destination) = self.text_copy_destination {
|
||||
self.render_clipboard_hint(copy_destination);
|
||||
} else if self.display_system_clipboard_failure {
|
||||
self.render_clipboard_error();
|
||||
} else {
|
||||
self.render_tabs(cols);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_clipboard_hint(&self, copy_destination: CopyDestination) {
|
||||
let hint = text_copied_hint(copy_destination).part;
|
||||
self.render_background_with_text(&hint);
|
||||
}
|
||||
|
||||
fn render_clipboard_error(&self) {
|
||||
let hint = system_clipboard_error().part;
|
||||
self.render_background_with_text(&hint);
|
||||
}
|
||||
|
||||
fn render_background_with_text(&self, text: &str) {
|
||||
let background = self.mode_info.style.colors.text_unselected.background;
|
||||
match background {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", text, r, g, b);
|
||||
},
|
||||
PaletteColor::EightBit(color) => {
|
||||
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", text, color);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn render_tabs(&mut self, cols: usize) {
|
||||
if self.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let tab_data = self.prepare_tab_data();
|
||||
let tab_line_output = tab_line(
|
||||
&self.mode_info,
|
||||
tab_data,
|
||||
cols,
|
||||
self.toggle_tooltip_key.clone(),
|
||||
self.tooltip_is_active,
|
||||
);
|
||||
self.tab_line = tab_line_output.parts;
|
||||
self.breadcrumb_range = tab_line_output.breadcrumb_range;
|
||||
|
||||
let output = self
|
||||
.tab_line
|
||||
.iter()
|
||||
.fold(String::new(), |acc, part| acc + &part.part);
|
||||
|
||||
self.render_background_with_text(&output);
|
||||
}
|
||||
|
||||
fn prepare_tab_data(&self) -> TabRenderData {
|
||||
let mut all_tabs = Vec::new();
|
||||
let mut active_tab_index = 0;
|
||||
let mut active_swap_layout_name = None;
|
||||
let mut is_swap_layout_dirty = false;
|
||||
let mut is_alternate_tab = false;
|
||||
let dimmed = self.mode_info.session_ascended == Some(true)
|
||||
|| self.mode_info.session_dimmed == Some(true);
|
||||
|
||||
for tab in &self.tabs {
|
||||
let tab_name = self.get_tab_display_name(tab);
|
||||
|
||||
if tab.active {
|
||||
active_tab_index = tab.position;
|
||||
if self.mode_info.mode != InputMode::RenameTab {
|
||||
is_swap_layout_dirty = tab.is_swap_layout_dirty;
|
||||
active_swap_layout_name = tab.active_swap_layout_name.clone();
|
||||
}
|
||||
}
|
||||
|
||||
let styled_tab = tab_style(
|
||||
tab_name,
|
||||
tab,
|
||||
is_alternate_tab,
|
||||
self.mode_info.style.colors,
|
||||
self.mode_info.capabilities,
|
||||
dimmed,
|
||||
);
|
||||
|
||||
is_alternate_tab = !is_alternate_tab;
|
||||
all_tabs.push(styled_tab);
|
||||
}
|
||||
|
||||
TabRenderData {
|
||||
tabs: all_tabs,
|
||||
active_tab_index,
|
||||
active_swap_layout_name,
|
||||
is_swap_layout_dirty,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_tab_display_name(&self, tab: &TabInfo) -> String {
|
||||
let mut tab_name = tab.name.clone();
|
||||
if tab.active && self.mode_info.mode == InputMode::RenameTab && tab_name.is_empty() {
|
||||
tab_name = "Enter name...".to_string();
|
||||
}
|
||||
tab_name
|
||||
}
|
||||
}
|
||||
|
||||
fn bind_toggle_key_config(toggle_key: &str, client_id: ClientId) -> String {
|
||||
format!(
|
||||
r#"
|
||||
keybinds {{
|
||||
shared {{
|
||||
bind "{}" {{
|
||||
MessagePlugin "compact-bar" {{
|
||||
name "toggle_tooltip"
|
||||
tooltip "{}"
|
||||
payload "{}"
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
"#,
|
||||
toggle_key, toggle_key, client_id
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
load_count: usize,
|
||||
instance_id: u32,
|
||||
configuration: BTreeMap<String, String>,
|
||||
slots: BTreeMap<SlotId, Slot>,
|
||||
removed_slots: Vec<SlotId>,
|
||||
clients: BTreeSet<ClientId>,
|
||||
disconnected_clients: Vec<ClientId>,
|
||||
key_events: Vec<(Option<SlotId>, Option<ClientId>)>,
|
||||
self_command_results: Vec<String>,
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
register_shared_plugin!(State);
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn render_line(
|
||||
state: &State,
|
||||
rows: usize,
|
||||
cols: usize,
|
||||
slot_id: SlotId,
|
||||
client_id: ClientId,
|
||||
) -> String {
|
||||
let slot_ids: Vec<SlotId> = state.slots.keys().copied().collect();
|
||||
let clients: Vec<ClientId> = state.clients.iter().copied().collect();
|
||||
let label = state
|
||||
.slots
|
||||
.get(&slot_id)
|
||||
.and_then(|slot| slot.configuration.get("label").cloned())
|
||||
.unwrap_or_default();
|
||||
format!(
|
||||
"instance:{} loads:{} slot:{} label:{} client:{} rows:{} cols:{} slots:{:?} removed:{:?} clients:{:?} disconnected:{:?} keys:{:?} self_commands:{:?}",
|
||||
state.instance_id,
|
||||
state.load_count,
|
||||
slot_id,
|
||||
label,
|
||||
client_id,
|
||||
rows,
|
||||
cols,
|
||||
slot_ids,
|
||||
state.removed_slots,
|
||||
clients,
|
||||
state.disconnected_clients,
|
||||
state.key_events,
|
||||
state.self_command_results,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(target_family = "wasm")]
|
||||
impl ZellijSharedPlugin for State {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
self.load_count += 1;
|
||||
self.configuration = configuration;
|
||||
self.instance_id = get_plugin_ids().plugin_id;
|
||||
subscribe(&[EventType::Key]);
|
||||
}
|
||||
fn slot_added(&mut self, slot: Slot) {
|
||||
if slot.kind == SlotKind::Pane {
|
||||
set_selectable(true);
|
||||
}
|
||||
self.slots.insert(slot.id, slot);
|
||||
}
|
||||
fn slot_removed(&mut self, slot_id: SlotId) {
|
||||
self.slots.remove(&slot_id);
|
||||
self.removed_slots.push(slot_id);
|
||||
}
|
||||
fn client_connected(&mut self, client_id: ClientId) {
|
||||
self.clients.insert(client_id);
|
||||
}
|
||||
fn client_disconnected(&mut self, client_id: ClientId) {
|
||||
self.clients.remove(&client_id);
|
||||
self.disconnected_clients.push(client_id);
|
||||
}
|
||||
fn update(&mut self, event: Event, context: EventContext) -> RenderResponse {
|
||||
match event {
|
||||
Event::Key(_) => {
|
||||
self.key_events.push((context.slot_id, context.client_id));
|
||||
match context.client_id {
|
||||
Some(client_id) => RenderResponse::Client(client_id),
|
||||
None => RenderResponse::All,
|
||||
}
|
||||
},
|
||||
_ => RenderResponse::Nothing,
|
||||
}
|
||||
}
|
||||
fn pipe(&mut self, pipe_message: PipeMessage, context: EventContext) -> RenderResponse {
|
||||
match pipe_message.name.as_str() {
|
||||
"render_all" => RenderResponse::All,
|
||||
"render_slot" => match pipe_message.payload.and_then(|p| p.parse::<SlotId>().ok()) {
|
||||
Some(slot_id) => RenderResponse::Slots(vec![slot_id]),
|
||||
None => RenderResponse::Nothing,
|
||||
},
|
||||
"selectable_slot" => {
|
||||
let results: Vec<String> = self
|
||||
.slots
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>()
|
||||
.into_iter()
|
||||
.map(|slot_id| format!("{:?}", set_selectable_slot(slot_id, false)))
|
||||
.collect();
|
||||
self.self_command_results.extend(results);
|
||||
self.self_command_results
|
||||
.push(format!("{:?}", set_selectable_slot(u32::MAX, false)));
|
||||
RenderResponse::All
|
||||
},
|
||||
"close_first_slot" => {
|
||||
if let Some(slot_id) = self.slots.keys().next().copied() {
|
||||
let result = close_slot(slot_id);
|
||||
self.self_command_results.push(format!("{:?}", result));
|
||||
}
|
||||
RenderResponse::All
|
||||
},
|
||||
_ => {
|
||||
let _ = context;
|
||||
RenderResponse::Nothing
|
||||
},
|
||||
}
|
||||
}
|
||||
fn render(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {
|
||||
print!("{}", render_line(self, rows, cols, slot_id, client_id));
|
||||
}
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
target = "wasm32-wasip1"
|
||||
@@ -1,9 +0,0 @@
|
||||
[package]
|
||||
name = "link"
|
||||
version = "0.1.0"
|
||||
authors = ["Aram Drevekenin <[email protected]>"]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
zellij-tile = { path = "../../zellij-tile" }
|
||||
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
target = "wasm32-wasip1"
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE.md
|
||||
@@ -1,2 +0,0 @@
|
||||
pub const DEFAULT_CACHE_FILE_PATH: &str = "/tmp/status-bar-tips.cache";
|
||||
pub const MAX_CACHE_HITS: usize = 20; // this should be 10, but right now there's a bug where the plugin load function is called twice, and sot he cache is hit twice
|
||||
@@ -1,2 +0,0 @@
|
||||
[build]
|
||||
target = "wasm32-wasip1"
|
||||
@@ -1,12 +0,0 @@
|
||||
[package]
|
||||
name = "tab-bar"
|
||||
version = "0.1.0"
|
||||
authors = ["Jonah Caplan <[email protected]>"]
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
|
||||
[dependencies]
|
||||
ansi_term = { workspace = true }
|
||||
unicode-width = { workspace = true }
|
||||
zellij-tile = { path = "../../zellij-tile" }
|
||||
zellij-tile-utils = { path = "../../zellij-tile-utils" }
|
||||
@@ -1 +0,0 @@
|
||||
../../LICENSE.md
|
||||
@@ -1,280 +0,0 @@
|
||||
mod line;
|
||||
mod tab;
|
||||
|
||||
use std::cmp::{max, min};
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryInto;
|
||||
|
||||
use tab::get_tab_to_focus;
|
||||
use zellij_tile::prelude::*;
|
||||
|
||||
use crate::line::tab_line;
|
||||
use crate::tab::tab_style;
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
pub struct LinePart {
|
||||
part: String,
|
||||
len: usize,
|
||||
tab_index: Option<usize>,
|
||||
}
|
||||
|
||||
impl LinePart {
|
||||
pub fn append(&mut self, to_append: &LinePart) {
|
||||
self.part.push_str(&to_append.part);
|
||||
self.len += to_append.len;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
struct State {
|
||||
tabs: Vec<TabInfo>,
|
||||
active_tab_idx: usize,
|
||||
mode_info: ModeInfo,
|
||||
tab_line: Vec<LinePart>,
|
||||
hide_swap_layout_indication: bool,
|
||||
cached_keybinds: KeybindsVec,
|
||||
active_pane_scroll: Option<(usize, usize)>,
|
||||
new_tab_button_range: Option<(usize, usize)>,
|
||||
breadcrumb_range: Option<(usize, usize)>,
|
||||
hovered_tab_idx: Option<usize>,
|
||||
hovered_new_tab_button: bool,
|
||||
hint_text: Option<BTreeMap<usize, StyledText>>,
|
||||
outstanding_hint_timeouts: usize,
|
||||
}
|
||||
|
||||
static ARROW_SEPARATOR: &str = "";
|
||||
|
||||
register_plugin!(State);
|
||||
|
||||
impl ZellijPlugin for State {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {
|
||||
self.hide_swap_layout_indication = configuration
|
||||
.get("hide_swap_layout_indication")
|
||||
.map(|s| s == "true")
|
||||
.unwrap_or(false);
|
||||
set_selectable(false);
|
||||
subscribe(&[
|
||||
EventType::TabUpdate,
|
||||
EventType::ModeUpdate,
|
||||
EventType::Mouse,
|
||||
EventType::InitialKeybinds,
|
||||
EventType::ActivePaneScroll,
|
||||
EventType::HintText,
|
||||
EventType::Timer,
|
||||
EventType::InputReceived,
|
||||
]);
|
||||
}
|
||||
|
||||
fn update(&mut self, event: Event) -> bool {
|
||||
let mut should_render = false;
|
||||
match event {
|
||||
Event::InitialKeybinds(keybinds) => {
|
||||
self.cached_keybinds = keybinds;
|
||||
if !self.cached_keybinds.is_empty() {
|
||||
self.mode_info.keybinds = self.cached_keybinds.clone();
|
||||
}
|
||||
should_render = true;
|
||||
},
|
||||
Event::ModeUpdate(mut mode_info) => {
|
||||
if mode_info.keybinds.is_empty() && !self.cached_keybinds.is_empty() {
|
||||
mode_info.keybinds = self.cached_keybinds.clone();
|
||||
} else if !mode_info.keybinds.is_empty() {
|
||||
self.cached_keybinds = mode_info.keybinds.clone();
|
||||
}
|
||||
if self.mode_info != mode_info {
|
||||
should_render = true;
|
||||
}
|
||||
self.mode_info = mode_info;
|
||||
},
|
||||
Event::TabUpdate(tabs) => {
|
||||
if let Some(active_tab_index) = tabs.iter().position(|t| t.active) {
|
||||
// tabs are indexed starting from 1 so we need to add 1
|
||||
let active_tab_idx = active_tab_index + 1;
|
||||
|
||||
if self.active_tab_idx != active_tab_idx || self.tabs != tabs {
|
||||
should_render = true;
|
||||
}
|
||||
self.active_tab_idx = active_tab_idx;
|
||||
self.tabs = tabs;
|
||||
} else {
|
||||
eprintln!("Could not find active tab.");
|
||||
}
|
||||
},
|
||||
Event::ActivePaneScroll(scroll) => {
|
||||
if self.active_pane_scroll != scroll {
|
||||
should_render = true;
|
||||
}
|
||||
self.active_pane_scroll = scroll;
|
||||
},
|
||||
Event::HintText(hint_variants) => {
|
||||
if hint_variants.is_empty() {
|
||||
if self.hint_text.is_some() {
|
||||
self.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
} else {
|
||||
self.hint_text = Some(hint_variants);
|
||||
self.outstanding_hint_timeouts += 1;
|
||||
set_timeout(5.0);
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Event::Timer(_) => {
|
||||
self.outstanding_hint_timeouts = self.outstanding_hint_timeouts.saturating_sub(1);
|
||||
if self.outstanding_hint_timeouts == 0 && self.hint_text.is_some() {
|
||||
self.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Event::InputReceived => {
|
||||
if self.hint_text.is_some() {
|
||||
self.hint_text = None;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Event::Mouse(me) => match me {
|
||||
Mouse::LeftClick(_, col) => {
|
||||
if let Some((start, end)) = self.breadcrumb_range {
|
||||
if col >= start && col < end {
|
||||
focus_host_session();
|
||||
return should_render;
|
||||
}
|
||||
}
|
||||
if let Some((start, end)) = self.new_tab_button_range {
|
||||
if col >= start && col < end {
|
||||
new_tab::<&str>(None, None);
|
||||
return should_render;
|
||||
}
|
||||
}
|
||||
let tab_to_focus = get_tab_to_focus(&self.tab_line, self.active_tab_idx, col);
|
||||
if let Some(idx) = tab_to_focus {
|
||||
switch_tab_to(idx.try_into().unwrap());
|
||||
}
|
||||
},
|
||||
Mouse::Hover(_, col) => {
|
||||
let simplified_ui = self.mode_info.capabilities.arrow_fonts;
|
||||
let mut new_hovered_new_tab_button = false;
|
||||
let mut new_hovered_tab_idx = None;
|
||||
if !simplified_ui {
|
||||
if let Some((start, end)) = self.new_tab_button_range {
|
||||
if col >= start && col < end {
|
||||
new_hovered_new_tab_button = true;
|
||||
}
|
||||
}
|
||||
if !new_hovered_new_tab_button {
|
||||
new_hovered_tab_idx =
|
||||
get_tab_to_focus(&self.tab_line, self.active_tab_idx, col);
|
||||
}
|
||||
}
|
||||
if self.hovered_new_tab_button != new_hovered_new_tab_button
|
||||
|| self.hovered_tab_idx != new_hovered_tab_idx
|
||||
{
|
||||
self.hovered_new_tab_button = new_hovered_new_tab_button;
|
||||
self.hovered_tab_idx = new_hovered_tab_idx;
|
||||
should_render = true;
|
||||
}
|
||||
},
|
||||
Mouse::ScrollUp(_) => {
|
||||
switch_tab_to(min(self.active_tab_idx + 1, self.tabs.len()) as u32);
|
||||
},
|
||||
Mouse::ScrollDown(_) => {
|
||||
switch_tab_to(max(self.active_tab_idx.saturating_sub(1), 1) as u32);
|
||||
},
|
||||
_ => {},
|
||||
},
|
||||
_ => {
|
||||
eprintln!("Got unrecognized event: {:?}", event);
|
||||
},
|
||||
}
|
||||
if self.tabs.is_empty() {
|
||||
// no need to render if we have no tabs, this can sometimes happen on startup before we
|
||||
// get the tab update and then we definitely don't want to render
|
||||
should_render = false;
|
||||
}
|
||||
should_render
|
||||
}
|
||||
|
||||
fn render(&mut self, _rows: usize, cols: usize) {
|
||||
if self.tabs.is_empty() {
|
||||
return;
|
||||
}
|
||||
let dimmed = self.mode_info.session_ascended == Some(true)
|
||||
|| self.mode_info.session_dimmed == Some(true);
|
||||
let mut all_tabs: Vec<LinePart> = vec![];
|
||||
let mut active_tab_index = 0;
|
||||
let mut is_alternate_tab = false;
|
||||
for t in &mut self.tabs {
|
||||
let mut tabname = t.name.clone();
|
||||
if t.active && self.mode_info.mode == InputMode::RenameTab {
|
||||
if tabname.is_empty() {
|
||||
tabname = String::from("Enter name...");
|
||||
}
|
||||
active_tab_index = t.position;
|
||||
} else if t.active {
|
||||
active_tab_index = t.position;
|
||||
}
|
||||
let is_hovered = self.hovered_tab_idx == Some(t.position + 1);
|
||||
let tab = tab_style(
|
||||
tabname,
|
||||
t,
|
||||
is_alternate_tab,
|
||||
is_hovered,
|
||||
self.mode_info.style.colors,
|
||||
self.mode_info.capabilities,
|
||||
dimmed,
|
||||
);
|
||||
is_alternate_tab = !is_alternate_tab;
|
||||
all_tabs.push(tab);
|
||||
}
|
||||
|
||||
let background = self.mode_info.style.colors.text_unselected.background;
|
||||
|
||||
let full_pane_frames = self.mode_info.pane_frame_style == Some(PaneFrameStyle::Full);
|
||||
let hint_text = if full_pane_frames {
|
||||
None
|
||||
} else {
|
||||
self.hint_text.as_ref()
|
||||
};
|
||||
let breadcrumb_ancestry: Vec<String> = if self.mode_info.host_fullscreen == Some(true) {
|
||||
self.mode_info.session_ancestry.clone()
|
||||
} else {
|
||||
vec![]
|
||||
};
|
||||
let (line, new_tab_button_range, breadcrumb_range) = tab_line(
|
||||
self.mode_info.session_name.as_deref(),
|
||||
all_tabs,
|
||||
active_tab_index,
|
||||
cols.saturating_sub(1),
|
||||
self.mode_info.style.colors,
|
||||
self.mode_info.capabilities,
|
||||
self.mode_info.style.hide_session_name,
|
||||
self.tabs.iter().find(|t| t.active),
|
||||
&self.mode_info,
|
||||
self.hide_swap_layout_indication,
|
||||
&background,
|
||||
self.active_pane_scroll,
|
||||
hint_text,
|
||||
is_alternate_tab,
|
||||
self.hovered_new_tab_button,
|
||||
dimmed,
|
||||
&breadcrumb_ancestry,
|
||||
);
|
||||
self.tab_line = line;
|
||||
self.new_tab_button_range = new_tab_button_range;
|
||||
self.breadcrumb_range = breadcrumb_range;
|
||||
|
||||
let output = self
|
||||
.tab_line
|
||||
.iter()
|
||||
.fold(String::new(), |output, part| output + &part.part);
|
||||
|
||||
match background {
|
||||
PaletteColor::Rgb((r, g, b)) => {
|
||||
print!("{}\u{1b}[48;2;{};{};{}m\u{1b}[0K", output, r, g, b);
|
||||
},
|
||||
PaletteColor::EightBit(color) => {
|
||||
print!("{}\u{1b}[48;5;{}m\u{1b}[0K", output, color);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
-7
@@ -168,13 +168,17 @@ pub fn ensure_plugin_assets(sh: &Shell) -> anyhow::Result<()> {
|
||||
Some((_, name)) => name,
|
||||
None => return true,
|
||||
};
|
||||
let asset_time = std::fs::metadata(plugin_asset_path(plugin_name))
|
||||
.and_then(|m| m.modified())
|
||||
.ok();
|
||||
match (asset_time, newest_source) {
|
||||
(Some(asset_time), Some(newest_source)) => asset_time < newest_source,
|
||||
_ => true,
|
||||
}
|
||||
std::iter::once(plugin_name)
|
||||
.chain(member.extra_artifacts.iter().copied())
|
||||
.any(|artifact_name| {
|
||||
let asset_time = std::fs::metadata(plugin_asset_path(artifact_name))
|
||||
.and_then(|m| m.modified())
|
||||
.ok();
|
||||
match (asset_time, newest_source) {
|
||||
(Some(asset_time), Some(newest_source)) => asset_time < newest_source,
|
||||
_ => true,
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
if !stale {
|
||||
@@ -205,6 +209,9 @@ fn build_plugins_release_into_assets(
|
||||
|
||||
for member in plugin_members {
|
||||
move_plugin_to_assets(sh, plugin_name_of(member)?)?;
|
||||
for artifact_name in member.extra_artifacts {
|
||||
move_plugin_to_assets(sh, artifact_name)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+18
-13
@@ -28,6 +28,7 @@ use xshell::Shell;
|
||||
pub struct WorkspaceMember {
|
||||
crate_name: &'static str,
|
||||
build: bool,
|
||||
extra_artifacts: &'static [&'static str],
|
||||
}
|
||||
|
||||
fn workspace_members() -> &'static Vec<WorkspaceMember> {
|
||||
@@ -35,80 +36,84 @@ fn workspace_members() -> &'static Vec<WorkspaceMember> {
|
||||
WORKSPACE_MEMBERS.get_or_init(|| {
|
||||
vec![
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/compact-bar",
|
||||
build: true,
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/status-bar",
|
||||
crate_name: "default-plugins/bars",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/strider",
|
||||
build: true,
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/tab-bar",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/fixture-plugin-for-tests",
|
||||
build: true,
|
||||
extra_artifacts: &["fixture-shared-plugin-for-tests"],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/session-manager",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/configuration",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/plugin-manager",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/about",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/multiple-select",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/share",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/layout-manager",
|
||||
build: true,
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "default-plugins/link",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "zellij-utils",
|
||||
build: false,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "zellij-tile-utils",
|
||||
build: false,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "zellij-tile",
|
||||
build: false,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "zellij-client",
|
||||
build: false,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: "zellij-server",
|
||||
build: false,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
WorkspaceMember {
|
||||
crate_name: ".",
|
||||
build: true,
|
||||
extra_artifacts: &[],
|
||||
},
|
||||
]
|
||||
})
|
||||
|
||||
@@ -185,14 +185,19 @@ impl ClientScreen {
|
||||
loop {
|
||||
let size = *self.size.lock().unwrap();
|
||||
let grid_snapshot = render_bytes(&received_bytes.bytes, size);
|
||||
if predicate(&grid_snapshot) {
|
||||
if !grid_snapshot.bars_partially_drawn() && predicate(&grid_snapshot) {
|
||||
return grid_snapshot;
|
||||
}
|
||||
let now = Instant::now();
|
||||
if now >= deadline {
|
||||
panic!(
|
||||
"timed out waiting for: {}\ncursor: {:?}\nlast rendered grid:\n{}\n=== (received {} stdout bytes, generation {}) ===\n=== zellij log tail ({}) ===\n{}",
|
||||
"timed out waiting for: {}{}\ncursor: {:?}\nlast rendered grid:\n{}\n=== (received {} stdout bytes, generation {}) ===\n=== zellij log tail ({}) ===\n{}",
|
||||
what,
|
||||
if grid_snapshot.bars_partially_drawn() {
|
||||
" (the status bar drew but the tab bar row stayed blank)"
|
||||
} else {
|
||||
""
|
||||
},
|
||||
grid_snapshot.cursor,
|
||||
grid_snapshot.text,
|
||||
received_bytes.bytes.len(),
|
||||
@@ -362,6 +367,12 @@ impl GridSnapshot {
|
||||
pub fn tab_bar_appears(&self) -> bool {
|
||||
self.text.contains("Tab #1")
|
||||
}
|
||||
pub fn bars_partially_drawn(&self) -> bool {
|
||||
let lines: Vec<&str> = self.text.lines().collect();
|
||||
let first_line_blank = lines.first().map_or(false, |line| line.trim().is_empty());
|
||||
let status_bar_at_bottom = lines.iter().rev().take(2).any(|line| line.contains("LOCK"));
|
||||
first_line_blank && status_bar_at_bottom
|
||||
}
|
||||
pub fn lines(&self) -> Vec<String> {
|
||||
self.text.lines().map(|l| l.to_owned()).collect()
|
||||
}
|
||||
|
||||
@@ -288,24 +288,15 @@ struct FakeAsyncReader {
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReader for FakeAsyncReader {
|
||||
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, std::io::Error> {
|
||||
async fn read_chunk(&mut self, max: usize) -> Result<Vec<u8>, std::io::Error> {
|
||||
if self.pending.is_empty() {
|
||||
match self.output_rx.recv().await {
|
||||
Some(bytes) => self.pending.extend(bytes),
|
||||
None => return Ok(0),
|
||||
None => return Ok(Vec::new()),
|
||||
}
|
||||
}
|
||||
let mut written = 0;
|
||||
while written < buf.len() {
|
||||
match self.pending.pop_front() {
|
||||
Some(byte) => {
|
||||
buf[written] = byte;
|
||||
written += 1;
|
||||
},
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(written)
|
||||
let n = max.min(self.pending.len());
|
||||
Ok(self.pending.drain(..n).collect())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -121,6 +121,7 @@ fn use_custom_layout_with_relative_path() {
|
||||
grid_snapshot.tab_bar_appears()
|
||||
&& grid_snapshot.status_bar_appears()
|
||||
&& grid_snapshot.contains("Zellij (test")
|
||||
&& !grid_snapshot.contains(" BASE ")
|
||||
&& grid_snapshot.cursor.is_some()
|
||||
});
|
||||
assert_snapshot!(normalized(&grid_snapshot));
|
||||
|
||||
@@ -444,6 +444,7 @@ fn entries_share_a_uniform_width_sized_by_the_widest_title() {
|
||||
}
|
||||
let grid_snapshot = zellij.wait_until("mixed-width entries rendered", |grid_snapshot| {
|
||||
grid_snapshot.status_bar_appears()
|
||||
&& grid_snapshot.tab_bar_appears()
|
||||
&& grid_snapshot.contains("member-command")
|
||||
&& grid_snapshot.contains(&selected_entry("Pane #1"))
|
||||
&& grid_snapshot.cursor_is_at(col(2).row(4))
|
||||
|
||||
@@ -78,6 +78,7 @@ pub enum BackgroundJob {
|
||||
StopFlashTabBell(usize), // usize = tab_id
|
||||
StartNestedGuestPing(PaneId),
|
||||
StopNestedGuestPing(PaneId),
|
||||
TrimAllocator,
|
||||
Exit,
|
||||
}
|
||||
|
||||
@@ -111,12 +112,25 @@ impl From<&BackgroundJob> for BackgroundJobContext {
|
||||
BackgroundJob::StopFlashTabBell(..) => BackgroundJobContext::StopFlashTabBell,
|
||||
BackgroundJob::StartNestedGuestPing(..) => BackgroundJobContext::StartNestedGuestPing,
|
||||
BackgroundJob::StopNestedGuestPing(..) => BackgroundJobContext::StopNestedGuestPing,
|
||||
BackgroundJob::TrimAllocator => BackgroundJobContext::TrimAllocator,
|
||||
BackgroundJob::Exit => BackgroundJobContext::Exit,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static LONG_FLASH_DURATION_MS: u64 = 1000;
|
||||
static TRIM_ALLOCATOR_DEBOUNCE_MS: u64 = 2000;
|
||||
static STARTUP_TRIM_ALLOCATOR_DELAY_MS: u64 = 10000;
|
||||
|
||||
#[cfg(all(target_os = "linux", target_env = "gnu"))]
|
||||
fn trim_allocator() {
|
||||
unsafe {
|
||||
libc::malloc_trim(0);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(all(target_os = "linux", target_env = "gnu")))]
|
||||
fn trim_allocator() {}
|
||||
static FLASH_DURATION_MS: u64 = 400; // Doherty threshold
|
||||
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
|
||||
static SESSION_METADATA_WRITE_INTERVAL_MS: u64 = 1000;
|
||||
@@ -171,6 +185,7 @@ pub(crate) fn background_jobs_main(
|
||||
let mut flashing_pane_bells: HashMap<PaneId, Arc<AtomicBool>> = HashMap::new();
|
||||
let mut flashing_tab_bells: HashMap<usize, Arc<AtomicBool>> = HashMap::new();
|
||||
let mut nested_guest_pings: HashMap<PaneId, Arc<AtomicBool>> = HashMap::new();
|
||||
let pending_allocator_trim: Arc<Mutex<Option<Instant>>> = Arc::new(Mutex::new(None));
|
||||
|
||||
let http_client = HttpClient::builder()
|
||||
// TODO: timeout?
|
||||
@@ -180,6 +195,11 @@ pub(crate) fn background_jobs_main(
|
||||
// We needn't do anything with the runtime, but it should exist at this point.
|
||||
let runtime = crate::global_async_runtime::get_tokio_runtime();
|
||||
|
||||
runtime.spawn(async move {
|
||||
tokio::time::sleep(Duration::from_millis(STARTUP_TRIM_ALLOCATOR_DELAY_MS)).await;
|
||||
trim_allocator();
|
||||
});
|
||||
|
||||
{
|
||||
let senders = bus.senders.clone();
|
||||
let serialization_ms = serialization_interval.unwrap_or(DEFAULT_SERIALIZATION_INTERVAL);
|
||||
@@ -711,6 +731,45 @@ pub(crate) fn background_jobs_main(
|
||||
flag.store(false, Ordering::SeqCst);
|
||||
}
|
||||
},
|
||||
BackgroundJob::TrimAllocator => {
|
||||
let should_spawn = {
|
||||
let mut pending = pending_allocator_trim.lock().unwrap();
|
||||
let should_spawn = pending.is_none();
|
||||
*pending = Some(Instant::now());
|
||||
should_spawn
|
||||
};
|
||||
if should_spawn {
|
||||
let pending = pending_allocator_trim.clone();
|
||||
runtime.spawn(async move {
|
||||
let debounce = Duration::from_millis(TRIM_ALLOCATOR_DEBOUNCE_MS);
|
||||
tokio::time::sleep(debounce).await;
|
||||
loop {
|
||||
let remaining = {
|
||||
let mut pending = pending.lock().unwrap();
|
||||
match *pending {
|
||||
Some(last_request) => {
|
||||
let elapsed = Instant::now().duration_since(last_request);
|
||||
if elapsed >= debounce {
|
||||
*pending = None;
|
||||
None
|
||||
} else {
|
||||
Some(debounce.saturating_sub(elapsed))
|
||||
}
|
||||
},
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
match remaining {
|
||||
Some(duration) => tokio::time::sleep(duration).await,
|
||||
None => {
|
||||
trim_allocator();
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
BackgroundJob::Exit => {
|
||||
for loading_plugin in loading_plugins.values() {
|
||||
loading_plugin.store(false, Ordering::SeqCst);
|
||||
|
||||
+197
-13
@@ -83,6 +83,8 @@ use zellij_utils::{
|
||||
|
||||
pub use zellij_utils::data::ClientId;
|
||||
|
||||
pub(crate) type SharedKeybinds = Arc<zellij_utils::data::KeybindsVec>;
|
||||
|
||||
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);
|
||||
@@ -240,14 +242,32 @@ impl SessionConfiguration {
|
||||
pub fn set_saved_configuration(&mut self, config: Config) {
|
||||
self.saved_config = config;
|
||||
}
|
||||
pub fn set_client_runtime_configuration(&mut self, client_id: ClientId, client_config: Config) {
|
||||
pub fn set_client_runtime_configuration(
|
||||
&mut self,
|
||||
client_id: ClientId,
|
||||
mut client_config: Config,
|
||||
) {
|
||||
self.share_keybinds(&mut client_config);
|
||||
self.runtime_config.insert(client_id, client_config);
|
||||
}
|
||||
fn share_keybinds(&self, config: &mut Config) {
|
||||
let known_configs =
|
||||
|| std::iter::once(&self.saved_config).chain(self.runtime_config.values());
|
||||
if known_configs().any(|known| Arc::ptr_eq(&known.keybinds, &config.keybinds)) {
|
||||
return;
|
||||
}
|
||||
if let Some(known) = known_configs().find(|known| known.keybinds == config.keybinds) {
|
||||
config.keybinds = known.keybinds.clone();
|
||||
}
|
||||
}
|
||||
pub fn remove_client(&mut self, client_id: &ClientId) {
|
||||
self.runtime_config.remove(client_id);
|
||||
}
|
||||
pub fn get_client_keybinds(&self, client_id: &ClientId) -> &Keybinds {
|
||||
self.runtime_config
|
||||
.get(client_id)
|
||||
.map(|c| &c.keybinds)
|
||||
.unwrap_or(&self.saved_config.keybinds)
|
||||
.map(|c| c.keybinds.as_ref())
|
||||
.unwrap_or(self.saved_config.keybinds.as_ref())
|
||||
}
|
||||
pub fn get_client_default_input_mode(&self, client_id: &ClientId) -> InputMode {
|
||||
self.runtime_config
|
||||
@@ -276,7 +296,8 @@ impl SessionConfiguration {
|
||||
&stringified_config,
|
||||
Some(current_client_configuration.clone()),
|
||||
) {
|
||||
Ok(new_config) => {
|
||||
Ok(mut new_config) => {
|
||||
self.share_keybinds(&mut new_config);
|
||||
config_changed = current_client_configuration != new_config;
|
||||
full_reconfigured_config = Some(new_config.clone());
|
||||
self.runtime_config.insert(*client_id, new_config);
|
||||
@@ -300,11 +321,20 @@ impl SessionConfiguration {
|
||||
self.runtime_config
|
||||
.insert(*client_id, self.saved_config.clone());
|
||||
}
|
||||
let mut rebound_config = None;
|
||||
match self.runtime_config.get_mut(client_id) {
|
||||
Some(config) => {
|
||||
for (input_mode, key_with_modifier) in keys_to_unbind {
|
||||
let keys_in_mode = config
|
||||
let needs_change = config
|
||||
.keybinds
|
||||
.0
|
||||
.get(&input_mode)
|
||||
.map(|keys_in_mode| keys_in_mode.contains_key(&key_with_modifier))
|
||||
.unwrap_or(true);
|
||||
if !needs_change {
|
||||
continue;
|
||||
}
|
||||
let keys_in_mode = Arc::make_mut(&mut config.keybinds)
|
||||
.0
|
||||
.entry(input_mode)
|
||||
.or_insert_with(Default::default);
|
||||
@@ -314,8 +344,16 @@ impl SessionConfiguration {
|
||||
}
|
||||
}
|
||||
for (input_mode, key_with_modifier, actions) in keys_to_rebind {
|
||||
let keys_in_mode = config
|
||||
let needs_change = config
|
||||
.keybinds
|
||||
.0
|
||||
.get(&input_mode)
|
||||
.map(|keys_in_mode| keys_in_mode.get(&key_with_modifier) != Some(&actions))
|
||||
.unwrap_or(true);
|
||||
if !needs_change {
|
||||
continue;
|
||||
}
|
||||
let keys_in_mode = Arc::make_mut(&mut config.keybinds)
|
||||
.0
|
||||
.entry(input_mode)
|
||||
.or_insert_with(Default::default);
|
||||
@@ -325,7 +363,7 @@ impl SessionConfiguration {
|
||||
}
|
||||
}
|
||||
if config_changed {
|
||||
full_reconfigured_config = Some(config.clone());
|
||||
rebound_config = Some(config.clone());
|
||||
}
|
||||
},
|
||||
None => {
|
||||
@@ -334,6 +372,12 @@ impl SessionConfiguration {
|
||||
);
|
||||
},
|
||||
}
|
||||
if let Some(mut config) = rebound_config {
|
||||
self.runtime_config.remove(client_id);
|
||||
self.share_keybinds(&mut config);
|
||||
self.runtime_config.insert(*client_id, config.clone());
|
||||
full_reconfigured_config = Some(config);
|
||||
}
|
||||
|
||||
(full_reconfigured_config, config_changed)
|
||||
}
|
||||
@@ -411,6 +455,7 @@ impl SessionMetaData {
|
||||
config_was_written_to_disk: bool,
|
||||
) {
|
||||
let mut new_plugin_config = None;
|
||||
let mut converted_keybinds: Vec<(Arc<Keybinds>, SharedKeybinds)> = vec![];
|
||||
for (client_id, new_config) in config_changes {
|
||||
if new_plugin_config.is_none() {
|
||||
new_plugin_config = Some(new_config.plugins.clone());
|
||||
@@ -447,10 +492,22 @@ impl SessionMetaData {
|
||||
);
|
||||
}
|
||||
let pane_frame_style = PaneFrameStyle::from_options(&new_config.options);
|
||||
let shared_keybinds = match converted_keybinds
|
||||
.iter()
|
||||
.find(|(keybinds, _)| Arc::ptr_eq(keybinds, &new_config.keybinds))
|
||||
{
|
||||
Some((_, shared_keybinds)) => shared_keybinds.clone(),
|
||||
None => {
|
||||
let shared_keybinds: SharedKeybinds =
|
||||
Arc::new(new_config.keybinds.to_keybinds_vec());
|
||||
converted_keybinds.push((new_config.keybinds.clone(), shared_keybinds.clone()));
|
||||
shared_keybinds
|
||||
},
|
||||
};
|
||||
self.senders
|
||||
.send_to_screen(ScreenInstruction::Reconfigure {
|
||||
client_id,
|
||||
keybinds: new_config.keybinds.clone(),
|
||||
keybinds: shared_keybinds.clone(),
|
||||
default_mode: new_config
|
||||
.options
|
||||
.default_mode
|
||||
@@ -515,7 +572,7 @@ impl SessionMetaData {
|
||||
self.senders
|
||||
.send_to_plugin(PluginInstruction::Reconfigure {
|
||||
client_id,
|
||||
keybinds: Some(new_config.keybinds),
|
||||
keybinds: Some(shared_keybinds),
|
||||
default_mode: new_config.options.default_mode,
|
||||
default_shell: self.default_shell.clone(),
|
||||
layout_dir: new_config.options.layout_dir,
|
||||
@@ -841,6 +898,125 @@ mod session_state_tests {
|
||||
assert!(picked == 1 || picked == 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_configuration_remove_client_drops_runtime_config() {
|
||||
let mut session_configuration = SessionConfiguration::default();
|
||||
session_configuration.set_client_runtime_configuration(1, Config::default());
|
||||
session_configuration.set_client_runtime_configuration(2, Config::default());
|
||||
session_configuration.remove_client(&1);
|
||||
assert!(!session_configuration.runtime_config.contains_key(&1));
|
||||
assert!(session_configuration.runtime_config.contains_key(&2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn session_configuration_shares_keybinds_until_a_client_changes_them() {
|
||||
use zellij_utils::data::BareKey;
|
||||
let config = Config::from_default_assets().unwrap();
|
||||
assert!(Arc::ptr_eq(&config.keybinds, &config.clone().keybinds));
|
||||
let mut session_configuration = SessionConfiguration::default();
|
||||
session_configuration.set_saved_configuration(config.clone());
|
||||
session_configuration.set_client_runtime_configuration(1, config.clone());
|
||||
session_configuration.set_client_runtime_configuration(2, config.clone());
|
||||
let keybinds_of = |session_configuration: &SessionConfiguration, client_id: ClientId| {
|
||||
session_configuration.runtime_config[&client_id]
|
||||
.keybinds
|
||||
.clone()
|
||||
};
|
||||
let saved_keybinds = session_configuration.saved_config.keybinds.clone();
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&saved_keybinds
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 2),
|
||||
&saved_keybinds
|
||||
));
|
||||
|
||||
let (_, changed) =
|
||||
session_configuration.reconfigure_runtime_config(&1, "simplified_ui true".to_owned());
|
||||
assert!(changed);
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&saved_keybinds
|
||||
));
|
||||
|
||||
let client_2_keybinds_before = (*keybinds_of(&session_configuration, 2)).clone();
|
||||
let new_binding = r#"
|
||||
keybinds {
|
||||
normal {
|
||||
bind "Alt F1" { NewPane; }
|
||||
}
|
||||
}
|
||||
"#;
|
||||
let (_, changed) =
|
||||
session_configuration.reconfigure_runtime_config(&1, new_binding.to_owned());
|
||||
assert!(changed);
|
||||
assert!(!Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&saved_keybinds
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 2),
|
||||
&saved_keybinds
|
||||
));
|
||||
assert_eq!(
|
||||
*keybinds_of(&session_configuration, 2),
|
||||
client_2_keybinds_before
|
||||
);
|
||||
assert_ne!(
|
||||
session_configuration.get_client_keybinds(&1),
|
||||
session_configuration.get_client_keybinds(&2)
|
||||
);
|
||||
let alt_f1 = KeyWithModifier::new(BareKey::F(1)).with_alt_modifier();
|
||||
assert!(session_configuration
|
||||
.get_client_keybinds(&1)
|
||||
.get_actions_for_key_in_mode(&InputMode::Normal, &alt_f1)
|
||||
.is_some());
|
||||
assert_eq!(
|
||||
session_configuration
|
||||
.get_client_keybinds(&2)
|
||||
.get_actions_for_key_in_mode(&InputMode::Normal, &alt_f1),
|
||||
config
|
||||
.keybinds
|
||||
.get_actions_for_key_in_mode(&InputMode::Normal, &alt_f1)
|
||||
);
|
||||
|
||||
let (_, changed) =
|
||||
session_configuration.reconfigure_runtime_config(&2, new_binding.to_owned());
|
||||
assert!(changed);
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&keybinds_of(&session_configuration, 2)
|
||||
));
|
||||
|
||||
let (_, changed) = session_configuration.rebind_keys(
|
||||
&2,
|
||||
vec![],
|
||||
vec![(InputMode::Normal, alt_f1.clone())],
|
||||
);
|
||||
assert!(changed);
|
||||
assert!(!Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&keybinds_of(&session_configuration, 2)
|
||||
));
|
||||
assert!(session_configuration
|
||||
.get_client_keybinds(&1)
|
||||
.get_actions_for_key_in_mode(&InputMode::Normal, &alt_f1)
|
||||
.is_some());
|
||||
|
||||
let config_changes = session_configuration.change_saved_config(config.clone());
|
||||
assert!(config_changes.iter().any(|(client_id, _)| *client_id == 1));
|
||||
let saved_keybinds = session_configuration.saved_config.keybinds.clone();
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 1),
|
||||
&saved_keybinds
|
||||
));
|
||||
assert!(Arc::ptr_eq(
|
||||
&keybinds_of(&session_configuration, 2),
|
||||
&saved_keybinds
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pick_forward_target_none_when_no_clients() {
|
||||
let s = SessionState::new();
|
||||
@@ -1471,7 +1647,8 @@ 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
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
if let Some(session_data) = session_data.write().unwrap().as_mut() {
|
||||
session_data.session_configuration.remove_client(&client_id);
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
@@ -1529,7 +1706,8 @@ pub fn start_server_impl(
|
||||
}
|
||||
// Handle regular client removal
|
||||
remove_client!(client_id, os_input, session_state, session_data);
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
if let Some(session_data) = session_data.write().unwrap().as_mut() {
|
||||
session_data.session_configuration.remove_client(&client_id);
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
@@ -1601,7 +1779,8 @@ 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 {
|
||||
if let Some(session_data) = session_data.write().unwrap().as_ref() {
|
||||
if let Some(session_data) = session_data.write().unwrap().as_mut() {
|
||||
session_data.session_configuration.remove_client(&client_id);
|
||||
let _ = session_data
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::RemoveClient(client_id));
|
||||
@@ -1736,6 +1915,9 @@ pub fn start_server_impl(
|
||||
remove_client!(client_id, os_input, session_state, session_data);
|
||||
drop(completion_tx); // do not deadlock with route thread
|
||||
|
||||
if let Some(session_data) = session_data.write().unwrap().as_mut() {
|
||||
session_data.session_configuration.remove_client(&client_id);
|
||||
}
|
||||
session_data
|
||||
.write()
|
||||
.unwrap()
|
||||
@@ -2190,7 +2372,7 @@ fn init_session(
|
||||
.unwrap_or_else(|| get_default_shell());
|
||||
|
||||
let default_mode = config_options.default_mode.unwrap_or_default();
|
||||
let default_keybinds = config.keybinds.clone();
|
||||
let default_keybinds: SharedKeybinds = Arc::new(config.keybinds.to_keybinds_vec());
|
||||
|
||||
let pty_thread = thread::Builder::new()
|
||||
.name("pty".to_string())
|
||||
@@ -2236,6 +2418,7 @@ fn init_session(
|
||||
let debug = cli_assets.is_debug;
|
||||
let layout = layout.clone();
|
||||
let config = config.clone();
|
||||
let default_keybinds = default_keybinds.clone();
|
||||
move || {
|
||||
screen_thread_main(
|
||||
screen_bus,
|
||||
@@ -2244,6 +2427,7 @@ fn init_session(
|
||||
config,
|
||||
debug,
|
||||
layout,
|
||||
default_keybinds,
|
||||
)
|
||||
.fatal();
|
||||
}
|
||||
|
||||
@@ -30,7 +30,10 @@ use std::{
|
||||
io::{self, Write},
|
||||
path::PathBuf,
|
||||
process::Command,
|
||||
sync::{Arc, Mutex},
|
||||
sync::{
|
||||
atomic::{AtomicUsize, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
};
|
||||
|
||||
pub use async_trait::async_trait;
|
||||
@@ -211,10 +214,58 @@ fn build_command(
|
||||
// this client and we'll stop sending messages to it.
|
||||
// If the client ever becomes responsive again, we'll send one final "Buffer full" message so it
|
||||
// knows what happened.
|
||||
const CLIENT_BUFFER_LIMIT: usize = 5000;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ClientBuffer {
|
||||
sender: channels::Sender<ServerToClientMsg>,
|
||||
queued: Arc<AtomicUsize>,
|
||||
limit: usize,
|
||||
}
|
||||
|
||||
struct ClientBufferReceiver {
|
||||
receiver: channels::Receiver<ServerToClientMsg>,
|
||||
queued: Arc<AtomicUsize>,
|
||||
}
|
||||
|
||||
fn client_buffer(limit: usize) -> (ClientBuffer, ClientBufferReceiver) {
|
||||
let (sender, receiver) = channels::unbounded();
|
||||
let queued = Arc::new(AtomicUsize::new(0));
|
||||
(
|
||||
ClientBuffer {
|
||||
sender,
|
||||
queued: queued.clone(),
|
||||
limit,
|
||||
},
|
||||
ClientBufferReceiver { receiver, queued },
|
||||
)
|
||||
}
|
||||
|
||||
impl ClientBuffer {
|
||||
fn try_send(&self, msg: ServerToClientMsg) -> Result<(), TrySendError<ServerToClientMsg>> {
|
||||
if self.queued.fetch_add(1, Ordering::AcqRel) >= self.limit {
|
||||
self.queued.fetch_sub(1, Ordering::AcqRel);
|
||||
return Err(TrySendError::Full(msg));
|
||||
}
|
||||
self.sender.send(msg).map_err(|err| {
|
||||
self.queued.fetch_sub(1, Ordering::AcqRel);
|
||||
TrySendError::Disconnected(err.0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientBufferReceiver {
|
||||
fn recv(&self) -> Option<ServerToClientMsg> {
|
||||
let msg = self.receiver.recv().ok()?;
|
||||
self.queued.fetch_sub(1, Ordering::AcqRel);
|
||||
Some(msg)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ClientSender {
|
||||
client_id: ClientId,
|
||||
client_buffer_sender: channels::Sender<ServerToClientMsg>,
|
||||
client_buffer_sender: ClientBuffer,
|
||||
}
|
||||
|
||||
impl ClientSender {
|
||||
@@ -229,10 +280,10 @@ impl ClientSender {
|
||||
// We, the zellij maintainers, have decided against an unbounded
|
||||
// queue for the time being because we want to prevent e.g. the whole session being killed
|
||||
// (by OOM-killers or some other mechanism) just because a single client doesn't respond.
|
||||
let (client_buffer_sender, client_buffer_receiver) = channels::bounded(5000);
|
||||
let (client_buffer_sender, client_buffer_receiver) = client_buffer(CLIENT_BUFFER_LIMIT);
|
||||
std::thread::spawn(move || {
|
||||
let err_context = || format!("failed to send message to client {client_id}");
|
||||
for msg in client_buffer_receiver.iter() {
|
||||
while let Some(msg) = client_buffer_receiver.recv() {
|
||||
sender
|
||||
.send_server_msg(msg)
|
||||
.with_context(err_context)
|
||||
@@ -284,13 +335,13 @@ pub(crate) struct NullAsyncReader;
|
||||
// used. See https://smallcultfollowing.com/babysteps/blog/2019/10/26/async-fn-in-traits-are-hard/
|
||||
#[async_trait]
|
||||
pub trait AsyncReader: Send + Sync {
|
||||
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error>;
|
||||
async fn read_chunk(&mut self, max: usize) -> Result<Vec<u8>, io::Error>;
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReader for NullAsyncReader {
|
||||
async fn read(&mut self, _buf: &mut [u8]) -> Result<usize, io::Error> {
|
||||
Ok(0) // EOF
|
||||
async fn read_chunk(&mut self, _max: usize) -> Result<Vec<u8>, io::Error> {
|
||||
Ok(Vec::new())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,18 +83,19 @@ impl RawFdAsyncReader {
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReader for RawFdAsyncReader {
|
||||
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
|
||||
async fn read_chunk(&mut self, max: usize) -> Result<Vec<u8>, io::Error> {
|
||||
let async_fd = self.get_async_fd()?;
|
||||
loop {
|
||||
let mut guard = async_fd.readable().await?;
|
||||
match guard.try_io(|inner| {
|
||||
let mut buf = vec![0u8; max];
|
||||
let fd = inner.get_ref().as_raw_fd();
|
||||
let ret =
|
||||
unsafe { libc::read(fd, buf.as_mut_ptr() as *mut libc::c_void, buf.len()) };
|
||||
if ret < 0 {
|
||||
Err(io::Error::last_os_error())
|
||||
} else {
|
||||
Ok(ret as usize)
|
||||
Ok(buf[..ret as usize].to_vec())
|
||||
}
|
||||
}) {
|
||||
Ok(result) => return result,
|
||||
@@ -469,7 +470,43 @@ mod tests {
|
||||
use super::*;
|
||||
use nix::fcntl::{fcntl, FcntlArg, OFlag};
|
||||
use nix::sys::termios;
|
||||
use std::io::Read;
|
||||
use std::io::{Read, Write};
|
||||
|
||||
#[test]
|
||||
fn raw_fd_reader_returns_exact_bytes_in_order() {
|
||||
let mut fds = [0 as libc::c_int; 2];
|
||||
assert_eq!(unsafe { libc::pipe(fds.as_mut_ptr()) }, 0);
|
||||
let mut writer = unsafe { File::from_raw_fd(fds[1]) };
|
||||
let mut reader = RawFdAsyncReader::new(fds[0]).expect("reader");
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
rt.block_on(async {
|
||||
writer.write_all(b"hello").unwrap();
|
||||
let chunk = reader.read_chunk(65536).await.unwrap();
|
||||
assert_eq!(chunk, b"hello");
|
||||
assert_eq!(chunk.capacity(), chunk.len());
|
||||
|
||||
writer.write_all(b"abc").unwrap();
|
||||
writer.write_all(b"def").unwrap();
|
||||
writer.write_all(b"ghij").unwrap();
|
||||
let mut collected = Vec::new();
|
||||
while collected.len() < 10 {
|
||||
let chunk = reader.read_chunk(4).await.unwrap();
|
||||
assert!(!chunk.is_empty() && chunk.len() <= 4);
|
||||
assert_eq!(chunk.capacity(), chunk.len());
|
||||
collected.extend_from_slice(&chunk);
|
||||
}
|
||||
assert_eq!(collected, b"abcdefghij");
|
||||
|
||||
drop(writer);
|
||||
let eof = reader.read_chunk(65536).await.unwrap();
|
||||
assert!(eof.is_empty());
|
||||
});
|
||||
}
|
||||
|
||||
/// Verify that `try_write_to_fd` writes as many bytes as the kernel will
|
||||
/// accept in one pass and returns a partial count (not an error) when the
|
||||
|
||||
@@ -13,7 +13,6 @@ use std::{
|
||||
},
|
||||
};
|
||||
|
||||
use tokio::io::AsyncReadExt;
|
||||
use tokio::net::windows::named_pipe::NamedPipeServer;
|
||||
|
||||
use windows_sys::Win32::Foundation::{CloseHandle, HANDLE, INVALID_HANDLE_VALUE, S_OK};
|
||||
@@ -98,16 +97,30 @@ impl ConPtyAsyncReader {
|
||||
|
||||
#[async_trait]
|
||||
impl AsyncReader for ConPtyAsyncReader {
|
||||
async fn read(&mut self, buf: &mut [u8]) -> Result<usize, io::Error> {
|
||||
async fn read_chunk(&mut self, max: usize) -> Result<Vec<u8>, io::Error> {
|
||||
let pipe = self.pipe()?;
|
||||
loop {
|
||||
pipe.readable().await?;
|
||||
let mut buf = vec![0u8; max];
|
||||
match pipe.try_read(&mut buf) {
|
||||
Ok(n) => return Ok(buf[..n].to_vec()),
|
||||
Err(e) if e.kind() == io::ErrorKind::WouldBlock => continue,
|
||||
Err(e) => return Err(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConPtyAsyncReader {
|
||||
fn pipe(&mut self) -> Result<&mut NamedPipeServer, io::Error> {
|
||||
if let Some(handle) = self.pending.take() {
|
||||
let pipe = unsafe { NamedPipeServer::from_raw_handle(handle.into_raw_handle()) }?;
|
||||
self.pipe = Some(pipe);
|
||||
}
|
||||
let pipe = self
|
||||
Ok(self
|
||||
.pipe
|
||||
.as_mut()
|
||||
.expect("ConPtyAsyncReader used after init");
|
||||
pipe.read(buf).await
|
||||
.expect("ConPtyAsyncReader used after init"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::rc::Rc;
|
||||
use std::time::Instant;
|
||||
use zellij_utils::{
|
||||
data::{ModeInfo, Style, Styling},
|
||||
data::{InputMode, ModeInfo, Style, Styling},
|
||||
errors::prelude::*,
|
||||
input::command::RunCommand,
|
||||
input::layout::{FloatingPaneLayout, Run, RunPluginOrAlias},
|
||||
@@ -44,7 +44,7 @@ pub struct FloatingPanes {
|
||||
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,
|
||||
default_mode: InputMode,
|
||||
style: Style,
|
||||
session_is_mirrored: bool,
|
||||
desired_pane_positions: HashMap<PaneId, PaneGeom>, // this represents the positions of panes the user moved with intention, rather than by resizing the terminal window
|
||||
@@ -77,7 +77,7 @@ impl FloatingPanes {
|
||||
fullscreen_covers_ui: Rc<RefCell<bool>>,
|
||||
pane_frame_style: PaneFrameStyle,
|
||||
session_is_mirrored: bool,
|
||||
default_mode_info: ModeInfo,
|
||||
default_mode: InputMode,
|
||||
style: Style,
|
||||
os_input: Box<dyn ServerOsApi>,
|
||||
senders: ThreadSenders,
|
||||
@@ -92,7 +92,7 @@ impl FloatingPanes {
|
||||
mode_info,
|
||||
character_cell_size,
|
||||
session_is_mirrored,
|
||||
default_mode_info,
|
||||
default_mode,
|
||||
style,
|
||||
desired_pane_positions: HashMap::new(),
|
||||
z_indices: vec![],
|
||||
@@ -608,8 +608,8 @@ impl FloatingPanes {
|
||||
.mode_info
|
||||
.borrow()
|
||||
.get(client_id)
|
||||
.unwrap_or(&self.default_mode_info)
|
||||
.mode;
|
||||
.map(|mode_info| mode_info.mode)
|
||||
.unwrap_or(self.default_mode);
|
||||
let is_floating = true;
|
||||
let skip_frame = no_ui_fullscreen
|
||||
|| (pane_is_regular_fullscreen
|
||||
|
||||
@@ -763,6 +763,40 @@ fn highlight_match<'t>(captures: ®ex::Captures<'t>) -> Option<regex::Match<'t
|
||||
captures.get(1).or_else(|| captures.get(0))
|
||||
}
|
||||
|
||||
fn highlight_matches<'r, 't>(
|
||||
regex: &'r regex::Regex,
|
||||
text: &'t str,
|
||||
) -> impl Iterator<Item = regex::Match<'t>> + 'r
|
||||
where
|
||||
't: 'r,
|
||||
{
|
||||
let mut pos = 0;
|
||||
std::iter::from_fn(move || {
|
||||
while pos <= text.len() {
|
||||
let captures = regex.captures_at(text, pos)?;
|
||||
let mat = highlight_match(&captures);
|
||||
let end = match (mat, captures.get(0)) {
|
||||
(Some(m), _) => m.end(),
|
||||
(None, Some(whole)) => whole.end(),
|
||||
(None, None) => return None,
|
||||
};
|
||||
pos = if end > pos {
|
||||
end
|
||||
} else {
|
||||
text[pos..]
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| pos + c.len_utf8())
|
||||
.unwrap_or(text.len() + 1)
|
||||
};
|
||||
if mat.is_some() {
|
||||
return mat;
|
||||
}
|
||||
}
|
||||
None
|
||||
})
|
||||
}
|
||||
|
||||
/// Check whether a (row, col) position falls within a display span.
|
||||
/// The span is inclusive at start and exclusive at end.
|
||||
fn position_in_span(
|
||||
@@ -2944,10 +2978,7 @@ impl Grid {
|
||||
)> = None;
|
||||
for (plugin_id, pattern_map) in &self.plugin_highlights {
|
||||
for (pattern, compiled) in pattern_map {
|
||||
for captures in compiled.regex.captures_iter(&logical_text) {
|
||||
let Some(mat) = highlight_match(&captures) else {
|
||||
continue;
|
||||
};
|
||||
for mat in highlight_matches(&compiled.regex, &logical_text) {
|
||||
if let Some((_sel, start_row, start_col, end_row, end_col)) =
|
||||
match_to_selection(&mat, &boundaries, &self.viewport)
|
||||
{
|
||||
@@ -3045,10 +3076,7 @@ impl Grid {
|
||||
if !compiled.on_hover || compiled.tooltip_text.is_none() {
|
||||
continue;
|
||||
}
|
||||
for captures in compiled.regex.captures_iter(&logical_text) {
|
||||
let Some(mat) = highlight_match(&captures) else {
|
||||
continue;
|
||||
};
|
||||
for mat in highlight_matches(&compiled.regex, &logical_text) {
|
||||
if let Some((_sel, start_row, start_col, end_row, end_col)) =
|
||||
match_to_selection(&mat, &boundaries, &self.viewport)
|
||||
{
|
||||
@@ -3104,10 +3132,7 @@ impl Grid {
|
||||
if !compiled.on_hover || !compiled.has_visual_effect() {
|
||||
continue;
|
||||
}
|
||||
for captures in compiled.regex.captures_iter(&logical_text) {
|
||||
let Some(mat) = highlight_match(&captures) else {
|
||||
continue;
|
||||
};
|
||||
for mat in highlight_matches(&compiled.regex, &logical_text) {
|
||||
if let Some((sel, start_row, start_col, end_row, end_col)) =
|
||||
match_to_selection(&mat, &boundaries, &self.viewport)
|
||||
{
|
||||
@@ -3141,10 +3166,7 @@ impl Grid {
|
||||
if compiled.on_hover || !compiled.has_visual_effect() {
|
||||
continue;
|
||||
}
|
||||
for captures in compiled.regex.captures_iter(&logical_text) {
|
||||
let Some(mat) = highlight_match(&captures) else {
|
||||
continue;
|
||||
};
|
||||
for mat in highlight_matches(&compiled.regex, &logical_text) {
|
||||
if let Some((sel, _, _, _, _)) =
|
||||
match_to_selection(&mat, &boundaries, &self.viewport)
|
||||
{
|
||||
|
||||
@@ -29,8 +29,8 @@ use crate::{
|
||||
use stacked_panes::StackedPanes;
|
||||
use zellij_utils::{
|
||||
data::{
|
||||
BorderStyle, BorderStyleOverride, Direction, LineStyle, ModeInfo, PaneInfo, Resize,
|
||||
ResizeStrategy, Style, Styling,
|
||||
BorderStyle, BorderStyleOverride, Direction, InputMode, LineStyle, ModeInfo, PaneInfo,
|
||||
Resize, ResizeStrategy, Style, Styling,
|
||||
},
|
||||
errors::prelude::*,
|
||||
input::{
|
||||
@@ -78,7 +78,7 @@ pub struct TiledPanes {
|
||||
character_cell_size: Rc<RefCell<Option<SizeInPixels>>>,
|
||||
stacked_resize: Rc<RefCell<bool>>,
|
||||
reserved_top_rows: Rc<RefCell<HashMap<PaneId, usize>>>,
|
||||
default_mode_info: ModeInfo,
|
||||
default_mode: InputMode,
|
||||
style: Style,
|
||||
session_is_mirrored: bool,
|
||||
active_panes: ActivePanes,
|
||||
@@ -109,7 +109,7 @@ impl TiledPanes {
|
||||
fullscreen_covers_ui: Rc<RefCell<bool>>,
|
||||
session_is_mirrored: bool,
|
||||
pane_frame_style: PaneFrameStyle,
|
||||
default_mode_info: ModeInfo,
|
||||
default_mode: InputMode,
|
||||
style: Style,
|
||||
os_api: Box<dyn ServerOsApi>,
|
||||
senders: ThreadSenders,
|
||||
@@ -125,7 +125,7 @@ impl TiledPanes {
|
||||
character_cell_size,
|
||||
stacked_resize,
|
||||
reserved_top_rows,
|
||||
default_mode_info,
|
||||
default_mode,
|
||||
style,
|
||||
session_is_mirrored,
|
||||
active_panes: ActivePanes::new(&os_api),
|
||||
@@ -1294,8 +1294,8 @@ impl TiledPanes {
|
||||
.mode_info
|
||||
.borrow()
|
||||
.get(client_id)
|
||||
.unwrap_or(&self.default_mode_info)
|
||||
.mode;
|
||||
.map(|mode_info| mode_info.mode)
|
||||
.unwrap_or(self.default_mode);
|
||||
let err_context =
|
||||
|| format!("failed to render tiled panes for client {client_id}");
|
||||
if let PaneId::Plugin(..) = kind {
|
||||
|
||||
@@ -5092,6 +5092,69 @@ fn plugin_highlight_at_wrapped_line() {
|
||||
assert_eq!(matched_string, "jklm");
|
||||
}
|
||||
|
||||
const COMBINED_ENTRIES_PATTERN: &str = r"(?:^|\s)((?:abc\.rs|b\.rs)(?:/[A-Za-z0-9_./\-+@%,#=~!\$\{\}\[\]]+)?(?::\d+(?::\d+)?)?)(?::|\s|$)";
|
||||
|
||||
#[test]
|
||||
fn combined_pattern_highlights_adjacent_entries_separated_by_single_space() {
|
||||
let mut grid = create_grid_with_content("abc.rs b.rs\n");
|
||||
let highlights = vec![create_highlight(
|
||||
COMBINED_ENTRIES_PATTERN,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
HighlightLayer::Hint,
|
||||
)];
|
||||
grid.set_plugin_regex_highlights(1, highlights, &Style::default());
|
||||
|
||||
let first = grid.plugin_highlight_at(&Position::new(0, 0)).unwrap();
|
||||
assert_eq!(first.2, "abc.rs");
|
||||
let second = grid.plugin_highlight_at(&Position::new(0, 8)).unwrap();
|
||||
assert_eq!(second.2, "b.rs");
|
||||
assert!(grid.plugin_highlight_at(&Position::new(0, 6)).is_none());
|
||||
assert_eq!(grid.compute_plugin_highlight_selections().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combined_pattern_highlights_entries_on_wrapped_line() {
|
||||
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
|
||||
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
|
||||
let mut grid = Grid::new(
|
||||
5,
|
||||
10,
|
||||
Rc::new(RefCell::new(Palette::default())),
|
||||
terminal_emulator_color_codes,
|
||||
Rc::new(RefCell::new(LinkHandler::new())),
|
||||
Rc::new(RefCell::new(None)),
|
||||
sixel_image_store,
|
||||
Rc::new(RefCell::new(KittyImageStore::default())),
|
||||
Style::default(),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
false,
|
||||
);
|
||||
let mut vte_parser = vte::Parser::new();
|
||||
vte_parser.advance(&mut grid, "12345 abc.rs b.rs".as_bytes());
|
||||
|
||||
let highlights = vec![create_highlight(
|
||||
COMBINED_ENTRIES_PATTERN,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
HighlightLayer::Hint,
|
||||
)];
|
||||
grid.set_plugin_regex_highlights(1, highlights, &Style::default());
|
||||
|
||||
let wrapped = grid.plugin_highlight_at(&Position::new(1, 0)).unwrap();
|
||||
assert_eq!(wrapped.2, "abc.rs");
|
||||
let after_wrap = grid.plugin_highlight_at(&Position::new(1, 4)).unwrap();
|
||||
assert_eq!(after_wrap.2, "b.rs");
|
||||
assert_eq!(grid.compute_plugin_highlight_selections().len(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hover_position_triggers_on_hover_highlight() {
|
||||
let mut grid = create_grid_with_content("hello link_text bar\n");
|
||||
|
||||
@@ -3,6 +3,7 @@ mod pipes;
|
||||
mod plugin_loader;
|
||||
mod plugin_map;
|
||||
mod plugin_worker;
|
||||
mod shared;
|
||||
mod wasm_bridge;
|
||||
mod watch_filesystem;
|
||||
mod zellij_exports;
|
||||
@@ -21,7 +22,7 @@ use crate::panes::PaneId;
|
||||
use crate::route::NotificationEnd;
|
||||
use crate::screen::ScreenInstruction;
|
||||
use crate::session_layout_metadata::SessionLayoutMetadata;
|
||||
use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction};
|
||||
use crate::{pty::PtyInstruction, thread_bus::Bus, ClientId, ServerInstruction, SharedKeybinds};
|
||||
use zellij_utils::data::PaneRenderReport;
|
||||
use zellij_utils::input::layout::TabLayoutInfo;
|
||||
|
||||
@@ -38,7 +39,6 @@ use zellij_utils::{
|
||||
input::{
|
||||
actions::Action,
|
||||
command::TerminalAction,
|
||||
keybinds::Keybinds,
|
||||
layout::{FloatingPaneLayout, Layout, Run, RunPlugin, RunPluginOrAlias, TiledPaneLayout},
|
||||
plugins::PluginAliases,
|
||||
},
|
||||
@@ -89,6 +89,7 @@ pub enum PluginInstruction {
|
||||
AddClient(ClientId),
|
||||
RemoveClient(ClientId),
|
||||
UpdatePluginTabIndices(Vec<(PluginId, usize)>),
|
||||
UpdateClientVisiblePlugins(HashMap<ClientId, HashSet<PluginId>>),
|
||||
NewTab(
|
||||
Option<PathBuf>,
|
||||
Option<TerminalAction>,
|
||||
@@ -185,7 +186,7 @@ pub enum PluginInstruction {
|
||||
UnblockCliPipes(Vec<PluginRenderAsset>),
|
||||
Reconfigure {
|
||||
client_id: ClientId,
|
||||
keybinds: Option<Keybinds>,
|
||||
keybinds: Option<SharedKeybinds>,
|
||||
default_mode: Option<InputMode>,
|
||||
default_shell: Option<TerminalAction>,
|
||||
layout_dir: Option<PathBuf>,
|
||||
@@ -238,6 +239,9 @@ impl From<&PluginInstruction> for PluginContext {
|
||||
PluginInstruction::AddClient(_) => PluginContext::AddClient,
|
||||
PluginInstruction::RemoveClient(_) => PluginContext::RemoveClient,
|
||||
PluginInstruction::UpdatePluginTabIndices(..) => PluginContext::UpdatePluginTabIndices,
|
||||
PluginInstruction::UpdateClientVisiblePlugins(..) => {
|
||||
PluginContext::UpdateClientVisiblePlugins
|
||||
},
|
||||
PluginInstruction::NewTab(..) => PluginContext::NewTab,
|
||||
PluginInstruction::OverrideLayout(..) => PluginContext::OverrideLayout,
|
||||
PluginInstruction::ApplyCachedEvents { .. } => PluginContext::ApplyCachedEvents,
|
||||
@@ -304,7 +308,7 @@ pub(crate) fn plugin_thread_main(
|
||||
default_shell: Option<TerminalAction>,
|
||||
plugin_aliases: PluginAliases,
|
||||
default_mode: InputMode,
|
||||
default_keybinds: Keybinds,
|
||||
default_keybinds: SharedKeybinds,
|
||||
background_plugins: HashSet<RunPluginOrAlias>,
|
||||
// the client id that started the session,
|
||||
// we need it here because the thread's own list of connected clients might not yet be updated
|
||||
@@ -515,6 +519,9 @@ pub(crate) fn plugin_thread_main(
|
||||
PluginInstruction::UpdatePluginTabIndices(tab_indices) => {
|
||||
wasm_bridge.update_plugin_tab_indices(tab_indices);
|
||||
},
|
||||
PluginInstruction::UpdateClientVisiblePlugins(visible_plugins) => {
|
||||
wasm_bridge.update_client_visible_plugins(visible_plugins);
|
||||
},
|
||||
PluginInstruction::NewTab(
|
||||
cwd,
|
||||
terminal_action,
|
||||
@@ -1353,6 +1360,13 @@ fn pipe_to_all_plugins(
|
||||
PipeMessage::new(pipe_source.clone(), name, payload, &args, is_private),
|
||||
));
|
||||
}
|
||||
for instance_id in wasm_bridge.shared_instance_ids() {
|
||||
pipe_messages.push((
|
||||
Some(instance_id),
|
||||
None,
|
||||
PipeMessage::new(pipe_source.clone(), name, payload, &args, is_private),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn pipe_to_specific_plugins(
|
||||
@@ -1433,13 +1447,15 @@ fn load_background_plugin(
|
||||
let run_plugin = run_plugin_or_alias.get_run_plugin();
|
||||
let size = Size::default();
|
||||
let skip_cache = false;
|
||||
match wasm_bridge.load_plugin(
|
||||
let is_background = true;
|
||||
match wasm_bridge.load_plugin_with_kind(
|
||||
&run_plugin,
|
||||
None,
|
||||
size,
|
||||
cwd.clone(),
|
||||
skip_cache,
|
||||
Some(client_id),
|
||||
is_background,
|
||||
) {
|
||||
Ok((plugin_id, client_id)) => {
|
||||
let should_float = None;
|
||||
@@ -1475,3 +1491,7 @@ const EXIT_TIMEOUT: Duration = Duration::from_secs(3);
|
||||
#[path = "./unit/plugin_tests.rs"]
|
||||
#[cfg(test)]
|
||||
mod plugin_tests;
|
||||
|
||||
#[path = "./unit/shared_plugin_tests.rs"]
|
||||
#[cfg(test)]
|
||||
mod shared_plugin_tests;
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::plugins::plugin_map::{
|
||||
PluginEnv, PluginMap, PluginMetadata, RunningPlugin, VecDequeInputStream, WriteOutputStream,
|
||||
PluginEnv, PluginMap, PluginMetadata, RunningPlugin, SharedEnv, VecDequeInputStream,
|
||||
WriteOutputStream,
|
||||
};
|
||||
use crate::plugins::plugin_worker::{plugin_worker, RunningWorker};
|
||||
use crate::plugins::wasm_bridge::{LoadingContext, PluginCache};
|
||||
@@ -20,13 +21,13 @@ use wasmi_wasi::WasiCtx;
|
||||
|
||||
use crate::{
|
||||
logging_pipe::LoggingPipe, thread_bus::ThreadSenders,
|
||||
ui::loading_indication::LoadingIndication, ClientId,
|
||||
ui::loading_indication::LoadingIndication, ClientId, SharedKeybinds,
|
||||
};
|
||||
|
||||
use zellij_utils::plugin_api::action::ProtobufPluginConfiguration;
|
||||
use zellij_utils::{
|
||||
consts::ZELLIJ_TMP_DIR, data::InputMode, errors::prelude::*, input::command::TerminalAction,
|
||||
input::keybinds::Keybinds, input::plugins::PluginConfig, pane_size::Size,
|
||||
input::plugins::PluginConfig, pane_size::Size,
|
||||
};
|
||||
|
||||
/// Open a directory as a `File` handle for WASI pre-opening.
|
||||
@@ -73,7 +74,7 @@ pub struct PluginLoader<'a> {
|
||||
default_shell: Option<TerminalAction>,
|
||||
layout_dir: Option<PathBuf>,
|
||||
default_mode: InputMode,
|
||||
keybinds: Keybinds,
|
||||
keybinds: SharedKeybinds,
|
||||
plugin_dir: PathBuf,
|
||||
size: Size,
|
||||
loading_indication: LoadingIndication,
|
||||
@@ -140,10 +141,21 @@ impl<'a> PluginLoader<'a> {
|
||||
.or_else(|_e| self.interpret_module())?
|
||||
};
|
||||
let (store, instance) = self.create_plugin_environment(module)?;
|
||||
self.load_plugin_instance(store, &instance)?;
|
||||
self.load_plugin_instance(store, &instance, false)?;
|
||||
self.clone_instance_for_other_clients()?;
|
||||
Ok(())
|
||||
}
|
||||
pub fn start_shared_plugin(&mut self) -> Result<Arc<Mutex<RunningPlugin>>> {
|
||||
let module = if self.skip_cache {
|
||||
self.interpret_module()?
|
||||
} else {
|
||||
self.load_module_from_memory()
|
||||
.or_else(|_e| self.interpret_module())?
|
||||
};
|
||||
let (mut store, instance) = self.create_plugin_environment(module)?;
|
||||
store.data_mut().shared = Some(SharedEnv::default());
|
||||
self.load_plugin_instance(store, &instance, true)
|
||||
}
|
||||
fn record_plugin_metadata(&mut self) {
|
||||
self.plugin_map.insert_metadata(
|
||||
self.plugin_id,
|
||||
@@ -183,7 +195,8 @@ impl<'a> PluginLoader<'a> {
|
||||
&mut self,
|
||||
mut store: Store<PluginEnv>,
|
||||
instance: &Instance,
|
||||
) -> Result<()> {
|
||||
shared: bool,
|
||||
) -> Result<Arc<Mutex<RunningPlugin>>> {
|
||||
let err_context = || format!("failed to load plugin from instance {instance:#?}");
|
||||
let main_user_instance = instance.clone();
|
||||
let start_function = instance
|
||||
@@ -220,13 +233,18 @@ impl<'a> PluginLoader<'a> {
|
||||
self.size.rows,
|
||||
self.size.cols,
|
||||
)));
|
||||
self.plugin_map.insert(
|
||||
self.plugin_id,
|
||||
self.client_id,
|
||||
plugin.clone(),
|
||||
subscriptions,
|
||||
workers,
|
||||
);
|
||||
if shared {
|
||||
self.plugin_map
|
||||
.insert_shared(self.plugin_id, (plugin.clone(), subscriptions, workers));
|
||||
} else {
|
||||
self.plugin_map.insert(
|
||||
self.plugin_id,
|
||||
self.client_id,
|
||||
plugin.clone(),
|
||||
subscriptions,
|
||||
workers,
|
||||
);
|
||||
}
|
||||
|
||||
start_function
|
||||
.call(&mut plugin.lock().unwrap().store, ())
|
||||
@@ -245,7 +263,7 @@ impl<'a> PluginLoader<'a> {
|
||||
.call(&mut plugin.lock().unwrap().store, ())
|
||||
.with_context(err_context)?;
|
||||
|
||||
Ok(())
|
||||
Ok(plugin)
|
||||
}
|
||||
pub fn create_plugin_environment(
|
||||
&self,
|
||||
@@ -294,6 +312,7 @@ impl<'a> PluginLoader<'a> {
|
||||
stdin_pipe,
|
||||
stdout_pipe,
|
||||
store_limits: create_optimized_store_limits(),
|
||||
shared: None,
|
||||
};
|
||||
let mut store = Store::new(&self.engine, plugin_env);
|
||||
|
||||
@@ -405,6 +424,7 @@ impl<'a> PluginLoader<'a> {
|
||||
stdin_pipe,
|
||||
stdout_pipe,
|
||||
store_limits: create_optimized_store_limits(),
|
||||
shared: None,
|
||||
};
|
||||
let mut store = Store::new(&self.engine, plugin_env);
|
||||
|
||||
|
||||
@@ -2,21 +2,20 @@ use crate::plugins::plugin_worker::MessageToWorker;
|
||||
use crate::plugins::PluginId;
|
||||
use std::io::Write;
|
||||
use std::{
|
||||
collections::{BTreeMap, HashMap, HashSet, VecDeque},
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet, VecDeque},
|
||||
path::PathBuf,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use wasmi::{Instance, Store, StoreLimits};
|
||||
use wasmi_wasi::WasiCtx;
|
||||
|
||||
use crate::{thread_bus::ThreadSenders, ClientId};
|
||||
use crate::{thread_bus::ThreadSenders, ClientId, SharedKeybinds};
|
||||
|
||||
use tokio::sync::mpsc::UnboundedSender;
|
||||
use zellij_utils::{
|
||||
data::EventType,
|
||||
data::InputMode,
|
||||
data::{Event, EventType, RenderResponse, SlotKind},
|
||||
input::command::TerminalAction,
|
||||
input::keybinds::Keybinds,
|
||||
input::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation},
|
||||
input::plugins::PluginConfig,
|
||||
};
|
||||
@@ -49,6 +48,7 @@ pub struct PluginMetadata {
|
||||
pub struct PluginMap {
|
||||
plugin_assets: HashMap<PluginKey, PluginAssets>,
|
||||
plugin_metadata: HashMap<PluginId, PluginMetadata>,
|
||||
shared_assets: HashMap<PluginId, PluginAssets>,
|
||||
}
|
||||
|
||||
impl PluginMap {
|
||||
@@ -77,6 +77,25 @@ impl PluginMap {
|
||||
}
|
||||
removed
|
||||
}
|
||||
pub fn insert_shared(&mut self, instance_id: PluginId, assets: PluginAssets) {
|
||||
self.shared_assets.insert(instance_id, assets);
|
||||
}
|
||||
pub fn remove_shared(&mut self, instance_id: PluginId) -> Option<PluginAssets> {
|
||||
self.shared_assets.remove(&instance_id)
|
||||
}
|
||||
pub fn shared_running_plugin(
|
||||
&self,
|
||||
instance_id: PluginId,
|
||||
) -> Option<(Arc<Mutex<RunningPlugin>>, Arc<Mutex<Subscriptions>>)> {
|
||||
self.shared_assets
|
||||
.get(&instance_id)
|
||||
.map(|(running_plugin, subscriptions, _)| {
|
||||
(running_plugin.clone(), subscriptions.clone())
|
||||
})
|
||||
}
|
||||
pub fn remove_metadata(&mut self, plugin_id: PluginId) {
|
||||
self.plugin_metadata.remove(&plugin_id);
|
||||
}
|
||||
pub fn insert_metadata(&mut self, plugin_id: PluginId, metadata: PluginMetadata) {
|
||||
self.plugin_metadata.insert(plugin_id, metadata);
|
||||
}
|
||||
@@ -168,6 +187,9 @@ impl PluginMap {
|
||||
client_id: ClientId,
|
||||
worker_name: &str,
|
||||
) -> Option<UnboundedSender<MessageToWorker>> {
|
||||
if let Some((_, _, workers)) = self.shared_assets.get(&plugin_id) {
|
||||
return workers.get(&format!("{}_worker", worker_name)).cloned();
|
||||
}
|
||||
self.plugin_assets
|
||||
.iter()
|
||||
.find(|((p_id, c_id), _)| p_id == &plugin_id && c_id == &client_id)
|
||||
@@ -311,9 +333,80 @@ pub struct PluginEnv {
|
||||
pub subscriptions: Arc<Mutex<Subscriptions>>,
|
||||
pub stdin_pipe: Arc<Mutex<VecDeque<u8>>>,
|
||||
pub stdout_pipe: Arc<Mutex<VecDeque<u8>>>,
|
||||
pub keybinds: Keybinds,
|
||||
pub keybinds: SharedKeybinds,
|
||||
pub intercepting_key_presses: bool,
|
||||
pub store_limits: StoreLimits,
|
||||
pub shared: Option<SharedEnv>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SharedSlot {
|
||||
pub kind: SlotKind,
|
||||
pub configuration: BTreeMap<String, String>,
|
||||
pub rows: usize,
|
||||
pub columns: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct SharedEnv {
|
||||
pub current_slot: Option<PluginId>,
|
||||
pub current_client: Option<ClientId>,
|
||||
pub self_commands_allowed: bool,
|
||||
pub slots: BTreeMap<PluginId, SharedSlot>,
|
||||
pub clients: BTreeSet<ClientId>,
|
||||
pub visible_slots: HashMap<ClientId, HashSet<PluginId>>,
|
||||
pub last_events: HashMap<(EventType, Option<ClientId>), (Event, Option<SharedKeybinds>)>,
|
||||
}
|
||||
|
||||
impl SharedEnv {
|
||||
pub fn first_pane_slot(&self) -> Option<PluginId> {
|
||||
self.slots
|
||||
.iter()
|
||||
.find(|(_, slot)| slot.kind == SlotKind::Pane)
|
||||
.map(|(id, _)| *id)
|
||||
}
|
||||
pub fn pane_slot_ids(&self) -> Vec<PluginId> {
|
||||
self.slots
|
||||
.iter()
|
||||
.filter(|(_, slot)| slot.kind == SlotKind::Pane)
|
||||
.map(|(id, _)| *id)
|
||||
.collect()
|
||||
}
|
||||
pub fn client_can_see(&self, client_id: ClientId, slot_id: PluginId) -> bool {
|
||||
match self.visible_slots.get(&client_id) {
|
||||
Some(visible) => visible.contains(&slot_id),
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
pub fn renderable_slot(&self, slot_id: PluginId) -> Option<(usize, usize)> {
|
||||
self.slots.get(&slot_id).and_then(|slot| {
|
||||
if slot.kind == SlotKind::Pane && slot.rows > 0 && slot.columns > 0 {
|
||||
Some((slot.rows, slot.columns))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
pub fn render_targets(&self, render: &RenderResponse) -> Vec<(PluginId, ClientId)> {
|
||||
let mut targets = vec![];
|
||||
for client_id in self.clients.iter() {
|
||||
for slot_id in self.slots.keys() {
|
||||
let wanted = match render {
|
||||
RenderResponse::Nothing => false,
|
||||
RenderResponse::All => true,
|
||||
RenderResponse::Client(c) => c == client_id,
|
||||
RenderResponse::Slots(slot_ids) => slot_ids.contains(slot_id),
|
||||
};
|
||||
if wanted
|
||||
&& self.renderable_slot(*slot_id).is_some()
|
||||
&& self.client_can_see(*client_id, *slot_id)
|
||||
{
|
||||
targets.push((*slot_id, *client_id));
|
||||
}
|
||||
}
|
||||
}
|
||||
targets
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
@@ -412,7 +505,7 @@ impl RunningPlugin {
|
||||
false
|
||||
}
|
||||
}
|
||||
pub fn update_keybinds(&mut self, keybinds: Keybinds) {
|
||||
pub fn update_keybinds(&mut self, keybinds: SharedKeybinds) {
|
||||
self.store.data_mut().keybinds = keybinds;
|
||||
}
|
||||
pub fn update_default_mode(&mut self, default_mode: InputMode) {
|
||||
|
||||
@@ -0,0 +1,913 @@
|
||||
use super::PluginId;
|
||||
use crate::background_jobs::BackgroundJob;
|
||||
use crate::plugins::pipes::{pipes_to_block_or_unblock, PipeStateChange};
|
||||
use crate::plugins::plugin_loader::PluginLoader;
|
||||
use crate::plugins::plugin_map::{PluginMap, RunningPlugin, SharedSlot, Subscriptions};
|
||||
use crate::plugins::plugin_worker::MessageToWorker;
|
||||
use crate::plugins::wasm_bridge::{
|
||||
check_event_permission, handle_plugin_crash, handle_plugin_loading_failure,
|
||||
handle_plugin_successful_loading, LoadingContext, PluginCache, PluginRenderAsset,
|
||||
};
|
||||
use crate::plugins::zellij_exports::{wasi_read_bytes, wasi_read_string, wasi_write_object};
|
||||
use crate::plugins::PluginInstruction;
|
||||
use crate::screen::ScreenInstruction;
|
||||
use crate::ui::loading_indication::LoadingIndication;
|
||||
use crate::{thread_bus::ThreadSenders, ClientId, SharedKeybinds};
|
||||
use prost::Message;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::str::FromStr;
|
||||
use std::sync::{Arc, Mutex};
|
||||
use wasmi::Engine;
|
||||
use zellij_utils::data::KeybindsVec;
|
||||
use zellij_utils::data::{
|
||||
Event, EventContext, EventType, PermissionStatus, PipeMessage, PipeSource, RenderResponse, Slot,
|
||||
};
|
||||
use zellij_utils::errors::prelude::*;
|
||||
use zellij_utils::input::command::TerminalAction;
|
||||
use zellij_utils::input::layout::RunPluginLocation;
|
||||
use zellij_utils::plugin_api::event::{event_to_protobuf_with_keybinds, ProtobufEvent};
|
||||
use zellij_utils::plugin_api::pipe_message::ProtobufPipeMessage;
|
||||
use zellij_utils::plugin_api::shared_plugin::{
|
||||
ProtobufEventContext, ProtobufRenderRequest, ProtobufSlot,
|
||||
};
|
||||
|
||||
pub const SHARED_PIPE_CLIENT: ClientId = ClientId::MAX;
|
||||
pub const SHARED_MARKER_EXPORT: &str = "zellij_shared_plugin";
|
||||
|
||||
pub type SharedKey = (RunPluginLocation, Option<String>);
|
||||
|
||||
pub type SharedEventBatchEntry = (Event, EventContext, Option<SharedKeybinds>);
|
||||
|
||||
fn read_leb_u32(bytes: &[u8], position: &mut usize) -> Option<u32> {
|
||||
let mut result: u32 = 0;
|
||||
let mut shift = 0;
|
||||
loop {
|
||||
let byte = *bytes.get(*position)?;
|
||||
*position += 1;
|
||||
result |= ((byte & 0x7f) as u32).checked_shl(shift)?;
|
||||
if byte & 0x80 == 0 {
|
||||
return Some(result);
|
||||
}
|
||||
shift += 7;
|
||||
if shift > 28 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn wasm_module_exports_function(bytes: &[u8], name: &str) -> bool {
|
||||
if bytes.len() < 8 || &bytes[0..4] != b"\0asm" {
|
||||
return false;
|
||||
}
|
||||
let mut position = 8;
|
||||
while position < bytes.len() {
|
||||
let section_id = bytes[position];
|
||||
position += 1;
|
||||
let Some(section_size) = read_leb_u32(bytes, &mut position) else {
|
||||
return false;
|
||||
};
|
||||
let section_end = position + section_size as usize;
|
||||
if section_end > bytes.len() {
|
||||
return false;
|
||||
}
|
||||
if section_id == 7 {
|
||||
let mut cursor = position;
|
||||
let Some(count) = read_leb_u32(bytes, &mut cursor) else {
|
||||
return false;
|
||||
};
|
||||
for _ in 0..count {
|
||||
let Some(name_len) = read_leb_u32(bytes, &mut cursor) else {
|
||||
return false;
|
||||
};
|
||||
let name_end = cursor + name_len as usize;
|
||||
if name_end > section_end {
|
||||
return false;
|
||||
}
|
||||
let export_name = &bytes[cursor..name_end];
|
||||
cursor = name_end;
|
||||
let Some(kind) = bytes.get(cursor).copied() else {
|
||||
return false;
|
||||
};
|
||||
cursor += 1;
|
||||
if read_leb_u32(bytes, &mut cursor).is_none() {
|
||||
return false;
|
||||
}
|
||||
if kind == 0 && export_name == name.as_bytes() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
position = section_end;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn set_call_context(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
slot_id: Option<PluginId>,
|
||||
client_id: Option<ClientId>,
|
||||
self_commands_allowed: bool,
|
||||
) {
|
||||
if let Some(shared) = running_plugin.store.data_mut().shared.as_mut() {
|
||||
shared.current_slot = slot_id;
|
||||
shared.current_client = client_id;
|
||||
shared.self_commands_allowed = self_commands_allowed;
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_call_context(running_plugin: &mut RunningPlugin) {
|
||||
set_call_context(running_plugin, None, None, false);
|
||||
}
|
||||
|
||||
fn call_with_i32(running_plugin: &mut RunningPlugin, name: &str, value: i32) -> Result<()> {
|
||||
let instance = running_plugin.instance.clone();
|
||||
instance
|
||||
.get_typed_func::<i32, ()>(&mut running_plugin.store, name)
|
||||
.and_then(|function| function.call(&mut running_plugin.store, value))
|
||||
.map_err(|e| anyhow!(e))
|
||||
.with_context(|| format!("failed to call {} on shared plugin", name))
|
||||
}
|
||||
|
||||
fn call_without_args(running_plugin: &mut RunningPlugin, name: &str) -> Result<()> {
|
||||
let instance = running_plugin.instance.clone();
|
||||
instance
|
||||
.get_typed_func::<(), ()>(&mut running_plugin.store, name)
|
||||
.and_then(|function| function.call(&mut running_plugin.store, ()))
|
||||
.map_err(|e| anyhow!(e))
|
||||
.with_context(|| format!("failed to call {} on shared plugin", name))
|
||||
}
|
||||
|
||||
fn call_returning_i32(running_plugin: &mut RunningPlugin, name: &str) -> Result<i32> {
|
||||
let instance = running_plugin.instance.clone();
|
||||
instance
|
||||
.get_typed_func::<(), i32>(&mut running_plugin.store, name)
|
||||
.and_then(|function| function.call(&mut running_plugin.store, ()))
|
||||
.map_err(|e| anyhow!(e))
|
||||
.with_context(|| format!("failed to call {} on shared plugin", name))
|
||||
}
|
||||
|
||||
pub fn pane_slot_ids(running_plugin: &RunningPlugin) -> Vec<PluginId> {
|
||||
running_plugin
|
||||
.store
|
||||
.data()
|
||||
.shared
|
||||
.as_ref()
|
||||
.map(|shared| shared.pane_slot_ids())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn call_client_connected(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
let is_new = running_plugin
|
||||
.store
|
||||
.data_mut()
|
||||
.shared
|
||||
.as_mut()
|
||||
.map(|shared| shared.clients.insert(client_id))
|
||||
.unwrap_or(false);
|
||||
if !is_new {
|
||||
return Ok(());
|
||||
}
|
||||
set_call_context(running_plugin, None, Some(client_id), false);
|
||||
let result = call_with_i32(running_plugin, "client_connected", client_id as i32);
|
||||
clear_call_context(running_plugin);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn call_client_disconnected(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
let was_connected = running_plugin
|
||||
.store
|
||||
.data_mut()
|
||||
.shared
|
||||
.as_mut()
|
||||
.map(|shared| {
|
||||
shared.visible_slots.remove(&client_id);
|
||||
shared
|
||||
.last_events
|
||||
.retain(|(_, event_client), _| *event_client != Some(client_id));
|
||||
shared.clients.remove(&client_id)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if !was_connected {
|
||||
return Ok(());
|
||||
}
|
||||
call_with_i32(running_plugin, "client_disconnected", client_id as i32)
|
||||
}
|
||||
|
||||
pub fn call_slot_added(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
slot_id: PluginId,
|
||||
slot: SharedSlot,
|
||||
) -> Result<()> {
|
||||
if let Some(shared) = running_plugin.store.data_mut().shared.as_mut() {
|
||||
shared.slots.insert(slot_id, slot.clone());
|
||||
}
|
||||
let protobuf_slot: ProtobufSlot = Slot {
|
||||
id: slot_id,
|
||||
kind: slot.kind,
|
||||
configuration: slot.configuration,
|
||||
}
|
||||
.into();
|
||||
wasi_write_object(running_plugin.store.data(), &protobuf_slot.encode_to_vec())?;
|
||||
set_call_context(running_plugin, Some(slot_id), None, true);
|
||||
let result = call_without_args(running_plugin, "slot_added");
|
||||
clear_call_context(running_plugin);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn call_slot_removed(running_plugin: &mut RunningPlugin, slot_id: PluginId) -> Result<bool> {
|
||||
let slots_left = match running_plugin.store.data_mut().shared.as_mut() {
|
||||
Some(shared) => {
|
||||
if shared.slots.remove(&slot_id).is_none() {
|
||||
return Ok(!shared.slots.is_empty());
|
||||
}
|
||||
for visible in shared.visible_slots.values_mut() {
|
||||
visible.remove(&slot_id);
|
||||
}
|
||||
!shared.slots.is_empty()
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
call_with_i32(running_plugin, "slot_removed", slot_id as i32)?;
|
||||
Ok(slots_left)
|
||||
}
|
||||
|
||||
fn read_render_request(running_plugin: &RunningPlugin) -> RenderResponse {
|
||||
match wasi_read_bytes(running_plugin.store.data())
|
||||
.and_then(|bytes| ProtobufRenderRequest::decode(bytes.as_slice()).map_err(|e| anyhow!(e)))
|
||||
{
|
||||
Ok(request) => RenderResponse::from(request),
|
||||
Err(e) => {
|
||||
log::error!("Failed to read render request from shared plugin: {:?}", e);
|
||||
RenderResponse::All
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn render_from_return_value(running_plugin: &RunningPlugin, value: i32) -> RenderResponse {
|
||||
match value {
|
||||
0 => RenderResponse::Nothing,
|
||||
1 => RenderResponse::All,
|
||||
_ => read_render_request(running_plugin),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn call_update(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
event: &Event,
|
||||
keybinds: Option<&KeybindsVec>,
|
||||
context: EventContext,
|
||||
) -> Result<RenderResponse> {
|
||||
let protobuf_context: ProtobufEventContext = context.into();
|
||||
let protobuf_event: ProtobufEvent = event_to_protobuf_with_keybinds(event.clone(), keybinds)
|
||||
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
|
||||
wasi_write_object(
|
||||
running_plugin.store.data(),
|
||||
&protobuf_context.encode_to_vec(),
|
||||
)?;
|
||||
wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec())?;
|
||||
set_call_context(running_plugin, context.slot_id, context.client_id, false);
|
||||
let result = call_returning_i32(running_plugin, "shared_update");
|
||||
clear_call_context(running_plugin);
|
||||
Ok(render_from_return_value(running_plugin, result?))
|
||||
}
|
||||
|
||||
pub fn call_pipe(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
pipe_message: &PipeMessage,
|
||||
context: EventContext,
|
||||
) -> Result<RenderResponse> {
|
||||
let protobuf_context: ProtobufEventContext = context.into();
|
||||
let protobuf_pipe_message: ProtobufPipeMessage = pipe_message
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
|
||||
wasi_write_object(
|
||||
running_plugin.store.data(),
|
||||
&protobuf_context.encode_to_vec(),
|
||||
)?;
|
||||
wasi_write_object(
|
||||
running_plugin.store.data(),
|
||||
&protobuf_pipe_message.encode_to_vec(),
|
||||
)?;
|
||||
set_call_context(running_plugin, context.slot_id, context.client_id, false);
|
||||
let result = call_returning_i32(running_plugin, "shared_pipe");
|
||||
clear_call_context(running_plugin);
|
||||
Ok(render_from_return_value(running_plugin, result?))
|
||||
}
|
||||
|
||||
pub fn render_targets(
|
||||
running_plugin: &mut RunningPlugin,
|
||||
targets: &[(PluginId, ClientId)],
|
||||
) -> Result<Vec<PluginRenderAsset>> {
|
||||
let mut assets = vec![];
|
||||
for (slot_id, client_id) in targets {
|
||||
let size = running_plugin
|
||||
.store
|
||||
.data()
|
||||
.shared
|
||||
.as_ref()
|
||||
.and_then(|shared| shared.renderable_slot(*slot_id));
|
||||
let Some((rows, columns)) = size else {
|
||||
continue;
|
||||
};
|
||||
set_call_context(running_plugin, Some(*slot_id), Some(*client_id), true);
|
||||
let instance = running_plugin.instance.clone();
|
||||
let result = instance
|
||||
.get_typed_func::<(i32, i32, i32, i32), ()>(&mut running_plugin.store, "shared_render")
|
||||
.and_then(|render| {
|
||||
render.call(
|
||||
&mut running_plugin.store,
|
||||
(
|
||||
rows as i32,
|
||||
columns as i32,
|
||||
*slot_id as i32,
|
||||
*client_id as i32,
|
||||
),
|
||||
)
|
||||
})
|
||||
.map_err(|e| anyhow!(e));
|
||||
clear_call_context(running_plugin);
|
||||
result.with_context(|| format!("failed to render shared plugin slot {}", slot_id))?;
|
||||
let rendered = wasi_read_string(running_plugin.store.data())?;
|
||||
assets.push(PluginRenderAsset::new(
|
||||
*slot_id,
|
||||
*client_id,
|
||||
rendered.into_bytes(),
|
||||
));
|
||||
}
|
||||
Ok(assets)
|
||||
}
|
||||
|
||||
fn targets_for(
|
||||
running_plugin: &RunningPlugin,
|
||||
render: &RenderResponse,
|
||||
) -> Vec<(PluginId, ClientId)> {
|
||||
running_plugin
|
||||
.store
|
||||
.data()
|
||||
.shared
|
||||
.as_ref()
|
||||
.map(|shared| shared.render_targets(render))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn render_and_send(
|
||||
senders: &ThreadSenders,
|
||||
running_plugin: &mut RunningPlugin,
|
||||
targets: Vec<(PluginId, ClientId)>,
|
||||
) -> Result<()> {
|
||||
if targets.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let assets = render_targets(running_plugin, &targets)?;
|
||||
if !assets.is_empty() {
|
||||
let _ = senders.send_to_screen(ScreenInstruction::PluginBytes(assets));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn render_request(
|
||||
senders: &ThreadSenders,
|
||||
running_plugin: &mut RunningPlugin,
|
||||
render: RenderResponse,
|
||||
) -> Result<()> {
|
||||
let targets = targets_for(running_plugin, &render);
|
||||
render_and_send(senders, running_plugin, targets)
|
||||
}
|
||||
|
||||
fn report_error(senders: &ThreadSenders, pane_slots: Vec<PluginId>, error: anyhow::Error) {
|
||||
log::error!("{:?}", error);
|
||||
let message = format!("{:?}", error).replace("\n", "\n\r");
|
||||
for slot_id in pane_slots {
|
||||
handle_plugin_crash(slot_id, message.clone(), senders.clone());
|
||||
}
|
||||
}
|
||||
|
||||
fn send_pipe_changes(
|
||||
senders: &ThreadSenders,
|
||||
running_plugin: &mut RunningPlugin,
|
||||
instance_id: PluginId,
|
||||
current_pipe: Option<&PipeSource>,
|
||||
) {
|
||||
let changes = pipes_to_block_or_unblock(running_plugin, current_pipe);
|
||||
if !changes.is_empty() {
|
||||
let asset =
|
||||
PluginRenderAsset::new(instance_id, SHARED_PIPE_CLIENT, vec![]).with_pipes(changes);
|
||||
let _ = senders.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![asset]));
|
||||
}
|
||||
}
|
||||
|
||||
fn release_pipe(senders: &ThreadSenders, instance_id: PluginId, pipe_message: &PipeMessage) {
|
||||
if let PipeSource::Cli(pipe_id) = &pipe_message.source {
|
||||
let mut changes = HashMap::new();
|
||||
changes.insert(pipe_id.to_owned(), PipeStateChange::NoChange);
|
||||
let asset =
|
||||
PluginRenderAsset::new(instance_id, SHARED_PIPE_CLIENT, vec![]).with_pipes(changes);
|
||||
let _ = senders.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![asset]));
|
||||
}
|
||||
}
|
||||
|
||||
fn lookup(
|
||||
plugin_map: &Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
) -> Option<(Arc<Mutex<RunningPlugin>>, Arc<Mutex<Subscriptions>>)> {
|
||||
plugin_map
|
||||
.lock()
|
||||
.unwrap()
|
||||
.shared_running_plugin(instance_id)
|
||||
}
|
||||
|
||||
fn is_deduplicated_event(event_type: EventType) -> bool {
|
||||
matches!(
|
||||
event_type,
|
||||
EventType::ModeUpdate | EventType::TabUpdate | EventType::PaneUpdate
|
||||
)
|
||||
}
|
||||
|
||||
pub fn apply_events_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
events: Vec<SharedEventBatchEntry>,
|
||||
) {
|
||||
let Some((running_plugin, subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
return;
|
||||
};
|
||||
let subscriptions = subscriptions.lock().unwrap().clone();
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let mut render = RenderResponse::Nothing;
|
||||
let strip_keybinds = subscriptions.contains(&EventType::InitialKeybinds);
|
||||
for (mut event, context, mut keybinds) in events {
|
||||
let Ok(event_type) = EventType::from_str(&event.to_string()) else {
|
||||
continue;
|
||||
};
|
||||
if let Event::ModeUpdate(mode_info) = &mut event {
|
||||
mode_info.keybinds = vec![];
|
||||
}
|
||||
if strip_keybinds {
|
||||
keybinds = None;
|
||||
}
|
||||
if !subscriptions.contains(&event_type) && event_type != EventType::PermissionRequestResult
|
||||
{
|
||||
continue;
|
||||
}
|
||||
if is_deduplicated_event(event_type) {
|
||||
if let Some(shared) = running_plugin.store.data_mut().shared.as_mut() {
|
||||
let key = (event_type, context.client_id);
|
||||
let already_delivered = shared
|
||||
.last_events
|
||||
.get(&key)
|
||||
.map(|(last_event, last_keybinds)| {
|
||||
last_event == &event && last_keybinds == &keybinds
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if already_delivered {
|
||||
continue;
|
||||
}
|
||||
shared
|
||||
.last_events
|
||||
.insert(key, (event.clone(), keybinds.clone()));
|
||||
}
|
||||
}
|
||||
match check_event_permission(running_plugin.store.data(), &event) {
|
||||
(PermissionStatus::Granted, _) => {},
|
||||
(PermissionStatus::Denied, permission) => {
|
||||
log::error!(
|
||||
"Shared plugin '{}' permission '{}' is not allowed - Event '{:?}' denied",
|
||||
instance_id,
|
||||
permission
|
||||
.map(|p| p.to_string())
|
||||
.unwrap_or("UNKNOWN".to_owned()),
|
||||
event_type
|
||||
);
|
||||
continue;
|
||||
},
|
||||
}
|
||||
match call_update(&mut running_plugin, &event, keybinds.as_deref(), context) {
|
||||
Ok(requested) => {
|
||||
render = render.merge(requested);
|
||||
if event_type == EventType::PermissionRequestResult {
|
||||
render = render.merge(RenderResponse::All);
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
send_pipe_changes(&senders, &mut running_plugin, instance_id, None);
|
||||
if let Err(e) = render_request(&senders, &mut running_plugin, render) {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_pipes_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
messages: Vec<(PipeMessage, EventContext)>,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
for (pipe_message, _) in &messages {
|
||||
release_pipe(&senders, instance_id, pipe_message);
|
||||
}
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let mut render = RenderResponse::Nothing;
|
||||
let mut messages = messages.into_iter();
|
||||
while let Some((pipe_message, context)) = messages.next() {
|
||||
match call_pipe(&mut running_plugin, &pipe_message, context) {
|
||||
Ok(requested) => {
|
||||
render = render.merge(requested);
|
||||
send_pipe_changes(
|
||||
&senders,
|
||||
&mut running_plugin,
|
||||
instance_id,
|
||||
Some(&pipe_message.source),
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
release_pipe(&senders, instance_id, &pipe_message);
|
||||
for (remaining, _) in messages {
|
||||
release_pipe(&senders, instance_id, &remaining);
|
||||
}
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
return;
|
||||
},
|
||||
}
|
||||
}
|
||||
if let Err(e) = render_request(&senders, &mut running_plugin, render) {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_slot_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
slot_id: PluginId,
|
||||
slot: SharedSlot,
|
||||
slot_added: std::sync::mpsc::Sender<()>,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
let mut loading_indication = LoadingIndication::new(String::new());
|
||||
handle_plugin_loading_failure(
|
||||
&senders,
|
||||
slot_id,
|
||||
&mut loading_indication,
|
||||
"Shared plugin instance failed to load",
|
||||
None,
|
||||
);
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let added = call_slot_added(&mut running_plugin, slot_id, slot);
|
||||
let _ = slot_added.send(());
|
||||
let result = added.and_then(|_| {
|
||||
render_request(
|
||||
&senders,
|
||||
&mut running_plugin,
|
||||
RenderResponse::Slots(vec![slot_id]),
|
||||
)
|
||||
});
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let plugin_list = plugin_map.lock().unwrap().list_plugins();
|
||||
handle_plugin_successful_loading(&senders, slot_id, plugin_list);
|
||||
},
|
||||
Err(e) => {
|
||||
let _ =
|
||||
senders.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(slot_id));
|
||||
report_error(&senders, vec![slot_id], e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn remove_slot_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
slot_id: PluginId,
|
||||
tear_down: bool,
|
||||
) {
|
||||
if let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) {
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
if let Err(e) = call_slot_removed(&mut running_plugin, slot_id) {
|
||||
log::error!(
|
||||
"Failed to remove slot {} from shared plugin: {:?}",
|
||||
slot_id,
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
if tear_down {
|
||||
tear_down_instance(&senders, &plugin_map, instance_id);
|
||||
}
|
||||
}
|
||||
|
||||
fn tear_down_instance(
|
||||
senders: &ThreadSenders,
|
||||
plugin_map: &Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
) {
|
||||
let removed = plugin_map.lock().unwrap().remove_shared(instance_id);
|
||||
let Some((running_plugin, subscriptions, workers)) = removed else {
|
||||
return;
|
||||
};
|
||||
for (_worker_name, worker_sender) in workers {
|
||||
drop(worker_sender.send(MessageToWorker::Exit));
|
||||
}
|
||||
let needs_before_close = subscriptions
|
||||
.lock()
|
||||
.unwrap()
|
||||
.contains(&EventType::BeforeClose);
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
if needs_before_close {
|
||||
if let Err(e) = call_update(
|
||||
&mut running_plugin,
|
||||
&Event::BeforeClose,
|
||||
None,
|
||||
EventContext::default(),
|
||||
) {
|
||||
log::error!("Failed to send BeforeClose to shared plugin: {:?}", e);
|
||||
}
|
||||
send_pipe_changes(senders, &mut running_plugin, instance_id, None);
|
||||
}
|
||||
if running_plugin.intercepting_key_presses() {
|
||||
let clients: Vec<ClientId> = running_plugin
|
||||
.store
|
||||
.data()
|
||||
.shared
|
||||
.as_ref()
|
||||
.map(|shared| shared.clients.iter().copied().collect())
|
||||
.unwrap_or_default();
|
||||
for client_id in clients {
|
||||
let _ = senders.send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(client_id));
|
||||
}
|
||||
}
|
||||
let data_dir = running_plugin.store.data().plugin_own_data_dir.clone();
|
||||
if let Err(e) = std::fs::remove_dir_all(&data_dir) {
|
||||
log::error!("Failed to remove data dir for shared plugin: {:?}", e);
|
||||
}
|
||||
let _ = senders.send_to_screen(ScreenInstruction::ClearAllPluginHighlights(instance_id));
|
||||
}
|
||||
|
||||
pub fn client_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
client_id: ClientId,
|
||||
connected: bool,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let result = if connected {
|
||||
call_client_connected(&mut running_plugin, client_id).and_then(|_| {
|
||||
render_request(
|
||||
&senders,
|
||||
&mut running_plugin,
|
||||
RenderResponse::Client(client_id),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
call_client_disconnected(&mut running_plugin, client_id)
|
||||
};
|
||||
if let Err(e) = result {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn resize_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
slot_id: PluginId,
|
||||
rows: usize,
|
||||
columns: usize,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let changed = match running_plugin.store.data_mut().shared.as_mut() {
|
||||
Some(shared) => match shared.slots.get_mut(&slot_id) {
|
||||
Some(slot) => {
|
||||
let changed = slot.rows != rows || slot.columns != columns;
|
||||
slot.rows = rows;
|
||||
slot.columns = columns;
|
||||
changed
|
||||
},
|
||||
None => false,
|
||||
},
|
||||
None => false,
|
||||
};
|
||||
if changed {
|
||||
if let Err(e) = render_request(
|
||||
&senders,
|
||||
&mut running_plugin,
|
||||
RenderResponse::Slots(vec![slot_id]),
|
||||
) {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn visibility_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
visible_slots: HashMap<ClientId, HashSet<PluginId>>,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
let before: HashSet<(PluginId, ClientId)> = targets_for(&running_plugin, &RenderResponse::All)
|
||||
.into_iter()
|
||||
.collect();
|
||||
if let Some(shared) = running_plugin.store.data_mut().shared.as_mut() {
|
||||
shared.visible_slots = visible_slots;
|
||||
}
|
||||
let newly_visible: Vec<(PluginId, ClientId)> =
|
||||
targets_for(&running_plugin, &RenderResponse::All)
|
||||
.into_iter()
|
||||
.filter(|target| !before.contains(target))
|
||||
.collect();
|
||||
if let Err(e) = render_and_send(&senders, &mut running_plugin, newly_visible) {
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn host_settings_job(
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
instance_id: PluginId,
|
||||
default_shell: Option<TerminalAction>,
|
||||
layout_dir: Option<PathBuf>,
|
||||
) {
|
||||
let Some((running_plugin, _subscriptions)) = lookup(&plugin_map, instance_id) else {
|
||||
return;
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
running_plugin.update_default_shell(default_shell);
|
||||
running_plugin.update_layout_dir(layout_dir);
|
||||
}
|
||||
|
||||
pub fn start_instance_job(
|
||||
senders: ThreadSenders,
|
||||
plugin_map: Arc<Mutex<PluginMap>>,
|
||||
connected_clients: Arc<Mutex<Vec<ClientId>>>,
|
||||
plugin_cache: PluginCache,
|
||||
engine: Engine,
|
||||
loading_context: LoadingContext,
|
||||
skip_cache: bool,
|
||||
instance_id: PluginId,
|
||||
initial_slots: BTreeMap<PluginId, SharedSlot>,
|
||||
) {
|
||||
let mut initial_slots = initial_slots;
|
||||
let previous = plugin_map.lock().unwrap().remove_shared(instance_id);
|
||||
let previous_visible_slots = match previous {
|
||||
Some((running_plugin, _subscriptions, workers)) => {
|
||||
for (_worker_name, worker_sender) in workers {
|
||||
drop(worker_sender.send(MessageToWorker::Exit));
|
||||
}
|
||||
let running_plugin = running_plugin.lock().unwrap();
|
||||
match running_plugin.store.data().shared.as_ref() {
|
||||
Some(shared) => {
|
||||
for (slot_id, slot) in shared.slots.iter() {
|
||||
initial_slots
|
||||
.entry(*slot_id)
|
||||
.or_insert_with(|| slot.clone());
|
||||
}
|
||||
shared.visible_slots.clone()
|
||||
},
|
||||
None => HashMap::new(),
|
||||
}
|
||||
},
|
||||
None => HashMap::new(),
|
||||
};
|
||||
let started = {
|
||||
let mut plugin_map = plugin_map.lock().unwrap();
|
||||
PluginLoader::new(
|
||||
skip_cache,
|
||||
loading_context,
|
||||
senders.clone(),
|
||||
engine,
|
||||
plugin_cache,
|
||||
&mut plugin_map,
|
||||
connected_clients.clone(),
|
||||
)
|
||||
.start_shared_plugin()
|
||||
};
|
||||
let running_plugin = match started {
|
||||
Ok(running_plugin) => running_plugin,
|
||||
Err(e) => {
|
||||
for slot_id in initial_slots.keys() {
|
||||
let mut loading_indication = LoadingIndication::new(String::new());
|
||||
handle_plugin_loading_failure(
|
||||
&senders,
|
||||
*slot_id,
|
||||
&mut loading_indication,
|
||||
format!("{:?}", e),
|
||||
None,
|
||||
);
|
||||
}
|
||||
return;
|
||||
},
|
||||
};
|
||||
let mut running_plugin = running_plugin.lock().unwrap();
|
||||
if let Some(shared) = running_plugin.store.data_mut().shared.as_mut() {
|
||||
shared.visible_slots = previous_visible_slots;
|
||||
}
|
||||
let clients: Vec<ClientId> = connected_clients.lock().unwrap().iter().copied().collect();
|
||||
let mut result: Result<()> = Ok(());
|
||||
for client_id in clients {
|
||||
if result.is_ok() {
|
||||
result = call_client_connected(&mut running_plugin, client_id);
|
||||
}
|
||||
}
|
||||
for (slot_id, slot) in initial_slots.iter() {
|
||||
if result.is_ok() {
|
||||
result = call_slot_added(&mut running_plugin, *slot_id, slot.clone());
|
||||
}
|
||||
}
|
||||
if result.is_ok() {
|
||||
result = render_request(&senders, &mut running_plugin, RenderResponse::All);
|
||||
}
|
||||
match result {
|
||||
Ok(()) => {
|
||||
let plugin_list = plugin_map.lock().unwrap().list_plugins();
|
||||
for slot_id in initial_slots.keys() {
|
||||
handle_plugin_successful_loading(&senders, *slot_id, plugin_list.clone());
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
for slot_id in initial_slots.keys() {
|
||||
let _ = senders
|
||||
.send_to_background_jobs(BackgroundJob::StopPluginLoadingAnimation(*slot_id));
|
||||
}
|
||||
let pane_slots = pane_slot_ids(&running_plugin);
|
||||
report_error(&senders, pane_slots, e);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn module_with_exports(names: &[&str]) -> Vec<u8> {
|
||||
let mut section = vec![names.len() as u8];
|
||||
for name in names {
|
||||
section.push(name.len() as u8);
|
||||
section.extend_from_slice(name.as_bytes());
|
||||
section.push(0);
|
||||
section.push(0);
|
||||
}
|
||||
let mut bytes = b"\0asm\x01\0\0\0".to_vec();
|
||||
bytes.push(1);
|
||||
bytes.push(0);
|
||||
bytes.push(7);
|
||||
bytes.push(section.len() as u8);
|
||||
bytes.extend(section);
|
||||
bytes
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detects_shared_marker_export() {
|
||||
let bytes = module_with_exports(&["load", SHARED_MARKER_EXPORT]);
|
||||
assert!(wasm_module_exports_function(&bytes, SHARED_MARKER_EXPORT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn legacy_module_is_not_shared() {
|
||||
let bytes = module_with_exports(&["load", "update", "render"]);
|
||||
assert!(!wasm_module_exports_function(&bytes, SHARED_MARKER_EXPORT));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn garbage_is_not_shared() {
|
||||
assert!(!wasm_module_exports_function(
|
||||
b"not a wasm module",
|
||||
SHARED_MARKER_EXPORT
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -292,7 +292,7 @@ macro_rules! grant_permissions_and_log_actions_in_thread_struct_variant {
|
||||
};
|
||||
}
|
||||
|
||||
fn create_plugin_thread(
|
||||
pub(super) fn create_plugin_thread(
|
||||
zellij_cwd: Option<PathBuf>,
|
||||
session_env_vars: Option<std::collections::BTreeMap<String, String>>,
|
||||
) -> (
|
||||
@@ -371,7 +371,7 @@ fn create_plugin_thread(
|
||||
default_shell_action,
|
||||
plugin_aliases,
|
||||
InputMode::Normal,
|
||||
Keybinds::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
initiating_client_id,
|
||||
)
|
||||
@@ -459,7 +459,7 @@ fn create_plugin_thread_with_server_receiver(
|
||||
default_shell_action,
|
||||
PluginAliases::default(),
|
||||
InputMode::Normal,
|
||||
Keybinds::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
initiating_client_id,
|
||||
)
|
||||
@@ -555,7 +555,7 @@ fn create_plugin_thread_with_pty_receiver(
|
||||
default_shell_action,
|
||||
PluginAliases::default(),
|
||||
InputMode::Normal,
|
||||
Keybinds::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
initiating_client_id,
|
||||
)
|
||||
@@ -644,7 +644,7 @@ fn create_plugin_thread_with_background_jobs_receiver(
|
||||
default_shell_action,
|
||||
PluginAliases::default(),
|
||||
InputMode::Normal,
|
||||
Keybinds::default(),
|
||||
Default::default(),
|
||||
Default::default(),
|
||||
initiating_client_id,
|
||||
)
|
||||
@@ -13018,7 +13018,7 @@ pub fn mode_update_payload_is_lightweight_for_opted_in_plugins() {
|
||||
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Reconfigure {
|
||||
client_id,
|
||||
keybinds: Some(test_keybinds),
|
||||
keybinds: Some(std::sync::Arc::new(test_keybinds.to_keybinds_vec())),
|
||||
default_mode: None,
|
||||
default_shell: None,
|
||||
layout_dir: None,
|
||||
@@ -13172,7 +13172,7 @@ pub fn reconfiguration_resends_keybinds_to_opted_in_plugins() {
|
||||
// Send Reconfigure
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Reconfigure {
|
||||
client_id,
|
||||
keybinds: Some(new_keybinds),
|
||||
keybinds: Some(std::sync::Arc::new(new_keybinds.to_keybinds_vec())),
|
||||
default_mode: None,
|
||||
default_shell: None,
|
||||
layout_dir: None,
|
||||
|
||||
@@ -0,0 +1,504 @@
|
||||
use super::plugin_tests::create_plugin_thread;
|
||||
use crate::plugins::{PluginId, PluginInstruction};
|
||||
use crate::screen::ScreenInstruction;
|
||||
use crate::ClientId;
|
||||
use std::collections::{BTreeMap, HashMap, HashSet};
|
||||
use std::path::PathBuf;
|
||||
use std::time::{Duration, Instant};
|
||||
use tempfile::tempdir;
|
||||
use zellij_utils::channels::{Receiver, SenderWithContext};
|
||||
use zellij_utils::data::{BareKey, Event, KeyWithModifier, PermissionStatus};
|
||||
use zellij_utils::errors::ErrorContext;
|
||||
use zellij_utils::input::layout::{
|
||||
PluginUserConfiguration, RunPlugin, RunPluginLocation, RunPluginOrAlias,
|
||||
};
|
||||
use zellij_utils::pane_size::Size;
|
||||
|
||||
type Renders = HashMap<(PluginId, ClientId), String>;
|
||||
|
||||
fn fixture_path(name: &str) -> PathBuf {
|
||||
PathBuf::from(format!(
|
||||
"{}/../target/e2e-data/plugins/{}.wasm",
|
||||
std::env::var_os("CARGO_MANIFEST_DIR")
|
||||
.unwrap()
|
||||
.to_string_lossy(),
|
||||
name
|
||||
))
|
||||
}
|
||||
|
||||
fn shared_fixture(configuration: &[(&str, &str)]) -> RunPluginOrAlias {
|
||||
let configuration: BTreeMap<String, String> = configuration
|
||||
.iter()
|
||||
.map(|(k, v)| (k.to_string(), v.to_string()))
|
||||
.collect();
|
||||
RunPluginOrAlias::RunPlugin(RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(fixture_path("fixture-shared-plugin-for-tests")),
|
||||
configuration: PluginUserConfiguration::new(configuration),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_fixture() -> RunPluginOrAlias {
|
||||
RunPluginOrAlias::RunPlugin(RunPlugin {
|
||||
_allow_exec_host_cmd: false,
|
||||
location: RunPluginLocation::File(fixture_path("fixture-plugin-for-tests")),
|
||||
configuration: Default::default(),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn load(
|
||||
plugin_thread_sender: &SenderWithContext<PluginInstruction>,
|
||||
run_plugin: RunPluginOrAlias,
|
||||
tab_index: usize,
|
||||
client_id: ClientId,
|
||||
) {
|
||||
let _ = plugin_thread_sender.send(PluginInstruction::Load(
|
||||
Some(false),
|
||||
false,
|
||||
false,
|
||||
None,
|
||||
run_plugin,
|
||||
Some(tab_index),
|
||||
None,
|
||||
client_id,
|
||||
Size { cols: 80, rows: 5 },
|
||||
None,
|
||||
None,
|
||||
false,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
fn key_event() -> Event {
|
||||
Event::Key(KeyWithModifier::new(BareKey::Char('!')))
|
||||
}
|
||||
|
||||
struct ScreenLog {
|
||||
receiver: Receiver<(ScreenInstruction, ErrorContext)>,
|
||||
plugin_thread_sender: SenderWithContext<PluginInstruction>,
|
||||
cache_path: PathBuf,
|
||||
renders: Renders,
|
||||
render_log: Vec<(PluginId, ClientId, String)>,
|
||||
}
|
||||
|
||||
impl ScreenLog {
|
||||
fn new(
|
||||
receiver: Receiver<(ScreenInstruction, ErrorContext)>,
|
||||
plugin_thread_sender: SenderWithContext<PluginInstruction>,
|
||||
cache_path: PathBuf,
|
||||
) -> Self {
|
||||
ScreenLog {
|
||||
receiver,
|
||||
plugin_thread_sender,
|
||||
cache_path,
|
||||
renders: HashMap::new(),
|
||||
render_log: vec![],
|
||||
}
|
||||
}
|
||||
fn receive_one(&mut self, timeout: Duration) {
|
||||
let Ok((instruction, _)) = self.receiver.recv_timeout(timeout) else {
|
||||
return;
|
||||
};
|
||||
match instruction {
|
||||
ScreenInstruction::PluginBytes(assets) => {
|
||||
for asset in assets {
|
||||
if asset.bytes.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let rendered = String::from_utf8_lossy(&asset.bytes).to_string();
|
||||
self.render_log
|
||||
.push((asset.plugin_id, asset.client_id, rendered.clone()));
|
||||
self.renders
|
||||
.insert((asset.plugin_id, asset.client_id), rendered);
|
||||
}
|
||||
},
|
||||
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
|
||||
let _ = self
|
||||
.plugin_thread_sender
|
||||
.send(PluginInstruction::PermissionRequestResult(
|
||||
plugin_id,
|
||||
None,
|
||||
plugin_permission.permissions,
|
||||
PermissionStatus::Granted,
|
||||
Some(self.cache_path.clone()),
|
||||
));
|
||||
},
|
||||
_ => {},
|
||||
}
|
||||
}
|
||||
fn wait_until(&mut self, description: &str, condition: impl Fn(&Renders) -> bool) {
|
||||
let deadline = Instant::now() + Duration::from_secs(30);
|
||||
while !condition(&self.renders) {
|
||||
if Instant::now() > deadline {
|
||||
panic!(
|
||||
"timed out waiting for: {}, renders: {:#?}",
|
||||
description, self.renders
|
||||
);
|
||||
}
|
||||
self.receive_one(Duration::from_millis(100));
|
||||
}
|
||||
}
|
||||
fn settle(&mut self, duration: Duration) {
|
||||
let deadline = Instant::now() + duration;
|
||||
while Instant::now() < deadline {
|
||||
self.receive_one(Duration::from_millis(50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn field<'a>(rendered: &'a str, name: &str) -> &'a str {
|
||||
let start = rendered
|
||||
.find(&format!("{}:", name))
|
||||
.map(|i| i + name.len() + 1)
|
||||
.unwrap_or(rendered.len());
|
||||
let rest = &rendered[start..];
|
||||
let end = if rest.starts_with('[') {
|
||||
rest.find(']').map(|i| i + 1).unwrap_or(rest.len())
|
||||
} else {
|
||||
rest.find(' ').unwrap_or(rest.len())
|
||||
};
|
||||
&rest[..end]
|
||||
}
|
||||
|
||||
fn slots_of_client(renders: &Renders, client_id: ClientId) -> Vec<PluginId> {
|
||||
let mut slots: Vec<PluginId> = renders
|
||||
.keys()
|
||||
.filter(|(_, c)| *c == client_id)
|
||||
.map(|(slot_id, _)| *slot_id)
|
||||
.collect();
|
||||
slots.sort();
|
||||
slots
|
||||
}
|
||||
|
||||
fn start() -> (
|
||||
SenderWithContext<PluginInstruction>,
|
||||
ScreenLog,
|
||||
Box<dyn FnOnce()>,
|
||||
tempfile::TempDir,
|
||||
) {
|
||||
let temp_folder = tempdir().unwrap();
|
||||
let cache_path = PathBuf::from(temp_folder.path()).join("permissions_test.kdl");
|
||||
let (plugin_thread_sender, screen_receiver, teardown) = create_plugin_thread(None, None);
|
||||
let screen_log = ScreenLog::new(screen_receiver, plugin_thread_sender.clone(), cache_path);
|
||||
(plugin_thread_sender, screen_log, teardown, temp_folder)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_one_instance_many_slots() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let client_id = 1;
|
||||
let _ = sender.send(PluginInstruction::AddClient(client_id));
|
||||
load(&sender, shared_fixture(&[("label", "a")]), 0, client_id);
|
||||
load(&sender, shared_fixture(&[("label", "b")]), 1, client_id);
|
||||
load(&sender, shared_fixture(&[("label", "c")]), 2, client_id);
|
||||
screen.wait_until("three slots rendered", |renders| {
|
||||
slots_of_client(renders, client_id).len() == 3
|
||||
});
|
||||
let first_slot = slots_of_client(&screen.renders, client_id)[0];
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(first_slot),
|
||||
Some(client_id),
|
||||
key_event(),
|
||||
)]));
|
||||
screen.wait_until("three slots rendered with all slots known", |renders| {
|
||||
let slots = slots_of_client(renders, client_id);
|
||||
slots.len() == 3
|
||||
&& slots.iter().all(|slot_id| {
|
||||
field(&renders[&(*slot_id, client_id)], "slots") == format!("{:?}", slots)
|
||||
})
|
||||
});
|
||||
teardown();
|
||||
let slots = slots_of_client(&screen.renders, client_id);
|
||||
let instances: HashSet<&str> = slots
|
||||
.iter()
|
||||
.map(|slot_id| field(&screen.renders[&(*slot_id, client_id)], "instance"))
|
||||
.collect();
|
||||
assert_eq!(instances.len(), 1, "all slots are served by one instance");
|
||||
for slot_id in &slots {
|
||||
let rendered = &screen.renders[&(*slot_id, client_id)];
|
||||
assert_eq!(
|
||||
field(rendered, "loads"),
|
||||
"1",
|
||||
"load runs once: {}",
|
||||
rendered
|
||||
);
|
||||
assert_eq!(field(rendered, "slot"), slot_id.to_string());
|
||||
}
|
||||
let names: HashSet<&str> = slots
|
||||
.iter()
|
||||
.map(|slot_id| field(&screen.renders[&(*slot_id, client_id)], "label"))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
names,
|
||||
HashSet::from(["a", "b", "c"]),
|
||||
"each slot keeps its own configuration"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_instance_key_creates_separate_instances() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let client_id = 1;
|
||||
let _ = sender.send(PluginInstruction::AddClient(client_id));
|
||||
load(
|
||||
&sender,
|
||||
shared_fixture(&[("instance", "one"), ("label", "a")]),
|
||||
0,
|
||||
client_id,
|
||||
);
|
||||
load(
|
||||
&sender,
|
||||
shared_fixture(&[("instance", "two"), ("label", "b")]),
|
||||
0,
|
||||
client_id,
|
||||
);
|
||||
load(
|
||||
&sender,
|
||||
shared_fixture(&[("instance", "one"), ("label", "c")]),
|
||||
1,
|
||||
client_id,
|
||||
);
|
||||
screen.wait_until("three slots rendered", |renders| {
|
||||
slots_of_client(renders, client_id).len() == 3
|
||||
});
|
||||
for slot_id in slots_of_client(&screen.renders, client_id) {
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(slot_id),
|
||||
Some(client_id),
|
||||
key_event(),
|
||||
)]));
|
||||
}
|
||||
screen.wait_until(
|
||||
"instance one has two slots and instance two has one",
|
||||
|renders| {
|
||||
let slots = slots_of_client(renders, client_id);
|
||||
slots.len() == 3 && {
|
||||
let mut slot_list_lengths: Vec<usize> = slots
|
||||
.iter()
|
||||
.map(|slot_id| {
|
||||
field(&renders[&(*slot_id, client_id)], "slots")
|
||||
.split(',')
|
||||
.count()
|
||||
})
|
||||
.collect();
|
||||
slot_list_lengths.sort();
|
||||
slot_list_lengths == vec![1, 2, 2]
|
||||
}
|
||||
},
|
||||
);
|
||||
teardown();
|
||||
let by_name: HashMap<&str, &str> = screen
|
||||
.renders
|
||||
.values()
|
||||
.map(|rendered| (field(rendered, "label"), field(rendered, "instance")))
|
||||
.collect();
|
||||
assert_eq!(
|
||||
by_name["a"], by_name["c"],
|
||||
"same instance name shares an instance"
|
||||
);
|
||||
assert_ne!(
|
||||
by_name["a"], by_name["b"],
|
||||
"different instance names do not share"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_renders_per_client() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let _ = sender.send(PluginInstruction::AddClient(1));
|
||||
let _ = sender.send(PluginInstruction::AddClient(2));
|
||||
load(&sender, shared_fixture(&[("label", "a")]), 0, 1);
|
||||
screen.wait_until("the slot rendered for both clients", |renders| {
|
||||
slots_of_client(renders, 1).len() == 1 && slots_of_client(renders, 2).len() == 1
|
||||
});
|
||||
let slot_id = slots_of_client(&screen.renders, 1)[0];
|
||||
assert_eq!(field(&screen.renders[&(slot_id, 1)], "client"), "1");
|
||||
assert_eq!(field(&screen.renders[&(slot_id, 2)], "client"), "2");
|
||||
let renders_before_key = screen.render_log.len();
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(slot_id),
|
||||
Some(2),
|
||||
key_event(),
|
||||
)]));
|
||||
screen.wait_until("client 2 re-rendered after its key", |renders| {
|
||||
field(&renders[&(slot_id, 2)], "keys") == format!("[(Some({}), Some(2))]", slot_id)
|
||||
});
|
||||
screen.settle(Duration::from_millis(300));
|
||||
teardown();
|
||||
let clients_rendered_after_key: HashSet<ClientId> = screen.render_log[renders_before_key..]
|
||||
.iter()
|
||||
.map(|(_, client_id, _)| *client_id)
|
||||
.collect();
|
||||
assert_eq!(
|
||||
clients_rendered_after_key,
|
||||
HashSet::from([2]),
|
||||
"RenderResponse::Client only re-renders the named client"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_slot_removed_when_pane_closes() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let client_id = 1;
|
||||
let _ = sender.send(PluginInstruction::AddClient(client_id));
|
||||
load(&sender, shared_fixture(&[("label", "a")]), 0, client_id);
|
||||
load(&sender, shared_fixture(&[("label", "b")]), 1, client_id);
|
||||
screen.wait_until("both slots rendered", |renders| {
|
||||
slots_of_client(renders, client_id).len() == 2
|
||||
});
|
||||
let slots = slots_of_client(&screen.renders, client_id);
|
||||
let (closed_slot, remaining_slot) = (slots[0], slots[1]);
|
||||
let _ = sender.send(PluginInstruction::Unload(closed_slot));
|
||||
let renders_before_key = screen.render_log.len();
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(remaining_slot),
|
||||
Some(client_id),
|
||||
key_event(),
|
||||
)]));
|
||||
screen.wait_until(
|
||||
"remaining slot knows the other slot was removed",
|
||||
|renders| {
|
||||
let rendered = &renders[&(remaining_slot, client_id)];
|
||||
field(rendered, "removed") == format!("[{}]", closed_slot)
|
||||
&& field(rendered, "slots") == format!("[{}]", remaining_slot)
|
||||
},
|
||||
);
|
||||
screen.settle(Duration::from_millis(300));
|
||||
teardown();
|
||||
assert!(
|
||||
screen.render_log[renders_before_key..]
|
||||
.iter()
|
||||
.all(|(slot_id, _, _)| *slot_id != closed_slot),
|
||||
"a removed slot is not rendered"
|
||||
);
|
||||
assert_eq!(
|
||||
field(&screen.renders[&(remaining_slot, client_id)], "loads"),
|
||||
"1"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_client_connect_and_disconnect() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let _ = sender.send(PluginInstruction::AddClient(1));
|
||||
load(&sender, shared_fixture(&[("label", "a")]), 0, 1);
|
||||
screen.wait_until("slot rendered for client 1", |renders| {
|
||||
slots_of_client(renders, 1).len() == 1
|
||||
});
|
||||
let slot_id = slots_of_client(&screen.renders, 1)[0];
|
||||
assert_eq!(field(&screen.renders[&(slot_id, 1)], "clients"), "[1]");
|
||||
let _ = sender.send(PluginInstruction::AddClient(2));
|
||||
screen.wait_until("new client is rendered without a new instance", |renders| {
|
||||
renders
|
||||
.get(&(slot_id, 2))
|
||||
.map(|rendered| field(rendered, "clients") == "[1, 2]")
|
||||
.unwrap_or(false)
|
||||
});
|
||||
let _ = sender.send(PluginInstruction::RemoveClient(2));
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(slot_id),
|
||||
Some(1),
|
||||
key_event(),
|
||||
)]));
|
||||
screen.wait_until("client 2 is gone", |renders| {
|
||||
let rendered = &renders[&(slot_id, 1)];
|
||||
field(rendered, "clients") == "[1]" && field(rendered, "disconnected") == "[2]"
|
||||
});
|
||||
teardown();
|
||||
let instances: HashSet<&str> = screen
|
||||
.renders
|
||||
.values()
|
||||
.map(|rendered| field(rendered, "instance"))
|
||||
.collect();
|
||||
assert_eq!(instances.len(), 1);
|
||||
assert!(screen
|
||||
.renders
|
||||
.values()
|
||||
.all(|rendered| field(rendered, "loads") == "1"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn shared_plugin_slot_commands_are_checked() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let client_id = 1;
|
||||
let _ = sender.send(PluginInstruction::AddClient(client_id));
|
||||
load(&sender, shared_fixture(&[("label", "a")]), 0, client_id);
|
||||
screen.wait_until("slot rendered", |renders| {
|
||||
slots_of_client(renders, client_id).len() == 1
|
||||
});
|
||||
let slot_id = slots_of_client(&screen.renders, client_id)[0];
|
||||
let _ = sender.send(PluginInstruction::KeybindPipe {
|
||||
name: "selectable_slot".to_owned(),
|
||||
payload: None,
|
||||
plugin: None,
|
||||
args: None,
|
||||
configuration: None,
|
||||
floating: None,
|
||||
pane_id_to_replace: None,
|
||||
pane_title: None,
|
||||
cwd: None,
|
||||
skip_cache: false,
|
||||
cli_client_id: client_id,
|
||||
plugin_and_client_id: Some((slot_id, client_id)),
|
||||
notification_end: None,
|
||||
});
|
||||
screen.wait_until("slot command results rendered", |renders| {
|
||||
field(&renders[&(slot_id, client_id)], "self_commands").contains("Err")
|
||||
});
|
||||
teardown();
|
||||
let rendered = &screen.renders[&(slot_id, client_id)];
|
||||
let results = field(rendered, "self_commands");
|
||||
assert!(
|
||||
results.starts_with("[\"Ok(())\""),
|
||||
"own slot is accepted: {}",
|
||||
results
|
||||
);
|
||||
assert!(
|
||||
results.contains("does not belong"),
|
||||
"foreign slot is rejected: {}",
|
||||
results
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
pub fn legacy_plugin_keeps_one_instance_per_client() {
|
||||
let (sender, mut screen, teardown, _temp_folder) = start();
|
||||
let _ = sender.send(PluginInstruction::AddClient(1));
|
||||
let _ = sender.send(PluginInstruction::AddClient(2));
|
||||
load(&sender, legacy_fixture(), 0, 1);
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
None,
|
||||
None,
|
||||
Event::InputReceived,
|
||||
)]));
|
||||
screen.wait_until("legacy plugin rendered for both clients", |renders| {
|
||||
slots_of_client(renders, 1).len() == 1 && slots_of_client(renders, 2).len() == 1
|
||||
});
|
||||
let plugin_id = slots_of_client(&screen.renders, 1)[0];
|
||||
let _ = sender.send(PluginInstruction::Update(vec![(
|
||||
Some(plugin_id),
|
||||
Some(1),
|
||||
key_event(),
|
||||
)]));
|
||||
screen.wait_until("client 1 instance received the key", |renders| {
|
||||
renders[&(plugin_id, 1)].contains("Key(")
|
||||
});
|
||||
screen.settle(Duration::from_millis(300));
|
||||
teardown();
|
||||
assert!(
|
||||
!screen.renders[&(plugin_id, 2)].contains("Key("),
|
||||
"the other client's instance did not receive the key"
|
||||
);
|
||||
assert!(screen.renders[&(plugin_id, 1)].starts_with("Rows: "));
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -3,8 +3,10 @@ use crate::background_jobs::BackgroundJob;
|
||||
use crate::global_async_runtime::get_tokio_runtime;
|
||||
use crate::plugins::plugin_map::PluginEnv;
|
||||
use crate::plugins::wasm_bridge::handle_plugin_crash;
|
||||
use crate::plugins::PluginId;
|
||||
use crate::pty::{ClientTabIndexOrPaneId, PtyInstruction};
|
||||
use crate::route::{route_action, wait_for_action_completion, NotificationEnd};
|
||||
use crate::ClientId;
|
||||
use crate::ServerInstruction;
|
||||
use log::warn;
|
||||
use serde::Serialize;
|
||||
@@ -108,7 +110,7 @@ use zellij_utils::{
|
||||
ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse, ProtobufOpenTerminalResponse,
|
||||
ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse,
|
||||
ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse,
|
||||
ProtobufShowFloatingPanesResponse,
|
||||
ProtobufShowFloatingPanesResponse, ProtobufSlotCommandResponse,
|
||||
},
|
||||
plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion},
|
||||
},
|
||||
@@ -124,9 +126,9 @@ macro_rules! apply_action {
|
||||
($action:ident, $error_message:ident, $env: ident) => {
|
||||
match route_action(
|
||||
$action,
|
||||
$env.client_id,
|
||||
acting_client(&$env),
|
||||
None,
|
||||
Some(PaneId::Plugin($env.plugin_id)),
|
||||
source_pane(&$env),
|
||||
$env.senders.clone(),
|
||||
$env.default_shell.clone(),
|
||||
None,
|
||||
@@ -159,6 +161,154 @@ fn translate_plugin_path(env: &PluginEnv, path: PathBuf) -> PathBuf {
|
||||
}
|
||||
}
|
||||
|
||||
fn acting_client(env: &PluginEnv) -> ClientId {
|
||||
env.shared
|
||||
.as_ref()
|
||||
.and_then(|shared| shared.current_client)
|
||||
.unwrap_or(env.client_id)
|
||||
}
|
||||
|
||||
fn self_pane_id(env: &PluginEnv) -> PluginId {
|
||||
env.shared
|
||||
.as_ref()
|
||||
.and_then(|shared| shared.current_slot)
|
||||
.unwrap_or(env.plugin_id)
|
||||
}
|
||||
|
||||
fn source_pane(env: &PluginEnv) -> Option<PaneId> {
|
||||
match env.shared.as_ref() {
|
||||
Some(shared) => shared.current_slot.map(PaneId::Plugin),
|
||||
None => Some(PaneId::Plugin(env.plugin_id)),
|
||||
}
|
||||
}
|
||||
|
||||
fn permission_pane_id(env: &PluginEnv) -> PluginId {
|
||||
match env.shared.as_ref() {
|
||||
Some(shared) => shared
|
||||
.current_slot
|
||||
.or_else(|| shared.first_pane_slot())
|
||||
.unwrap_or(env.plugin_id),
|
||||
None => env.plugin_id,
|
||||
}
|
||||
}
|
||||
|
||||
fn crash_pane_ids(env: &PluginEnv) -> Vec<PluginId> {
|
||||
match env.shared.as_ref() {
|
||||
Some(shared) => shared.pane_slot_ids(),
|
||||
None => vec![env.plugin_id],
|
||||
}
|
||||
}
|
||||
|
||||
fn is_self_command(command: &PluginCommand) -> bool {
|
||||
matches!(
|
||||
command,
|
||||
PluginCommand::SetSelectable(..)
|
||||
| PluginCommand::ShowCursor(..)
|
||||
| PluginCommand::HideSelf
|
||||
| PluginCommand::ShowSelf(..)
|
||||
| PluginCommand::CloseSelf
|
||||
| PluginCommand::SetSelfMouseSelectionSupport(..)
|
||||
) || self_command_has_response(command)
|
||||
}
|
||||
|
||||
fn self_command_has_response(command: &PluginCommand) -> bool {
|
||||
matches!(
|
||||
command,
|
||||
PluginCommand::OpenFileNearPlugin(..)
|
||||
| PluginCommand::OpenFileFloatingNearPlugin(..)
|
||||
| PluginCommand::OpenFileInPlaceOfPlugin(..)
|
||||
| PluginCommand::OpenTerminalNearPlugin(..)
|
||||
| PluginCommand::OpenTerminalFloatingNearPlugin(..)
|
||||
| PluginCommand::OpenTerminalInPlaceOfPlugin(..)
|
||||
| PluginCommand::OpenCommandPaneNearPlugin(..)
|
||||
| PluginCommand::OpenCommandPaneFloatingNearPlugin(..)
|
||||
| PluginCommand::OpenCommandPaneInPlaceOfPlugin(..)
|
||||
)
|
||||
}
|
||||
|
||||
fn self_command_is_forbidden(env: &PluginEnv, command: &PluginCommand) -> bool {
|
||||
match env.shared.as_ref() {
|
||||
Some(shared) => {
|
||||
is_self_command(command)
|
||||
&& !(shared.self_commands_allowed && shared.current_slot.is_some())
|
||||
},
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn slot_command_target(env: &PluginEnv, slot_id: PluginId) -> std::result::Result<(), String> {
|
||||
match env.shared.as_ref() {
|
||||
Some(shared) if shared.slots.contains_key(&slot_id) => Ok(()),
|
||||
Some(_) => Err(format!("Slot {} does not belong to this plugin", slot_id)),
|
||||
None if slot_id == env.plugin_id => Ok(()),
|
||||
None => Err(format!(
|
||||
"Slot {} does not belong to this plugin, slot commands are meant for shared plugins",
|
||||
slot_id
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
fn write_slot_command_response(env: &PluginEnv, result: std::result::Result<(), String>) {
|
||||
let response = ProtobufSlotCommandResponse::from(result);
|
||||
wasi_write_object(env, &response.encode_to_vec())
|
||||
.with_context(|| format!("failed to write slot command response"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn set_selectable_slot(env: &PluginEnv, slot_id: PluginId, selectable: bool) {
|
||||
let result = slot_command_target(env, slot_id).and_then(|_| {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SetSelectable(
|
||||
PaneId::Plugin(slot_id),
|
||||
selectable,
|
||||
))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
write_slot_command_response(env, result);
|
||||
}
|
||||
|
||||
fn hide_slot(env: &PluginEnv, slot_id: PluginId) {
|
||||
let result = slot_command_target(env, slot_id).and_then(|_| {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SuppressPane(
|
||||
PaneId::Plugin(slot_id),
|
||||
acting_client(env),
|
||||
))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
write_slot_command_response(env, result);
|
||||
}
|
||||
|
||||
fn show_slot(env: &PluginEnv, slot_id: PluginId, should_float_if_hidden: bool) {
|
||||
let result = slot_command_target(env, slot_id).map(|_| {
|
||||
let action = Action::FocusPluginPaneWithId {
|
||||
pane_id: slot_id,
|
||||
should_float_if_hidden,
|
||||
should_be_in_place_if_hidden: false,
|
||||
};
|
||||
let error_msg = || format!("Failed to show slot");
|
||||
apply_action!(action, error_msg, env);
|
||||
});
|
||||
write_slot_command_response(env, result);
|
||||
}
|
||||
|
||||
fn close_slot(env: &PluginEnv, slot_id: PluginId) {
|
||||
let result = slot_command_target(env, slot_id).and_then(|_| {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::ClosePane(
|
||||
PaneId::Plugin(slot_id),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
))
|
||||
.map_err(|e| e.to_string())?;
|
||||
env.senders
|
||||
.send_to_plugin(PluginInstruction::Unload(slot_id))
|
||||
.map_err(|e| e.to_string())
|
||||
});
|
||||
write_slot_command_response(env, result);
|
||||
}
|
||||
|
||||
pub fn zellij_exports(linker: &mut Linker<PluginEnv>) {
|
||||
linker
|
||||
.func_wrap("zellij", "host_run_plugin_command", host_run_plugin_command)
|
||||
@@ -175,8 +325,25 @@ fn host_run_plugin_command(mut caller: Caller<'_, PluginEnv>) {
|
||||
let command: PluginCommand = command
|
||||
.try_into()
|
||||
.map_err(|e| anyhow!("failed to convert serialized command: {}", e))?;
|
||||
if self_command_is_forbidden(&env, &command) {
|
||||
if self_command_has_response(&command) {
|
||||
wasi_write_object(env, &Vec::<u8>::new())?;
|
||||
}
|
||||
return Err(anyhow!(
|
||||
"'self' commands of shared plugins are only available inside render and slot_added, use the explicit slot form instead: {:?}",
|
||||
CommandType::from_str(&command.to_string()).ok()
|
||||
));
|
||||
}
|
||||
match check_command_permission(&env, &command) {
|
||||
(PermissionStatus::Granted, _) => match command {
|
||||
PluginCommand::SetSelectableSlot(slot_id, selectable) => {
|
||||
set_selectable_slot(env, slot_id, selectable)
|
||||
},
|
||||
PluginCommand::HideSlot(slot_id) => hide_slot(env, slot_id),
|
||||
PluginCommand::ShowSlot(slot_id, should_float_if_hidden) => {
|
||||
show_slot(env, slot_id, should_float_if_hidden)
|
||||
},
|
||||
PluginCommand::CloseSlot(slot_id) => close_slot(env, slot_id),
|
||||
PluginCommand::Subscribe(event_list) => subscribe(env, event_list)?,
|
||||
PluginCommand::Unsubscribe(event_list) => unsubscribe(env, event_list)?,
|
||||
PluginCommand::SetSelectable(selectable) => set_selectable(env, selectable),
|
||||
@@ -843,7 +1010,7 @@ fn subscribe(env: &PluginEnv, event_list: HashSet<EventType>) -> Result<()> {
|
||||
env.senders
|
||||
.send_to_plugin(PluginInstruction::PluginSubscribedToEvents(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
event_list,
|
||||
))
|
||||
}
|
||||
@@ -895,7 +1062,7 @@ fn unsubscribe(env: &PluginEnv, event_list: HashSet<EventType>) -> Result<()> {
|
||||
fn set_selectable(env: &PluginEnv, selectable: bool) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SetSelectable(
|
||||
PaneId::Plugin(env.plugin_id),
|
||||
PaneId::Plugin(self_pane_id(env)),
|
||||
selectable,
|
||||
))
|
||||
.with_context(|| {
|
||||
@@ -911,8 +1078,8 @@ fn set_selectable(env: &PluginEnv, selectable: bool) {
|
||||
fn show_cursor(env: &PluginEnv, cursor_position: Option<(usize, usize)>) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::ShowPluginCursor(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
self_pane_id(env),
|
||||
acting_client(env),
|
||||
cursor_position,
|
||||
))
|
||||
.with_context(|| {
|
||||
@@ -938,7 +1105,7 @@ fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Resu
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::PermissionRequestResult(
|
||||
env.plugin_id,
|
||||
Some(env.client_id),
|
||||
Some(acting_client(env)),
|
||||
permissions.to_vec(),
|
||||
PermissionStatus::Granted,
|
||||
None,
|
||||
@@ -955,7 +1122,7 @@ fn request_permission(env: &PluginEnv, permissions: Vec<PermissionType>) -> Resu
|
||||
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::RequestPluginPermissions(
|
||||
env.plugin_id,
|
||||
permission_pane_id(env),
|
||||
PluginPermission::new(env.plugin.location.to_string(), permissions),
|
||||
))
|
||||
}
|
||||
@@ -965,7 +1132,7 @@ fn get_plugin_ids(env: &PluginEnv) {
|
||||
plugin_id: env.plugin_id,
|
||||
zellij_pid: process::id(),
|
||||
initial_cwd: env.plugin_cwd.clone(),
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
};
|
||||
ProtobufPluginIds::try_from(ids)
|
||||
.map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e))
|
||||
@@ -1091,7 +1258,7 @@ fn open_command_pane_in_new_tab(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title: false,
|
||||
@@ -1277,7 +1444,7 @@ fn get_focused_pane_info(env: &PluginEnv) {
|
||||
let err_context = || {
|
||||
format!(
|
||||
"failed to get focused pane info for client {:?} from plugin {}",
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
env.name()
|
||||
)
|
||||
};
|
||||
@@ -1288,7 +1455,7 @@ fn get_focused_pane_info(env: &PluginEnv) {
|
||||
// Send request to screen thread
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GetFocusedPaneInfo {
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
response_channel: response_sender,
|
||||
})
|
||||
.with_context(err_context)
|
||||
@@ -1301,7 +1468,7 @@ fn get_focused_pane_info(env: &PluginEnv) {
|
||||
log::error!(
|
||||
"GetFocusedPaneInfo timed out for plugin {} for client {:?}",
|
||||
env.plugin_id,
|
||||
env.client_id
|
||||
acting_client(env)
|
||||
);
|
||||
GetFocusedPaneInfoResponse::Err("Timeout retrieving focused pane info".to_string())
|
||||
},
|
||||
@@ -1446,7 +1613,7 @@ fn open_file(env: &PluginEnv, file_to_open: FileToOpen, context: BTreeMap<String
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let action = Action::EditFile {
|
||||
payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
),
|
||||
direction: None,
|
||||
floating,
|
||||
@@ -1474,11 +1641,11 @@ fn run_action(env: &PluginEnv, mut action: Action, context: BTreeMap<String, Str
|
||||
// Clone the necessary data to move into the thread
|
||||
action.populate_originating_plugin(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context.clone(),
|
||||
));
|
||||
let action_clone = action.clone();
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
let plugin_id = env.plugin_id;
|
||||
let senders = env.senders.clone();
|
||||
let default_shell = env.default_shell.clone();
|
||||
@@ -1543,7 +1710,7 @@ fn open_file_floating(
|
||||
.or_else(|| Some(env.plugin_cwd.clone()));
|
||||
let action = Action::EditFile {
|
||||
payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
),
|
||||
direction: None,
|
||||
floating,
|
||||
@@ -1588,7 +1755,7 @@ fn open_file_in_place(
|
||||
|
||||
let action = Action::EditFile {
|
||||
payload: OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
),
|
||||
direction: None,
|
||||
floating,
|
||||
@@ -1626,7 +1793,7 @@ fn open_file_near_plugin(
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
);
|
||||
let title = format!("Editing: {}", open_file_payload.path.display());
|
||||
let start_suppressed = false;
|
||||
@@ -1639,7 +1806,7 @@ fn open_file_near_plugin(
|
||||
Some(title),
|
||||
NewPanePlacement::default().with_border_style(border_style),
|
||||
start_suppressed,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
);
|
||||
@@ -1673,7 +1840,7 @@ fn open_file_floating_near_plugin(
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
);
|
||||
let title = format!("Editing: {}", open_file_payload.path.display());
|
||||
let start_suppressed = false;
|
||||
@@ -1686,7 +1853,7 @@ fn open_file_floating_near_plugin(
|
||||
Some(title),
|
||||
NewPanePlacement::Floating(floating_pane_coordinates),
|
||||
start_suppressed,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
);
|
||||
@@ -1716,7 +1883,7 @@ fn open_file_in_place_of_plugin(
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
);
|
||||
let title = format!("Editing: {}", open_file_payload.path.display());
|
||||
let open_file = TerminalAction::OpenFile(open_file_payload);
|
||||
@@ -1727,7 +1894,7 @@ fn open_file_in_place_of_plugin(
|
||||
Some(open_file),
|
||||
Some(title),
|
||||
close_plugin_after_replace,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
);
|
||||
let _ = env.senders.send_to_pty(pty_instr);
|
||||
@@ -1807,7 +1974,7 @@ fn open_terminal_near_plugin(
|
||||
border_style,
|
||||
},
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
));
|
||||
@@ -1886,7 +2053,7 @@ fn open_terminal_floating_near_plugin(
|
||||
name,
|
||||
NewPanePlacement::Floating(floating_pane_coordinates),
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
));
|
||||
@@ -1964,7 +2131,7 @@ fn open_terminal_in_place_of_plugin(
|
||||
Some(default_shell),
|
||||
name,
|
||||
close_plugin_after_replace,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
|
||||
@@ -2006,7 +2173,7 @@ fn open_command_pane_in_place_of_plugin(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2021,7 +2188,7 @@ fn open_command_pane_in_place_of_plugin(
|
||||
Some(run_cmd),
|
||||
name,
|
||||
close_plugin_after_replace,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
|
||||
@@ -2106,7 +2273,7 @@ fn open_command_pane_in_place_of_pane_id(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2151,7 +2318,7 @@ fn open_edit_pane_in_place_of_pane_id(
|
||||
let path = translate_plugin_path(env, file_to_open.path);
|
||||
let open_file_payload =
|
||||
OpenFilePayload::new(path, file_to_open.line_number, cwd).with_originating_plugin(
|
||||
OriginatingPlugin::new(env.plugin_id, env.client_id, context),
|
||||
OriginatingPlugin::new(env.plugin_id, acting_client(env), context),
|
||||
);
|
||||
let title = format!("Editing: {}", open_file_payload.path.display());
|
||||
let open_file = TerminalAction::OpenFile(open_file_payload);
|
||||
@@ -2202,7 +2369,7 @@ fn open_command_pane(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2255,7 +2422,7 @@ fn open_command_pane_near_plugin(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2273,7 +2440,7 @@ fn open_command_pane_near_plugin(
|
||||
border_style,
|
||||
},
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
));
|
||||
@@ -2316,7 +2483,7 @@ fn open_command_pane_floating(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2371,7 +2538,7 @@ fn open_command_pane_floating_near_plugin(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2388,7 +2555,7 @@ fn open_command_pane_floating_near_plugin(
|
||||
border_style,
|
||||
)),
|
||||
false,
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(env.plugin_id)),
|
||||
ClientTabIndexOrPaneId::PaneId(PaneId::Plugin(self_pane_id(env))),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
));
|
||||
@@ -2435,7 +2602,7 @@ fn open_command_pane_in_place(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2488,7 +2655,7 @@ fn open_command_pane_background(
|
||||
hold_on_start,
|
||||
originating_plugin: Some(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
use_terminal_title,
|
||||
@@ -2502,7 +2669,7 @@ fn open_command_pane_background(
|
||||
name,
|
||||
NewPanePlacement::default(),
|
||||
start_suppressed,
|
||||
ClientTabIndexOrPaneId::ClientId(env.client_id),
|
||||
ClientTabIndexOrPaneId::ClientId(acting_client(env)),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
false, // set_blocking
|
||||
));
|
||||
@@ -2528,7 +2695,7 @@ fn switch_tab_to(env: &PluginEnv, tab_idx: u32) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GoToTab(
|
||||
tab_idx,
|
||||
Some(env.client_id),
|
||||
Some(acting_client(env)),
|
||||
None,
|
||||
))
|
||||
.with_context(|| {
|
||||
@@ -2543,7 +2710,7 @@ fn switch_tab_to(env: &PluginEnv, tab_idx: u32) {
|
||||
fn set_timeout(env: &PluginEnv, secs: f64) {
|
||||
let send_plugin_instructions = env.senders.to_plugin.clone();
|
||||
let update_target = Some(env.plugin_id);
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
let plugin_name = env.name();
|
||||
// Use tokio runtime for async I/O (timer operation)
|
||||
get_tokio_runtime().spawn(async move {
|
||||
@@ -2615,7 +2782,7 @@ fn run_command(
|
||||
.senders
|
||||
.send_to_background_jobs(BackgroundJob::RunCommand(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
command,
|
||||
command_line,
|
||||
env_variables,
|
||||
@@ -2637,7 +2804,7 @@ fn web_request(
|
||||
.senders
|
||||
.send_to_background_jobs(BackgroundJob::WebRequest(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
url,
|
||||
verb,
|
||||
headers,
|
||||
@@ -2653,7 +2820,7 @@ fn post_message_to(env: &PluginEnv, plugin_message: PluginMessage) -> Result<()>
|
||||
env.senders
|
||||
.send_to_plugin(PluginInstruction::PostMessagesToPluginWorker(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
worker_name,
|
||||
vec![(plugin_message.name, plugin_message.payload)],
|
||||
))
|
||||
@@ -2669,7 +2836,7 @@ fn post_message_to_plugin(env: &PluginEnv, plugin_message: PluginMessage) -> Res
|
||||
env.senders
|
||||
.send_to_plugin(PluginInstruction::PostMessageToPlugin(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
plugin_message.name,
|
||||
plugin_message.payload,
|
||||
))
|
||||
@@ -2678,21 +2845,21 @@ fn post_message_to_plugin(env: &PluginEnv, plugin_message: PluginMessage) -> Res
|
||||
fn hide_self(env: &PluginEnv) -> Result<()> {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SuppressPane(
|
||||
PaneId::Plugin(env.plugin_id),
|
||||
env.client_id,
|
||||
PaneId::Plugin(self_pane_id(env)),
|
||||
acting_client(env),
|
||||
))
|
||||
.with_context(|| format!("failed to hide self"))
|
||||
}
|
||||
|
||||
fn hide_pane_with_id(env: &PluginEnv, pane_id: PaneId) -> Result<()> {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SuppressPane(pane_id, env.client_id))
|
||||
.send_to_screen(ScreenInstruction::SuppressPane(pane_id, acting_client(env)))
|
||||
.with_context(|| format!("failed to hide self"))
|
||||
}
|
||||
|
||||
fn show_self(env: &PluginEnv, should_float_if_hidden: bool) {
|
||||
let action = Action::FocusPluginPaneWithId {
|
||||
pane_id: env.plugin_id,
|
||||
pane_id: self_pane_id(env),
|
||||
should_float_if_hidden,
|
||||
should_be_in_place_if_hidden: false,
|
||||
};
|
||||
@@ -2713,7 +2880,7 @@ fn show_pane_with_id(
|
||||
pane_id,
|
||||
should_float_if_hidden,
|
||||
false,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
None,
|
||||
));
|
||||
} else {
|
||||
@@ -2729,7 +2896,7 @@ fn show_pane_with_id(
|
||||
fn close_self(env: &PluginEnv) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::ClosePane(
|
||||
PaneId::Plugin(env.plugin_id),
|
||||
PaneId::Plugin(self_pane_id(env)),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
@@ -2737,14 +2904,14 @@ fn close_self(env: &PluginEnv) {
|
||||
.with_context(|| format!("failed to close self"))
|
||||
.non_fatal();
|
||||
env.senders
|
||||
.send_to_plugin(PluginInstruction::Unload(env.plugin_id))
|
||||
.send_to_plugin(PluginInstruction::Unload(self_pane_id(env)))
|
||||
.with_context(|| format!("failed to close self"))
|
||||
.non_fatal();
|
||||
}
|
||||
|
||||
fn reconfigure(env: &PluginEnv, new_config: String, write_config_to_disk: bool) -> Result<()> {
|
||||
let err_context = || "Failed to reconfigure";
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
env.senders
|
||||
.send_to_server(ServerInstruction::Reconfigure {
|
||||
client_id,
|
||||
@@ -2762,7 +2929,7 @@ fn rebind_keys(
|
||||
write_config_to_disk: bool,
|
||||
) -> Result<()> {
|
||||
let err_context = || "Failed to rebind_keys";
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
env.senders
|
||||
.send_to_server(ServerInstruction::RebindKeys {
|
||||
client_id,
|
||||
@@ -3038,7 +3205,7 @@ fn switch_session(
|
||||
{
|
||||
log::error!("Session names cannot contain \'/\'");
|
||||
} else {
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
let tab_position = tab_position.map(|p| p + 1); // ¯\_()_/¯
|
||||
let cwd = cwd
|
||||
.map(|c| translate_plugin_path(env, c))
|
||||
@@ -3219,7 +3386,10 @@ fn toggle_focus_no_ui_fullscreen(env: &PluginEnv) {
|
||||
|
||||
fn focus_host_session(env: &PluginEnv) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::FocusHostSession(env.client_id, None))
|
||||
.send_to_screen(ScreenInstruction::FocusHostSession(
|
||||
acting_client(env),
|
||||
None,
|
||||
))
|
||||
.with_context(|| format!("failed to focus host session from plugin {}", env.name()))
|
||||
.non_fatal();
|
||||
}
|
||||
@@ -3513,7 +3683,9 @@ fn rename_session(env: &PluginEnv, new_session_name: String) {
|
||||
fn disconnect_other_clients(env: &PluginEnv) {
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::DisconnectAllClientsExcept(env.client_id))
|
||||
.send_to_server(ServerInstruction::DisconnectAllClientsExcept(
|
||||
acting_client(env),
|
||||
))
|
||||
.context("failed to send disconnect other clients instruction");
|
||||
}
|
||||
|
||||
@@ -3733,7 +3905,7 @@ fn save_session(env: &PluginEnv) {
|
||||
let (completion_tx, completion_rx) = oneshot::channel();
|
||||
|
||||
let send_result = env.senders.send_to_screen(ScreenInstruction::SaveSession(
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
Some(NotificationEnd::new(completion_tx)),
|
||||
));
|
||||
|
||||
@@ -3761,7 +3933,7 @@ fn show_floating_panes(env: &PluginEnv, tab_id: Option<usize>) {
|
||||
let send_result = env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::ShowFloatingPanes {
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
tab_id,
|
||||
completion: Some(NotificationEnd::new(completion_tx)),
|
||||
});
|
||||
@@ -3792,7 +3964,7 @@ fn hide_floating_panes(env: &PluginEnv, tab_id: Option<usize>) {
|
||||
let send_result = env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::HideFloatingPanes {
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
tab_id,
|
||||
completion: Some(NotificationEnd::new(completion_tx)),
|
||||
});
|
||||
@@ -4008,7 +4180,7 @@ fn list_clients(env: &PluginEnv) {
|
||||
let _ = env.senders.to_screen.as_ref().map(|sender| {
|
||||
sender.send(ScreenInstruction::ListClientsToPlugin(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
))
|
||||
});
|
||||
}
|
||||
@@ -4018,7 +4190,7 @@ fn change_host_folder(env: &PluginEnv, new_host_folder: PathBuf) {
|
||||
sender.send(PluginInstruction::ChangePluginHostDir(
|
||||
new_host_folder,
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
))
|
||||
});
|
||||
}
|
||||
@@ -4033,9 +4205,11 @@ fn set_floating_pane_pinned(env: &PluginEnv, pane_id: PaneId, should_be_pinned:
|
||||
}
|
||||
|
||||
fn stack_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
|
||||
let _ =
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::StackPanes(pane_ids, env.client_id, None));
|
||||
let _ = env.senders.send_to_screen(ScreenInstruction::StackPanes(
|
||||
pane_ids,
|
||||
acting_client(env),
|
||||
None,
|
||||
));
|
||||
}
|
||||
|
||||
fn change_floating_panes_coordinates(
|
||||
@@ -4108,7 +4282,7 @@ fn scan_host_folder(env: &PluginEnv, folder_to_scan: PathBuf) {
|
||||
Ok(reading_folder) => {
|
||||
let send_plugin_instructions = env.senders.to_plugin.clone();
|
||||
let update_target = Some(env.plugin_id);
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
thread::spawn({
|
||||
move || {
|
||||
let mut paths_in_folder = vec![];
|
||||
@@ -4158,7 +4332,7 @@ fn list_windows_volumes(_env: &PluginEnv) {
|
||||
fn set_soft_keyboard(env: &PluginEnv, on: bool) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SetSoftKeyboard {
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
on,
|
||||
})
|
||||
.with_context(|| {
|
||||
@@ -4174,7 +4348,7 @@ fn set_soft_keyboard(env: &PluginEnv, on: bool) {
|
||||
fn list_windows_volumes(env: &PluginEnv) {
|
||||
let send_plugin_instructions = env.senders.to_plugin.clone();
|
||||
let update_target = Some(env.plugin_id);
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
thread::spawn(move || {
|
||||
let mut entries = enumerate_drives();
|
||||
entries.extend(enumerate_wsl_distributions());
|
||||
@@ -4280,7 +4454,7 @@ fn get_pane_scrollback(env: &PluginEnv, pane_id: PaneId, get_full_scrollback: bo
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GetPaneScrollback {
|
||||
pane_id,
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
get_full_scrollback,
|
||||
response_channel: response_sender,
|
||||
})
|
||||
@@ -4829,7 +5003,7 @@ fn try_edit_layout(
|
||||
)
|
||||
.with_originating_plugin(OriginatingPlugin::new(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
context,
|
||||
)),
|
||||
direction: None,
|
||||
@@ -4845,9 +5019,9 @@ fn try_edit_layout(
|
||||
// Route the action - this is fallible
|
||||
route_action(
|
||||
action,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
None,
|
||||
Some(PaneId::Plugin(env.plugin_id)),
|
||||
source_pane(env),
|
||||
env.senders.clone(),
|
||||
env.default_shell.clone(),
|
||||
None,
|
||||
@@ -4995,7 +5169,7 @@ fn break_panes_to_new_tab(
|
||||
default_shell,
|
||||
new_tab_name,
|
||||
should_change_focus_to_new_tab,
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
completion_tx,
|
||||
});
|
||||
|
||||
@@ -5024,7 +5198,7 @@ fn break_panes_to_tab_with_index(
|
||||
.send_to_screen(ScreenInstruction::BreakPanesToTabWithIndex {
|
||||
pane_ids,
|
||||
tab_index,
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
should_change_focus_to_new_tab,
|
||||
completion_tx,
|
||||
});
|
||||
@@ -5044,7 +5218,7 @@ fn switch_tab_to_id(env: &PluginEnv, tab_id: u64) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GoToTabWithId(
|
||||
tab_id,
|
||||
Some(env.client_id),
|
||||
Some(acting_client(env)),
|
||||
None,
|
||||
))
|
||||
.with_context(|| {
|
||||
@@ -5062,7 +5236,7 @@ fn go_to_tab_with_id(env: &PluginEnv, tab_id: u64) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::GoToTabWithId(
|
||||
tab_id,
|
||||
Some(env.client_id),
|
||||
Some(acting_client(env)),
|
||||
None,
|
||||
))
|
||||
.with_context(|| format!("failed to go to tab {} from plugin {}", tab_id, env.name()))
|
||||
@@ -5104,7 +5278,7 @@ fn break_panes_to_tab_with_id(
|
||||
pane_ids,
|
||||
tab_id,
|
||||
should_change_focus_to_target_tab,
|
||||
client_id: env.client_id,
|
||||
client_id: acting_client(env),
|
||||
completion_tx,
|
||||
});
|
||||
|
||||
@@ -5143,7 +5317,7 @@ fn load_new_plugin(
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::LoadBackgroundPlugin(
|
||||
run_plugin_or_alias,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
},
|
||||
Err(e) => {
|
||||
@@ -5156,7 +5330,7 @@ fn load_new_plugin(
|
||||
let pane_title = None;
|
||||
let tab_index = None;
|
||||
let pane_id_to_replace = None;
|
||||
let client_id = env.client_id;
|
||||
let client_id = acting_client(env);
|
||||
let size = Default::default();
|
||||
let cwd = Some(env.plugin_cwd.clone());
|
||||
let skip_cache = skip_plugin_cache;
|
||||
@@ -5190,7 +5364,7 @@ fn load_new_plugin(
|
||||
fn start_web_server(env: &PluginEnv) {
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::StartWebServer(env.client_id));
|
||||
.send_to_server(ServerInstruction::StartWebServer(acting_client(env)));
|
||||
}
|
||||
|
||||
fn stop_web_server(_env: &PluginEnv) {
|
||||
@@ -5209,13 +5383,15 @@ fn query_web_server_status(env: &PluginEnv) {
|
||||
fn share_current_session(env: &PluginEnv) {
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::ShareCurrentSession(env.client_id));
|
||||
.send_to_server(ServerInstruction::ShareCurrentSession(acting_client(env)));
|
||||
}
|
||||
|
||||
fn stop_sharing_current_session(env: &PluginEnv) {
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_server(ServerInstruction::StopSharingCurrentSession(env.client_id));
|
||||
.send_to_server(ServerInstruction::StopSharingCurrentSession(acting_client(
|
||||
env,
|
||||
)));
|
||||
}
|
||||
|
||||
fn group_and_ungroup_panes(
|
||||
@@ -5230,7 +5406,7 @@ fn group_and_ungroup_panes(
|
||||
panes_to_group,
|
||||
panes_to_ungroup,
|
||||
for_all_clients,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5244,7 +5420,7 @@ fn highlight_and_unhighlight_panes(
|
||||
.send_to_screen(ScreenInstruction::HighlightAndUnhighlightPanes(
|
||||
panes_to_highlight,
|
||||
panes_to_unhighlight,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5266,7 +5442,7 @@ fn float_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::FloatMultiplePanes(
|
||||
pane_ids,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5275,7 +5451,7 @@ fn embed_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::EmbedMultiplePanes(
|
||||
pane_ids,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5402,7 +5578,7 @@ fn list_web_login_tokens(env: &PluginEnv) {
|
||||
fn set_self_mouse_selection_support(env: &PluginEnv, selection_support: bool) {
|
||||
env.senders
|
||||
.send_to_screen(ScreenInstruction::SetMouseSelectionSupport(
|
||||
PaneId::Plugin(env.plugin_id),
|
||||
PaneId::Plugin(self_pane_id(env)),
|
||||
selection_support,
|
||||
))
|
||||
.with_context(|| {
|
||||
@@ -5421,7 +5597,7 @@ fn intercept_key_presses(env: &mut PluginEnv) {
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::InterceptKeyPresses(
|
||||
env.plugin_id,
|
||||
env.client_id,
|
||||
acting_client(env),
|
||||
));
|
||||
}
|
||||
|
||||
@@ -5429,7 +5605,9 @@ fn clear_key_presses_intercepts(env: &mut PluginEnv) {
|
||||
env.intercepting_key_presses = false;
|
||||
let _ = env
|
||||
.senders
|
||||
.send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(env.client_id));
|
||||
.send_to_screen(ScreenInstruction::ClearKeyPressesIntercepts(acting_client(
|
||||
env,
|
||||
)));
|
||||
}
|
||||
|
||||
fn replace_pane_with_existing_pane(
|
||||
@@ -5508,7 +5686,9 @@ fn override_layout(
|
||||
// formatted as string from the plugin.
|
||||
fn report_panic(env: &PluginEnv, msg: &str) {
|
||||
log::error!("PANIC IN PLUGIN!\n\r{}", msg);
|
||||
handle_plugin_crash(env.plugin_id, msg.to_owned(), env.senders.clone());
|
||||
for pane_id in crash_pane_ids(env) {
|
||||
handle_plugin_crash(pane_id, msg.to_owned(), env.senders.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Helper Functions ---------------------------------------------------------------------------------------------------
|
||||
|
||||
@@ -1833,6 +1833,10 @@ impl Pty {
|
||||
.send_to_plugin(PluginInstruction::Unload(pid)),
|
||||
),
|
||||
}
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_background_jobs(BackgroundJob::TrimAllocator);
|
||||
Ok(())
|
||||
}
|
||||
pub fn close_tab(&mut self, ids: Vec<PaneId>) -> Result<()> {
|
||||
|
||||
+92
-27
@@ -34,9 +34,11 @@ use std::net::{IpAddr, Ipv4Addr};
|
||||
use std::path::PathBuf;
|
||||
use std::rc::Rc;
|
||||
use std::str;
|
||||
use std::sync::Arc;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
use crate::route::NotificationEnd;
|
||||
use crate::SharedKeybinds;
|
||||
|
||||
use log::{debug, warn};
|
||||
use zellij_utils::data::{
|
||||
@@ -805,7 +807,7 @@ pub enum ScreenInstruction {
|
||||
},
|
||||
Reconfigure {
|
||||
client_id: ClientId,
|
||||
keybinds: Keybinds,
|
||||
keybinds: SharedKeybinds,
|
||||
default_mode: InputMode,
|
||||
theme: Styling,
|
||||
/// Resolved styling for `theme_dark`. When both this and
|
||||
@@ -1604,6 +1606,8 @@ pub(crate) struct Screen {
|
||||
pane_history: BTreeMap<ClientId, Vec<PaneId>>,
|
||||
mode_info: BTreeMap<ClientId, ModeInfo>,
|
||||
default_mode_info: ModeInfo, // TODO: restructure ModeInfo to prevent this duplication
|
||||
default_keybinds: SharedKeybinds,
|
||||
client_keybinds: BTreeMap<ClientId, SharedKeybinds>,
|
||||
style: Style,
|
||||
pane_frame_style: PaneFrameStyle,
|
||||
auto_layout: bool,
|
||||
@@ -1656,6 +1660,8 @@ pub(crate) struct Screen {
|
||||
pane_render_subscribers: HashMap<ClientId, PaneRenderSubscription>,
|
||||
background_plugin_subscriptions: HashMap<(PluginId, ClientId), HashSet<EventType>>,
|
||||
last_reported_plugin_tab_indices: HashMap<PluginId, usize>,
|
||||
last_reported_client_visible_plugins: HashMap<ClientId, HashSet<PluginId>>,
|
||||
pending_selectable_panes: HashMap<PaneId, bool>,
|
||||
state_report_target: Option<(PluginId, ClientId)>,
|
||||
next_forward_token: u32,
|
||||
pending_forwarded_queries: HashMap<u32, PendingForwardEntry>,
|
||||
@@ -1763,7 +1769,7 @@ impl Screen {
|
||||
bus: Bus<ScreenInstruction>,
|
||||
client_attributes: &ClientAttributes,
|
||||
max_panes: Option<usize>,
|
||||
mode_info: ModeInfo,
|
||||
mut mode_info: ModeInfo,
|
||||
pane_frame_style: PaneFrameStyle,
|
||||
auto_layout: bool,
|
||||
session_is_mirrored: bool,
|
||||
@@ -1832,6 +1838,8 @@ impl Screen {
|
||||
tab_history: BTreeMap::new(),
|
||||
pane_history: BTreeMap::new(),
|
||||
mode_info: BTreeMap::new(),
|
||||
default_keybinds: Arc::new(std::mem::take(&mut mode_info.keybinds)),
|
||||
client_keybinds: BTreeMap::new(),
|
||||
default_mode_info: mode_info,
|
||||
pane_frame_style,
|
||||
auto_layout,
|
||||
@@ -1878,6 +1886,8 @@ impl Screen {
|
||||
pane_render_subscribers: HashMap::new(),
|
||||
background_plugin_subscriptions: HashMap::new(),
|
||||
last_reported_plugin_tab_indices: HashMap::new(),
|
||||
last_reported_client_visible_plugins: HashMap::new(),
|
||||
pending_selectable_panes: HashMap::new(),
|
||||
state_report_target: None,
|
||||
next_forward_token: 1, // 0 is reserved as the startup sentinel
|
||||
pending_forwarded_queries: HashMap::new(),
|
||||
@@ -4146,6 +4156,7 @@ impl Screen {
|
||||
if !changed && !ascend_keys_changed {
|
||||
return;
|
||||
}
|
||||
self.ensure_client_keybinds(client_id);
|
||||
let mode_info = self
|
||||
.mode_info
|
||||
.entry(client_id)
|
||||
@@ -4169,29 +4180,23 @@ impl Screen {
|
||||
}
|
||||
|
||||
fn own_ascend_shortcut(&self) -> Vec<KeyWithModifier> {
|
||||
shortcut_for_action(
|
||||
&self.default_mode_info.keybinds,
|
||||
self.base_input_mode(),
|
||||
|action| matches!(action, Action::FocusHostSession),
|
||||
)
|
||||
shortcut_for_action(&self.default_keybinds, self.base_input_mode(), |action| {
|
||||
matches!(action, Action::FocusHostSession)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn own_descend_shortcut(&self) -> Vec<KeyWithModifier> {
|
||||
shortcut_for_action(
|
||||
&self.default_mode_info.keybinds,
|
||||
self.base_input_mode(),
|
||||
|action| matches!(action, Action::FocusGuestSession),
|
||||
)
|
||||
shortcut_for_action(&self.default_keybinds, self.base_input_mode(), |action| {
|
||||
matches!(action, Action::FocusGuestSession)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn own_host_zoom_shortcut(&self) -> Vec<KeyWithModifier> {
|
||||
shortcut_for_action(
|
||||
&self.default_mode_info.keybinds,
|
||||
self.base_input_mode(),
|
||||
|action| matches!(action, Action::ToggleHostFullscreen),
|
||||
)
|
||||
shortcut_for_action(&self.default_keybinds, self.base_input_mode(), |action| {
|
||||
matches!(action, Action::ToggleHostFullscreen)
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -4292,6 +4297,7 @@ impl Screen {
|
||||
}
|
||||
}
|
||||
for client_id in client_ids {
|
||||
self.ensure_client_keybinds(client_id);
|
||||
let mode_info = self
|
||||
.mode_info
|
||||
.entry(client_id)
|
||||
@@ -5751,6 +5757,8 @@ impl Screen {
|
||||
self.recompute_tab_size(prev_tab_id)
|
||||
.with_context(err_context)?;
|
||||
}
|
||||
self.mode_info.remove(&client_id);
|
||||
self.client_keybinds.remove(&client_id);
|
||||
self.held_keybinds_requests.remove(&client_id);
|
||||
self.pending_keybinds_requests.retain(|_, pending| {
|
||||
!matches!(pending.reply_to, KeybindsReplyTo::Host { client_id: host_client_id, .. } if host_client_id == client_id)
|
||||
@@ -6057,14 +6065,52 @@ impl Screen {
|
||||
.send_to_plugin(PluginInstruction::UpdatePluginTabIndices(tab_indices));
|
||||
self.last_reported_plugin_tab_indices = current;
|
||||
}
|
||||
fn apply_pending_selectable_panes(&mut self) {
|
||||
if self.pending_selectable_panes.is_empty() {
|
||||
return;
|
||||
}
|
||||
let pending: Vec<(PaneId, bool)> = self.pending_selectable_panes.drain().collect();
|
||||
for (pane_id, selectable) in pending {
|
||||
let tab = self
|
||||
.tabs
|
||||
.values_mut()
|
||||
.find(|tab| tab.has_pane_with_pid(&pane_id));
|
||||
match tab {
|
||||
Some(tab) => tab.set_pane_selectable(pane_id, selectable),
|
||||
None => {
|
||||
self.pending_selectable_panes.insert(pane_id, selectable);
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
fn report_client_visible_plugins(&mut self) {
|
||||
let mut visible: HashMap<ClientId, HashSet<PluginId>> = HashMap::new();
|
||||
for (client_id, tab_id) in self.active_tab_ids.iter() {
|
||||
if let Some(tab) = self.tabs.get(tab_id) {
|
||||
visible.insert(*client_id, tab.get_plugin_ids().into_iter().collect());
|
||||
}
|
||||
}
|
||||
if visible == self.last_reported_client_visible_plugins {
|
||||
return;
|
||||
}
|
||||
let _ = self
|
||||
.bus
|
||||
.senders
|
||||
.send_to_plugin(PluginInstruction::UpdateClientVisiblePlugins(
|
||||
visible.clone(),
|
||||
));
|
||||
self.last_reported_client_visible_plugins = visible;
|
||||
}
|
||||
fn log_and_report_session_state(&mut self) -> Result<()> {
|
||||
let err_context = || format!("Failed to log and report session state");
|
||||
self.apply_pending_selectable_panes();
|
||||
|
||||
self.update_active_pane_ids();
|
||||
self.revert_fit_disabled_without_reference_client()
|
||||
.with_context(err_context)?;
|
||||
self.reconcile_all_single_pane_focus();
|
||||
self.report_plugin_tab_indices();
|
||||
self.report_client_visible_plugins();
|
||||
// generate own session info
|
||||
let pane_manifest = self.generate_and_report_pane_state()?;
|
||||
let tab_infos = self.generate_and_report_tab_state()?;
|
||||
@@ -6610,6 +6656,7 @@ impl Screen {
|
||||
base_mode: Option<InputMode>,
|
||||
client_id: ClientId,
|
||||
) -> Result<()> {
|
||||
self.ensure_client_keybinds(client_id);
|
||||
let mut mode_info = self
|
||||
.mode_info
|
||||
.get(&client_id)
|
||||
@@ -6687,10 +6734,25 @@ impl Screen {
|
||||
}
|
||||
|
||||
fn keybinds_for_client(&self, client_id: ClientId) -> KeybindsVec {
|
||||
self.mode_info
|
||||
self.client_keybinds
|
||||
.get(&client_id)
|
||||
.map(|mode_info| mode_info.keybinds.clone())
|
||||
.unwrap_or_else(|| self.default_mode_info.keybinds.clone())
|
||||
.unwrap_or(&self.default_keybinds)
|
||||
.as_ref()
|
||||
.clone()
|
||||
}
|
||||
|
||||
fn update_keybinds(&mut self, new_keybinds: SharedKeybinds, client_id: ClientId) {
|
||||
self.default_keybinds = new_keybinds.clone();
|
||||
if self.connected_clients_contains(&client_id) {
|
||||
self.client_keybinds.insert(client_id, new_keybinds);
|
||||
}
|
||||
}
|
||||
|
||||
fn ensure_client_keybinds(&mut self, client_id: ClientId) {
|
||||
if !self.client_keybinds.contains_key(&client_id) {
|
||||
self.client_keybinds
|
||||
.insert(client_id, self.default_keybinds.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the client's mode in sync with the pane it just focused: entering a scrolled
|
||||
@@ -7482,7 +7544,7 @@ impl Screen {
|
||||
}
|
||||
pub fn reconfigure(
|
||||
&mut self,
|
||||
new_keybinds: Keybinds,
|
||||
new_keybinds: SharedKeybinds,
|
||||
new_default_mode: InputMode,
|
||||
theme: Styling,
|
||||
simplified_ui: bool,
|
||||
@@ -7529,7 +7591,7 @@ impl Screen {
|
||||
// `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.
|
||||
self.default_mode_info.update_keybinds(new_keybinds.clone());
|
||||
self.update_keybinds(new_keybinds, client_id);
|
||||
self.default_mode_info.update_default_mode(new_default_mode);
|
||||
self.default_shell = default_shell.clone().unwrap_or_else(|| get_default_shell());
|
||||
self.default_editor = default_editor.clone().or_else(|| get_default_editor());
|
||||
@@ -7596,7 +7658,6 @@ impl Screen {
|
||||
.mode_info
|
||||
.entry(client_id)
|
||||
.or_insert_with(|| self.default_mode_info.clone());
|
||||
mode_info.update_keybinds(new_keybinds);
|
||||
mode_info.update_default_mode(new_default_mode);
|
||||
mode_info.update_theme(theme);
|
||||
mode_info.update_arrow_fonts(should_support_arrow_fonts);
|
||||
@@ -7658,6 +7719,7 @@ impl Screen {
|
||||
let client_ids: Vec<ClientId> = self.active_tab_ids.keys().copied().collect();
|
||||
let default_for_new = self.default_mode_info.clone();
|
||||
for client_id in client_ids {
|
||||
self.ensure_client_keybinds(client_id);
|
||||
let mode_info = self
|
||||
.mode_info
|
||||
.entry(client_id)
|
||||
@@ -8704,6 +8766,7 @@ pub(crate) fn screen_thread_main(
|
||||
config: Config,
|
||||
debug: bool,
|
||||
default_layout: Box<Layout>,
|
||||
default_keybinds: SharedKeybinds,
|
||||
) -> Result<()> {
|
||||
// Resolve `theme_dark` / `theme_light` to concrete `Styling` from the
|
||||
// bundled themes BEFORE `config.options` is moved out below. These
|
||||
@@ -8821,7 +8884,7 @@ pub(crate) fn screen_thread_main(
|
||||
// ¯\_(ツ)_/¯
|
||||
arrow_fonts: !arrow_fonts,
|
||||
},
|
||||
&config.keybinds,
|
||||
&Keybinds::default(),
|
||||
config_options.default_mode,
|
||||
),
|
||||
pane_frame_style,
|
||||
@@ -8860,6 +8923,7 @@ pub(crate) fn screen_thread_main(
|
||||
web_server_port,
|
||||
nested_session_handling,
|
||||
);
|
||||
screen.default_keybinds = default_keybinds;
|
||||
screen.host_theme_dark_styling = host_theme_dark_styling;
|
||||
screen.host_theme_light_styling = host_theme_light_styling;
|
||||
screen.paste_buffer_read_enabled = dangerously_enable_paste_buffer_read;
|
||||
@@ -10198,9 +10262,10 @@ pub(crate) fn screen_thread_main(
|
||||
break;
|
||||
}
|
||||
}
|
||||
if !found_plugin {
|
||||
pending_events_waiting_for_tab
|
||||
.push(ScreenInstruction::SetSelectable(pid, selectable));
|
||||
if found_plugin {
|
||||
screen.pending_selectable_panes.remove(&pid);
|
||||
} else {
|
||||
screen.pending_selectable_panes.insert(pid, selectable);
|
||||
}
|
||||
screen.render(None)?;
|
||||
screen.log_and_report_session_state()?;
|
||||
|
||||
@@ -937,7 +937,7 @@ impl Tab {
|
||||
fullscreen_covers_ui.clone(),
|
||||
session_is_mirrored,
|
||||
pane_frame_style,
|
||||
default_mode_info.clone(),
|
||||
default_mode_info.mode,
|
||||
style,
|
||||
os_api.clone(),
|
||||
senders.clone(),
|
||||
@@ -953,7 +953,7 @@ impl Tab {
|
||||
fullscreen_covers_ui.clone(),
|
||||
pane_frame_style,
|
||||
session_is_mirrored,
|
||||
default_mode_info.clone(),
|
||||
default_mode_info.mode,
|
||||
style,
|
||||
os_api.clone(),
|
||||
senders.clone(),
|
||||
@@ -2350,10 +2350,7 @@ impl Tab {
|
||||
None
|
||||
};
|
||||
}
|
||||
self.mode_info
|
||||
.borrow_mut()
|
||||
.get_mut(&client_id)
|
||||
.map(|c| c.change_to_default_mode()); // TODO: no races?
|
||||
self.mode_info.borrow_mut().remove(&client_id);
|
||||
self.connected_clients.borrow_mut().remove(&client_id);
|
||||
self.mouse_help_text_visible.remove(&client_id);
|
||||
self.mouse_last_pane_id.remove(&client_id);
|
||||
|
||||
@@ -206,7 +206,7 @@ fn create_layout_applier_fixtures(
|
||||
fullscreen_covers_ui.clone(),
|
||||
session_is_mirrored,
|
||||
draw_pane_frames,
|
||||
default_mode_info.clone(),
|
||||
default_mode_info.mode,
|
||||
style.clone(),
|
||||
os_api.box_clone(),
|
||||
senders.clone(),
|
||||
@@ -224,7 +224,7 @@ fn create_layout_applier_fixtures(
|
||||
fullscreen_covers_ui,
|
||||
draw_pane_frames,
|
||||
session_is_mirrored,
|
||||
default_mode_info,
|
||||
default_mode_info.mode,
|
||||
style.clone(),
|
||||
os_api.box_clone(),
|
||||
senders.clone(),
|
||||
@@ -344,7 +344,7 @@ fn create_layout_applier_fixtures_with_receivers(
|
||||
fullscreen_covers_ui.clone(),
|
||||
session_is_mirrored,
|
||||
draw_pane_frames,
|
||||
default_mode_info.clone(),
|
||||
default_mode_info.mode,
|
||||
style.clone(),
|
||||
os_api.box_clone(),
|
||||
senders.clone(),
|
||||
@@ -362,7 +362,7 @@ fn create_layout_applier_fixtures_with_receivers(
|
||||
fullscreen_covers_ui,
|
||||
draw_pane_frames,
|
||||
session_is_mirrored,
|
||||
default_mode_info,
|
||||
default_mode_info.mode,
|
||||
style.clone(),
|
||||
os_api.box_clone(),
|
||||
senders.clone(),
|
||||
|
||||
@@ -10,6 +10,8 @@ use zellij_utils::{
|
||||
logging::debug_to_file,
|
||||
};
|
||||
|
||||
const READ_BUFFER_SIZE: usize = 65536;
|
||||
|
||||
pub(crate) struct TerminalBytes {
|
||||
terminal_id: u32,
|
||||
senders: ThreadSenders,
|
||||
@@ -50,26 +52,21 @@ impl TerminalBytes {
|
||||
|
||||
let mut err_ctx = get_current_ctx();
|
||||
err_ctx.add_call(ContextType::AsyncTask);
|
||||
let mut buf = [0u8; 65536];
|
||||
loop {
|
||||
match self.async_reader.read(&mut buf).await {
|
||||
Ok(0) => break, // EOF
|
||||
match self.async_reader.read_chunk(READ_BUFFER_SIZE).await {
|
||||
Ok(bytes) if bytes.is_empty() => break,
|
||||
Err(err) => {
|
||||
log::error!("{}", err);
|
||||
break;
|
||||
},
|
||||
Ok(n_bytes) => {
|
||||
Ok(bytes) => {
|
||||
self.activity_flag.store(true, Ordering::Relaxed);
|
||||
let bytes = &buf[..n_bytes];
|
||||
if self.debug {
|
||||
let _ = debug_to_file(bytes, self.terminal_id as i32);
|
||||
let _ = debug_to_file(&bytes, self.terminal_id as i32);
|
||||
}
|
||||
self.async_send_to_screen(ScreenInstruction::PtyBytes(
|
||||
self.terminal_id,
|
||||
bytes.to_vec(),
|
||||
))
|
||||
.await
|
||||
.with_context(err_context)?;
|
||||
self.async_send_to_screen(ScreenInstruction::PtyBytes(self.terminal_id, bytes))
|
||||
.await
|
||||
.with_context(err_context)?;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,6 @@ fn spawn_and_read_output() {
|
||||
|
||||
// Read output from the spawned terminal
|
||||
let mut output = Vec::new();
|
||||
let mut buf = [0u8; 4096];
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(5);
|
||||
|
||||
let rt = tokio::runtime::Builder::new_current_thread()
|
||||
@@ -170,12 +169,15 @@ fn spawn_and_read_output() {
|
||||
if std::time::Instant::now() > deadline {
|
||||
break;
|
||||
}
|
||||
match tokio::time::timeout(std::time::Duration::from_millis(500), reader.read(&mut buf))
|
||||
.await
|
||||
match tokio::time::timeout(
|
||||
std::time::Duration::from_millis(500),
|
||||
reader.read_chunk(4096),
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Ok(0)) => break,
|
||||
Ok(Ok(n)) => {
|
||||
output.extend_from_slice(&buf[..n]);
|
||||
Ok(Ok(bytes)) if bytes.is_empty() => break,
|
||||
Ok(Ok(bytes)) => {
|
||||
output.extend_from_slice(&bytes);
|
||||
let s = String::from_utf8_lossy(&output);
|
||||
if s.contains(test_message) {
|
||||
break;
|
||||
@@ -255,3 +257,30 @@ fn tcgetpgrp_returns_foreground_group() {
|
||||
let _ = server.force_kill(child_pid);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_buffer_refuses_messages_when_limit_is_reached() {
|
||||
let (buffer, receiver) = client_buffer(CLIENT_BUFFER_LIMIT);
|
||||
let msg = || ServerToClientMsg::Exit {
|
||||
exit_reason: ExitReason::Normal,
|
||||
};
|
||||
for _ in 0..CLIENT_BUFFER_LIMIT {
|
||||
assert!(buffer.try_send(msg()).is_ok());
|
||||
}
|
||||
assert!(matches!(buffer.try_send(msg()), Err(TrySendError::Full(_))));
|
||||
assert!(matches!(buffer.try_send(msg()), Err(TrySendError::Full(_))));
|
||||
assert!(receiver.recv().is_some());
|
||||
assert!(buffer.try_send(msg()).is_ok());
|
||||
assert!(matches!(buffer.try_send(msg()), Err(TrySendError::Full(_))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_buffer_reports_disconnect_when_receiver_is_gone() {
|
||||
let (buffer, receiver) = client_buffer(CLIENT_BUFFER_LIMIT);
|
||||
drop(receiver);
|
||||
let result = buffer.try_send(ServerToClientMsg::Exit {
|
||||
exit_reason: ExitReason::Normal,
|
||||
});
|
||||
assert!(matches!(result, Err(TrySendError::Disconnected(_))));
|
||||
assert_eq!(buffer.queued.load(Ordering::Acquire), 0);
|
||||
}
|
||||
|
||||
@@ -454,6 +454,7 @@ impl MockScreen {
|
||||
)
|
||||
.should_silently_fail();
|
||||
let debug = false;
|
||||
let default_keybinds = std::sync::Arc::new(config.keybinds.to_keybinds_vec());
|
||||
let screen_thread = std::thread::Builder::new()
|
||||
.name("screen_thread".to_string())
|
||||
.spawn(move || {
|
||||
@@ -465,6 +466,7 @@ impl MockScreen {
|
||||
config,
|
||||
debug,
|
||||
Box::new(Layout::default()),
|
||||
default_keybinds,
|
||||
)
|
||||
.expect("TEST")
|
||||
})
|
||||
@@ -540,6 +542,7 @@ impl MockScreen {
|
||||
)
|
||||
.should_silently_fail();
|
||||
let debug = false;
|
||||
let default_keybinds = std::sync::Arc::new(config.keybinds.to_keybinds_vec());
|
||||
let screen_thread = std::thread::Builder::new()
|
||||
.name("screen_thread".to_string())
|
||||
.spawn(move || {
|
||||
@@ -551,6 +554,7 @@ impl MockScreen {
|
||||
config,
|
||||
debug,
|
||||
Box::new(Layout::default()),
|
||||
default_keybinds,
|
||||
)
|
||||
.expect("TEST")
|
||||
})
|
||||
@@ -14824,3 +14828,113 @@ mod nested_hint_reporting {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
fn keybinds_with_quit_on(c: char) -> crate::SharedKeybinds {
|
||||
std::sync::Arc::new(vec![(
|
||||
InputMode::Normal,
|
||||
vec![(
|
||||
zellij_utils::data::KeyWithModifier::new(zellij_utils::data::BareKey::Char(c)),
|
||||
vec![Action::Quit],
|
||||
)],
|
||||
)])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn client_mode_and_keybinds_entries_are_removed_on_disconnect() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let mut screen = create_new_screen(size, true, true);
|
||||
new_tab(&mut screen, 1, 0);
|
||||
new_tab(&mut screen, 2, 1);
|
||||
screen.add_client(2, false).expect("TEST");
|
||||
screen.change_mode(InputMode::Tab, None, 1).expect("TEST");
|
||||
screen.change_mode(InputMode::Pane, None, 2).expect("TEST");
|
||||
assert!(screen.mode_info.contains_key(&2));
|
||||
assert!(screen.client_keybinds.contains_key(&2));
|
||||
|
||||
screen.remove_client(2).expect("TEST");
|
||||
|
||||
assert!(!screen.mode_info.contains_key(&2));
|
||||
assert!(!screen.client_keybinds.contains_key(&2));
|
||||
for tab in screen.tabs.values() {
|
||||
assert_eq!(tab.get_client_input_mode(2), None);
|
||||
assert_eq!(tab.get_client_input_mode(1), Some(InputMode::Tab));
|
||||
}
|
||||
assert!(screen.mode_info.contains_key(&1));
|
||||
assert!(screen.client_keybinds.contains_key(&1));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reconfiguring_one_client_keybinds_does_not_affect_another() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let mut screen = create_new_screen(size, true, true);
|
||||
new_tab(&mut screen, 1, 0);
|
||||
screen.add_client(2, false).expect("TEST");
|
||||
let original = keybinds_with_quit_on('a');
|
||||
screen.update_keybinds(original.clone(), 1);
|
||||
screen.update_keybinds(original.clone(), 2);
|
||||
screen
|
||||
.change_mode(InputMode::Normal, None, 1)
|
||||
.expect("TEST");
|
||||
screen
|
||||
.change_mode(InputMode::Normal, None, 2)
|
||||
.expect("TEST");
|
||||
|
||||
let reconfigured = keybinds_with_quit_on('b');
|
||||
screen.update_keybinds(reconfigured.clone(), 1);
|
||||
|
||||
assert_eq!(screen.keybinds_for_client(1), *reconfigured);
|
||||
assert_eq!(screen.keybinds_for_client(2), *original);
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
screen.client_keybinds.get(&1).unwrap(),
|
||||
&reconfigured
|
||||
));
|
||||
assert!(std::sync::Arc::ptr_eq(
|
||||
&screen.default_keybinds,
|
||||
&reconfigured
|
||||
));
|
||||
for mode_info in screen.mode_info.values() {
|
||||
assert!(mode_info.keybinds.is_empty());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn per_client_modes_are_kept_across_tabs() {
|
||||
let size = Size {
|
||||
cols: 121,
|
||||
rows: 20,
|
||||
};
|
||||
let mut screen = create_new_screen(size, true, true);
|
||||
new_tab(&mut screen, 1, 0);
|
||||
new_tab(&mut screen, 2, 1);
|
||||
screen.add_client(2, false).expect("TEST");
|
||||
screen
|
||||
.change_mode(InputMode::Normal, None, 2)
|
||||
.expect("TEST");
|
||||
screen.change_mode(InputMode::Pane, None, 1).expect("TEST");
|
||||
|
||||
for tab in screen.tabs.values() {
|
||||
assert_eq!(tab.get_client_input_mode(1), Some(InputMode::Pane));
|
||||
assert_ne!(tab.get_client_input_mode(2), Some(InputMode::Pane));
|
||||
}
|
||||
|
||||
new_tab(&mut screen, 3, 2);
|
||||
assert_eq!(
|
||||
screen.get_active_tab(1).unwrap().get_client_input_mode(1),
|
||||
Some(InputMode::Pane)
|
||||
);
|
||||
screen.switch_tab_prev(None, true, 1).expect("TEST");
|
||||
assert_eq!(
|
||||
screen.get_active_tab(1).unwrap().get_client_input_mode(1),
|
||||
Some(InputMode::Pane)
|
||||
);
|
||||
assert_eq!(
|
||||
screen.get_active_tab(2).unwrap().get_client_input_mode(2),
|
||||
Some(InputMode::Normal)
|
||||
);
|
||||
}
|
||||
|
||||
+179
-1
@@ -21,7 +21,9 @@ pub mod ui_components;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::BTreeMap;
|
||||
use zellij_utils::data::{Event, PipeMessage};
|
||||
use zellij_utils::data::{
|
||||
ClientId, Event, EventContext, PipeMessage, RenderResponse, Slot, SlotId,
|
||||
};
|
||||
|
||||
// use zellij_tile::shim::plugin_api::event::ProtobufEvent;
|
||||
|
||||
@@ -48,6 +50,55 @@ pub trait ZellijPlugin: Default {
|
||||
fn render(&mut self, rows: usize, cols: usize) {}
|
||||
}
|
||||
|
||||
#[allow(unused_variables)]
|
||||
pub trait ZellijSharedPlugin: Default {
|
||||
fn load(&mut self, configuration: BTreeMap<String, String>) {}
|
||||
fn slot_added(&mut self, slot: Slot) {}
|
||||
fn slot_removed(&mut self, slot_id: SlotId) {}
|
||||
fn client_connected(&mut self, client_id: ClientId) {}
|
||||
fn client_disconnected(&mut self, client_id: ClientId) {}
|
||||
fn update(&mut self, event: Event, context: EventContext) -> RenderResponse {
|
||||
RenderResponse::Nothing
|
||||
}
|
||||
fn pipe(&mut self, pipe_message: PipeMessage, context: EventContext) -> RenderResponse {
|
||||
RenderResponse::Nothing
|
||||
}
|
||||
fn render(&mut self, rows: usize, cols: usize, slot_id: SlotId, client_id: ClientId) {}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn read_shared_context() -> EventContext {
|
||||
use prost::Message;
|
||||
use zellij_utils::plugin_api::shared_plugin::ProtobufEventContext;
|
||||
let protobuf_bytes: Vec<u8> = shim::protobuf_bytes_from_stdin().unwrap_or_default();
|
||||
ProtobufEventContext::decode(protobuf_bytes.as_slice())
|
||||
.map(EventContext::from)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn read_shared_slot() -> Slot {
|
||||
use prost::Message;
|
||||
use zellij_utils::plugin_api::shared_plugin::ProtobufSlot;
|
||||
let protobuf_bytes: Vec<u8> = shim::protobuf_bytes_from_stdin().unwrap();
|
||||
Slot::from(ProtobufSlot::decode(protobuf_bytes.as_slice()).unwrap())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn write_render_request(render: RenderResponse) -> i32 {
|
||||
use prost::Message;
|
||||
use zellij_utils::plugin_api::shared_plugin::ProtobufRenderRequest;
|
||||
match render {
|
||||
RenderResponse::Nothing => 0,
|
||||
RenderResponse::All => 1,
|
||||
render => {
|
||||
let request: ProtobufRenderRequest = render.into();
|
||||
shim::object_to_stdout(&request.encode_to_vec());
|
||||
2
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// This trait is used to create workers. Workers can be used by plugins to run longer running
|
||||
/// background tasks without blocking their own rendering (eg. and showing some sort of loading
|
||||
/// indication in part of the UI as needed while waiting for the task to complete).
|
||||
@@ -218,3 +269,130 @@ macro_rules! register_worker {
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! register_shared_plugin {
|
||||
($t:ty) => {
|
||||
thread_local! {
|
||||
static STATE: std::cell::RefCell<$t> = std::cell::RefCell::new(Default::default());
|
||||
}
|
||||
|
||||
fn main() {
|
||||
std::panic::set_hook(Box::new(|info| {
|
||||
$crate::shim::report_panic(info);
|
||||
}));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn zellij_shared_plugin() {}
|
||||
|
||||
#[no_mangle]
|
||||
fn load() {
|
||||
STATE.with(|state| {
|
||||
use std::collections::BTreeMap;
|
||||
use std::convert::TryFrom;
|
||||
use $crate::shim::plugin_api::action::ProtobufPluginConfiguration;
|
||||
use $crate::shim::prost::Message;
|
||||
let protobuf_bytes: Vec<u8> = $crate::shim::protobuf_bytes_from_stdin().unwrap();
|
||||
let protobuf_configuration: ProtobufPluginConfiguration =
|
||||
ProtobufPluginConfiguration::decode(protobuf_bytes.as_slice()).unwrap();
|
||||
let plugin_configuration: BTreeMap<String, String> =
|
||||
BTreeMap::try_from(&protobuf_configuration).unwrap();
|
||||
<$t as $crate::ZellijSharedPlugin>::load(
|
||||
&mut *state.borrow_mut(),
|
||||
plugin_configuration,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn shared_update() -> i32 {
|
||||
let context = $crate::read_shared_context();
|
||||
let event = {
|
||||
let protobuf_bytes: Vec<u8> = $crate::shim::protobuf_bytes_from_stdin().unwrap();
|
||||
$crate::shim::plugin_api::event::event_from_protobuf_bytes(&protobuf_bytes).unwrap()
|
||||
};
|
||||
let render = STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::update(&mut *state.borrow_mut(), event, context)
|
||||
});
|
||||
$crate::write_render_request(render)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn shared_pipe() -> i32 {
|
||||
use std::convert::TryInto;
|
||||
use $crate::shim::plugin_api::pipe_message::ProtobufPipeMessage;
|
||||
use $crate::shim::prost::Message;
|
||||
let context = $crate::read_shared_context();
|
||||
let protobuf_pipe_message: ProtobufPipeMessage = {
|
||||
let protobuf_bytes: Vec<u8> = $crate::shim::protobuf_bytes_from_stdin().unwrap();
|
||||
ProtobufPipeMessage::decode(protobuf_bytes.as_slice()).unwrap()
|
||||
};
|
||||
let pipe_message = protobuf_pipe_message.try_into().unwrap();
|
||||
let render = STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::pipe(
|
||||
&mut *state.borrow_mut(),
|
||||
pipe_message,
|
||||
context,
|
||||
)
|
||||
});
|
||||
$crate::write_render_request(render)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn shared_render(rows: i32, cols: i32, slot_id: i32, client_id: i32) {
|
||||
STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::render(
|
||||
&mut *state.borrow_mut(),
|
||||
rows as usize,
|
||||
cols as usize,
|
||||
slot_id as u32,
|
||||
client_id as u32,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn slot_added() {
|
||||
let slot = $crate::read_shared_slot();
|
||||
STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::slot_added(&mut *state.borrow_mut(), slot);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn slot_removed(slot_id: i32) {
|
||||
STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::slot_removed(
|
||||
&mut *state.borrow_mut(),
|
||||
slot_id as u32,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn client_connected(client_id: i32) {
|
||||
STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::client_connected(
|
||||
&mut *state.borrow_mut(),
|
||||
client_id as u32,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn client_disconnected(client_id: i32) {
|
||||
STATE.with(|state| {
|
||||
<$t as $crate::ZellijSharedPlugin>::client_disconnected(
|
||||
&mut *state.borrow_mut(),
|
||||
client_id as u32,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub fn plugin_version() {
|
||||
println!("{}", $crate::prelude::VERSION);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
+166
-1
@@ -44,7 +44,8 @@ use zellij_utils::plugin_api::plugin_command::{
|
||||
ProtobufOpenTerminalPaneInPlaceOfPaneIdResponse, ProtobufOpenTerminalResponse,
|
||||
ProtobufParseLayoutResponse, ProtobufPluginCommand, ProtobufRenameLayoutResponse,
|
||||
ProtobufSaveLayoutResponse, ProtobufSaveSessionResponse, ProtobufShowFloatingPanesResponse,
|
||||
RenameWebTokenResponse, RevokeAllWebTokensResponse, RevokeTokenResponse,
|
||||
ProtobufSlotCommandResponse, RenameWebTokenResponse, RevokeAllWebTokensResponse,
|
||||
RevokeTokenResponse,
|
||||
};
|
||||
use zellij_utils::plugin_api::plugin_ids::{ProtobufPluginIds, ProtobufZellijVersion};
|
||||
|
||||
@@ -92,6 +93,31 @@ pub fn show_cursor(cursor_position: Option<(usize, usize)>) {
|
||||
unsafe { host_run_plugin_command() };
|
||||
}
|
||||
|
||||
fn run_slot_command(plugin_command: PluginCommand) -> Result<(), String> {
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
|
||||
unsafe { host_run_plugin_command() };
|
||||
let response =
|
||||
ProtobufSlotCommandResponse::decode(bytes_from_stdin().unwrap().as_slice()).unwrap();
|
||||
response.into()
|
||||
}
|
||||
|
||||
pub fn set_selectable_slot(slot_id: SlotId, selectable: bool) -> Result<(), String> {
|
||||
run_slot_command(PluginCommand::SetSelectableSlot(slot_id, selectable))
|
||||
}
|
||||
|
||||
pub fn hide_slot(slot_id: SlotId) -> Result<(), String> {
|
||||
run_slot_command(PluginCommand::HideSlot(slot_id))
|
||||
}
|
||||
|
||||
pub fn show_slot(slot_id: SlotId, should_float_if_hidden: bool) -> Result<(), String> {
|
||||
run_slot_command(PluginCommand::ShowSlot(slot_id, should_float_if_hidden))
|
||||
}
|
||||
|
||||
pub fn close_slot(slot_id: SlotId) -> Result<(), String> {
|
||||
run_slot_command(PluginCommand::CloseSlot(slot_id))
|
||||
}
|
||||
|
||||
pub fn request_permission(permissions: &[PermissionType]) {
|
||||
let plugin_command = PluginCommand::RequestPluginPermissions(permissions.into());
|
||||
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
|
||||
@@ -2905,6 +2931,101 @@ pub fn bytes_from_stdin() -> Result<Vec<u8>> {
|
||||
serde_json::from_str(&json).with_context(err_context)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn protobuf_bytes_from_stdin() -> Result<Vec<u8>> {
|
||||
let stdin = io::stdin();
|
||||
let mut stdin = stdin.lock();
|
||||
read_json_byte_array_line(&mut stdin)
|
||||
.with_context(|| "failed to deserialize bytes from stdin".to_string())
|
||||
}
|
||||
|
||||
enum JsonBytesPosition {
|
||||
BeforeArray,
|
||||
InArray,
|
||||
AfterArray,
|
||||
}
|
||||
|
||||
struct JsonBytesParser {
|
||||
position: JsonBytesPosition,
|
||||
bytes: Vec<u8>,
|
||||
current: Option<u16>,
|
||||
}
|
||||
|
||||
impl JsonBytesParser {
|
||||
fn feed(&mut self, byte: u8) -> std::result::Result<(), &'static str> {
|
||||
match self.position {
|
||||
JsonBytesPosition::BeforeArray => match byte {
|
||||
b'[' => self.position = JsonBytesPosition::InArray,
|
||||
byte if byte.is_ascii_whitespace() => {},
|
||||
_ => return Err("expected a byte array"),
|
||||
},
|
||||
JsonBytesPosition::InArray => match byte {
|
||||
b'0'..=b'9' => {
|
||||
let value = self.current.unwrap_or(0) * 10 + (byte - b'0') as u16;
|
||||
if value > u8::MAX as u16 {
|
||||
return Err("byte value out of range");
|
||||
}
|
||||
self.current = Some(value);
|
||||
},
|
||||
b',' | b']' => {
|
||||
match self.current.take() {
|
||||
Some(value) => self.bytes.push(value as u8),
|
||||
None if byte == b']' && self.bytes.is_empty() => {},
|
||||
None => return Err("missing byte value"),
|
||||
}
|
||||
if byte == b']' {
|
||||
self.position = JsonBytesPosition::AfterArray;
|
||||
}
|
||||
},
|
||||
byte if byte.is_ascii_whitespace() && self.current.is_none() => {},
|
||||
_ => return Err("unexpected character in byte array"),
|
||||
},
|
||||
JsonBytesPosition::AfterArray => {
|
||||
if !byte.is_ascii_whitespace() {
|
||||
return Err("unexpected character after byte array");
|
||||
}
|
||||
},
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn read_json_byte_array_line(reader: &mut impl io::BufRead) -> Result<Vec<u8>> {
|
||||
let mut parser = JsonBytesParser {
|
||||
position: JsonBytesPosition::BeforeArray,
|
||||
bytes: Vec::new(),
|
||||
current: None,
|
||||
};
|
||||
let mut error: Option<&'static str> = None;
|
||||
loop {
|
||||
let chunk = reader.fill_buf()?;
|
||||
if chunk.is_empty() {
|
||||
break;
|
||||
}
|
||||
let mut consumed = 0;
|
||||
let mut line_ended = false;
|
||||
for byte in chunk {
|
||||
consumed += 1;
|
||||
if *byte == b'\n' {
|
||||
line_ended = true;
|
||||
break;
|
||||
}
|
||||
if error.is_none() {
|
||||
error = parser.feed(*byte).err();
|
||||
}
|
||||
}
|
||||
reader.consume(consumed);
|
||||
if line_ended {
|
||||
break;
|
||||
}
|
||||
}
|
||||
match (error, parser.position) {
|
||||
(Some(error), _) => Err(anyhow!(error)),
|
||||
(None, JsonBytesPosition::AfterArray) => Ok(parser.bytes),
|
||||
(None, _) => Err(anyhow!("incomplete byte array")),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub fn object_to_stdout(object: &impl Serialize) {
|
||||
// TODO: no crashy
|
||||
@@ -3025,3 +3146,47 @@ extern "C" {
|
||||
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
unsafe fn host_run_plugin_command() {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::read_json_byte_array_line;
|
||||
use std::io::{BufRead, BufReader, Cursor};
|
||||
|
||||
fn read_all(input: &str, capacity: usize) -> Vec<Result<Vec<u8>, String>> {
|
||||
let mut reader = BufReader::with_capacity(capacity, Cursor::new(input.as_bytes().to_vec()));
|
||||
let mut results = vec![];
|
||||
while !reader.fill_buf().unwrap().is_empty() {
|
||||
results.push(read_json_byte_array_line(&mut reader).map_err(|e| e.to_string()));
|
||||
}
|
||||
results
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reads_serde_json_byte_arrays_line_by_line() {
|
||||
let first: Vec<u8> = (0..=255).collect();
|
||||
let second: Vec<u8> = vec![];
|
||||
let third: Vec<u8> = vec![7, 42, 255];
|
||||
let input = format!(
|
||||
"{}\r\n{}\r\n{}\r\n",
|
||||
serde_json::to_string(&first).unwrap(),
|
||||
serde_json::to_string(&second).unwrap(),
|
||||
serde_json::to_string(&third).unwrap()
|
||||
);
|
||||
for capacity in [1, 3, 8192] {
|
||||
assert_eq!(
|
||||
read_all(&input, capacity),
|
||||
vec![Ok(first.clone()), Ok(second.clone()), Ok(third.clone())]
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn malformed_line_is_consumed_and_reported() {
|
||||
let results = read_all("[1,256]\r\n{\"a\":1}\r\n[1,,2]\r\n[3]\r\n", 4);
|
||||
assert_eq!(results.len(), 4);
|
||||
assert!(results[0].is_err());
|
||||
assert!(results[1].is_err());
|
||||
assert!(results[2].is_err());
|
||||
assert_eq!(results[3], Ok(vec![3]));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -240,10 +240,16 @@ keybinds {
|
||||
// Plugin aliases - can be used to change the implementation of Zellij
|
||||
// changing these requires a restart to take effect
|
||||
plugins {
|
||||
tab-bar location="zellij:tab-bar"
|
||||
status-bar location="zellij:status-bar"
|
||||
tab-bar location="zellij:bars" {
|
||||
role "tab-bar"
|
||||
}
|
||||
status-bar location="zellij:bars" {
|
||||
role "status-bar"
|
||||
}
|
||||
strider location="zellij:strider"
|
||||
compact-bar location="zellij:compact-bar"
|
||||
compact-bar location="zellij:bars" {
|
||||
role "compact-bar"
|
||||
}
|
||||
session-manager location="zellij:session-manager"
|
||||
welcome-screen location="zellij:session-manager" {
|
||||
welcome_screen true
|
||||
@@ -258,7 +264,9 @@ plugins {
|
||||
|
||||
// Plugins to load in the background when a new session starts
|
||||
load_plugins {
|
||||
"zellij:link"
|
||||
"zellij:bars" {
|
||||
role "link"
|
||||
}
|
||||
// "file:/path/to/my-plugin.wasm"
|
||||
// "https://example.com/my-plugin.wasm"
|
||||
}
|
||||
|
||||
Binary file not shown.
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
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