* intercept/clear-intercept key APIs * allow opening a pinned unfocused floating pane * rework plugin * improve some apis * fix tests * tests for pane groups * more exact placement and tests * plugin command permission and cleanup * improve some multiselect ux * improve plugin ui * remove old status indicator * allow moving plugin out of the way * style(fmt): rustfmt * update plugins * remove old keybinding * cleanups * fix: only rename pane if needed * changelog and some cleanups * style(fmt): rustfmt
469 lines
16 KiB
Rust
469 lines
16 KiB
Rust
use std::collections::{HashMap, HashSet};
|
|
|
|
use zellij_utils::data::FloatingPaneCoordinates;
|
|
use zellij_utils::input::layout::{RunPluginOrAlias, SplitSize};
|
|
use zellij_utils::pane_size::Size;
|
|
|
|
use crate::{panes::PaneId, pty::PtyInstruction, thread_bus::ThreadSenders, ClientId};
|
|
|
|
pub struct PaneGroups {
|
|
panes_in_group: HashMap<ClientId, Vec<PaneId>>,
|
|
senders: ThreadSenders,
|
|
}
|
|
|
|
impl std::fmt::Debug for PaneGroups {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("PaneGroups")
|
|
.field("panes_in_group", &self.panes_in_group)
|
|
.finish_non_exhaustive()
|
|
}
|
|
}
|
|
|
|
impl PaneGroups {
|
|
pub fn new(senders: ThreadSenders) -> Self {
|
|
PaneGroups {
|
|
panes_in_group: HashMap::new(),
|
|
senders,
|
|
}
|
|
}
|
|
pub fn clone_inner(&self) -> HashMap<ClientId, Vec<PaneId>> {
|
|
self.panes_in_group.clone()
|
|
}
|
|
pub fn get_client_pane_group(&self, client_id: &ClientId) -> HashSet<PaneId> {
|
|
self.panes_in_group
|
|
.get(client_id)
|
|
.map(|p| p.iter().copied().collect())
|
|
.unwrap_or_else(|| HashSet::new())
|
|
}
|
|
pub fn clear_pane_group(&mut self, client_id: &ClientId) {
|
|
self.panes_in_group.get_mut(client_id).map(|p| p.clear());
|
|
}
|
|
pub fn toggle_pane_id_in_group(
|
|
&mut self,
|
|
pane_id: PaneId,
|
|
screen_size: Size,
|
|
client_id: &ClientId,
|
|
) {
|
|
let previous_groups = self.clone_inner();
|
|
let client_pane_group = self
|
|
.panes_in_group
|
|
.entry(*client_id)
|
|
.or_insert_with(|| vec![]);
|
|
if client_pane_group.contains(&pane_id) {
|
|
client_pane_group.retain(|p| p != &pane_id);
|
|
} else {
|
|
client_pane_group.push(pane_id);
|
|
};
|
|
if self.should_launch_plugin(&previous_groups, client_id) {
|
|
self.launch_plugin(screen_size, client_id);
|
|
}
|
|
}
|
|
pub fn add_pane_id_to_group(
|
|
&mut self,
|
|
pane_id: PaneId,
|
|
screen_size: Size,
|
|
client_id: &ClientId,
|
|
) {
|
|
let previous_groups = self.clone_inner();
|
|
let client_pane_group = self
|
|
.panes_in_group
|
|
.entry(*client_id)
|
|
.or_insert_with(|| vec![]);
|
|
if !client_pane_group.contains(&pane_id) {
|
|
client_pane_group.push(pane_id);
|
|
}
|
|
if self.should_launch_plugin(&previous_groups, client_id) {
|
|
self.launch_plugin(screen_size, client_id);
|
|
}
|
|
}
|
|
pub fn group_and_ungroup_panes(
|
|
&mut self,
|
|
mut pane_ids_to_group: Vec<PaneId>,
|
|
pane_ids_to_ungroup: Vec<PaneId>,
|
|
screen_size: Size,
|
|
client_id: &ClientId,
|
|
) {
|
|
let previous_groups = self.clone_inner();
|
|
let client_pane_group = self
|
|
.panes_in_group
|
|
.entry(*client_id)
|
|
.or_insert_with(|| vec![]);
|
|
client_pane_group.append(&mut pane_ids_to_group);
|
|
client_pane_group.retain(|p| !pane_ids_to_ungroup.contains(p));
|
|
if self.should_launch_plugin(&previous_groups, client_id) {
|
|
self.launch_plugin(screen_size, client_id);
|
|
}
|
|
}
|
|
pub fn override_groups_with(&mut self, new_pane_groups: HashMap<ClientId, Vec<PaneId>>) {
|
|
self.panes_in_group = new_pane_groups;
|
|
}
|
|
fn should_launch_plugin(
|
|
&self,
|
|
previous_groups: &HashMap<ClientId, Vec<PaneId>>,
|
|
client_id: &ClientId,
|
|
) -> bool {
|
|
let mut should_launch = false;
|
|
for (client_id, previous_panes) in previous_groups {
|
|
let previous_panes_has_panes = !previous_panes.is_empty();
|
|
let current_panes_has_panes = self
|
|
.panes_in_group
|
|
.get(&client_id)
|
|
.map(|g| !g.is_empty())
|
|
.unwrap_or(false);
|
|
if !previous_panes_has_panes && current_panes_has_panes {
|
|
should_launch = true;
|
|
}
|
|
}
|
|
should_launch || previous_groups.get(&client_id).is_none()
|
|
}
|
|
fn launch_plugin(&self, screen_size: Size, client_id: &ClientId) {
|
|
if let Ok(run_plugin) =
|
|
RunPluginOrAlias::from_url("zellij:multiple-select", &None, None, None)
|
|
{
|
|
let tab_index = 1;
|
|
let size = Size::default();
|
|
let should_float = Some(true);
|
|
let should_be_opened_in_place = false;
|
|
let pane_title = None;
|
|
let skip_cache = false;
|
|
let cwd = None;
|
|
let should_focus_plugin = Some(false);
|
|
let width_30_percent = (screen_size.cols as f64 * 0.3) as usize;
|
|
let height_30_percent = (screen_size.rows as f64 * 0.3) as usize;
|
|
let width = std::cmp::max(width_30_percent, 48);
|
|
let height = std::cmp::max(height_30_percent, 10);
|
|
let y_position = screen_size.rows.saturating_sub(height + 2);
|
|
let floating_pane_coordinates = FloatingPaneCoordinates {
|
|
x: Some(SplitSize::Fixed(2)),
|
|
y: Some(SplitSize::Fixed(y_position)),
|
|
width: Some(SplitSize::Fixed(width)),
|
|
height: Some(SplitSize::Fixed(height)),
|
|
pinned: Some(true),
|
|
};
|
|
let _ = self.senders.send_to_pty(PtyInstruction::FillPluginCwd(
|
|
should_float,
|
|
should_be_opened_in_place,
|
|
pane_title,
|
|
run_plugin,
|
|
tab_index,
|
|
None,
|
|
*client_id,
|
|
size,
|
|
skip_cache,
|
|
cwd,
|
|
should_focus_plugin,
|
|
Some(floating_pane_coordinates),
|
|
));
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use std::collections::HashMap;
|
|
|
|
fn create_mock_senders() -> ThreadSenders {
|
|
let mut mock = ThreadSenders::default();
|
|
mock.should_silently_fail = true;
|
|
mock
|
|
}
|
|
|
|
fn create_test_pane_groups() -> PaneGroups {
|
|
PaneGroups::new(create_mock_senders())
|
|
}
|
|
|
|
fn create_test_screen_size() -> Size {
|
|
Size { rows: 24, cols: 80 }
|
|
}
|
|
|
|
#[test]
|
|
fn new_creates_empty_pane_groups() {
|
|
let pane_groups = create_test_pane_groups();
|
|
assert!(pane_groups.panes_in_group.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn clone_inner_returns_copy_of_internal_map() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_id = PaneId::Terminal(10);
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
let cloned = pane_groups.clone_inner();
|
|
|
|
assert_eq!(cloned.len(), 1);
|
|
assert!(cloned.contains_key(&client_id));
|
|
assert_eq!(cloned[&client_id], vec![pane_id]);
|
|
}
|
|
|
|
#[test]
|
|
fn get_client_pane_group_returns_empty_set_for_nonexistent_client() {
|
|
let pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 999;
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert!(result.is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn get_client_pane_group_returns_correct_panes() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_ids = vec![
|
|
PaneId::Terminal(10),
|
|
PaneId::Plugin(20),
|
|
PaneId::Terminal(30),
|
|
];
|
|
let screen_size = create_test_screen_size();
|
|
|
|
for pane_id in &pane_ids {
|
|
pane_groups.add_pane_id_to_group(*pane_id, screen_size, &client_id);
|
|
}
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert_eq!(result.len(), 3);
|
|
for pane_id in pane_ids {
|
|
assert!(result.contains(&pane_id));
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn clear_pane_group_clears_existing_group() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_ids = vec![
|
|
PaneId::Terminal(10),
|
|
PaneId::Plugin(20),
|
|
PaneId::Terminal(30),
|
|
];
|
|
let screen_size = create_test_screen_size();
|
|
|
|
for pane_id in pane_ids {
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
}
|
|
|
|
assert!(!pane_groups.get_client_pane_group(&client_id).is_empty());
|
|
|
|
pane_groups.clear_pane_group(&client_id);
|
|
|
|
assert!(pane_groups.get_client_pane_group(&client_id).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn clear_pane_group_handles_nonexistent_client() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 999;
|
|
|
|
pane_groups.clear_pane_group(&client_id);
|
|
assert!(pane_groups.get_client_pane_group(&client_id).is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_pane_id_adds_new_pane() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_id = PaneId::Terminal(10);
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.toggle_pane_id_in_group(pane_id, screen_size, &client_id);
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert!(result.contains(&pane_id));
|
|
}
|
|
|
|
#[test]
|
|
fn toggle_pane_id_removes_existing_pane() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_id = PaneId::Plugin(10);
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
assert!(pane_groups
|
|
.get_client_pane_group(&client_id)
|
|
.contains(&pane_id));
|
|
|
|
pane_groups.toggle_pane_id_in_group(pane_id, screen_size, &client_id);
|
|
assert!(!pane_groups
|
|
.get_client_pane_group(&client_id)
|
|
.contains(&pane_id));
|
|
}
|
|
|
|
#[test]
|
|
fn add_pane_id_to_group_adds_new_pane() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_id = PaneId::Terminal(10);
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert!(result.contains(&pane_id));
|
|
}
|
|
|
|
#[test]
|
|
fn add_pane_id_to_group_does_not_duplicate() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let pane_id = PaneId::Plugin(10);
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
pane_groups.add_pane_id_to_group(pane_id, screen_size, &client_id);
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert_eq!(result.len(), 1);
|
|
assert!(result.contains(&pane_id));
|
|
}
|
|
|
|
#[test]
|
|
fn group_and_ungroup_panes_adds_and_removes_correctly() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
let initial_panes = vec![PaneId::Terminal(1), PaneId::Plugin(2), PaneId::Terminal(3)];
|
|
for pane_id in &initial_panes {
|
|
pane_groups.add_pane_id_to_group(*pane_id, screen_size, &client_id);
|
|
}
|
|
|
|
let panes_to_add = vec![PaneId::Plugin(4), PaneId::Terminal(5)];
|
|
let panes_to_remove = vec![PaneId::Plugin(2), PaneId::Terminal(3)];
|
|
|
|
pane_groups.group_and_ungroup_panes(panes_to_add, panes_to_remove, screen_size, &client_id);
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
|
|
assert!(result.contains(&PaneId::Terminal(1)));
|
|
assert!(result.contains(&PaneId::Plugin(4)));
|
|
assert!(result.contains(&PaneId::Terminal(5)));
|
|
assert!(!result.contains(&PaneId::Plugin(2)));
|
|
assert!(!result.contains(&PaneId::Terminal(3)));
|
|
assert_eq!(result.len(), 3);
|
|
}
|
|
|
|
#[test]
|
|
fn override_groups_with_replaces_all_groups() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id1: ClientId = 1;
|
|
let client_id2: ClientId = 2;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(PaneId::Terminal(10), screen_size, &client_id1);
|
|
|
|
let mut new_groups = HashMap::new();
|
|
new_groups.insert(client_id2, vec![PaneId::Plugin(20), PaneId::Terminal(30)]);
|
|
|
|
pane_groups.override_groups_with(new_groups);
|
|
|
|
assert!(pane_groups.get_client_pane_group(&client_id1).is_empty());
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id2);
|
|
assert!(result.contains(&PaneId::Plugin(20)));
|
|
assert!(result.contains(&PaneId::Terminal(30)));
|
|
assert_eq!(result.len(), 2);
|
|
}
|
|
|
|
#[test]
|
|
fn multiple_clients_independent_groups() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id1: ClientId = 1;
|
|
let client_id2: ClientId = 2;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(PaneId::Terminal(10), screen_size, &client_id1);
|
|
pane_groups.add_pane_id_to_group(PaneId::Plugin(20), screen_size, &client_id2);
|
|
|
|
let group1 = pane_groups.get_client_pane_group(&client_id1);
|
|
let group2 = pane_groups.get_client_pane_group(&client_id2);
|
|
|
|
assert!(group1.contains(&PaneId::Terminal(10)));
|
|
assert!(!group1.contains(&PaneId::Plugin(20)));
|
|
|
|
assert!(group2.contains(&PaneId::Plugin(20)));
|
|
assert!(!group2.contains(&PaneId::Terminal(10)));
|
|
}
|
|
|
|
#[test]
|
|
fn pane_id_variants_work_correctly() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
let terminal_pane = PaneId::Terminal(100);
|
|
let plugin_pane = PaneId::Plugin(200);
|
|
|
|
pane_groups.add_pane_id_to_group(terminal_pane, screen_size, &client_id);
|
|
pane_groups.add_pane_id_to_group(plugin_pane, screen_size, &client_id);
|
|
|
|
let result = pane_groups.get_client_pane_group(&client_id);
|
|
assert!(result.contains(&terminal_pane));
|
|
assert!(result.contains(&plugin_pane));
|
|
assert_eq!(result.len(), 2);
|
|
|
|
let another_terminal = PaneId::Terminal(200);
|
|
assert!(!result.contains(&another_terminal));
|
|
}
|
|
|
|
#[test]
|
|
fn should_launch_plugin_returns_true_when_first_pane_added() {
|
|
let pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let previous_groups = HashMap::new();
|
|
|
|
assert!(pane_groups.should_launch_plugin(&previous_groups, &client_id));
|
|
}
|
|
|
|
#[test]
|
|
fn should_launch_plugin_returns_true_when_empty_to_non_empty() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
let mut previous_groups = HashMap::new();
|
|
previous_groups.insert(client_id, vec![]);
|
|
|
|
pane_groups.add_pane_id_to_group(PaneId::Terminal(10), screen_size, &client_id);
|
|
|
|
assert!(pane_groups.should_launch_plugin(&previous_groups, &client_id));
|
|
}
|
|
|
|
#[test]
|
|
fn should_launch_plugin_returns_false_when_non_empty_to_non_empty() {
|
|
let mut pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
let screen_size = create_test_screen_size();
|
|
|
|
pane_groups.add_pane_id_to_group(PaneId::Terminal(10), screen_size, &client_id);
|
|
let previous_groups = pane_groups.clone_inner();
|
|
|
|
pane_groups.add_pane_id_to_group(PaneId::Plugin(20), screen_size, &client_id);
|
|
|
|
assert!(!pane_groups.should_launch_plugin(&previous_groups, &client_id));
|
|
}
|
|
|
|
#[test]
|
|
fn should_launch_plugin_returns_false_when_non_empty_to_empty() {
|
|
let pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
|
|
let mut previous_groups = HashMap::new();
|
|
previous_groups.insert(client_id, vec![PaneId::Terminal(10)]);
|
|
|
|
assert!(!pane_groups.should_launch_plugin(&previous_groups, &client_id));
|
|
}
|
|
|
|
#[test]
|
|
fn should_launch_plugin_returns_false_when_empty_to_empty() {
|
|
let pane_groups = create_test_pane_groups();
|
|
let client_id: ClientId = 1;
|
|
|
|
let mut previous_groups = HashMap::new();
|
|
previous_groups.insert(client_id, vec![]);
|
|
|
|
assert!(!pane_groups.should_launch_plugin(&previous_groups, &client_id));
|
|
}
|
|
}
|