diff options
| author | Aram Drevekenin <aram@poor.dev> | 2025-06-03 17:15:32 +0200 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2025-06-03 17:15:32 +0200 |
| commit | a9f8bbcd197c8d14832fc1ab1fbe4d4111a2014d (patch) | |
| tree | 6d354fdd36cf534d73e0cb81a7cacd3fedc1ebb9 /default-plugins/multiple-select/src | |
| parent | docs(changelog): theme fixes (diff) | |
| download | sl0ppij-a9f8bbcd197c8d14832fc1ab1fbe4d4111a2014d.tar.gz sl0ppij-a9f8bbcd197c8d14832fc1ab1fbe4d4111a2014d.tar.xz sl0ppij-a9f8bbcd197c8d14832fc1ab1fbe4d4111a2014d.zip | |
feat(ux): improve multiple select (#4221)
* intercept/clear-intercept key APIs
* allow opening a pinned unfocused floating pane
* rework plugin
* improve some apis
* fix tests
* tests for pane groups
* more exact placement and tests
* plugin command permission and cleanup
* improve some multiselect ux
* improve plugin ui
* remove old status indicator
* allow moving plugin out of the way
* style(fmt): rustfmt
* update plugins
* remove old keybinding
* cleanups
* fix: only rename pane if needed
* changelog and some cleanups
* style(fmt): rustfmt
Diffstat (limited to 'default-plugins/multiple-select/src')
| -rw-r--r-- | default-plugins/multiple-select/src/main.rs | 770 | ||||
| -rw-r--r-- | default-plugins/multiple-select/src/state.rs | 634 | ||||
| -rw-r--r-- | default-plugins/multiple-select/src/ui.rs | 873 |
3 files changed, 591 insertions, 1686 deletions
diff --git a/default-plugins/multiple-select/src/main.rs b/default-plugins/multiple-select/src/main.rs index 773fd6fb..6bcd9b9b 100644 --- a/default-plugins/multiple-select/src/main.rs +++ b/default-plugins/multiple-select/src/main.rs @@ -1,9 +1,6 @@ -pub mod state; -pub mod ui; - -use state::{MarkedIndex, VisibilityAndFocus}; use std::collections::BTreeMap; -use ui::PaneItem; +use std::time::Instant; +use zellij_tile::prelude::actions::Action; use zellij_tile::prelude::*; #[derive(Debug, Default)] @@ -12,13 +9,17 @@ pub struct App { 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>, + grouped_panes: Vec<PaneId>, + grouped_panes_count: usize, + mode_info: ModeInfo, + closing: bool, + highlighted_at: Option<Instant>, + baseline_ui_width: usize, + current_rows: usize, + current_cols: usize, + display_area_rows: usize, + display_area_cols: usize, + alternate_coordinates: bool, } register_plugin!(App); @@ -27,186 +28,597 @@ impl ZellijPlugin for App { fn load(&mut self, _configuration: BTreeMap<String, String>) { subscribe(&[ EventType::Key, - EventType::Mouse, + EventType::InterceptedKeyPress, EventType::ModeUpdate, - EventType::RunCommandResult, - EventType::TabUpdate, EventType::PaneUpdate, - EventType::FailedToWriteConfigToDisk, - EventType::ConfigWasWrittenToDisk, - EventType::BeforeClose, + EventType::TabUpdate, + EventType::Timer, ]); + 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"); + + intercept_key_presses(); + set_selectable(false); } + fn update(&mut self, event: Event) -> bool { - let mut should_render = false; + if self.closing { + return 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(); - }, - _ => {}, + Event::ModeUpdate(mode_info) => self.handle_mode_update(mode_info), + Event::PaneUpdate(pane_manifest) => self.handle_pane_update(pane_manifest), + Event::TabUpdate(tab_infos) => self.handle_tab_update(tab_infos), + Event::InterceptedKeyPress(key) => self.handle_key_press(key), + Event::Timer(_) => self.handle_timer(), + _ => false, } - 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.update_current_size(rows, cols); + let ui_width = self.calculate_ui_width(); + self.update_baseline_ui_width(ui_width); + let base_x = cols.saturating_sub(self.baseline_ui_width) / 2; + let base_y = rows.saturating_sub(8) / 2; + self.render_header(base_x, base_y); + self.render_shortcuts(base_x, base_y + 2); + self.render_controls(base_x, base_y + 7); + } +} + +impl App { + fn update_current_size(&mut self, new_rows: usize, new_cols: usize) { + let size_changed = new_rows != self.current_rows || new_cols != self.current_cols; + self.current_rows = new_rows; + self.current_cols = new_cols; + if size_changed { + self.baseline_ui_width = 0; + } + } + fn update_baseline_ui_width(&mut self, current_ui_width: usize) { + if current_ui_width > self.baseline_ui_width { + self.baseline_ui_width = current_ui_width; + } + } + + fn calculate_ui_width(&self) -> usize { + let controls_width = group_controls_length(&self.mode_info); + + let header_width = Self::header_text().0.len(); + let shortcuts_max_width = Self::shortcuts_max_width(); + + std::cmp::max( + controls_width, + std::cmp::max(header_width, shortcuts_max_width), + ) + } + + fn header_text() -> (&'static str, Text) { + let header_text = "<ESC> - cancel, <TAB> - move"; + let header_text_component = Text::new(header_text) + .color_substring(3, "<ESC>") + .color_substring(3, "<TAB>"); + (header_text, header_text_component) + } + + fn shortcuts_max_width() -> usize { + std::cmp::max( + std::cmp::max( + Self::group_actions_text().0.len(), + Self::shortcuts_line1_text().0.len(), + ), + std::cmp::max( + Self::shortcuts_line2_text().0.len(), + Self::shortcuts_line3_text().0.len(), + ), + ) + } + + fn group_actions_text() -> (&'static str, Text) { + let text = "GROUP ACTIONS"; + let component = Text::new(text).color_all(2); + (text, component) + } + + fn shortcuts_line1_text() -> (&'static str, Text) { + let text = "<b> - break out, <s> - stack, <c> - close"; + let component = Text::new(text) + .color_substring(3, "<b>") + .color_substring(3, "<s>") + .color_substring(3, "<c>"); + (text, component) + } + + fn shortcuts_line2_text() -> (&'static str, Text) { + let text = "<r> - break right, <l> - break left"; + let component = Text::new(text) + .color_substring(3, "<r>") + .color_substring(3, "<l>"); + (text, component) + } + + fn shortcuts_line3_text() -> (&'static str, Text) { + let text = "<e> - embed, <f> - float"; + let component = Text::new(text) + .color_substring(3, "<e>") + .color_substring(3, "<f>"); + (text, component) + } + + fn handle_mode_update(&mut self, mode_info: ModeInfo) -> bool { + if self.mode_info != mode_info { + self.mode_info = mode_info; + let ui_width = self.calculate_ui_width(); + self.update_baseline_ui_width(ui_width); + true + } else { + false + } + } + + fn handle_pane_update(&mut self, pane_manifest: PaneManifest) -> bool { + let Some(own_client_id) = self.own_client_id else { + return false; + }; + + self.update_grouped_panes(&pane_manifest, own_client_id); + self.update_tab_info(&pane_manifest); + self.total_tabs_in_session = Some(pane_manifest.panes.keys().count()); + + true + } + + fn handle_tab_update(&mut self, tab_infos: Vec<TabInfo>) -> bool { + for tab in tab_infos { + if tab.active { + self.display_area_rows = tab.display_area_rows; + self.display_area_cols = tab.display_area_columns; + break; + } + } + + false + } + + fn update_grouped_panes(&mut self, pane_manifest: &PaneManifest, own_client_id: ClientId) { + self.grouped_panes.clear(); + let mut count = 0; + + for (_tab_index, pane_infos) in &pane_manifest.panes { + for pane_info in pane_infos { + if pane_info.index_in_pane_group.get(&own_client_id).is_some() { + let pane_id = if pane_info.is_plugin { + PaneId::Plugin(pane_info.id) + } else { + PaneId::Terminal(pane_info.id) + }; + self.grouped_panes.push(pane_id); + count += 1; + } + } + } + if count == 0 { + self.close_self(); + } + + let previous_count = self.grouped_panes_count; + self.grouped_panes_count = count; + if let Some(own_plugin_id) = self.own_plugin_id { + let title = if count == 1 { + "SELECTED PANE" + } else { + "SELECTED PANES" + }; + if previous_count != count { + rename_plugin_pane(own_plugin_id, format!("{} {}", count, title)); + } + if previous_count != 0 && count != 0 && previous_count != count { + if self.doherty_threshold_elapsed_since_highlight() { + self.highlighted_at = Some(Instant::now()); + highlight_and_unhighlight_panes(vec![PaneId::Plugin(own_plugin_id)], vec![]); + set_timeout(0.4); + } + } + } + } + + fn doherty_threshold_elapsed_since_highlight(&self) -> bool { + self.highlighted_at + .map(|h| h.elapsed() >= std::time::Duration::from_millis(400)) + .unwrap_or(true) + } + + 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); + return; + } + } + } + } + + fn handle_key_press(&mut self, key: KeyWithModifier) -> bool { + if !key.has_no_modifiers() { + return false; + } + + match key.bare_key { + BareKey::Char('b') => self.break_grouped_panes_to_new_tab(), + BareKey::Char('s') => self.stack_grouped_panes(), + BareKey::Char('f') => self.float_grouped_panes(), + BareKey::Char('e') => self.embed_grouped_panes(), + BareKey::Char('r') => self.break_grouped_panes_right(), + BareKey::Char('l') => self.break_grouped_panes_left(), + BareKey::Char('c') => self.close_grouped_panes(), + BareKey::Tab => self.next_coordinates(), + BareKey::Esc => { + self.ungroup_panes_in_zellij(&self.grouped_panes.clone()); + self.close_self(); }, + _ => return false, + } + false + } + fn handle_timer(&mut self) -> bool { + if let Some(own_plugin_id) = self.own_plugin_id { + if self.doherty_threshold_elapsed_since_highlight() { + highlight_and_unhighlight_panes(vec![], vec![PaneId::Plugin(own_plugin_id)]); + } + } + false + } + + fn render_header(&self, base_x: usize, base_y: usize) { + let header_text = Self::header_text(); + + print_text_with_coordinates(header_text.1, base_x, base_y, None, None); + } + + fn render_shortcuts(&self, base_x: usize, base_y: usize) { + let mut running_y = base_y; + print_text_with_coordinates(Self::group_actions_text().1, base_x, running_y, None, None); + running_y += 1; + + print_text_with_coordinates( + Self::shortcuts_line1_text().1, + base_x, + running_y, + None, + None, + ); + running_y += 1; + + print_text_with_coordinates( + Self::shortcuts_line2_text().1, + base_x, + running_y, + None, + None, + ); + running_y += 1; + + print_text_with_coordinates( + Self::shortcuts_line3_text().1, + base_x, + running_y, + None, + None, + ); + } + + fn render_controls(&self, base_x: usize, base_y: usize) { + render_group_controls(&self.mode_info, base_x, base_y); + } + + fn execute_action_and_close<F>(&mut self, action: F) + where + F: FnOnce(&[PaneId]), + { + let pane_ids = self.grouped_panes.clone(); + action(&pane_ids); + self.close_self(); + } + + pub fn break_grouped_panes_to_new_tab(&mut self) { + self.execute_action_and_close(|pane_ids| { + break_panes_to_new_tab(pane_ids, None, true); + }); + self.ungroup_panes_in_zellij(&self.grouped_panes.clone()); + } + + pub fn stack_grouped_panes(&mut self) { + self.execute_action_and_close(|pane_ids| { + stack_panes(pane_ids.to_vec()); + }); + self.ungroup_panes_in_zellij(&self.grouped_panes.clone()); + } + + pub fn float_grouped_panes(&mut self) { + self.execute_action_and_close(|pane_ids| { + float_multiple_panes(pane_ids.to_vec()); + }); + self.ungroup_panes_in_zellij(&self.grouped_panes.clone()); + } + + pub fn embed_grouped_panes(&mut self) { + self.execute_action_and_close(|pane_ids| { + embed_multiple_panes(pane_ids.to_vec()); + }); + self.ungroup_panes_in_zellij(&self.grouped_panes.clone()); + } + + pub fn break_grouped_panes_right(&mut self) { + let Some(own_tab_index) = self.own_tab_index else { + return; + }; + + let pane_ids = self.grouped_panes.clone(); + + if Some(own_tab_index + 1) < self.total_tabs_in_session { + break_panes_to_tab_with_index(&pane_ids, own_tab_index + 1, true); + } else { + break_panes_to_new_tab(&pane_ids, None, true); + } + + self.close_self(); + } + + pub fn break_grouped_panes_left(&mut self) { + let Some(own_tab_index) = self.own_tab_index else { + return; + }; + + let pane_ids = self.grouped_panes.clone(); + + if own_tab_index > 0 { + break_panes_to_tab_with_index(&pane_ids, own_tab_index - 1, true); + } else { + break_panes_to_new_tab(&pane_ids, None, true); + } + + self.close_self(); + } + + pub fn close_grouped_panes(&mut self) { + self.execute_action_and_close(|pane_ids| { + close_multiple_panes(pane_ids.to_vec()); + }); + } + + pub fn ungroup_panes_in_zellij(&mut self, pane_ids: &[PaneId]) { + group_and_ungroup_panes(vec![], pane_ids.to_vec()); + } + pub fn close_self(&mut self) { + self.closing = true; + close_self(); + } + pub fn next_coordinates(&mut self) { + let width_30_percent = (self.display_area_cols as f64 * 0.3) as usize; + let height_30_percent = (self.display_area_rows as f64 * 0.3) as usize; + let width = std::cmp::max(width_30_percent, 48); + let height = std::cmp::max(height_30_percent, 10); + let y_position = self.display_area_rows.saturating_sub(height + 2); + if let Some(own_plugin_id) = self.own_plugin_id { + if self.alternate_coordinates { + let x_position = 2; + let Some(next_coordinates) = FloatingPaneCoordinates::new( + Some(format!("{}", x_position)), + Some(format!("{}", y_position)), + Some(format!("{}", width)), + Some(format!("{}", height)), + Some(true), + ) else { + return; + }; + change_floating_panes_coordinates(vec![( + PaneId::Plugin(own_plugin_id), + next_coordinates, + )]); + self.alternate_coordinates = false; + } else { + let x_position = self + .display_area_cols + .saturating_sub(width) + .saturating_sub(2); + let Some(next_coordinates) = FloatingPaneCoordinates::new( + Some(format!("{}", x_position)), + Some(format!("{}", y_position)), + Some(format!("{}", width)), + Some(format!("{}", height)), + Some(true), + ) else { + return; + }; + change_floating_panes_coordinates(vec![( + PaneId::Plugin(own_plugin_id), + next_coordinates, + )]); + self.alternate_coordinates = true; + } } - self.render_focus_boundary(rows, cols); - self.render_help_line(rows, cols); } } + +fn render_group_controls(mode_info: &ModeInfo, base_x: usize, base_y: usize) { + let keymap = mode_info.get_mode_keybinds(); + let (common_modifiers, pane_group_key, group_mark_key) = extract_key_bindings(&keymap); + + let pane_group_bound = pane_group_key != "UNBOUND"; + let group_mark_bound = group_mark_key != "UNBOUND"; + + if !pane_group_bound && !group_mark_bound { + return; + } + + render_common_modifiers(&common_modifiers, base_x, base_y); + + let mut next_x = base_x + render_common_modifiers(&common_modifiers, base_x, base_y); + + if pane_group_bound { + next_x = render_toggle_group_ribbon(&pane_group_key, next_x, base_y); + } + + if group_mark_bound { + render_follow_focus_ribbon(&group_mark_key, next_x, base_y, mode_info); + } +} + +fn group_controls_length(mode_info: &ModeInfo) -> usize { + let keymap = mode_info.get_mode_keybinds(); + let (common_modifiers, pane_group_key, group_mark_key) = extract_key_bindings(&keymap); + + let pane_group_bound = pane_group_key != "UNBOUND"; + let group_mark_bound = group_mark_key != "UNBOUND"; + + let mut length = 0; + + if !common_modifiers.is_empty() { + let modifiers_text = format!( + "{} + ", + common_modifiers + .iter() + .map(|m| m.to_string()) + .collect::<Vec<_>>() + .join(" ") + ); + length += modifiers_text.chars().count(); + } + + if pane_group_bound { + let toggle_text = format!("<{}> Toggle", pane_group_key); + length += toggle_text.chars().count() + 4; + } + + if group_mark_bound { + let follow_text = format!("<{}> Follow Focus", group_mark_key); + length += follow_text.chars().count() + 4; + } + + length +} + +fn extract_key_bindings( + keymap: &[(KeyWithModifier, Vec<Action>)], +) -> (Vec<KeyModifier>, String, String) { + let pane_group_keys = get_key_for_action(keymap, &[Action::TogglePaneInGroup]); + let group_mark_keys = get_key_for_action(keymap, &[Action::ToggleGroupMarking]); + + let key_refs: Vec<&KeyWithModifier> = [pane_group_keys.first(), group_mark_keys.first()] + .into_iter() + .flatten() + .collect(); + + let common_modifiers = get_common_modifiers(key_refs); + + let pane_group_key = format_key_without_modifiers(&pane_group_keys, &common_modifiers); + let group_mark_key = format_key_without_modifiers(&group_mark_keys, &common_modifiers); + + (common_modifiers, pane_group_key, group_mark_key) +} + +fn format_key_without_modifiers( + keys: &[KeyWithModifier], + common_modifiers: &[KeyModifier], +) -> String { + keys.first() + .map(|key| format!("{}", key.strip_common_modifiers(&common_modifiers.to_vec()))) + .unwrap_or_else(|| "UNBOUND".to_string()) +} + +fn render_common_modifiers( + common_modifiers: &[KeyModifier], + base_x: usize, + base_y: usize, +) -> usize { + if !common_modifiers.is_empty() { + let modifiers_text = format!( + "{} + ", + common_modifiers + .iter() + .map(|m| m.to_string()) + .collect::<Vec<_>>() + .join(" ") + ); + + print_text_with_coordinates( + Text::new(&modifiers_text).color_all(0), + base_x, + base_y, + None, + None, + ); + + modifiers_text.chars().count() + } else { + 0 + } +} + +fn get_key_for_action( + keymap: &[(KeyWithModifier, Vec<Action>)], + target_action: &[Action], +) -> Vec<KeyWithModifier> { + keymap + .iter() + .find_map(|(key, actions)| { + if actions.first() == target_action.first() { + Some(key.clone()) + } else { + None + } + }) + .map(|key| vec![key]) + .unwrap_or_default() +} + +fn get_common_modifiers(keys: Vec<&KeyWithModifier>) -> Vec<KeyModifier> { + if keys.is_empty() { + return vec![]; + } + + let mut common = keys[0].key_modifiers.clone(); + + for key in keys.iter().skip(1) { + common = common.intersection(&key.key_modifiers).cloned().collect(); + } + + common.into_iter().collect() +} + +fn render_follow_focus_ribbon( + group_mark_key: &str, + x_position: usize, + base_y: usize, + mode_info: &ModeInfo, +) { + let follow_text = format!("<{}> Follow Focus", group_mark_key); + let key_highlight = format!("{}", group_mark_key); + + let mut ribbon = Text::new(&follow_text).color_substring(0, &key_highlight); + + if mode_info.currently_marking_pane_group.unwrap_or(false) { + ribbon = ribbon.selected(); + } + + print_ribbon_with_coordinates(ribbon, x_position, base_y, None, None); +} + +fn render_toggle_group_ribbon(pane_group_key: &str, base_x: usize, base_y: usize) -> usize { + let toggle_text = format!("<{}> Toggle", pane_group_key); + let key_highlight = format!("{}", pane_group_key); + + print_ribbon_with_coordinates( + Text::new(&toggle_text).color_substring(0, &key_highlight), + base_x, + base_y, + None, + None, + ); + + base_x + toggle_text.len() + 4 +} diff --git a/default-plugins/multiple-select/src/state.rs b/default-plugins/multiple-select/src/state.rs deleted file mode 100644 index 3bf863f8..00000000 --- a/default-plugins/multiple-select/src/state.rs +++ /dev/null @@ -1,634 +0,0 @@ -use crate::{ui::PaneItem, App}; -use std::collections::{BTreeMap, BTreeSet, HashSet}; - -use fuzzy_matcher::skim::SkimMatcherV2; -use fuzzy_matcher::FuzzyMatcher; - -use zellij_tile::prelude::*; - -#[derive(Debug, Default)] -pub struct MarkedIndex { - pub main_index: usize, - pub additional_indices: HashSet<usize>, -} - -impl MarkedIndex { - pub fn new(main_index: usize) -> Self { - MarkedIndex { - main_index, - additional_indices: HashSet::new(), - } - } -} - -impl MarkedIndex { - pub fn toggle_additional_mark(&mut self) { - if self.additional_indices.contains(&self.main_index) { - self.additional_indices.retain(|a| a != &self.main_index); - } else { - self.additional_indices.insert(self.main_index); - } - } -} - -#[derive(Debug)] -pub enum VisibilityAndFocus { - OnlyLeftSideVisible, - OnlyRightSideVisible, - BothSidesVisibleLeftSideFocused, - BothSidesVisibleRightSideFocused, -} - -impl Default for VisibilityAndFocus { - fn default() -> Self { - VisibilityAndFocus::OnlyLeftSideVisible - } -} - -impl VisibilityAndFocus { - pub fn only_left_side_is_focused(&self) -> bool { - match self { - VisibilityAndFocus::OnlyLeftSideVisible => true, - _ => false, - } - } - pub fn left_side_is_focused(&self) -> bool { - match self { - VisibilityAndFocus::OnlyLeftSideVisible - | VisibilityAndFocus::BothSidesVisibleLeftSideFocused => true, - _ => false, - } - } - pub fn right_side_is_focused(&self) -> bool { - match self { - VisibilityAndFocus::OnlyRightSideVisible - | VisibilityAndFocus::BothSidesVisibleRightSideFocused => true, - _ => false, - } - } - pub fn hide_left_side(&mut self) { - *self = VisibilityAndFocus::OnlyRightSideVisible - } - pub fn hide_right_side(&mut self) { - *self = VisibilityAndFocus::OnlyLeftSideVisible - } - pub fn focus_right_side(&mut self) { - *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused - } - pub fn toggle_focus(&mut self) { - match self { - VisibilityAndFocus::BothSidesVisibleLeftSideFocused => { - *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused - }, - VisibilityAndFocus::BothSidesVisibleRightSideFocused => { - *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused - }, - VisibilityAndFocus::OnlyLeftSideVisible => { - *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused - }, - VisibilityAndFocus::OnlyRightSideVisible => { - *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused - }, - } - } - pub fn show_both_sides(&mut self) { - match self { - VisibilityAndFocus::OnlyLeftSideVisible => { - *self = VisibilityAndFocus::BothSidesVisibleLeftSideFocused - }, - VisibilityAndFocus::OnlyRightSideVisible => { - *self = VisibilityAndFocus::BothSidesVisibleRightSideFocused - }, - VisibilityAndFocus::BothSidesVisibleLeftSideFocused - | VisibilityAndFocus::BothSidesVisibleRightSideFocused => { - // no-op - }, - } - } -} - -impl App { - pub fn react_to_zellij_state_update(&mut self, pane_manifest: PaneManifest) { - let is_first_update = self.right_side_panes.is_empty() && self.left_side_panes.is_empty(); - let panes_on_the_left_before = self.left_side_panes.len(); - let panes_on_the_right_before = self.right_side_panes.len(); - self.update_tab_info(&pane_manifest); - self.update_panes(pane_manifest); - if is_first_update && !self.right_side_panes.is_empty() { - // in this case, the plugin was started with an existing group - // most likely, the user wants to perform operations just on this group, so we - // only show the group, giving the option to add more panes - self.visibility_and_focus.hide_left_side(); - } - let pane_count_changed = (panes_on_the_left_before != self.left_side_panes.len()) - || (panes_on_the_right_before != self.right_side_panes.len()); - if !is_first_update && pane_count_changed { - let has_panes_on_the_right = !self.right_side_panes.is_empty(); - let has_panes_on_the_left = !self.left_side_panes.is_empty(); - if has_panes_on_the_right && has_panes_on_the_left { - self.visibility_and_focus.show_both_sides(); - } else if has_panes_on_the_right { - self.visibility_and_focus.hide_left_side(); - } else if has_panes_on_the_left { - self.visibility_and_focus.hide_right_side(); - } - } - } - pub fn update_panes(&mut self, pane_manifest: PaneManifest) { - let mut all_panes = BTreeMap::new(); - for (_tab_index, pane_infos) in pane_manifest.panes { - for pane_info in pane_infos { - if pane_info.is_selectable { - if pane_info.is_plugin { - all_panes.insert(PaneId::Plugin(pane_info.id), pane_info); - } else { - all_panes.insert(PaneId::Terminal(pane_info.id), pane_info); - } - } - } - } - self.left_side_panes - .retain(|p| all_panes.contains_key(&p.id)); - self.right_side_panes - .retain(|p| all_panes.contains_key(&p.id)); - let mut new_selected_panes: BTreeMap<usize, PaneItem> = BTreeMap::new(); // usize -> index_in_pane_group - for (pane_id, pane) in all_panes.into_iter() { - let is_known = self - .left_side_panes - .iter() - .find(|p| p.id == pane_id) - .is_some() - || self - .right_side_panes - .iter() - .find(|p| p.id == pane_id) - .is_some(); - let index_in_pane_group = self - .own_client_id - .and_then(|own_client_id| pane.index_in_pane_group.get(&own_client_id)); - let is_grouped_for_own_client_id = index_in_pane_group.is_some(); - if !is_known { - if is_grouped_for_own_client_id { - if let Some(index_in_pane_group) = index_in_pane_group { - // we do this rather than adding them directly to right_side_panes so that - // we can make sure they're in the same order as the group is so that - // things like stacking order will do the right thing - new_selected_panes.insert( - *index_in_pane_group, - PaneItem { - text: pane.title, - id: pane_id, - color_indices: vec![], - }, - ); - } - } else { - self.left_side_panes.push(PaneItem { - text: pane.title, - id: pane_id, - color_indices: vec![], - }); - } - } else { - if is_grouped_for_own_client_id { - if let Some(position) = - self.left_side_panes.iter().position(|p| p.id == pane_id) - { - // pane was added to a pane group outside the plugin (eg. with mouse selection) - let mut pane = self.left_side_panes.remove(position); - pane.clear(); - self.right_side_panes.push(pane); - } - } else { - if let Some(position) = - self.right_side_panes.iter().position(|p| p.id == pane_id) - { - // pane was removed from a pane group outside the plugin (eg. with mouse selection) - let mut pane = self.right_side_panes.remove(position); - pane.clear(); - self.left_side_panes.push(pane); - } - } - } - } - for (_index_in_pane_group, pane_item) in new_selected_panes.into_iter() { - self.right_side_panes.push(pane_item); - } - } - pub fn update_tab_info(&mut self, pane_manifest: &PaneManifest) { - for (tab_index, pane_infos) in &pane_manifest.panes { - for pane_info in pane_infos { - if pane_info.is_plugin && Some(pane_info.id) == self.own_plugin_id { - self.own_tab_index = Some(*tab_index); - } - } - } - self.total_tabs_in_session = Some(pane_manifest.panes.keys().count()); - } - pub fn update_search_results(&mut self) { - let mut matches = vec![]; - let matcher = SkimMatcherV2::default().use_cache(true); - for pane_item in &self.left_side_panes { - if let Some((score, indices)) = - matcher.fuzzy_indices(&pane_item.text, &self.search_string) - { - let mut pane_item = pane_item.clone(); - pane_item.color_indices = indices; - matches.push((score, pane_item)); - } - } - matches.sort_by(|(a_score, _a), (b_score, _b)| b_score.cmp(&a_score)); - if self.search_string.is_empty() { - self.search_results = None; - } else { - self.search_results = Some( - matches - .into_iter() - .map(|(_s, pane_item)| pane_item) - .collect(), - ); - } - } - pub fn group_panes_in_zellij(&mut self, pane_ids: Vec<PaneId>) { - group_and_ungroup_panes(pane_ids, vec![]); - } - pub fn ungroup_panes_in_zellij(&mut self, pane_ids: Vec<PaneId>) { - group_and_ungroup_panes(vec![], pane_ids); - } - pub fn update_highlighted_panes(&self) { - let mut pane_ids_to_highlight = vec![]; - let mut pane_ids_to_unhighlight = vec![]; - if let Some(marked_index) = &self.marked_index { - if self.visibility_and_focus.left_side_is_focused() { - if let Some(main_index_pane_id) = self - .search_results - .as_ref() - .and_then(|s| s.get(marked_index.main_index)) - .or_else(|| self.left_side_panes.get(marked_index.main_index)) - .map(|p| p.id) - { - pane_ids_to_highlight.push(main_index_pane_id); - } - for index in &marked_index.additional_indices { - if let Some(pane_id) = self - .search_results - .as_ref() - .and_then(|s| s.get(*index)) - .or_else(|| self.left_side_panes.get(*index)) - .map(|p| p.id) - { - pane_ids_to_highlight.push(pane_id); - } - } - } else { - if let Some(main_index_pane_id) = self - .right_side_panes - .get(marked_index.main_index) - .map(|p| p.id) - { - pane_ids_to_highlight.push(main_index_pane_id); - } - for index in &marked_index.additional_indices { - if let Some(pane_id) = self.right_side_panes.get(*index).map(|p| p.id) { - pane_ids_to_highlight.push(pane_id); - } - } - } - } - for pane in &self.left_side_panes { - if !pane_ids_to_highlight.contains(&pane.id) { - pane_ids_to_unhighlight.push(pane.id); - } - } - for pane in &self.right_side_panes { - if !pane_ids_to_highlight.contains(&pane.id) { - pane_ids_to_unhighlight.push(pane.id); - } - } - highlight_and_unhighlight_panes(pane_ids_to_highlight, pane_ids_to_unhighlight); - } - pub fn unhighlight_all_panes(&mut self) { - let mut pane_ids_to_unhighlight = HashSet::new(); - for pane_item in &self.left_side_panes { - pane_ids_to_unhighlight.insert(pane_item.id); - } - for pane_item in &self.right_side_panes { - pane_ids_to_unhighlight.insert(pane_item.id); - } - highlight_and_unhighlight_panes(vec![], pane_ids_to_unhighlight.into_iter().collect()); - } - pub fn ungroup_all_panes(&mut self) { - let mut unselected_panes = vec![]; - for pane_item in self.right_side_panes.iter_mut() { - pane_item.clear(); - unselected_panes.push(pane_item.id); - } - self.left_side_panes.append(&mut self.right_side_panes); - self.ungroup_panes_in_zellij(unselected_panes); - self.visibility_and_focus.hide_right_side(); - self.marked_index = None; - } - pub fn ungroup_all_panes_and_close_self(&mut self) { - let mut pane_ids_to_ungroup = HashSet::new(); - for pane_item in &self.left_side_panes { - pane_ids_to_ungroup.insert(pane_item.id); - } - for pane_item in &self.right_side_panes { - pane_ids_to_ungroup.insert(pane_item.id); - } - group_and_ungroup_panes(vec![], pane_ids_to_ungroup.into_iter().collect()); - close_self(); - } - pub fn group_panes(&mut self, mut marked_index: MarkedIndex, keep_left_side_focused: bool) { - let mut all_selected_indices: BTreeSet<usize> = - marked_index.additional_indices.drain().collect(); - all_selected_indices.insert(marked_index.main_index); - - // reverse so that the indices will remain consistent while - // removing - let mut selected_panes = vec![]; - for index in all_selected_indices.iter().rev() { - let index = self - .search_results - .as_mut() - .and_then(|search_results| { - if search_results.len() > *index { - Some(search_results.remove(*index)) - } else { - None - } - }) - .and_then(|selected_search_result| { - self.left_side_panes - .iter() - .position(|p| p.id == selected_search_result.id) - }) - .unwrap_or(*index); - if self.left_side_panes.len() > index { - let selected_pane = self.left_side_panes.remove(index); - selected_panes.push(selected_pane); - } - } - let pane_ids_to_make_selected: Vec<PaneId> = selected_panes.iter().map(|p| p.id).collect(); - self.right_side_panes - .append(&mut selected_panes.into_iter().rev().collect()); - - let displayed_list_len = match self.search_results.as_ref() { - Some(search_results) => search_results.len(), - None => self.left_side_panes.len(), - }; - - if displayed_list_len == 0 { - self.handle_left_side_emptied(); - } else if keep_left_side_focused { - if marked_index.main_index > displayed_list_len.saturating_sub(1) { - self.marked_index = Some(MarkedIndex::new(displayed_list_len.saturating_sub(1))); - } else { - self.marked_index = Some(marked_index); - } - self.visibility_and_focus.show_both_sides(); - } else { - self.visibility_and_focus.focus_right_side(); - } - - self.group_panes_in_zellij(pane_ids_to_make_selected); - self.update_highlighted_panes(); - } - pub fn ungroup_panes(&mut self, mut marked_index: MarkedIndex) { - let mut all_selected_indices: BTreeSet<usize> = - marked_index.additional_indices.drain().collect(); - all_selected_indices.insert(marked_index.main_index); - - // reverse so that the indices will remain consistent while - // removing - let mut selected_panes = vec![]; - for index in all_selected_indices.iter().rev() { - if self.right_side_panes.len() > *index { - let mut selected_pane = self.right_side_panes.remove(*index); - selected_pane.clear(); - selected_panes.push(selected_pane); - } - } - self.ungroup_panes_in_zellij(selected_panes.iter().map(|p| p.id).collect()); - self.left_side_panes - .append(&mut selected_panes.into_iter().rev().collect()); - - if self.right_side_panes.is_empty() { - self.marked_index = None; - self.visibility_and_focus.hide_right_side(); - } else if marked_index.main_index > self.right_side_panes.len().saturating_sub(1) { - self.marked_index = Some(MarkedIndex::new( - self.right_side_panes.len().saturating_sub(1), - )); - self.visibility_and_focus.show_both_sides(); - } else { - self.marked_index = Some(marked_index); - self.visibility_and_focus.show_both_sides(); - } - self.update_highlighted_panes(); - } - pub fn group_search_results(&mut self, search_results: Vec<PaneItem>) { - let mut pane_ids_to_make_selected = vec![]; - for search_result in search_results { - let pane_id = search_result.id; - pane_ids_to_make_selected.push(pane_id); - self.left_side_panes.retain(|p| p.id != pane_id); - self.right_side_panes.push(search_result); - } - self.group_panes_in_zellij(pane_ids_to_make_selected); - } - pub fn group_all_panes(&mut self) { - let pane_ids_to_make_selected: Vec<PaneId> = - self.left_side_panes.iter().map(|p| p.id).collect(); - self.right_side_panes.append(&mut self.left_side_panes); - self.group_panes_in_zellij(pane_ids_to_make_selected); - } - pub fn handle_left_side_emptied(&mut self) { - self.visibility_and_focus.hide_left_side(); - self.previous_search_string = self.search_string.drain(..).collect(); - self.marked_index = None; - self.search_results = None; - self.update_highlighted_panes(); - } - pub fn move_marked_index_down(&mut self) { - match self.marked_index.as_mut() { - Some(marked_index) => { - let is_searching = self.search_results.is_some(); - let search_result_count = - self.search_results.as_ref().map(|s| s.len()).unwrap_or(0); - if self.visibility_and_focus.left_side_is_focused() - && is_searching - && marked_index.main_index == search_result_count.saturating_sub(1) - { - marked_index.main_index = 0; - } else if self.visibility_and_focus.left_side_is_focused() - && !is_searching - && marked_index.main_index == self.left_side_panes.len().saturating_sub(1) - { - marked_index.main_index = 0; - } else if self.visibility_and_focus.right_side_is_focused() - && marked_index.main_index == self.right_side_panes.len().saturating_sub(1) - { - marked_index.main_index = 0; - } else { - marked_index.main_index += 1 - } - }, - None => { - if self.visibility_and_focus.left_side_is_focused() { - let is_searching = self.search_results.is_some(); - let has_search_results = self - .search_results - .as_ref() - .map(|s| !s.is_empty()) - .unwrap_or(false); - if is_searching && has_search_results { - self.marked_index = Some(MarkedIndex::new(0)); - } else if !is_searching && !self.left_side_panes.is_empty() { - self.marked_index = Some(MarkedIndex::new(0)); - } - } else if self.visibility_and_focus.right_side_is_focused() - && !self.right_side_panes.is_empty() - { - self.marked_index = Some(MarkedIndex::new(0)); - } - }, - } - self.update_highlighted_panes(); - } - pub fn move_marked_index_up(&mut self) { - match self.marked_index.as_mut() { - Some(marked_index) => { - if self.visibility_and_focus.left_side_is_focused() && marked_index.main_index == 0 - { - if let Some(search_result_count) = self.search_results.as_ref().map(|s| s.len()) - { - marked_index.main_index = search_result_count.saturating_sub(1); - } else { - marked_index.main_index = self.left_side_panes.len().saturating_sub(1); - } - } else if self.visibility_and_focus.right_side_is_focused() - && marked_index.main_index == 0 - { - marked_index.main_index = self.right_side_panes.len().saturating_sub(1); - } else { - marked_index.main_index = marked_index.main_index.saturating_sub(1); - } - }, - None => { - if self.visibility_and_focus.left_side_is_focused() { - let is_searching = self.search_results.is_some(); - let has_search_results = self - .search_results - .as_ref() - .map(|s| !s.is_empty()) - .unwrap_or(false); - if is_searching && has_search_results { - let search_results_count = - self.search_results.as_ref().map(|s| s.len()).unwrap_or(0); - self.marked_index = - Some(MarkedIndex::new(search_results_count.saturating_sub(1))); - } else if !is_searching && !self.left_side_panes.is_empty() { - self.marked_index = Some(MarkedIndex::new( - self.left_side_panes.len().saturating_sub(1), - )); - } - } else if self.visibility_and_focus.right_side_is_focused() - && !self.right_side_panes.is_empty() - { - self.marked_index = Some(MarkedIndex::new( - self.right_side_panes.len().saturating_sub(1), - )); - } - }, - } - self.update_highlighted_panes(); - } - pub fn mark_entry(&mut self) { - if let Some(marked_index) = self.marked_index.as_mut() { - marked_index.toggle_additional_mark(); - self.update_highlighted_panes(); - } - } - pub fn break_grouped_panes_to_new_tab(&mut self) { - let pane_ids_to_break_to_new_tab: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - let title_for_new_tab = if !self.previous_search_string.is_empty() { - Some(self.previous_search_string.clone()) - } else { - None - }; - break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true); - self.ungroup_panes_in_zellij(pane_ids_to_break_to_new_tab); - close_self(); - } - pub fn stack_grouped_panes(&mut self) { - let pane_ids_to_stack: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - stack_panes(pane_ids_to_stack.clone()); - self.ungroup_panes_in_zellij(pane_ids_to_stack); - close_self(); - } - pub fn float_grouped_panes(&mut self) { - let pane_ids_to_float: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - float_multiple_panes(pane_ids_to_float.clone()); - self.ungroup_panes_in_zellij(pane_ids_to_float); - close_self(); - } - pub fn embed_grouped_panes(&mut self) { - let pane_ids_to_embed: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - embed_multiple_panes(pane_ids_to_embed.clone()); - self.ungroup_panes_in_zellij(pane_ids_to_embed); - close_self(); - } - pub fn break_grouped_panes_right(&mut self) { - if let Some(own_tab_index) = self.own_tab_index { - if Some(own_tab_index + 1) < self.total_tabs_in_session { - let pane_ids_to_break_right: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - break_panes_to_tab_with_index(&pane_ids_to_break_right, own_tab_index + 1, true); - } else { - let pane_ids_to_break_to_new_tab: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - let title_for_new_tab = if !self.previous_search_string.is_empty() { - Some(self.previous_search_string.clone()) - } else { - None - }; - break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true); - } - close_self(); - } - } - pub fn break_grouped_panes_left(&mut self) { - if let Some(own_tab_index) = self.own_tab_index { - if own_tab_index > 0 { - let pane_ids_to_break_left: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - break_panes_to_tab_with_index( - &pane_ids_to_break_left, - own_tab_index.saturating_sub(1), - true, - ); - } else { - let pane_ids_to_break_to_new_tab: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - let title_for_new_tab = if !self.previous_search_string.is_empty() { - Some(self.previous_search_string.clone()) - } else { - None - }; - break_panes_to_new_tab(&pane_ids_to_break_to_new_tab, title_for_new_tab, true); - } - close_self(); - } - } - pub fn close_grouped_panes(&mut self) { - let pane_ids_to_close: Vec<PaneId> = - self.right_side_panes.drain(..).map(|p| p.id).collect(); - close_multiple_panes(pane_ids_to_close); - close_self(); - } -} diff --git a/default-plugins/multiple-select/src/ui.rs b/default-plugins/multiple-select/src/ui.rs deleted file mode 100644 index 0c74b0fe..00000000 --- a/default-plugins/multiple-select/src/ui.rs +++ /dev/null @@ -1,873 +0,0 @@ -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, - ); - } -} |
