diff options
| author | Aram Drevekenin <aram@poor.dev> | 2026-03-10 22:47:09 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-03-10 22:47:09 +0100 |
| commit | 578a7db7d75025ee5113a829db36ba1e4de98fe3 (patch) | |
| tree | b78b44d050e3b1c8774a5e945394f464d163138e /default-plugins | |
| parent | Web security hardening (#4830) (diff) | |
| download | sl0ppij-578a7db7d75025ee5113a829db36ba1e4de98fe3.tar.gz sl0ppij-578a7db7d75025ee5113a829db36ba1e4de98fe3.tar.xz sl0ppij-578a7db7d75025ee5113a829db36ba1e4de98fe3.zip | |
Layout manager ux fixes (#4831)
* close self after applying and rename tab as layout
* general ui/ux fixes
* change session manager title on startup
* add pr
Diffstat (limited to 'default-plugins')
4 files changed, 125 insertions, 49 deletions
diff --git a/default-plugins/layout-manager/src/screens/layout_list/mod.rs b/default-plugins/layout-manager/src/screens/layout_list/mod.rs index e643657b..4c231b50 100644 --- a/default-plugins/layout-manager/src/screens/layout_list/mod.rs +++ b/default-plugins/layout-manager/src/screens/layout_list/mod.rs @@ -27,7 +27,7 @@ impl Default for LayoutListScreen { retain_plugin_panes: false, apply_only_to_active_tab: false, show_more_override_options: false, - search_state: SearchState::new(), + search_state: SearchState::new_in_search_mode(), last_rows: 0, last_cols: 0, } @@ -42,7 +42,7 @@ impl LayoutListScreen { retain_plugin_panes: false, apply_only_to_active_tab: false, show_more_override_options: false, - search_state: SearchState::new(), + search_state: SearchState::new_in_search_mode(), last_rows: 0, last_cols: 0, } @@ -101,7 +101,7 @@ impl LayoutListScreen { self.open_selected_layout(display_layouts); KeyResponse::none() }, - BareKey::Tab if key.has_no_modifiers() => { + BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Ctrl]) => { self.apply_selected_layout(display_layouts); KeyResponse::none() }, @@ -125,8 +125,8 @@ impl LayoutListScreen { KeyResponse::render() }, BareKey::Esc if key.has_no_modifiers() => { - self.clear_filter(); - KeyResponse::render() + close_self(); + KeyResponse::none() }, _ => KeyResponse::none(), } @@ -138,27 +138,50 @@ impl LayoutListScreen { key: KeyWithModifier, display_layouts: &[DisplayLayout], ) -> KeyResponse { - // Special handling for Enter and Esc + // Search-first mode: Enter opens, Ctrl+O applies, Tab autocompletes, + // arrows navigate, Esc exits to management mode match key.bare_key { BareKey::Esc if key.has_no_modifiers() => { - self.clear_filter(); + // Clear text first; only exit to management mode if already empty + if self.search_state.get_filter_input().is_empty() { + self.clear_filter(); + } else { + self.search_state.get_filter_input_mut().clear(); + self.update_filter(display_layouts); + } return KeyResponse::render(); }, BareKey::Enter if key.has_no_modifiers() => { - if self.search_state.get_filter_input().is_empty() - || self.search_state.get_search_results().is_empty() - { - self.clear_filter(); - } else { - self.search_state.stop_typing(); - show_cursor(None); + // Open the currently selected layout as new tab(s) + self.open_selected_layout(display_layouts); + return KeyResponse::none(); + }, + BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Ctrl]) => { + // Apply/override the currently selected layout to the session + self.apply_selected_layout(display_layouts); + return KeyResponse::none(); + }, + BareKey::Tab if key.has_no_modifiers() => { + // Complete: fill input with selected match name + if self.search_state.fill_input_with_selected_match() { + self.update_filter(display_layouts); } return KeyResponse::render(); }, + BareKey::Up if key.has_no_modifiers() => { + // Navigate filtered results while typing + self.navigate_up(display_layouts); + return KeyResponse::render(); + }, + BareKey::Down if key.has_no_modifiers() => { + // Navigate filtered results while typing + self.navigate_down(display_layouts); + return KeyResponse::render(); + }, _ => {}, } - // Pass all keys to TextInput + // Pass remaining keys to TextInput let action = self.search_state.get_filter_input_mut().handle_key(key); match action { @@ -167,8 +190,13 @@ impl LayoutListScreen { KeyResponse::render() }, InputAction::Cancel => { - // Ctrl-C or Esc - clear filter - self.clear_filter(); + // Ctrl-C - clear text first; only exit to management mode if already empty + if self.search_state.get_filter_input().is_empty() { + self.clear_filter(); + } else { + self.search_state.get_filter_input_mut().clear(); + self.update_filter(display_layouts); + } KeyResponse::render() }, InputAction::Submit => { @@ -304,6 +332,7 @@ impl LayoutListScreen { self.apply_only_to_active_tab, Default::default(), ); + close_self(); } } @@ -364,10 +393,16 @@ impl LayoutListScreen { } fn open_selected_layout(&self, display_layouts: &[DisplayLayout]) { - if let Some(DisplayLayout::Valid(chosen_layout)) = - display_layouts.get(self.selected_layout_index) - { - new_tabs_with_layout_info(chosen_layout); + let selected = display_layouts.get(self.selected_layout_index); + if let Some(DisplayLayout::Valid(chosen_layout)) = selected { + let tab_ids = new_tabs_with_layout_info(chosen_layout); + if self.should_default_to_current_tab(display_layouts) { + if let Some(&tab_id) = tab_ids.first() { + let layout_name = selected.unwrap().name(); + rename_tab_with_id(tab_id as u64, layout_name); + } + } + close_self(); } } @@ -385,6 +420,13 @@ impl LayoutListScreen { let (base_x, base_y) = self.calculate_base_coordinates(rows, cols, total_width, total_height); + // In search mode, shift everything down by 1 row + let base_y = if self.search_state.is_typing() || self.search_state.is_active() { + base_y + 1 + } else { + base_y + }; + let layouts_to_render = self.effective_layouts(display_layouts); let (content_height, controls_y) = self.calculate_layout(rows, &display_layouts); @@ -461,13 +503,17 @@ impl LayoutListScreen { let rows_in_table = display_layouts.len() + 1; // 1 for the title row let controls_height = self.get_controls_height(); let filter_row_height = if self.is_searching() { 1 } else { 0 }; + let search_mode_offset = if self.is_searching() { 1 } else { 0 }; let padding = 1; let mut content_height = std::cmp::max(rows_in_table, 5); - if content_height + controls_height + padding + filter_row_height >= rows { + if content_height + controls_height + padding + filter_row_height + search_mode_offset + >= rows + { content_height = rows .saturating_sub(controls_height) .saturating_sub(padding) .saturating_sub(filter_row_height) + .saturating_sub(search_mode_offset) } let controls_y = content_height + padding + filter_row_height; @@ -520,10 +566,12 @@ impl LayoutListScreen { display_layouts: &[DisplayLayout], ) -> (usize, usize) { let filter_row_height = if self.is_searching() { 1 } else { 0 }; + let search_mode_offset = if self.is_searching() { 1 } else { 0 }; let (content_height, _) = self.calculate_layout(rows, display_layouts); let padding = 1; let controls_height = self.get_controls_height(); - let total_height = filter_row_height + content_height + padding + controls_height; + let total_height = + filter_row_height + search_mode_offset + content_height + padding + controls_height; let controls = Controls::new( self.retain_terminal_panes, @@ -612,6 +660,13 @@ impl LayoutListScreen { total_height, ); + // In search mode, cursor must account for the extra row offset + let base_y = if self.search_state.is_typing() || self.search_state.is_active() { + base_y + 1 + } else { + base_y + }; + self.search_state .update_filter(display_layouts, base_x, base_y); diff --git a/default-plugins/layout-manager/src/screens/layout_list/search.rs b/default-plugins/layout-manager/src/screens/layout_list/search.rs index 1414d6a3..90c5a10a 100644 --- a/default-plugins/layout-manager/src/screens/layout_list/search.rs +++ b/default-plugins/layout-manager/src/screens/layout_list/search.rs @@ -34,8 +34,22 @@ impl Default for SearchState { } impl SearchState { - pub fn new() -> Self { - Self::default() + pub fn new_in_search_mode() -> Self { + Self { + typing_filter: true, + ..Self::default() + } + } + + pub fn fill_input_with_selected_match(&mut self) -> bool { + if let Some(result) = self.search_results.get(self.selected_search_index) { + let name = result.layout.name(); + if self.filter_input.get_text() != name { + self.filter_input.set_text(name); + return true; + } + } + false } pub fn is_active(&self) -> bool { @@ -50,10 +64,6 @@ impl SearchState { self.typing_filter = true; } - pub fn stop_typing(&mut self) { - self.typing_filter = false; - } - pub fn get_filter_input(&self) -> &TextInput { &self.filter_input } @@ -80,7 +90,7 @@ impl SearchState { base_x: usize, base_y: usize, ) { - let filter_prompt = "Filter:"; + let filter_prompt = "Layout:"; let filter_text = self.filter_input.get_text(); // Clear results if filter is empty @@ -155,15 +165,15 @@ impl SearchState { let filter_text_str = self.filter_input.get_text(); let filter_text = if self.typing_filter { let mut filter_line = - Text::new(format!("Filter: {}", filter_text_str)).color_substring(2, "Filter:"); + Text::new(format!("Layout: {}", filter_text_str)).color_substring(2, "Layout:"); if !filter_text_str.is_empty() { filter_line = filter_line.color_last_substring(3, filter_text_str) } filter_line } else { - Text::new(format!("Filter: {} (<Esc> - clear)", filter_text_str)) + Text::new(format!("Layout: {} (<Esc> - clear)", filter_text_str)) .color_substring(3, "<Esc>") - .color_substring(2, "Filter:") + .color_substring(2, "Layout:") }; print_text_with_coordinates(filter_text, base_x, base_y, None, None); } diff --git a/default-plugins/layout-manager/src/ui/layout_table.rs b/default-plugins/layout-manager/src/ui/layout_table.rs index 2f22f06e..3f42a8da 100644 --- a/default-plugins/layout-manager/src/ui/layout_table.rs +++ b/default-plugins/layout-manager/src/ui/layout_table.rs @@ -184,9 +184,10 @@ impl Controls { } fn get_typing_filter_controls(&self, max_cols: usize) -> (&str, &[&str]) { - let long_text = "- <Enter> - accept filter"; - let short_text = "<Enter> - accept filter"; - let minimum_text = "<Enter> ..."; + let long_text = + "- <↑↓> Navigate, <Tab> Complete, <Enter> Open, <Ctrl+o> Apply, <Esc> Manage & New"; + let short_text = "<↑↓>/<Tab>/<Enter>/<Ctrl+o>/<Esc> Nav/Complete/Open/Apply/Manage"; + let minimum_text = "<↑↓>/<Tab>/<Enter>/<Ctrl+o> ..."; let text = if max_cols >= long_text.chars().count() { long_text } else if max_cols >= short_text.chars().count() { @@ -194,13 +195,14 @@ impl Controls { } else { minimum_text }; - (text, &["<Enter>"]) + (text, &["<↑↓>", "<Tab>", "<Enter>", "<Ctrl+o>", "<Esc>"]) } fn get_filter_active_controls(&self, max_cols: usize) -> (&str, &[&str]) { - let long_text = "- <Enter> Open, <↓↑> Nav, <e> Edit, <r> Rename, <Del> - Delete"; - let short_text = "<Enter>/<↓↑>/<e>/<r>/<Del> Open/Nav/Edit/Rename/Del"; - let minimum_text = "<Enter>/<↓↑>/<e>/<r>/<Del> ..."; + let long_text = + "- <Enter> Open, <↓↑> Nav, <Ctrl+o> Apply, <e> Edit, <r> Rename, <Del> Delete"; + let short_text = "<Enter>/<↓↑>/<Ctrl+o>/<e>/<r>/<Del> Open/Nav/Apply/Edit/Rename/Del"; + let minimum_text = "<Enter>/<↓↑>/<Ctrl+o>/<e>/<r>/<Del> ..."; let text = if max_cols >= long_text.chars().count() { long_text } else if max_cols >= short_text.chars().count() { @@ -208,13 +210,18 @@ impl Controls { } else { minimum_text }; - (text, &["<Enter>", "<↓↑>", "<e>", "<r>", "<Del>"]) + ( + text, + &["<Enter>", "<↓↑>", "<Ctrl+o>", "<e>", "<r>", "<Del>"], + ) } fn get_default_controls(&self, max_cols: usize) -> (&str, &[&str]) { - let long_text = "- <Enter> Open, <↓↑> Nav, </> Filter, <e> Edit, <r> Rename, <Del> - Del"; - let short_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> Open/Nav/Filter/Edit/Rename/Del"; - let minimum_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> ..."; + let long_text = + "- <Enter> Open, <↓↑> Nav, </> Search, <Ctrl+o> Apply, <e> Edit, <r> Rename, <Del> Del"; + let short_text = + "<Enter>/<↓↑>/</>/<Ctrl+o>/<e>/<r>/<Del> Open/Nav/Search/Apply/Edit/Rename/Del"; + let minimum_text = "<Enter>/<↓↑>/</>/<Ctrl+o>/<e>/<r>/<Del> ..."; let text = if max_cols >= long_text.chars().count() { long_text } else if max_cols >= short_text.chars().count() { @@ -222,7 +229,10 @@ impl Controls { } else { minimum_text }; - (text, &["<Enter>", "<↓↑>", "</>", "<e>", "<r>", "<Del>"]) + ( + text, + &["<Enter>", "<↓↑>", "</>", "<Ctrl+o>", "<e>", "<r>", "<Del>"], + ) } fn get_basic_controls_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) { @@ -240,11 +250,11 @@ impl Controls { "more" }; let long_text = format!( - "- <Tab> Override Session Layout, <?> {} options", + "- <Ctrl+o> Override Session Layout, <?> {} options", toggle_word ); - let short_text = format!("<Tab> Override, <?> {} options", toggle_word); - let minimum_text = format!("<Tab>/<?> ..."); + let short_text = format!("<Ctrl+o> Override, <?> {} options", toggle_word); + let minimum_text = format!("<Ctrl+o>/<?> ..."); let text = if max_cols >= long_text.chars().count() { long_text } else if max_cols >= short_text.chars().count() { @@ -252,7 +262,7 @@ impl Controls { } else { minimum_text }; - (text, &["<Tab>", "<?>"]) + (text, &["<Ctrl+o>", "<?>"]) } fn get_new_layout_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) { diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs index 59bdf258..c4ad26fb 100644 --- a/default-plugins/session-manager/src/main.rs +++ b/default-plugins/session-manager/src/main.rs @@ -86,6 +86,7 @@ impl ZellijPlugin for State { EventType::RunCommandResult, EventType::Timer, ]); + rename_plugin_pane(get_plugin_ids().plugin_id, "Session Manager"); } fn pipe(&mut self, pipe_message: PipeMessage) -> bool { |
