aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2025-04-29 20:52:17 +0200
committerGitHub <noreply@github.com>2025-04-29 20:52:17 +0200
commit27c8986939a1642f3e4afda73e9717b7bd328824 (patch)
treee80ee57aeede2578a5ce9fa6d0ab4a350ed9d838 /default-plugins
parentchore: update development version (diff)
downloadsl0ppij-27c8986939a1642f3e4afda73e9717b7bd328824.tar.gz
sl0ppij-27c8986939a1642f3e4afda73e9717b7bd328824.tar.xz
sl0ppij-27c8986939a1642f3e4afda73e9717b7bd328824.zip
feat: multiple Select and Bulk Pane Actions (#4169)
* initial implementation with break panes to new tab * break pane group left/right * group embed/eject panes * stack pane group on resize * close pane group * style(fmt): rustfmt * fix tests * group drag and ungroup with the mouse * fix mouse hover for multiple clients * fix for multiple clients * multiple select plugin initial * use real data in plugin * adjust functionality * fix some ux issues * reflect group mouse group selections in plugin * group/ungroup panes in Zellij * highlight frames when marked by the plugin * refactor: render function in plugin * some ui responsiveness * some more responsiveness and adjust hover text * break out functionality * stack functionality * break panes left/right and close multiple panes * fix(tab): only relayout the relevant layout when non-focused pane is closed * status bar UI * embed and float panes * work * fix some ui/ux issues * refactor: move stuff around * some responsiveness and fix search result browsing bug * change plugin pane title * differentiate group from focused pane * add keyboard shortcut * add ui to compact bar * make boundary colors appear properly without pane frames * get plugins to also display their frame color * make hover shortcuts appear on command panes * fix: do not render search string component if it's empty * BeforeClose Event and unhighlight panes on exit * some UI/UX fixes * some fixes to the catppuccin-latte theme * remove ungroup shortcut * make some ui components opaque * fix more opaque elements * fix some issues with stacking pane order * keyboard shortcuts for grouping * config to opt out of advanced mouse actions * make selected + focused frame color distinct * group marking mode * refactor: multiple-select plugin * adjust stacking group behavior * adjust flashing periods * render common modifier in group controls * add to compact bar * adjust key hint wording * add key to presets and default config * some cleanups * some refactoring * fix tests * fix plugin system tests * tests: group/ungroup/hover * test: BeforeClose plugin event * new plugin assets * style(fmt): rustfmt * remove warnings * tests: give plugin more time to load
Diffstat (limited to 'default-plugins')
-rw-r--r--default-plugins/compact-bar/src/line.rs300
-rw-r--r--default-plugins/compact-bar/src/main.rs28
-rw-r--r--default-plugins/configuration/src/presets.rs32
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs5
-rw-r--r--default-plugins/multiple-select/.cargo/config.toml2
-rw-r--r--default-plugins/multiple-select/.gitignore1
-rw-r--r--default-plugins/multiple-select/Cargo.toml10
l---------default-plugins/multiple-select/LICENSE.md1
-rw-r--r--default-plugins/multiple-select/src/main.rs212
-rw-r--r--default-plugins/multiple-select/src/state.rs634
-rw-r--r--default-plugins/multiple-select/src/ui.rs873
-rw-r--r--default-plugins/status-bar/src/main.rs27
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs254
13 files changed, 2366 insertions, 13 deletions
diff --git a/default-plugins/compact-bar/src/line.rs b/default-plugins/compact-bar/src/line.rs
index 5d50d527..43431446 100644
--- a/default-plugins/compact-bar/src/line.rs
+++ b/default-plugins/compact-bar/src/line.rs
@@ -2,6 +2,7 @@ use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr;
use crate::{LinePart, ARROW_SEPARATOR};
+use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*;
use zellij_tile_utils::style;
@@ -251,6 +252,8 @@ pub fn tab_line(
mode: InputMode,
active_swap_layout_name: &Option<String>,
is_swap_layout_dirty: bool,
+ mode_info: &ModeInfo,
+ grouped_pane_count: Option<usize>,
) -> Vec<LinePart> {
let mut tabs_after_active = all_tabs.split_off(active_tab_index);
let mut tabs_before_active = all_tabs;
@@ -286,15 +289,21 @@ pub fn tab_line(
if current_title_len < cols {
let mut remaining_space = cols - current_title_len;
let remaining_bg = palette.text_unselected.background;
- if let Some(swap_layout_status) = swap_layout_status(
- remaining_space,
- active_swap_layout_name,
- is_swap_layout_dirty,
- mode,
- &palette,
- tab_separator(capabilities),
- ) {
- remaining_space -= swap_layout_status.len;
+ let right_side_component = match grouped_pane_count {
+ Some(grouped_pane_count) => {
+ render_group_controls(mode_info, grouped_pane_count, remaining_space)
+ },
+ None => swap_layout_status(
+ remaining_space,
+ active_swap_layout_name,
+ is_swap_layout_dirty,
+ mode,
+ &palette,
+ tab_separator(capabilities),
+ ),
+ };
+ if let Some(right_side_component) = right_side_component {
+ remaining_space -= right_side_component.len;
let mut buffer = String::new();
for _ in 0..remaining_space {
buffer.push_str(&style!(remaining_bg, remaining_bg).paint(" ").to_string());
@@ -304,7 +313,7 @@ pub fn tab_line(
len: remaining_space,
tab_index: None,
});
- prefix.push(swap_layout_status);
+ prefix.push(right_side_component);
}
}
@@ -373,3 +382,274 @@ fn swap_layout_status(
None => None,
}
}
+
+fn render_group_controls(
+ help: &ModeInfo,
+ grouped_pane_count: usize,
+ max_len: usize,
+) -> Option<LinePart> {
+ let currently_marking_group = help.currently_marking_pane_group.unwrap_or(false);
+ let keymap = help.get_mode_keybinds();
+ let (common_modifiers, multiple_select_key, pane_group_toggle_key, group_mark_toggle_key) = {
+ let multiple_select_key = multiple_select_key(&keymap);
+ let pane_group_toggle_key = single_action_key(&keymap, &[Action::TogglePaneInGroup]);
+ let group_mark_toggle_key = single_action_key(&keymap, &[Action::ToggleGroupMarking]);
+ let common_modifiers = get_common_modifiers(
+ vec![
+ multiple_select_key.iter().next(),
+ pane_group_toggle_key.iter().next(),
+ group_mark_toggle_key.iter().next(),
+ ]
+ .into_iter()
+ .filter_map(|k| k)
+ .collect(),
+ );
+ let multiple_select_key: Vec<KeyWithModifier> = multiple_select_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let pane_group_toggle_key: Vec<KeyWithModifier> = pane_group_toggle_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let group_mark_toggle_key: Vec<KeyWithModifier> = group_mark_toggle_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ (
+ common_modifiers,
+ multiple_select_key,
+ pane_group_toggle_key,
+ group_mark_toggle_key,
+ )
+ };
+ let multiple_select_key = multiple_select_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let pane_group_toggle_key = pane_group_toggle_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let group_mark_toggle_key = group_mark_toggle_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let background = help.style.colors.text_unselected.background;
+ let foreground = help.style.colors.text_unselected.base;
+ let superkey_prefix_style = style!(foreground, background).bold();
+ let common_modifier_text = if common_modifiers.is_empty() {
+ "".to_owned()
+ } else {
+ format!(
+ "{} + ",
+ common_modifiers
+ .iter()
+ .map(|c| c.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ )
+ };
+
+ // full
+ let full_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED PANES", grouped_pane_count)
+ } else {
+ format!("{} SELECTED PANES |", grouped_pane_count)
+ };
+ let full_group_actions_text = format!("<{}> Group Actions", &multiple_select_key);
+ let full_toggle_group_text = format!("<{}> Toggle Group", &pane_group_toggle_key);
+ let full_group_mark_toggle_text = format!("<{}> Follow Focus", &group_mark_toggle_key);
+ let ribbon_paddings_len = 12;
+ let full_controls_line_len = full_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + full_group_actions_text.chars().count()
+ + full_toggle_group_text.chars().count()
+ + full_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ // medium
+ let medium_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED", grouped_pane_count)
+ } else {
+ format!("{} SELECTED |", grouped_pane_count)
+ };
+ let medium_group_actions_text = format!("<{}> Actions", &multiple_select_key);
+ let medium_toggle_group_text = format!("<{}> Toggle", &pane_group_toggle_key);
+ let medium_group_mark_toggle_text = format!("<{}> Follow", &group_mark_toggle_key);
+ let ribbon_paddings_len = 12;
+ let medium_controls_line_len = medium_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + medium_group_actions_text.chars().count()
+ + medium_toggle_group_text.chars().count()
+ + medium_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ // short
+ let short_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED", grouped_pane_count)
+ } else {
+ format!("{} SELECTED |", grouped_pane_count)
+ };
+ let short_group_actions_text = format!("<{}>", &multiple_select_key);
+ let short_toggle_group_text = format!("<{}>", &pane_group_toggle_key);
+ let short_group_mark_toggle_text = format!("<{}>", &group_mark_toggle_key);
+ let color_emphasis_range_end = if common_modifier_text.is_empty() {
+ 0
+ } else {
+ 2
+ };
+ let ribbon_paddings_len = 12;
+ let short_controls_line_len = short_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + short_group_actions_text.chars().count()
+ + short_toggle_group_text.chars().count()
+ + short_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ let (
+ selected_panes_text,
+ group_actions_text,
+ toggle_group_text,
+ group_mark_toggle_text,
+ controls_line_len,
+ ) = if max_len >= full_controls_line_len {
+ (
+ full_selected_panes_text,
+ full_group_actions_text,
+ full_toggle_group_text,
+ full_group_mark_toggle_text,
+ full_controls_line_len,
+ )
+ } else if max_len >= medium_controls_line_len {
+ (
+ medium_selected_panes_text,
+ medium_group_actions_text,
+ medium_toggle_group_text,
+ medium_group_mark_toggle_text,
+ medium_controls_line_len,
+ )
+ } else if max_len >= short_controls_line_len {
+ (
+ short_selected_panes_text,
+ short_group_actions_text,
+ short_toggle_group_text,
+ short_group_mark_toggle_text,
+ short_controls_line_len,
+ )
+ } else {
+ return None;
+ };
+ let selected_panes = serialize_text(
+ &Text::new(&selected_panes_text)
+ .color_range(
+ 3,
+ ..selected_panes_text
+ .chars()
+ .count()
+ .saturating_sub(color_emphasis_range_end),
+ )
+ .opaque(),
+ );
+ let group_actions_ribbon = serialize_ribbon(
+ &Text::new(&group_actions_text).color_range(0, 1..=multiple_select_key.chars().count()),
+ );
+ let toggle_group_ribbon = serialize_ribbon(
+ &Text::new(&toggle_group_text).color_range(0, 1..=pane_group_toggle_key.chars().count()),
+ );
+ let mut group_mark_toggle_ribbon = Text::new(&group_mark_toggle_text)
+ .color_range(0, 1..=group_mark_toggle_key.chars().count());
+ if currently_marking_group {
+ group_mark_toggle_ribbon = group_mark_toggle_ribbon.selected();
+ }
+ let group_mark_toggle_ribbon = serialize_ribbon(&group_mark_toggle_ribbon);
+ let controls_line = if common_modifiers.is_empty() {
+ format!(
+ "{} {}{}{}",
+ selected_panes, group_actions_ribbon, toggle_group_ribbon, group_mark_toggle_ribbon
+ )
+ } else {
+ let common_modifier =
+ serialize_text(&Text::new(&common_modifier_text).color_range(0, ..).opaque());
+ format!(
+ "{} {}{}{}{}",
+ selected_panes,
+ common_modifier,
+ group_actions_ribbon,
+ toggle_group_ribbon,
+ group_mark_toggle_ribbon
+ )
+ };
+ let remaining_space = max_len.saturating_sub(controls_line_len);
+ let mut padding = String::new();
+ let mut padding_len = 0;
+ for _ in 0..remaining_space {
+ padding.push_str(&ANSIStrings(&[superkey_prefix_style.paint(" ")]).to_string());
+ padding_len += 1;
+ }
+ Some(LinePart {
+ part: format!("{}{}", padding, controls_line),
+ len: controls_line_len + padding_len,
+ tab_index: None,
+ })
+}
+
+fn multiple_select_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ let has_match = acvec
+ .iter()
+ .find(|a| a.launches_plugin("zellij:multiple-select")) // TODO: make this an alias
+ .is_some();
+ if has_match {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
+fn single_action_key(
+ keymap: &[(KeyWithModifier, Vec<Action>)],
+ action: &[Action],
+) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ if acvec.iter().next() == action.iter().next() {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
+fn get_common_modifiers(mut keyvec: Vec<&KeyWithModifier>) -> Vec<KeyModifier> {
+ if keyvec.is_empty() {
+ return vec![];
+ }
+ let mut common_modifiers = keyvec.pop().unwrap().key_modifiers.clone();
+ for key in keyvec {
+ common_modifiers = common_modifiers
+ .intersection(&key.key_modifiers)
+ .cloned()
+ .collect();
+ }
+ common_modifiers.into_iter().collect()
+}
diff --git a/default-plugins/compact-bar/src/main.rs b/default-plugins/compact-bar/src/main.rs
index 3775b12f..d4a43ab8 100644
--- a/default-plugins/compact-bar/src/main.rs
+++ b/default-plugins/compact-bar/src/main.rs
@@ -26,6 +26,8 @@ struct State {
tab_line: Vec<LinePart>,
text_copy_destination: Option<CopyDestination>,
display_system_clipboard_failure: bool,
+ own_client_id: Option<ClientId>,
+ grouped_panes_count: Option<usize>,
}
static ARROW_SEPARATOR: &str = "";
@@ -42,7 +44,9 @@ impl ZellijPlugin for State {
EventType::CopyToClipboard,
EventType::InputReceived,
EventType::SystemClipboardFailure,
+ EventType::PaneUpdate,
]);
+ self.own_client_id = Some(get_plugin_ids().client_id);
}
fn update(&mut self, event: Event) -> bool {
@@ -108,6 +112,28 @@ impl ZellijPlugin for State {
self.text_copy_destination = None;
self.display_system_clipboard_failure = false;
},
+ Event::PaneUpdate(pane_manifest) => {
+ if let Some(own_client_id) = self.own_client_id {
+ let mut grouped_panes_count = 0;
+ for (_tab_index, pane_infos) in pane_manifest.panes {
+ for pane_info in pane_infos {
+ let is_in_pane_group =
+ pane_info.index_in_pane_group.get(&own_client_id).is_some();
+ if is_in_pane_group {
+ grouped_panes_count += 1;
+ }
+ }
+ }
+ if Some(grouped_panes_count) != self.grouped_panes_count {
+ if grouped_panes_count == 0 {
+ self.grouped_panes_count = None;
+ } else {
+ self.grouped_panes_count = Some(grouped_panes_count);
+ }
+ should_render = true;
+ }
+ }
+ },
_ => {
eprintln!("Got unrecognized event: {:?}", event);
},
@@ -181,6 +207,8 @@ impl ZellijPlugin for State {
self.mode_info.mode,
&active_swap_layout_name,
is_swap_layout_dirty,
+ &self.mode_info,
+ self.grouped_panes_count,
);
let output = self
.tab_line
diff --git a/default-plugins/configuration/src/presets.rs b/default-plugins/configuration/src/presets.rs
index 41d7a79e..54713635 100644
--- a/default-plugins/configuration/src/presets.rs
+++ b/default-plugins/configuration/src/presets.rs
@@ -168,6 +168,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }}
+ bind "{secondary_modifier} m" {{
+ LaunchOrFocusPlugin "zellij:multiple-select" {{
+ floating true
+ move_to_focused_tab true
+ }}
+ }}
+ bind "{secondary_modifier} p" {{ TogglePaneInGroup; }}
+ bind "{secondary_modifier} Shift p" {{ ToggleGroupMarking; }}
}}
shared_except "locked" "renametab" "renamepane" {{
bind "Enter" {{ SwitchToMode "Locked"; }}
@@ -392,6 +400,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }}
+ bind "{secondary_modifier} m" {{
+ LaunchOrFocusPlugin "zellij:multiple-select" {{
+ floating true
+ move_to_focused_tab true
+ }}
+ }}
+ bind "{secondary_modifier} p" {{ TogglePaneInGroup; }}
+ bind "{secondary_modifier} Shift p" {{ ToggleGroupMarking; }}
}}
shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}
@@ -595,6 +611,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }}
+ bind "{secondary_modifier} m" {{
+ LaunchOrFocusPlugin "zellij:multiple-select" {{
+ floating true
+ move_to_focused_tab true
+ }}
+ }}
+ bind "{secondary_modifier} p" {{ TogglePaneInGroup; }}
+ bind "{secondary_modifier} Shift p" {{ ToggleGroupMarking; }}
}}
shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}
@@ -1158,6 +1182,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }}
+ bind "{secondary_modifier} m" {{
+ LaunchOrFocusPlugin "zellij:multiple-select" {{
+ floating true
+ move_to_focused_tab true
+ }}
+ }}
+ bind "{secondary_modifier} p" {{ TogglePaneInGroup; }}
+ bind "{secondary_modifier} Shift p" {{ ToggleGroupMarking; }}
}}
shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index 24f0f649..7f66fa4e 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -67,6 +67,7 @@ impl ZellijPlugin for State {
EventType::FileSystemCreate,
EventType::FileSystemUpdate,
EventType::FileSystemDelete,
+ EventType::BeforeClose,
]);
watch_filesystem();
}
@@ -535,6 +536,10 @@ impl ZellijPlugin for State {
self.received_payload = Some(payload.clone());
}
},
+ Event::BeforeClose => {
+ // this is just to assert something to make sure this event was triggered
+ highlight_and_unhighlight_panes(vec![PaneId::Terminal(1)], vec![PaneId::Plugin(1)]);
+ },
Event::SystemClipboardFailure => {
// this is just to trigger the worker message
post_message_to(PluginMessage {
diff --git a/default-plugins/multiple-select/.cargo/config.toml b/default-plugins/multiple-select/.cargo/config.toml
new file mode 100644
index 00000000..6b509f5b
--- /dev/null
+++ b/default-plugins/multiple-select/.cargo/config.toml
@@ -0,0 +1,2 @@
+[build]
+target = "wasm32-wasip1"
diff --git a/default-plugins/multiple-select/.gitignore b/default-plugins/multiple-select/.gitignore
new file mode 100644
index 00000000..ea8c4bf7
--- /dev/null
+++ b/default-plugins/multiple-select/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/default-plugins/multiple-select/Cargo.toml b/default-plugins/multiple-select/Cargo.toml
new file mode 100644
index 00000000..c5580ad4
--- /dev/null
+++ b/default-plugins/multiple-select/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "multiple-select"
+version = "0.1.0"
+authors = ["Aram Drevekenin <aram@poor.dev>"]
+edition = "2021"
+license = "MIT"
+
+[dependencies]
+zellij-tile = { path = "../../zellij-tile" }
+fuzzy-matcher = "0.3.7"
diff --git a/default-plugins/multiple-select/LICENSE.md b/default-plugins/multiple-select/LICENSE.md
new file mode 120000
index 00000000..f0608a63
--- /dev/null
+++ b/default-plugins/multiple-select/LICENSE.md
@@ -0,0 +1 @@
+../../LICENSE.md \ No newline at end of file
diff --git a/default-plugins/multiple-select/src/main.rs b/default-plugins/multiple-select/src/main.rs
new file mode 100644
index 00000000..773fd6fb
--- /dev/null
+++ b/default-plugins/multiple-select/src/main.rs
@@ -0,0 +1,212 @@
+pub mod state;
+pub mod ui;
+
+use state::{MarkedIndex, VisibilityAndFocus};
+use std::collections::BTreeMap;
+use ui::PaneItem;
+use zellij_tile::prelude::*;
+
+#[derive(Debug, Default)]
+pub struct App {
+ own_plugin_id: Option<u32>,
+ own_client_id: Option<ClientId>,
+ own_tab_index: Option<usize>,
+ total_tabs_in_session: Option<usize>,
+ search_string: String,
+ previous_search_string: String, // used eg. for the new tab title when breaking panes
+ left_side_panes: Vec<PaneItem>,
+ right_side_panes: Vec<PaneItem>,
+ search_results: Option<Vec<PaneItem>>,
+ visibility_and_focus: VisibilityAndFocus,
+ marked_index: Option<MarkedIndex>,
+}
+
+register_plugin!(App);
+
+impl ZellijPlugin for App {
+ fn load(&mut self, _configuration: BTreeMap<String, String>) {
+ subscribe(&[
+ EventType::Key,
+ EventType::Mouse,
+ EventType::ModeUpdate,
+ EventType::RunCommandResult,
+ EventType::TabUpdate,
+ EventType::PaneUpdate,
+ EventType::FailedToWriteConfigToDisk,
+ EventType::ConfigWasWrittenToDisk,
+ EventType::BeforeClose,
+ ]);
+ let plugin_ids = get_plugin_ids();
+ self.own_plugin_id = Some(plugin_ids.plugin_id);
+ self.own_client_id = Some(plugin_ids.client_id);
+ rename_plugin_pane(plugin_ids.plugin_id, "Multiple Select");
+ }
+ fn update(&mut self, event: Event) -> bool {
+ let mut should_render = false;
+ match event {
+ Event::PaneUpdate(pane_manifest) => {
+ self.react_to_zellij_state_update(pane_manifest);
+ should_render = true;
+ },
+ Event::Key(key) => {
+ match key.bare_key {
+ BareKey::Tab if key.has_no_modifiers() => {
+ self.visibility_and_focus.toggle_focus();
+ self.marked_index = None;
+ self.update_highlighted_panes();
+ should_render = true;
+ },
+ BareKey::Char(character)
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.left_side_is_focused()
+ && self.marked_index.is_none() =>
+ {
+ self.search_string.push(character);
+ self.update_search_results();
+ should_render = true;
+ },
+ BareKey::Backspace
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.left_side_is_focused()
+ && self.marked_index.is_none() =>
+ {
+ self.search_string.pop();
+ self.update_search_results();
+ should_render = true;
+ },
+ BareKey::Enter if key.has_no_modifiers() => {
+ if self.visibility_and_focus.left_side_is_focused() {
+ if let Some(marked_index) = self.marked_index.take() {
+ let keep_left_side_focused = false;
+ self.group_panes(marked_index, keep_left_side_focused);
+ } else {
+ match self.search_results.take() {
+ Some(search_results) => {
+ self.group_search_results(search_results)
+ },
+ None => self.group_all_panes(),
+ }
+ self.handle_left_side_emptied();
+ }
+ }
+ should_render = true;
+ },
+ BareKey::Right
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.left_side_is_focused() =>
+ {
+ if let Some(marked_index) = self.marked_index.take() {
+ let keep_left_side_focused = true;
+ self.group_panes(marked_index, keep_left_side_focused);
+ should_render = true;
+ }
+ },
+ BareKey::Left
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ if self.visibility_and_focus.right_side_is_focused() {
+ if let Some(marked_index) = self.marked_index.take() {
+ self.ungroup_panes(marked_index);
+ should_render = true;
+ }
+ }
+ },
+ BareKey::Char('c') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
+ if self.visibility_and_focus.right_side_is_focused() {
+ // this means we're in the selection panes part and we want to clear
+ // them
+ self.ungroup_all_panes();
+ } else if self.visibility_and_focus.left_side_is_focused() {
+ if self.marked_index.is_some() {
+ self.marked_index = None;
+ self.update_highlighted_panes();
+ } else {
+ self.ungroup_all_panes_and_close_self();
+ }
+ }
+ should_render = true;
+ },
+ BareKey::Down if key.has_no_modifiers() => {
+ self.move_marked_index_down();
+ should_render = true;
+ },
+ BareKey::Up if key.has_no_modifiers() => {
+ self.move_marked_index_up();
+ should_render = true;
+ },
+ BareKey::Char(' ') if key.has_no_modifiers() && self.marked_index.is_some() => {
+ self.mark_entry();
+ should_render = true;
+ },
+ BareKey::Char('b')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.break_grouped_panes_to_new_tab();
+ },
+ BareKey::Char('s')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.stack_grouped_panes();
+ },
+ BareKey::Char('f')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.float_grouped_panes();
+ },
+ BareKey::Char('e')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.embed_grouped_panes();
+ },
+ BareKey::Char('r')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.break_grouped_panes_right();
+ },
+ BareKey::Char('l')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.break_grouped_panes_left();
+ },
+ BareKey::Char('c')
+ if key.has_no_modifiers()
+ && self.visibility_and_focus.right_side_is_focused() =>
+ {
+ self.close_grouped_panes();
+ },
+ _ => {},
+ }
+ },
+ Event::BeforeClose => {
+ self.unhighlight_all_panes();
+ },
+ _ => {},
+ }
+ should_render
+ }
+ fn render(&mut self, rows: usize, cols: usize) {
+ self.render_close_shortcut(cols);
+ self.render_tab_shortcut(cols, rows);
+ match self.visibility_and_focus {
+ VisibilityAndFocus::OnlyLeftSideVisible => self.render_left_side(rows, cols, true),
+ VisibilityAndFocus::OnlyRightSideVisible => self.render_right_side(rows, cols, true),
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused => {
+ self.render_left_side(rows, cols, true);
+ self.render_right_side(rows, cols, false);
+ },
+ VisibilityAndFocus::BothSidesVisibleRightSideFocused => {
+ self.render_left_side(rows, cols, false);
+ self.render_right_side(rows, cols, true);
+ },
+ }
+ self.render_focus_boundary(rows, cols);
+ self.render_help_line(rows, cols);
+ }
+}
diff --git a/default-plugins/multiple-select/src/state.rs b/default-plugins/multiple-select/src/state.rs
new file mode 100644
index 00000000..3bf863f8
--- /dev/null
+++ b/default-plugins/multiple-select/src/state.rs
@@ -0,0 +1,634 @@
+use crate::{ui::PaneItem, App};
+use std::collections::{BTreeMap, BTreeSet, HashSet};
+
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+
+use zellij_tile::prelude::*;
+
+#[derive(Debug, Default)]
+pub struct MarkedIndex {
+ pub main_index: usize,
+ pub additional_indices: HashSet<usize>,
+}
+
+impl MarkedIndex {
+ pub fn new(main_index: usize) -> Self {
+ MarkedIndex {
+ main_index,
+ additional_indices: HashSet::new(),
+ }
+ }
+}
+
+impl MarkedIndex {
+ pub fn toggle_additional_mark(&mut self) {
+ if self.additional_indices.contains(&self.main_index) {
+ self.additional_indices.retain(|a| a != &self.main_index);
+ } else {
+ self.additional_indices.insert(self.main_index);
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum VisibilityAndFocus {
+ OnlyLeftSideVisible,
+ OnlyRightSideVisible,
+ BothSidesVisibleLeftSideFocused,
+ BothSidesVisibleRightSideFocused,
+}
+
+impl Default for VisibilityAndFocus {
+ fn default() -> Self {
+ VisibilityAndFocus::OnlyLeftSideVisible
+ }
+}
+
+impl VisibilityAndFocus {
+ pub fn only_left_side_is_focused(&self) -> bool {
+ match self {
+ VisibilityAndFocus::OnlyLeftSideVisible => true,
+ _ => false,
+ }
+ }
+ pub fn left_side_is_focused(&self) -> bool {
+ match self {
+ VisibilityAndFocus::OnlyLeftSideVisible
+ | VisibilityAndFocus::BothSidesVisibleLeftSideFocused => true,
+ _ => false,
+ }
+ }
+ pub fn right_side_is_focused(&self) -> bool {
+ match self {
+ VisibilityAndFocus::OnlyRightSideVisible
+ | VisibilityAndFocus::BothSidesVisibleRightSideFocused => true,
+ _ => false,
+ }
+ }
+ pub fn hide_left_side(&mut self) {
+ *self = VisibilityAndFocus::OnlyRightSideVisible
+ }
+ pub fn hide_right_side(&mut self) {
+ *self = VisibilityAndFocus::OnlyLeftSideVisible
+ }
+ pub fn focus_right_side(&mut self) {
+ *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused
+ }
+ pub fn toggle_focus(&mut self) {
+ match self {
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused => {
+ *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused
+ },
+ VisibilityAndFocus::BothSidesVisibleRightSideFocused => {
+ *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ },
+ VisibilityAndFocus::OnlyLeftSideVisible => {
+ *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused
+ },
+ VisibilityAndFocus::OnlyRightSideVisible => {
+ *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ },
+ }
+ }
+ pub fn show_both_sides(&mut self) {
+ match self {
+ VisibilityAndFocus::OnlyLeftSideVisible => {
+ *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ },
+ VisibilityAndFocus::OnlyRightSideVisible => {
+ *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused
+ },
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ | VisibilityAndFocus::BothSidesVisibleRightSideFocused => {
+ // no-op
+ },
+ }
+ }
+}
+
+impl App {
+ pub fn react_to_zellij_state_update(&mut self, pane_manifest: PaneManifest) {
+ let is_first_update = self.right_side_panes.is_empty() && self.left_side_panes.is_empty();
+ let panes_on_the_left_before = self.left_side_panes.len();
+ let panes_on_the_right_before = self.right_side_panes.len();
+ self.update_tab_info(&pane_manifest);
+ self.update_panes(pane_manifest);
+ if is_first_update && !self.right_side_panes.is_empty() {
+ // in this case, the plugin was started with an existing group
+ // most likely, the user wants to perform operations just on this group, so we
+ // only show the group, giving the option to add more panes
+ self.visibility_and_focus.hide_left_side();
+ }
+ let pane_count_changed = (panes_on_the_left_before != self.left_side_panes.len())
+ || (panes_on_the_right_before != self.right_side_panes.len());
+ if !is_first_update && pane_count_changed {
+ let has_panes_on_the_right = !self.right_side_panes.is_empty();
+ let has_panes_on_the_left = !self.left_side_panes.is_empty();
+ if has_panes_on_the_right && has_panes_on_the_left {
+ self.visibility_and_focus.show_both_sides();
+ } else if has_panes_on_the_right {
+ self.visibility_and_focus.hide_left_side();
+ } else if has_panes_on_the_left {
+ self.visibility_and_focus.hide_right_side();
+ }
+ }
+ }
+ pub fn update_panes(&mut self, pane_manifest: PaneManifest) {
+ let mut all_panes = BTreeMap::new();
+ for (_tab_index, pane_infos) in pane_manifest.panes {
+ for pane_info in pane_infos {
+ if pane_info.is_selectable {
+ if pane_info.is_plugin {
+ all_panes.insert(PaneId::Plugin(pane_info.id), pane_info);
+ } else {
+ all_panes.insert(PaneId::Terminal(pane_info.id), pane_info);
+ }
+ }
+ }
+ }
+ self.left_side_panes
+ .retain(|p| all_panes.contains_key(&p.id));
+ self.right_side_panes
+ .retain(|p| all_panes.contains_key(&p.id));
+ let mut new_selected_panes: BTreeMap<usize, PaneItem> = BTreeMap::new(); // usize -> index_in_pane_group
+ for (pane_id, pane) in all_panes.into_iter() {
+ let is_known = self
+ .left_side_panes
+ .iter()
+ .find(|p| p.id == pane_id)
+ .is_some()
+ || self
+ .right_side_panes
+ .iter()
+ .find(|p| p.id == pane_id)
+ .is_some();
+ let index_in_pane_group = self
+ .own_client_id
+ .and_then(|own_client_id| pane.index_in_pane_group.get(&own_client_id));
+ let is_grouped_for_own_client_id = index_in_pane_group.is_some();
+ if !is_known {
+ if is_grouped_for_own_client_id {
+ if let Some(index_in_pane_group) = index_in_pane_group {
+ // we do this rather than adding them directly to right_side_panes so that
+ // we can make sure they're in the same order as the group is so that
+ // things like stacking order will do the right thing
+ new_selected_panes.insert(
+ *index_in_pane_group,
+ PaneItem {
+ text: pane.title,
+ id: pane_id,
+ color_indices: vec![],
+ },
+ );
+ }
+ } else {
+ self.left_side_panes.push(PaneItem {
+ text: pane.title,
+ id: pane_id,
+ color_indices: vec![],
+ });
+ }
+ } else {
+ if is_grouped_for_own_client_id {
+ if let Some(position) =
+ self.left_side_panes.iter().position(|p| p.id == pane_id)
+ {
+ // pane was added to a pane group outside the plugin (eg. with mouse selection)
+ let mut pane = self.left_side_panes.remove(position);
+ pane.clear();
+ self.right_side_panes.push(pane);
+ }
+ } else {
+ if let Some(position) =
+ self.right_side_panes.iter().position(|p| p.id == pane_id)
+ {
+ // pane was removed from a pane group outside the plugin (eg. with mouse selection)
+ let mut pane = self.right_side_panes.remove(position);
+ pane.clear();
+ self.left_side_panes.push(pane);
+ }
+ }
+ }
+ }
+ for (_index_in_pane_group, pane_item) in new_selected_panes.into_iter() {
+ self.right_side_panes.push(pane_item);
+ }
+ }
+ pub fn update_tab_info(&mut self, pane_manifest: &PaneManifest) {
+ for (tab_index, pane_infos) in &pane_manifest.panes {
+ for pane_info in pane_infos {
+ if pane_info.is_plugin && Some(pane_info.id) == self.own_plugin_id {
+ self.own_tab_index = Some(*tab_index);
+ }
+ }
+ }
+ self.total_tabs_in_session = Some(pane_manifest.panes.keys().count());
+ }
+ pub fn update_search_results(&mut self) {
+ let mut matches = vec![];
+ let matcher = SkimMatcherV2::default().use_cache(true);
+ for pane_item in &self.left_side_panes {
+ if let Some((score, indices)) =
+ matcher.fuzzy_indices(&pane_item.text, &self.search_string)
+ {
+ let mut pane_item = pane_item.clone();
+ pane_item.color_indices = indices;
+ matches.push((score, pane_item));
+ }
+ }
+ matches.sort_by(|(a_score, _a), (b_score, _b)| b_score.cmp(&a_score));
+ if self.search_string.is_empty() {
+ self.search_results = None;
+ } else {
+ self.search_results = Some(
+ matches
+ .into_iter()
+ .map(|(_s, pane_item)| pane_item)
+ .collect(),
+ );
+ }
+ }
+ pub fn group_panes_in_zellij(&mut self, pane_ids: Vec<PaneId>) {
+ group_and_ungroup_panes(pane_ids, vec![]);
+ }
+ pub fn ungroup_panes_in_zellij(&mut self, pane_ids: Vec<PaneId>) {
+ group_and_ungroup_panes(vec![], pane_ids);
+ }
+ pub fn update_highlighted_panes(&self) {
+ let mut pane_ids_to_highlight = vec![];
+ let mut pane_ids_to_unhighlight = vec![];
+ if let Some(marked_index) = &self.marked_index {
+ if self.visibility_and_focus.left_side_is_focused() {
+ if let Some(main_index_pane_id) = self
+ .search_results
+ .as_ref()
+ .and_then(|s| s.get(marked_index.main_index))
+ .or_else(|| self.left_side_panes.get(marked_index.main_index))
+ .map(|p| p.id)
+ {
+ pane_ids_to_highlight.push(main_index_pane_id);
+ }
+ for index in &marked_index.additional_indices {
+ if let Some(pane_id) = self
+ .search_results
+ .as_ref()
+ .and_then(|s| s.get(*index))
+ .or_else(|| self.left_side_panes.get(*index))
+ .map(|p| p.id)
+ {
+ pane_ids_to_highlight.push(pane_id);
+ }
+ }
+ } else {
+ if let Some(main_index_pane_id) = self
+ .right_side_panes
+ .get(marked_index.main_index)
+ .map(|p| p.id)
+ {
+ pane_ids_to_highlight.push(main_index_pane_id);
+ }
+ for index in &marked_index.additional_indices {
+ if let Some(pane_id) = self.right_side_panes.get(*index).map(|p| p.id) {
+ pane_ids_to_highlight.push(pane_id);
+ }
+ }
+ }
+ }
+ for pane in &self.left_side_panes {
+ if !pane_ids_to_highlight.contains(&pane.id) {
+ pane_ids_to_unhighlight.push(pane.id);
+ }
+ }
+ for pane in &self.right_side_panes {
+ if !pane_ids_to_highlight.contains(&pane.id) {
+ pane_ids_to_unhighlight.push(pane.id);
+ }
+ }
+ highlight_and_unhighlight_panes(pane_ids_to_highlight, pane_ids_to_unhighlight);
+ }
+ pub fn unhighlight_all_panes(&mut self) {
+ let mut pane_ids_to_unhighlight = HashSet::new();
+ for pane_item in &self.left_side_panes {
+ pane_ids_to_unhighlight.insert(pane_item.id);
+ }
+ for pane_item in &self.right_side_panes {
+ pane_ids_to_unhighlight.insert(pane_item.id);
+ }
+ highlight_and_unhighlight_panes(vec![], pane_ids_to_unhighlight.into_iter().collect());
+ }
+ pub fn ungroup_all_panes(&mut self) {
+ let mut unselected_panes = vec![];
+ for pane_item in self.right_side_panes.iter_mut() {
+ pane_item.clear();
+ unselected_panes.push(pane_item.id);
+ }
+ self.left_side_panes.append(&mut self.right_side_panes);
+ self.ungroup_panes_in_zellij(unselected_panes);
+ self.visibility_and_focus.hide_right_side();
+ self.marked_index = None;
+ }
+ pub fn ungroup_all_panes_and_close_self(&mut self) {
+ let mut pane_ids_to_ungroup = HashSet::new();
+ for pane_item in &self.left_side_panes {
+ pane_ids_to_ungroup.insert(pane_item.id);
+ }
+ for pane_item in &self.right_side_panes {
+ pane_ids_to_ungroup.insert(pane_item.id);
+ }
+ group_and_ungroup_panes(vec![], pane_ids_to_ungroup.into_iter().collect());
+ close_self();
+ }
+ pub fn group_panes(&mut self, mut marked_index: MarkedIndex, keep_left_side_focused: bool) {
+ let mut all_selected_indices: BTreeSet<usize> =
+ marked_index.additional_indices.drain().collect();
+ all_selected_indices.insert(marked_index.main_index);
+
+ // reverse so that the indices will remain consistent while
+ // removing
+ let mut selected_panes = vec![];
+ for index in all_selected_indices.iter().rev() {
+ let index = self
+ .search_results
+ .as_mut()
+ .and_then(|search_results| {
+ if search_results.len() > *index {
+ Some(search_results.remove(*index))
+ } else {
+ None
+ }
+ })
+ .and_then(|selected_search_result| {
+ self.left_side_panes
+ .iter()
+ .position(|p| p.id == selected_search_result.id)
+ })
+ .unwrap_or(*index);
+ if self.left_side_panes.len() > index {
+ let selected_pane = self.left_side_panes.remove(index);
+ selected_panes.push(selected_pane);
+ }
+ }
+ let pane_ids_to_make_selected: Vec<PaneId> = selected_panes.iter().map(|p| p.id).collect();
+ self.right_side_panes
+ .append(&mut selected_panes.into_iter().rev().collect());
+
+ let displayed_list_len = match self.search_results.as_ref() {
+ Some(search_results) => search_results.len(),
+ None => self.left_side_panes.len(),
+ };
+
+ if displayed_list_len == 0 {
+ self.handle_left_side_emptied();
+ } else if keep_left_side_focused {
+ if marked_index.main_index > displayed_list_len.saturating_sub(1) {
+ self.marked_index = Some(MarkedIndex::new(displayed_list_len.saturating_sub(1)));
+ } else {
+ self.marked_index = Some(marked_index);
+ }
+ self.visibility_and_focus.show_both_sides();
+ } else {
+ self.visibility_and_focus.focus_right_side();
+ }
+
+ self.group_panes_in_zellij(pane_ids_to_make_selected);
+ self.update_highlighted_panes();
+ }
+ pub fn ungroup_panes(&mut self, mut marked_index: MarkedIndex) {
+ let mut all_selected_indices: BTreeSet<usize> =
+ marked_index.additional_indices.drain().collect();
+ all_selected_indices.insert(marked_index.main_index);
+
+ // reverse so that the indices will remain consistent while
+ // removing
+ let mut selected_panes = vec![];
+ for index in all_selected_indices.iter().rev() {
+ if self.right_side_panes.len() > *index {
+ let mut selected_pane = self.right_side_panes.remove(*index);
+ selected_pane.clear();
+ selected_panes.push(selected_pane);
+ }
+ }
+ self.ungroup_panes_in_zellij(selected_panes.iter().map(|p| p.id).collect());
+ self.left_side_panes
+ .append(&mut selected_panes.into_iter().rev().collect());
+
+ if self.right_side_panes.is_empty() {
+ self.marked_index = None;
+ self.visibility_and_focus.hide_right_side();
+ } else if marked_index.main_index > self.right_side_panes.len().saturating_sub(1) {
+ self.marked_index = Some(MarkedIndex::new(
+ self.right_side_panes.len().saturating_sub(1),
+ ));
+ self.visibility_and_focus.show_both_sides();
+ } else {
+ self.marked_index = Some(marked_index);
+ self.visibility_and_focus.show_both_sides();
+ }
+ self.update_highlighted_panes();
+ }
+ pub fn group_search_results(&mut self, search_results: Vec<PaneItem>) {
+ let mut pane_ids_to_make_selected = vec![];
+ for search_result in search_results {
+ let pane_id = search_result.id;
+ pane_ids_to_make_selected.push(pane_id);
+ self.left_side_panes.retain(|p| p.id != pane_id);
+ self.right_side_panes.push(search_result);
+ }
+ self.group_panes_in_zellij(pane_ids_to_make_selected);
+ }
+ pub fn group_all_panes(&mut self) {
+ let pane_ids_to_make_selected: Vec<PaneId> =
+ self.left_side_panes.iter().map(|p| p.id).collect();
+ self.right_side_panes.append(&mut self.left_side_panes);
+ self.group_panes_in_zellij(pane_ids_to_make_selected);
+ }
+ pub fn handle_left_side_emptied(&mut self) {
+ self.visibility_and_focus.hide_left_side();
+ self.previous_search_string = self.search_string.drain(..).collect();
+ self.marked_index = None;
+ self.search_results = None;
+ self.update_highlighted_panes();
+ }
+ pub fn move_marked_index_down(&mut self) {
+ match self.marked_index.as_mut() {
+ Some(marked_index) => {
+ let is_searching = self.search_results.is_some();
+ let search_result_count =
+ self.search_results.as_ref().map(|s| s.len()).unwrap_or(0);
+ if self.visibility_and_focus.left_side_is_focused()
+ && is_searching
+ && marked_index.main_index == search_result_count.saturating_sub(1)
+ {
+ marked_index.main_index = 0;
+ } else if self.visibility_and_focus.left_side_is_focused()
+ && !is_searching
+ && marked_index.main_index == self.left_side_panes.len().saturating_sub(1)
+ {
+ marked_index.main_index = 0;
+ } else if self.visibility_and_focus.right_side_is_focused()
+ && marked_index.main_index == self.right_side_panes.len().saturating_sub(1)
+ {
+ marked_index.main_index = 0;
+ } else {
+ marked_index.main_index += 1
+ }
+ },
+ None => {
+ if self.visibility_and_focus.left_side_is_focused() {
+ let is_searching = self.search_results.is_some();
+ let has_search_results = self
+ .search_results
+ .as_ref()
+ .map(|s| !s.is_empty())
+ .unwrap_or(false);
+ if is_searching && has_search_results {
+ self.marked_index = Some(MarkedIndex::new(0));
+ } else if !is_searching && !self.left_side_panes.is_empty() {
+ self.marked_index = Some(MarkedIndex::new(0));
+ }
+ } else if self.visibility_and_focus.right_side_is_focused()
+ && !self.right_side_panes.is_empty()
+ {
+ self.marked_index = Some(MarkedIndex::new(0));
+ }
+ },
+ }
+ self.update_highlighted_panes();
+ }
+ pub fn move_marked_index_up(&mut self) {
+ match self.marked_index.as_mut() {
+ Some(marked_index) => {
+ if self.visibility_and_focus.left_side_is_focused() && marked_index.main_index == 0
+ {
+ if let Some(search_result_count) = self.search_results.as_ref().map(|s| s.len())
+ {
+ marked_index.main_index = search_result_count.saturating_sub(1);
+ } else {
+ marked_index.main_index = self.left_side_panes.len().saturating_sub(1);
+ }
+ } else if self.visibility_and_focus.right_side_is_focused()
+ && marked_index.main_index == 0
+ {
+ marked_index.main_index = self.right_side_panes.len().saturating_sub(1);
+ } else {
+ marked_index.main_index = marked_index.main_index.saturating_sub(1);
+ }
+ },
+ None => {
+ if self.visibility_and_focus.left_side_is_focused() {
+ let is_searching = self.search_results.is_some();
+ let has_search_results = self
+ .search_results
+ .as_ref()
+ .map(|s| !s.is_empty())
+ .unwrap_or(false);
+ if is_searching && has_search_results {
+ let search_results_count =
+ self.search_results.as_ref().map(|s| s.len()).unwrap_or(0);
+ self.marked_index =
+ Some(MarkedIndex::new(search_results_count.saturating_sub(1)));
+ } else if !is_searching && !self.left_side_panes.is_empty() {
+ self.marked_index = Some(MarkedIndex::new(
+ self.left_side_panes.len().saturating_sub(1),
+ ));
+ }
+ } else if self.visibility_and_focus.right_side_is_focused()
+ && !self.right_side_panes.is_empty()
+ {
+ self.marked_index = Some(MarkedIndex::new(
+ self.right_side_panes.len().saturating_sub(1),
+ ));
+ }
+ },
+ }
+ self.update_highlighted_panes();
+ }
+ pub fn mark_entry(&mut self) {
+ if let Some(marked_index) = self.marked_index.as_mut() {
+ marked_index.toggle_additional_mark();
+ self.update_highlighted_panes();
+ }
+ }
+ pub fn break_grouped_panes_to_new_tab(&mut self) {
+ let pane_ids_to_break_to_new_tab: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ let title_for_new_tab = if !self.previous_search_string.is_empty() {
+ Some(self.previous_search_string.clone())
+ } else {
+ None
+ };
+ break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true);
+ self.ungroup_panes_in_zellij(pane_ids_to_break_to_new_tab);
+ close_self();
+ }
+ pub fn stack_grouped_panes(&mut self) {
+ let pane_ids_to_stack: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ stack_panes(pane_ids_to_stack.clone());
+ self.ungroup_panes_in_zellij(pane_ids_to_stack);
+ close_self();
+ }
+ pub fn float_grouped_panes(&mut self) {
+ let pane_ids_to_float: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ float_multiple_panes(pane_ids_to_float.clone());
+ self.ungroup_panes_in_zellij(pane_ids_to_float);
+ close_self();
+ }
+ pub fn embed_grouped_panes(&mut self) {
+ let pane_ids_to_embed: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ embed_multiple_panes(pane_ids_to_embed.clone());
+ self.ungroup_panes_in_zellij(pane_ids_to_embed);
+ close_self();
+ }
+ pub fn break_grouped_panes_right(&mut self) {
+ if let Some(own_tab_index) = self.own_tab_index {
+ if Some(own_tab_index + 1) < self.total_tabs_in_session {
+ let pane_ids_to_break_right: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ break_panes_to_tab_with_index(&pane_ids_to_break_right, own_tab_index + 1, true);
+ } else {
+ let pane_ids_to_break_to_new_tab: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ let title_for_new_tab = if !self.previous_search_string.is_empty() {
+ Some(self.previous_search_string.clone())
+ } else {
+ None
+ };
+ break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true);
+ }
+ close_self();
+ }
+ }
+ pub fn break_grouped_panes_left(&mut self) {
+ if let Some(own_tab_index) = self.own_tab_index {
+ if own_tab_index > 0 {
+ let pane_ids_to_break_left: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ break_panes_to_tab_with_index(
+ &pane_ids_to_break_left,
+ own_tab_index.saturating_sub(1),
+ true,
+ );
+ } else {
+ let pane_ids_to_break_to_new_tab: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ let title_for_new_tab = if !self.previous_search_string.is_empty() {
+ Some(self.previous_search_string.clone())
+ } else {
+ None
+ };
+ break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true);
+ }
+ close_self();
+ }
+ }
+ pub fn close_grouped_panes(&mut self) {
+ let pane_ids_to_close: Vec<PaneId> =
+ self.right_side_panes.drain(..).map(|p| p.id).collect();
+ close_multiple_panes(pane_ids_to_close);
+ close_self();
+ }
+}
diff --git a/default-plugins/multiple-select/src/ui.rs b/default-plugins/multiple-select/src/ui.rs
new file mode 100644
index 00000000..0c74b0fe
--- /dev/null
+++ b/default-plugins/multiple-select/src/ui.rs
@@ -0,0 +1,873 @@
+use crate::{App, VisibilityAndFocus};
+use zellij_tile::prelude::*;
+
+const TOP_LEFT_CORNER_CHARACTER: &'static str = "┌";
+const TOP_RIGHT_CORNER_CHARACTER: &'static str = "┐";
+const BOTTOM_LEFT_CORNER_CHARACTER: &'static str = "└";
+const BOTTOM_RIGHT_CORNER_CHARACTER: &'static str = "┘";
+const BOUNDARY_CHARACTER: &'static str = "│";
+const HORIZONTAL_BOUNDARY_CHARACTER: &'static str = "─";
+
+#[derive(Debug, Clone)]
+pub struct PaneItem {
+ pub text: String,
+ pub id: PaneId,
+ pub color_indices: Vec<usize>,
+}
+
+impl PaneItem {
+ pub fn clear(&mut self) {
+ self.color_indices.clear();
+ }
+ pub fn render(&self, max_width_for_item: usize) -> NestedListItem {
+ let pane_item_text_len = self.text.chars().count();
+ if pane_item_text_len <= max_width_for_item {
+ NestedListItem::new(&self.text)
+ .color_range(0, ..)
+ .color_indices(3, self.color_indices.iter().copied().collect())
+ } else {
+ let length_of_each_half = max_width_for_item.saturating_sub(3) / 2;
+ let first_half: String = self.text.chars().take(length_of_each_half).collect();
+ let second_half: String = self
+ .text
+ .chars()
+ .rev()
+ .take(length_of_each_half)
+ .collect::<Vec<_>>()
+ .iter()
+ .rev()
+ .collect();
+ let second_half_start_index = pane_item_text_len.saturating_sub(length_of_each_half);
+ let adjusted_indices: Vec<usize> = self
+ .color_indices
+ .iter()
+ .filter_map(|i| {
+ if i < &length_of_each_half {
+ Some(*i)
+ } else if i >= &second_half_start_index {
+ Some(i.saturating_sub(second_half_start_index) + length_of_each_half + 3)
+ //3 for the bulletin
+ } else {
+ None
+ }
+ })
+ .collect();
+ NestedListItem::new(format!("{}...{}", first_half, second_half))
+ .color_range(0, ..)
+ .color_indices(3, adjusted_indices)
+ }
+ }
+}
+
+// rendering code
+impl App {
+ pub fn render_close_shortcut(&self, cols: usize) {
+ let should_render_close_shortcut =
+ self.visibility_and_focus.left_side_is_focused() && self.marked_index.is_none();
+ if should_render_close_shortcut {
+ let x_coordinates_right_padding =
+ if self.visibility_and_focus.only_left_side_is_focused() {
+ 5
+ } else {
+ 1
+ };
+ let ctrl_c_shortcut_text = "<Ctrl c> - Close";
+ let ctrl_c_shortcut = Text::new(ctrl_c_shortcut_text).color_range(3, ..=7);
+ print_text_with_coordinates(
+ ctrl_c_shortcut,
+ cols.saturating_sub(ctrl_c_shortcut_text.chars().count())
+ .saturating_sub(x_coordinates_right_padding),
+ 0,
+ None,
+ None,
+ );
+ }
+ }
+ pub fn render_tab_shortcut(&self, cols: usize, rows: usize) {
+ match self.visibility_and_focus {
+ VisibilityAndFocus::BothSidesVisibleRightSideFocused => {
+ let side_width = self.calculate_side_width(cols);
+ let tab_shortcut = Text::new("<TAB> - select more panes").color_range(3, ..=4);
+ print_text_with_coordinates(
+ tab_shortcut,
+ side_width + 6,
+ rows.saturating_sub(2),
+ None,
+ None,
+ );
+ },
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused => {
+ let side_width = self.calculate_side_width(cols);
+ let tab_shortcut_text = "<TAB> - browse selected panes";
+ let tab_shortcut = Text::new(tab_shortcut_text).color_range(3, ..=4);
+ print_text_with_coordinates(
+ tab_shortcut,
+ side_width.saturating_sub(tab_shortcut_text.chars().count() + 1),
+ rows.saturating_sub(2),
+ None,
+ None,
+ );
+ },
+ VisibilityAndFocus::OnlyRightSideVisible => {
+ let tab_shortcut = Text::new("<TAB> - select more panes").color_range(3, ..=4);
+ print_text_with_coordinates(tab_shortcut, 4, rows.saturating_sub(2), None, None);
+ },
+ VisibilityAndFocus::OnlyLeftSideVisible => {
+ // not visible
+ },
+ };
+ }
+ pub fn render_left_side(&self, rows: usize, cols: usize, is_focused: bool) {
+ let title_y = 0;
+ let left_side_base_x = 1;
+ let list_y = 2;
+ let side_width = self.calculate_side_width(cols);
+ let max_left_list_height = rows.saturating_sub(8);
+ let (
+ extra_pane_count_on_top_left,
+ extra_pane_count_on_bottom_left,
+ extra_selected_item_count_on_top_left,
+ extra_selected_item_count_on_bottom_left,
+ left_side_panes,
+ ) = self.left_side_panes_list(side_width, max_left_list_height);
+ let (filter_prompt_text, filter_prompt) = self.filter_panes_prompt();
+ let filter = self.filter(side_width.saturating_sub(filter_prompt_text.chars().count() + 1));
+ let (
+ _enter_select_panes_text,
+ enter_select_panes,
+ space_shortcut_text,
+ space_shortcut,
+ _escape_shortcut_text,
+ escape_shortcut,
+ ) = self.left_side_controls(side_width);
+ print_text_with_coordinates(filter_prompt, left_side_base_x, title_y, None, None);
+ if is_focused {
+ print_text_with_coordinates(
+ filter,
+ left_side_base_x + filter_prompt_text.chars().count(),
+ title_y,
+ None,
+ None,
+ );
+ }
+ print_nested_list_with_coordinates(
+ left_side_panes.clone(),
+ left_side_base_x,
+ list_y,
+ Some(side_width),
+ None,
+ );
+ if is_focused {
+ if let Some(marked_index) = self.marked_index.as_ref().map(|i| i.main_index) {
+ print_text_with_coordinates(
+ Text::new(">").color_range(3, ..).selected(),
+ left_side_base_x + 1,
+ (list_y + marked_index).saturating_sub(extra_pane_count_on_top_left),
+ None,
+ None,
+ );
+ }
+ }
+ if extra_pane_count_on_top_left > 0 {
+ self.print_extra_pane_count(
+ extra_pane_count_on_top_left,
+ extra_selected_item_count_on_top_left,
+ list_y.saturating_sub(1),
+ left_side_base_x,
+ side_width,
+ );
+ }
+ if extra_pane_count_on_bottom_left > 0 {
+ self.print_extra_pane_count(
+ extra_pane_count_on_bottom_left,
+ extra_selected_item_count_on_bottom_left,
+ list_y + left_side_panes.len(),
+ left_side_base_x,
+ side_width,
+ );
+ }
+ if is_focused && !left_side_panes.is_empty() {
+ let controls_x = 1;
+ print_text_with_coordinates(
+ enter_select_panes,
+ controls_x,
+ list_y + left_side_panes.len() + 1,
+ None,
+ None,
+ );
+ if self.marked_index.is_some() {
+ print_text_with_coordinates(
+ space_shortcut.clone(),
+ controls_x,
+ list_y + left_side_panes.len() + 2,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ escape_shortcut.clone(),
+ controls_x + space_shortcut_text.chars().count() + 1,
+ list_y + left_side_panes.len() + 2,
+ None,
+ None,
+ );
+ }
+ }
+ }
+ pub fn render_right_side(&self, rows: usize, cols: usize, is_focused: bool) {
+ let side_width = self.calculate_side_width(cols);
+ let right_side_base_x = match self.visibility_and_focus {
+ VisibilityAndFocus::OnlyLeftSideVisible | VisibilityAndFocus::OnlyRightSideVisible => 1,
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ | VisibilityAndFocus::BothSidesVisibleRightSideFocused => side_width + 4,
+ };
+ let title_y = 0;
+ let list_y: usize = 2;
+ let max_right_list_height = rows.saturating_sub(11);
+ let selected_prompt = self.selected_panes_title();
+ let (
+ extra_pane_count_on_top_right,
+ extra_pane_count_on_bottom_right,
+ extra_selected_item_count_on_top_right,
+ extra_selected_item_count_on_bottom_right,
+ right_side_panes,
+ ) = self.right_side_panes_list(side_width, max_right_list_height);
+ let right_side_pane_count = right_side_panes.len();
+ let (
+ right_side_controls_1,
+ right_side_controls_2,
+ right_side_controls_3,
+ right_side_controls_4,
+ ) = self.right_side_controls(side_width);
+ if extra_pane_count_on_top_right > 0 {
+ self.print_extra_pane_count(
+ extra_pane_count_on_top_right,
+ extra_selected_item_count_on_top_right,
+ list_y.saturating_sub(1),
+ right_side_base_x,
+ side_width,
+ );
+ }
+ if extra_pane_count_on_bottom_right > 0 {
+ self.print_extra_pane_count(
+ extra_pane_count_on_bottom_right,
+ extra_selected_item_count_on_bottom_right,
+ list_y + right_side_panes.len(),
+ right_side_base_x,
+ side_width,
+ );
+ }
+ print_text_with_coordinates(selected_prompt, right_side_base_x + 3, title_y, None, None);
+ print_nested_list_with_coordinates(
+ right_side_panes,
+ right_side_base_x,
+ list_y,
+ Some(side_width),
+ None,
+ );
+ if is_focused {
+ if let Some(marked_index) = self.marked_index.as_ref().map(|i| i.main_index) {
+ print_text_with_coordinates(
+ Text::new(">").color_range(3, ..).selected(),
+ right_side_base_x + 1,
+ (list_y + marked_index).saturating_sub(extra_pane_count_on_top_right),
+ None,
+ None,
+ );
+ }
+ }
+ if is_focused && !self.right_side_panes.is_empty() {
+ print_text_with_coordinates(
+ right_side_controls_1,
+ right_side_base_x + 1,
+ list_y + right_side_pane_count + 1,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ right_side_controls_2,
+ right_side_base_x + 1,
+ list_y + right_side_pane_count + 3,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ right_side_controls_3,
+ right_side_base_x + 1,
+ list_y + right_side_pane_count + 4,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ right_side_controls_4,
+ right_side_base_x + 1,
+ list_y + right_side_pane_count + 5,
+ None,
+ None,
+ );
+ }
+ }
+ pub fn render_help_line(&self, rows: usize, cols: usize) {
+ let help_line_text = match self.visibility_and_focus {
+ VisibilityAndFocus::OnlyLeftSideVisible => {
+ let full_help_line = "Help: Select one or more panes to group for bulk operations";
+ let short_help_line = "Help: Select panes to group";
+ if cols >= full_help_line.chars().count() {
+ full_help_line
+ } else {
+ short_help_line
+ }
+ },
+ VisibilityAndFocus::OnlyRightSideVisible => {
+ let full_help_line = "Help: Perform bulk operations on all selected panes";
+ let short_help_line = "Help: Perform bulk operations";
+ if cols >= full_help_line.chars().count() {
+ full_help_line
+ } else {
+ short_help_line
+ }
+ },
+ _ => {
+ let full_help_line =
+ "Help: Select panes on the left, then perform operations on the right.";
+ let short_help_line = "Help: Group panes for bulk operations";
+ if cols >= full_help_line.chars().count() {
+ full_help_line
+ } else {
+ short_help_line
+ }
+ },
+ };
+ let help_line = Text::new(help_line_text);
+ print_text_with_coordinates(help_line, 0, rows, None, None);
+ }
+ pub fn render_focus_boundary(&self, rows: usize, cols: usize) {
+ let side_width = self.calculate_side_width(cols);
+ let x = match self.visibility_and_focus {
+ VisibilityAndFocus::OnlyRightSideVisible => 0,
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ | VisibilityAndFocus::BothSidesVisibleRightSideFocused
+ | VisibilityAndFocus::OnlyLeftSideVisible => side_width + 2,
+ };
+ let y = 0;
+ let height = rows.saturating_sub(2);
+ for i in y..=height {
+ if i == y && self.visibility_and_focus.left_side_is_focused() {
+ print_text_with_coordinates(
+ Text::new(TOP_RIGHT_CORNER_CHARACTER),
+ x,
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x.saturating_sub(1),
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x.saturating_sub(2),
+ i,
+ None,
+ None,
+ );
+ } else if i == y && !self.visibility_and_focus.left_side_is_focused() {
+ print_text_with_coordinates(Text::new(TOP_LEFT_CORNER_CHARACTER), x, i, None, None);
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x + 1,
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x + 2,
+ i,
+ None,
+ None,
+ );
+ } else if i == height && self.visibility_and_focus.left_side_is_focused() {
+ print_text_with_coordinates(
+ Text::new(BOTTOM_RIGHT_CORNER_CHARACTER),
+ x,
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x.saturating_sub(1),
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x.saturating_sub(2),
+ i,
+ None,
+ None,
+ );
+ } else if i == height && !self.visibility_and_focus.left_side_is_focused() {
+ print_text_with_coordinates(
+ Text::new(BOTTOM_LEFT_CORNER_CHARACTER),
+ x,
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x + 1,
+ i,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(HORIZONTAL_BOUNDARY_CHARACTER),
+ x + 2,
+ i,
+ None,
+ None,
+ );
+ } else {
+ print_text_with_coordinates(Text::new(BOUNDARY_CHARACTER), x, i, None, None);
+ }
+ }
+ }
+ pub fn calculate_side_width(&self, cols: usize) -> usize {
+ match self.visibility_and_focus {
+ VisibilityAndFocus::OnlyLeftSideVisible | VisibilityAndFocus::OnlyRightSideVisible => {
+ cols.saturating_sub(4)
+ },
+ VisibilityAndFocus::BothSidesVisibleLeftSideFocused
+ | VisibilityAndFocus::BothSidesVisibleRightSideFocused => (cols / 2).saturating_sub(3),
+ }
+ }
+}
+
+// ui components
+impl App {
+ fn filter_panes_prompt(&self) -> (&'static str, Text) {
+ let search_prompt_text = if self.search_string.is_empty() {
+ "ALL PANES "
+ } else {
+ "FILTER: "
+ };
+ let search_prompt = if self.visibility_and_focus.left_side_is_focused() {
+ Text::new(&search_prompt_text).color_range(2, ..)
+ } else {
+ Text::new(&search_prompt_text)
+ };
+ (search_prompt_text, search_prompt)
+ }
+ fn filter(&self, max_width: usize) -> Text {
+ let search_string_text = if self.marked_index.is_none() && self.search_string.is_empty() {
+ let full = "[Type filter term...]";
+ let short = "[...]";
+ if max_width >= full.chars().count() {
+ full.to_owned()
+ } else {
+ short.to_owned()
+ }
+ } else if self.marked_index.is_none() && !self.search_string.is_empty() {
+ if max_width >= self.search_string.chars().count() + 1 {
+ format!("{}_", self.search_string)
+ } else {
+ let truncated: String = self
+ .search_string
+ .chars()
+ .rev()
+ .take(max_width.saturating_sub(4))
+ .collect::<Vec<_>>()
+ .iter()
+ .rev()
+ .collect();
+ format!("...{}_", truncated)
+ }
+ } else if self.marked_index.is_some() && !self.search_string.is_empty() {
+ if max_width >= self.search_string.chars().count() {
+ format!("{}", self.search_string)
+ } else {
+ let truncated: String = self
+ .search_string
+ .chars()
+ .rev()
+ .take(max_width.saturating_sub(4))
+ .collect::<Vec<_>>()
+ .iter()
+ .rev()
+ .collect();
+ format!("...{}", truncated)
+ }
+ } else {
+ format!("")
+ };
+ Text::new(&search_string_text).color_range(3, ..)
+ }
+ fn left_side_panes_list(
+ &self,
+ max_width: usize,
+ max_list_height: usize,
+ ) -> (usize, usize, usize, usize, Vec<NestedListItem>) {
+ // returns: extra_pane_count_on_top, extra_pane_count_on_bottom,
+ // extra_selected_item_count_on_top, extra_selected_item_count_on_bottom, list
+ let mut left_side_panes = vec![];
+ let pane_items_on_the_left = self
+ .search_results
+ .as_ref()
+ .unwrap_or_else(|| &self.left_side_panes);
+ let max_width_for_item = max_width.saturating_sub(3); // 3 for the list bulletin
+ let item_count = pane_items_on_the_left.iter().count();
+ let first_item_index = if self.visibility_and_focus.left_side_is_focused() {
+ self.marked_index
+ .as_ref()
+ .map(|s| s.main_index.saturating_sub(max_list_height / 2))
+ .unwrap_or(0)
+ } else {
+ 0
+ };
+ let last_item_index = std::cmp::min(
+ (max_list_height + first_item_index).saturating_sub(1),
+ item_count.saturating_sub(1),
+ );
+ for (i, pane_item) in pane_items_on_the_left
+ .iter()
+ .enumerate()
+ .skip(first_item_index)
+ {
+ if i > last_item_index {
+ break;
+ }
+ let mut item = pane_item.render(max_width_for_item);
+ if Some(i) == self.marked_index.as_ref().map(|s| s.main_index)
+ && self.visibility_and_focus.left_side_is_focused()
+ {
+ item = item.selected();
+ if self
+ .marked_index
+ .as_ref()
+ .map(|s| s.additional_indices.contains(&i))
+ .unwrap_or(false)
+ {
+ item = item.selected().color_range(1, ..);
+ }
+ } else if self
+ .marked_index
+ .as_ref()
+ .map(|s| s.additional_indices.contains(&i))
+ .unwrap_or(false)
+ && self.visibility_and_focus.left_side_is_focused()
+ {
+ item = item.selected();
+ }
+ left_side_panes.push(item);
+ }
+ let extra_panes_on_top = first_item_index;
+ let extra_panes_on_bottom = item_count.saturating_sub(last_item_index + 1);
+ let extra_selected_item_count_on_top = if self.visibility_and_focus.left_side_is_focused() {
+ self.marked_index
+ .as_ref()
+ .map(|s| {
+ s.additional_indices
+ .iter()
+ .filter(|a| a < &&first_item_index)
+ .count()
+ })
+ .unwrap_or(0)
+ } else {
+ 0
+ };
+ let extra_selected_item_count_on_bottom =
+ if self.visibility_and_focus.left_side_is_focused() {
+ self.marked_index
+ .as_ref()
+ .map(|s| {
+ s.additional_indices
+ .iter()
+ .filter(|a| a > &&last_item_index)
+ .count()
+ })
+ .unwrap_or(0)
+ } else {
+ 0
+ };
+ (
+ extra_panes_on_top,
+ extra_panes_on_bottom,
+ extra_selected_item_count_on_top,
+ extra_selected_item_count_on_bottom,
+ left_side_panes,
+ )
+ }
+ fn left_side_controls(
+ &self,
+ max_width: usize,
+ ) -> (&'static str, Text, &'static str, Text, &'static str, Text) {
+ // returns three components and their text
+ let (enter_select_panes_text, enter_select_panes) = if self.marked_index.is_some() {
+ let enter_select_panes_text_full = "<ENTER> - select, <↓↑→> - navigate";
+ let enter_select_panes_text_short = "<ENTER> / <↓↑→>...";
+ if max_width >= enter_select_panes_text_full.chars().count() {
+ let enter_select_panes_full = Text::new(enter_select_panes_text_full)
+ .color_range(3, ..=6)
+ .color_range(3, 18..=22);
+ (enter_select_panes_text_full, enter_select_panes_full)
+ } else {
+ let enter_select_panes_short = Text::new(enter_select_panes_text_short)
+ .color_range(3, ..=6)
+ .color_range(3, 10..=14);
+ (enter_select_panes_text_short, enter_select_panes_short)
+ }
+ } else {
+ let enter_select_panes_text_full = "<ENTER> - select all, <↓↑> - navigate";
+ let enter_select_panes_text_short = "<ENTER> / <↓↑>...";
+ if max_width >= enter_select_panes_text_full.chars().count() {
+ let enter_select_panes_full = Text::new(enter_select_panes_text_full)
+ .color_range(3, ..=6)
+ .color_range(3, 21..=25);
+ (enter_select_panes_text_full, enter_select_panes_full)
+ } else {
+ let enter_select_panes_short = Text::new(enter_select_panes_text_short)
+ .color_range(3, ..=6)
+ .color_range(3, 10..=13);
+ (enter_select_panes_text_short, enter_select_panes_short)
+ }
+ };
+ let space_shortcut_text_full = "<SPACE> - mark many,";
+ let space_shortcut_text_short = "<SPACE> /";
+ if self.marked_index.is_some() {
+ let escape_shortcut_text_full = "<Ctrl c> - remove marks";
+ let escape_shortcut_text_short = "<Ctrl c>...";
+ let (escape_shortcut, space_shortcut, escape_shortcut_text, space_shortcut_text) =
+ if max_width
+ >= space_shortcut_text_full.chars().count()
+ + escape_shortcut_text_full.chars().count()
+ {
+ (
+ Text::new(escape_shortcut_text_full).color_range(3, ..=7),
+ Text::new(space_shortcut_text_full).color_range(3, ..=6),
+ escape_shortcut_text_full,
+ space_shortcut_text_full,
+ )
+ } else {
+ (
+ Text::new(escape_shortcut_text_short).color_range(3, ..=7),
+ Text::new(space_shortcut_text_short).color_range(3, ..=6),
+ escape_shortcut_text_short,
+ space_shortcut_text_short,
+ )
+ };
+ (
+ enter_select_panes_text,
+ enter_select_panes,
+ space_shortcut_text,
+ space_shortcut,
+ escape_shortcut_text,
+ escape_shortcut,
+ )
+ } else {
+ let escape_shortcut_text = if self.right_side_panes.is_empty() {
+ "<Ctrl c> - Close"
+ } else {
+ ""
+ };
+ let escape_shortcut = Text::new(escape_shortcut_text).color_range(3, ..=7);
+ let space_shortcut = Text::new(space_shortcut_text_full).color_range(3, ..=6);
+ (
+ enter_select_panes_text,
+ enter_select_panes,
+ space_shortcut_text_full,
+ space_shortcut,
+ escape_shortcut_text,
+ escape_shortcut,
+ )
+ }
+ }
+ fn selected_panes_title(&self) -> Text {
+ let selected_prompt_text = "SELECTED PANES: ";
+ let selected_prompt = if self.visibility_and_focus.left_side_is_focused() {
+ Text::new(selected_prompt_text)
+ } else {
+ Text::new(selected_prompt_text).color_range(2, ..)
+ };
+ selected_prompt
+ }
+ fn right_side_panes_list(
+ &self,
+ max_width: usize,
+ max_list_height: usize,
+ ) -> (usize, usize, usize, usize, Vec<NestedListItem>) {
+ // returns: extra_pane_count_on_top, extra_pane_count_on_bottom,
+ // extra_selected_item_count_on_top, extra_selected_item_count_on_bottom, list
+ let mut right_side_panes = vec![];
+ let item_count = self.right_side_panes.iter().count();
+ let first_item_index = if self.visibility_and_focus.left_side_is_focused() {
+ 0
+ } else {
+ self.marked_index
+ .as_ref()
+ .map(|s| s.main_index.saturating_sub(max_list_height / 2))
+ .unwrap_or(0)
+ };
+ let last_item_index = std::cmp::min(
+ (max_list_height + first_item_index).saturating_sub(1),
+ item_count.saturating_sub(1),
+ );
+
+ let max_width_for_item = max_width.saturating_sub(3); // 3 for the list bulletin
+ for (i, pane_item) in self
+ .right_side_panes
+ .iter()
+ .enumerate()
+ .skip(first_item_index)
+ {
+ if i > last_item_index {
+ break;
+ }
+ let mut item = pane_item.render(max_width_for_item);
+ if &Some(i) == &self.marked_index.as_ref().map(|s| s.main_index)
+ && self.visibility_and_focus.right_side_is_focused()
+ {
+ item = item.selected();
+ if self
+ .marked_index
+ .as_ref()
+ .map(|s| s.additional_indices.contains(&i))
+ .unwrap_or(false)
+ {
+ item = item.selected().color_range(1, ..);
+ }
+ } else if self
+ .marked_index
+ .as_ref()
+ .map(|s| s.additional_indices.contains(&i))
+ .unwrap_or(false)
+ && self.visibility_and_focus.right_side_is_focused()
+ {
+ item = item.selected();
+ }
+ right_side_panes.push(item);
+ }
+
+ let extra_panes_on_top = first_item_index;
+ let extra_panes_on_bottom = self
+ .right_side_panes
+ .iter()
+ .len()
+ .saturating_sub(last_item_index + 1);
+ let extra_selected_item_count_on_top = if self.visibility_and_focus.left_side_is_focused() {
+ 0
+ } else {
+ self.marked_index
+ .as_ref()
+ .map(|s| {
+ s.additional_indices
+ .iter()
+ .filter(|a| a < &&first_item_index)
+ .count()
+ })
+ .unwrap_or(0)
+ };
+ let extra_selected_item_count_on_bottom =
+ if self.visibility_and_focus.left_side_is_focused() {
+ 0
+ } else {
+ self.marked_index
+ .as_ref()
+ .map(|s| {
+ s.additional_indices
+ .iter()
+ .filter(|a| a > &&last_item_index)
+ .count()
+ })
+ .unwrap_or(0)
+ };
+ (
+ extra_panes_on_top,
+ extra_panes_on_bottom,
+ extra_selected_item_count_on_top,
+ extra_selected_item_count_on_bottom,
+ right_side_panes,
+ )
+ }
+ fn right_side_controls(&self, cols: usize) -> (Text, Text, Text, Text) {
+ let right_side_controls_text_1_full = "<←↓↑> - navigate, <Ctrl c> - clear";
+ let right_side_controls_text_1_short = "<←↓↑>/<Ctrl c>...";
+ let right_side_controls_1 = if cols >= right_side_controls_text_1_full.chars().count() {
+ Text::new(right_side_controls_text_1_full)
+ .color_range(3, ..=4)
+ .color_range(3, 18..=25)
+ } else {
+ Text::new(right_side_controls_text_1_short)
+ .color_range(3, ..=4)
+ .color_range(3, 6..=13)
+ };
+ let right_side_controls_text_2_full = "<b> - break out, <s> - stack, <c> - close";
+ let right_side_controls_text_2_short = "<b>/<s>/<c>...";
+ let right_side_controls_2 = if cols >= right_side_controls_text_2_full.chars().count() {
+ Text::new(right_side_controls_text_2_full)
+ .color_range(3, ..=2)
+ .color_range(3, 17..=19)
+ .color_range(3, 30..=32)
+ } else {
+ Text::new(right_side_controls_text_2_short)
+ .color_range(3, ..=2)
+ .color_range(3, 4..=6)
+ .color_range(3, 8..=10)
+ };
+ let right_side_controls_text_3_full = "<r> - break right, <l> - break left";
+ let right_side_controls_text_3_short = "<r>/<l>...";
+ let right_side_controls_3 = if cols >= right_side_controls_text_3_full.chars().count() {
+ Text::new(right_side_controls_text_3_full)
+ .color_range(3, ..=2)
+ .color_range(3, 19..=21)
+ } else {
+ Text::new(right_side_controls_text_3_short)
+ .color_range(3, ..=2)
+ .color_range(3, 4..=6)
+ };
+ let right_side_controls_text_4_full = "<e> - embed, <f> - float";
+ let right_side_controls_text_4_short = "<e>/<f>...";
+ let right_side_controls_4 = if cols >= right_side_controls_text_4_full.chars().count() {
+ Text::new(right_side_controls_text_4_full)
+ .color_range(3, ..=2)
+ .color_range(3, 13..=15)
+ } else {
+ Text::new(right_side_controls_text_4_short)
+ .color_range(3, ..=2)
+ .color_range(3, 4..=6)
+ };
+ (
+ right_side_controls_1,
+ right_side_controls_2,
+ right_side_controls_3,
+ right_side_controls_4,
+ )
+ }
+ fn print_extra_pane_count(
+ &self,
+ count: usize,
+ selected_count: usize,
+ y: usize,
+ list_x: usize,
+ list_width: usize,
+ ) {
+ let extra_count_text = if selected_count > 0 {
+ format!("[+{} ({} selected)]", count, selected_count)
+ } else {
+ format!("[+{}]", count)
+ };
+ let extra_count = Text::new(&extra_count_text).color_range(1, ..);
+ print_text_with_coordinates(
+ extra_count,
+ (list_x + list_width).saturating_sub(extra_count_text.chars().count()),
+ y,
+ None,
+ None,
+ );
+ }
+}
diff --git a/default-plugins/status-bar/src/main.rs b/default-plugins/status-bar/src/main.rs
index 65b73819..dd5d54c5 100644
--- a/default-plugins/status-bar/src/main.rs
+++ b/default-plugins/status-bar/src/main.rs
@@ -39,6 +39,8 @@ struct State {
display_system_clipboard_failure: bool,
classic_ui: bool,
base_mode_is_locked: bool,
+ own_client_id: Option<ClientId>,
+ grouped_panes_count: Option<usize>,
}
register_plugin!(State);
@@ -197,10 +199,12 @@ impl ZellijPlugin for State {
.get("classic")
.map(|c| c == "true")
.unwrap_or(false);
+ self.own_client_id = Some(get_plugin_ids().client_id);
set_selectable(false);
subscribe(&[
EventType::ModeUpdate,
EventType::TabUpdate,
+ EventType::PaneUpdate,
EventType::CopyToClipboard,
EventType::InputReceived,
EventType::SystemClipboardFailure,
@@ -223,6 +227,28 @@ impl ZellijPlugin for State {
}
self.tabs = tabs;
},
+ Event::PaneUpdate(pane_manifest) => {
+ if let Some(own_client_id) = self.own_client_id {
+ let mut grouped_panes_count = 0;
+ for (_tab_index, pane_infos) in pane_manifest.panes {
+ for pane_info in pane_infos {
+ let is_in_pane_group =
+ pane_info.index_in_pane_group.get(&own_client_id).is_some();
+ if is_in_pane_group {
+ grouped_panes_count += 1;
+ }
+ }
+ }
+ if Some(grouped_panes_count) != self.grouped_panes_count {
+ if grouped_panes_count == 0 {
+ self.grouped_panes_count = None;
+ } else {
+ self.grouped_panes_count = Some(grouped_panes_count);
+ }
+ should_render = true;
+ }
+ }
+ },
Event::CopyToClipboard(copy_destination) => {
match self.text_copy_destination {
Some(text_copy_destination) => {
@@ -280,6 +306,7 @@ impl ZellijPlugin for State {
self.base_mode_is_locked,
self.text_copy_destination,
self.display_system_clipboard_failure,
+ self.grouped_panes_count,
),
fill_bg,
);
diff --git a/default-plugins/status-bar/src/one_line_ui.rs b/default-plugins/status-bar/src/one_line_ui.rs
index 83217425..f6b88491 100644
--- a/default-plugins/status-bar/src/one_line_ui.rs
+++ b/default-plugins/status-bar/src/one_line_ui.rs
@@ -6,7 +6,7 @@ use ansi_term::{
use std::collections::HashMap;
use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*;
-use zellij_tile_utils::palette_match;
+use zellij_tile_utils::{palette_match, style};
use crate::first_line::{to_char, KeyAction, KeyMode, KeyShortcut};
use crate::second_line::{system_clipboard_error, text_copied_hint};
@@ -22,6 +22,7 @@ pub fn one_line_ui(
base_mode_is_locked: bool,
text_copied_to_clipboard_destination: Option<CopyDestination>,
clipboard_failure: bool,
+ grouped_pane_count: Option<usize>,
) -> LinePart {
if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination {
return text_copied_hint(text_copied_to_clipboard_destination);
@@ -35,11 +36,18 @@ pub fn one_line_ui(
*max_len = max_len.saturating_sub(line_part.len);
};
+ let currently_marking_pane_group = help.currently_marking_pane_group.unwrap_or(false);
render_mode_key_indicators(help, max_len, separator, base_mode_is_locked)
.map(|mode_key_indicators| append(&mode_key_indicators, &mut max_len))
.and_then(|_| match help.mode {
- InputMode::Normal | InputMode::Locked => render_secondary_info(help, tab_info, max_len)
- .map(|secondary_info| append(&secondary_info, &mut max_len)),
+ InputMode::Normal | InputMode::Locked => match grouped_pane_count {
+ Some(grouped_pane_count) => {
+ render_group_controls(help, grouped_pane_count, max_len)
+ },
+ None if currently_marking_pane_group => render_group_controls(help, 0, max_len),
+ None => render_secondary_info(help, tab_info, max_len),
+ }
+ .map(|secondary_info| append(&secondary_info, &mut max_len)),
_ => add_keygroup_separator(help, max_len)
.map(|key_group_separator| append(&key_group_separator, &mut max_len))
.and_then(|_| keybinds(help, max_len))
@@ -656,6 +664,225 @@ fn render_common_modifiers(
line_part_to_render.len += prefix_text.chars().count() + separator.chars().count();
}
+fn render_group_controls(
+ help: &ModeInfo,
+ grouped_pane_count: usize,
+ max_len: usize,
+) -> Option<LinePart> {
+ let currently_marking_group = help.currently_marking_pane_group.unwrap_or(false);
+ let keymap = help.get_mode_keybinds();
+ let (common_modifiers, multiple_select_key, pane_group_toggle_key, group_mark_toggle_key) = {
+ let multiple_select_key = multiple_select_key(&keymap);
+ let pane_group_toggle_key = single_action_key(&keymap, &[Action::TogglePaneInGroup]);
+ let group_mark_toggle_key = single_action_key(&keymap, &[Action::ToggleGroupMarking]);
+ let common_modifiers = get_common_modifiers(
+ vec![
+ multiple_select_key.iter().next(),
+ pane_group_toggle_key.iter().next(),
+ group_mark_toggle_key.iter().next(),
+ ]
+ .into_iter()
+ .filter_map(|k| k)
+ .collect(),
+ );
+ let multiple_select_key: Vec<KeyWithModifier> = multiple_select_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let pane_group_toggle_key: Vec<KeyWithModifier> = pane_group_toggle_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ let group_mark_toggle_key: Vec<KeyWithModifier> = group_mark_toggle_key
+ .iter()
+ .map(|k| k.strip_common_modifiers(&common_modifiers))
+ .collect();
+ (
+ common_modifiers,
+ multiple_select_key,
+ pane_group_toggle_key,
+ group_mark_toggle_key,
+ )
+ };
+ let multiple_select_key = multiple_select_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let pane_group_toggle_key = pane_group_toggle_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let group_mark_toggle_key = group_mark_toggle_key
+ .iter()
+ .next()
+ .map(|key| format!("{}", key))
+ .unwrap_or("UNBOUND".to_owned());
+ let background = help.style.colors.text_unselected.background;
+ let foreground = help.style.colors.text_unselected.base;
+ let superkey_prefix_style = style!(foreground, background).bold();
+ let common_modifier_text = if common_modifiers.is_empty() {
+ "".to_owned()
+ } else {
+ format!(
+ "{} + ",
+ common_modifiers
+ .iter()
+ .map(|c| c.to_string())
+ .collect::<Vec<_>>()
+ .join("-")
+ )
+ };
+
+ // full
+ let full_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED PANES", grouped_pane_count)
+ } else {
+ format!("{} SELECTED PANES |", grouped_pane_count)
+ };
+ let full_group_actions_text = format!("<{}> Group Actions", &multiple_select_key);
+ let full_toggle_group_text = format!("<{}> Toggle Group", &pane_group_toggle_key);
+ let full_group_mark_toggle_text = format!("<{}> Follow Focus", &group_mark_toggle_key);
+ let ribbon_paddings_len = 12;
+ let full_controls_line_len = full_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + full_group_actions_text.chars().count()
+ + full_toggle_group_text.chars().count()
+ + full_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ // medium
+ let medium_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED", grouped_pane_count)
+ } else {
+ format!("{} SELECTED |", grouped_pane_count)
+ };
+ let medium_group_actions_text = format!("<{}> Actions", &multiple_select_key);
+ let medium_toggle_group_text = format!("<{}> Toggle", &pane_group_toggle_key);
+ let medium_group_mark_toggle_text = format!("<{}> Follow", &group_mark_toggle_key);
+ let ribbon_paddings_len = 12;
+ let medium_controls_line_len = medium_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + medium_group_actions_text.chars().count()
+ + medium_toggle_group_text.chars().count()
+ + medium_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ // short
+ let short_selected_panes_text = if common_modifier_text.is_empty() {
+ format!("{} SELECTED", grouped_pane_count)
+ } else {
+ format!("{} SELECTED |", grouped_pane_count)
+ };
+ let short_group_actions_text = format!("<{}>", &multiple_select_key);
+ let short_toggle_group_text = format!("<{}>", &pane_group_toggle_key);
+ let short_group_mark_toggle_text = format!("<{}>", &group_mark_toggle_key);
+ let color_emphasis_range_end = if common_modifier_text.is_empty() {
+ 0
+ } else {
+ 2
+ };
+ let ribbon_paddings_len = 12;
+ let short_controls_line_len = short_selected_panes_text.chars().count()
+ + 1
+ + common_modifier_text.chars().count()
+ + short_group_actions_text.chars().count()
+ + short_toggle_group_text.chars().count()
+ + short_group_mark_toggle_text.chars().count()
+ + ribbon_paddings_len
+ + 1; // 1 for the end padding
+
+ let (
+ selected_panes_text,
+ group_actions_text,
+ toggle_group_text,
+ group_mark_toggle_text,
+ controls_line_len,
+ ) = if max_len >= full_controls_line_len {
+ (
+ full_selected_panes_text,
+ full_group_actions_text,
+ full_toggle_group_text,
+ full_group_mark_toggle_text,
+ full_controls_line_len,
+ )
+ } else if max_len >= medium_controls_line_len {
+ (
+ medium_selected_panes_text,
+ medium_group_actions_text,
+ medium_toggle_group_text,
+ medium_group_mark_toggle_text,
+ medium_controls_line_len,
+ )
+ } else if max_len >= short_controls_line_len {
+ (
+ short_selected_panes_text,
+ short_group_actions_text,
+ short_toggle_group_text,
+ short_group_mark_toggle_text,
+ short_controls_line_len,
+ )
+ } else {
+ return None;
+ };
+ let selected_panes = serialize_text(
+ &Text::new(&selected_panes_text)
+ .color_range(
+ 3,
+ ..selected_panes_text
+ .chars()
+ .count()
+ .saturating_sub(color_emphasis_range_end),
+ )
+ .opaque(),
+ );
+ let group_actions_ribbon = serialize_ribbon(
+ &Text::new(&group_actions_text).color_range(0, 1..=multiple_select_key.chars().count()),
+ );
+ let toggle_group_ribbon = serialize_ribbon(
+ &Text::new(&toggle_group_text).color_range(0, 1..=pane_group_toggle_key.chars().count()),
+ );
+ let mut group_mark_toggle_ribbon = Text::new(&group_mark_toggle_text)
+ .color_range(0, 1..=group_mark_toggle_key.chars().count());
+ if currently_marking_group {
+ group_mark_toggle_ribbon = group_mark_toggle_ribbon.selected();
+ }
+ let group_mark_toggle_ribbon = serialize_ribbon(&group_mark_toggle_ribbon);
+ let controls_line = if common_modifiers.is_empty() {
+ format!(
+ "{} {}{}{}",
+ selected_panes, group_actions_ribbon, toggle_group_ribbon, group_mark_toggle_ribbon
+ )
+ } else {
+ let common_modifier =
+ serialize_text(&Text::new(&common_modifier_text).color_range(0, ..).opaque());
+ format!(
+ "{} {}{}{}{}",
+ selected_panes,
+ common_modifier,
+ group_actions_ribbon,
+ toggle_group_ribbon,
+ group_mark_toggle_ribbon
+ )
+ };
+ let remaining_space = max_len.saturating_sub(controls_line_len);
+ let mut padding = String::new();
+ let mut padding_len = 0;
+ for _ in 0..remaining_space {
+ padding.push_str(&ANSIStrings(&[superkey_prefix_style.paint(" ")]).to_string());
+ padding_len += 1;
+ }
+ Some(LinePart {
+ part: format!("{}{}", padding, controls_line),
+ len: controls_line_len + padding_len,
+ })
+}
+
fn render_secondary_info(
help: &ModeInfo,
tab_info: Option<&TabInfo>,
@@ -1168,6 +1395,7 @@ fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
bits.push(
Style::new()
.fg(separator_color)
+ .on(bg_color)
.bold()
.paint(format!(" {} ", mode_help_text)),
);
@@ -1330,6 +1558,7 @@ fn get_keys_and_hints(mi: &ModeInfo) -> Vec<(String, String, Vec<KeyWithModifier
(s("Switch Location"), s("Move"), action_key_group(&km, &[
&[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))],
&[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])),
+ (s("When done"), s("Back"), to_basemode_key),
]} else if mi.mode == IM::Scroll { vec![
(s("Enter search term"), s("Search"),
action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])),
@@ -1516,6 +1745,25 @@ fn configuration_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithMo
}
}
+fn multiple_select_key(keymap: &[(KeyWithModifier, Vec<Action>)]) -> Vec<KeyWithModifier> {
+ let mut matching = keymap.iter().find_map(|(key, acvec)| {
+ let has_match = acvec
+ .iter()
+ .find(|a| a.launches_plugin("zellij:multiple-select")) // TODO: make this an alias
+ .is_some();
+ if has_match {
+ Some(key.clone())
+ } else {
+ None
+ }
+ });
+ if let Some(matching) = matching.take() {
+ vec![matching]
+ } else {
+ vec![]
+ }
+}
+
fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart {
if keyvec.is_empty() {
return LinePart::default();