diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-04-29 20:52:17 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-04-29 20:52:17 +0200 |
| commit | 27c8986939a1642f3e4afda73e9717b7bd328824 (patch) | |
| tree | e80ee57aeede2578a5ce9fa6d0ab4a350ed9d838 /default-plugins/compact-bar | |
| parent | chore: update development version (diff) | |
| download | sl0ppij-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/compact-bar')
| -rw-r--r-- | default-plugins/compact-bar/src/line.rs | 300 | ||||
| -rw-r--r-- | default-plugins/compact-bar/src/main.rs | 28 |
2 files changed, 318 insertions, 10 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 |
