aboutsummaryrefslogtreecommitdiffstats
path: root/default-plugins
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2026-03-10 22:47:09 +0100
committerGitHub <noreply@github.com>2026-03-10 22:47:09 +0100
commit578a7db7d75025ee5113a829db36ba1e4de98fe3 (patch)
treeb78b44d050e3b1c8774a5e945394f464d163138e /default-plugins
parentWeb security hardening (#4830) (diff)
downloadsl0ppij-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')
-rw-r--r--default-plugins/layout-manager/src/screens/layout_list/mod.rs101
-rw-r--r--default-plugins/layout-manager/src/screens/layout_list/search.rs30
-rw-r--r--default-plugins/layout-manager/src/ui/layout_table.rs42
-rw-r--r--default-plugins/session-manager/src/main.rs1
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 {