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
This commit is contained in:
Aram Drevekenin 2025-04-29 20:52:17 +02:00 committed by GitHub
parent 905892439f
commit 27c8986939
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
111 changed files with 7853 additions and 958 deletions

8
Cargo.lock generated
View file

@ -2231,6 +2231,14 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
[[package]]
name = "multiple-select"
version = "0.1.0"
dependencies = [
"fuzzy-matcher",
"zellij-tile",
]
[[package]] [[package]]
name = "names" name = "names"
version = "0.14.0" version = "0.14.0"

View file

@ -47,6 +47,7 @@ members = [
"default-plugins/configuration", "default-plugins/configuration",
"default-plugins/plugin-manager", "default-plugins/plugin-manager",
"default-plugins/about", "default-plugins/about",
"default-plugins/multiple-select",
"zellij-client", "zellij-client",
"zellij-server", "zellij-server",
"zellij-utils", "zellij-utils",

View file

@ -2,6 +2,7 @@ use ansi_term::ANSIStrings;
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use crate::{LinePart, ARROW_SEPARATOR}; use crate::{LinePart, ARROW_SEPARATOR};
use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use zellij_tile_utils::style; use zellij_tile_utils::style;
@ -251,6 +252,8 @@ pub fn tab_line(
mode: InputMode, mode: InputMode,
active_swap_layout_name: &Option<String>, active_swap_layout_name: &Option<String>,
is_swap_layout_dirty: bool, is_swap_layout_dirty: bool,
mode_info: &ModeInfo,
grouped_pane_count: Option<usize>,
) -> Vec<LinePart> { ) -> Vec<LinePart> {
let mut tabs_after_active = all_tabs.split_off(active_tab_index); let mut tabs_after_active = all_tabs.split_off(active_tab_index);
let mut tabs_before_active = all_tabs; let mut tabs_before_active = all_tabs;
@ -286,15 +289,21 @@ pub fn tab_line(
if current_title_len < cols { if current_title_len < cols {
let mut remaining_space = cols - current_title_len; let mut remaining_space = cols - current_title_len;
let remaining_bg = palette.text_unselected.background; let remaining_bg = palette.text_unselected.background;
if let Some(swap_layout_status) = swap_layout_status( 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, remaining_space,
active_swap_layout_name, active_swap_layout_name,
is_swap_layout_dirty, is_swap_layout_dirty,
mode, mode,
&palette, &palette,
tab_separator(capabilities), tab_separator(capabilities),
) { ),
remaining_space -= swap_layout_status.len; };
if let Some(right_side_component) = right_side_component {
remaining_space -= right_side_component.len;
let mut buffer = String::new(); let mut buffer = String::new();
for _ in 0..remaining_space { for _ in 0..remaining_space {
buffer.push_str(&style!(remaining_bg, remaining_bg).paint(" ").to_string()); buffer.push_str(&style!(remaining_bg, remaining_bg).paint(" ").to_string());
@ -304,7 +313,7 @@ pub fn tab_line(
len: remaining_space, len: remaining_space,
tab_index: None, tab_index: None,
}); });
prefix.push(swap_layout_status); prefix.push(right_side_component);
} }
} }
@ -373,3 +382,274 @@ fn swap_layout_status(
None => None, 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()
}

View file

@ -26,6 +26,8 @@ struct State {
tab_line: Vec<LinePart>, tab_line: Vec<LinePart>,
text_copy_destination: Option<CopyDestination>, text_copy_destination: Option<CopyDestination>,
display_system_clipboard_failure: bool, display_system_clipboard_failure: bool,
own_client_id: Option<ClientId>,
grouped_panes_count: Option<usize>,
} }
static ARROW_SEPARATOR: &str = ""; static ARROW_SEPARATOR: &str = "";
@ -42,7 +44,9 @@ impl ZellijPlugin for State {
EventType::CopyToClipboard, EventType::CopyToClipboard,
EventType::InputReceived, EventType::InputReceived,
EventType::SystemClipboardFailure, EventType::SystemClipboardFailure,
EventType::PaneUpdate,
]); ]);
self.own_client_id = Some(get_plugin_ids().client_id);
} }
fn update(&mut self, event: Event) -> bool { fn update(&mut self, event: Event) -> bool {
@ -108,6 +112,28 @@ impl ZellijPlugin for State {
self.text_copy_destination = None; self.text_copy_destination = None;
self.display_system_clipboard_failure = false; 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); eprintln!("Got unrecognized event: {:?}", event);
}, },
@ -181,6 +207,8 @@ impl ZellijPlugin for State {
self.mode_info.mode, self.mode_info.mode,
&active_swap_layout_name, &active_swap_layout_name,
is_swap_layout_dirty, is_swap_layout_dirty,
&self.mode_info,
self.grouped_panes_count,
); );
let output = self let output = self
.tab_line .tab_line

View file

@ -168,6 +168,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }} bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }} bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }} 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" {{ shared_except "locked" "renametab" "renamepane" {{
bind "Enter" {{ SwitchToMode "Locked"; }} bind "Enter" {{ SwitchToMode "Locked"; }}
@ -392,6 +400,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }} bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }} bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }} 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" {{ shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }} bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}
@ -595,6 +611,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }} bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }} bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }} 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" {{ shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }} bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}
@ -1158,6 +1182,14 @@ keybinds clear-defaults=true {{
bind "{secondary_modifier} -" {{ Resize "Decrease"; }} bind "{secondary_modifier} -" {{ Resize "Decrease"; }}
bind "{secondary_modifier} [" {{ PreviousSwapLayout; }} bind "{secondary_modifier} [" {{ PreviousSwapLayout; }}
bind "{secondary_modifier} ]" {{ NextSwapLayout; }} 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" {{ shared_except "normal" "locked" {{
bind "Enter" "Esc" {{ SwitchToMode "Normal"; }} bind "Enter" "Esc" {{ SwitchToMode "Normal"; }}

View file

@ -67,6 +67,7 @@ impl ZellijPlugin for State {
EventType::FileSystemCreate, EventType::FileSystemCreate,
EventType::FileSystemUpdate, EventType::FileSystemUpdate,
EventType::FileSystemDelete, EventType::FileSystemDelete,
EventType::BeforeClose,
]); ]);
watch_filesystem(); watch_filesystem();
} }
@ -535,6 +536,10 @@ impl ZellijPlugin for State {
self.received_payload = Some(payload.clone()); 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 => { Event::SystemClipboardFailure => {
// this is just to trigger the worker message // this is just to trigger the worker message
post_message_to(PluginMessage { post_message_to(PluginMessage {

View file

@ -0,0 +1,2 @@
[build]
target = "wasm32-wasip1"

View file

@ -0,0 +1 @@
/target

View file

@ -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"

View file

@ -0,0 +1 @@
../../LICENSE.md

View file

@ -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);
}
}

View file

@ -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();
}
}

View file

@ -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,
);
}
}

View file

@ -39,6 +39,8 @@ struct State {
display_system_clipboard_failure: bool, display_system_clipboard_failure: bool,
classic_ui: bool, classic_ui: bool,
base_mode_is_locked: bool, base_mode_is_locked: bool,
own_client_id: Option<ClientId>,
grouped_panes_count: Option<usize>,
} }
register_plugin!(State); register_plugin!(State);
@ -197,10 +199,12 @@ impl ZellijPlugin for State {
.get("classic") .get("classic")
.map(|c| c == "true") .map(|c| c == "true")
.unwrap_or(false); .unwrap_or(false);
self.own_client_id = Some(get_plugin_ids().client_id);
set_selectable(false); set_selectable(false);
subscribe(&[ subscribe(&[
EventType::ModeUpdate, EventType::ModeUpdate,
EventType::TabUpdate, EventType::TabUpdate,
EventType::PaneUpdate,
EventType::CopyToClipboard, EventType::CopyToClipboard,
EventType::InputReceived, EventType::InputReceived,
EventType::SystemClipboardFailure, EventType::SystemClipboardFailure,
@ -223,6 +227,28 @@ impl ZellijPlugin for State {
} }
self.tabs = tabs; 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) => { Event::CopyToClipboard(copy_destination) => {
match self.text_copy_destination { match self.text_copy_destination {
Some(text_copy_destination) => { Some(text_copy_destination) => {
@ -280,6 +306,7 @@ impl ZellijPlugin for State {
self.base_mode_is_locked, self.base_mode_is_locked,
self.text_copy_destination, self.text_copy_destination,
self.display_system_clipboard_failure, self.display_system_clipboard_failure,
self.grouped_panes_count,
), ),
fill_bg, fill_bg,
); );

View file

@ -6,7 +6,7 @@ use ansi_term::{
use std::collections::HashMap; use std::collections::HashMap;
use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::actions::Action;
use zellij_tile::prelude::*; 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::first_line::{to_char, KeyAction, KeyMode, KeyShortcut};
use crate::second_line::{system_clipboard_error, text_copied_hint}; use crate::second_line::{system_clipboard_error, text_copied_hint};
@ -22,6 +22,7 @@ pub fn one_line_ui(
base_mode_is_locked: bool, base_mode_is_locked: bool,
text_copied_to_clipboard_destination: Option<CopyDestination>, text_copied_to_clipboard_destination: Option<CopyDestination>,
clipboard_failure: bool, clipboard_failure: bool,
grouped_pane_count: Option<usize>,
) -> LinePart { ) -> LinePart {
if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination { if let Some(text_copied_to_clipboard_destination) = text_copied_to_clipboard_destination {
return text_copied_hint(text_copied_to_clipboard_destination); return text_copied_hint(text_copied_to_clipboard_destination);
@ -35,10 +36,17 @@ pub fn one_line_ui(
*max_len = max_len.saturating_sub(line_part.len); *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) render_mode_key_indicators(help, max_len, separator, base_mode_is_locked)
.map(|mode_key_indicators| append(&mode_key_indicators, &mut max_len)) .map(|mode_key_indicators| append(&mode_key_indicators, &mut max_len))
.and_then(|_| match help.mode { .and_then(|_| match help.mode {
InputMode::Normal | InputMode::Locked => render_secondary_info(help, tab_info, 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)), .map(|secondary_info| append(&secondary_info, &mut max_len)),
_ => add_keygroup_separator(help, max_len) _ => add_keygroup_separator(help, max_len)
.map(|key_group_separator| append(&key_group_separator, &mut max_len)) .map(|key_group_separator| append(&key_group_separator, &mut max_len))
@ -656,6 +664,225 @@ fn render_common_modifiers(
line_part_to_render.len += prefix_text.chars().count() + separator.chars().count(); 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( fn render_secondary_info(
help: &ModeInfo, help: &ModeInfo,
tab_info: Option<&TabInfo>, tab_info: Option<&TabInfo>,
@ -1168,6 +1395,7 @@ fn add_keygroup_separator(help: &ModeInfo, max_len: usize) -> Option<LinePart> {
bits.push( bits.push(
Style::new() Style::new()
.fg(separator_color) .fg(separator_color)
.on(bg_color)
.bold() .bold()
.paint(format!(" {} ", mode_help_text)), .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, &[ (s("Switch Location"), s("Move"), action_key_group(&km, &[
&[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))], &[Action::MovePane(Some(Dir::Left))], &[Action::MovePane(Some(Dir::Down))],
&[Action::MovePane(Some(Dir::Up))], &[Action::MovePane(Some(Dir::Right))]])), &[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![ ]} else if mi.mode == IM::Scroll { vec![
(s("Enter search term"), s("Search"), (s("Enter search term"), s("Search"),
action_key(&km, &[A::SwitchToMode(IM::EnterSearch), A::SearchInput(vec![0])])), 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 { fn style_key_with_modifier(keyvec: &[KeyWithModifier], color_index: Option<usize>) -> LinePart {
if keyvec.is_empty() { if keyvec.is_empty() {
return LinePart::default(); return LinePart::default();

View file

@ -138,6 +138,14 @@ keybinds {
bind "Alt k" "Alt Up" { MoveFocus "Up"; } bind "Alt k" "Alt Up" { MoveFocus "Up"; }
bind "Alt =" "Alt +" { Resize "Increase"; } bind "Alt =" "Alt +" { Resize "Increase"; }
bind "Alt -" { Resize "Decrease"; } bind "Alt -" { Resize "Decrease"; }
bind "Alt m" {
LaunchOrFocusPlugin "zellij:multiple-select" {
floating true
move_to_focused_tab true
}
}
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
} }
shared_except "normal" "locked" { shared_except "normal" "locked" {
bind "Enter" "Space" "Esc" { SwitchToMode "Normal"; } bind "Enter" "Space" "Esc" { SwitchToMode "Normal"; }

View file

@ -69,6 +69,10 @@ fn workspace_members() -> &'static Vec<WorkspaceMember> {
crate_name: "default-plugins/about", crate_name: "default-plugins/about",
build: true, build: true,
}, },
WorkspaceMember {
crate_name: "default-plugins/multiple-select",
build: true,
},
WorkspaceMember { WorkspaceMember {
crate_name: "zellij-utils", crate_name: "zellij-utils",
build: false, build: false,

View file

@ -55,6 +55,7 @@ pub enum BackgroundJob {
Vec<u8>, // body Vec<u8>, // body
BTreeMap<String, String>, // context BTreeMap<String, String>, // context
), ),
HighlightPanesWithMessage(Vec<PaneId>, String),
RenderToClients, RenderToClients,
Exit, Exit,
} }
@ -76,12 +77,16 @@ impl From<&BackgroundJob> for BackgroundJobContext {
BackgroundJob::WebRequest(..) => BackgroundJobContext::WebRequest, BackgroundJob::WebRequest(..) => BackgroundJobContext::WebRequest,
BackgroundJob::ReportPluginList(..) => BackgroundJobContext::ReportPluginList, BackgroundJob::ReportPluginList(..) => BackgroundJobContext::ReportPluginList,
BackgroundJob::RenderToClients => BackgroundJobContext::ReportPluginList, BackgroundJob::RenderToClients => BackgroundJobContext::ReportPluginList,
BackgroundJob::HighlightPanesWithMessage(..) => {
BackgroundJobContext::HighlightPanesWithMessage
},
BackgroundJob::Exit => BackgroundJobContext::Exit, BackgroundJob::Exit => BackgroundJobContext::Exit,
} }
} }
} }
static FLASH_DURATION_MS: u64 = 1000; static LONG_FLASH_DURATION_MS: u64 = 1000;
static FLASH_DURATION_MS: u64 = 400; // Doherty threshold
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500; static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
static SESSION_READ_DURATION: u64 = 1000; static SESSION_READ_DURATION: u64 = 1000;
static DEFAULT_SERIALIZATION_INTERVAL: u64 = 60000; static DEFAULT_SERIALIZATION_INTERVAL: u64 = 60000;
@ -129,7 +134,7 @@ pub(crate) fn background_jobs_main(
Some(text), Some(text),
), ),
); );
task::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)).await; task::sleep(std::time::Duration::from_millis(LONG_FLASH_DURATION_MS)).await;
let _ = senders.send_to_screen( let _ = senders.send_to_screen(
ScreenInstruction::ClearPaneFrameColorOverride(pane_ids), ScreenInstruction::ClearPaneFrameColorOverride(pane_ids),
); );
@ -411,6 +416,26 @@ pub(crate) fn background_jobs_main(
}); });
} }
}, },
BackgroundJob::HighlightPanesWithMessage(pane_ids, text) => {
if job_already_running(job, &mut running_jobs) {
continue;
}
task::spawn({
let senders = bus.senders.clone();
async move {
let _ = senders.send_to_screen(
ScreenInstruction::AddHighlightPaneFrameColorOverride(
pane_ids.clone(),
Some(text),
),
);
task::sleep(std::time::Duration::from_millis(FLASH_DURATION_MS)).await;
let _ = senders.send_to_screen(
ScreenInstruction::ClearPaneFrameColorOverride(pane_ids),
);
}
});
},
BackgroundJob::Exit => { BackgroundJob::Exit => {
for loading_plugin in loading_plugins.values() { for loading_plugin in loading_plugins.values() {
loading_plugin.store(false, Ordering::SeqCst); loading_plugin.store(false, Ordering::SeqCst);
@ -431,7 +456,9 @@ fn job_already_running(
) -> bool { ) -> bool {
match running_jobs.get_mut(&job) { match running_jobs.get_mut(&job) {
Some(current_running_job_start_time) => { Some(current_running_job_start_time) => {
if current_running_job_start_time.elapsed() > Duration::from_millis(FLASH_DURATION_MS) { if current_running_job_start_time.elapsed()
> Duration::from_millis(LONG_FLASH_DURATION_MS)
{
*current_running_job_start_time = Instant::now(); *current_running_job_start_time = Instant::now();
false false
} else { } else {

View file

@ -369,6 +369,10 @@ impl SessionMetaData {
hide_session_name: new_config.ui.pane_frames.hide_session_name, hide_session_name: new_config.ui.pane_frames.hide_session_name,
stacked_resize: new_config.options.stacked_resize.unwrap_or(true), stacked_resize: new_config.options.stacked_resize.unwrap_or(true),
default_editor: new_config.options.scrollback_editor.clone(), default_editor: new_config.options.scrollback_editor.clone(),
advanced_mouse_actions: new_config
.options
.advanced_mouse_actions
.unwrap_or(true),
}) })
.unwrap(); .unwrap();
self.senders self.senders

View file

@ -343,7 +343,12 @@ impl FloatingPanes {
} }
Ok(()) Ok(())
} }
pub fn render(&mut self, output: &mut Output) -> Result<()> { pub fn render(
&mut self,
output: &mut Output,
mouse_hover_pane_id: &HashMap<ClientId, PaneId>,
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
) -> Result<()> {
let err_context = || "failed to render output"; let err_context = || "failed to render output";
let connected_clients: Vec<ClientId> = let connected_clients: Vec<ClientId> =
{ self.connected_clients.borrow().iter().copied().collect() }; { self.connected_clients.borrow().iter().copied().collect() };
@ -393,6 +398,8 @@ impl FloatingPanes {
false, false,
false, false,
true, true,
mouse_hover_pane_id,
current_pane_group.clone(),
); );
for client_id in &connected_clients { for client_id in &connected_clients {
let client_mode = self let client_mode = self
@ -1095,10 +1102,10 @@ impl FloatingPanes {
Err(anyhow!("Pane not found")) Err(anyhow!("Pane not found"))
} }
} }
pub fn pane_info(&self) -> Vec<PaneInfo> { pub fn pane_info(&self, current_pane_group: &HashMap<ClientId, Vec<PaneId>>) -> Vec<PaneInfo> {
let mut pane_infos = vec![]; let mut pane_infos = vec![];
for (pane_id, pane) in self.panes.iter() { for (pane_id, pane) in self.panes.iter() {
let mut pane_info_for_pane = pane_info_for_pane(pane_id, pane); let mut pane_info_for_pane = pane_info_for_pane(pane_id, pane, current_pane_group);
let is_focused = self.active_panes.pane_id_is_focused(pane_id); let is_focused = self.active_panes.pane_id_is_focused(pane_id);
pane_info_for_pane.is_floating = true; pane_info_for_pane.is_floating = true;
pane_info_for_pane.is_suppressed = false; pane_info_for_pane.is_suppressed = false;
@ -1139,4 +1146,48 @@ impl FloatingPanes {
pane.update_rounded_corners(rounded_corners); pane.update_rounded_corners(rounded_corners);
} }
} }
pub fn next_selectable_pane_id_above(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.next_selectable_pane_id_above(&pane_id)
}
pub fn next_selectable_pane_id_below(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.next_selectable_pane_id_below(&pane_id)
}
pub fn next_selectable_pane_id_to_the_left(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.next_selectable_pane_id_to_the_left(&pane_id)
}
pub fn next_selectable_pane_id_to_the_right(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let display_area = *self.display_area.borrow();
let viewport = *self.viewport.borrow();
let floating_pane_grid = FloatingPaneGrid::new(
&mut self.panes,
&mut self.desired_pane_positions,
display_area,
viewport,
);
floating_pane_grid.next_selectable_pane_id_to_the_right(&pane_id)
}
} }

View file

@ -646,7 +646,16 @@ impl Pane for PluginPane {
fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) { fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) {
self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text)); self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text));
} }
fn clear_pane_frame_color_override(&mut self) { fn add_highlight_pane_frame_color_override(
&mut self,
text: Option<String>,
_client_id: Option<ClientId>,
) {
// TODO: if we have a client_id, we should only highlight the frame for this client
self.pane_frame_color_override = Some((self.style.colors.frame_highlight.base, text));
}
fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) {
// TODO: if we have a client_id, we should only clear the highlight for this client
self.pane_frame_color_override = None; self.pane_frame_color_override = None;
} }
fn frame_color_override(&self) -> Option<PaletteColor> { fn frame_color_override(&self) -> Option<PaletteColor> {

View file

@ -742,7 +742,16 @@ impl Pane for TerminalPane {
fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) { fn add_red_pane_frame_color_override(&mut self, error_text: Option<String>) {
self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text)); self.pane_frame_color_override = Some((self.style.colors.exit_code_error.base, error_text));
} }
fn clear_pane_frame_color_override(&mut self) { fn add_highlight_pane_frame_color_override(
&mut self,
text: Option<String>,
_client_id: Option<ClientId>,
) {
// TODO: if we have a client_id, we should only highlight the frame for this client
self.pane_frame_color_override = Some((self.style.colors.frame_highlight.base, text));
}
fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) {
// TODO: if we have a client_id, we should only clear the highlight for this client
self.pane_frame_color_override = None; self.pane_frame_color_override = None;
} }
fn frame_color_override(&self) -> Option<PaletteColor> { fn frame_color_override(&self) -> Option<PaletteColor> {

View file

@ -200,6 +200,15 @@ impl TiledPanes {
.is_some(); .is_some();
has_room_for_new_pane || pane_grid.has_room_for_new_stacked_pane() || self.panes.is_empty() has_room_for_new_pane || pane_grid.has_room_for_new_stacked_pane() || self.panes.is_empty()
} }
pub fn room_left_in_stack_of_pane_id(&mut self, pane_id: &PaneId) -> Option<usize> {
let mut pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
pane_grid.room_left_in_stack_of_pane_id(pane_id)
}
pub fn assign_geom_for_pane_with_run(&mut self, run: Option<Run>) { pub fn assign_geom_for_pane_with_run(&mut self, run: Option<Run>) {
// here we're removing the first pane we find with this run instruction and re-adding it so // here we're removing the first pane we find with this run instruction and re-adding it so
@ -226,6 +235,26 @@ impl TiledPanes {
} }
} }
} }
pub fn add_pane_to_stack(&mut self, pane_id_in_stack: &PaneId, mut pane: Box<dyn Pane>) {
let mut pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
match pane_grid.make_room_in_stack_of_pane_id_for_pane(pane_id_in_stack) {
Ok(new_pane_geom) => {
pane.set_geom(new_pane_geom);
self.panes.insert(pane.pid(), pane);
self.set_force_render(); // TODO: why do we need this?
return;
},
Err(_e) => {
let should_relayout = false;
return self.add_pane_without_stacked_resize(pane.pid(), pane, should_relayout);
},
}
}
fn add_pane( fn add_pane(
&mut self, &mut self,
pane_id: PaneId, pane_id: PaneId,
@ -658,6 +687,18 @@ impl TiledPanes {
self.set_force_render(); self.set_force_render();
self.reapply_pane_frames(); self.reapply_pane_frames();
} }
pub fn pane_ids_in_stack_of_pane_id(&mut self, pane_id: &PaneId) -> Vec<PaneId> {
if let Some(stack_id) = self
.panes
.get(pane_id)
.and_then(|p| p.position_and_size().stacked)
{
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.pane_ids_in_stack(stack_id)
} else {
vec![]
}
}
pub fn focus_pane_for_all_clients_in_stack(&mut self, pane_id: PaneId, stack_id: usize) { pub fn focus_pane_for_all_clients_in_stack(&mut self, pane_id: PaneId, stack_id: usize) {
let connected_clients: Vec<ClientId> = let connected_clients: Vec<ClientId> =
self.connected_clients.borrow().iter().copied().collect(); self.connected_clients.borrow().iter().copied().collect();
@ -880,7 +921,13 @@ impl TiledPanes {
pub fn has_panes(&self) -> bool { pub fn has_panes(&self) -> bool {
!self.panes.is_empty() !self.panes.is_empty()
} }
pub fn render(&mut self, output: &mut Output, floating_panes_are_visible: bool) -> Result<()> { pub fn render(
&mut self,
output: &mut Output,
floating_panes_are_visible: bool,
mouse_hover_pane_id: &HashMap<ClientId, PaneId>,
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
) -> Result<()> {
let err_context = || "failed to render tiled panes"; let err_context = || "failed to render tiled panes";
let connected_clients: Vec<ClientId> = let connected_clients: Vec<ClientId> =
@ -930,6 +977,8 @@ impl TiledPanes {
pane_is_stacked_under, pane_is_stacked_under,
pane_is_stacked_over, pane_is_stacked_over,
should_draw_pane_frames, should_draw_pane_frames,
&mouse_hover_pane_id,
current_pane_group.clone(),
); );
for client_id in &connected_clients { for client_id in &connected_clients {
let client_mode = self let client_mode = self
@ -1716,7 +1765,7 @@ impl TiledPanes {
*self.viewport.borrow(), *self.viewport.borrow(),
); );
let next_index = pane_grid let next_index = pane_grid
.next_selectable_pane_id_below(&active_pane_id) .next_selectable_pane_id_below(&active_pane_id, false)
.or_else(|| pane_grid.progress_stack_down_if_in_stack(&active_pane_id)); .or_else(|| pane_grid.progress_stack_down_if_in_stack(&active_pane_id));
match next_index { match next_index {
Some(p) => { Some(p) => {
@ -1767,7 +1816,7 @@ impl TiledPanes {
*self.viewport.borrow(), *self.viewport.borrow(),
); );
let next_index = pane_grid let next_index = pane_grid
.next_selectable_pane_id_above(&active_pane_id) .next_selectable_pane_id_above(&active_pane_id, false)
.or_else(|| pane_grid.progress_stack_up_if_in_stack(&active_pane_id)); .or_else(|| pane_grid.progress_stack_up_if_in_stack(&active_pane_id));
match next_index { match next_index {
Some(p) => { Some(p) => {
@ -1972,7 +2021,7 @@ impl TiledPanes {
*self.viewport.borrow(), *self.viewport.borrow(),
); );
let next_index = pane_grid let next_index = pane_grid
.next_selectable_pane_id_below(&pane_id) .next_selectable_pane_id_below(&pane_id, false)
.or_else(|| pane_grid.progress_stack_down_if_in_stack(&pane_id)); .or_else(|| pane_grid.progress_stack_down_if_in_stack(&pane_id));
if let Some(p) = next_index { if let Some(p) = next_index {
let current_position = self.panes.get(&pane_id).unwrap(); let current_position = self.panes.get(&pane_id).unwrap();
@ -2127,7 +2176,7 @@ impl TiledPanes {
*self.viewport.borrow(), *self.viewport.borrow(),
); );
let next_index = pane_grid let next_index = pane_grid
.next_selectable_pane_id_above(&pane_id) .next_selectable_pane_id_above(&pane_id, false)
.or_else(|| pane_grid.progress_stack_up_if_in_stack(&pane_id)); .or_else(|| pane_grid.progress_stack_up_if_in_stack(&pane_id));
if let Some(p) = next_index { if let Some(p) = next_index {
let current_position = self.panes.get(&pane_id).unwrap(); let current_position = self.panes.get(&pane_id).unwrap();
@ -2443,10 +2492,10 @@ impl TiledPanes {
.find(|(_id, pane)| run_plugin_or_alias.is_equivalent_to_run(pane.invoked_with())) .find(|(_id, pane)| run_plugin_or_alias.is_equivalent_to_run(pane.invoked_with()))
.map(|(id, _)| *id) .map(|(id, _)| *id)
} }
pub fn pane_info(&self) -> Vec<PaneInfo> { pub fn pane_info(&self, current_pane_group: &HashMap<ClientId, Vec<PaneId>>) -> Vec<PaneInfo> {
let mut pane_infos = vec![]; let mut pane_infos = vec![];
for (pane_id, pane) in self.panes.iter() { for (pane_id, pane) in self.panes.iter() {
let mut pane_info_for_pane = pane_info_for_pane(pane_id, pane); let mut pane_info_for_pane = pane_info_for_pane(pane_id, pane, &current_pane_group);
let is_focused = self.active_panes.pane_id_is_focused(pane_id); let is_focused = self.active_panes.pane_id_is_focused(pane_id);
pane_info_for_pane.is_floating = false; pane_info_for_pane.is_floating = false;
pane_info_for_pane.is_suppressed = false; pane_info_for_pane.is_suppressed = false;
@ -2495,6 +2544,44 @@ impl TiledPanes {
StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide) StackedPanes::new_from_btreemap(&mut self.panes, &self.panes_to_hide)
.stacked_pane_ids_under_and_over_flexible_panes() .stacked_pane_ids_under_and_over_flexible_panes()
} }
pub fn next_selectable_pane_id_above(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
let include_panes_in_stack = true;
pane_grid.next_selectable_pane_id_above(&pane_id, include_panes_in_stack)
}
pub fn next_selectable_pane_id_below(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
let include_panes_in_stack = true;
pane_grid.next_selectable_pane_id_below(&pane_id, include_panes_in_stack)
}
pub fn next_selectable_pane_id_to_the_left(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
pane_grid.next_selectable_pane_id_to_the_left(&pane_id)
}
pub fn next_selectable_pane_id_to_the_right(&mut self, pane_id: &PaneId) -> Option<PaneId> {
let pane_grid = TiledPaneGrid::new(
&mut self.panes,
&self.panes_to_hide,
*self.display_area.borrow(),
*self.viewport.borrow(),
);
pane_grid.next_selectable_pane_id_to_the_right(&pane_id)
}
} }
#[allow(clippy::borrowed_box)] #[allow(clippy::borrowed_box)]

View file

@ -500,6 +500,20 @@ impl<'a> StackedPanes<'a> {
} }
Err(anyhow!("Not enough room for another pane!")) Err(anyhow!("Not enough room for another pane!"))
} }
pub fn room_left_in_stack_of_pane_id(&self, pane_id: &PaneId) -> Option<usize> {
// if the pane is stacked, returns the number of panes possible to add to this stack
let Ok(stack) = self.positions_in_stack(pane_id) else {
return None;
};
stack.iter().find_map(|(_p_id, p)| {
if !p.rows.is_fixed() {
// this is the flexible pane
Some(p.rows.as_usize().saturating_sub(MIN_TERMINAL_HEIGHT))
} else {
None
}
})
}
pub fn new_stack(&self, root_pane_id: PaneId, pane_count_in_stack: usize) -> Vec<PaneGeom> { pub fn new_stack(&self, root_pane_id: PaneId, pane_count_in_stack: usize) -> Vec<PaneGeom> {
let mut stacked_geoms = vec![]; let mut stacked_geoms = vec![];
let panes = self.panes.borrow(); let panes = self.panes.borrow();

View file

@ -1009,7 +1009,11 @@ impl<'a> TiledPaneGrid<'a> {
None => None, None => None,
} }
} }
pub fn next_selectable_pane_id_below(&self, current_pane_id: &PaneId) -> Option<PaneId> { pub fn next_selectable_pane_id_below(
&self,
current_pane_id: &PaneId,
include_panes_in_stack: bool,
) -> Option<PaneId> {
let panes = self.panes.borrow(); let panes = self.panes.borrow();
let current_pane = panes.get(current_pane_id)?; let current_pane = panes.get(current_pane_id)?;
let panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes let panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes
@ -1021,9 +1025,14 @@ impl<'a> TiledPaneGrid<'a> {
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
if include_panes_in_stack {
c.is_directly_below(Box::as_ref(current_pane))
&& c.vertically_overlaps_with(Box::as_ref(current_pane))
} else {
c.is_directly_below(Box::as_ref(current_pane)) c.is_directly_below(Box::as_ref(current_pane))
&& c.vertically_overlaps_with(Box::as_ref(current_pane)) && c.vertically_overlaps_with(Box::as_ref(current_pane))
&& !c.current_geom().is_stacked() && !c.current_geom().is_stacked()
}
}) })
.max_by_key(|(_, (_, c))| c.active_at()) .max_by_key(|(_, (_, c))| c.active_at())
.map(|(_, (pid, _))| pid) .map(|(_, (pid, _))| pid)
@ -1074,7 +1083,11 @@ impl<'a> TiledPaneGrid<'a> {
.copied(); .copied();
next_index next_index
} }
pub fn next_selectable_pane_id_above(&self, current_pane_id: &PaneId) -> Option<PaneId> { pub fn next_selectable_pane_id_above(
&self,
current_pane_id: &PaneId,
include_panes_in_stack: bool,
) -> Option<PaneId> {
let panes = self.panes.borrow(); let panes = self.panes.borrow();
let current_pane = panes.get(current_pane_id)?; let current_pane = panes.get(current_pane_id)?;
let panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes let panes: Vec<(PaneId, &&mut Box<dyn Pane>)> = panes
@ -1086,9 +1099,14 @@ impl<'a> TiledPaneGrid<'a> {
.iter() .iter()
.enumerate() .enumerate()
.filter(|(_, (_, c))| { .filter(|(_, (_, c))| {
if include_panes_in_stack {
c.is_directly_above(Box::as_ref(current_pane))
&& c.vertically_overlaps_with(Box::as_ref(current_pane))
} else {
c.is_directly_above(Box::as_ref(current_pane)) c.is_directly_above(Box::as_ref(current_pane))
&& c.vertically_overlaps_with(Box::as_ref(current_pane)) && c.vertically_overlaps_with(Box::as_ref(current_pane))
&& !c.current_geom().is_stacked() && !c.current_geom().is_stacked()
}
}) })
.max_by_key(|(_, (_, c))| c.active_at()) .max_by_key(|(_, (_, c))| c.active_at())
.map(|(_, (pid, _))| pid) .map(|(_, (pid, _))| pid)
@ -1413,6 +1431,9 @@ impl<'a> TiledPaneGrid<'a> {
.iter() .iter()
.any(|(_p_id, p)| p.current_geom().rows.as_usize() > MIN_TERMINAL_HEIGHT) .any(|(_p_id, p)| p.current_geom().rows.as_usize() > MIN_TERMINAL_HEIGHT)
} }
pub fn room_left_in_stack_of_pane_id(&mut self, pane_id: &PaneId) -> Option<usize> {
StackedPanes::new(self.panes.clone()).room_left_in_stack_of_pane_id(pane_id)
}
pub fn make_room_in_stack_for_pane(&mut self) -> Result<PaneGeom> { pub fn make_room_in_stack_for_pane(&mut self) -> Result<PaneGeom> {
StackedPanes::new(self.panes.clone()).make_room_for_new_pane() StackedPanes::new(self.panes.clone()).make_room_for_new_pane()
} }

View file

@ -1218,7 +1218,7 @@ impl Pane for MockPane {
fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>) { fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>) {
unimplemented!() unimplemented!()
} }
fn clear_pane_frame_color_override(&mut self) { fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>) {
unimplemented!() unimplemented!()
} }
fn frame_color_override(&self) -> Option<PaletteColor> { fn frame_color_override(&self) -> Option<PaletteColor> {

View file

@ -50,18 +50,21 @@ impl PluginMap {
pub fn remove_plugins( pub fn remove_plugins(
&mut self, &mut self,
pid: PluginId, pid: PluginId,
) -> Vec<( ) -> HashMap<
(PluginId, ClientId),
(
Arc<Mutex<RunningPlugin>>, Arc<Mutex<RunningPlugin>>,
Arc<Mutex<Subscriptions>>, Arc<Mutex<Subscriptions>>,
HashMap<String, Sender<MessageToWorker>>, HashMap<String, Sender<MessageToWorker>>,
)> { ),
let mut removed = vec![]; > {
let mut removed = HashMap::new();
let ids_in_plugin_map: Vec<(PluginId, ClientId)> = let ids_in_plugin_map: Vec<(PluginId, ClientId)> =
self.plugin_assets.keys().copied().collect(); self.plugin_assets.keys().copied().collect();
for (plugin_id, client_id) in ids_in_plugin_map { for (plugin_id, client_id) in ids_in_plugin_map {
if pid == plugin_id { if pid == plugin_id {
if let Some(plugin_asset) = self.plugin_assets.remove(&(plugin_id, client_id)) { if let Some(plugin_asset) = self.plugin_assets.remove(&(plugin_id, client_id)) {
removed.push(plugin_asset); removed.insert((plugin_id, client_id), plugin_asset);
} }
} }
} }

View file

@ -8622,3 +8622,74 @@ pub fn list_clients_plugin_command() {
.unwrap(); .unwrap();
assert_snapshot!(format!("{:#?}", list_clients_instruction)); assert_snapshot!(format!("{:#?}", list_clients_instruction));
} }
#[test]
#[ignore]
pub fn before_close_plugin_event() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, screen_receiver, teardown) =
create_plugin_thread(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPluginOrAlias::RunPlugin(RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
..Default::default()
});
let tab_index = 1;
let client_id = 1;
let size = Size {
cols: 121,
rows: 20,
};
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let screen_thread = grant_permissions_and_log_actions_in_thread!(
received_screen_instructions,
ScreenInstruction::HighlightAndUnhighlightPanes,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
Some(tab_index),
None,
client_id,
size,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(5000));
// here we send an unload to plugin id 0 (the first plugin id, presumably this plugin)
// so that its BeforeClose Event will be triggered and it will send a
// HighlightAndUnhighlightPanes
// instruction which we can assert below
let _ = plugin_thread_sender.send(PluginInstruction::Unload(0));
screen_thread.join().unwrap(); // this might take a while if the cache is cold
teardown();
let sent_instruction = received_screen_instructions
.lock()
.unwrap()
.iter()
.find_map(|i| {
if let ScreenInstruction::HighlightAndUnhighlightPanes(..) = i {
Some(i.clone())
} else {
None
}
})
.unwrap();
assert_snapshot!(format!("{:#?}", sent_instruction));
}

View file

@ -0,0 +1,17 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
expression: "format!(\"{:#?}\", sent_instruction)"
---
HighlightAndUnhighlightPanes(
[
Terminal(
1,
),
],
[
Plugin(
1,
),
],
1,
)

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 1143
expression: "format!(\"{:#?}\", switch_to_mode_event)" expression: "format!(\"{:#?}\", switch_to_mode_event)"
--- ---
Some( Some(
@ -222,10 +221,10 @@ Some(
154, 154,
), ),
emphasis_0: EightBit( emphasis_0: EightBit(
154, 201,
), ),
emphasis_1: EightBit( emphasis_1: EightBit(
154, 99,
), ),
emphasis_2: EightBit( emphasis_2: EightBit(
154, 154,
@ -318,6 +317,7 @@ Some(
), ),
editor: None, editor: None,
shell: None, shell: None,
currently_marking_pane_group: None,
}, },
1, 1,
), ),

View file

@ -318,16 +318,45 @@ impl WasmBridge {
pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> { pub fn unload_plugin(&mut self, pid: PluginId) -> Result<()> {
info!("Bye from plugin {}", &pid); info!("Bye from plugin {}", &pid);
let mut plugin_map = self.plugin_map.lock().unwrap(); let mut plugin_map = self.plugin_map.lock().unwrap();
for (running_plugin, _, workers) in plugin_map.remove_plugins(pid) { for ((plugin_id, client_id), (running_plugin, subscriptions, workers)) in
plugin_map.remove_plugins(pid)
{
for (_worker_name, worker_sender) in workers { for (_worker_name, worker_sender) in workers {
drop(worker_sender.send(MessageToWorker::Exit)); drop(worker_sender.send(MessageToWorker::Exit));
} }
let subscriptions = subscriptions.lock().unwrap();
if subscriptions.contains(&EventType::BeforeClose) {
let mut running_plugin = running_plugin.lock().unwrap();
match apply_before_close_event_to_plugin(
pid,
client_id,
&mut running_plugin,
self.senders.clone(),
) {
Ok(()) => {},
Err(e) => {
log::error!("{:?}", e);
// https://stackoverflow.com/questions/66450942/in-rust-is-there-a-way-to-make-literal-newlines-in-r-using-windows-c
let stringified_error = format!("{:?}", e).replace("\n", "\n\r");
handle_plugin_crash(plugin_id, stringified_error, self.senders.clone());
},
}
let cache_dir = running_plugin.store.data().plugin_own_data_dir.clone();
if let Err(e) = std::fs::remove_dir_all(cache_dir) {
log::error!("Failed to remove cache dir for plugin: {:?}", e);
}
} else {
// this is duplicated because of locking/unlocking order between running_plugin and
// subscriptions
let running_plugin = running_plugin.lock().unwrap(); let running_plugin = running_plugin.lock().unwrap();
let cache_dir = running_plugin.store.data().plugin_own_data_dir.clone(); let cache_dir = running_plugin.store.data().plugin_own_data_dir.clone();
if let Err(e) = std::fs::remove_dir_all(cache_dir) { if let Err(e) = std::fs::remove_dir_all(cache_dir) {
log::error!("Failed to remove cache dir for plugin: {:?}", e); log::error!("Failed to remove cache dir for plugin: {:?}", e);
} }
} }
}
self.cached_plugin_map.clear(); self.cached_plugin_map.clear();
let mut pipes_to_unblock = self.pending_pipes.unload_plugin(&pid); let mut pipes_to_unblock = self.pending_pipes.unload_plugin(&pid);
for pipe_name in pipes_to_unblock.drain(..) { for pipe_name in pipes_to_unblock.drain(..) {
@ -1651,3 +1680,36 @@ pub fn handle_plugin_crash(plugin_id: PluginId, message: String, senders: Thread
loading_indication, loading_indication,
)); ));
} }
pub fn apply_before_close_event_to_plugin(
plugin_id: PluginId,
client_id: ClientId,
running_plugin: &mut RunningPlugin,
senders: ThreadSenders,
) -> Result<()> {
let instance = &running_plugin.instance;
let err_context = || format!("Failed to apply event to plugin {plugin_id}");
let event = Event::BeforeClose;
let protobuf_event: ProtobufEvent = event
.clone()
.try_into()
.map_err(|e| anyhow!("Failed to convert to protobuf: {:?}", e))?;
let update = instance
.get_typed_func::<(), i32>(&mut running_plugin.store, "update")
.with_context(err_context)?;
wasi_write_object(running_plugin.store.data(), &protobuf_event.encode_to_vec())
.with_context(err_context)?;
let _should_render = update
.call(&mut running_plugin.store, ())
.with_context(err_context)?;
let pipes_to_block_or_unblock = pipes_to_block_or_unblock(running_plugin, None);
let plugin_render_asset =
PluginRenderAsset::new(plugin_id, client_id, vec![]).with_pipes(pipes_to_block_or_unblock);
let _ = senders
.send_to_plugin(PluginInstruction::UnblockCliPipes(vec![
plugin_render_asset,
]))
.context("failed to unblock input pipe");
Ok(())
}

View file

@ -431,6 +431,30 @@ fn host_run_plugin_command(caller: Caller<'_, PluginEnv>) {
close_plugin_after_replace, close_plugin_after_replace,
context, context,
), ),
PluginCommand::GroupAndUngroupPanes(panes_to_group, panes_to_ungroup) => {
group_and_ungroup_panes(
env,
panes_to_group.into_iter().map(|p| p.into()).collect(),
panes_to_ungroup.into_iter().map(|p| p.into()).collect(),
)
},
PluginCommand::HighlightAndUnhighlightPanes(
panes_to_highlight,
panes_to_unhighlight,
) => highlight_and_unhighlight_panes(
env,
panes_to_highlight.into_iter().map(|p| p.into()).collect(),
panes_to_unhighlight.into_iter().map(|p| p.into()).collect(),
),
PluginCommand::CloseMultiplePanes(pane_ids) => {
close_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect())
},
PluginCommand::FloatMultiplePanes(pane_ids) => {
float_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect())
},
PluginCommand::EmbedMultiplePanes(pane_ids) => {
embed_multiple_panes(env, pane_ids.into_iter().map(|p| p.into()).collect())
},
}, },
(PermissionStatus::Denied, permission) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -547,6 +571,7 @@ fn get_plugin_ids(env: &PluginEnv) {
plugin_id: env.plugin_id, plugin_id: env.plugin_id,
zellij_pid: process::id(), zellij_pid: process::id(),
initial_cwd: env.plugin_cwd.clone(), initial_cwd: env.plugin_cwd.clone(),
client_id: env.client_id,
}; };
ProtobufPluginIds::try_from(ids) ProtobufPluginIds::try_from(ids)
.map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e)) .map_err(|e| anyhow!("Failed to serialized plugin ids: {}", e))
@ -1864,7 +1889,7 @@ fn set_floating_pane_pinned(env: &PluginEnv, pane_id: PaneId, should_be_pinned:
fn stack_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) { fn stack_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
let _ = env let _ = env
.senders .senders
.send_to_screen(ScreenInstruction::StackPanes(pane_ids)); .send_to_screen(ScreenInstruction::StackPanes(pane_ids, env.client_id));
} }
fn change_floating_panes_coordinates( fn change_floating_panes_coordinates(
@ -2147,6 +2172,65 @@ fn load_new_plugin(
} }
} }
fn group_and_ungroup_panes(
env: &PluginEnv,
panes_to_group: Vec<PaneId>,
panes_to_ungroup: Vec<PaneId>,
) {
let _ = env
.senders
.send_to_screen(ScreenInstruction::GroupAndUngroupPanes(
panes_to_group,
panes_to_ungroup,
env.client_id,
));
}
fn highlight_and_unhighlight_panes(
env: &PluginEnv,
panes_to_highlight: Vec<PaneId>,
panes_to_unhighlight: Vec<PaneId>,
) {
let _ = env
.senders
.send_to_screen(ScreenInstruction::HighlightAndUnhighlightPanes(
panes_to_highlight,
panes_to_unhighlight,
env.client_id,
));
}
fn close_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
for pane_id in pane_ids {
match pane_id {
PaneId::Terminal(terminal_pane_id) => {
close_terminal_pane(env, terminal_pane_id);
},
PaneId::Plugin(plugin_pane_id) => {
close_plugin_pane(env, plugin_pane_id);
},
}
}
}
fn float_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
let _ = env
.senders
.send_to_screen(ScreenInstruction::FloatMultiplePanes(
pane_ids,
env.client_id,
));
}
fn embed_multiple_panes(env: &PluginEnv, pane_ids: Vec<PaneId>) {
let _ = env
.senders
.send_to_screen(ScreenInstruction::EmbedMultiplePanes(
pane_ids,
env.client_id,
));
}
// Custom panic handler for plugins. // Custom panic handler for plugins.
// //
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the // This is called when a panic occurs in a plugin. Since most panics will likely originate in the
@ -2308,6 +2392,11 @@ fn check_command_permission(
| PluginCommand::SetFloatingPanePinned(..) | PluginCommand::SetFloatingPanePinned(..)
| PluginCommand::StackPanes(..) | PluginCommand::StackPanes(..)
| PluginCommand::ChangeFloatingPanesCoordinates(..) | PluginCommand::ChangeFloatingPanesCoordinates(..)
| PluginCommand::GroupAndUngroupPanes(..)
| PluginCommand::HighlightAndUnhighlightPanes(..)
| PluginCommand::CloseMultiplePanes(..)
| PluginCommand::FloatMultiplePanes(..)
| PluginCommand::EmbedMultiplePanes(..)
| PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState, | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..) PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..)

View file

@ -938,6 +938,7 @@ pub(crate) fn route_action(
senders senders
.send_to_screen(ScreenInstruction::StackPanes( .send_to_screen(ScreenInstruction::StackPanes(
pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(), pane_ids_to_stack.iter().map(|p| PaneId::from(*p)).collect(),
client_id,
)) ))
.with_context(err_context)?; .with_context(err_context)?;
}, },
@ -949,6 +950,16 @@ pub(crate) fn route_action(
)])) )]))
.with_context(err_context)?; .with_context(err_context)?;
}, },
Action::TogglePaneInGroup => {
senders
.send_to_screen(ScreenInstruction::TogglePaneInGroup(client_id))
.with_context(err_context)?;
},
Action::ToggleGroupMarking => {
senders
.send_to_screen(ScreenInstruction::ToggleGroupMarking(client_id))
.with_context(err_context)?;
},
} }
Ok(should_break) Ok(should_break)
} }

View file

@ -371,6 +371,7 @@ pub enum ScreenInstruction {
hide_session_name: bool, hide_session_name: bool,
stacked_resize: bool, stacked_resize: bool,
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
}, },
RerunCommandPane(u32), // u32 - terminal pane id RerunCommandPane(u32), // u32 - terminal pane id
ResizePaneWithId(ResizeStrategy, PaneId), ResizePaneWithId(ResizeStrategy, PaneId),
@ -404,8 +405,16 @@ pub enum ScreenInstruction {
ListClientsToPlugin(PluginId, ClientId), ListClientsToPlugin(PluginId, ClientId),
TogglePanePinned(ClientId), TogglePanePinned(ClientId),
SetFloatingPanePinned(PaneId, bool), SetFloatingPanePinned(PaneId, bool),
StackPanes(Vec<PaneId>), StackPanes(Vec<PaneId>, ClientId),
ChangeFloatingPanesCoordinates(Vec<(PaneId, FloatingPaneCoordinates)>), ChangeFloatingPanesCoordinates(Vec<(PaneId, FloatingPaneCoordinates)>),
AddHighlightPaneFrameColorOverride(Vec<PaneId>, Option<String>), // Option<String> => optional
// message
GroupAndUngroupPanes(Vec<PaneId>, Vec<PaneId>, ClientId), // panes_to_group, panes_to_ungroup
HighlightAndUnhighlightPanes(Vec<PaneId>, Vec<PaneId>, ClientId), // panes_to_highlight, panes_to_unhighlight
FloatMultiplePanes(Vec<PaneId>, ClientId),
EmbedMultiplePanes(Vec<PaneId>, ClientId),
TogglePaneInGroup(ClientId),
ToggleGroupMarking(ClientId),
} }
impl From<&ScreenInstruction> for ScreenContext { impl From<&ScreenInstruction> for ScreenContext {
@ -616,6 +625,17 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::ChangeFloatingPanesCoordinates(..) => { ScreenInstruction::ChangeFloatingPanesCoordinates(..) => {
ScreenContext::ChangeFloatingPanesCoordinates ScreenContext::ChangeFloatingPanesCoordinates
}, },
ScreenInstruction::AddHighlightPaneFrameColorOverride(..) => {
ScreenContext::AddHighlightPaneFrameColorOverride
},
ScreenInstruction::GroupAndUngroupPanes(..) => ScreenContext::GroupAndUngroupPanes,
ScreenInstruction::HighlightAndUnhighlightPanes(..) => {
ScreenContext::HighlightAndUnhighlightPanes
},
ScreenInstruction::FloatMultiplePanes(..) => ScreenContext::FloatMultiplePanes,
ScreenInstruction::EmbedMultiplePanes(..) => ScreenContext::EmbedMultiplePanes,
ScreenInstruction::TogglePaneInGroup(..) => ScreenContext::TogglePaneInGroup,
ScreenInstruction::ToggleGroupMarking(..) => ScreenContext::ToggleGroupMarking,
} }
} }
} }
@ -697,6 +717,9 @@ pub(crate) struct Screen {
default_layout_name: Option<String>, default_layout_name: Option<String>,
explicitly_disable_kitty_keyboard_protocol: bool, explicitly_disable_kitty_keyboard_protocol: bool,
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
current_pane_group: Rc<RefCell<HashMap<ClientId, Vec<PaneId>>>>,
advanced_mouse_actions: bool,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
} }
impl Screen { impl Screen {
@ -723,6 +746,7 @@ impl Screen {
explicitly_disable_kitty_keyboard_protocol: bool, explicitly_disable_kitty_keyboard_protocol: bool,
stacked_resize: bool, stacked_resize: bool,
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
) -> Self { ) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone()); let session_info = SessionInfo::new(session_name.clone());
@ -766,6 +790,9 @@ impl Screen {
layout_dir, layout_dir,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
default_editor, default_editor,
current_pane_group: Rc::new(RefCell::new(HashMap::new())),
currently_marking_pane_group: Rc::new(RefCell::new(HashMap::new())),
advanced_mouse_actions,
} }
} }
@ -1257,6 +1284,10 @@ impl Screen {
&mut self.tabs &mut self.tabs
} }
pub fn get_tabs(&self) -> &BTreeMap<usize, Tab> {
&self.tabs
}
/// Returns an immutable reference to this [`Screen`]'s active [`Tab`]. /// Returns an immutable reference to this [`Screen`]'s active [`Tab`].
pub fn get_active_tab(&self, client_id: ClientId) -> Result<&Tab> { pub fn get_active_tab(&self, client_id: ClientId) -> Result<&Tab> {
match self.active_tab_indices.get(&client_id) { match self.active_tab_indices.get(&client_id) {
@ -1366,6 +1397,9 @@ impl Screen {
self.styled_underlines, self.styled_underlines,
self.explicitly_disable_kitty_keyboard_protocol, self.explicitly_disable_kitty_keyboard_protocol,
self.default_editor.clone(), self.default_editor.clone(),
self.current_pane_group.clone(),
self.currently_marking_pane_group.clone(),
self.advanced_mouse_actions,
); );
for (client_id, mode_info) in &self.mode_info { for (client_id, mode_info) in &self.mode_info {
tab.change_mode_info(mode_info.clone(), *client_id); tab.change_mode_info(mode_info.clone(), *client_id);
@ -2474,6 +2508,7 @@ impl Screen {
hide_session_name: bool, hide_session_name: bool,
stacked_resize: bool, stacked_resize: bool,
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
advanced_mouse_actions: bool,
client_id: ClientId, client_id: ClientId,
) -> Result<()> { ) -> Result<()> {
let should_support_arrow_fonts = !simplified_ui; let should_support_arrow_fonts = !simplified_ui;
@ -2488,6 +2523,7 @@ impl Screen {
self.copy_options.command = copy_command.clone(); self.copy_options.command = copy_command.clone();
self.copy_options.copy_on_select = copy_on_select; self.copy_options.copy_on_select = copy_on_select;
self.draw_pane_frames = pane_frames; self.draw_pane_frames = pane_frames;
self.advanced_mouse_actions = advanced_mouse_actions;
self.default_mode_info self.default_mode_info
.update_arrow_fonts(should_support_arrow_fonts); .update_arrow_fonts(should_support_arrow_fonts);
self.default_mode_info self.default_mode_info
@ -2507,6 +2543,7 @@ impl Screen {
tab.update_copy_options(&self.copy_options); tab.update_copy_options(&self.copy_options);
tab.set_pane_frames(pane_frames); tab.set_pane_frames(pane_frames);
tab.update_arrow_fonts(should_support_arrow_fonts); tab.update_arrow_fonts(should_support_arrow_fonts);
tab.update_advanced_mouse_actions(advanced_mouse_actions);
} }
// client specific configuration // client specific configuration
@ -2559,10 +2596,11 @@ impl Screen {
); );
} }
} }
pub fn stack_panes(&mut self, mut pane_ids_to_stack: Vec<PaneId>) { pub fn stack_panes(&mut self, mut pane_ids_to_stack: Vec<PaneId>) -> Option<PaneId> {
// if successful, returns the pane id of the root pane
if pane_ids_to_stack.is_empty() { if pane_ids_to_stack.is_empty() {
log::error!("Got an empty list of pane_ids to stack"); log::error!("Got an empty list of pane_ids to stack");
return; return None;
} }
let stack_size = pane_ids_to_stack.len(); let stack_size = pane_ids_to_stack.len();
let root_pane_id = pane_ids_to_stack.remove(0); let root_pane_id = pane_ids_to_stack.remove(0);
@ -2579,19 +2617,20 @@ impl Screen {
.copied() .copied()
else { else {
log::error!("Failed to find tab for root_pane_id: {:?}", root_pane_id); log::error!("Failed to find tab for root_pane_id: {:?}", root_pane_id);
return; return None;
}; };
let mut panes_to_stack = vec![];
let target_tab_has_room_for_stack = self let target_tab_has_room_for_stack = self
.tabs .tabs
.get(&root_tab_id) .get_mut(&root_tab_id)
.map(|t| t.has_room_for_stack(root_pane_id, stack_size)) .map(|t| t.has_room_for_stack(root_pane_id, stack_size))
.unwrap_or(false); .unwrap_or(false);
if !target_tab_has_room_for_stack { if !target_tab_has_room_for_stack {
log::error!("No room for stack with root pane id: {:?}", root_pane_id); log::error!("No room for stack with root pane id: {:?}", root_pane_id);
return; return None;
} }
let mut panes_to_stack = vec![];
for (tab_id, tab) in self.tabs.iter_mut() { for (tab_id, tab) in self.tabs.iter_mut() {
if tab_id == &root_tab_id { if tab_id == &root_tab_id {
// we do this before we extract panes so that the extraction won't trigger a // we do this before we extract panes so that the extraction won't trigger a
@ -2614,6 +2653,7 @@ impl Screen {
self.tabs self.tabs
.get_mut(&root_tab_id) .get_mut(&root_tab_id)
.map(|t| t.stack_panes(root_pane_id, panes_to_stack)); .map(|t| t.stack_panes(root_pane_id, panes_to_stack));
return Some(root_pane_id);
} }
pub fn change_floating_panes_coordinates( pub fn change_floating_panes_coordinates(
&mut self, &mut self,
@ -2629,6 +2669,87 @@ impl Screen {
} }
} }
} }
pub fn handle_mouse_event(&mut self, event: MouseEvent, client_id: ClientId) {
match self
.get_active_tab_mut(client_id)
.and_then(|tab| tab.handle_mouse_event(&event, client_id))
{
Ok(mouse_effect) => {
if let Some(pane_id) = mouse_effect.group_toggle {
if self.advanced_mouse_actions {
self.toggle_pane_id_in_group(pane_id, &client_id);
}
}
if let Some(pane_id) = mouse_effect.group_add {
if self.advanced_mouse_actions {
self.add_pane_id_to_group(pane_id, &client_id);
}
}
if mouse_effect.ungroup {
if self.advanced_mouse_actions {
self.clear_pane_group(&client_id);
}
}
if mouse_effect.state_changed {
let _ = self.log_and_report_session_state();
}
if !mouse_effect.leave_clipboard_message {
let _ = self
.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::InputReceived,
)]));
}
self.render(None).non_fatal();
},
Err(e) => {
eprintln!("mouse event error: {:?}", e);
log::error!("Failed to process MouseEvent: {}", e);
},
}
}
pub fn toggle_pane_in_group(&mut self, client_id: ClientId) -> Result<()> {
let err_context = "Can't add pane to group";
let active_tab = self
.get_active_tab(client_id)
.with_context(|| err_context)?;
let active_pane_id = active_tab
.get_active_pane_id(client_id)
.with_context(|| err_context)?;
self.toggle_pane_id_in_group(active_pane_id, &client_id);
let _ = self.log_and_report_session_state();
Ok(())
}
pub fn toggle_group_marking(&mut self, client_id: ClientId) -> Result<()> {
let (was_marking_before, marking_pane_group_now) = {
let mut currently_marking_pane_group = self.currently_marking_pane_group.borrow_mut();
let previous_value = currently_marking_pane_group
.remove(&client_id)
.unwrap_or(false);
let new_value = !previous_value;
if new_value {
currently_marking_pane_group.insert(client_id, true);
}
(previous_value, new_value)
};
if marking_pane_group_now {
let active_pane_id = self.get_active_pane_id(&client_id);
if let Some(active_pane_id) = active_pane_id {
self.add_pane_id_to_group(active_pane_id, &client_id);
}
}
let value_changed = was_marking_before != marking_pane_group_now;
if value_changed {
for tab in self.tabs.values_mut() {
tab.update_input_modes()?;
}
let _ = self.log_and_report_session_state();
}
Ok(())
}
fn unblock_input(&self) -> Result<()> { fn unblock_input(&self) -> Result<()> {
self.bus self.bus
.senders .senders
@ -2773,6 +2894,117 @@ impl Screen {
fn connected_clients_contains(&self, client_id: &ClientId) -> bool { fn connected_clients_contains(&self, client_id: &ClientId) -> bool {
self.connected_clients.borrow().contains(client_id) self.connected_clients.borrow().contains(client_id)
} }
fn get_client_pane_group(&self, client_id: &ClientId) -> HashSet<PaneId> {
self.current_pane_group
.borrow()
.get(client_id)
.map(|p| p.iter().copied().collect())
.unwrap_or_else(|| HashSet::new())
}
fn clear_pane_group(&mut self, client_id: &ClientId) {
self.current_pane_group
.borrow_mut()
.get_mut(client_id)
.map(|p| p.clear());
self.currently_marking_pane_group
.borrow_mut()
.remove(client_id);
}
fn toggle_pane_id_in_group(&mut self, pane_id: PaneId, client_id: &ClientId) {
{
let mut pane_groups = self.current_pane_group.borrow_mut();
let client_pane_group = pane_groups.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);
};
}
self.retain_only_existing_panes_in_pane_groups();
}
fn add_pane_id_to_group(&mut self, pane_id: PaneId, client_id: &ClientId) {
{
let mut pane_groups = self.current_pane_group.borrow_mut();
let client_pane_group = pane_groups.entry(*client_id).or_insert_with(|| vec![]);
if !client_pane_group.contains(&pane_id) {
client_pane_group.push(pane_id);
}
}
self.retain_only_existing_panes_in_pane_groups();
}
fn add_active_pane_to_group_if_marking(&mut self, client_id: &ClientId) {
{
if self
.currently_marking_pane_group
.borrow()
.get(client_id)
.copied()
.unwrap_or(false)
{
let active_pane_id = self.get_active_pane_id(&client_id);
if let Some(active_pane_id) = active_pane_id {
self.add_pane_id_to_group(active_pane_id, &client_id);
}
}
}
self.retain_only_existing_panes_in_pane_groups();
}
fn get_active_pane_id(&self, client_id: &ClientId) -> Option<PaneId> {
let active_tab = self.get_active_tab(*client_id).ok()?;
active_tab.get_active_pane_id(*client_id)
}
fn group_and_ungroup_panes(
&mut self,
mut pane_ids_to_group: Vec<PaneId>,
pane_ids_to_ungroup: Vec<PaneId>,
client_id: ClientId,
) {
{
let mut current_pane_group = self.current_pane_group.borrow_mut();
let client_pane_group = current_pane_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));
}
self.retain_only_existing_panes_in_pane_groups();
let _ = self.log_and_report_session_state();
}
fn retain_only_existing_panes_in_pane_groups(&mut self) {
let clients_with_empty_group = {
let mut clients_with_empty_group = vec![];
let mut current_pane_group = self.current_pane_group.borrow_mut();
for (client_id, panes_in_group) in current_pane_group.iter_mut() {
let all_tabs = self.get_tabs();
panes_in_group.retain(|p_id| {
let mut found = false;
for tab in all_tabs.values() {
if tab.has_pane_with_pid(&p_id) {
found = true;
break;
}
}
found
});
if panes_in_group.is_empty() {
clients_with_empty_group.push(*client_id)
}
}
clients_with_empty_group
};
for client_id in &clients_with_empty_group {
self.currently_marking_pane_group
.borrow_mut()
.remove(client_id);
}
if !clients_with_empty_group.is_empty() {
let all_tabs = self.get_tabs_mut();
for tab in all_tabs.values_mut() {
let _ = tab.update_input_modes();
}
}
}
} }
#[cfg(not(test))] #[cfg(not(test))]
@ -2839,6 +3071,7 @@ pub(crate) fn screen_thread_main(
.unwrap_or(false); // by default, we try to support this if the terminal supports it and .unwrap_or(false); // by default, we try to support this if the terminal supports it and
// the program running inside a pane requests it // the program running inside a pane requests it
let stacked_resize = config_options.stacked_resize.unwrap_or(true); let stacked_resize = config_options.stacked_resize.unwrap_or(true);
let advanced_mouse_actions = config_options.advanced_mouse_actions.unwrap_or(true);
let thread_senders = bus.senders.clone(); let thread_senders = bus.senders.clone();
let mut screen = Screen::new( let mut screen = Screen::new(
@ -2872,6 +3105,7 @@ pub(crate) fn screen_thread_main(
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
stacked_resize, stacked_resize,
default_editor, default_editor,
advanced_mouse_actions,
); );
let mut pending_tab_ids: HashSet<usize> = HashSet::new(); let mut pending_tab_ids: HashSet<usize> = HashSet::new();
@ -3053,7 +3287,6 @@ pub(crate) fn screen_thread_main(
.toggle_pane_embed_or_floating(client_id), ?); .toggle_pane_embed_or_floating(client_id), ?);
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.render(None)?; screen.render(None)?;
}, },
ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => { ScreenInstruction::ToggleFloatingPanes(client_id, default_shell) => {
@ -3195,12 +3428,14 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_left(client_id),
? ?
); );
screen.add_active_pane_to_group_if_marking(&client_id);
screen.render(None)?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => { ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => {
screen.move_focus_left_or_previous_tab(client_id)?; screen.move_focus_left_or_previous_tab(client_id)?;
screen.add_active_pane_to_group_if_marking(&client_id);
screen.unblock_input()?; screen.unblock_input()?;
screen.render(None)?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
@ -3212,6 +3447,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_down(client_id),
? ?
); );
screen.add_active_pane_to_group_if_marking(&client_id);
screen.render(None)?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
@ -3223,12 +3459,14 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_right(client_id),
? ?
); );
screen.add_active_pane_to_group_if_marking(&client_id);
screen.render(None)?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusRightOrNextTab(client_id) => { ScreenInstruction::MoveFocusRightOrNextTab(client_id) => {
screen.move_focus_right_or_next_tab(client_id)?; screen.move_focus_right_or_next_tab(client_id)?;
screen.add_active_pane_to_group_if_marking(&client_id);
screen.unblock_input()?; screen.unblock_input()?;
screen.render(None)?; screen.render(None)?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
@ -3240,6 +3478,7 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id), |tab: &mut Tab, client_id: ClientId| tab.move_focus_up(client_id),
? ?
); );
screen.add_active_pane_to_group_if_marking(&client_id);
screen.render(None)?; screen.render(None)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
@ -3546,6 +3785,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.log_and_report_session_state()?; screen.log_and_report_session_state()?;
screen.retain_only_existing_panes_in_pane_groups();
}, },
ScreenInstruction::HoldPane(id, exit_status, run_command) => { ScreenInstruction::HoldPane(id, exit_status, run_command) => {
let is_first_run = false; let is_first_run = false;
@ -3863,31 +4103,7 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::MouseEvent(event, client_id) => { ScreenInstruction::MouseEvent(event, client_id) => {
match screen screen.handle_mouse_event(event, client_id);
.get_active_tab_mut(client_id)
.and_then(|tab| tab.handle_mouse_event(&event, client_id))
{
Ok(mouse_effect) => {
if mouse_effect.state_changed {
screen.log_and_report_session_state()?;
}
if !mouse_effect.leave_clipboard_message {
let _ =
screen
.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::InputReceived,
)]));
}
screen.render(None).non_fatal();
},
Err(e) => {
log::error!("Failed to process MouseEvent: {}", e);
},
}
}, },
ScreenInstruction::Copy(client_id) => { ScreenInstruction::Copy(client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
@ -4019,12 +4235,28 @@ pub(crate) fn screen_thread_main(
} }
screen.render(None)?; screen.render(None)?;
}, },
ScreenInstruction::AddHighlightPaneFrameColorOverride(pane_ids, error_text) => {
let all_tabs = screen.get_tabs_mut();
for pane_id in pane_ids {
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) {
tab.add_highlight_pane_frame_color_override(
pane_id,
error_text.clone(),
None,
);
break;
}
}
}
screen.render(None)?;
},
ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => { ScreenInstruction::ClearPaneFrameColorOverride(pane_ids) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
for pane_id in pane_ids { for pane_id in pane_ids {
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) { if tab.has_pane_with_pid(&pane_id) {
tab.clear_pane_frame_color_override(pane_id); tab.clear_pane_frame_color_override(pane_id, None);
break; break;
} }
} }
@ -4642,6 +4874,7 @@ pub(crate) fn screen_thread_main(
hide_session_name, hide_session_name,
stacked_resize, stacked_resize,
default_editor, default_editor,
advanced_mouse_actions,
} => { } => {
screen screen
.reconfigure( .reconfigure(
@ -4659,6 +4892,7 @@ pub(crate) fn screen_thread_main(
hide_session_name, hide_session_name,
stacked_resize, stacked_resize,
default_editor, default_editor,
advanced_mouse_actions,
client_id, client_id,
) )
.non_fatal(); .non_fatal();
@ -4845,7 +5079,7 @@ pub(crate) fn screen_thread_main(
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) { if tab.has_pane_with_pid(&pane_id) {
tab.toggle_pane_embed_or_floating_for_pane_id(pane_id) tab.toggle_pane_embed_or_floating_for_pane_id(pane_id, None)
.non_fatal(); .non_fatal();
break; break;
} }
@ -4869,6 +5103,17 @@ pub(crate) fn screen_thread_main(
new_tab_name, new_tab_name,
client_id, client_id,
)?; )?;
// TODO: is this a race?
let pane_group = screen.get_client_pane_group(&client_id);
if !pane_group.is_empty() {
let _ = screen.bus.senders.send_to_background_jobs(
BackgroundJob::HighlightPanesWithMessage(
pane_group.iter().copied().collect(),
"BROKEN OUT".to_owned(),
),
);
}
screen.clear_pane_group(&client_id);
}, },
ScreenInstruction::BreakPanesToTabWithIndex { ScreenInstruction::BreakPanesToTabWithIndex {
pane_ids, pane_ids,
@ -4882,6 +5127,16 @@ pub(crate) fn screen_thread_main(
should_change_focus_to_new_tab, should_change_focus_to_new_tab,
client_id, client_id,
)?; )?;
let pane_group = screen.get_client_pane_group(&client_id);
if !pane_group.is_empty() {
let _ = screen.bus.senders.send_to_background_jobs(
BackgroundJob::HighlightPanesWithMessage(
pane_group.iter().copied().collect(),
"BROKEN OUT".to_owned(),
),
);
}
screen.clear_pane_group(&client_id);
}, },
ScreenInstruction::TogglePanePinned(client_id) => { ScreenInstruction::TogglePanePinned(client_id) => {
screen.toggle_pane_pinned(client_id); screen.toggle_pane_pinned(client_id);
@ -4889,16 +5144,133 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::SetFloatingPanePinned(pane_id, should_be_pinned) => { ScreenInstruction::SetFloatingPanePinned(pane_id, should_be_pinned) => {
screen.set_floating_pane_pinned(pane_id, should_be_pinned); screen.set_floating_pane_pinned(pane_id, should_be_pinned);
}, },
ScreenInstruction::StackPanes(pane_ids_to_stack) => { ScreenInstruction::StackPanes(pane_ids_to_stack, client_id) => {
screen.stack_panes(pane_ids_to_stack); if let Some(root_pane_id) = screen.stack_panes(pane_ids_to_stack) {
let _ = screen.focus_pane_with_id(root_pane_id, false, client_id);
let _ = screen.unblock_input(); let _ = screen.unblock_input();
let _ = screen.render(None); let _ = screen.render(None);
let pane_group = screen.get_client_pane_group(&client_id);
if !pane_group.is_empty() {
let _ = screen.bus.senders.send_to_background_jobs(
BackgroundJob::HighlightPanesWithMessage(
pane_group.iter().copied().collect(),
"STACKED".to_owned(),
),
);
}
screen.clear_pane_group(&client_id);
}
}, },
ScreenInstruction::ChangeFloatingPanesCoordinates(pane_ids_and_coordinates) => { ScreenInstruction::ChangeFloatingPanesCoordinates(pane_ids_and_coordinates) => {
screen.change_floating_panes_coordinates(pane_ids_and_coordinates); screen.change_floating_panes_coordinates(pane_ids_and_coordinates);
let _ = screen.unblock_input(); let _ = screen.unblock_input();
let _ = screen.render(None); let _ = screen.render(None);
}, },
ScreenInstruction::GroupAndUngroupPanes(
pane_ids_to_group,
pane_ids_to_ungroup,
client_id,
) => {
screen.group_and_ungroup_panes(pane_ids_to_group, pane_ids_to_ungroup, client_id);
let _ = screen.log_and_report_session_state();
},
ScreenInstruction::TogglePaneInGroup(client_id) => {
screen.toggle_pane_in_group(client_id).non_fatal();
},
ScreenInstruction::ToggleGroupMarking(client_id) => {
screen.toggle_group_marking(client_id).non_fatal();
},
ScreenInstruction::HighlightAndUnhighlightPanes(
pane_ids_to_highlight,
pane_ids_to_unhighlight,
client_id,
) => {
{
let all_tabs = screen.get_tabs_mut();
for pane_id in pane_ids_to_highlight {
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) {
tab.add_highlight_pane_frame_color_override(
pane_id,
None,
Some(client_id),
);
}
}
}
for pane_id in pane_ids_to_unhighlight {
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) {
tab.clear_pane_frame_color_override(pane_id, Some(client_id));
}
}
}
screen.render(None)?;
}
let _ = screen.log_and_report_session_state();
},
ScreenInstruction::FloatMultiplePanes(pane_ids_to_float, client_id) => {
{
let all_tabs = screen.get_tabs_mut();
let mut ejected_panes_in_group = vec![];
for pane_id in pane_ids_to_float {
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) {
if !tab.pane_id_is_floating(&pane_id) {
ejected_panes_in_group.push(pane_id);
tab.toggle_pane_embed_or_floating_for_pane_id(
pane_id,
Some(client_id),
)
.non_fatal();
}
tab.show_floating_panes();
}
}
}
screen.render(None)?;
if !ejected_panes_in_group.is_empty() {
let _ = screen.bus.senders.send_to_background_jobs(
BackgroundJob::HighlightPanesWithMessage(
ejected_panes_in_group,
"EJECTED".to_owned(),
),
);
}
}
let _ = screen.log_and_report_session_state();
},
ScreenInstruction::EmbedMultiplePanes(pane_ids_to_float, client_id) => {
{
let all_tabs = screen.get_tabs_mut();
let mut embedded_panes_in_group = vec![];
for pane_id in pane_ids_to_float {
for tab in all_tabs.values_mut() {
if tab.has_pane_with_pid(&pane_id) {
if tab.pane_id_is_floating(&pane_id) {
embedded_panes_in_group.push(pane_id);
tab.toggle_pane_embed_or_floating_for_pane_id(
pane_id,
Some(client_id),
)
.non_fatal();
}
tab.hide_floating_panes();
}
}
}
screen.render(None)?;
if !embedded_panes_in_group.is_empty() {
let _ = screen.bus.senders.send_to_background_jobs(
BackgroundJob::HighlightPanesWithMessage(
embedded_panes_in_group,
"EMBEDDED".to_owned(),
),
);
}
}
let _ = screen.log_and_report_session_state();
},
} }
} }
Ok(()) Ok(())

View file

@ -46,7 +46,7 @@ use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use std::time::Instant; use std::time::Instant;
use std::{ use std::{
collections::{HashMap, HashSet}, collections::{BTreeMap, HashMap, HashSet},
str, str,
}; };
use zellij_utils::{ use zellij_utils::{
@ -152,6 +152,9 @@ enum BufferedTabInstruction {
pub struct MouseEffect { pub struct MouseEffect {
pub state_changed: bool, pub state_changed: bool,
pub leave_clipboard_message: bool, pub leave_clipboard_message: bool,
pub group_toggle: Option<PaneId>,
pub group_add: Option<PaneId>,
pub ungroup: bool,
} }
impl MouseEffect { impl MouseEffect {
@ -159,18 +162,54 @@ impl MouseEffect {
MouseEffect { MouseEffect {
state_changed: true, state_changed: true,
leave_clipboard_message: false, leave_clipboard_message: false,
group_toggle: None,
group_add: None,
ungroup: false,
} }
} }
pub fn leave_clipboard_message() -> Self { pub fn leave_clipboard_message() -> Self {
MouseEffect { MouseEffect {
state_changed: false, state_changed: false,
leave_clipboard_message: true, leave_clipboard_message: true,
group_toggle: None,
group_add: None,
ungroup: false,
} }
} }
pub fn state_changed_and_leave_clipboard_message() -> Self { pub fn state_changed_and_leave_clipboard_message() -> Self {
MouseEffect { MouseEffect {
state_changed: true, state_changed: true,
leave_clipboard_message: true, leave_clipboard_message: true,
group_toggle: None,
group_add: None,
ungroup: false,
}
}
pub fn group_toggle(pane_id: PaneId) -> Self {
MouseEffect {
state_changed: true,
leave_clipboard_message: false,
group_toggle: Some(pane_id),
group_add: None,
ungroup: false,
}
}
pub fn group_add(pane_id: PaneId) -> Self {
MouseEffect {
state_changed: true,
leave_clipboard_message: false,
group_toggle: None,
group_add: Some(pane_id),
ungroup: false,
}
}
pub fn ungroup() -> Self {
MouseEffect {
state_changed: true,
leave_clipboard_message: false,
group_toggle: None,
group_add: None,
ungroup: true,
} }
} }
} }
@ -222,6 +261,10 @@ pub(crate) struct Tab {
arrow_fonts: bool, arrow_fonts: bool,
styled_underlines: bool, styled_underlines: bool,
explicitly_disable_kitty_keyboard_protocol: bool, explicitly_disable_kitty_keyboard_protocol: bool,
mouse_hover_pane_id: HashMap<ClientId, PaneId>,
current_pane_group: Rc<RefCell<HashMap<ClientId, Vec<PaneId>>>>,
advanced_mouse_actions: bool,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
} }
#[derive(Clone, Debug, Default, Serialize, Deserialize)] #[derive(Clone, Debug, Default, Serialize, Deserialize)]
@ -525,7 +568,13 @@ pub trait Pane {
// No-op by default, only terminal panes support holding // No-op by default, only terminal panes support holding
} }
fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>); fn add_red_pane_frame_color_override(&mut self, _error_text: Option<String>);
fn clear_pane_frame_color_override(&mut self); fn add_highlight_pane_frame_color_override(
&mut self,
_text: Option<String>,
_client_id: Option<ClientId>,
) {
}
fn clear_pane_frame_color_override(&mut self, _client_id: Option<ClientId>);
fn frame_color_override(&self) -> Option<PaletteColor>; fn frame_color_override(&self) -> Option<PaletteColor>;
fn invoked_with(&self) -> &Option<Run>; fn invoked_with(&self) -> &Option<Run>;
fn set_title(&mut self, title: String); fn set_title(&mut self, title: String);
@ -626,6 +675,9 @@ impl Tab {
styled_underlines: bool, styled_underlines: bool,
explicitly_disable_kitty_keyboard_protocol: bool, explicitly_disable_kitty_keyboard_protocol: bool,
default_editor: Option<PathBuf>, default_editor: Option<PathBuf>,
current_pane_group: Rc<RefCell<HashMap<ClientId, Vec<PaneId>>>>,
currently_marking_pane_group: Rc<RefCell<HashMap<ClientId, bool>>>,
advanced_mouse_actions: bool,
) -> Self { ) -> Self {
let name = if name.is_empty() { let name = if name.is_empty() {
format!("Tab #{}", index + 1) format!("Tab #{}", index + 1)
@ -720,6 +772,10 @@ impl Tab {
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
default_editor, default_editor,
mouse_hover_pane_id: HashMap::new(),
current_pane_group,
currently_marking_pane_group,
advanced_mouse_actions,
} }
} }
@ -829,6 +885,9 @@ impl Tab {
.non_fatal(); .non_fatal();
} }
self.set_force_render(); self.set_force_render();
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to apply cached resizes"))?;
Ok(()) Ok(())
} }
fn relayout_tiled_panes(&mut self, search_backwards: bool) -> Result<()> { fn relayout_tiled_panes(&mut self, search_backwards: bool) -> Result<()> {
@ -873,6 +932,9 @@ impl Tab {
// we do this so that the new swap layout has a chance to pass through the constraint system // we do this so that the new swap layout has a chance to pass through the constraint system
self.tiled_panes.resize(display_area); self.tiled_panes.resize(display_area);
self.set_should_clear_display_before_rendering(); self.set_should_clear_display_before_rendering();
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to apply cached resizes"))?;
Ok(()) Ok(())
} }
pub fn previous_swap_layout(&mut self) -> Result<()> { pub fn previous_swap_layout(&mut self) -> Result<()> {
@ -882,9 +944,6 @@ impl Tab {
} else { } else {
self.relayout_tiled_panes(search_backwards)?; self.relayout_tiled_panes(search_backwards)?;
} }
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to update plugins with mode info"))?;
Ok(()) Ok(())
} }
pub fn next_swap_layout(&mut self) -> Result<()> { pub fn next_swap_layout(&mut self) -> Result<()> {
@ -894,9 +953,6 @@ impl Tab {
} else { } else {
self.relayout_tiled_panes(search_backwards)?; self.relayout_tiled_panes(search_backwards)?;
} }
self.senders
.send_to_pty_writer(PtyWriteInstruction::ApplyCachedResizes)
.with_context(|| format!("failed to update plugins with mode info"))?;
Ok(()) Ok(())
} }
pub fn apply_buffered_instructions(&mut self) -> Result<()> { pub fn apply_buffered_instructions(&mut self) -> Result<()> {
@ -936,6 +992,7 @@ impl Tab {
// this updates all plugins with the client's input mode // this updates all plugins with the client's input mode
let mode_infos = self.mode_info.borrow(); let mode_infos = self.mode_info.borrow();
let mut plugin_updates = vec![]; let mut plugin_updates = vec![];
let currently_marking_pane_group = self.currently_marking_pane_group.borrow();
for client_id in self.connected_clients.borrow().iter() { for client_id in self.connected_clients.borrow().iter() {
let mut mode_info = mode_infos let mut mode_info = mode_infos
.get(client_id) .get(client_id)
@ -943,6 +1000,8 @@ impl Tab {
.clone(); .clone();
mode_info.shell = Some(self.default_shell.clone()); mode_info.shell = Some(self.default_shell.clone());
mode_info.editor = self.default_editor.clone(); mode_info.editor = self.default_editor.clone();
mode_info.currently_marking_pane_group =
currently_marking_pane_group.get(client_id).copied();
plugin_updates.push((None, Some(*client_id), Event::ModeUpdate(mode_info))); plugin_updates.push((None, Some(*client_id), Event::ModeUpdate(mode_info)));
} }
self.senders self.senders
@ -1044,6 +1103,9 @@ impl Tab {
pub fn has_no_connected_clients(&self) -> bool { pub fn has_no_connected_clients(&self) -> bool {
self.connected_clients.borrow().is_empty() self.connected_clients.borrow().is_empty()
} }
pub fn pane_id_is_floating(&self, pane_id: &PaneId) -> bool {
self.floating_panes.panes_contain(pane_id)
}
pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> { pub fn toggle_pane_embed_or_floating(&mut self, client_id: ClientId) -> Result<()> {
let err_context = let err_context =
|| format!("failed to toggle embedded/floating pane for client {client_id}"); || format!("failed to toggle embedded/floating pane for client {client_id}");
@ -1079,7 +1141,11 @@ impl Tab {
} }
Ok(()) Ok(())
} }
pub fn toggle_pane_embed_or_floating_for_pane_id(&mut self, pane_id: PaneId) -> Result<()> { pub fn toggle_pane_embed_or_floating_for_pane_id(
&mut self,
pane_id: PaneId,
client_id: Option<ClientId>,
) -> Result<()> {
let err_context = || { let err_context = || {
format!( format!(
"failed to toggle embedded/floating pane for pane_id {:?}", "failed to toggle embedded/floating pane for pane_id {:?}",
@ -1097,7 +1163,7 @@ impl Tab {
format!("failed to find floating pane (ID: {pane_id:?}) to embed",) format!("failed to find floating pane (ID: {pane_id:?}) to embed",)
}) })
.with_context(err_context)?; .with_context(err_context)?;
self.add_tiled_pane(floating_pane_to_embed, pane_id, None)?; self.add_tiled_pane(floating_pane_to_embed, pane_id, client_id)?;
} }
} else if self.tiled_panes.panes_contain(&pane_id) { } else if self.tiled_panes.panes_contain(&pane_id) {
if self.get_selectable_tiled_panes().count() <= 1 { if self.get_selectable_tiled_panes().count() <= 1 {
@ -2210,14 +2276,21 @@ impl Tab {
floating_panes_stack, floating_panes_stack,
); );
let current_pane_group: HashMap<ClientId, Vec<PaneId>> =
{ self.current_pane_group.borrow().clone() };
self.tiled_panes self.tiled_panes
.render(output, self.floating_panes.panes_are_visible()) .render(
output,
self.floating_panes.panes_are_visible(),
&self.mouse_hover_pane_id,
current_pane_group.clone(),
)
.with_context(err_context)?; .with_context(err_context)?;
if (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes()) if (self.floating_panes.panes_are_visible() && self.floating_panes.has_active_panes())
|| self.floating_panes.has_pinned_panes() || self.floating_panes.has_pinned_panes()
{ {
self.floating_panes self.floating_panes
.render(output) .render(output, &self.mouse_hover_pane_id, current_pane_group)
.with_context(err_context)?; .with_context(err_context)?;
} }
@ -2817,7 +2890,7 @@ impl Tab {
self.swap_layouts.set_is_floating_damaged(); self.swap_layouts.set_is_floating_damaged();
// only relayout if the user is already "in" a layout, otherwise this might be // only relayout if the user is already "in" a layout, otherwise this might be
// confusing // confusing
let _ = self.next_swap_layout(); let _ = self.relayout_floating_panes(false);
} }
} else { } else {
if self.tiled_panes.fullscreen_is_active() { if self.tiled_panes.fullscreen_is_active() {
@ -2830,7 +2903,7 @@ impl Tab {
self.swap_layouts.set_is_tiled_damaged(); self.swap_layouts.set_is_tiled_damaged();
// only relayout if the user is already "in" a layout, otherwise this might be // only relayout if the user is already "in" a layout, otherwise this might be
// confusing // confusing
let _ = self.next_swap_layout(); let _ = self.relayout_tiled_panes(false);
} }
}; };
let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![( let _ = self.senders.send_to_plugin(PluginInstruction::Update(vec![(
@ -2878,7 +2951,7 @@ impl Tab {
self.swap_layouts.set_is_floating_damaged(); self.swap_layouts.set_is_floating_damaged();
// only relayout if the user is already "in" a layout, otherwise this might be // only relayout if the user is already "in" a layout, otherwise this might be
// confusing // confusing
let _ = self.next_swap_layout(); let _ = self.relayout_floating_panes(false);
} }
// we do this so that the logical index will not affect ordering in the target tab // we do this so that the logical index will not affect ordering in the target tab
if let Some(closed_pane) = closed_pane.as_mut() { if let Some(closed_pane) = closed_pane.as_mut() {
@ -2896,7 +2969,7 @@ impl Tab {
self.swap_layouts.set_is_tiled_damaged(); self.swap_layouts.set_is_tiled_damaged();
// only relayout if the user is already "in" a layout, otherwise this might be // only relayout if the user is already "in" a layout, otherwise this might be
// confusing // confusing
let _ = self.next_swap_layout(); let _ = self.relayout_tiled_panes(false);
} }
// we do this so that the logical index will not affect ordering in the target tab // we do this so that the logical index will not affect ordering in the target tab
if let Some(closed_pane) = closed_pane.as_mut() { if let Some(closed_pane) = closed_pane.as_mut() {
@ -3460,6 +3533,13 @@ impl Tab {
.ok_or_else(|| anyhow!("Failed to find pane at position"))? .ok_or_else(|| anyhow!("Failed to find pane at position"))?
.pid(); .pid();
match event.event_type { match event.event_type {
MouseEventType::Press if event.alt => {
self.mouse_hover_pane_id.remove(&client_id);
Ok(MouseEffect::group_toggle(pane_id_at_position))
},
MouseEventType::Motion if event.alt => {
Ok(MouseEffect::group_add(pane_id_at_position))
},
MouseEventType::Press => { MouseEventType::Press => {
if pane_id_at_position == active_pane_id { if pane_id_at_position == active_pane_id {
self.handle_active_pane_left_mouse_press(event, client_id) self.handle_active_pane_left_mouse_press(event, client_id)
@ -3474,6 +3554,9 @@ impl Tab {
self.handle_scrollwheel_up(&event.position, 3, client_id) self.handle_scrollwheel_up(&event.position, 3, client_id)
} else if event.wheel_down { } else if event.wheel_down {
self.handle_scrollwheel_down(&event.position, 3, client_id) self.handle_scrollwheel_down(&event.position, 3, client_id)
} else if event.right && event.alt {
self.mouse_hover_pane_id.remove(&client_id);
Ok(MouseEffect::ungroup())
} else if event.right { } else if event.right {
self.handle_right_click(&event, client_id) self.handle_right_click(&event, client_id)
} else if event.middle { } else if event.middle {
@ -3838,6 +3921,12 @@ impl Tab {
.with_context(err_context)?; .with_context(err_context)?;
} }
} }
self.mouse_hover_pane_id.remove(&client_id);
} else {
let pane_id = pane.pid();
if self.advanced_mouse_actions {
self.mouse_hover_pane_id.insert(client_id, pane_id);
}
} }
}; };
Ok(MouseEffect::leave_clipboard_message()) Ok(MouseEffect::leave_clipboard_message())
@ -4024,7 +4113,7 @@ impl Tab {
Ok(()) Ok(())
} }
pub fn visible(&self, visible: bool) -> Result<()> { pub fn visible(&mut self, visible: bool) -> Result<()> {
let pids_in_this_tab = self.tiled_panes.pane_ids().filter_map(|p| match p { let pids_in_this_tab = self.tiled_panes.pane_ids().filter_map(|p| match p {
PaneId::Plugin(pid) => Some(pid), PaneId::Plugin(pid) => Some(pid),
_ => None, _ => None,
@ -4036,6 +4125,9 @@ impl Tab {
self.senders self.senders
.send_to_plugin(PluginInstruction::Update(plugin_updates)) .send_to_plugin(PluginInstruction::Update(plugin_updates))
.with_context(|| format!("failed to set visibility of tab to {visible}"))?; .with_context(|| format!("failed to set visibility of tab to {visible}"))?;
if !visible {
self.mouse_hover_pane_id.clear();
}
Ok(()) Ok(())
} }
@ -4200,7 +4292,12 @@ impl Tab {
pane.add_red_pane_frame_color_override(error_text); pane.add_red_pane_frame_color_override(error_text);
} }
} }
pub fn clear_pane_frame_color_override(&mut self, pane_id: PaneId) { pub fn add_highlight_pane_frame_color_override(
&mut self,
pane_id: PaneId,
error_text: Option<String>,
client_id: Option<ClientId>,
) {
if let Some(pane) = self if let Some(pane) = self
.tiled_panes .tiled_panes
.get_pane_mut(pane_id) .get_pane_mut(pane_id)
@ -4212,7 +4309,26 @@ impl Tab {
.map(|s_p| &mut s_p.1) .map(|s_p| &mut s_p.1)
}) })
{ {
pane.clear_pane_frame_color_override(); pane.add_highlight_pane_frame_color_override(error_text, client_id);
}
}
pub fn clear_pane_frame_color_override(
&mut self,
pane_id: PaneId,
client_id: Option<ClientId>,
) {
if let Some(pane) = self
.tiled_panes
.get_pane_mut(pane_id)
.or_else(|| self.floating_panes.get_pane_mut(pane_id))
.or_else(|| {
self.suppressed_panes
.values_mut()
.find(|s_p| s_p.1.pid() == pane_id)
.map(|s_p| &mut s_p.1)
})
{
pane.clear_pane_frame_color_override(client_id);
} }
} }
pub fn update_plugin_loading_stage(&mut self, pid: u32, loading_indication: LoadingIndication) { pub fn update_plugin_loading_stage(&mut self, pid: u32, loading_indication: LoadingIndication) {
@ -4351,12 +4467,14 @@ impl Tab {
} }
pub fn pane_infos(&self) -> Vec<PaneInfo> { pub fn pane_infos(&self) -> Vec<PaneInfo> {
let mut pane_info = vec![]; let mut pane_info = vec![];
let mut tiled_pane_info = self.tiled_panes.pane_info(); let current_pane_group = { self.current_pane_group.borrow().clone() };
let mut floating_pane_info = self.floating_panes.pane_info(); let mut tiled_pane_info = self.tiled_panes.pane_info(&current_pane_group);
let mut floating_pane_info = self.floating_panes.pane_info(&current_pane_group);
pane_info.append(&mut tiled_pane_info); pane_info.append(&mut tiled_pane_info);
pane_info.append(&mut floating_pane_info); pane_info.append(&mut floating_pane_info);
for (pane_id, (_is_scrollback_editor, pane)) in self.suppressed_panes.iter() { for (pane_id, (_is_scrollback_editor, pane)) in self.suppressed_panes.iter() {
let mut pane_info_for_suppressed_pane = pane_info_for_pane(pane_id, pane); let mut pane_info_for_suppressed_pane =
pane_info_for_pane(pane_id, pane, &current_pane_group);
pane_info_for_suppressed_pane.is_floating = false; pane_info_for_suppressed_pane.is_floating = false;
pane_info_for_suppressed_pane.is_suppressed = true; pane_info_for_suppressed_pane.is_suppressed = true;
pane_info_for_suppressed_pane.is_focused = false; pane_info_for_suppressed_pane.is_focused = false;
@ -4397,7 +4515,7 @@ impl Tab {
// confusing and not what the user intends // confusing and not what the user intends
self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the self.swap_layouts.set_is_floating_damaged(); // we do this so that we won't skip to the
// next layout // next layout
self.next_swap_layout()?; self.relayout_floating_panes(false)?;
} }
Ok(()) Ok(())
} }
@ -4431,7 +4549,7 @@ impl Tab {
// confusing and not what the user intends // confusing and not what the user intends
self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the self.swap_layouts.set_is_tiled_damaged(); // we do this so that we won't skip to the
// next layout // next layout
self.next_swap_layout()?; self.relayout_tiled_panes(false)?;
} }
Ok(()) Ok(())
} }
@ -4589,6 +4707,9 @@ impl Tab {
pub fn update_auto_layout(&mut self, auto_layout: bool) { pub fn update_auto_layout(&mut self, auto_layout: bool) {
self.auto_layout = auto_layout; self.auto_layout = auto_layout;
} }
pub fn update_advanced_mouse_actions(&mut self, advanced_mouse_actions: bool) {
self.advanced_mouse_actions = advanced_mouse_actions;
}
pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes { pub fn extract_suppressed_panes(&mut self) -> SuppressedPanes {
self.suppressed_panes.drain().collect() self.suppressed_panes.drain().collect()
} }
@ -4609,17 +4730,25 @@ impl Tab {
self.set_force_render(); self.set_force_render();
} }
} }
pub fn has_room_for_stack(&self, root_pane_id: PaneId, stack_size: usize) -> bool { pub fn has_room_for_stack(&mut self, root_pane_id: PaneId, stack_size: usize) -> bool {
if self.floating_panes.panes_contain(&root_pane_id) if self.floating_panes.panes_contain(&root_pane_id)
|| self.suppressed_panes.contains_key(&root_pane_id) || self.suppressed_panes.contains_key(&root_pane_id)
{ {
log::error!("Root pane of stack cannot be floating or suppressed"); log::error!("Root pane of stack cannot be floating or suppressed");
return false; return false;
} }
if self.pane_is_stacked(root_pane_id) {
let room_left_in_stack = self
.tiled_panes
.room_left_in_stack_of_pane_id(&root_pane_id)
.unwrap_or(0);
stack_size <= room_left_in_stack
} else {
self.get_pane_with_id(root_pane_id) self.get_pane_with_id(root_pane_id)
.map(|p| p.position_and_size().rows.as_usize() >= stack_size + MIN_TERMINAL_HEIGHT) .map(|p| p.position_and_size().rows.as_usize() >= stack_size + MIN_TERMINAL_HEIGHT)
.unwrap_or(false) .unwrap_or(false)
} }
}
pub fn set_tiled_panes_damaged(&mut self) { pub fn set_tiled_panes_damaged(&mut self) {
self.swap_layouts.set_is_tiled_damaged(); self.swap_layouts.set_is_tiled_damaged();
} }
@ -4629,7 +4758,26 @@ impl Tab {
return; return;
} }
self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first self.swap_layouts.set_is_tiled_damaged(); // TODO: verify we can do all the below first
if self.pane_is_stacked(root_pane_id) {
if let Some(lowest_pane_id_in_stack) = self
.tiled_panes
.pane_ids_in_stack_of_pane_id(&root_pane_id)
.last()
{
// we get lowest_pane_id_in_stack so that we can extract the pane below and re-add
// it to its own stack - this has the effect of making it the last pane in the
// stack so that the rest of the panes will later be added below it - which makes
// sense from the perspective of the user
if let Some(pane) = self.extract_pane(root_pane_id, true) {
self.tiled_panes
.add_pane_to_stack(&lowest_pane_id_in_stack, pane);
}
}
for pane in panes_to_stack.drain(..) {
self.tiled_panes.add_pane_to_stack(&root_pane_id, pane);
}
self.tiled_panes.expand_pane_in_stack(root_pane_id);
} else {
// + 1 for the root pane // + 1 for the root pane
let mut stack_geoms = self let mut stack_geoms = self
.tiled_panes .tiled_panes
@ -4658,6 +4806,7 @@ impl Tab {
self.tiled_panes.expand_pane_in_stack(root_pane_id); self.tiled_panes.expand_pane_in_stack(root_pane_id);
} }
} }
}
pub fn change_floating_pane_coordinates( pub fn change_floating_pane_coordinates(
&mut self, &mut self,
pane_id: &PaneId, pane_id: &PaneId,
@ -4723,9 +4872,18 @@ impl Tab {
(is_scrollback_editor, replaced_pane), (is_scrollback_editor, replaced_pane),
); );
} }
fn pane_is_stacked(&self, pane_id: PaneId) -> bool {
self.get_pane_with_id(pane_id)
.map(|p| p.position_and_size().stacked.is_some())
.unwrap_or(false)
}
} }
pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo { pub fn pane_info_for_pane(
pane_id: &PaneId,
pane: &Box<dyn Pane>,
current_pane_group: &HashMap<ClientId, Vec<PaneId>>,
) -> PaneInfo {
let mut pane_info = PaneInfo::default(); let mut pane_info = PaneInfo::default();
pane_info.pane_x = pane.x(); pane_info.pane_x = pane.x();
pane_info.pane_content_x = pane.get_content_x(); pane_info.pane_content_x = pane.get_content_x();
@ -4741,6 +4899,17 @@ pub fn pane_info_for_pane(pane_id: &PaneId, pane: &Box<dyn Pane>) -> PaneInfo {
pane_info.exited = pane.exited(); pane_info.exited = pane.exited();
pane_info.exit_status = pane.exit_status(); pane_info.exit_status = pane.exit_status();
pane_info.is_held = pane.is_held(); pane_info.is_held = pane.is_held();
let index_in_pane_group: BTreeMap<ClientId, usize> = current_pane_group
.iter()
.filter_map(|(client_id, pane_ids)| {
if let Some(position) = pane_ids.iter().position(|p_id| p_id == &pane.pid()) {
Some((*client_id, position))
} else {
None
}
})
.collect();
pane_info.index_in_pane_group = index_in_pane_group;
match pane_id { match pane_id {
PaneId::Terminal(terminal_id) => { PaneId::Terminal(terminal_id) => {

View file

@ -224,10 +224,13 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -256,6 +259,9 @@ fn create_new_tab(size: Size, default_mode: ModeInfo) -> Tab {
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),
@ -292,10 +298,13 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -324,6 +333,9 @@ fn create_new_tab_without_pane_frames(size: Size, default_mode: ModeInfo) -> Tab
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),
@ -375,10 +387,13 @@ fn create_new_tab_with_swap_layouts(
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -407,6 +422,9 @@ fn create_new_tab_with_swap_layouts(
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
let ( let (
base_layout, base_layout,
@ -459,10 +477,13 @@ fn create_new_tab_with_os_api(
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -491,6 +512,9 @@ fn create_new_tab_with_os_api(
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),
@ -529,10 +553,13 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let layout = Layout::from_str(layout, "layout_file_name".into(), None, None).unwrap(); let layout = Layout::from_str(layout, "layout_file_name".into(), None, None).unwrap();
let (tab_layout, floating_panes_layout) = layout.new_tab(); let (tab_layout, floating_panes_layout) = layout.new_tab();
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -561,6 +588,9 @@ fn create_new_tab_with_layout(size: Size, default_mode: ModeInfo, layout: &str)
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
let pane_ids = tab_layout let pane_ids = tab_layout
.extract_run_instructions() .extract_run_instructions()
@ -613,10 +643,13 @@ fn create_new_tab_with_mock_pty_writer(
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -645,6 +678,9 @@ fn create_new_tab_with_mock_pty_writer(
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),
@ -688,10 +724,13 @@ fn create_new_tab_with_sixel_support(
let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default())); let terminal_emulator_colors = Rc::new(RefCell::new(Palette::default()));
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let current_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -720,6 +759,9 @@ fn create_new_tab_with_sixel_support(
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),

View file

@ -165,10 +165,13 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab {
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let current_pane_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -197,6 +200,9 @@ fn create_new_tab(size: Size, stacked_resize: bool) -> Tab {
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_pane_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),
@ -232,10 +238,13 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab {
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let current_pane_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -264,6 +273,9 @@ fn create_new_tab_with_layout(size: Size, layout: TiledPaneLayout) -> Tab {
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_pane_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
let mut new_terminal_ids = vec![]; let mut new_terminal_ids = vec![];
for i in 0..layout.extract_run_instructions().len() { for i in 0..layout.extract_run_instructions().len() {
@ -304,11 +316,14 @@ fn create_new_tab_with_cell_size(
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default())); let sixel_image_store = Rc::new(RefCell::new(SixelImageStore::default()));
let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new())); let terminal_emulator_color_codes = Rc::new(RefCell::new(HashMap::new()));
let stacked_resize = Rc::new(RefCell::new(true));
let current_pane_group = Rc::new(RefCell::new(HashMap::new()));
let currently_marking_pane_group = Rc::new(RefCell::new(HashMap::new()));
let debug = false; let debug = false;
let arrow_fonts = true; let arrow_fonts = true;
let styled_underlines = true; let styled_underlines = true;
let explicitly_disable_kitty_keyboard_protocol = false; let explicitly_disable_kitty_keyboard_protocol = false;
let stacked_resize = Rc::new(RefCell::new(true)); let advanced_mouse_actions = true;
let mut tab = Tab::new( let mut tab = Tab::new(
index, index,
position, position,
@ -337,6 +352,9 @@ fn create_new_tab_with_cell_size(
styled_underlines, styled_underlines,
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
None, None,
current_pane_group,
currently_marking_pane_group,
advanced_mouse_actions,
); );
tab.apply_layout( tab.apply_layout(
TiledPaneLayout::default(), TiledPaneLayout::default(),

View file

@ -33,7 +33,7 @@ pub type BoundaryType = &'static str; // easy way to refer to boundary_type abov
pub struct BoundarySymbol { pub struct BoundarySymbol {
boundary_type: BoundaryType, boundary_type: BoundaryType,
invisible: bool, invisible: bool,
color: Option<PaletteColor>, color: Option<(PaletteColor, usize)>, // (color, color_precedence)
} }
impl BoundarySymbol { impl BoundarySymbol {
@ -41,10 +41,10 @@ impl BoundarySymbol {
BoundarySymbol { BoundarySymbol {
boundary_type, boundary_type,
invisible: false, invisible: false,
color: Some(PaletteColor::EightBit(colors::GRAY)), color: Some((PaletteColor::EightBit(colors::GRAY), 0)),
} }
} }
pub fn color(&mut self, color: Option<PaletteColor>) -> Self { pub fn color(&mut self, color: Option<(PaletteColor, usize)>) -> Self {
self.color = color; self.color = color;
*self *self
} }
@ -66,7 +66,7 @@ impl BoundarySymbol {
TerminalCharacter::new_singlewidth_styled( TerminalCharacter::new_singlewidth_styled(
character, character,
RESET_STYLES RESET_STYLES
.foreground(self.color.map(|palette_color| palette_color.into())) .foreground(self.color.map(|palette_color| palette_color.0.into()))
.into(), .into(),
) )
}; };
@ -79,7 +79,7 @@ impl Display for BoundarySymbol {
match self.invisible { match self.invisible {
true => write!(f, " "), true => write!(f, " "),
false => match self.color { false => match self.color {
Some(color) => match color { Some(color) => match color.0 {
PaletteColor::Rgb((r, g, b)) => { PaletteColor::Rgb((r, g, b)) => {
write!(f, "{}", RGB(r, g, b).paint(self.boundary_type)) write!(f, "{}", RGB(r, g, b).paint(self.boundary_type))
}, },
@ -99,7 +99,17 @@ fn combine_symbols(
) -> Option<BoundarySymbol> { ) -> Option<BoundarySymbol> {
use boundary_type::*; use boundary_type::*;
let invisible = current_symbol.invisible || next_symbol.invisible; let invisible = current_symbol.invisible || next_symbol.invisible;
let color = current_symbol.color.or(next_symbol.color); let color = match (current_symbol.color, next_symbol.color) {
(Some(current_symbol_color), Some(next_symbol_color)) => {
let ret = if current_symbol_color.1 >= next_symbol_color.1 {
Some(current_symbol_color)
} else {
Some(next_symbol_color)
};
ret
},
_ => current_symbol.color.or(next_symbol.color),
};
match (current_symbol.boundary_type, next_symbol.boundary_type) { match (current_symbol.boundary_type, next_symbol.boundary_type) {
(CROSS, _) | (_, CROSS) => { (CROSS, _) | (_, CROSS) => {
// (┼, *) or (*, ┼) => Some(┼) // (┼, *) or (*, ┼) => Some(┼)
@ -447,7 +457,7 @@ impl Boundaries {
pub fn add_rect( pub fn add_rect(
&mut self, &mut self,
rect: &dyn Pane, rect: &dyn Pane,
color: Option<PaletteColor>, color: Option<(PaletteColor, usize)>, // (color, color_precedence)
pane_is_on_top_of_stack: bool, pane_is_on_top_of_stack: bool,
pane_is_on_bottom_of_stack: bool, pane_is_on_bottom_of_stack: bool,
pane_is_stacked_under: bool, pane_is_stacked_under: bool,

View file

@ -59,13 +59,13 @@ pub fn stringify_text(
} }
text_width += character_width; text_width += character_width;
if text.selected { if text.selected || text.opaque {
// we do this so that selected text will appear selected // we do this so that selected text will appear selected
// even if it does not have color indices // even if it does not have color indices
stringified.push_str(&format!("{}", base_text_style)); stringified.push_str(&format!("{}", base_text_style));
} }
if !text.indices.is_empty() || text.selected { if !text.indices.is_empty() || text.selected || text.opaque {
let character_with_styling = let character_with_styling =
color_index_character(character, i, &text, style, base_text_style); color_index_character(character, i, &text, style, base_text_style);
stringified.push_str(&character_with_styling); stringified.push_str(&character_with_styling);

View file

@ -53,7 +53,7 @@ enum ExitStatus {
pub struct FrameParams { pub struct FrameParams {
pub focused_client: Option<ClientId>, pub focused_client: Option<ClientId>,
pub is_main_client: bool, pub is_main_client: bool, // more accurately: is_focused_for_main_client
pub other_focused_clients: Vec<ClientId>, pub other_focused_clients: Vec<ClientId>,
pub style: Style, pub style: Style,
pub color: Option<PaletteColor>, pub color: Option<PaletteColor>,
@ -63,6 +63,7 @@ pub struct FrameParams {
pub should_draw_pane_frames: bool, pub should_draw_pane_frames: bool,
pub pane_is_floating: bool, pub pane_is_floating: bool,
pub content_offset: Offset, pub content_offset: Offset,
pub mouse_is_hovering_over_pane: bool,
} }
#[derive(Default, PartialEq)] #[derive(Default, PartialEq)]
@ -84,6 +85,7 @@ pub struct PaneFrame {
is_pinned: bool, is_pinned: bool,
is_floating: bool, is_floating: bool,
content_offset: Offset, content_offset: Offset,
mouse_is_hovering_over_pane: bool,
} }
impl PaneFrame { impl PaneFrame {
@ -111,6 +113,7 @@ impl PaneFrame {
is_pinned: false, is_pinned: false,
is_floating: frame_params.pane_is_floating, is_floating: frame_params.pane_is_floating,
content_offset: frame_params.content_offset, content_offset: frame_params.content_offset,
mouse_is_hovering_over_pane: frame_params.mouse_is_hovering_over_pane,
} }
} }
pub fn is_pinned(mut self, is_pinned: bool) -> Self { pub fn is_pinned(mut self, is_pinned: bool) -> Self {
@ -755,6 +758,34 @@ impl PaneFrame {
}; };
Ok(res) Ok(res)
} }
fn render_mouse_shortcuts_undertitle(&self) -> Result<Vec<TerminalCharacter>> {
let max_undertitle_length = self.geom.cols.saturating_sub(2); // 2 for the left and right corners
let mut left_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);
let mut right_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_RIGHT), self.color);
let res = if self.is_main_client {
self.empty_undertitle(max_undertitle_length)
} else {
let (mut hover_shortcuts, hover_shortcuts_len) = self.hover_shortcuts_part_full();
if hover_shortcuts_len <= max_undertitle_length {
// render exit status and tips
let mut padding = String::new();
for _ in hover_shortcuts_len..max_undertitle_length {
padding.push_str(boundary_type::HORIZONTAL);
}
let mut ret = vec![];
ret.append(&mut left_boundary);
ret.append(&mut hover_shortcuts);
ret.append(&mut foreground_color(&padding, self.color));
ret.append(&mut right_boundary);
ret
} else {
self.empty_undertitle(max_undertitle_length)
}
};
Ok(res)
}
pub fn clicked_on_pinned(&mut self, position: Position) -> bool { pub fn clicked_on_pinned(&mut self, position: Position) -> bool {
if self.is_floating { if self.is_floating {
// TODO: this is not entirely accurate because our relative position calculation in // TODO: this is not entirely accurate because our relative position calculation in
@ -810,7 +841,16 @@ impl PaneFrame {
character_chunks.push(CharacterChunk::new(title, x, y)); character_chunks.push(CharacterChunk::new(title, x, y));
} else if row == self.geom.rows - 1 { } else if row == self.geom.rows - 1 {
// bottom row // bottom row
if self.exit_status.is_some() || self.is_first_run { if self.mouse_is_hovering_over_pane && !self.is_main_client {
let x = self.geom.x;
let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new(
self.render_mouse_shortcuts_undertitle()
.with_context(err_context)?,
x,
y,
));
} else if self.exit_status.is_some() || self.is_first_run {
let x = self.geom.x; let x = self.geom.x;
let y = self.geom.y + row; let y = self.geom.y + row;
character_chunks.push(CharacterChunk::new( character_chunks.push(CharacterChunk::new(
@ -964,6 +1004,26 @@ impl PaneFrame {
+ break_tip.len(), + break_tip.len(),
) )
} }
fn hover_shortcuts_part_full(&self) -> (Vec<TerminalCharacter>, usize) {
// (title part, length)
let mut hover_shortcuts = vec![];
let alt_click_text = " Alt <Click>";
let alt_click_tip = " - group,";
let alt_right_click_text = " Alt <Right-Click>";
let alt_right_click_tip = " - ungroup all ";
hover_shortcuts.append(&mut foreground_color(alt_click_text, self.color));
hover_shortcuts.append(&mut foreground_color(alt_click_tip, self.color));
hover_shortcuts.append(&mut foreground_color(alt_right_click_text, self.color));
hover_shortcuts.append(&mut foreground_color(alt_right_click_tip, self.color));
(
hover_shortcuts,
alt_click_text.chars().count()
+ alt_click_tip.chars().count()
+ alt_right_click_text.chars().count()
+ alt_right_click_tip.chars().count(),
)
}
fn empty_undertitle(&self, max_undertitle_length: usize) -> Vec<TerminalCharacter> { fn empty_undertitle(&self, max_undertitle_length: usize) -> Vec<TerminalCharacter> {
let mut left_boundary = let mut left_boundary =
foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color); foreground_color(self.get_corner(boundary_type::BOTTOM_LEFT), self.color);

View file

@ -4,7 +4,7 @@ use crate::tab::Pane;
use crate::ui::boundaries::Boundaries; use crate::ui::boundaries::Boundaries;
use crate::ui::pane_boundaries_frame::FrameParams; use crate::ui::pane_boundaries_frame::FrameParams;
use crate::ClientId; use crate::ClientId;
use std::collections::HashMap; use std::collections::{HashMap, HashSet};
use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style}; use zellij_utils::data::{client_id_to_colors, InputMode, PaletteColor, Style};
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
pub struct PaneContentsAndUi<'a> { pub struct PaneContentsAndUi<'a> {
@ -17,6 +17,8 @@ pub struct PaneContentsAndUi<'a> {
pane_is_stacked_under: bool, pane_is_stacked_under: bool,
pane_is_stacked_over: bool, pane_is_stacked_over: bool,
should_draw_pane_frames: bool, should_draw_pane_frames: bool,
mouse_is_hovering_over_pane_for_clients: HashSet<ClientId>,
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
} }
impl<'a> PaneContentsAndUi<'a> { impl<'a> PaneContentsAndUi<'a> {
@ -30,6 +32,8 @@ impl<'a> PaneContentsAndUi<'a> {
pane_is_stacked_under: bool, pane_is_stacked_under: bool,
pane_is_stacked_over: bool, pane_is_stacked_over: bool,
should_draw_pane_frames: bool, should_draw_pane_frames: bool,
mouse_hover_pane_id: &HashMap<ClientId, PaneId>,
current_pane_group: HashMap<ClientId, Vec<PaneId>>,
) -> Self { ) -> Self {
let mut focused_clients: Vec<ClientId> = active_panes let mut focused_clients: Vec<ClientId> = active_panes
.iter() .iter()
@ -37,6 +41,16 @@ impl<'a> PaneContentsAndUi<'a> {
.map(|(c_id, _p_id)| *c_id) .map(|(c_id, _p_id)| *c_id)
.collect(); .collect();
focused_clients.sort_unstable(); focused_clients.sort_unstable();
let mouse_is_hovering_over_pane_for_clients = mouse_hover_pane_id
.iter()
.filter_map(|(client_id, pane_id)| {
if pane_id == &pane.pid() {
Some(*client_id)
} else {
None
}
})
.collect();
PaneContentsAndUi { PaneContentsAndUi {
pane, pane,
output, output,
@ -47,6 +61,8 @@ impl<'a> PaneContentsAndUi<'a> {
pane_is_stacked_under, pane_is_stacked_under,
pane_is_stacked_over, pane_is_stacked_over,
should_draw_pane_frames, should_draw_pane_frames,
mouse_is_hovering_over_pane_for_clients,
current_pane_group,
} }
} }
pub fn render_pane_contents_to_multiple_clients( pub fn render_pane_contents_to_multiple_clients(
@ -217,13 +233,16 @@ impl<'a> PaneContentsAndUi<'a> {
is_main_client: pane_focused_for_client_id, is_main_client: pane_focused_for_client_id,
other_focused_clients: vec![], other_focused_clients: vec![],
style: self.style, style: self.style,
color: frame_color, color: frame_color.map(|c| c.0),
other_cursors_exist_in_session: false, other_cursors_exist_in_session: false,
pane_is_stacked_over: self.pane_is_stacked_over, pane_is_stacked_over: self.pane_is_stacked_over,
pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames, should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating, pane_is_floating,
content_offset: self.pane.get_content_offset(), content_offset: self.pane.get_content_offset(),
mouse_is_hovering_over_pane: self
.mouse_is_hovering_over_pane_for_clients
.contains(&client_id),
} }
} else { } else {
FrameParams { FrameParams {
@ -231,13 +250,16 @@ impl<'a> PaneContentsAndUi<'a> {
is_main_client: pane_focused_for_client_id, is_main_client: pane_focused_for_client_id,
other_focused_clients, other_focused_clients,
style: self.style, style: self.style,
color: frame_color, color: frame_color.map(|c| c.0),
other_cursors_exist_in_session: self.multiple_users_exist_in_session, other_cursors_exist_in_session: self.multiple_users_exist_in_session,
pane_is_stacked_over: self.pane_is_stacked_over, pane_is_stacked_over: self.pane_is_stacked_over,
pane_is_stacked_under: self.pane_is_stacked_under, pane_is_stacked_under: self.pane_is_stacked_under,
should_draw_pane_frames: self.should_draw_pane_frames, should_draw_pane_frames: self.should_draw_pane_frames,
pane_is_floating, pane_is_floating,
content_offset: self.pane.get_content_offset(), content_offset: self.pane.get_content_offset(),
mouse_is_hovering_over_pane: self
.mouse_is_hovering_over_pane_for_clients
.contains(&client_id),
} }
}; };
@ -279,27 +301,48 @@ impl<'a> PaneContentsAndUi<'a> {
client_id: ClientId, client_id: ClientId,
mode: InputMode, mode: InputMode,
session_is_mirrored: bool, session_is_mirrored: bool,
) -> Option<PaletteColor> { ) -> Option<(PaletteColor, usize)> {
// (color, color_precedence) (the color_precedence is used
// for the no-pane-frames mode)
let pane_focused_for_client_id = self.focused_clients.contains(&client_id); let pane_focused_for_client_id = self.focused_clients.contains(&client_id);
if let Some(override_color) = self.pane.frame_color_override() { let pane_is_in_group = self
Some(override_color) .current_pane_group
.get(&client_id)
.map(|p| p.contains(&self.pane.pid()))
.unwrap_or(false);
if self.pane.frame_color_override().is_some() && !pane_is_in_group {
self.pane
.frame_color_override()
.map(|override_color| (override_color, 4))
} else if pane_is_in_group && !pane_focused_for_client_id {
Some((self.style.colors.frame_highlight.emphasis_0, 2))
} else if pane_is_in_group && pane_focused_for_client_id {
Some((self.style.colors.frame_highlight.emphasis_1, 3))
} else if pane_focused_for_client_id { } else if pane_focused_for_client_id {
match mode { match mode {
InputMode::Normal | InputMode::Locked => { InputMode::Normal | InputMode::Locked => {
if session_is_mirrored || !self.multiple_users_exist_in_session { if session_is_mirrored || !self.multiple_users_exist_in_session {
Some(self.style.colors.frame_selected.base) Some((self.style.colors.frame_selected.base, 3))
} else { } else {
let colors = client_id_to_colors( let colors = client_id_to_colors(
client_id, client_id,
self.style.colors.multiplayer_user_colors, self.style.colors.multiplayer_user_colors,
); );
colors.map(|colors| colors.0) colors.map(|colors| (colors.0, 3))
} }
}, },
_ => Some(self.style.colors.frame_highlight.base), _ => Some((self.style.colors.frame_highlight.base, 3)),
} }
} else if self
.mouse_is_hovering_over_pane_for_clients
.contains(&client_id)
{
Some((self.style.colors.frame_highlight.base, 1))
} else { } else {
self.style.colors.frame_unselected.map(|frame| frame.base) self.style
.colors
.frame_unselected
.map(|frame| (frame.base, 0))
} }
} }
} }

View file

@ -19,9 +19,11 @@ use zellij_utils::input::layout::{
FloatingPaneLayout, Layout, PluginAlias, PluginUserConfiguration, Run, RunPlugin, FloatingPaneLayout, Layout, PluginAlias, PluginUserConfiguration, Run, RunPlugin,
RunPluginLocation, RunPluginOrAlias, SplitDirection, SplitSize, TiledPaneLayout, RunPluginLocation, RunPluginOrAlias, SplitDirection, SplitSize, TiledPaneLayout,
}; };
use zellij_utils::input::mouse::MouseEvent;
use zellij_utils::input::options::Options; use zellij_utils::input::options::Options;
use zellij_utils::ipc::IpcReceiverWithContext; use zellij_utils::ipc::IpcReceiverWithContext;
use zellij_utils::pane_size::{Size, SizeInPixels}; use zellij_utils::pane_size::{Size, SizeInPixels};
use zellij_utils::position::Position;
use crate::background_jobs::BackgroundJob; use crate::background_jobs::BackgroundJob;
use crate::pty_writer::PtyWriteInstruction; use crate::pty_writer::PtyWriteInstruction;
@ -243,7 +245,7 @@ impl ServerOsApi for FakeInputOutput {
} }
} }
fn create_new_screen(size: Size) -> Screen { fn create_new_screen(size: Size, advanced_mouse_actions: bool) -> Screen {
let mut bus: Bus<ScreenInstruction> = Bus::empty(); let mut bus: Bus<ScreenInstruction> = Bus::empty();
let fake_os_input = FakeInputOutput::default(); let fake_os_input = FakeInputOutput::default();
bus.os_input = Some(Box::new(fake_os_input)); bus.os_input = Some(Box::new(fake_os_input));
@ -293,6 +295,7 @@ fn create_new_screen(size: Size) -> Screen {
explicitly_disable_kitty_keyboard_protocol, explicitly_disable_kitty_keyboard_protocol,
stacked_resize, stacked_resize,
None, None,
advanced_mouse_actions,
); );
screen screen
} }
@ -316,6 +319,7 @@ struct MockScreen {
pub config_options: Options, pub config_options: Options,
pub session_metadata: SessionMetaData, pub session_metadata: SessionMetaData,
pub config: Config, pub config: Config,
advanced_mouse_actions: bool,
last_opened_tab_index: Option<usize>, last_opened_tab_index: Option<usize>,
} }
@ -325,7 +329,8 @@ impl MockScreen {
initial_layout: Option<TiledPaneLayout>, initial_layout: Option<TiledPaneLayout>,
initial_floating_panes_layout: Vec<FloatingPaneLayout>, initial_floating_panes_layout: Vec<FloatingPaneLayout>,
) -> std::thread::JoinHandle<()> { ) -> std::thread::JoinHandle<()> {
let config = self.config.clone(); let mut config = self.config.clone();
config.options.advanced_mouse_actions = Some(self.advanced_mouse_actions);
let client_attributes = self.client_attributes.clone(); let client_attributes = self.client_attributes.clone();
let screen_bus = Bus::new( let screen_bus = Bus::new(
vec![self.screen_receiver.take().unwrap()], vec![self.screen_receiver.take().unwrap()],
@ -626,8 +631,12 @@ impl MockScreen {
session_metadata, session_metadata,
last_opened_tab_index: None, last_opened_tab_index: None,
config: Config::default(), config: Config::default(),
advanced_mouse_actions: true,
} }
} }
pub fn set_advanced_hover_effects(&mut self, advanced_mouse_actions: bool) {
self.advanced_mouse_actions = advanced_mouse_actions;
}
} }
macro_rules! log_actions_in_thread { macro_rules! log_actions_in_thread {
@ -682,7 +691,7 @@ fn open_new_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1); new_tab(&mut screen, 2, 1);
@ -701,7 +710,7 @@ pub fn switch_to_prev_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -720,7 +729,7 @@ pub fn switch_to_next_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -740,7 +749,7 @@ pub fn switch_to_tab_name() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -774,7 +783,7 @@ pub fn close_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -794,7 +803,7 @@ pub fn close_the_middle_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -816,7 +825,7 @@ fn move_focus_left_at_left_screen_edge_changes_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -848,10 +857,13 @@ fn basic_move_of_active_tab_to_left() {
} }
fn create_fixed_size_screen() -> Screen { fn create_fixed_size_screen() -> Screen {
create_new_screen(Size { create_new_screen(
Size {
cols: 121, cols: 121,
rows: 20, rows: 20,
}) },
true,
)
} }
#[test] #[test]
@ -981,7 +993,7 @@ fn move_focus_right_at_right_screen_edge_changes_tab() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -1002,7 +1014,7 @@ pub fn toggle_to_previous_tab_simple() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(position_and_size); let mut screen = create_new_screen(position_and_size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
new_tab(&mut screen, 2, 2); new_tab(&mut screen, 2, 2);
@ -1030,7 +1042,7 @@ pub fn toggle_to_previous_tab_create_tabs_only() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(position_and_size); let mut screen = create_new_screen(position_and_size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1); new_tab(&mut screen, 2, 1);
@ -1080,7 +1092,7 @@ pub fn toggle_to_previous_tab_delete() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(position_and_size); let mut screen = create_new_screen(position_and_size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1); new_tab(&mut screen, 2, 1);
@ -1176,7 +1188,7 @@ fn switch_to_tab_with_fullscreen() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 1); new_tab(&mut screen, 1, 1);
{ {
@ -1212,7 +1224,7 @@ fn update_screen_pixel_dimensions() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
let initial_pixel_dimensions = screen.pixel_dimensions; let initial_pixel_dimensions = screen.pixel_dimensions;
screen.update_pixel_dimensions(PixelDimensions { screen.update_pixel_dimensions(PixelDimensions {
character_cell_size: Some(SizeInPixels { character_cell_size: Some(SizeInPixels {
@ -1291,7 +1303,7 @@ fn attach_after_first_tab_closed() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
{ {
@ -1314,7 +1326,7 @@ fn open_new_floating_pane_with_custom_coordinates() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
let active_tab = screen.get_active_tab_mut(1).unwrap(); let active_tab = screen.get_active_tab_mut(1).unwrap();
@ -1349,7 +1361,7 @@ fn open_new_floating_pane_with_custom_coordinates_exceeding_viewport() {
cols: 121, cols: 121,
rows: 20, rows: 20,
}; };
let mut screen = create_new_screen(size); let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0); new_tab(&mut screen, 1, 0);
let active_tab = screen.get_active_tab_mut(1).unwrap(); let active_tab = screen.get_active_tab_mut(1).unwrap();
@ -1378,6 +1390,257 @@ fn open_new_floating_pane_with_custom_coordinates_exceeding_viewport() {
assert_eq!(active_pane.cols(), 10, "columns set properly"); assert_eq!(active_pane.cols(), 10, "columns set properly");
} }
#[test]
pub fn mouse_hover_effect() {
let size = Size {
cols: 130,
rows: 20,
};
let client_id = 1;
let mut initial_layout = TiledPaneLayout::default();
initial_layout.children_split_direction = SplitDirection::Vertical;
initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()];
let mut mock_screen = MockScreen::new(size);
let screen_thread = mock_screen.run(Some(initial_layout), vec![]);
let received_server_instructions = Arc::new(Mutex::new(vec![]));
let server_receiver = mock_screen.server_receiver.take().unwrap();
let server_thread = log_actions_in_thread!(
received_server_instructions,
ServerInstruction::KillSession,
server_receiver
);
let hover_mouse_event_1 = MouseEvent::new_buttonless_motion(Position::new(5, 70));
let _ = mock_screen.to_screen.send(ScreenInstruction::MouseEvent(
hover_mouse_event_1,
client_id,
));
std::thread::sleep(std::time::Duration::from_millis(100));
mock_screen.teardown(vec![server_thread, screen_thread]);
let snapshots = take_snapshots_and_cursor_coordinates_from_render_events(
received_server_instructions.lock().unwrap().iter(),
size,
);
for (_cursor_coordinates, snapshot) in snapshots {
assert_snapshot!(format!("{}", snapshot));
}
}
#[test]
pub fn disabled_mouse_hover_effect() {
let size = Size {
cols: 130,
rows: 20,
};
let client_id = 1;
let mut initial_layout = TiledPaneLayout::default();
initial_layout.children_split_direction = SplitDirection::Vertical;
initial_layout.children = vec![TiledPaneLayout::default(), TiledPaneLayout::default()];
let mut mock_screen = MockScreen::new(size);
mock_screen.set_advanced_hover_effects(false);
let screen_thread = mock_screen.run(Some(initial_layout), vec![]);
let received_server_instructions = Arc::new(Mutex::new(vec![]));
let server_receiver = mock_screen.server_receiver.take().unwrap();
let server_thread = log_actions_in_thread!(
received_server_instructions,
ServerInstruction::KillSession,
server_receiver
);
let hover_mouse_event_1 = MouseEvent::new_buttonless_motion(Position::new(5, 70));
let _ = mock_screen.to_screen.send(ScreenInstruction::MouseEvent(
hover_mouse_event_1,
client_id,
));
std::thread::sleep(std::time::Duration::from_millis(100));
mock_screen.teardown(vec![server_thread, screen_thread]);
let snapshots = take_snapshots_and_cursor_coordinates_from_render_events(
received_server_instructions.lock().unwrap().iter(),
size,
);
for (_cursor_coordinates, snapshot) in snapshots {
assert_snapshot!(format!("{}", snapshot));
}
}
#[test]
fn group_panes_with_mouse() {
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
screen.handle_mouse_event(
MouseEvent::new_left_press_with_alt_event(Position::new(2, 80)),
client_id,
);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![PaneId::Terminal(2)]),
"Pane Id added to client's pane group"
);
screen.handle_mouse_event(
MouseEvent::new_left_press_with_alt_event(Position::new(2, 80)),
client_id,
);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![]),
"Pane Id removed from client's pane group"
);
}
#[test]
fn group_panes_with_keyboard() {
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0);
new_tab(&mut screen, 2, 1);
let _ = screen.toggle_pane_in_group(client_id);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![PaneId::Terminal(2)]),
"Pane Id added to client's pane group"
);
let _ = screen.toggle_pane_in_group(client_id);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![]),
"Pane Id removed from client's pane group"
);
}
#[test]
fn group_panes_following_focus() {
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0);
{
let active_tab = screen.get_active_tab_mut(client_id).unwrap();
let should_float = Some(false);
for i in 2..5 {
active_tab
.new_pane(
PaneId::Terminal(i),
None,
should_float,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
}
{
screen.toggle_group_marking(client_id).unwrap();
screen
.get_active_tab_mut(client_id)
.unwrap()
.move_focus_up(client_id)
.unwrap();
screen.add_active_pane_to_group_if_marking(&client_id);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![PaneId::Terminal(4), PaneId::Terminal(3)]),
"Pane Id of focused pane and newly focused pane above added to pane group"
);
}
{
let _ = screen.toggle_group_marking(client_id);
screen
.get_active_tab_mut(client_id)
.unwrap()
.move_focus_up(client_id)
.unwrap();
let _ = screen.add_active_pane_to_group_if_marking(&client_id);
assert_eq!(screen.current_pane_group.borrow().get(&client_id), Some(&vec![PaneId::Terminal(4), PaneId::Terminal(3)]), "Pane Id of newly focused pane not added to group after the group marking was toggled off");
}
}
#[test]
fn break_group_with_mouse() {
let size = Size {
cols: 121,
rows: 20,
};
let client_id = 1;
let mut screen = create_new_screen(size, true);
new_tab(&mut screen, 1, 0);
{
let active_tab = screen.get_active_tab_mut(client_id).unwrap();
let should_float = Some(false);
for i in 2..5 {
active_tab
.new_pane(
PaneId::Terminal(i),
None,
should_float,
None,
None,
false,
Some(client_id),
)
.unwrap();
}
}
{
screen.toggle_group_marking(client_id).unwrap();
screen
.get_active_tab_mut(client_id)
.unwrap()
.move_focus_up(client_id)
.unwrap();
screen.add_active_pane_to_group_if_marking(&client_id);
screen
.get_active_tab_mut(client_id)
.unwrap()
.move_focus_up(client_id)
.unwrap();
screen.add_active_pane_to_group_if_marking(&client_id);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![
PaneId::Terminal(4),
PaneId::Terminal(3),
PaneId::Terminal(2)
]),
"Group contains 3 panes"
);
}
screen.handle_mouse_event(
MouseEvent::new_right_press_with_alt_event(Position::new(2, 80)),
client_id,
);
assert_eq!(
screen.current_pane_group.borrow().get(&client_id),
Some(&vec![]),
"Group cleared by mouse event"
);
}
// Following are tests for sending CLI actions // Following are tests for sending CLI actions
// these tests are only partially relevant to Screen // these tests are only partially relevant to Screen
// and are included here for two reasons: // and are included here for two reasons:

View file

@ -0,0 +1,24 @@
---
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────┐┌ Pane #2 ──────────────────────────────────────────────────────┐
01 (C): │ ││ │
02 (C): │ ││ │
03 (C): │ ││ │
04 (C): │ ││ │
05 (C): │ ││ │
06 (C): │ ││ │
07 (C): │ ││ │
08 (C): │ ││ │
09 (C): │ ││ │
10 (C): │ ││ │
11 (C): │ ││ │
12 (C): │ ││ │
13 (C): │ ││ │
14 (C): │ ││ │
15 (C): │ ││ │
16 (C): │ ││ │
17 (C): │ ││ │
18 (C): │ ││ │
19 (C): └───────────────────────────────────────────────────────────────┘└───────────────────────────────────────────────────────────────┘

View file

@ -0,0 +1,24 @@
---
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────┐┌ Pane #2 ──────────────────────────────────────────────────────┐
01 (C): │ ││ │
02 (C): │ ││ │
03 (C): │ ││ │
04 (C): │ ││ │
05 (C): │ ││ │
06 (C): │ ││ │
07 (C): │ ││ │
08 (C): │ ││ │
09 (C): │ ││ │
10 (C): │ ││ │
11 (C): │ ││ │
12 (C): │ ││ │
13 (C): │ ││ │
14 (C): │ ││ │
15 (C): │ ││ │
16 (C): │ ││ │
17 (C): │ ││ │
18 (C): │ ││ │
19 (C): └───────────────────────────────────────────────────────────────┘└ Alt <Click> - group, Alt <Right-Click> - ungroup all ─────────┘

View file

@ -0,0 +1,24 @@
---
source: zellij-server/src/./unit/screen_tests.rs
expression: "format!(\"{}\", snapshot)"
---
00 (C): ┌ Pane #1 ──────────────────────────────────────────────────────┐┌ Pane #2 ──────────────────────────────────────────────────────┐
01 (C): │ ││ │
02 (C): │ ││ │
03 (C): │ ││ │
04 (C): │ ││ │
05 (C): │ ││ │
06 (C): │ ││ │
07 (C): │ ││ │
08 (C): │ ││ │
09 (C): │ ││ │
10 (C): │ ││ │
11 (C): │ ││ │
12 (C): │ ││ │
13 (C): │ ││ │
14 (C): │ ││ │
15 (C): │ ││ │
16 (C): │ ││ │
17 (C): │ ││ │
18 (C): │ ││ │
19 (C): └───────────────────────────────────────────────────────────────┘└───────────────────────────────────────────────────────────────┘

View file

@ -1,16 +1,14 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 3687
expression: "format!(\"{}\", snapshot)" expression: "format!(\"{}\", snapshot)"
--- ---
00 (C): ┌ Pane #1 ─────┐┌ Pane #2 ─────────────────────────────────────┐┌ Pane #5 ─────┐ 00 (C): ┌ Pane #1 ─────┐┌ Pane #2 ─────────────────────────────────────┐┌ Pane #5 ─────┐
01 (C): │ │┌ Pane #3 ─────────────────────────────────────┐│ │ 01 (C): │ ││ ││ │
02 (C): │ │┌ Pane #4 ─────────────────────────────────────┐│ │ 02 (C): │ ││ ││ │
03 (C): │ ││ ││ │ 03 (C): │ ││ ││ │
04 (C): │ ││ ││ │ 04 (C): │ ││ ││ │
05 (C): │ ││ ││ │ 05 (C): │ ││ ││ │
06 (C): │ ││ ││ │ 06 (C): │ ││ ││ │
07 (C): │ ││ ││ │ 07 (C): │ │└──────────────────────────────────────────────┘│ │
08 (C): │ ││ ││ │ 08 (C): │ │└ Pane #3 ─────────────────────────────────────┘│ │
09 (C): └──────────────┘└──────────────────────────────────────────────┘└──────────────┘ 09 (C): └──────────────┘└ Pane #4 ─────────────────────────────────────┘└──────────────┘

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 2183
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
--- ---
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), Write([102, 111, 111], 1), ApplyCachedResizes, Exit] [StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ApplyCachedResizes, ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 58, 18, None, None), ResizePty(1, 59, 18, None, None), ResizePty(0, 59, 18, None, None), ResizePty(1, 58, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), Write([102, 111, 111], 1), ApplyCachedResizes, Exit]

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1423
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
--- ---
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), ApplyCachedResizes, Exit] [StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([102, 111, 111], 0), ApplyCachedResizes, Exit]

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-server/src/./unit/screen_tests.rs source: zellij-server/src/./unit/screen_tests.rs
assertion_line: 1397
expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())" expression: "format!(\"{:?}\", *received_pty_instructions.lock().unwrap())"
--- ---
[StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0), ApplyCachedResizes, Exit] [StartCachingResizes, ApplyCachedResizes, StartCachingResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ResizePty(0, 119, 18, None, None), ApplyCachedResizes, ApplyCachedResizes, ApplyCachedResizes, StartCachingResizes, ApplyCachedResizes, StartCachingResizes, Write([105, 110, 112, 117, 116, 32, 102, 114, 111, 109, 32, 116, 104, 101, 32, 99, 108, 105], 0), ApplyCachedResizes, Exit]

View file

@ -1273,6 +1273,46 @@ pub fn change_floating_panes_coordinates(
unsafe { host_run_plugin_command() }; unsafe { host_run_plugin_command() };
} }
pub fn group_and_ungroup_panes(pane_ids_to_group: Vec<PaneId>, pane_ids_to_ungroup: Vec<PaneId>) {
let plugin_command =
PluginCommand::GroupAndUngroupPanes(pane_ids_to_group, pane_ids_to_ungroup);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
pub fn highlight_and_unhighlight_panes(
pane_ids_to_highlight: Vec<PaneId>,
pane_ids_to_unhighlight: Vec<PaneId>,
) {
let plugin_command =
PluginCommand::HighlightAndUnhighlightPanes(pane_ids_to_highlight, pane_ids_to_unhighlight);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
pub fn close_multiple_panes(pane_ids: Vec<PaneId>) {
let plugin_command = PluginCommand::CloseMultiplePanes(pane_ids);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
pub fn float_multiple_panes(pane_ids: Vec<PaneId>) {
let plugin_command = PluginCommand::FloatMultiplePanes(pane_ids);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
pub fn embed_multiple_panes(pane_ids: Vec<PaneId>) {
let plugin_command = PluginCommand::EmbedMultiplePanes(pane_ids);
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -84,14 +84,14 @@ impl Text {
let mut prefix = "".to_owned(); let mut prefix = "".to_owned();
if self.opaque {
prefix = format!("z{}", prefix);
}
if self.selected { if self.selected {
prefix = format!("x{}", prefix); prefix = format!("x{}", prefix);
} }
if self.opaque {
prefix = format!("z{}", prefix);
}
format!("{}{}{}", prefix, indices, text) format!("{}{}{}", prefix, indices, text)
} }
pub fn len(&self) -> usize { pub fn len(&self) -> usize {

View file

@ -181,6 +181,14 @@ keybinds {
bind "Alt -" { Resize "Decrease"; } bind "Alt -" { Resize "Decrease"; }
bind "Alt [" { PreviousSwapLayout; } bind "Alt [" { PreviousSwapLayout; }
bind "Alt ]" { NextSwapLayout; } bind "Alt ]" { NextSwapLayout; }
bind "Alt m" {
LaunchOrFocusPlugin "zellij:multiple-select" {
floating true
move_to_focused_tab true
}
}
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
} }
shared_except "normal" "locked" { shared_except "normal" "locked" {
bind "Enter" "Esc" { SwitchToMode "Normal"; } bind "Enter" "Esc" { SwitchToMode "Normal"; }
@ -427,7 +435,7 @@ load_plugins {
// //
// show_release_notes false // show_release_notes false
// Whether to show startup tips on session start // Whether to enable mouse hover effects and pane grouping functionality
// Default: true // Default: true
// //
// show_startup_tips false // advanced_mouse_actions false

Binary file not shown.

View file

@ -3,10 +3,7 @@
pub struct Action { pub struct Action {
#[prost(enumeration="ActionName", tag="1")] #[prost(enumeration="ActionName", tag="1")]
pub name: i32, pub name: i32,
#[prost( #[prost(oneof="action::OptionalPayload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49")]
oneof = "action::OptionalPayload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49"
)]
pub optional_payload: ::core::option::Option<action::OptionalPayload>, pub optional_payload: ::core::option::Option<action::OptionalPayload>,
} }
/// Nested message and enum types in `Action`. /// Nested message and enum types in `Action`.
@ -476,6 +473,8 @@ pub enum ActionName {
KeybindPipe = 84, KeybindPipe = 84,
TogglePanePinned = 85, TogglePanePinned = 85,
MouseEvent = 86, MouseEvent = 86,
TogglePaneInGroup = 87,
ToggleGroupMarking = 88,
} }
impl ActionName { impl ActionName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -568,6 +567,8 @@ impl ActionName {
ActionName::KeybindPipe => "KeybindPipe", ActionName::KeybindPipe => "KeybindPipe",
ActionName::TogglePanePinned => "TogglePanePinned", ActionName::TogglePanePinned => "TogglePanePinned",
ActionName::MouseEvent => "MouseEvent", ActionName::MouseEvent => "MouseEvent",
ActionName::TogglePaneInGroup => "TogglePaneInGroup",
ActionName::ToggleGroupMarking => "ToggleGroupMarking",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -657,6 +658,8 @@ impl ActionName {
"KeybindPipe" => Some(Self::KeybindPipe), "KeybindPipe" => Some(Self::KeybindPipe),
"TogglePanePinned" => Some(Self::TogglePanePinned), "TogglePanePinned" => Some(Self::TogglePanePinned),
"MouseEvent" => Some(Self::MouseEvent), "MouseEvent" => Some(Self::MouseEvent),
"TogglePaneInGroup" => Some(Self::TogglePaneInGroup),
"ToggleGroupMarking" => Some(Self::ToggleGroupMarking),
_ => None, _ => None,
} }
} }

View file

@ -9,10 +9,7 @@ pub struct EventNameList {
pub struct Event { pub struct Event {
#[prost(enumeration="EventType", tag="1")] #[prost(enumeration="EventType", tag="1")]
pub name: i32, pub name: i32,
#[prost( #[prost(oneof="event::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26")]
oneof = "event::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26"
)]
pub payload: ::core::option::Option<event::Payload>, pub payload: ::core::option::Option<event::Payload>,
} }
/// Nested message and enum types in `Event`. /// Nested message and enum types in `Event`.
@ -264,9 +261,7 @@ pub struct MouseEventPayload {
#[prost(enumeration="MouseEventName", tag="1")] #[prost(enumeration="MouseEventName", tag="1")]
pub mouse_event_name: i32, pub mouse_event_name: i32,
#[prost(oneof="mouse_event_payload::MouseEventPayload", tags="2, 3")] #[prost(oneof="mouse_event_payload::MouseEventPayload", tags="2, 3")]
pub mouse_event_payload: ::core::option::Option< pub mouse_event_payload: ::core::option::Option<mouse_event_payload::MouseEventPayload>,
mouse_event_payload::MouseEventPayload,
>,
} }
/// Nested message and enum types in `MouseEventPayload`. /// Nested message and enum types in `MouseEventPayload`.
pub mod mouse_event_payload { pub mod mouse_event_payload {
@ -400,6 +395,16 @@ pub struct PaneInfo {
pub plugin_url: ::core::option::Option<::prost::alloc::string::String>, pub plugin_url: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, tag="22")] #[prost(bool, tag="22")]
pub is_selectable: bool, pub is_selectable: bool,
#[prost(message, repeated, tag="23")]
pub index_in_pane_group: ::prost::alloc::vec::Vec<IndexInPaneGroup>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct IndexInPaneGroup {
#[prost(uint32, tag="1")]
pub client_id: u32,
#[prost(uint32, tag="2")]
pub index: u32,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -456,6 +461,8 @@ pub struct ModeUpdatePayload {
pub editor: ::core::option::Option<::prost::alloc::string::String>, pub editor: ::core::option::Option<::prost::alloc::string::String>,
#[prost(string, optional, tag="8")] #[prost(string, optional, tag="8")]
pub shell: ::core::option::Option<::prost::alloc::string::String>, pub shell: ::core::option::Option<::prost::alloc::string::String>,
#[prost(bool, optional, tag="9")]
pub currently_marking_pane_group: ::core::option::Option<bool>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -522,6 +529,7 @@ pub enum EventType {
FailedToChangeHostFolder = 28, FailedToChangeHostFolder = 28,
PastedText = 29, PastedText = 29,
ConfigWasWrittenToDisk = 30, ConfigWasWrittenToDisk = 30,
BeforeClose = 31,
} }
impl EventType { impl EventType {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -561,6 +569,7 @@ impl EventType {
EventType::FailedToChangeHostFolder => "FailedToChangeHostFolder", EventType::FailedToChangeHostFolder => "FailedToChangeHostFolder",
EventType::PastedText => "PastedText", EventType::PastedText => "PastedText",
EventType::ConfigWasWrittenToDisk => "ConfigWasWrittenToDisk", EventType::ConfigWasWrittenToDisk => "ConfigWasWrittenToDisk",
EventType::BeforeClose => "BeforeClose",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -597,6 +606,7 @@ impl EventType {
"FailedToChangeHostFolder" => Some(Self::FailedToChangeHostFolder), "FailedToChangeHostFolder" => Some(Self::FailedToChangeHostFolder),
"PastedText" => Some(Self::PastedText), "PastedText" => Some(Self::PastedText),
"ConfigWasWrittenToDisk" => Some(Self::ConfigWasWrittenToDisk), "ConfigWasWrittenToDisk" => Some(Self::ConfigWasWrittenToDisk),
"BeforeClose" => Some(Self::BeforeClose),
_ => None, _ => None,
} }
} }

View file

@ -10,17 +10,7 @@ pub struct Key {
} }
/// Nested message and enum types in `Key`. /// Nested message and enum types in `Key`.
pub mod key { pub mod key {
#[derive( #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
::prost::Enumeration
)]
#[repr(i32)] #[repr(i32)]
pub enum KeyModifier { pub enum KeyModifier {
Ctrl = 0, Ctrl = 0,
@ -52,17 +42,7 @@ pub mod key {
} }
} }
} }
#[derive( #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
::prost::Enumeration
)]
#[repr(i32)] #[repr(i32)]
pub enum NamedKey { pub enum NamedKey {
PageDown = 0, PageDown = 0,
@ -178,17 +158,7 @@ pub mod key {
} }
} }
} }
#[derive( #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, ::prost::Enumeration)]
Clone,
Copy,
Debug,
PartialEq,
Eq,
Hash,
PartialOrd,
Ord,
::prost::Enumeration
)]
#[repr(i32)] #[repr(i32)]
pub enum Char { pub enum Char {
A = 0, A = 0,

View file

@ -3,10 +3,7 @@
pub struct PluginCommand { pub struct PluginCommand {
#[prost(enumeration="CommandName", tag="1")] #[prost(enumeration="CommandName", tag="1")]
pub name: i32, pub name: i32,
#[prost( #[prost(oneof="plugin_command::Payload", tags="2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106")]
oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60, 61, 62, 63, 64, 65, 66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90, 91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101"
)]
pub payload: ::core::option::Option<plugin_command::Payload>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
/// Nested message and enum types in `PluginCommand`. /// Nested message and enum types in `PluginCommand`.
@ -139,9 +136,7 @@ pub mod plugin_command {
#[prost(message, tag="72")] #[prost(message, tag="72")]
MovePaneWithPaneIdPayload(super::MovePaneWithPaneIdPayload), MovePaneWithPaneIdPayload(super::MovePaneWithPaneIdPayload),
#[prost(message, tag="73")] #[prost(message, tag="73")]
MovePaneWithPaneIdInDirectionPayload( MovePaneWithPaneIdInDirectionPayload(super::MovePaneWithPaneIdInDirectionPayload),
super::MovePaneWithPaneIdInDirectionPayload,
),
#[prost(message, tag="74")] #[prost(message, tag="74")]
ClearScreenForPaneIdPayload(super::ClearScreenForPaneIdPayload), ClearScreenForPaneIdPayload(super::ClearScreenForPaneIdPayload),
#[prost(message, tag="75")] #[prost(message, tag="75")]
@ -159,9 +154,7 @@ pub mod plugin_command {
#[prost(message, tag="81")] #[prost(message, tag="81")]
TogglePaneIdFullscreenPayload(super::TogglePaneIdFullscreenPayload), TogglePaneIdFullscreenPayload(super::TogglePaneIdFullscreenPayload),
#[prost(message, tag="82")] #[prost(message, tag="82")]
TogglePaneEmbedOrEjectForPaneIdPayload( TogglePaneEmbedOrEjectForPaneIdPayload(super::TogglePaneEmbedOrEjectForPaneIdPayload),
super::TogglePaneEmbedOrEjectForPaneIdPayload,
),
#[prost(message, tag="83")] #[prost(message, tag="83")]
CloseTabWithIndexPayload(super::CloseTabWithIndexPayload), CloseTabWithIndexPayload(super::CloseTabWithIndexPayload),
#[prost(message, tag="84")] #[prost(message, tag="84")]
@ -181,37 +174,73 @@ pub mod plugin_command {
#[prost(message, tag="91")] #[prost(message, tag="91")]
StackPanesPayload(super::StackPanesPayload), StackPanesPayload(super::StackPanesPayload),
#[prost(message, tag="92")] #[prost(message, tag="92")]
ChangeFloatingPanesCoordinatesPayload( ChangeFloatingPanesCoordinatesPayload(super::ChangeFloatingPanesCoordinatesPayload),
super::ChangeFloatingPanesCoordinatesPayload,
),
#[prost(message, tag="93")] #[prost(message, tag="93")]
OpenCommandPaneNearPluginPayload(super::OpenCommandPaneNearPluginPayload), OpenCommandPaneNearPluginPayload(super::OpenCommandPaneNearPluginPayload),
#[prost(message, tag="94")] #[prost(message, tag="94")]
OpenTerminalNearPluginPayload(super::OpenTerminalNearPluginPayload), OpenTerminalNearPluginPayload(super::OpenTerminalNearPluginPayload),
#[prost(message, tag="95")] #[prost(message, tag="95")]
OpenTerminalFloatingNearPluginPayload( OpenTerminalFloatingNearPluginPayload(super::OpenTerminalFloatingNearPluginPayload),
super::OpenTerminalFloatingNearPluginPayload,
),
#[prost(message, tag="96")] #[prost(message, tag="96")]
OpenTerminalInPlaceOfPluginPayload(super::OpenTerminalInPlaceOfPluginPayload), OpenTerminalInPlaceOfPluginPayload(super::OpenTerminalInPlaceOfPluginPayload),
#[prost(message, tag="97")] #[prost(message, tag="97")]
OpenCommandPaneFloatingNearPluginPayload( OpenCommandPaneFloatingNearPluginPayload(super::OpenCommandPaneFloatingNearPluginPayload),
super::OpenCommandPaneFloatingNearPluginPayload,
),
#[prost(message, tag="98")] #[prost(message, tag="98")]
OpenCommandPaneInPlaceOfPluginPayload( OpenCommandPaneInPlaceOfPluginPayload(super::OpenCommandPaneInPlaceOfPluginPayload),
super::OpenCommandPaneInPlaceOfPluginPayload,
),
#[prost(message, tag="99")] #[prost(message, tag="99")]
OpenFileNearPluginPayload(super::OpenFileNearPluginPayload), OpenFileNearPluginPayload(super::OpenFileNearPluginPayload),
#[prost(message, tag="100")] #[prost(message, tag="100")]
OpenFileFloatingNearPluginPayload(super::OpenFileFloatingNearPluginPayload), OpenFileFloatingNearPluginPayload(super::OpenFileFloatingNearPluginPayload),
#[prost(message, tag="101")] #[prost(message, tag="101")]
OpenFileInPlaceOfPluginPayload(super::OpenFileInPlaceOfPluginPayload), OpenFileInPlaceOfPluginPayload(super::OpenFileInPlaceOfPluginPayload),
#[prost(message, tag="102")]
GroupAndUngroupPanesPayload(super::GroupAndUngroupPanesPayload),
#[prost(message, tag="103")]
HighlightAndUnhighlightPanesPayload(super::HighlightAndUnhighlightPanesPayload),
#[prost(message, tag="104")]
CloseMultiplePanesPayload(super::CloseMultiplePanesPayload),
#[prost(message, tag="105")]
FloatMultiplePanesPayload(super::FloatMultiplePanesPayload),
#[prost(message, tag="106")]
EmbedMultiplePanesPayload(super::EmbedMultiplePanesPayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct EmbedMultiplePanesPayload {
#[prost(message, repeated, tag="1")]
pub pane_ids: ::prost::alloc::vec::Vec<PaneId>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct FloatMultiplePanesPayload {
#[prost(message, repeated, tag="1")]
pub pane_ids: ::prost::alloc::vec::Vec<PaneId>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CloseMultiplePanesPayload {
#[prost(message, repeated, tag="1")]
pub pane_ids: ::prost::alloc::vec::Vec<PaneId>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct HighlightAndUnhighlightPanesPayload {
#[prost(message, repeated, tag="1")]
pub pane_ids_to_highlight: ::prost::alloc::vec::Vec<PaneId>,
#[prost(message, repeated, tag="2")]
pub pane_ids_to_unhighlight: ::prost::alloc::vec::Vec<PaneId>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct GroupAndUngroupPanesPayload {
#[prost(message, repeated, tag="1")]
pub pane_ids_to_group: ::prost::alloc::vec::Vec<PaneId>,
#[prost(message, repeated, tag="2")]
pub pane_ids_to_ungroup: ::prost::alloc::vec::Vec<PaneId>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct OpenFileInPlaceOfPluginPayload { pub struct OpenFileInPlaceOfPluginPayload {
#[prost(message, optional, tag="1")] #[prost(message, optional, tag="1")]
pub file_to_open: ::core::option::Option<super::file::File>, pub file_to_open: ::core::option::Option<super::file::File>,
@ -304,9 +333,7 @@ pub struct OpenCommandPaneNearPluginPayload {
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct ChangeFloatingPanesCoordinatesPayload { pub struct ChangeFloatingPanesCoordinatesPayload {
#[prost(message, repeated, tag="1")] #[prost(message, repeated, tag="1")]
pub pane_ids_and_floating_panes_coordinates: ::prost::alloc::vec::Vec< pub pane_ids_and_floating_panes_coordinates: ::prost::alloc::vec::Vec<PaneIdAndFloatingPaneCoordinates>,
PaneIdAndFloatingPaneCoordinates,
>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -605,11 +632,7 @@ pub struct SwitchSessionPayload {
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
pub struct RequestPluginPermissionPayload { pub struct RequestPluginPermissionPayload {
#[prost( #[prost(enumeration="super::plugin_permission::PermissionType", repeated, tag="1")]
enumeration = "super::plugin_permission::PermissionType",
repeated,
tag = "1"
)]
pub permissions: ::prost::alloc::vec::Vec<i32>, pub permissions: ::prost::alloc::vec::Vec<i32>,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
@ -891,6 +914,11 @@ pub enum CommandName {
OpenFileNearPlugin = 124, OpenFileNearPlugin = 124,
OpenFileFloatingNearPlugin = 125, OpenFileFloatingNearPlugin = 125,
OpenFileInPlaceOfPlugin = 126, OpenFileInPlaceOfPlugin = 126,
GroupAndUngroupPanes = 127,
HighlightAndUnhighlightPanes = 128,
CloseMultiplePanes = 129,
FloatMultiplePanes = 130,
EmbedMultiplePanes = 131,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -1005,9 +1033,7 @@ impl CommandName {
CommandName::PageScrollUpInPaneId => "PageScrollUpInPaneId", CommandName::PageScrollUpInPaneId => "PageScrollUpInPaneId",
CommandName::PageScrollDownInPaneId => "PageScrollDownInPaneId", CommandName::PageScrollDownInPaneId => "PageScrollDownInPaneId",
CommandName::TogglePaneIdFullscreen => "TogglePaneIdFullscreen", CommandName::TogglePaneIdFullscreen => "TogglePaneIdFullscreen",
CommandName::TogglePaneEmbedOrEjectForPaneId => { CommandName::TogglePaneEmbedOrEjectForPaneId => "TogglePaneEmbedOrEjectForPaneId",
"TogglePaneEmbedOrEjectForPaneId"
}
CommandName::CloseTabWithIndex => "CloseTabWithIndex", CommandName::CloseTabWithIndex => "CloseTabWithIndex",
CommandName::BreakPanesToNewTab => "BreakPanesToNewTab", CommandName::BreakPanesToNewTab => "BreakPanesToNewTab",
CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex", CommandName::BreakPanesToTabWithIndex => "BreakPanesToTabWithIndex",
@ -1018,24 +1044,21 @@ impl CommandName {
CommandName::ChangeHostFolder => "ChangeHostFolder", CommandName::ChangeHostFolder => "ChangeHostFolder",
CommandName::SetFloatingPanePinned => "SetFloatingPanePinned", CommandName::SetFloatingPanePinned => "SetFloatingPanePinned",
CommandName::StackPanes => "StackPanes", CommandName::StackPanes => "StackPanes",
CommandName::ChangeFloatingPanesCoordinates => { CommandName::ChangeFloatingPanesCoordinates => "ChangeFloatingPanesCoordinates",
"ChangeFloatingPanesCoordinates"
}
CommandName::OpenCommandPaneNearPlugin => "OpenCommandPaneNearPlugin", CommandName::OpenCommandPaneNearPlugin => "OpenCommandPaneNearPlugin",
CommandName::OpenTerminalNearPlugin => "OpenTerminalNearPlugin", CommandName::OpenTerminalNearPlugin => "OpenTerminalNearPlugin",
CommandName::OpenTerminalFloatingNearPlugin => { CommandName::OpenTerminalFloatingNearPlugin => "OpenTerminalFloatingNearPlugin",
"OpenTerminalFloatingNearPlugin"
}
CommandName::OpenTerminalInPlaceOfPlugin => "OpenTerminalInPlaceOfPlugin", CommandName::OpenTerminalInPlaceOfPlugin => "OpenTerminalInPlaceOfPlugin",
CommandName::OpenCommandPaneFloatingNearPlugin => { CommandName::OpenCommandPaneFloatingNearPlugin => "OpenCommandPaneFloatingNearPlugin",
"OpenCommandPaneFloatingNearPlugin" CommandName::OpenCommandPaneInPlaceOfPlugin => "OpenCommandPaneInPlaceOfPlugin",
}
CommandName::OpenCommandPaneInPlaceOfPlugin => {
"OpenCommandPaneInPlaceOfPlugin"
}
CommandName::OpenFileNearPlugin => "OpenFileNearPlugin", CommandName::OpenFileNearPlugin => "OpenFileNearPlugin",
CommandName::OpenFileFloatingNearPlugin => "OpenFileFloatingNearPlugin", CommandName::OpenFileFloatingNearPlugin => "OpenFileFloatingNearPlugin",
CommandName::OpenFileInPlaceOfPlugin => "OpenFileInPlaceOfPlugin", CommandName::OpenFileInPlaceOfPlugin => "OpenFileInPlaceOfPlugin",
CommandName::GroupAndUngroupPanes => "GroupAndUngroupPanes",
CommandName::HighlightAndUnhighlightPanes => "HighlightAndUnhighlightPanes",
CommandName::CloseMultiplePanes => "CloseMultiplePanes",
CommandName::FloatMultiplePanes => "FloatMultiplePanes",
CommandName::EmbedMultiplePanes => "EmbedMultiplePanes",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -1147,9 +1170,7 @@ impl CommandName {
"PageScrollUpInPaneId" => Some(Self::PageScrollUpInPaneId), "PageScrollUpInPaneId" => Some(Self::PageScrollUpInPaneId),
"PageScrollDownInPaneId" => Some(Self::PageScrollDownInPaneId), "PageScrollDownInPaneId" => Some(Self::PageScrollDownInPaneId),
"TogglePaneIdFullscreen" => Some(Self::TogglePaneIdFullscreen), "TogglePaneIdFullscreen" => Some(Self::TogglePaneIdFullscreen),
"TogglePaneEmbedOrEjectForPaneId" => { "TogglePaneEmbedOrEjectForPaneId" => Some(Self::TogglePaneEmbedOrEjectForPaneId),
Some(Self::TogglePaneEmbedOrEjectForPaneId)
}
"CloseTabWithIndex" => Some(Self::CloseTabWithIndex), "CloseTabWithIndex" => Some(Self::CloseTabWithIndex),
"BreakPanesToNewTab" => Some(Self::BreakPanesToNewTab), "BreakPanesToNewTab" => Some(Self::BreakPanesToNewTab),
"BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex), "BreakPanesToTabWithIndex" => Some(Self::BreakPanesToTabWithIndex),
@ -1160,24 +1181,21 @@ impl CommandName {
"ChangeHostFolder" => Some(Self::ChangeHostFolder), "ChangeHostFolder" => Some(Self::ChangeHostFolder),
"SetFloatingPanePinned" => Some(Self::SetFloatingPanePinned), "SetFloatingPanePinned" => Some(Self::SetFloatingPanePinned),
"StackPanes" => Some(Self::StackPanes), "StackPanes" => Some(Self::StackPanes),
"ChangeFloatingPanesCoordinates" => { "ChangeFloatingPanesCoordinates" => Some(Self::ChangeFloatingPanesCoordinates),
Some(Self::ChangeFloatingPanesCoordinates)
}
"OpenCommandPaneNearPlugin" => Some(Self::OpenCommandPaneNearPlugin), "OpenCommandPaneNearPlugin" => Some(Self::OpenCommandPaneNearPlugin),
"OpenTerminalNearPlugin" => Some(Self::OpenTerminalNearPlugin), "OpenTerminalNearPlugin" => Some(Self::OpenTerminalNearPlugin),
"OpenTerminalFloatingNearPlugin" => { "OpenTerminalFloatingNearPlugin" => Some(Self::OpenTerminalFloatingNearPlugin),
Some(Self::OpenTerminalFloatingNearPlugin)
}
"OpenTerminalInPlaceOfPlugin" => Some(Self::OpenTerminalInPlaceOfPlugin), "OpenTerminalInPlaceOfPlugin" => Some(Self::OpenTerminalInPlaceOfPlugin),
"OpenCommandPaneFloatingNearPlugin" => { "OpenCommandPaneFloatingNearPlugin" => Some(Self::OpenCommandPaneFloatingNearPlugin),
Some(Self::OpenCommandPaneFloatingNearPlugin) "OpenCommandPaneInPlaceOfPlugin" => Some(Self::OpenCommandPaneInPlaceOfPlugin),
}
"OpenCommandPaneInPlaceOfPlugin" => {
Some(Self::OpenCommandPaneInPlaceOfPlugin)
}
"OpenFileNearPlugin" => Some(Self::OpenFileNearPlugin), "OpenFileNearPlugin" => Some(Self::OpenFileNearPlugin),
"OpenFileFloatingNearPlugin" => Some(Self::OpenFileFloatingNearPlugin), "OpenFileFloatingNearPlugin" => Some(Self::OpenFileFloatingNearPlugin),
"OpenFileInPlaceOfPlugin" => Some(Self::OpenFileInPlaceOfPlugin), "OpenFileInPlaceOfPlugin" => Some(Self::OpenFileInPlaceOfPlugin),
"GroupAndUngroupPanes" => Some(Self::GroupAndUngroupPanes),
"HighlightAndUnhighlightPanes" => Some(Self::HighlightAndUnhighlightPanes),
"CloseMultiplePanes" => Some(Self::CloseMultiplePanes),
"FloatMultiplePanes" => Some(Self::FloatMultiplePanes),
"EmbedMultiplePanes" => Some(Self::EmbedMultiplePanes),
_ => None, _ => None,
} }
} }

View file

@ -7,6 +7,8 @@ pub struct PluginIds {
pub zellij_pid: i32, pub zellij_pid: i32,
#[prost(string, tag="3")] #[prost(string, tag="3")]
pub initial_cwd: ::prost::alloc::string::String, pub initial_cwd: ::prost::alloc::string::String,
#[prost(uint32, tag="4")]
pub client_id: u32,
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]

View file

@ -28,9 +28,7 @@ impl PermissionType {
PermissionType::WriteToStdin => "WriteToStdin", PermissionType::WriteToStdin => "WriteToStdin",
PermissionType::WebAccess => "WebAccess", PermissionType::WebAccess => "WebAccess",
PermissionType::ReadCliPipes => "ReadCliPipes", PermissionType::ReadCliPipes => "ReadCliPipes",
PermissionType::MessageAndLaunchOtherPlugins => { PermissionType::MessageAndLaunchOtherPlugins => "MessageAndLaunchOtherPlugins",
"MessageAndLaunchOtherPlugins"
}
PermissionType::Reconfigure => "Reconfigure", PermissionType::Reconfigure => "Reconfigure",
PermissionType::FullHdAccess => "FullHdAccess", PermissionType::FullHdAccess => "FullHdAccess",
} }

View file

@ -6,7 +6,7 @@ themes {
emphasis_0 254 100 11 emphasis_0 254 100 11
emphasis_1 4 165 229 emphasis_1 4 165 229
emphasis_2 64 160 43 emphasis_2 64 160 43
emphasis_3 234 118 203 emphasis_3 210 15 57
} }
text_selected { text_selected {
base 76 79 105 base 76 79 105
@ -27,7 +27,7 @@ themes {
ribbon_unselected { ribbon_unselected {
base 220 224 232 base 220 224 232
background 92 95 119 background 92 95 119
emphasis_0 210 15 57 emphasis_0 234 118 203
emphasis_1 76 79 105 emphasis_1 76 79 105
emphasis_2 30 102 245 emphasis_2 30 102 245
emphasis_3 234 118 203 emphasis_3 234 118 203
@ -83,8 +83,8 @@ themes {
frame_highlight { frame_highlight {
base 254 100 11 base 254 100 11
background 0 background 0
emphasis_0 254 100 11 emphasis_0 210 15 57
emphasis_1 254 100 11 emphasis_1 136 57 239
emphasis_2 254 100 11 emphasis_2 254 100 11
emphasis_3 254 100 11 emphasis_3 254 100 11
} }

View file

@ -131,6 +131,7 @@ mod not_wasm {
add_plugin!(assets, "configuration.wasm"); add_plugin!(assets, "configuration.wasm");
add_plugin!(assets, "plugin-manager.wasm"); add_plugin!(assets, "plugin-manager.wasm");
add_plugin!(assets, "about.wasm"); add_plugin!(assets, "about.wasm");
add_plugin!(assets, "multiple-select.wasm");
assets assets
}; };
} }

View file

@ -494,6 +494,10 @@ impl KeyWithModifier {
pub fn is_key_with_super_modifier(&self, key: BareKey) -> bool { pub fn is_key_with_super_modifier(&self, key: BareKey) -> bool {
self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Super) self.bare_key == key && self.key_modifiers.contains(&KeyModifier::Super)
} }
pub fn is_cancel_key(&self) -> bool {
// self.bare_key == BareKey::Esc || self.is_key_with_ctrl_modifier(BareKey::Char('c'))
self.bare_key == BareKey::Esc
}
#[cfg(not(target_family = "wasm"))] #[cfg(not(target_family = "wasm"))]
pub fn to_termwiz_modifiers(&self) -> Modifiers { pub fn to_termwiz_modifiers(&self) -> Modifiers {
let mut modifiers = Modifiers::empty(); let mut modifiers = Modifiers::empty();
@ -933,6 +937,7 @@ pub enum Event {
FailedToChangeHostFolder(Option<String>), // String -> the error we got when changing FailedToChangeHostFolder(Option<String>), // String -> the error we got when changing
PastedText(String), PastedText(String),
ConfigWasWrittenToDisk, ConfigWasWrittenToDisk,
BeforeClose,
} }
#[derive( #[derive(
@ -1274,8 +1279,8 @@ pub const DEFAULT_STYLES: Styling = Styling {
}, },
frame_highlight: StyleDeclaration { frame_highlight: StyleDeclaration {
base: PaletteColor::EightBit(default_colors::ORANGE), base: PaletteColor::EightBit(default_colors::ORANGE),
emphasis_0: PaletteColor::EightBit(default_colors::GREEN), emphasis_0: PaletteColor::EightBit(default_colors::MAGENTA),
emphasis_1: PaletteColor::EightBit(default_colors::GREEN), emphasis_1: PaletteColor::EightBit(default_colors::PURPLE),
emphasis_2: PaletteColor::EightBit(default_colors::GREEN), emphasis_2: PaletteColor::EightBit(default_colors::GREEN),
emphasis_3: PaletteColor::EightBit(default_colors::GREEN), emphasis_3: PaletteColor::EightBit(default_colors::GREEN),
background: PaletteColor::EightBit(default_colors::GREEN), background: PaletteColor::EightBit(default_colors::GREEN),
@ -1432,8 +1437,8 @@ impl From<Palette> for Styling {
}, },
frame_highlight: StyleDeclaration { frame_highlight: StyleDeclaration {
base: palette.orange, base: palette.orange,
emphasis_0: palette.orange, emphasis_0: palette.magenta,
emphasis_1: palette.orange, emphasis_1: palette.purple,
emphasis_2: palette.orange, emphasis_2: palette.orange,
emphasis_3: palette.orange, emphasis_3: palette.orange,
background: Default::default(), background: Default::default(),
@ -1508,6 +1513,7 @@ pub struct ModeInfo {
pub session_name: Option<String>, pub session_name: Option<String>,
pub editor: Option<PathBuf>, pub editor: Option<PathBuf>,
pub shell: Option<PathBuf>, pub shell: Option<PathBuf>,
pub currently_marking_pane_group: Option<bool>,
} }
impl ModeInfo { impl ModeInfo {
@ -1733,6 +1739,9 @@ pub struct PaneInfo {
/// Unselectable panes are often used for UI elements that do not have direct user interaction /// Unselectable panes are often used for UI elements that do not have direct user interaction
/// (eg. the default `status-bar` or `tab-bar`). /// (eg. the default `status-bar` or `tab-bar`).
pub is_selectable: bool, pub is_selectable: bool,
/// Grouped panes (usually through an explicit user action) that are staged for a bulk action
/// the index is kept track of in order to preserve the pane group order
pub index_in_pane_group: BTreeMap<ClientId, usize>,
} }
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct ClientInfo { pub struct ClientInfo {
@ -1763,6 +1772,7 @@ pub struct PluginIds {
pub plugin_id: u32, pub plugin_id: u32,
pub zellij_pid: u32, pub zellij_pid: u32,
pub initial_cwd: PathBuf, pub initial_cwd: PathBuf,
pub client_id: ClientId,
} }
/// Tag used to identify the plugin in layout and config kdl files /// Tag used to identify the plugin in layout and config kdl files
@ -2307,4 +2317,10 @@ pub enum PluginCommand {
OpenFileNearPlugin(FileToOpen, Context), OpenFileNearPlugin(FileToOpen, Context),
OpenFileFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>, Context), OpenFileFloatingNearPlugin(FileToOpen, Option<FloatingPaneCoordinates>, Context),
OpenFileInPlaceOfPlugin(FileToOpen, bool, Context), // bool -> close_plugin_after_replace OpenFileInPlaceOfPlugin(FileToOpen, bool, Context), // bool -> close_plugin_after_replace
GroupAndUngroupPanes(Vec<PaneId>, Vec<PaneId>), // panes to group, panes to ungroup
HighlightAndUnhighlightPanes(Vec<PaneId>, Vec<PaneId>), // panes to highlight, panes to
// unhighlight
CloseMultiplePanes(Vec<PaneId>),
FloatMultiplePanes(Vec<PaneId>),
EmbedMultiplePanes(Vec<PaneId>),
} }

View file

@ -375,6 +375,13 @@ pub enum ScreenContext {
SetFloatingPanePinned, SetFloatingPanePinned,
StackPanes, StackPanes,
ChangeFloatingPanesCoordinates, ChangeFloatingPanesCoordinates,
AddHighlightPaneFrameColorOverride,
GroupAndUngroupPanes,
HighlightAndUnhighlightPanes,
FloatMultiplePanes,
EmbedMultiplePanes,
TogglePaneInGroup,
ToggleGroupMarking,
} }
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s. /// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
@ -512,6 +519,7 @@ pub enum BackgroundJobContext {
WebRequest, WebRequest,
ReportPluginList, ReportPluginList,
RenderToClients, RenderToClients,
HighlightPanesWithMessage,
Exit, Exit,
} }

View file

@ -296,6 +296,8 @@ pub enum Action {
TogglePanePinned, TogglePanePinned,
StackPanes(Vec<PaneId>), StackPanes(Vec<PaneId>),
ChangeFloatingPaneCoordinates(PaneId, FloatingPaneCoordinates), ChangeFloatingPaneCoordinates(PaneId, FloatingPaneCoordinates),
TogglePaneInGroup,
ToggleGroupMarking,
} }
impl Action { impl Action {

View file

@ -45,6 +45,7 @@ mod not_wasm {
session_name, session_name,
editor: None, editor: None,
shell: None, shell: None,
currently_marking_pane_group: None,
} }
} }

View file

@ -208,4 +208,34 @@ impl MouseEvent {
}; };
event event
} }
pub fn new_left_press_with_alt_event(position: Position) -> Self {
let event = MouseEvent {
event_type: MouseEventType::Press,
left: true,
right: false,
middle: false,
wheel_up: false,
wheel_down: false,
shift: false,
alt: true,
ctrl: false,
position,
};
event
}
pub fn new_right_press_with_alt_event(position: Position) -> Self {
let event = MouseEvent {
event_type: MouseEventType::Press,
left: false,
right: true,
middle: false,
wheel_up: false,
wheel_down: false,
shift: false,
alt: true,
ctrl: false,
position,
};
event
}
} }

View file

@ -179,6 +179,12 @@ pub struct Options {
#[clap(long, value_parser)] #[clap(long, value_parser)]
#[serde(default)] #[serde(default)]
pub show_release_notes: Option<bool>, pub show_release_notes: Option<bool>,
/// Whether to enable mouse hover effects and pane grouping functionality
/// default is true
#[clap(long, value_parser)]
#[serde(default)]
pub advanced_mouse_actions: Option<bool>,
} }
#[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)] #[derive(ArgEnum, Deserialize, Serialize, Debug, Clone, Copy, PartialEq)]
@ -260,6 +266,7 @@ impl Options {
let stacked_resize = other.stacked_resize.or(self.stacked_resize); let stacked_resize = other.stacked_resize.or(self.stacked_resize);
let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips); let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
let show_release_notes = other.show_release_notes.or(self.show_release_notes); let show_release_notes = other.show_release_notes.or(self.show_release_notes);
let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
Options { Options {
simplified_ui, simplified_ui,
@ -292,6 +299,7 @@ impl Options {
stacked_resize, stacked_resize,
show_startup_tips, show_startup_tips,
show_release_notes, show_release_notes,
advanced_mouse_actions,
} }
} }
@ -353,6 +361,7 @@ impl Options {
let stacked_resize = other.stacked_resize.or(self.stacked_resize); let stacked_resize = other.stacked_resize.or(self.stacked_resize);
let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips); let show_startup_tips = other.show_startup_tips.or(self.show_startup_tips);
let show_release_notes = other.show_release_notes.or(self.show_release_notes); let show_release_notes = other.show_release_notes.or(self.show_release_notes);
let advanced_mouse_actions = other.advanced_mouse_actions.or(self.advanced_mouse_actions);
Options { Options {
simplified_ui, simplified_ui,
@ -385,6 +394,7 @@ impl Options {
stacked_resize, stacked_resize,
show_startup_tips, show_startup_tips,
show_release_notes, show_release_notes,
advanced_mouse_actions,
} }
} }
@ -451,8 +461,9 @@ impl From<CliOptions> for Options {
serialization_interval: opts.serialization_interval, serialization_interval: opts.serialization_interval,
support_kitty_keyboard_protocol: opts.support_kitty_keyboard_protocol, support_kitty_keyboard_protocol: opts.support_kitty_keyboard_protocol,
stacked_resize: opts.stacked_resize, stacked_resize: opts.stacked_resize,
show_release_notes: opts.show_release_notes,
show_startup_tips: opts.show_startup_tips, show_startup_tips: opts.show_startup_tips,
show_release_notes: opts.show_release_notes,
advanced_mouse_actions: opts.advanced_mouse_actions,
..Default::default() ..Default::default()
} }
} }

View file

@ -65,6 +65,7 @@ impl PluginConfig {
|| tag == "configuration" || tag == "configuration"
|| tag == "plugin-manager" || tag == "plugin-manager"
|| tag == "about" || tag == "about"
|| tag == "multiple-select"
{ {
Some(PluginConfig { Some(PluginConfig {
path: PathBuf::from(&tag), path: PathBuf::from(&tag),

View file

@ -449,16 +449,12 @@ expression: "format!(\"{:#?}\", theme)"
emphasis_0: Rgb( emphasis_0: Rgb(
( (
255, 255,
184, 121,
108, 198,
), ),
), ),
emphasis_1: Rgb( emphasis_1: EightBit(
( 0,
255,
184,
108,
),
), ),
emphasis_2: Rgb( emphasis_2: Rgb(
( (

View file

@ -1103,6 +1103,8 @@ impl Action {
Some(node) Some(node)
}, },
Action::TogglePanePinned => Some(KdlNode::new("TogglePanePinned")), Action::TogglePanePinned => Some(KdlNode::new("TogglePanePinned")),
Action::TogglePaneInGroup => Some(KdlNode::new("TogglePaneInGroup")),
Action::ToggleGroupMarking => Some(KdlNode::new("ToggleGroupMarking")),
_ => None, _ => None,
} }
} }
@ -1798,6 +1800,8 @@ impl TryFrom<(&KdlNode, &Options)> for Action {
}) })
}, },
"TogglePanePinned" => Ok(Action::TogglePanePinned), "TogglePanePinned" => Ok(Action::TogglePanePinned),
"TogglePaneInGroup" => Ok(Action::TogglePaneInGroup),
"ToggleGroupMarking" => Ok(Action::ToggleGroupMarking),
_ => Err(ConfigError::new_kdl_error( _ => Err(ConfigError::new_kdl_error(
format!("Unsupported action: {}", action_name).into(), format!("Unsupported action: {}", action_name).into(),
kdl_action.span().offset(), kdl_action.span().offset(),
@ -2299,6 +2303,9 @@ impl Options {
let show_release_notes = let show_release_notes =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_release_notes") kdl_property_first_arg_as_bool_or_error!(kdl_options, "show_release_notes")
.map(|(v, _)| v); .map(|(v, _)| v);
let advanced_mouse_actions =
kdl_property_first_arg_as_bool_or_error!(kdl_options, "advanced_mouse_actions")
.map(|(v, _)| v);
Ok(Options { Ok(Options {
simplified_ui, simplified_ui,
theme, theme,
@ -2330,6 +2337,7 @@ impl Options {
stacked_resize, stacked_resize,
show_startup_tips, show_startup_tips,
show_release_notes, show_release_notes,
advanced_mouse_actions,
}) })
} }
pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> { pub fn from_string(stringified_keybindings: &String) -> Result<Self, ConfigError> {
@ -3207,6 +3215,33 @@ impl Options {
None None
} }
} }
fn advanced_mouse_actions_to_kdl(&self, add_comments: bool) -> Option<KdlNode> {
let comment_text = format!(
"{}\n{}\n{}",
" ",
"// Whether to enable mouse hover effects and pane grouping functionality",
"// default is true",
);
let create_node = |node_value: bool| -> KdlNode {
let mut node = KdlNode::new("advanced_mouse_actions");
node.push(KdlValue::Bool(node_value));
node
};
if let Some(advanced_mouse_actions) = self.advanced_mouse_actions {
let mut node = create_node(advanced_mouse_actions);
if add_comments {
node.set_leading(format!("{}\n", comment_text));
}
Some(node)
} else if add_comments {
let mut node = create_node(false);
node.set_leading(format!("{}\n// ", comment_text));
Some(node)
} else {
None
}
}
pub fn to_kdl(&self, add_comments: bool) -> Vec<KdlNode> { pub fn to_kdl(&self, add_comments: bool) -> Vec<KdlNode> {
let mut nodes = vec![]; let mut nodes = vec![];
if let Some(simplified_ui_node) = self.simplified_ui_to_kdl(add_comments) { if let Some(simplified_ui_node) = self.simplified_ui_to_kdl(add_comments) {
@ -3303,6 +3338,9 @@ impl Options {
if let Some(show_release_notes) = self.show_release_notes_to_kdl(add_comments) { if let Some(show_release_notes) = self.show_release_notes_to_kdl(add_comments) {
nodes.push(show_release_notes); nodes.push(show_release_notes);
} }
if let Some(advanced_mouse_actions) = self.advanced_mouse_actions_to_kdl(add_comments) {
nodes.push(advanced_mouse_actions);
}
nodes nodes
} }
} }
@ -4886,6 +4924,7 @@ impl PaneInfo {
terminal_command, terminal_command,
plugin_url, plugin_url,
is_selectable, is_selectable,
index_in_pane_group: Default::default(), // we don't serialize this
}; };
Ok((tab_position, pane_info)) Ok((tab_position, pane_info))
} }
@ -5033,6 +5072,7 @@ fn serialize_and_deserialize_session_info_with_data() {
terminal_command: Some("foo".to_owned()), terminal_command: Some("foo".to_owned()),
plugin_url: None, plugin_url: None,
is_selectable: true, is_selectable: true,
index_in_pane_group: Default::default(), // we don't serialize this
}, },
PaneInfo { PaneInfo {
id: 1, id: 1,
@ -5057,6 +5097,7 @@ fn serialize_and_deserialize_session_info_with_data() {
terminal_command: None, terminal_command: None,
plugin_url: Some("i_am_a_fake_plugin".to_owned()), plugin_url: Some("i_am_a_fake_plugin".to_owned()),
is_selectable: true, is_selectable: true,
index_in_pane_group: Default::default(), // we don't serialize this
}, },
]; ];
let mut panes = HashMap::new(); let mut panes = HashMap::new();

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 5922
expression: fake_config_stringified expression: fake_config_stringified
--- ---
keybinds clear-defaults=true { keybinds clear-defaults=true {
@ -147,8 +146,16 @@ keybinds clear-defaults=true {
bind "Alt j" { MoveFocus "down"; } bind "Alt j" { MoveFocus "down"; }
bind "Alt k" { MoveFocus "up"; } bind "Alt k" { MoveFocus "up"; }
bind "Alt l" { MoveFocusOrTab "right"; } bind "Alt l" { MoveFocusOrTab "right"; }
bind "Alt m" {
LaunchOrFocusPlugin "zellij:multiple-select" {
floating true
move_to_focused_tab true
}
}
bind "Alt n" { NewPane; } bind "Alt n" { NewPane; }
bind "Alt o" { MoveTab "right"; } bind "Alt o" { MoveTab "right"; }
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
bind "Ctrl q" { Quit; } bind "Ctrl q" { Quit; }
} }
shared_except "locked" "move" { shared_except "locked" "move" {
@ -256,4 +263,3 @@ plugins {
} }
load_plugins { load_plugins {
} }

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 5934
expression: fake_config_stringified expression: fake_config_stringified
--- ---
keybinds clear-defaults=true { keybinds clear-defaults=true {
@ -147,8 +146,16 @@ keybinds clear-defaults=true {
bind "Alt j" { MoveFocus "down"; } bind "Alt j" { MoveFocus "down"; }
bind "Alt k" { MoveFocus "up"; } bind "Alt k" { MoveFocus "up"; }
bind "Alt l" { MoveFocusOrTab "right"; } bind "Alt l" { MoveFocusOrTab "right"; }
bind "Alt m" {
LaunchOrFocusPlugin "zellij:multiple-select" {
floating true
move_to_focused_tab true
}
}
bind "Alt n" { NewPane; } bind "Alt n" { NewPane; }
bind "Alt o" { MoveTab "right"; } bind "Alt o" { MoveTab "right"; }
bind "Alt p" { TogglePaneInGroup; }
bind "Alt Shift p" { ToggleGroupMarking; }
bind "Ctrl q" { Quit; } bind "Ctrl q" { Quit; }
} }
shared_except "locked" "move" { shared_except "locked" "move" {
@ -456,3 +463,6 @@ load_plugins {
// //
// show_release_notes false // show_release_notes false
// Whether to enable mouse hover effects and pane grouping functionality
// default is true
// advanced_mouse_actions false

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 5873
expression: fake_document.to_string() expression: fake_document.to_string()
--- ---
@ -196,3 +195,6 @@ support_kitty_keyboard_protocol false
// //
// show_release_notes false // show_release_notes false
// Whether to enable mouse hover effects and pane grouping functionality
// default is true
// advanced_mouse_actions false

View file

@ -87,8 +87,8 @@ themes {
frame_highlight { frame_highlight {
base 255 184 108 base 255 184 108
background 0 background 0
emphasis_0 255 184 108 emphasis_0 255 121 198
emphasis_1 255 184 108 emphasis_1 0
emphasis_2 255 184 108 emphasis_2 255 184 108
emphasis_3 255 184 108 emphasis_3 255 184 108
} }

View file

@ -87,8 +87,8 @@ themes {
frame_highlight { frame_highlight {
base 208 135 112 base 208 135 112
background 0 background 0
emphasis_0 208 135 112 emphasis_0 70
emphasis_1 208 135 112 emphasis_1 0
emphasis_2 208 135 112 emphasis_2 208 135 112
emphasis_3 208 135 112 emphasis_3 208 135 112
} }

View file

@ -87,8 +87,8 @@ themes {
frame_highlight { frame_highlight {
base 254 base 254
background 0 background 0
emphasis_0 254 emphasis_0 70
emphasis_1 254 emphasis_1 0
emphasis_2 254 emphasis_2 254
emphasis_3 254 emphasis_3 254
} }

View file

@ -87,8 +87,8 @@ themes {
frame_highlight { frame_highlight {
base 208 135 112 base 208 135 112
background 0 background 0
emphasis_0 208 135 112 emphasis_0 180 142 173
emphasis_1 208 135 112 emphasis_1 0
emphasis_2 208 135 112 emphasis_2 208 135 112
emphasis_3 208 135 112 emphasis_3 208 135 112
} }

View file

@ -87,8 +87,8 @@ themes {
frame_highlight { frame_highlight {
base 255 184 108 base 255 184 108
background 0 background 0
emphasis_0 255 184 108 emphasis_0 255 121 198
emphasis_1 255 184 108 emphasis_1 0
emphasis_2 255 184 108 emphasis_2 255 184 108
emphasis_3 255 184 108 emphasis_3 255 184 108
} }
@ -205,8 +205,8 @@ themes {
frame_highlight { frame_highlight {
base 208 135 112 base 208 135 112
background 0 background 0
emphasis_0 208 135 112 emphasis_0 180 142 173
emphasis_1 208 135 112 emphasis_1 0
emphasis_2 208 135 112 emphasis_2 208 135 112
emphasis_3 208 135 112 emphasis_3 208 135 112
} }

View file

@ -241,6 +241,8 @@ enum ActionName {
KeybindPipe = 84; KeybindPipe = 84;
TogglePanePinned = 85; TogglePanePinned = 85;
MouseEvent = 86; MouseEvent = 86;
TogglePaneInGroup = 87;
ToggleGroupMarking = 88;
} }
message Position { message Position {

View file

@ -692,6 +692,16 @@ impl TryFrom<ProtobufAction> for Action {
Some(_) => Err("TogglePanePinned should not have a payload"), Some(_) => Err("TogglePanePinned should not have a payload"),
None => Ok(Action::TogglePanePinned), None => Ok(Action::TogglePanePinned),
}, },
Some(ProtobufActionName::TogglePaneInGroup) => match protobuf_action.optional_payload {
Some(_) => Err("TogglePaneInGroup should not have a payload"),
None => Ok(Action::TogglePaneInGroup),
},
Some(ProtobufActionName::ToggleGroupMarking) => {
match protobuf_action.optional_payload {
Some(_) => Err("ToggleGroupMarking should not have a payload"),
None => Ok(Action::ToggleGroupMarking),
}
},
Some(ProtobufActionName::KeybindPipe) => match protobuf_action.optional_payload { Some(ProtobufActionName::KeybindPipe) => match protobuf_action.optional_payload {
Some(_) => Err("KeybindPipe should not have a payload"), Some(_) => Err("KeybindPipe should not have a payload"),
// TODO: at some point we might want to support a payload here // TODO: at some point we might want to support a payload here
@ -1236,6 +1246,14 @@ impl TryFrom<Action> for ProtobufAction {
name: ProtobufActionName::TogglePanePinned as i32, name: ProtobufActionName::TogglePanePinned as i32,
optional_payload: None, optional_payload: None,
}), }),
Action::TogglePaneInGroup { .. } => Ok(ProtobufAction {
name: ProtobufActionName::TogglePaneInGroup as i32,
optional_payload: None,
}),
Action::ToggleGroupMarking { .. } => Ok(ProtobufAction {
name: ProtobufActionName::ToggleGroupMarking as i32,
optional_payload: None,
}),
Action::NoOp Action::NoOp
| Action::Confirm | Action::Confirm
| Action::NewInPlacePane(..) | Action::NewInPlacePane(..)

View file

@ -54,6 +54,7 @@ enum EventType {
FailedToChangeHostFolder = 28; FailedToChangeHostFolder = 28;
PastedText = 29; PastedText = 29;
ConfigWasWrittenToDisk = 30; ConfigWasWrittenToDisk = 30;
BeforeClose = 31;
} }
message EventNameList { message EventNameList {
@ -304,6 +305,12 @@ message PaneInfo {
optional string terminal_command = 20; optional string terminal_command = 20;
optional string plugin_url = 21; optional string plugin_url = 21;
bool is_selectable = 22; bool is_selectable = 22;
repeated IndexInPaneGroup index_in_pane_group = 23;
}
message IndexInPaneGroup {
uint32 client_id = 1;
uint32 index = 2;
} }
message TabInfo { message TabInfo {
@ -334,6 +341,7 @@ message ModeUpdatePayload {
optional input_mode.InputMode base_mode = 6; optional input_mode.InputMode base_mode = 6;
optional string editor = 7; optional string editor = 7;
optional string shell = 8; optional string shell = 8;
optional bool currently_marking_pane_group = 9;
} }
message InputModeKeybinds { message InputModeKeybinds {

View file

@ -359,6 +359,10 @@ impl TryFrom<ProtobufEvent> for Event {
None => Ok(Event::ConfigWasWrittenToDisk), None => Ok(Event::ConfigWasWrittenToDisk),
_ => Err("Malformed payload for the ConfigWasWrittenToDisk Event"), _ => Err("Malformed payload for the ConfigWasWrittenToDisk Event"),
}, },
Some(ProtobufEventType::BeforeClose) => match protobuf_event.payload {
None => Ok(Event::BeforeClose),
_ => Err("Malformed payload for the BeforeClose Event"),
},
None => Err("Unknown Protobuf Event"), None => Err("Unknown Protobuf Event"),
} }
} }
@ -733,6 +737,10 @@ impl TryFrom<Event> for ProtobufEvent {
name: ProtobufEventType::ConfigWasWrittenToDisk as i32, name: ProtobufEventType::ConfigWasWrittenToDisk as i32,
payload: None, payload: None,
}), }),
Event::BeforeClose => Ok(ProtobufEvent {
name: ProtobufEventType::BeforeClose as i32,
payload: None,
}),
} }
} }
} }
@ -1072,6 +1080,16 @@ impl TryFrom<ProtobufPaneInfo> for PaneInfo {
terminal_command: protobuf_pane_info.terminal_command, terminal_command: protobuf_pane_info.terminal_command,
plugin_url: protobuf_pane_info.plugin_url, plugin_url: protobuf_pane_info.plugin_url,
is_selectable: protobuf_pane_info.is_selectable, is_selectable: protobuf_pane_info.is_selectable,
index_in_pane_group: protobuf_pane_info
.index_in_pane_group
.iter()
.map(|index_in_pane_group| {
(
index_in_pane_group.client_id as u16,
index_in_pane_group.index as usize,
)
})
.collect(),
}) })
} }
} }
@ -1107,6 +1125,14 @@ impl TryFrom<PaneInfo> for ProtobufPaneInfo {
terminal_command: pane_info.terminal_command, terminal_command: pane_info.terminal_command,
plugin_url: pane_info.plugin_url, plugin_url: pane_info.plugin_url,
is_selectable: pane_info.is_selectable, is_selectable: pane_info.is_selectable,
index_in_pane_group: pane_info
.index_in_pane_group
.iter()
.map(|(&client_id, &index)| IndexInPaneGroup {
client_id: client_id as u32,
index: index as u32,
})
.collect(),
}) })
} }
} }
@ -1216,6 +1242,8 @@ impl TryFrom<ProtobufModeUpdatePayload> for ModeInfo {
let capabilities = PluginCapabilities { let capabilities = PluginCapabilities {
arrow_fonts: protobuf_mode_update_payload.arrow_fonts_support, arrow_fonts: protobuf_mode_update_payload.arrow_fonts_support,
}; };
let currently_marking_pane_group =
protobuf_mode_update_payload.currently_marking_pane_group;
let mode_info = ModeInfo { let mode_info = ModeInfo {
mode: current_mode, mode: current_mode,
keybinds, keybinds,
@ -1225,6 +1253,7 @@ impl TryFrom<ProtobufModeUpdatePayload> for ModeInfo {
base_mode, base_mode,
editor, editor,
shell, shell,
currently_marking_pane_group,
}; };
Ok(mode_info) Ok(mode_info)
} }
@ -1242,6 +1271,7 @@ impl TryFrom<ModeInfo> for ProtobufModeUpdatePayload {
let session_name = mode_info.session_name; let session_name = mode_info.session_name;
let editor = mode_info.editor.map(|e| e.display().to_string()); let editor = mode_info.editor.map(|e| e.display().to_string());
let shell = mode_info.shell.map(|s| s.display().to_string()); let shell = mode_info.shell.map(|s| s.display().to_string());
let currently_marking_pane_group = mode_info.currently_marking_pane_group;
let mut protobuf_input_mode_keybinds: Vec<ProtobufInputModeKeybinds> = vec![]; let mut protobuf_input_mode_keybinds: Vec<ProtobufInputModeKeybinds> = vec![];
for (input_mode, input_mode_keybinds) in mode_info.keybinds { for (input_mode, input_mode_keybinds) in mode_info.keybinds {
let mode: ProtobufInputMode = input_mode.try_into()?; let mode: ProtobufInputMode = input_mode.try_into()?;
@ -1275,6 +1305,7 @@ impl TryFrom<ModeInfo> for ProtobufModeUpdatePayload {
base_mode: base_mode.map(|b_m| b_m as i32), base_mode: base_mode.map(|b_m| b_m as i32),
editor, editor,
shell, shell,
currently_marking_pane_group,
}) })
} }
} }
@ -1344,6 +1375,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::FailedToChangeHostFolder => EventType::FailedToChangeHostFolder, ProtobufEventType::FailedToChangeHostFolder => EventType::FailedToChangeHostFolder,
ProtobufEventType::PastedText => EventType::PastedText, ProtobufEventType::PastedText => EventType::PastedText,
ProtobufEventType::ConfigWasWrittenToDisk => EventType::ConfigWasWrittenToDisk, ProtobufEventType::ConfigWasWrittenToDisk => EventType::ConfigWasWrittenToDisk,
ProtobufEventType::BeforeClose => EventType::BeforeClose,
}) })
} }
} }
@ -1383,6 +1415,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::FailedToChangeHostFolder => ProtobufEventType::FailedToChangeHostFolder, EventType::FailedToChangeHostFolder => ProtobufEventType::FailedToChangeHostFolder,
EventType::PastedText => ProtobufEventType::PastedText, EventType::PastedText => ProtobufEventType::PastedText,
EventType::ConfigWasWrittenToDisk => ProtobufEventType::ConfigWasWrittenToDisk, EventType::ConfigWasWrittenToDisk => ProtobufEventType::ConfigWasWrittenToDisk,
EventType::BeforeClose => ProtobufEventType::BeforeClose,
}) })
} }
} }
@ -1523,6 +1556,7 @@ fn serialize_mode_update_event_with_non_default_values() {
base_mode: Some(InputMode::Locked), base_mode: Some(InputMode::Locked),
editor: Some(PathBuf::from("my_awesome_editor")), editor: Some(PathBuf::from("my_awesome_editor")),
shell: Some(PathBuf::from("my_awesome_shell")), shell: Some(PathBuf::from("my_awesome_shell")),
currently_marking_pane_group: Some(false),
}); });
let protobuf_event: ProtobufEvent = mode_update_event.clone().try_into().unwrap(); let protobuf_event: ProtobufEvent = mode_update_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec(); let serialized_protobuf_event = protobuf_event.encode_to_vec();
@ -1884,6 +1918,14 @@ fn serialize_session_update_event_with_non_default_values() {
TabInfo::default(), TabInfo::default(),
]; ];
let mut panes = HashMap::new(); let mut panes = HashMap::new();
let mut index_in_pane_group_1 = BTreeMap::new();
index_in_pane_group_1.insert(1, 0);
index_in_pane_group_1.insert(2, 0);
index_in_pane_group_1.insert(3, 0);
let mut index_in_pane_group_2 = BTreeMap::new();
index_in_pane_group_2.insert(1, 1);
index_in_pane_group_2.insert(2, 1);
index_in_pane_group_2.insert(3, 1);
let panes_list = vec![ let panes_list = vec![
PaneInfo { PaneInfo {
id: 1, id: 1,
@ -1908,6 +1950,7 @@ fn serialize_session_update_event_with_non_default_values() {
terminal_command: Some("foo".to_owned()), terminal_command: Some("foo".to_owned()),
plugin_url: None, plugin_url: None,
is_selectable: true, is_selectable: true,
index_in_pane_group: index_in_pane_group_1,
}, },
PaneInfo { PaneInfo {
id: 1, id: 1,
@ -1932,6 +1975,7 @@ fn serialize_session_update_event_with_non_default_values() {
terminal_command: None, terminal_command: None,
plugin_url: Some("i_am_a_fake_plugin".to_owned()), plugin_url: Some("i_am_a_fake_plugin".to_owned()),
is_selectable: true, is_selectable: true,
index_in_pane_group: index_in_pane_group_2,
}, },
]; ];
panes.insert(0, panes_list); panes.insert(0, panes_list);

View file

@ -140,6 +140,11 @@ enum CommandName {
OpenFileNearPlugin = 124; OpenFileNearPlugin = 124;
OpenFileFloatingNearPlugin = 125; OpenFileFloatingNearPlugin = 125;
OpenFileInPlaceOfPlugin = 126; OpenFileInPlaceOfPlugin = 126;
GroupAndUngroupPanes = 127;
HighlightAndUnhighlightPanes = 128;
CloseMultiplePanes = 129;
FloatMultiplePanes = 130;
EmbedMultiplePanes = 131;
} }
message PluginCommand { message PluginCommand {
@ -236,9 +241,36 @@ message PluginCommand {
OpenFileNearPluginPayload open_file_near_plugin_payload = 99; OpenFileNearPluginPayload open_file_near_plugin_payload = 99;
OpenFileFloatingNearPluginPayload open_file_floating_near_plugin_payload = 100; OpenFileFloatingNearPluginPayload open_file_floating_near_plugin_payload = 100;
OpenFileInPlaceOfPluginPayload open_file_in_place_of_plugin_payload = 101; OpenFileInPlaceOfPluginPayload open_file_in_place_of_plugin_payload = 101;
GroupAndUngroupPanesPayload group_and_ungroup_panes_payload = 102;
HighlightAndUnhighlightPanesPayload highlight_and_unhighlight_panes_payload = 103;
CloseMultiplePanesPayload close_multiple_panes_payload = 104;
FloatMultiplePanesPayload float_multiple_panes_payload = 105;
EmbedMultiplePanesPayload embed_multiple_panes_payload = 106;
} }
} }
message EmbedMultiplePanesPayload {
repeated PaneId pane_ids = 1;
}
message FloatMultiplePanesPayload {
repeated PaneId pane_ids = 1;
}
message CloseMultiplePanesPayload {
repeated PaneId pane_ids = 1;
}
message HighlightAndUnhighlightPanesPayload {
repeated PaneId pane_ids_to_highlight = 1;
repeated PaneId pane_ids_to_unhighlight = 2;
}
message GroupAndUngroupPanesPayload {
repeated PaneId pane_ids_to_group = 1;
repeated PaneId pane_ids_to_ungroup = 2;
}
message OpenFileInPlaceOfPluginPayload { message OpenFileInPlaceOfPluginPayload {
file.File file_to_open = 1; file.File file_to_open = 1;
optional FloatingPaneCoordinates floating_pane_coordinates = 2; optional FloatingPaneCoordinates floating_pane_coordinates = 2;

View file

@ -5,17 +5,19 @@ pub use super::generated_api::api::{
plugin_command::{ plugin_command::{
plugin_command::Payload, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload, plugin_command::Payload, BreakPanesToNewTabPayload, BreakPanesToTabWithIndexPayload,
ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload, ChangeFloatingPanesCoordinatesPayload, ChangeHostFolderPayload,
ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseTabWithIndexPayload, CommandName, ClearScreenForPaneIdPayload, CliPipeOutputPayload, CloseMultiplePanesPayload,
ContextItem, EditScrollbackForPaneWithIdPayload, EnvVariable, ExecCmdPayload, CloseTabWithIndexPayload, CommandName, ContextItem, EditScrollbackForPaneWithIdPayload,
EmbedMultiplePanesPayload, EnvVariable, ExecCmdPayload,
FixedOrPercent as ProtobufFixedOrPercent, FixedOrPercent as ProtobufFixedOrPercent,
FixedOrPercentValue as ProtobufFixedOrPercentValue, FixedOrPercentValue as ProtobufFixedOrPercentValue, FloatMultiplePanesPayload,
FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, HidePaneWithIdPayload, FloatingPaneCoordinates as ProtobufFloatingPaneCoordinates, GroupAndUngroupPanesPayload,
HttpVerb as ProtobufHttpVerb, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, HidePaneWithIdPayload, HighlightAndUnhighlightPanesPayload, HttpVerb as ProtobufHttpVerb,
LoadNewPluginPayload, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, IdAndNewName, KeyToRebind, KeyToUnbind, KillSessionsPayload, LoadNewPluginPayload,
MovePaneWithPaneIdPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs, MessageToPluginPayload, MovePaneWithPaneIdInDirectionPayload, MovePaneWithPaneIdPayload,
NewTabsWithLayoutInfoPayload, OpenCommandPaneFloatingNearPluginPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs, NewTabsWithLayoutInfoPayload,
OpenCommandPaneInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload, OpenCommandPaneFloatingNearPluginPayload, OpenCommandPaneInPlaceOfPluginPayload,
OpenCommandPanePayload, OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload, OpenCommandPaneNearPluginPayload, OpenCommandPanePayload,
OpenFileFloatingNearPluginPayload, OpenFileInPlaceOfPluginPayload,
OpenFileNearPluginPayload, OpenFilePayload, OpenTerminalFloatingNearPluginPayload, OpenFileNearPluginPayload, OpenFilePayload, OpenTerminalFloatingNearPluginPayload,
OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload, OpenTerminalInPlaceOfPluginPayload, OpenTerminalNearPluginPayload,
PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId, PageScrollDownInPaneIdPayload, PageScrollUpInPaneIdPayload, PaneId as ProtobufPaneId,
@ -1540,6 +1542,78 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for OpenFileInPlaceOfPlugin"), _ => Err("Mismatched payload for OpenFileInPlaceOfPlugin"),
}, },
Some(CommandName::GroupAndUngroupPanes) => match protobuf_plugin_command.payload {
Some(Payload::GroupAndUngroupPanesPayload(group_and_ungroup_panes_payload)) => {
Ok(PluginCommand::GroupAndUngroupPanes(
group_and_ungroup_panes_payload
.pane_ids_to_group
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
group_and_ungroup_panes_payload
.pane_ids_to_ungroup
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
))
},
_ => Err("Mismatched payload for GroupAndUngroupPanes"),
},
Some(CommandName::HighlightAndUnhighlightPanes) => {
match protobuf_plugin_command.payload {
Some(Payload::HighlightAndUnhighlightPanesPayload(
highlight_and_unhighlight_panes_payload,
)) => Ok(PluginCommand::HighlightAndUnhighlightPanes(
highlight_and_unhighlight_panes_payload
.pane_ids_to_highlight
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
highlight_and_unhighlight_panes_payload
.pane_ids_to_unhighlight
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
)),
_ => Err("Mismatched payload for HighlightAndUnhighlightPanes"),
}
},
Some(CommandName::CloseMultiplePanes) => match protobuf_plugin_command.payload {
Some(Payload::CloseMultiplePanesPayload(close_multiple_panes_payload)) => {
Ok(PluginCommand::CloseMultiplePanes(
close_multiple_panes_payload
.pane_ids
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
))
},
_ => Err("Mismatched payload for CloseMultiplePanes"),
},
Some(CommandName::FloatMultiplePanes) => match protobuf_plugin_command.payload {
Some(Payload::FloatMultiplePanesPayload(float_multiple_panes_payload)) => {
Ok(PluginCommand::FloatMultiplePanes(
float_multiple_panes_payload
.pane_ids
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
))
},
_ => Err("Mismatched payload for FloatMultiplePanes"),
},
Some(CommandName::EmbedMultiplePanes) => match protobuf_plugin_command.payload {
Some(Payload::EmbedMultiplePanesPayload(embed_multiple_panes_payload)) => {
Ok(PluginCommand::EmbedMultiplePanes(
embed_multiple_panes_payload
.pane_ids
.into_iter()
.filter_map(|p| p.try_into().ok())
.collect(),
))
},
_ => Err("Mismatched payload for EmbedMultiplePanes"),
},
None => Err("Unrecognized plugin command"), None => Err("Unrecognized plugin command"),
} }
} }
@ -2551,6 +2625,65 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
}, },
)), )),
}), }),
PluginCommand::GroupAndUngroupPanes(panes_to_group, panes_to_ungroup) => {
Ok(ProtobufPluginCommand {
name: CommandName::GroupAndUngroupPanes as i32,
payload: Some(Payload::GroupAndUngroupPanesPayload(
GroupAndUngroupPanesPayload {
pane_ids_to_group: panes_to_group
.iter()
.filter_map(|&p| p.try_into().ok())
.collect(),
pane_ids_to_ungroup: panes_to_ungroup
.iter()
.filter_map(|&p| p.try_into().ok())
.collect(),
},
)),
})
},
PluginCommand::HighlightAndUnhighlightPanes(
panes_to_highlight,
panes_to_unhighlight,
) => Ok(ProtobufPluginCommand {
name: CommandName::HighlightAndUnhighlightPanes as i32,
payload: Some(Payload::HighlightAndUnhighlightPanesPayload(
HighlightAndUnhighlightPanesPayload {
pane_ids_to_highlight: panes_to_highlight
.iter()
.filter_map(|&p| p.try_into().ok())
.collect(),
pane_ids_to_unhighlight: panes_to_unhighlight
.iter()
.filter_map(|&p| p.try_into().ok())
.collect(),
},
)),
}),
PluginCommand::CloseMultiplePanes(pane_ids) => Ok(ProtobufPluginCommand {
name: CommandName::CloseMultiplePanes as i32,
payload: Some(Payload::CloseMultiplePanesPayload(
CloseMultiplePanesPayload {
pane_ids: pane_ids.iter().filter_map(|&p| p.try_into().ok()).collect(),
},
)),
}),
PluginCommand::FloatMultiplePanes(pane_ids) => Ok(ProtobufPluginCommand {
name: CommandName::FloatMultiplePanes as i32,
payload: Some(Payload::FloatMultiplePanesPayload(
FloatMultiplePanesPayload {
pane_ids: pane_ids.iter().filter_map(|&p| p.try_into().ok()).collect(),
},
)),
}),
PluginCommand::EmbedMultiplePanes(pane_ids) => Ok(ProtobufPluginCommand {
name: CommandName::EmbedMultiplePanes as i32,
payload: Some(Payload::EmbedMultiplePanesPayload(
EmbedMultiplePanesPayload {
pane_ids: pane_ids.iter().filter_map(|&p| p.try_into().ok()).collect(),
},
)),
}),
} }
} }
} }

View file

@ -6,6 +6,7 @@ message PluginIds {
int32 plugin_id = 1; int32 plugin_id = 1;
int32 zellij_pid = 2; int32 zellij_pid = 2;
string initial_cwd = 3; string initial_cwd = 3;
uint32 client_id = 4;
} }
message ZellijVersion { message ZellijVersion {

View file

@ -13,6 +13,7 @@ impl TryFrom<ProtobufPluginIds> for PluginIds {
plugin_id: protobuf_plugin_ids.plugin_id as u32, plugin_id: protobuf_plugin_ids.plugin_id as u32,
zellij_pid: protobuf_plugin_ids.zellij_pid as u32, zellij_pid: protobuf_plugin_ids.zellij_pid as u32,
initial_cwd: PathBuf::from(protobuf_plugin_ids.initial_cwd), initial_cwd: PathBuf::from(protobuf_plugin_ids.initial_cwd),
client_id: protobuf_plugin_ids.client_id as u16,
}) })
} }
} }
@ -24,6 +25,7 @@ impl TryFrom<PluginIds> for ProtobufPluginIds {
plugin_id: plugin_ids.plugin_id as i32, plugin_id: plugin_ids.plugin_id as i32,
zellij_pid: plugin_ids.zellij_pid as i32, zellij_pid: plugin_ids.zellij_pid as i32,
initial_cwd: plugin_ids.initial_cwd.display().to_string(), initial_cwd: plugin_ids.initial_cwd.display().to_string(),
client_id: plugin_ids.client_id as u32,
}) })
} }
} }

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 770
expression: "format!(\"{:#?}\", options)" expression: "format!(\"{:#?}\", options)"
--- ---
Options { Options {
@ -36,4 +35,5 @@ Options {
stacked_resize: None, stacked_resize: None,
show_startup_tips: None, show_startup_tips: None,
show_release_notes: None, show_release_notes: None,
advanced_mouse_actions: None,
} }

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 798
expression: "format!(\"{:#?}\", options)" expression: "format!(\"{:#?}\", options)"
--- ---
Options { Options {
@ -36,4 +35,5 @@ Options {
stacked_resize: None, stacked_resize: None,
show_startup_tips: None, show_startup_tips: None,
show_release_notes: None, show_release_notes: None,
advanced_mouse_actions: None,
} }

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 757
expression: "format!(\"{:#?}\", options)" expression: "format!(\"{:#?}\", options)"
--- ---
Options { Options {
@ -34,4 +33,5 @@ Options {
stacked_resize: None, stacked_resize: None,
show_startup_tips: None, show_startup_tips: None,
show_release_notes: None, show_release_notes: None,
advanced_mouse_actions: None,
} }

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 755
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -211,6 +210,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -273,6 +301,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -709,6 +758,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -771,6 +849,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -1165,6 +1264,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -1250,6 +1378,27 @@ Config {
Normal, Normal,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -1763,6 +1912,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -1843,6 +2021,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -2251,6 +2450,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -2313,6 +2541,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -2605,6 +2854,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -2667,6 +2945,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3031,6 +3330,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3123,6 +3451,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3410,6 +3759,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3472,6 +3850,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3742,6 +4141,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3804,6 +4232,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4128,6 +4577,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4218,6 +4696,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4591,6 +5090,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4671,6 +5199,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4936,6 +5485,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4998,6 +5576,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -5446,6 +6045,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -5538,6 +6166,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -5643,6 +6292,7 @@ Config {
stacked_resize: None, stacked_resize: None,
show_startup_tips: None, show_startup_tips: None,
show_release_notes: None, show_release_notes: None,
advanced_mouse_actions: None,
}, },
themes: {}, themes: {},
plugins: PluginAliases { plugins: PluginAliases {

View file

@ -1,6 +1,5 @@
--- ---
source: zellij-utils/src/setup.rs source: zellij-utils/src/setup.rs
assertion_line: 813
expression: "format!(\"{:#?}\", config)" expression: "format!(\"{:#?}\", config)"
--- ---
Config { Config {
@ -211,6 +210,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -273,6 +301,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -709,6 +758,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -771,6 +849,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -1165,6 +1264,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -1250,6 +1378,27 @@ Config {
Normal, Normal,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -1763,6 +1912,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -1843,6 +2021,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -2251,6 +2450,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -2313,6 +2541,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -2605,6 +2854,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -2667,6 +2945,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3031,6 +3330,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3123,6 +3451,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3410,6 +3759,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3472,6 +3850,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -3742,6 +4141,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -3804,6 +4232,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4128,6 +4577,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4218,6 +4696,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4591,6 +5090,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4671,6 +5199,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -4936,6 +5485,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -4998,6 +5576,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -5446,6 +6045,35 @@ Config {
Right, Right,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'm',
),
key_modifiers: {
Alt,
},
}: [
LaunchOrFocusPlugin(
RunPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"multiple-select",
),
),
configuration: PluginUserConfiguration(
{},
),
initial_cwd: None,
},
),
true,
true,
false,
false,
),
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'n', 'n',
@ -5538,6 +6166,27 @@ Config {
Pane, Pane,
), ),
], ],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
},
}: [
TogglePaneInGroup,
],
KeyWithModifier {
bare_key: Char(
'p',
),
key_modifiers: {
Alt,
Shift,
},
}: [
ToggleGroupMarking,
],
KeyWithModifier { KeyWithModifier {
bare_key: Char( bare_key: Char(
'q', 'q',
@ -5643,6 +6292,7 @@ Config {
stacked_resize: None, stacked_resize: None,
show_startup_tips: None, show_startup_tips: None,
show_release_notes: None, show_release_notes: None,
advanced_mouse_actions: None,
}, },
themes: {}, themes: {},
plugins: PluginAliases { plugins: PluginAliases {

Some files were not shown because too many files have changed in this diff Show more