Skip to content

Commit 578a7db

Browse files
authored
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
1 parent 458f90c commit 578a7db

File tree

5 files changed

+126
-50
lines changed

5 files changed

+126
-50
lines changed

CHANGELOG.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
2121
* feat: allow overriding layouts at runtime (https://github.com/zellij-org/zellij/pull/4566)
2222
* build: Update Rust toolchain to 1.92.0 (https://github.com/zellij-org/zellij/pull/4579)
2323
* docs: Explain Rust toolchain update strategy in CONTRIBUTING (https://github.com/zellij-org/zellij/pull/4585)
24-
* feat: new `layout-manager` interface and plugin API commands (https://github.com/zellij-org/zellij/pull/4601)
24+
* feat: new `layout-manager` interface and plugin API commands (https://github.com/zellij-org/zellij/pull/4601 and https://github.com/zellij-org/zellij/pull/4831)
2525
* fix: keep serializing sessions resurrected from the welcome screen (https://github.com/zellij-org/zellij/pull/4604)
2626
* fix: sanitize session names when deleting them from the CLI (https://github.com/zellij-org/zellij/pull/4583)
2727
* fix: properly close the welcome screen session when switching sessions away from it (https://github.com/zellij-org/zellij/pull/4605)

default-plugins/layout-manager/src/screens/layout_list/mod.rs

Lines changed: 78 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ impl Default for LayoutListScreen {
2727
retain_plugin_panes: false,
2828
apply_only_to_active_tab: false,
2929
show_more_override_options: false,
30-
search_state: SearchState::new(),
30+
search_state: SearchState::new_in_search_mode(),
3131
last_rows: 0,
3232
last_cols: 0,
3333
}
@@ -42,7 +42,7 @@ impl LayoutListScreen {
4242
retain_plugin_panes: false,
4343
apply_only_to_active_tab: false,
4444
show_more_override_options: false,
45-
search_state: SearchState::new(),
45+
search_state: SearchState::new_in_search_mode(),
4646
last_rows: 0,
4747
last_cols: 0,
4848
}
@@ -101,7 +101,7 @@ impl LayoutListScreen {
101101
self.open_selected_layout(display_layouts);
102102
KeyResponse::none()
103103
},
104-
BareKey::Tab if key.has_no_modifiers() => {
104+
BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
105105
self.apply_selected_layout(display_layouts);
106106
KeyResponse::none()
107107
},
@@ -125,8 +125,8 @@ impl LayoutListScreen {
125125
KeyResponse::render()
126126
},
127127
BareKey::Esc if key.has_no_modifiers() => {
128-
self.clear_filter();
129-
KeyResponse::render()
128+
close_self();
129+
KeyResponse::none()
130130
},
131131
_ => KeyResponse::none(),
132132
}
@@ -138,27 +138,50 @@ impl LayoutListScreen {
138138
key: KeyWithModifier,
139139
display_layouts: &[DisplayLayout],
140140
) -> KeyResponse {
141-
// Special handling for Enter and Esc
141+
// Search-first mode: Enter opens, Ctrl+O applies, Tab autocompletes,
142+
// arrows navigate, Esc exits to management mode
142143
match key.bare_key {
143144
BareKey::Esc if key.has_no_modifiers() => {
144-
self.clear_filter();
145+
// Clear text first; only exit to management mode if already empty
146+
if self.search_state.get_filter_input().is_empty() {
147+
self.clear_filter();
148+
} else {
149+
self.search_state.get_filter_input_mut().clear();
150+
self.update_filter(display_layouts);
151+
}
145152
return KeyResponse::render();
146153
},
147154
BareKey::Enter if key.has_no_modifiers() => {
148-
if self.search_state.get_filter_input().is_empty()
149-
|| self.search_state.get_search_results().is_empty()
150-
{
151-
self.clear_filter();
152-
} else {
153-
self.search_state.stop_typing();
154-
show_cursor(None);
155+
// Open the currently selected layout as new tab(s)
156+
self.open_selected_layout(display_layouts);
157+
return KeyResponse::none();
158+
},
159+
BareKey::Char('o') if key.has_modifiers(&[KeyModifier::Ctrl]) => {
160+
// Apply/override the currently selected layout to the session
161+
self.apply_selected_layout(display_layouts);
162+
return KeyResponse::none();
163+
},
164+
BareKey::Tab if key.has_no_modifiers() => {
165+
// Complete: fill input with selected match name
166+
if self.search_state.fill_input_with_selected_match() {
167+
self.update_filter(display_layouts);
155168
}
156169
return KeyResponse::render();
157170
},
171+
BareKey::Up if key.has_no_modifiers() => {
172+
// Navigate filtered results while typing
173+
self.navigate_up(display_layouts);
174+
return KeyResponse::render();
175+
},
176+
BareKey::Down if key.has_no_modifiers() => {
177+
// Navigate filtered results while typing
178+
self.navigate_down(display_layouts);
179+
return KeyResponse::render();
180+
},
158181
_ => {},
159182
}
160183

161-
// Pass all keys to TextInput
184+
// Pass remaining keys to TextInput
162185
let action = self.search_state.get_filter_input_mut().handle_key(key);
163186

164187
match action {
@@ -167,8 +190,13 @@ impl LayoutListScreen {
167190
KeyResponse::render()
168191
},
169192
InputAction::Cancel => {
170-
// Ctrl-C or Esc - clear filter
171-
self.clear_filter();
193+
// Ctrl-C - clear text first; only exit to management mode if already empty
194+
if self.search_state.get_filter_input().is_empty() {
195+
self.clear_filter();
196+
} else {
197+
self.search_state.get_filter_input_mut().clear();
198+
self.update_filter(display_layouts);
199+
}
172200
KeyResponse::render()
173201
},
174202
InputAction::Submit => {
@@ -304,6 +332,7 @@ impl LayoutListScreen {
304332
self.apply_only_to_active_tab,
305333
Default::default(),
306334
);
335+
close_self();
307336
}
308337
}
309338

@@ -364,10 +393,16 @@ impl LayoutListScreen {
364393
}
365394

366395
fn open_selected_layout(&self, display_layouts: &[DisplayLayout]) {
367-
if let Some(DisplayLayout::Valid(chosen_layout)) =
368-
display_layouts.get(self.selected_layout_index)
369-
{
370-
new_tabs_with_layout_info(chosen_layout);
396+
let selected = display_layouts.get(self.selected_layout_index);
397+
if let Some(DisplayLayout::Valid(chosen_layout)) = selected {
398+
let tab_ids = new_tabs_with_layout_info(chosen_layout);
399+
if self.should_default_to_current_tab(display_layouts) {
400+
if let Some(&tab_id) = tab_ids.first() {
401+
let layout_name = selected.unwrap().name();
402+
rename_tab_with_id(tab_id as u64, layout_name);
403+
}
404+
}
405+
close_self();
371406
}
372407
}
373408

@@ -385,6 +420,13 @@ impl LayoutListScreen {
385420
let (base_x, base_y) =
386421
self.calculate_base_coordinates(rows, cols, total_width, total_height);
387422

423+
// In search mode, shift everything down by 1 row
424+
let base_y = if self.search_state.is_typing() || self.search_state.is_active() {
425+
base_y + 1
426+
} else {
427+
base_y
428+
};
429+
388430
let layouts_to_render = self.effective_layouts(display_layouts);
389431
let (content_height, controls_y) = self.calculate_layout(rows, &display_layouts);
390432

@@ -461,13 +503,17 @@ impl LayoutListScreen {
461503
let rows_in_table = display_layouts.len() + 1; // 1 for the title row
462504
let controls_height = self.get_controls_height();
463505
let filter_row_height = if self.is_searching() { 1 } else { 0 };
506+
let search_mode_offset = if self.is_searching() { 1 } else { 0 };
464507
let padding = 1;
465508
let mut content_height = std::cmp::max(rows_in_table, 5);
466-
if content_height + controls_height + padding + filter_row_height >= rows {
509+
if content_height + controls_height + padding + filter_row_height + search_mode_offset
510+
>= rows
511+
{
467512
content_height = rows
468513
.saturating_sub(controls_height)
469514
.saturating_sub(padding)
470515
.saturating_sub(filter_row_height)
516+
.saturating_sub(search_mode_offset)
471517
}
472518
let controls_y = content_height + padding + filter_row_height;
473519

@@ -520,10 +566,12 @@ impl LayoutListScreen {
520566
display_layouts: &[DisplayLayout],
521567
) -> (usize, usize) {
522568
let filter_row_height = if self.is_searching() { 1 } else { 0 };
569+
let search_mode_offset = if self.is_searching() { 1 } else { 0 };
523570
let (content_height, _) = self.calculate_layout(rows, display_layouts);
524571
let padding = 1;
525572
let controls_height = self.get_controls_height();
526-
let total_height = filter_row_height + content_height + padding + controls_height;
573+
let total_height =
574+
filter_row_height + search_mode_offset + content_height + padding + controls_height;
527575

528576
let controls = Controls::new(
529577
self.retain_terminal_panes,
@@ -612,6 +660,13 @@ impl LayoutListScreen {
612660
total_height,
613661
);
614662

663+
// In search mode, cursor must account for the extra row offset
664+
let base_y = if self.search_state.is_typing() || self.search_state.is_active() {
665+
base_y + 1
666+
} else {
667+
base_y
668+
};
669+
615670
self.search_state
616671
.update_filter(display_layouts, base_x, base_y);
617672

default-plugins/layout-manager/src/screens/layout_list/search.rs

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,22 @@ impl Default for SearchState {
3434
}
3535

3636
impl SearchState {
37-
pub fn new() -> Self {
38-
Self::default()
37+
pub fn new_in_search_mode() -> Self {
38+
Self {
39+
typing_filter: true,
40+
..Self::default()
41+
}
42+
}
43+
44+
pub fn fill_input_with_selected_match(&mut self) -> bool {
45+
if let Some(result) = self.search_results.get(self.selected_search_index) {
46+
let name = result.layout.name();
47+
if self.filter_input.get_text() != name {
48+
self.filter_input.set_text(name);
49+
return true;
50+
}
51+
}
52+
false
3953
}
4054

4155
pub fn is_active(&self) -> bool {
@@ -50,10 +64,6 @@ impl SearchState {
5064
self.typing_filter = true;
5165
}
5266

53-
pub fn stop_typing(&mut self) {
54-
self.typing_filter = false;
55-
}
56-
5767
pub fn get_filter_input(&self) -> &TextInput {
5868
&self.filter_input
5969
}
@@ -80,7 +90,7 @@ impl SearchState {
8090
base_x: usize,
8191
base_y: usize,
8292
) {
83-
let filter_prompt = "Filter:";
93+
let filter_prompt = "Layout:";
8494
let filter_text = self.filter_input.get_text();
8595

8696
// Clear results if filter is empty
@@ -155,15 +165,15 @@ impl SearchState {
155165
let filter_text_str = self.filter_input.get_text();
156166
let filter_text = if self.typing_filter {
157167
let mut filter_line =
158-
Text::new(format!("Filter: {}", filter_text_str)).color_substring(2, "Filter:");
168+
Text::new(format!("Layout: {}", filter_text_str)).color_substring(2, "Layout:");
159169
if !filter_text_str.is_empty() {
160170
filter_line = filter_line.color_last_substring(3, filter_text_str)
161171
}
162172
filter_line
163173
} else {
164-
Text::new(format!("Filter: {} (<Esc> - clear)", filter_text_str))
174+
Text::new(format!("Layout: {} (<Esc> - clear)", filter_text_str))
165175
.color_substring(3, "<Esc>")
166-
.color_substring(2, "Filter:")
176+
.color_substring(2, "Layout:")
167177
};
168178
print_text_with_coordinates(filter_text, base_x, base_y, None, None);
169179
}

default-plugins/layout-manager/src/ui/layout_table.rs

Lines changed: 26 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -184,45 +184,55 @@ impl Controls {
184184
}
185185

186186
fn get_typing_filter_controls(&self, max_cols: usize) -> (&str, &[&str]) {
187-
let long_text = "- <Enter> - accept filter";
188-
let short_text = "<Enter> - accept filter";
189-
let minimum_text = "<Enter> ...";
187+
let long_text =
188+
"- <↑↓> Navigate, <Tab> Complete, <Enter> Open, <Ctrl+o> Apply, <Esc> Manage & New";
189+
let short_text = "<↑↓>/<Tab>/<Enter>/<Ctrl+o>/<Esc> Nav/Complete/Open/Apply/Manage";
190+
let minimum_text = "<↑↓>/<Tab>/<Enter>/<Ctrl+o> ...";
190191
let text = if max_cols >= long_text.chars().count() {
191192
long_text
192193
} else if max_cols >= short_text.chars().count() {
193194
short_text
194195
} else {
195196
minimum_text
196197
};
197-
(text, &["<Enter>"])
198+
(text, &["<↑↓>", "<Tab>", "<Enter>", "<Ctrl+o>", "<Esc>"])
198199
}
199200

200201
fn get_filter_active_controls(&self, max_cols: usize) -> (&str, &[&str]) {
201-
let long_text = "- <Enter> Open, <↓↑> Nav, <e> Edit, <r> Rename, <Del> - Delete";
202-
let short_text = "<Enter>/<↓↑>/<e>/<r>/<Del> Open/Nav/Edit/Rename/Del";
203-
let minimum_text = "<Enter>/<↓↑>/<e>/<r>/<Del> ...";
202+
let long_text =
203+
"- <Enter> Open, <↓↑> Nav, <Ctrl+o> Apply, <e> Edit, <r> Rename, <Del> Delete";
204+
let short_text = "<Enter>/<↓↑>/<Ctrl+o>/<e>/<r>/<Del> Open/Nav/Apply/Edit/Rename/Del";
205+
let minimum_text = "<Enter>/<↓↑>/<Ctrl+o>/<e>/<r>/<Del> ...";
204206
let text = if max_cols >= long_text.chars().count() {
205207
long_text
206208
} else if max_cols >= short_text.chars().count() {
207209
short_text
208210
} else {
209211
minimum_text
210212
};
211-
(text, &["<Enter>", "<↓↑>", "<e>", "<r>", "<Del>"])
213+
(
214+
text,
215+
&["<Enter>", "<↓↑>", "<Ctrl+o>", "<e>", "<r>", "<Del>"],
216+
)
212217
}
213218

214219
fn get_default_controls(&self, max_cols: usize) -> (&str, &[&str]) {
215-
let long_text = "- <Enter> Open, <↓↑> Nav, </> Filter, <e> Edit, <r> Rename, <Del> - Del";
216-
let short_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> Open/Nav/Filter/Edit/Rename/Del";
217-
let minimum_text = "<Enter>/<↓↑>/</>/<e>/<r>/<Del> ...";
220+
let long_text =
221+
"- <Enter> Open, <↓↑> Nav, </> Search, <Ctrl+o> Apply, <e> Edit, <r> Rename, <Del> Del";
222+
let short_text =
223+
"<Enter>/<↓↑>/</>/<Ctrl+o>/<e>/<r>/<Del> Open/Nav/Search/Apply/Edit/Rename/Del";
224+
let minimum_text = "<Enter>/<↓↑>/</>/<Ctrl+o>/<e>/<r>/<Del> ...";
218225
let text = if max_cols >= long_text.chars().count() {
219226
long_text
220227
} else if max_cols >= short_text.chars().count() {
221228
short_text
222229
} else {
223230
minimum_text
224231
};
225-
(text, &["<Enter>", "<↓↑>", "</>", "<e>", "<r>", "<Del>"])
232+
(
233+
text,
234+
&["<Enter>", "<↓↑>", "</>", "<Ctrl+o>", "<e>", "<r>", "<Del>"],
235+
)
226236
}
227237

228238
fn get_basic_controls_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) {
@@ -240,19 +250,19 @@ impl Controls {
240250
"more"
241251
};
242252
let long_text = format!(
243-
"- <Tab> Override Session Layout, <?> {} options",
253+
"- <Ctrl+o> Override Session Layout, <?> {} options",
244254
toggle_word
245255
);
246-
let short_text = format!("<Tab> Override, <?> {} options", toggle_word);
247-
let minimum_text = format!("<Tab>/<?> ...");
256+
let short_text = format!("<Ctrl+o> Override, <?> {} options", toggle_word);
257+
let minimum_text = format!("<Ctrl+o>/<?> ...");
248258
let text = if max_cols >= long_text.chars().count() {
249259
long_text
250260
} else if max_cols >= short_text.chars().count() {
251261
short_text
252262
} else {
253263
minimum_text
254264
};
255-
(text, &["<Tab>", "<?>"])
265+
(text, &["<Ctrl+o>", "<?>"])
256266
}
257267

258268
fn get_new_layout_text_and_keys(&self, max_cols: usize) -> (&str, &[&str]) {

default-plugins/session-manager/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ impl ZellijPlugin for State {
8686
EventType::RunCommandResult,
8787
EventType::Timer,
8888
]);
89+
rename_plugin_pane(get_plugin_ids().plugin_id, "Session Manager");
8990
}
9091

9192
fn pipe(&mut self, pipe_message: PipeMessage) -> bool {

0 commit comments

Comments
 (0)