aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins/status-bar/src
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/status-bar/src
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/status-bar/src')
-rw-r--r--default-plugins/status-bar/src/main.rs27
-rw-r--r--default-plugins/status-bar/src/one_line_ui.rs254
2 files changed, 278 insertions, 3 deletions
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();