aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2024-02-06 14:26:14 +0100
committerGitHub <noreply@github.com>2024-02-06 14:26:14 +0100
commit6b20a958f4e8db5614fffb27fe5d32f07ddfe855 (patch)
treec8383fb7941b5e0c23cb348a655f2ea0fafd8668
parentdocs(readme): add sponsor (diff)
downloadsl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.tar.gz
sl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.tar.xz
sl0ppij-6b20a958f4e8db5614fffb27fe5d32f07ddfe855.zip
feat(sessions): welcome screen (#3112)
* prototype - can send layout name for new session from session-manager * feat(sessions): ui for selecting layout for new session in the session-manager * fix: send available layouts to plugins * make tests compile * fix tests * improve ui * fix: respect built-in layouts * ui for built-in layouts * some cleanups * style(fmt): rustfmt * welcome screen ui * fix: make sure layout config is not shared between sessions * allow disconnecting other users from current session and killing other sessions * fix: respect default layout * add welcome screen layout * tests(plugins): new api methods * fix(session-manager): do not quit welcome screen on esc and break * fix(plugins): adjust permissions * style(fmt): rustfmt * style(fmt): fix warnings
-rw-r--r--default-plugins/fixture-plugin-for-tests/src/main.rs10
-rw-r--r--default-plugins/session-manager/src/main.rs473
-rw-r--r--default-plugins/session-manager/src/new_session_info.rs265
-rw-r--r--default-plugins/session-manager/src/resurrectable_sessions.rs37
-rw-r--r--default-plugins/session-manager/src/session_list.rs12
-rw-r--r--default-plugins/session-manager/src/ui/components.rs382
-rw-r--r--default-plugins/session-manager/src/ui/mod.rs7
-rw-r--r--default-plugins/session-manager/src/ui/welcome_screen.rs168
-rw-r--r--src/commands.rs71
-rw-r--r--src/sessions.rs11
-rw-r--r--zellij-server/src/lib.rs35
-rw-r--r--zellij-server/src/plugins/unit/plugin_tests.rs240
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap10
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap18
-rw-r--r--zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap22
-rw-r--r--zellij-server/src/plugins/zellij_exports.rs41
-rw-r--r--zellij-server/src/screen.rs21
-rw-r--r--zellij-server/src/tab/mod.rs2
-rw-r--r--zellij-server/src/unit/screen_tests.rs12
-rw-r--r--zellij-tile/src/shim.rs33
-rw-r--r--zellij-utils/assets/layouts/welcome.kdl8
-rw-r--r--zellij-utils/assets/prost/api.event.rs10
-rw-r--r--zellij-utils/assets/prost/api.plugin_command.rs18
-rw-r--r--zellij-utils/src/data.rs45
-rw-r--r--zellij-utils/src/errors.rs1
-rw-r--r--zellij-utils/src/input/config.rs9
-rw-r--r--zellij-utils/src/input/keybinds.rs11
-rw-r--r--zellij-utils/src/input/layout.rs122
-rw-r--r--zellij-utils/src/kdl/mod.rs50
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap4
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap7
-rw-r--r--zellij-utils/src/plugin_api/event.proto6
-rw-r--r--zellij-utils/src/plugin_api/event.rs57
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto8
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs30
-rw-r--r--zellij-utils/src/setup.rs71
36 files changed, 1934 insertions, 393 deletions
diff --git a/default-plugins/fixture-plugin-for-tests/src/main.rs b/default-plugins/fixture-plugin-for-tests/src/main.rs
index fcdc363f..17f29943 100644
--- a/default-plugins/fixture-plugin-for-tests/src/main.rs
+++ b/default-plugins/fixture-plugin-for-tests/src/main.rs
@@ -280,6 +280,16 @@ impl ZellijPlugin for State {
context,
);
},
+ Key::Ctrl('5') => {
+ switch_session(Some("my_new_session"));
+ },
+ Key::Ctrl('6') => disconnect_other_clients(),
+ Key::Ctrl('7') => {
+ switch_session_with_layout(
+ Some("my_other_new_session"),
+ LayoutInfo::BuiltIn("compact".to_owned()),
+ );
+ },
_ => {},
},
Event::CustomMessage(message, payload) => {
diff --git a/default-plugins/session-manager/src/main.rs b/default-plugins/session-manager/src/main.rs
index 8f70cc29..8040f79e 100644
--- a/default-plugins/session-manager/src/main.rs
+++ b/default-plugins/session-manager/src/main.rs
@@ -1,3 +1,4 @@
+mod new_session_info;
mod resurrectable_sessions;
mod session_list;
mod ui;
@@ -5,34 +6,58 @@ use zellij_tile::prelude::*;
use std::collections::BTreeMap;
+use new_session_info::NewSessionInfo;
use ui::{
components::{
- render_controls_line, render_error, render_new_session_line, render_prompt,
- render_renaming_session_screen, render_resurrection_toggle, Colors,
+ render_controls_line, render_error, render_new_session_block, render_prompt,
+ render_renaming_session_screen, render_screen_toggle, Colors,
},
+ welcome_screen::{render_banner, render_welcome_boundaries},
SessionUiInfo,
};
use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList;
+#[derive(Clone, Debug, Copy)]
+enum ActiveScreen {
+ NewSession,
+ AttachToSession,
+ ResurrectSession,
+}
+
+impl Default for ActiveScreen {
+ fn default() -> Self {
+ ActiveScreen::AttachToSession
+ }
+}
+
#[derive(Default)]
struct State {
session_name: Option<String>,
sessions: SessionList,
resurrectable_sessions: ResurrectableSessions,
search_term: String,
- new_session_name: Option<String>,
+ new_session_info: NewSessionInfo,
renaming_session_name: Option<String>,
error: Option<String>,
- browsing_resurrection_sessions: bool,
+ active_screen: ActiveScreen,
colors: Colors,
+ is_welcome_screen: bool,
+ show_kill_all_sessions_warning: bool,
}
register_plugin!(State);
impl ZellijPlugin for State {
- fn load(&mut self, _configuration: BTreeMap<String, String>) {
+ fn load(&mut self, configuration: BTreeMap<String, String>) {
+ self.is_welcome_screen = configuration
+ .get("welcome_screen")
+ .map(|v| v == "true")
+ .unwrap_or(false);
+ if self.is_welcome_screen {
+ self.active_screen = ActiveScreen::NewSession;
+ }
subscribe(&[
EventType::ModeUpdate,
EventType::SessionUpdate,
@@ -55,6 +80,12 @@ impl ZellijPlugin for State {
should_render = true;
},
Event::SessionUpdate(session_infos, resurrectable_session_list) => {
+ for session_info in &session_infos {
+ if session_info.is_current_session {
+ self.new_session_info
+ .update_layout_list(session_info.available_layouts.clone());
+ }
+ }
self.resurrectable_sessions
.update(resurrectable_session_list);
self.update_session_infos(session_infos);
@@ -66,36 +97,53 @@ impl ZellijPlugin for State {
}
fn render(&mut self, rows: usize, cols: usize) {
- if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.render(rows, cols);
- return;
- } else if let Some(new_session_name) = self.renaming_session_name.as_ref() {
- render_renaming_session_screen(&new_session_name, rows, cols);
- return;
+ let (x, y, width, height) = self.main_menu_size(rows, cols);
+
+ if self.is_welcome_screen {
+ render_banner(x, 0, rows.saturating_sub(height), width);
}
- render_resurrection_toggle(cols, false);
- render_prompt(
- self.new_session_name.is_some(),
- &self.search_term,
- self.colors,
- );
- let room_for_list = rows.saturating_sub(5); // search line and controls
- self.sessions.update_rows(room_for_list);
- let list = self
- .sessions
- .render(room_for_list, cols.saturating_sub(7), self.colors); // 7 for various ui
- for line in list {
- println!("{}", line.render());
+ render_screen_toggle(self.active_screen, x, y, width.saturating_sub(2));
+
+ match self.active_screen {
+ ActiveScreen::NewSession => {
+ render_new_session_block(
+ &self.new_session_info,
+ self.colors,
+ height,
+ width,
+ x,
+ y + 2,
+ );
+ },
+ ActiveScreen::AttachToSession => {
+ if let Some(new_session_name) = self.renaming_session_name.as_ref() {
+ render_renaming_session_screen(&new_session_name, height, width, x, y + 2);
+ } else if self.show_kill_all_sessions_warning {
+ self.render_kill_all_sessions_warning(height, width, x, y);
+ } else {
+ render_prompt(&self.search_term, self.colors, x, y + 2);
+ let room_for_list = height.saturating_sub(6); // search line and controls;
+ self.sessions.update_rows(room_for_list);
+ let list =
+ self.sessions
+ .render(room_for_list, width.saturating_sub(7), self.colors); // 7 for various ui
+ for (i, line) in list.iter().enumerate() {
+ print!("\u{1b}[{};{}H{}", y + i + 5, x, line.render());
+ }
+ }
+ },
+ ActiveScreen::ResurrectSession => {
+ self.resurrectable_sessions.render(height, width, x, y);
+ },
}
- render_new_session_line(
- &self.new_session_name,
- self.sessions.is_searching,
- self.colors,
- );
if let Some(error) = self.error.as_ref() {
- render_error(&error, rows, cols);
+ render_error(&error, height, width, x, y);
} else {
- render_controls_line(self.sessions.is_searching, rows, cols, self.colors);
+ render_controls_line(self.active_screen, width, self.colors, x + 1, rows);
+ }
+ if self.is_welcome_screen {
+ render_welcome_boundaries(rows, cols); // explicitly done in the end to override some
+ // stuff, see comment in function
}
}
}
@@ -109,40 +157,77 @@ impl State {
self.error = None;
return true;
}
+ match self.active_screen {
+ ActiveScreen::NewSession => self.handle_new_session_key(key),
+ ActiveScreen::AttachToSession => self.handle_attach_to_session(key),
+ ActiveScreen::ResurrectSession => self.handle_resurrect_session_key(key),
+ }
+ }
+ fn handle_new_session_key(&mut self, key: Key) -> bool {
let mut should_render = false;
- if let Key::Right = key {
- if self.new_session_name.is_none() {
- self.sessions.result_expand();
+ if let Key::Down = key {
+ self.new_session_info.handle_key(key);
+ should_render = true;
+ } else if let Key::Up = key {
+ self.new_session_info.handle_key(key);
+ should_render = true;
+ } else if let Key::Char(character) = key {
+ if character == '\n' {
+ self.handle_selection();
+ } else {
+ self.new_session_info.handle_key(key);
}
should_render = true;
- } else if let Key::Left = key {
- if self.new_session_name.is_none() {
- self.sessions.result_shrink();
+ } else if let Key::Backspace = key {
+ self.new_session_info.handle_key(key);
+ should_render = true;
+ } else if let Key::Ctrl('w') = key {
+ self.active_screen = ActiveScreen::NewSession;
+ should_render = true;
+ } else if let Key::Ctrl('c') = key {
+ self.new_session_info.handle_key(key);
+ should_render = true;
+ } else if let Key::BackTab = key {
+ self.toggle_active_screen();
+ should_render = true;
+ } else if let Key::Esc = key {
+ self.new_session_info.handle_key(key);
+ should_render = true;
+ }
+ should_render
+ }
+ fn handle_attach_to_session(&mut self, key: Key) -> bool {
+ let mut should_render = false;
+ if self.show_kill_all_sessions_warning {
+ if let Key::Char('y') = key {
+ let all_other_sessions = self.sessions.all_other_sessions();
+ kill_sessions(&all_other_sessions);
+ self.reset_selected_index();
+ self.search_term.clear();
+ self.sessions
+ .update_search_term(&self.search_term, &self.colors);
+ self.show_kill_all_sessions_warning = false
+ } else if let Key::Char('n') | Key::Esc | Key::Ctrl('c') = key {
+ self.show_kill_all_sessions_warning = false
}
should_render = true;
+ } else if let Key::Right = key {
+ self.sessions.result_expand();
+ should_render = true;
+ } else if let Key::Left = key {
+ self.sessions.result_shrink();
+ should_render = true;
} else if let Key::Down = key {
- if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.move_selection_down();
- } else if self.new_session_name.is_none() && self.renaming_session_name.is_none() {
- self.sessions.move_selection_down();
- }
+ self.sessions.move_selection_down();
should_render = true;
} else if let Key::Up = key {
- if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.move_selection_up();
- } else if self.new_session_name.is_none() && self.renaming_session_name.is_none() {
- self.sessions.move_selection_up();
- }
+ self.sessions.move_selection_up();
should_render = true;
} else if let Key::Char(character) = key {
if character == '\n' {
self.handle_selection();
- } else if let Some(new_session_name) = self.new_session_name.as_mut() {
+ } else if let Some(new_session_name) = self.renaming_session_name.as_mut() {
new_session_name.push(character);
- } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
- renaming_session_name.push(character);
- } else if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.handle_character(character);
} else {
self.search_term.push(character);
self.sessions
@@ -150,20 +235,12 @@ impl State {
}
should_render = true;
} else if let Key::Backspace = key {
- if let Some(new_session_name) = self.new_session_name.as_mut() {
+ if let Some(new_session_name) = self.renaming_session_name.as_mut() {
if new_session_name.is_empty() {
- self.new_session_name = None;
- } else {
- new_session_name.pop();
- }
- } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
- if renaming_session_name.is_empty() {
self.renaming_session_name = None;
} else {
- renaming_session_name.pop();
+ new_session_name.pop();
}
- } else if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.handle_backspace();
} else {
self.search_term.pop();
self.sessions
@@ -171,136 +248,165 @@ impl State {
}
should_render = true;
} else if let Key::Ctrl('w') = key {
- if self.sessions.is_searching || self.browsing_resurrection_sessions {
- // no-op
- } else if self.new_session_name.is_some() {
- self.new_session_name = None;
+ self.active_screen = ActiveScreen::NewSession;
+ should_render = true;
+ } else if let Key::Ctrl('r') = key {
+ self.renaming_session_name = Some(String::new());
+ should_render = true;
+ } else if let Key::Delete = key {
+ if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
+ kill_sessions(&[selected_session_name]);
+ self.reset_selected_index();
+ self.search_term.clear();
+ self.sessions
+ .update_search_term(&self.search_term, &self.colors);
} else {
- self.new_session_name = Some(String::new());
+ self.show_error("Must select session before killing it.");
}
should_render = true;
- } else if let Key::Ctrl('r') = key {
- if self.sessions.is_searching || self.browsing_resurrection_sessions {
- // no-op
- } else if self.renaming_session_name.is_some() {
- self.renaming_session_name = None;
+ } else if let Key::Ctrl('d') = key {
+ let all_other_sessions = self.sessions.all_other_sessions();
+ if all_other_sessions.is_empty() {
+ self.show_error("No other sessions to kill. Quit to kill the current one.");
} else {
- self.renaming_session_name = Some(String::new());
+ self.show_kill_all_sessions_warning = true;
}
should_render = true;
+ } else if let Key::Ctrl('x') = key {
+ disconnect_other_clients();
} else if let Key::Ctrl('c') = key {
- if let Some(new_session_name) = self.new_session_name.as_mut() {
- if new_session_name.is_empty() {
- self.new_session_name = None;
- } else {
- new_session_name.clear()
- }
- } else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
- if renaming_session_name.is_empty() {
- self.renaming_session_name = None;
- } else {
- renaming_session_name.clear()
- }
- } else if !self.search_term.is_empty() {
+ if !self.search_term.is_empty() {
self.search_term.clear();
self.sessions
.update_search_term(&self.search_term, &self.colors);
self.reset_selected_index();
- } else {
+ } else if !self.is_welcome_screen {
self.reset_selected_index();
hide_self();
}
should_render = true;
} else if let Key::BackTab = key {
- self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions;
+ self.toggle_active_screen();
should_render = true;
- } else if let Key::Delete = key {
- if self.browsing_resurrection_sessions {
- self.resurrectable_sessions.delete_selected_session();
- should_render = true;
- }
- } else if let Key::Ctrl('d') = key {
- if self.browsing_resurrection_sessions {
- self.resurrectable_sessions
- .show_delete_all_sessions_warning();
- should_render = true;
- }
} else if let Key::Esc = key {
if self.renaming_session_name.is_some() {
self.renaming_session_name = None;
should_render = true;
- } else if self.new_session_name.is_some() {
- self.new_session_name = None;
- should_render = true;
- } else {
+ } else if !self.is_welcome_screen {
hide_self();
}
}
should_render
}
- fn handle_selection(&mut self) {
- if self.browsing_resurrection_sessions {
- if let Some(session_name_to_resurrect) =
- self.resurrectable_sessions.get_selected_session_name()
- {
- switch_session(Some(&session_name_to_resurrect));
- }
- } else if let Some(new_session_name) = &self.new_session_name {
- if new_session_name.is_empty() {
- switch_session(None);
- } else if self.session_name.as_ref() == Some(new_session_name) {
- // noop - we're already here!
- self.new_session_name = None;
+ fn handle_resurrect_session_key(&mut self, key: Key) -> bool {
+ let mut should_render = false;
+ if let Key::Down = key {
+ self.resurrectable_sessions.move_selection_down();
+ should_render = true;
+ } else if let Key::Up = key {
+ self.resurrectable_sessions.move_selection_up();
+ should_render = true;
+ } else if let Key::Char(character) = key {
+ if character == '\n' {
+ self.handle_selection();
} else {
- switch_session(Some(new_session_name));
+ self.resurrectable_sessions.handle_character(character);
}
- } else if let Some(renaming_session_name) = &self.renaming_session_name.take() {
- if renaming_session_name.is_empty() {
- // TODO: implement these, then implement the error UI, then implement the renaming
- // session screen, then test it
- self.show_error("New name must not be empty.");
- return; // s that we don't hide self
- } else if self.session_name.as_ref() == Some(renaming_session_name) {
- // noop - we're already called that!
- return; // s that we don't hide self
- } else if self.sessions.has_session(&renaming_session_name) {
- self.show_error("A session by this name already exists.");
- return; // s that we don't hide self
- } else if self
- .resurrectable_sessions
- .has_session(&renaming_session_name)
- {
- self.show_error("A resurrectable session by this name already exists.");
- return; // s that we don't hide self
- } else {
- self.update_current_session_name_in_ui(&renaming_session_name);
- rename_session(&renaming_session_name);
- return; // s that we don't hide self
+ should_render = true;
+ } else if let Key::Backspace = key {
+ self.resurrectable_sessions.handle_backspace();
+ should_render = true;
+ } else if let Key::Ctrl('w') = key {
+ self.active_screen = ActiveScreen::NewSession;
+ should_render = true;
+ } else if let Key::BackTab = key {
+ self.toggle_active_screen();
+ should_render = true;
+ } else if let Key::Delete = key {
+ self.resurrectable_sessions.delete_selected_session();
+ should_render = true;
+ } else if let Key::Ctrl('d') = key {
+ self.resurrectable_sessions
+ .show_delete_all_sessions_warning();
+ should_render = true;
+ } else if let Key::Esc = key {
+ if !self.is_welcome_screen {
+ hide_self();
}
- } else if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
- let selected_tab = self.sessions.get_selected_tab_position();
- let selected_pane = self.sessions.get_selected_pane_id();
- let is_current_session = self.sessions.selected_is_current_session();
- if is_current_session {
- if let Some((pane_id, is_plugin)) = selected_pane {
- if is_plugin {
- focus_plugin_pane(pane_id, true);
+ }
+ should_render
+ }
+ fn handle_selection(&mut self) {
+ match self.active_screen {
+ ActiveScreen::NewSession => {
+ self.new_session_info.handle_selection(&self.session_name);
+ },
+ ActiveScreen::AttachToSession => {
+ if let Some(renaming_session_name) = &self.renaming_session_name.take() {
+ if renaming_session_name.is_empty() {
+ self.show_error("New name must not be empty.");
+ return; // so that we don't hide self
+ } else if self.session_name.as_ref() == Some(renaming_session_name) {
+ // noop - we're already called that!
+ return; // so that we don't hide self
+ } else if self.sessions.has_session(&renaming_session_name) {
+ self.show_error("A session by this name already exists.");
+ return; // so that we don't hide self
+ } else if self
+ .resurrectable_sessions
+ .has_session(&renaming_session_name)
+ {
+ self.show_error("A resurrectable session by this name already exists.");
+ return; // s that we don't hide self
} else {
- focus_terminal_pane(pane_id, true);
+ self.update_current_session_name_in_ui(&renaming_session_name);
+ rename_session(&renaming_session_name);
+ return; // s that we don't hide self
}
- } else if let Some(tab_position) = selected_tab {
- go_to_tab(tab_position as u32);
}
- } else {
- switch_session_with_focus(&selected_session_name, selected_tab, selected_pane);
- }
+ if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
+ let selected_tab = self.sessions.get_selected_tab_position();
+ let selected_pane = self.sessions.get_selected_pane_id();
+ let is_current_session = self.sessions.selected_is_current_session();
+ if is_current_session {
+ if let Some((pane_id, is_plugin)) = selected_pane {
+ if is_plugin {
+ focus_plugin_pane(pane_id, true);
+ } else {
+ focus_terminal_pane(pane_id, true);
+ }
+ } else if let Some(tab_position) = selected_tab {
+ go_to_tab(tab_position as u32);
+ }
+ } else {
+ switch_session_with_focus(
+ &selected_session_name,
+ selected_tab,
+ selected_pane,
+ );
+ }
+ }
+ self.reset_selected_index();
+ self.search_term.clear();
+ self.sessions
+ .update_search_term(&self.search_term, &self.colors);
+ hide_self();
+ },
+ ActiveScreen::ResurrectSession => {
+ if let Some(session_name_to_resurrect) =
+ self.resurrectable_sessions.get_selected_session_name()
+ {
+ switch_session(Some(&session_name_to_resurrect));
+ }
+ },
}
- self.reset_selected_index();
- self.new_session_name = None;
- self.search_term.clear();
- self.sessions
- .update_search_term(&self.search_term, &self.colors);
- hide_self();
+ }
+ fn toggle_active_screen(&mut self) {
+ self.active_screen = match self.active_screen {
+ ActiveScreen::NewSession => ActiveScreen::AttachToSession,
+ ActiveScreen::AttachToSession => ActiveScreen::ResurrectSession,
+ ActiveScreen::ResurrectSession => ActiveScreen::NewSession,
+ };
}
fn show_error(&mut self, error_text: &str) {
self.error = Some(error_text.to_owned());
@@ -329,4 +435,53 @@ impl State {
}
self.sessions.set_sessions(session_infos);
}
+ fn main_menu_size(&self, rows: usize, cols: usize) -> (usize, usize, usize, usize) {
+ // x, y, width, height
+ let width = if self.is_welcome_screen {
+ std::cmp::min(cols, 101)
+ } else {
+ cols
+ };
+ let x = if self.is_welcome_screen {
+ (cols.saturating_sub(width) as f64 / 2.0).floor() as usize + 2
+ } else {
+ 0
+ };
+ let y = if self.is_welcome_screen {
+ (rows.saturating_sub(15) as f64 / 2.0).floor() as usize
+ } else {
+ 0
+ };
+ let height = rows.saturating_sub(y);
+ (x, y, width, height)
+ }
+ fn render_kill_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
+ if rows == 0 || columns == 0 {
+ return;
+ }
+ let session_count = self.sessions.all_other_sessions().len();
+ let session_count_len = session_count.to_string().chars().count();
+ let warning_description_text = format!("This will kill {} active sessions", session_count);
+ let confirmation_text = "Are you sure? (y/n)";
+ let warning_y_location = y + (rows / 2).saturating_sub(1);
+ let confirmation_y_location = y + (rows / 2) + 1;
+ let warning_x_location =
+ x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
+ let confirmation_x_location =
+ x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
+ print_text_with_coordinates(
+ Text::new(warning_description_text).color_range(0, 15..16 + session_count_len),
+ warning_x_location,
+ warning_y_location,
+ None,
+ None,
+ );
+ print_text_with_coordinates(
+ Text::new(confirmation_text).color_indices(2, vec![15, 17]),
+ confirmation_x_location,
+ confirmation_y_location,
+ None,
+ None,
+ );
+ }
}
diff --git a/default-plugins/session-manager/src/new_session_info.rs b/default-plugins/session-manager/src/new_session_info.rs
new file mode 100644
index 00000000..6a2456f1
--- /dev/null
+++ b/default-plugins/session-manager/src/new_session_info.rs
@@ -0,0 +1,265 @@
+use fuzzy_matcher::skim::SkimMatcherV2;
+use fuzzy_matcher::FuzzyMatcher;
+use std::cmp::Ordering;
+use zellij_tile::prelude::*;
+
+#[derive(Default)]
+pub struct NewSessionInfo {
+ name: String,
+ layout_list: LayoutList,
+ entering_new_session_info: EnteringState,
+}
+
+#[derive(Eq, PartialEq)]
+enum EnteringState {
+ EnteringName,
+ EnteringLayoutSearch,
+}
+
+impl Default for EnteringState {
+ fn default() -> Self {
+ EnteringState::EnteringName
+ }
+}
+
+impl NewSessionInfo {
+ pub fn name(&self) -> &str {
+ &self.name
+ }
+ pub fn layout_search_term(&self) -> &str {
+ &self.layout_list.layout_search_term
+ }
+ pub fn entering_new_session_info(&self) -> bool {
+ true
+ }
+ pub fn entering_new_session_name(&self) -> bool {
+ self.entering_new_session_info == EnteringState::EnteringName
+ }
+ pub fn entering_layout_search_term(&self) -> bool {
+ self.entering_new_session_info == EnteringState::EnteringLayoutSearch
+ }
+ pub fn add_char(&mut self, character: char) {
+ match self.entering_new_session_info {
+ EnteringState::EnteringName => {
+ self.name.push(character);
+ },
+ EnteringState::EnteringLayoutSearch => {
+ self.layout_list.layout_search_term.push(character);
+ self.update_layout_search_term();
+ },
+ }
+ }
+ pub fn handle_backspace(&mut self) {
+ match self.entering_new_session_info {
+ EnteringState::EnteringName => {
+ self.name.pop();
+ },
+ EnteringState::EnteringLayoutSearch => {
+ self.layout_list.layout_search_term.pop();
+ self.update_layout_search_term();
+ },
+ }
+ }
+ pub fn handle_break(&mut self) {
+ match self.entering_new_session_info {
+ EnteringState::EnteringName => {
+ self.name.clear();
+ },
+ EnteringState::EnteringLayoutSearch => {
+ self.layout_list.layout_search_term.clear();
+ self.entering_new_session_info = EnteringState::EnteringName;
+ self.update_layout_search_term();
+ },
+ }
+ }
+ pub fn handle_key(&mut self, key: Key) {
+ match key {
+ Key::Backspace => {
+ self.handle_backspace();
+ },
+ Key::Ctrl('c') | Key::Esc => {
+ self.handle_break();
+ },
+ Key::Char(character) => {
+ self.add_char(character);
+ },
+ Key::Up => {
+ self.move_selection_up();
+ },
+ Key::Down => {
+ self.move_selection_down();
+ },
+ _ => {},
+ }
+ }
+ pub fn handle_selection(&mut self, current_session_name: &Option<String>) {
+ match self.entering_new_session_info {
+ EnteringState::EnteringLayoutSearch => {
+ let new_session_layout: Option<LayoutInfo> = self.selected_layout_info();
+ let new_session_name = if self.name.is_empty() {
+ None
+ } else {
+ Some(self.name.as_str())
+ };
+ if new_session_name != current_session_name.as_ref().map(|s| s.as_str()) {
+ match new_session_layout {
+ Some(new_session_layout) => {
+ switch_session_with_layout(new_session_name, new_session_layout)
+ },
+ None => {
+ switch_session(new_session_name);
+ },
+ }
+ }
+ self.name.clear();
+ self.layout_list.clear_selection();
+ hide_self();
+ },
+ EnteringState::EnteringName => {
+ self.entering_new_session_info = EnteringState::EnteringLayoutSearch;
+ },
+ }
+ }
+ pub fn update_layout_list(&mut self, layout_info: Vec<LayoutInfo>) {
+ self.layout_list.update_layout_list(layout_info);
+ }
+ pub fn layout_list(&self) -> Vec<(LayoutInfo, bool)> {
+ // bool - is_selected
+ self.layout_list
+ .layout_list
+ .iter()
+ .enumerate()
+ .map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
+ .collect()
+ }
+ pub fn layouts_to_render(&self) -> Vec<(LayoutInfo, Vec<usize>, bool)> {
+ // (layout_info,
+ // search_indices,
+ // is_selected)
+ if self.is_searching() {
+ self.layout_search_results()
+ .into_iter()
+ .map(|(layout_search_result, is_selected)| {
+ (
+ layout_search_result.layout_info,
+ layout_search_result.indices,
+ is_selected,
+ )
+ })
+ .collect()
+ } else {
+ self.layout_list()
+ .into_iter()
+ .map(|(layout_info, is_selected)| (layout_info, vec![], is_selected))
+ .collect()
+ }
+ }
+ pub fn layout_search_results(&self) -> Vec<(LayoutSearchResult, bool)> {
+ // bool - is_selected
+ self.layout_list
+ .layout_search_results
+ .iter()
+ .enumerate()
+ .map(|(i, l)| (l.clone(), i == self.layout_list.selected_layout_index))
+ .collect()
+ }
+ pub fn is_searching(&self) -> bool {
+ !self.layout_list.layout_search_term.is_empty()
+ }
+ pub fn layout_count(&self) -> usize {
+ self.layout_list.layout_list.len()
+ }
+ pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
+ self.layout_list.selected_layout_info()
+ }
+ fn update_layout_search_term(&mut self) {
+ if self.layout_list.layout_search_term.is_empty() {
+ self.layout_list.clear_selection();
+ self.layout_list.layout_search_results = vec![];
+ } else {
+ let mut matches = vec![];
+ let matcher = SkimMatcherV2::default().use_cache(true);
+ for layout_info in &self.layout_list.layout_list {
+ if let Some((score, indices)) =
+ matcher.fuzzy_indices(&layout_info.name(), &self.layout_list.layout_search_term)
+ {
+ matches.push(LayoutSearchResult {
+ layout_info: layout_info.clone(),
+ score,
+ indices,
+ });
+ }
+ }
+ matches.sort_by(|a, b| b.score.cmp(&a.score));
+ self.layout_list.layout_search_results = matches;
+ self.layout_list.clear_selection();
+ }
+ }
+ fn move_selection_up(&mut self) {
+ self.layout_list.move_selection_up();
+ }
+ fn move_selection_down(&mut self) {
+ self.layout_list.move_selection_down();
+ }
+}
+
+#[derive(Default)]
+pub struct LayoutList {
+ layout_list: Vec<LayoutInfo>,
+ layout_search_results: Vec<LayoutSearchResult>,
+ selected_layout_index: usize,
+ layout_search_term: String,
+}
+
+impl LayoutList {
+ pub fn update_layout_list(&mut self, layout_list: Vec<LayoutInfo>) {
+ let old_layout_length = self.layout_list.len();
+ self.layout_list = layout_list;
+ if old_layout_length != self.layout_list.len() {
+ // honestly, this is just the UX choice that sucks the least...
+ self.clear_selection();
+ }
+ }
+ pub fn selected_layout_info(&self) -> Option<LayoutInfo> {
+ if !self.layout_search_term.is_empty() {
+ self.layout_search_results
+ .get(self.selected_layout_index)
+ .map(|l| l.layout_info.clone())
+ } else {
+ self.layout_list.get(self.selected_layout_index).cloned()
+ }
+ }
+ pub fn clear_selection(&mut self) {
+ self.selected_layout_index = 0;
+ }
+ fn max_index(&self) -> usize {
+ if self.layout_search_term.is_empty() {
+ self.layout_list.len().saturating_sub(1)
+ } else {
+ self.layout_search_results.len().saturating_sub(1)
+ }
+ }
+ fn move_selection_up(&mut self) {
+ let max_index = self.max_index();
+ if self.selected_layout_index > 0 {
+ self.selected_layout_index -= 1;
+ } else {
+ self.selected_layout_index = max_index;
+ }
+ }
+ fn move_selection_down(&mut self) {
+ let max_index = self.max_index();
+ if self.selected_layout_index < max_index {
+ self.selected_layout_index += 1;
+ } else {
+ self.selected_layout_index = 0;
+ }
+ }
+}
+
+#[derive(Clone)]
+pub struct LayoutSearchResult {
+ pub layout_info: LayoutInfo,
+ pub score: i64,
+ pub indices: Vec<usize>,
+}
diff --git a/default-plugins/session-manager/src/resurrectable_sessions.rs b/default-plugins/session-manager/src/resurrectable_sessions.rs
index dfbb86f0..98887036 100644
--- a/default-plugins/session-manager/src/resurrectable_sessions.rs
+++ b/default-plugins/session-manager/src/resurrectable_sessions.rs
@@ -2,8 +2,6 @@ use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use humantime::format_duration;
-use crate::ui::components::render_resurrection_toggle;
-
use std::time::Duration;
use zellij_tile::shim::*;
@@ -24,23 +22,22 @@ impl ResurrectableSessions {
list.sort_by(|a, b| a.1.cmp(&b.1));
self.all_resurrectable_sessions = list;
}
- pub fn render(&self, rows: usize, columns: usize) {
+ pub fn render(&self, rows: usize, columns: usize, x: usize, y: usize) {
if self.delete_all_dead_sessions_warning {
- self.render_delete_all_sessions_warning(rows, columns);
+ self.render_delete_all_sessions_warning(rows, columns, x, y);
return;
}
- render_resurrection_toggle(columns, true);
- let search_indication = Text::new(format!("> {}_", self.search_term)).color_range(1, ..);
- let table_rows = rows.saturating_sub(3);
+ let search_indication =
+ Text::new(format!("Search: {}_", self.search_term)).color_range(2, ..7);
+ let table_rows = rows.saturating_sub(5); // search row, toggle row and some padding
let table_columns = columns;
let table = if self.is_searching {
self.render_search_results(table_rows, columns)
} else {
self.render_all_entries(table_rows, columns)
};
- print_text_with_coordinates(search_indication, 0, 0, None, None);
- print_table_with_coordinates(table, 0, 1, Some(table_columns), Some(table_rows));
- self.render_controls_line(rows);
+ print_text_with_coordinates(search_indication, x.saturating_sub(1), y + 2, None, None);
+ print_table_with_coordinates(table, x, y + 3, Some(table_columns), Some(table_rows));
}
fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table {
let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row
@@ -103,7 +100,7 @@ impl ResurrectableSessions {
}
table
}
- fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize) {
+ fn render_delete_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
if rows == 0 || columns == 0 {
return;
}
@@ -112,11 +109,12 @@ impl ResurrectableSessions {
let warning_description_text =
format!("This will delete {} resurrectable sessions", session_count,);
let confirmation_text = "Are you sure? (y/n)";
- let warning_y_location = (rows / 2).saturating_sub(1);
- let confirmation_y_location = (rows / 2) + 1;
+ let warning_y_location = y + (rows / 2).saturating_sub(1);
+ let confirmation_y_location = y + (rows / 2) + 1;
let warning_x_location =
- columns.saturating_sub(warning_description_text.chars().count()) / 2;
- let confirmation_x_location = columns.saturating_sub(confirmation_text.chars().count()) / 2;
+ x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
+ let confirmation_x_location =
+ x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
print_text_with_coordinates(
Text::new(warning_description_text).color_range(0, 17..18 + session_count_len),
warning_x_location,
@@ -200,15 +198,6 @@ impl ResurrectableSessions {
Text::new(" ")
}
}
- fn render_controls_line(&self, rows: usize) {
- let controls_line = Text::new(format!(
- "Help: <↓↑> - Navigate, <DEL> - Delete Session, <Ctrl d> - Delete all sessions"
- ))
- .color_range(3, 6..10)
- .color_range(3, 23..29)
- .color_range(3, 47..56);
- print_text_with_coordinates(controls_line, 0, rows.saturating_sub(1), None, None);
- }
pub fn move_selection_down(&mut self) {
if self.is_searching {
if let Some(selected_index) = self.selected_search_index.as_mut() {
diff --git a/default-plugins/session-manager/src/session_list.rs b/default-plugins/session-manager/src/session_list.rs
index fa63571e..01c75ad5 100644
--- a/default-plugins/session-manager/src/session_list.rs
+++ b/default-plugins/session-manager/src/session_list.rs
@@ -323,6 +323,18 @@ impl SessionList {
.find(|s| s.name == old_name)
.map(|s| s.name = new_name.to_owned());
}
+ pub fn all_other_sessions(&self) -> Vec<String> {
+ self.session_ui_infos
+ .iter()
+ .filter_map(|s| {
+ if !s.is_current_session {
+ Some(s.name.clone())
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
}
#[derive(Debug, Clone, Default)]
diff --git a/default-plugins/session-manager/src/ui/components.rs b/default-plugins/session-manager/src/ui/components.rs
index 4d9839c0..d3c4afe8 100644
--- a/default-plugins/session-manager/src/ui/components.rs
+++ b/default-plugins/session-manager/src/ui/components.rs
@@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
+use crate::{ActiveScreen, NewSessionInfo};
#[derive(Debug)]
pub struct ListItem {
@@ -292,18 +293,45 @@ impl LineToRender {
pub fn append(&mut self, to_append: &str) {
self.line.push_str(to_append)
}
- pub fn make_selected(&mut self) {
+ pub fn make_selected_as_search(&mut self, add_arrows: bool) {
self.is_selected = true;
+ let arrows = if add_arrows {
+ self.colors.magenta(" <↓↑> ")
+ } else {
+ " ".to_owned()
+ };
+ match self.colors.palette.bg {
+ PaletteColor::EightBit(byte) => {
+ self.line = format!(
+ "\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}",
+ self.line
+ );
+ },
+ PaletteColor::Rgb((r, g, b)) => {
+ self.line = format!(
+ "\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}",
+ r, g, b, r, g, b, self.line
+ );
+ },
+ }
+ }
+ pub fn make_selected(&mut self, add_arrows: bool) {
+ self.is_selected = true;
+ let arrows = if add_arrows {
+ self.colors.magenta("<←↓↑→>")
+ } else {
+ " ".to_owned()
+ };
match self.colors.palette.bg {
PaletteColor::EightBit(byte) => {
self.line = format!(
- "\u{1b}[48;5;{byte}m\u{1b}[K\r\u{1b}[48;5;{byte}m{}",
+ "\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}",
self.line
);
},
PaletteColor::Rgb((r, g, b)) => {
self.line = format!(
- "\u{1b}[48;2;{};{};{}m\u{1b}[K\r\u{1b}[48;2;{};{};{}m{}",
+ "\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}",
r, g, b, r, g, b, self.line
);
},
@@ -323,7 +351,7 @@ impl LineToRender {
if self.is_selected {
self.line.clone()
} else {
- format!("\u{1b}[49m{}", line)
+ format!("\u{1b}[49m {}", line)
}
}
pub fn add_truncated_results(&mut self, result_count: usize) {
@@ -475,151 +503,275 @@ pub fn minimize_lines(
(start_index, anchor_index, end_index, line_count_to_remove)
}
-pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) {
- if !typing_session_name {
- let prompt = colors.bold(&format!("> {}_", search_term));
- println!("\u{1b}[H{}\n", prompt);
- } else {
- println!("\n");
- }
+pub fn render_prompt(search_term: &str, colors: Colors, x: usize, y: usize) {
+ let prompt = colors.green(&format!("Search:"));
+ let search_term = colors.bold(&format!("{}_", search_term));
+ println!("\u{1b}[{};{}H{} {}\n", y + 1, x, prompt, search_term);
}
-pub fn render_resurrection_toggle(cols: usize, resurrection_screen_is_active: bool) {
+pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max_cols: usize) {
let key_indication_text = "<TAB>";
- let running_sessions_text = "Running";
- let exited_sessions_text = "Exited";
+ let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 {
+ ("New Session", "Attach to Session", "Resurrect Session")
+ } else {
+ ("New", "Attach", "Resurrect")
+ };
let key_indication_len = key_indication_text.chars().count() + 1;
- let first_ribbon_length = running_sessions_text.chars().count() + 4;
- let second_ribbon_length = exited_sessions_text.chars().count() + 4;
- let key_indication_x =
- cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length);
+ let first_ribbon_length = new_session_text.chars().count() + 4;
+ let second_ribbon_length = running_sessions_text.chars().count() + 4;
+ let third_ribbon_length = exited_sessions_text.chars().count() + 4;
+ let total_len =
+ key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length;
+ let key_indication_x = x;
let first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
+ let third_ribbon_x = second_ribbon_x + second_ribbon_length;
+ let mut new_session_text = Text::new(new_session_text);
+ let mut running_sessions_text = Text::new(running_sessions_text);
+ let mut exited_sessions_text = Text::new(exited_sessions_text);
+ match active_screen {
+ ActiveScreen::NewSession => {
+ new_session_text = new_session_text.selected();
+ },
+ ActiveScreen::AttachToSession => {
+ running_sessions_text = running_sessions_text.selected();
+ },
+ ActiveScreen::ResurrectSession => {
+ exited_sessions_text = exited_sessions_text.selected();
+ },
+ }
print_text_with_coordinates(
Text::new(key_indication_text).color_range(3, ..),
key_indication_x,
- 0,
+ y,
None,
None,
);
- if resurrection_screen_is_active {
- print_ribbon_with_coordinates(
- Text::new(running_sessions_text),
- first_ribbon_x,
- 0,
- None,
- None,
- );
- print_ribbon_with_coordinates(
- Text::new(exited_sessions_text).selected(),
- second_ribbon_x,
- 0,
- None,
- None,
- );
- } else {
- print_ribbon_with_coordinates(
- Text::new(running_sessions_text).selected(),
- first_ribbon_x,
- 0,
- None,
- None,
- );
- print_ribbon_with_coordinates(
- Text::new(exited_sessions_text),
- second_ribbon_x,
- 0,
- None,
- None,
- );
- }
+ print_ribbon_with_coordinates(new_session_text, first_ribbon_x, y, None, None);
+ print_ribbon_with_coordinates(running_sessions_text, second_ribbon_x, y, None, None);
+ print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
}
-pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) {
- if is_searching {
- return;
- }
- let new_session_shortcut_text = "<Ctrl w>";
- let new_session_shortcut = colors.magenta(new_session_shortcut_text);
- let new_session = colors.bold("New session");
+pub fn render_new_session_block(
+ new_session_info: &NewSessionInfo,
+ colors: Colors,
+ max_rows_of_new_session_block: usize,
+ max_cols_of_new_session_block: usize,
+ x: usize,
+ y: usize,
+) {
let enter = colors.magenta("<ENTER>");
- match session_name {
- Some(session_name) => {
+ if new_session_info.entering_new_session_name() {
+ let prompt = "New session name:";
+ let long_instruction = "when done, blank for random";
+ let new_session_name = new_session_info.name();
+ if max_cols_of_new_session_block
+ > prompt.width() + long_instruction.width() + new_session_name.width() + 15
+ {
println!(
- "\u{1b}[m > {}_ ({}, {} when done)",
- colors.orange(session_name),
- colors.bold("Type optional name"),
- enter
+ "\u{1b}[m{}{} {}_ ({} {})",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(prompt),
+ colors.orange(&new_session_name),
+ enter,
+ long_instruction,
);
- },
- None => {
- println!("\u{1b}[m > {new_session_shortcut} - {new_session}");
- },
+ } else {
+ println!(
+ "\u{1b}[m{}{} {}_ {}",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green(prompt),
+ colors.orange(&new_session_name),
+ enter,
+ );
+ }
+ } else if new_session_info.entering_layout_search_term() {
+ let new_session_name = if new_session_info.name().is_empty() {
+ "<RANDOM>"
+ } else {
+ new_session_info.name()
+ };
+ let prompt = "New session name:";
+ let long_instruction = "to correct";
+ let esc = colors.magenta("<ESC>");
+ if max_cols_of_new_session_block
+ > prompt.width() + long_instruction.width() + new_session_name.width() + 15
+ {
+ println!(
+ "\u{1b}[m{}{}: {} ({} to correct)",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green("New session name"),
+ colors.orange(new_session_name),
+ esc,
+ );
+ } else {
+ println!(
+ "\u{1b}[m{}{}: {} {}",
+ format!("\u{1b}[{};{}H", y + 1, x + 1),
+ colors.green("New session name"),
+ colors.orange(new_session_name),
+ esc,
+ );
+ }
+ render_layout_selection_list(
+ new_session_info,
+ max_rows_of_new_session_block.saturating_sub(1),
+ max_cols_of_new_session_block,
+ x,
+ y + 1,
+ );
}
}
-pub fn render_error(error_text: &str, rows: usize, columns: usize) {
+pub fn render_layout_selection_list(
+ new_session_info: &NewSessionInfo,
+ max_rows_of_new_session_block: usize,
+ max_cols_of_new_session_block: usize,
+ x: usize,
+ y: usize,
+) {
+ let layout_search_term = new_session_info.layout_search_term();
+ let search_term_len = layout_search_term.width();
+ let layout_indication_line = if max_cols_of_new_session_block > 73 + search_term_len {
+ Text::new(format!(
+ "New session layout: {}_ (Search and select from list, <ENTER> when done)",
+ layout_search_term
+ ))
+ .color_range(2, ..20 + search_term_len)
+ .color_range(3, 20..20 + search_term_len)
+ .color_range(3, 52 + search_term_len..59 + search_term_len)
+ } else {
+ Text::new(format!(
+ "New session layout: {}_ <ENTER>",
+ layout_search_term
+ ))
+ .color_range(2, ..20 + search_term_len)
+ .color_range(3, 20..20 + search_term_len)
+ .color_range(3, 22 + search_term_len..)
+ };
+ print_text_with_coordinates(layout_indication_line, x, y + 1, None, None);
+ println!();
+ let mut table = Table::new();
+ for (i, (layout_info, indices, is_selected)) in
+ new_session_info.layouts_to_render().into_iter().enumerate()
+ {
+ let layout_name = layout_info.name();
+ let layout_name_len = layout_name.width();
+ let is_builtin = layout_info.is_builtin();
+ if i > max_rows_of_new_session_block {
+ break;
+ } else {
+ let mut layout_cell = if is_builtin {
+ Text::new(format!("{} (built-in)", layout_name))
+ .color_range(1, 0..layout_name_len)
+ .color_range(0, layout_name_len + 1..)
+ .color_indices(3, indices)
+ } else {
+ Text::new(format!("{}", layout_name))
+ .color_range(1, ..)
+ .color_indices(3, indices)
+ };
+ if is_selected {
+ layout_cell = layout_cell.selected();
+ }
+ let arrow_cell = if is_selected {
+ Text::new(format!("<↓↑>")).selected().color_range(3, ..)
+ } else {
+ Text::new(format!(" ")).color_range(3, ..)
+ };
+ table = table.add_styled_row(vec![arrow_cell, layout_cell]);
+ }
+ }
+ print_table_with_coordinates(table, x, y + 3, None, None);
+}
+
+pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) {
print_text_with_coordinates(
Text::new(format!("Error: {}", error_text)).color_range(3, ..),
- 0,
- rows,
+ x,
+ y + rows,
Some(columns),
None,
);
}
-pub fn render_renaming_session_screen(new_session_name: &str, rows: usize, columns: usize) {
+pub fn render_renaming_session_screen(
+ new_session_name: &str,
+ rows: usize,
+ columns: usize,
+ x: usize,
+ y: usize,
+) {
if rows == 0 || columns == 0 {
return;
}
- let prompt_text = "NEW NAME FOR CURRENT SESSION";
- let new_session_name = format!("{}_", new_session_name);
- let prompt_y_location = (rows / 2).saturating_sub(1);
- let session_name_y_location = (rows / 2) + 1;
- let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2;
- let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2;
- print_text_with_coordinates(
- Text::new(prompt_text).color_range(0, ..),
- prompt_x_location,
- prompt_y_location,
- None,
- None,
- );
- print_text_with_coordinates(
- Text::new(new_session_name).color_range(3, ..),
- session_name_x_location,
- session_name_y_location,
- None,
- None,
+ let text = Text::new(format!(
+ "New name for current session: {}_ (<ENTER> when done)",
+ new_session_name
+ ))
+ .color_range(2, ..29)
+ .color_range(
+ 3,
+ 33 + new_session_name.width()..40 + new_session_name.width(),
);
+ print_text_with_coordinates(text, x, y, None, None);
}
-pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) {
- let (arrows, navigate) = if is_searching {
- (colors.magenta("<↓↑>"), colors.bold("Navigate"))
- } else {
- (colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand"))
- };
- let rename = colors.magenta("<Ctrl r>");
- let rename_text = colors.bold("Rename session");
- let enter = colors.magenta("<ENTER>");
- let select = colors.bold("Switch to selected");
- let esc = colors.magenta("<ESC>");
- let to_hide = colors.bold("Hide");
+pub fn render_controls_line(
+ active_screen: ActiveScreen,
+ max_cols: usize,
+ colors: Colors,
+ x: usize,
+ y: usize,
+) {
+ match active_screen {
+ ActiveScreen::NewSession => {
+ if max_cols >= 50 {
+ print!(
+ "\u{1b}[m\u{1b}[{y};{x}H\u{1b}[1mHelp: Fill in the form to start a new session."
+ );
+ }
+ },
+ ActiveScreen::AttachToSession => {
+ let arrows = colors.magenta("<←↓↑→>");
+ let navigate = colors.bold("Navigate");
+ let enter = colors.magenta("<ENTER>");
+ let select = colors.bold("Attach");
+ let rename = colors.magenta("<Ctrl r>");
+ let rename_text = colors.bold("Rename");
+ let disconnect = colors.magenta("<Ctrl x>");
+ let disconnect_text = colors.bold("Disconnect others");
+ let kill = colors.magenta("<Del>");
+ let kill_text = colors.bold("Kill");
+ let kill_all = colors.magenta("<Ctrl d>");
+ let kill_all_text = colors.bold("Kill all");
- if max_cols >= 104 {
- print!(
- "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}"
- );
- } else if max_cols >= 73 {
- let navigate = colors.bold("Navigate");
- let select = colors.bold("Switch");
- let rename_text = colors.bold("Rename");
- print!(
- "\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}"
- );
- } else if max_cols >= 28 {
- print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}");
+ if max_cols > 90 {
+ print!(
+ "\u{1b}[m\u{1b}[{y};{x}HHelp: {rename} - {rename_text}, {disconnect} - {disconnect_text}, {kill} - {kill_text}, {kill_all} - {kill_all_text}"
+ );
+ } else if max_cols >= 28 {
+ print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}");
+ }
+ },
+ ActiveScreen::ResurrectSession => {
+ let arrows = colors.magenta("<↓↑>");
+ let navigate = colors.bold("Navigate");
+ let enter = colors.magenta("<ENTER>");
+ let select = colors.bold("Resurrect");
+ let del = colors.magenta("<DEL>");
+ let del_text = colors.bold("Delete");
+ let del_all = colors.magenta("<Ctrl d>");
+ let del_all_text = colors.bold("Delete all");
+
+ if max_cols > 83 {
+ print!(
+ "\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}"
+ );
+ } else if max_cols >= 28 {
+ print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}");
+ }
+ },
}
}
diff --git a/default-plugins/session-manager/src/ui/mod.rs b/default-plugins/session-manager/src/ui/mod.rs
index 94a1a8a7..b86db946 100644
--- a/default-plugins/session-manager/src/ui/mod.rs
+++ b/default-plugins/session-manager/src/ui/mod.rs
@@ -1,4 +1,5 @@
pub mod components;
+pub mod welcome_screen;
use zellij_tile::prelude::*;
use crate::session_list::{SelectedIndex, SessionList};
@@ -29,7 +30,7 @@ macro_rules! render_assets {
if $selected_index.is_some() && !$has_deeper_selected_assets {
let mut selected_asset: LineToRender =
selected_asset.as_line_to_render(current_index, $max_cols, $colors);
- selected_asset.make_selected();
+ selected_asset.make_selected(true);
selected_asset.add_truncated_results(truncated_result_count_above);
if anchor_asset_index + 1 >= end_index {
// no more results below, let's add the more indication if we need to
@@ -76,8 +77,10 @@ impl SessionList {
if lines_to_render.len() + result.lines_to_render() <= max_rows {
let mut result_lines = result.render(max_cols);
if Some(i) == self.selected_search_index {
+ let mut render_arrows = true;
for line_to_render in result_lines.iter_mut() {
- line_to_render.make_selected();
+ line_to_render.make_selected_as_search(render_arrows);
+ render_arrows = false; // only render arrows on the first search result
}
}
lines_to_render.append(&mut result_lines);
diff --git a/default-plugins/session-manager/src/ui/welcome_screen.rs b/default-plugins/session-manager/src/ui/welcome_screen.rs
new file mode 100644
index 00000000..ccf20c50
--- /dev/null
+++ b/default-plugins/session-manager/src/ui/welcome_screen.rs
@@ -0,0 +1,168 @@
+static BANNER: &str = "
+██╗ ██╗██╗ ███████╗██████╗ ██████╗ ███╗ ███╗ ███████╗███████╗██╗ ██╗ ██╗ ██╗██╗
+██║ ██║██║ ██╔════╝██╔══██╗██╔═══██╗████╗ ████║ ╚══███╔╝██╔════╝██║ ██║ ██║ ██║██║
+███████║██║ █████╗ ██████╔╝██║ ██║██╔████╔██║ ███╔╝ █████╗ ██║ ██║ ██║ ██║██║
+██╔══██║██║ ██╔══╝ ██╔══██╗██║ ██║██║╚██╔╝██║ ███╔╝ ██╔══╝ ██║ ██║ ██║██ ██║╚═╝
+██║ ██║██║ ██║ ██║ ██║╚██████╔╝██║ ╚═╝ ██║ ███████╗███████╗███████╗███████╗██║╚█████╔╝██╗
+╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝ ╚═════╝ ╚═╝ ╚═╝ ╚══════╝╚══════╝╚══════╝╚══════╝╚═╝ ╚════╝ ╚═╝
+";
+
+static SMALL_BANNER: &str = "
+██╗ ██╗██╗ ██╗
+██║ ██║██║ ██║
+███████║██║ ██║
+██╔══██║██║ ╚═╝
+██║ ██║██║ ██╗
+╚═╝ ╚═╝╚═╝ ╚═╝
+";
+
+static MEDIUM_BANNER: &str = "
+██╗ ██╗██╗ ████████╗██╗ ██╗███████╗██████╗ ███████╗ ██╗
+██║ ██║██║ ╚══██╔══╝██║ ██║██╔════╝██╔══██╗██╔════╝ ██║
+███████║██║ ██║ ███████║█████╗ ██████╔╝█████╗ ██║
+██╔══██║██║ ██║ ██╔══██║██╔══╝ ██╔══██╗██╔══╝ ╚═╝
+██║ ██║██║ ██║ ██║ ██║███████╗██║ ██║███████╗ ██╗
+╚═╝ ╚═╝╚═╝ ╚═╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚══════╝ ╚═╝
+";
+
+pub fn render_banner(x: usize, y: usize, rows: usize, cols: usize) {
+ if rows >= 8 {
+ if cols > 100 {
+ println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
+ for line in BANNER.lines() {
+ println!("\u{1b}[{}C{}", x.saturating_sub(1), line);
+ }
+ } else if cols > 63 {
+ println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
+ let x = (cols.saturating_sub(63) as f64 / 2.0) as usize;
+ for line in MEDIUM_BANNER.lines() {
+ println!("\u{1b}[{}C{}", x, line);
+ }
+ } else {
+ println!("\u{1b}[{}H", y + rows.saturating_sub(8) / 2);
+ let x = (cols.saturating_sub(18) as f64 / 2.0) as usize;
+ for line in SMALL_BANNER.lines() {
+ println!("\u{1b}[{}C{}", x, line);
+ }
+ }
+ } else if rows > 2 {
+ println!(
+ "\u{1b}[{};{}H\u{1b}[1mHi from Zellij!",
+ (y + rows / 2) + 1,
+ (x + cols.saturating_sub(15) / 2).saturating_sub(1)
+ );
+ }
+}
+
+pub fn render_welcome_boundaries(rows: usize, cols: usize) {
+ let width_of_main_menu = std::cmp::min(cols, 101);
+ let has_room_for_logos = cols.saturating_sub(width_of_main_menu) > 100;
+ let left_boundary_x = (cols.saturating_sub(width_of_main_menu) as f64 / 2.0).floor() as usize;
+ let right_boundary_x = left_boundary_x + width_of_main_menu;
+ let y_starting_point = rows.saturating_sub(15) / 2;
+ let middle_row =
+ (y_starting_point + rows.saturating_sub(y_starting_point) / 2).saturating_sub(1);
+ for i in y_starting_point..rows {
+ if i == middle_row {
+ if has_room_for_logos {
+ print!("\u{1b}[{};{}H┤", i + 1, left_boundary_x + 1);
+ print!(
+ "\u{1b}[m\u{1b}[{};{}H├\u{1b}[K",
+ i + 1,
+ right_boundary_x + 1
+ );
+ print!("\u{1b}[{};{}H", i + 1, left_boundary_x.saturating_sub(9));
+ for _ in 0..10 {
+ print!("─");
+ }
+ print!("\u{1b}[{};{}H", i + 1, right_boundary_x + 2);
+ for _ in 0..10 {
+ print!("─");
+ }
+ } else {
+ print!("\u{1b}[{};{}H│", i + 1, left_boundary_x + 1);
+ print!(
+ "\u{1b}[m\u{1b}[{};{}H│\u{1b}[K",
+ i + 1,
+ right_boundary_x + 1
+ );
+ }
+ } else {
+ if i == y_starting_point {
+ print!("\u{1b}[{};{}H┌", i + 1, left_boundary_x + 1);
+ print!(
+ "\u{1b}[m\u{1b}[{};{}H┐\u{1b}[K",
+ i + 1,
+ right_boundary_x + 1
+ );
+ } else if i == rows.saturating_sub(1) {
+ print!("\u{1b}[{};{}H└", i + 1, left_boundary_x + 1);
+ print!(
+ "\u{1b}[m\u{1b}[{};{}H┘\u{1b}[K",
+ i + 1,
+ right_boundary_x + 1
+ );
+ } else {
+ print!("\u{1b}[{};{}H│", i + 1, left_boundary_x + 1);
+ print!(
+ "\u{1b}[m\u{1b}[{};{}H│\u{1b}[K",
+ i + 1,
+ right_boundary_x + 1
+ ); // this includes some
+ // ANSI magic to delete
+ // everything after this
+ // boundary in order to
+ // fix some rendering
+ // bugs in the legacy
+ // components of this
+ // plugin
+ }
+ }
+ }
+ if rows.saturating_sub(y_starting_point) > 25 && has_room_for_logos {
+ for (i, line) in LOGO.lines().enumerate() {
+ print!(
+ "\u{1b}[{};{}H{}",
+ middle_row.saturating_sub(12) + i,
+ 0,
+ line
+ );
+ }
+ for (i, line) in LOGO.lines().enumerate() {
+ print!(
+ "\u{1b}[{};{}H{}",
+ middle_row.saturating_sub(12) + i,
+ cols.saturating_sub(47),
+ line
+ );
+ }
+ }
+}
+static LOGO: &str = r#" 
+ 
+  _y$ y@g_
+  ya@@@@L4@@@@gy_
+  u@@@@@@F "@@@@@@@@y_
+  _a@@, @@@P~` __ ~T@@@@@@@@g
+  _yg@@@@@$ "~ _yg@@@@gy `~PR@F~_yggy_
+  y$@@@@@@PF _y$@@@@@@@@@@gy_ 4@@@@@@@y_
+  g@@@@@F~ _yg@@@@@@@@@@@@@@@@@@g_ ~F@@@@@@
+  $@@@F yg@@@@@@@@@@@@@@@@@@@@@@@@gy 9@@F"
+  $@@@$ 4@@@@@@~@@@@@@@@@@@@@@@@@@@@@ "_yg$
+  $@@@$ 4@@@@@ ~@@@@@@@@@@@@@@@@@@@ g@@@@
+  $@@@$ 4@@@@@@y ~@@@@@@@@@@@@@@@@@ $@@@F
+  $@@@$ 4@@@@@@@@y ~@@@@@@@@@@@@@@@ `~_y_
+  $@@@$ 4@@@@@@@P` _g@@@@@@@@@@@@@@@ y@@@@
+  $@@@$ 4@@@@@P` _y@@@@@@@@@@@@@@@@@ 4@@@@
+  $@@@$ 4@@@@$_ y@@@@@ $@@@@ 4@@@@
+  $@@@$ 4@@@@@@g@@@@@@@@@@@@@@@@@@@@@ ~@@@
+  $@@P 7R@@@@@@@@@@@@@@@@@@@@@@@@P~ _@g_7@
+  ~~_yg@gy_ ~5@@@@@@@@@@@@@@@@@@F~ _yg@@@@y
+  R@@@@@@@ ~~@@@@@@@@@@@P~` ya@@@@@@@@F
+  ~5@@@@ 4@gy `~4@@@@F~ y@@@@@@@P~`
+  `~P 4@@@@gy_ `` _yr a@@@@@F~
+  5@@@@@@@gg@@~_g@@@~`
+  ~~@@@@@@@^y@F~
+  `~4@F ~
+ 
+  "#;
diff --git a/src/commands.rs b/src/commands.rs
index 6074ea7d..52dff211 100644
--- a/src/commands.rs
+++ b/src/commands.rs
@@ -18,16 +18,17 @@ use zellij_client::{
use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl};
use zellij_utils::{
cli::{CliArgs, Command, SessionCommand, Sessions},
- data::ConnectToSession,
+ data::{ConnectToSession, LayoutInfo},
envs,
input::{
actions::Action,
config::{Config, ConfigError},
+ layout::Layout,
options::Options,
},
miette::{Report, Result},
nix,
- setup::Setup,
+ setup::{find_default_config_dir, get_layout_dir, Setup},
};
pub(crate) use crate::sessions::list_sessions;
@@ -383,24 +384,25 @@ fn attach_with_session_name(
pub(crate) fn start_client(opts: CliArgs) {
// look for old YAML config/layout/theme files and convert them to KDL
convert_old_yaml_files(&opts);
- let (config, layout, config_options) = match Setup::from_cli_args(&opts) {
- Ok(results) => results,
- Err(e) => {
- if let ConfigError::KdlError(error) = e {
- let report: Report = error.into();
- eprintln!("{:?}", report);
- } else {
- eprintln!("{}", e);
- }
- process::exit(1);
- },
- };
+ let (config, layout, config_options, config_without_layout, config_options_without_layout) =
+ match Setup::from_cli_args(&opts) {
+ Ok(results) => results,
+ Err(e) => {
+ if let ConfigError::KdlError(error) = e {
+ let report: Report = error.into();
+ eprintln!("{:?}", report);
+ } else {
+ eprintln!("{}", e);
+ }
+ process::exit(1);
+ },
+ };
let mut reconnect_to_session: Option<ConnectToSession> = None;
let os_input = get_os_input(get_client_os_input);
loop {
let os_input = os_input.clone();
- let config = config.clone();
- let layout = layout.clone();
+ let mut config = config.clone();
+ let mut layout = layout.clone();
let mut config_options = config_options.clone();
let mut opts = opts.clone();
let mut is_a_reconnect = false;
@@ -423,6 +425,43 @@ pub(crate) fn start_client(opts: CliArgs) {
opts.session = None;
config_options.attach_to_session = None;
}
+
+ if let Some(reconnect_layout) = &reconnect_to_session.layout {
+ let layout_dir = config.options.layout_dir.clone().or_else(|| {
+ get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir))
+ });
+ let new_session_layout = match reconnect_layout {
+ LayoutInfo::BuiltIn(layout_name) => Layout::from_default_assets(
+ &PathBuf::from(layout_name),
+ layout_dir.clone(),
+ config_without_layout.clone(),
+ ),
+ LayoutInfo::File(layout_name) => Layout::from_path_or_default(
+ Some(&PathBuf::from(layout_name)),
+ layout_dir.clone(),
+ config_without_layout.clone(),
+ ),
+ };
+ match new_session_layout {
+ Ok(new_session_layout) => {
+ // here we make sure to override both the layout and the config, but we do
+ // this with an instance of the config before it was merged with the
+ // layout configuration of the previous iteration of the loop, since we do
+ // not want it to mix with the config of this session
+ let (new_layout, new_layout_config) = new_session_layout;
+ layout = new_layout;
+ let mut new_config = config_without_layout.clone();
+ let _ = new_config.merge(new_layout_config.clone());
+ config = new_config;
+ config_options =
+ config_options_without_layout.merge(new_layout_config.options);
+ },
+ Err(e) => {
+ log::error!("Failed to parse new session layout: {:?}", e);
+ },
+ }
+ }
+
is_a_reconnect = true;
}
diff --git a/src/sessions.rs b/src/sessions.rs
index c577acc6..58722071 100644
--- a/src/sessions.rs
+++ b/src/sessions.rs
@@ -52,8 +52,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
session_layout_cache_file_name(&folder_name.display().to_string());
let raw_layout = match std::fs::read_to_string(&layout_file_name) {
Ok(raw_layout) => raw_layout,
- Err(e) => {
- log::error!("Failed to read resurrection layout file: {:?}", e);
+ Err(_e) => {
return None;
},
};
@@ -61,13 +60,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
.and_then(|metadata| metadata.created())
{
Ok(created) => Some(created),
- Err(e) => {
- log::error!(
- "Failed to read created stamp of resurrection file: {:?}",
- e
- );
- None
- },
+ Err(_e) => None,
};
let layout = match Layout::from_kdl(
&raw_layout,
diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs
index 437f465b..00895570 100644
--- a/zellij-server/src/lib.rs
+++ b/zellij-server/src/lib.rs
@@ -44,7 +44,7 @@ use zellij_utils::{
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{ConnectToSession, Event, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
- home::get_default_data_dir,
+ home::{default_layout_dir, get_default_data_dir},
input::{
command::{RunCommand, TerminalAction},
get_mode_info,
@@ -93,6 +93,7 @@ pub enum ServerInstruction {
pipe_id: String,
client_id: ClientId,
},
+ DisconnectAllClientsExcept(ClientId),
}
impl From<&ServerInstruction> for ServerContext {
@@ -117,6 +118,9 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::AssociatePipeWithClient { .. } => {
ServerContext::AssociatePipeWithClient
},
+ ServerInstruction::DisconnectAllClientsExcept(..) => {
+ ServerContext::DisconnectAllClientsExcept
+ },
}
}
}
@@ -133,6 +137,7 @@ pub(crate) struct SessionMetaData {
pub client_attributes: ClientAttributes,
pub default_shell: Option<TerminalAction>,
pub layout: Box<Layout>,
+ pub config_options: Box<Options>,
screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>,
plugin_thread: Option<thread::JoinHandle<()>>,
@@ -650,6 +655,21 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
}
break;
},
+ ServerInstruction::DisconnectAllClientsExcept(client_id) => {
+ let client_ids: Vec<ClientId> = session_state
+ .read()
+ .unwrap()
+ .client_ids()
+ .iter()
+ .copied()
+ .filter(|c| c != &client_id)
+ .collect();
+ for client_id in client_ids {
+ let _ = os_input
+ .send_to_client(client_id, ServerToClientMsg::Exit(ExitReason::Normal));
+ remove_client!(client_id, os_input, session_state);
+ }
+ },
ServerInstruction::DetachSession(client_ids) => {
for client_id in client_ids {
let _ = os_input
@@ -749,7 +769,16 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
session_state
);
},
- ServerInstruction::SwitchSession(connect_to_session, client_id) => {
+ ServerInstruction::SwitchSession(mut connect_to_session, client_id) => {
+ let layout_dir = session_data
+ .read()
+ .unwrap()
+ .as_ref()
+ .and_then(|c| c.config_options.layout_dir.clone())
+ .or_else(|| default_layout_dir());
+ if let Some(layout_dir) = layout_dir {
+ connect_to_session.apply_layout_dir(&layout_dir);
+ }
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
session_data
.write()
@@ -906,6 +935,7 @@ fn init_session(
let client_attributes_clone = client_attributes.clone();
let debug = opts.debug;
let layout = layout.clone();
+ let config_options = config_options.clone();
move || {
screen_thread_main(
screen_bus,
@@ -1006,6 +1036,7 @@ fn init_session(
default_shell,
client_attributes,
layout,
+ config_options: config_options.clone(),
screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread),
plugin_thread: Some(plugin_thread),
diff --git a/zellij-server/src/plugins/unit/plugin_tests.rs b/zellij-server/src/plugins/unit/plugin_tests.rs
index 6791213c..6b6e345e 100644
--- a/zellij-server/src/plugins/unit/plugin_tests.rs
+++ b/zellij-server/src/plugins/unit/plugin_tests.rs
@@ -5970,3 +5970,243 @@ pub fn pipe_message_to_plugin_plugin_command() {
});
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
}
+
+#[test]
+#[ignore]
+pub fn switch_session_plugin_command() {
+ let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+ // destructor removes the directory
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
+ create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ };
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+ received_screen_instructions,
+ ScreenInstruction::Exit,
+ screen_receiver,
+ 1,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ client_id
+ );
+ let received_server_instruction = Arc::new(Mutex::new(vec![]));
+ let server_thread = log_actions_in_thread!(
+ received_server_instruction,
+ ServerInstruction::SwitchSession,
+ server_receiver,
+ 1
+ );
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ tab_index,
+ None,
+ client_id,
+ size,
+ None,
+ false,
+ ));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(Key::Ctrl('5')), // this triggers the enent in the fixture plugin
+ )]));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ teardown();
+ server_thread.join().unwrap(); // this might take a while if the cache is cold
+ let switch_session_event = received_server_instruction
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ServerInstruction::SwitchSession(..) = i {
+ Some(i.clone())
+ } else {
+ None
+ }
+ })
+ .clone();
+ assert_snapshot!(format!("{:#?}", switch_session_event));
+}
+
+#[test]
+#[ignore]
+pub fn switch_session_with_layout_plugin_command() {
+ let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+ // destructor removes the directory
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
+ create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ };
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+ received_screen_instructions,
+ ScreenInstruction::Exit,
+ screen_receiver,
+ 1,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ client_id
+ );
+ let received_server_instruction = Arc::new(Mutex::new(vec![]));
+ let server_thread = log_actions_in_thread!(
+ received_server_instruction,
+ ServerInstruction::SwitchSession,
+ server_receiver,
+ 1
+ );
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ tab_index,
+ None,
+ client_id,
+ size,
+ None,
+ false,
+ ));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(Key::Ctrl('7')), // this triggers the enent in the fixture plugin
+ )]));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ teardown();
+ server_thread.join().unwrap(); // this might take a while if the cache is cold
+ let switch_session_event = received_server_instruction
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ServerInstruction::SwitchSession(..) = i {
+ Some(i.clone())
+ } else {
+ None
+ }
+ })
+ .clone();
+ assert_snapshot!(format!("{:#?}", switch_session_event));
+}
+
+#[test]
+#[ignore]
+pub fn disconnect_other_clients_plugins_command() {
+ let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
+ // destructor removes the directory
+ let plugin_host_folder = PathBuf::from(temp_folder.path());
+ let cache_path = plugin_host_folder.join("permissions_test.kdl");
+ let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
+ create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
+ let plugin_should_float = Some(false);
+ let plugin_title = Some("test_plugin".to_owned());
+ let run_plugin = RunPlugin {
+ _allow_exec_host_cmd: false,
+ location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
+ configuration: Default::default(),
+ };
+ let tab_index = 1;
+ let client_id = 1;
+ let size = Size {
+ cols: 121,
+ rows: 20,
+ };
+ let received_screen_instructions = Arc::new(Mutex::new(vec![]));
+ let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
+ received_screen_instructions,
+ ScreenInstruction::Exit,
+ screen_receiver,
+ 1,
+ &PermissionType::ChangeApplicationState,
+ cache_path,
+ plugin_thread_sender,
+ client_id
+ );
+ let received_server_instruction = Arc::new(Mutex::new(vec![]));
+ let server_thread = log_actions_in_thread!(
+ received_server_instruction,
+ ServerInstruction::DisconnectAllClientsExcept,
+ server_receiver,
+ 1
+ );
+
+ let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
+ let _ = plugin_thread_sender.send(PluginInstruction::Load(
+ plugin_should_float,
+ false,
+ plugin_title,
+ run_plugin,
+ tab_index,
+ None,
+ client_id,
+ size,
+ None,
+ false,
+ ));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+
+ let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
+ None,
+ Some(client_id),
+ Event::Key(Key::Ctrl('6')), // this triggers the enent in the fixture plugin
+ )]));
+ std::thread::sleep(std::time::Duration::from_millis(500));
+ teardown();
+ server_thread.join().unwrap(); // this might take a while if the cache is cold
+ let switch_session_event = received_server_instruction
+ .lock()
+ .unwrap()
+ .iter()
+ .rev()
+ .find_map(|i| {
+ if let ServerInstruction::DisconnectAllClientsExcept(..) = i {
+ Some(i.clone())
+ } else {
+ None
+ }
+ })
+ .clone();
+ assert_snapshot!(format!("{:#?}", switch_session_event));
+}
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap
new file mode 100644
index 00000000..d463df1c
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__disconnect_other_clients_plugins_command.snap
@@ -0,0 +1,10 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 6131
+expression: "format!(\"{:#?}\", switch_session_event)"
+---
+Some(
+ DisconnectAllClientsExcept(
+ 1,
+ ),
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap
new file mode 100644
index 00000000..f254f039
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_plugin_command.snap
@@ -0,0 +1,18 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 6051
+expression: "format!(\"{:#?}\", switch_session_event)"
+---
+Some(
+ SwitchSession(
+ ConnectToSession {
+ name: Some(
+ "my_new_session",
+ ),
+ tab_position: None,
+ pane_id: None,
+ layout: None,
+ },
+ 1,
+ ),
+)
diff --git a/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap
new file mode 100644
index 00000000..46f6819a
--- /dev/null
+++ b/zellij-server/src/plugins/unit/snapshots/zellij_server__plugins__plugin_tests__switch_session_with_layout_plugin_command.snap
@@ -0,0 +1,22 @@
+---
+source: zellij-server/src/plugins/./unit/plugin_tests.rs
+assertion_line: 6131
+expression: "format!(\"{:#?}\", switch_session_event)"
+---
+Some(
+ SwitchSession(
+ ConnectToSession {
+ name: Some(
+ "my_other_new_session",
+ ),
+ tab_position: None,
+ pane_id: None,
+ layout: Some(
+ BuiltIn(
+ "compact",
+ ),
+ ),
+ },
+ 1,
+ ),
+)
diff --git a/zellij-server/src/plugins/zellij_exports.rs b/zellij-server/src/plugins/zellij_exports.rs
index 8791e128..e6787c24 100644
--- a/zellij-server/src/plugins/zellij_exports.rs
+++ b/zellij-server/src/plugins/zellij_exports.rs
@@ -18,10 +18,14 @@ use std::{
use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports};
use wasmer_wasi::WasiEnv;
use zellij_utils::data::{
- CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType,
- PluginPermission,
+ CommandType, ConnectToSession, HttpVerb, LayoutInfo, MessageToPlugin, PermissionStatus,
+ PermissionType, PluginPermission,
};
use zellij_utils::input::permission::PermissionCache;
+use zellij_utils::{
+ interprocess::local_socket::LocalSocketStream,
+ ipc::{ClientToServerMsg, IpcSenderWithContext},
+};
use url::Url;
@@ -225,6 +229,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
connect_to_session.name,
connect_to_session.tab_position,
connect_to_session.pane_id,
+ connect_to_session.layout,
)?,
PluginCommand::DeleteDeadSession(session_name) => {
delete_dead_session(session_name)?
@@ -252,6 +257,8 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
cli_pipe_output(env, pipe_name, output)?
},
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
+ PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
+ PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
},
(PermissionStatus::Denied, permission) => {
log::error!(
@@ -900,6 +907,7 @@ fn switch_session(
session_name: Option<String>,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
+ layout: Option<LayoutInfo>,
) -> Result<()> {
// pane_id is (id, is_plugin)
let err_context = || format!("Failed to switch session");
@@ -909,6 +917,7 @@ fn switch_session(
name: session_name,
tab_position,
pane_id,
+ layout,
};
env.plugin_env
.senders
@@ -1278,6 +1287,30 @@ fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) {
apply_action!(action, error_msg, env);
}
+fn disconnect_other_clients(env: &ForeignFunctionEnv) {
+ let _ = env
+ .plugin_env
+ .senders
+ .send_to_server(ServerInstruction::DisconnectAllClientsExcept(
+ env.plugin_env.client_id,
+ ))
+ .context("failed to send disconnect other clients instruction");
+}
+
+fn kill_sessions(session_names: Vec<String>) {
+ for session_name in session_names {
+ let path = &*ZELLIJ_SOCK_DIR.join(&session_name);
+ match LocalSocketStream::connect(path) {
+ Ok(stream) => {
+ let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession);
+ },
+ Err(e) => {
+ log::error!("Failed to kill session {}: {:?}", session_name, e);
+ },
+ };
+ }
+}
+
// Custom panic handler for plugins.
//
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
@@ -1406,7 +1439,9 @@ fn check_command_permission(
| PluginCommand::DeleteDeadSession(..)
| PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameSession(..)
- | PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
+ | PluginCommand::RenameTab(..)
+ | PluginCommand::DisconnectOtherClients
+ | PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,
diff --git a/zellij-server/src/screen.rs b/zellij-server/src/screen.rs
index c312480d..5b0b6bea 100644
--- a/zellij-server/src/screen.rs
+++ b/zellij-server/src/screen.rs
@@ -577,6 +577,8 @@ pub(crate) struct Screen {
default_shell: Option<PathBuf>,
styled_underlines: bool,
arrow_fonts: bool,
+ layout_dir: Option<PathBuf>,
+ default_layout_name: Option<String>,
}
impl Screen {
@@ -592,12 +594,14 @@ impl Screen {
copy_options: CopyOptions,
debug: bool,
default_layout: Box<Layout>,
+ default_layout_name: Option<String>,
default_shell: Option<PathBuf>,
session_serialization: bool,
serialize_pane_viewport: bool,
scrollback_lines_to_serialize: Option<usize>,
styled_underlines: bool,
arrow_fonts: bool,
+ layout_dir: Option<PathBuf>,
) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone());
@@ -629,6 +633,7 @@ impl Screen {
session_name,
session_infos_on_machine,
default_layout,
+ default_layout_name,
default_shell,
session_serialization,
serialize_pane_viewport,
@@ -636,6 +641,7 @@ impl Screen {
styled_underlines,
arrow_fonts,
resurrectable_sessions,
+ layout_dir,
}
}
@@ -1412,12 +1418,21 @@ impl Screen {
// generate own session info
let pane_manifest = self.generate_and_report_pane_state()?;
let tab_infos = self.generate_and_report_tab_state()?;
+ // in the context of unit/integration tests, we don't need to list available layouts
+ // because this is mostly about HD access - it does however throw off the timing in the
+ // tests and causes them to flake, which is why we skip it here
+ #[cfg(not(test))]
+ let available_layouts =
+ Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name);
+ #[cfg(test)]
+ let available_layouts = vec![];
let session_info = SessionInfo {
name: self.session_name.clone(),
tabs: tab_infos,
panes: pane_manifest,
connected_clients: self.active_tab_indices.keys().len(),
is_current_session: true,
+ available_layouts,
};
self.bus
.senders
@@ -2101,7 +2116,11 @@ pub(crate) fn screen_thread_main(
let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false);
let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize;
let session_is_mirrored = config_options.mirror_session.unwrap_or(false);
+ let layout_dir = config_options.layout_dir;
let default_shell = config_options.default_shell;
+ let default_layout_name = config_options
+ .default_layout
+ .map(|l| format!("{}", l.display()));
let copy_options = CopyOptions::new(
config_options.copy_command,
config_options.copy_clipboard.unwrap_or_default(),
@@ -2128,12 +2147,14 @@ pub(crate) fn screen_thread_main(
copy_options,
debug,
default_layout,
+ default_layout_name,
default_shell,
session_serialization,
serialize_pane_viewport,
scrollback_lines_to_serialize,
styled_underlines,
arrow_fonts,
+ layout_dir,
);
let mut pending_tab_ids: HashSet<usize> = HashSet::new();
diff --git a/zellij-server/src/tab/mod.rs b/zellij-server/src/tab/mod.rs
index 0352eb96..2ed9eb35 100644
--- a/zellij-server/src/tab/mod.rs
+++ b/zellij-server/src/tab/mod.rs
@@ -847,7 +847,7 @@ impl Tab {
pub fn rename_session(&mut self, new_session_name: String) -> Result<()> {
{
let mode_infos = &mut self.mode_info.borrow_mut();
- for (_client_id, mut mode_info) in mode_infos.iter_mut() {
+ for (_client_id, mode_info) in mode_infos.iter_mut() {
mode_info.session_name = Some(new_session_name.clone());
}
self.default_mode_info.session_name = Some(new_session_name);
diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs
index 34076d70..4ce4dbc1 100644
--- a/zellij-server/src/unit/screen_tests.rs
+++ b/zellij-server/src/unit/screen_tests.rs
@@ -241,10 +241,12 @@ fn create_new_screen(size: Size) -> Screen {
let session_is_mirrored = true;
let copy_options = CopyOptions::default();
let default_layout = Box::new(Layout::default());
+ let default_layout_name = None;
let default_shell = None;
let session_serialization = true;
let serialize_pane_viewport = false;
let scrollback_lines_to_serialize = None;
+ let layout_dir = None;
let debug = false;
let styled_underlines = true;
@@ -260,12 +262,14 @@ fn create_new_screen(size: Size) -> Screen {
copy_options,
debug,
default_layout,
+ default_layout_name,
default_shell,
session_serialization,
serialize_pane_viewport,
scrollback_lines_to_serialize,
styled_underlines,
arrow_fonts,
+ layout_dir,
);
screen
}
@@ -425,6 +429,7 @@ impl MockScreen {
plugin_thread: None,
pty_writer_thread: None,
background_jobs_thread: None,
+ config_options: Default::default(),
layout,
}
}
@@ -481,6 +486,7 @@ impl MockScreen {
plugin_thread: None,
pty_writer_thread: None,
background_jobs_thread: None,
+ config_options: Default::default(),
layout,
};
@@ -2869,17 +2875,17 @@ pub fn screen_can_break_floating_pane_to_a_new_tab() {
1,
1,
));
- std::thread::sleep(std::time::Duration::from_millis(100));
+ std::thread::sleep(std::time::Duration::from_millis(200));
// move back to make sure the other pane is in the previous tab
let _ = mock_screen
.to_screen
.send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1));
- std::thread::sleep(std::time::Duration::from_millis(100));
+ std::thread::sleep(std::time::Duration::from_millis(200));
// move forward to make sure the broken pane is in the previous tab
let _ = mock_screen
.to_screen
.send(ScreenInstruction::MoveFocusRightOrNextTab(1));
- std::thread::sleep(std::time::Duration::from_millis(100));
+ std::thread::sleep(std::time::Duration::from_millis(200));
mock_screen.teardown(vec![server_thread, screen_thread]);
diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs
index 975e3499..dd5d1807 100644
--- a/zellij-tile/src/shim.rs
+++ b/zellij-tile/src/shim.rs
@@ -653,6 +653,18 @@ pub fn switch_session(name: Option<&str>) {
unsafe { host_run_plugin_command() };
}
+/// Switch to a session with the given name, create one if no name is given
+pub fn switch_session_with_layout(name: Option<&str>, layout: LayoutInfo) {
+ let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
+ name: name.map(|n| n.to_string()),
+ layout: Some(layout),
+ ..Default::default()
+ });
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
/// Switch to a session with the given name, focusing either the provided pane_id or the provided
/// tab position (in that order)
pub fn switch_session_with_focus(
@@ -664,6 +676,7 @@ pub fn switch_session_with_focus(
name: Some(name.to_owned()),
tab_position,
pane_id,
+ ..Default::default()
});
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
@@ -726,6 +739,26 @@ pub fn pipe_message_to_plugin(message_to_plugin: MessageToPlugin) {
unsafe { host_run_plugin_command() };
}
+/// Disconnect all other clients from the current session
+pub fn disconnect_other_clients() {
+ let plugin_command = PluginCommand::DisconnectOtherClients;
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
+/// Kill all Zellij sessions in the list
+pub fn kill_sessions<S: AsRef<str>>(session_names: &[S])
+where
+ S: ToString,
+{
+ let plugin_command =
+ PluginCommand::KillSessions(session_names.into_iter().map(|s| s.to_string()).collect());
+ let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
+ object_to_stdout(&protobuf_plugin_command.encode_to_vec());
+ unsafe { host_run_plugin_command() };
+}
+
// Utility Functions
#[allow(unused)]
diff --git a/zellij-utils/assets/layouts/welcome.kdl b/zellij-utils/assets/layouts/welcome.kdl
new file mode 100644
index 00000000..1884fe1f
--- /dev/null
+++ b/zellij-utils/assets/layouts/welcome.kdl
@@ -0,0 +1,8 @@
+layout {
+ pane borderless=true {
+ plugin location="zellij:session-manager" {
+ welcome_screen true
+ }
+ }
+}
+session_serialization false // this will apply only to the initial welcome screen layout, and is intended to prevent lots of garbage sessions left around
diff --git a/zellij-utils/assets/prost/api.event.rs b/zellij-utils/assets/prost/api.event.rs
index c095bed2..6d4ed729 100644
--- a/zellij-utils/assets/prost/api.event.rs
+++ b/zellij-utils/assets/prost/api.event.rs
@@ -172,6 +172,16 @@ pub struct SessionManifest {
pub connected_clients: u32,
#[prost(bool, tag = "5")]
pub is_current_session: bool,
+ #[prost(message, repeated, tag = "6")]
+ pub available_layouts: ::prost::alloc::vec::Vec<LayoutInfo>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct LayoutInfo {
+ #[prost(string, tag = "1")]
+ pub name: ::prost::alloc::string::String,
+ #[prost(string, tag = "2")]
+ pub source: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
diff --git a/zellij-utils/assets/prost/api.plugin_command.rs b/zellij-utils/assets/prost/api.plugin_command.rs
index d9b528ae..5127bb81 100644
--- a/zellij-utils/assets/prost/api.plugin_command.rs
+++ b/zellij-utils/assets/prost/api.plugin_command.rs
@@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
oneof = "plugin_command::Payload",
- tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50"
+ tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60"
)]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
@@ -112,10 +112,18 @@ pub mod plugin_command {
CliPipeOutputPayload(super::CliPipeOutputPayload),
#[prost(message, tag = "50")]
MessageToPluginPayload(super::MessageToPluginPayload),
+ #[prost(message, tag = "60")]
+ KillSessionsPayload(super::KillSessionsPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
+pub struct KillSessionsPayload {
+ #[prost(string, repeated, tag = "1")]
+ pub session_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
+}
+#[allow(clippy::derive_partial_eq_without_eq)]
+#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CliPipeOutputPayload {
#[prost(string, tag = "1")]
pub pipe_name: ::prost::alloc::string::String,
@@ -171,6 +179,8 @@ pub struct SwitchSessionPayload {
pub pane_id: ::core::option::Option<u32>,
#[prost(bool, optional, tag = "4")]
pub pane_id_is_plugin: ::core::option::Option<bool>,
+ #[prost(message, optional, tag = "5")]
+ pub layout: ::core::option::Option<super::event::LayoutInfo>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@@ -376,6 +386,8 @@ pub enum CommandName {
BlockCliPipeInput = 77,
CliPipeOutput = 78,
MessageToPlugin = 79,
+ DisconnectOtherClients = 80,
+ KillSessions = 81,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@@ -464,6 +476,8 @@ impl CommandName {
CommandName::BlockCliPipeInput => "BlockCliPipeInput",
CommandName::CliPipeOutput => "CliPipeOutput",
CommandName::MessageToPlugin => "MessageToPlugin",
+ CommandName::DisconnectOtherClients => "DisconnectOtherClients",
+ CommandName::KillSessions => "KillSessions",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@@ -549,6 +563,8 @@ impl CommandName {
"BlockCliPipeInput" => Some(Self::BlockCliPipeInput),
"CliPipeOutput" => Some(Self::CliPipeOutput),
"MessageToPlugin" => Some(Self::MessageToPlugin),
+ "DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
+ "KillSessions" => Some(Self::KillSessions),
_ => None,
}
}
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index f1e01115..d001b75f 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -765,6 +765,28 @@ pub struct SessionInfo {
pub panes: PaneManifest,
pub connected_clients: usize,
pub is_current_session: bool,
+ pub available_layouts: Vec<LayoutInfo>,
+}
+
+#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
+pub enum LayoutInfo {
+ BuiltIn(String),
+ File(String),
+}
+
+impl LayoutInfo {
+ pub fn name(&self) -> &str {
+ match self {
+ LayoutInfo::BuiltIn(name) => &name,
+ LayoutInfo::File(name) => &name,
+ }
+ }
+ pub fn is_builtin(&self) -> bool {
+ match self {
+ LayoutInfo::BuiltIn(_name) => true,
+ LayoutInfo::File(_name) => false,
+ }
+ }
}
use std::hash::{Hash, Hasher};
@@ -1032,12 +1054,12 @@ impl MessageToPlugin {
self
}
pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
- let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
+ let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.should_float = Some(should_float);
self
}
pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self {
- let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
+ let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.pane_id_to_replace = Some(pane_id);
self
}
@@ -1045,17 +1067,17 @@ impl MessageToPlugin {
mut self,
pane_title: impl Into<String>,
) -> Self {
- let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
+ let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.pane_title = Some(pane_title.into());
self
}
pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self {
- let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
+ let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.cwd = Some(cwd);
self
}
pub fn new_plugin_instance_should_skip_cache(mut self) -> Self {
- let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
+ let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.skip_cache = true;
self
}
@@ -1066,6 +1088,17 @@ pub struct ConnectToSession {
pub name: Option<String>,
pub tab_position: Option<usize>,
pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
+ pub layout: Option<LayoutInfo>,
+}
+
+impl ConnectToSession {
+ pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
+ if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() {
+ *file_path = Path::join(layout_dir, &file_path)
+ .to_string_lossy()
+ .to_string();
+ }
+ }
}
#[derive(Debug, Default, Clone)]
@@ -1228,4 +1261,6 @@ pub enum PluginCommand {
BlockCliPipeInput(String), // String => pipe name
CliPipeOutput(String, String), // String => pipe name, String => output
MessageToPlugin(MessageToPlugin),
+ DisconnectOtherClients,
+ KillSessions(Vec<String>), // one or more session names
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 26ad521c..5b323399 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -442,6 +442,7 @@ pub enum ServerContext {
UnblockCliPipeInput,
CliPipeOutput,
AssociatePipeWithClient,
+ DisconnectAllClientsExcept,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs
index 23c05d4f..eb8fe521 100644
--- a/zellij-utils/src/input/config.rs
+++ b/zellij-utils/src/input/config.rs
@@ -222,6 +222,15 @@ impl Config {
Err(e) => Err(ConfigError::IoPath(e, path.into())),
}
}
+ pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
+ self.options = self.options.merge(other.options);
+ self.keybinds.merge(other.keybinds.clone());
+ self.themes = self.themes.merge(other.themes);
+ self.plugins = self.plugins.merge(other.plugins);
+ self.ui = self.ui.merge(other.ui);
+ self.env = self.env.merge(other.env);
+ Ok(())
+ }
}
#[cfg(test)]
diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs
index eaed03fd..ab13f711 100644
--- a/zellij-utils/src/input/keybinds.rs
+++ b/zellij-utils/src/input/keybinds.rs
@@ -65,6 +65,17 @@ impl Keybinds {
}
ret
}
+ pub fn merge(&mut self, mut other: Keybinds) {
+ for (other_input_mode, mut other_input_mode_keybinds) in other.0.drain() {
+ let input_mode_keybinds = self
+ .0
+ .entry(other_input_mode)
+ .or_insert_with(|| Default::default());
+ for (other_action, other_action_keybinds) in other_input_mode_keybinds.drain() {
+ input_mode_keybinds.insert(other_action, other_action_keybinds);
+ }
+ }
+ }
}
// The unit test location.
diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs
index 82dab15e..7f65d605 100644
--- a/zellij-utils/src/input/layout.rs
+++ b/zellij-utils/src/input/layout.rs
@@ -9,8 +9,8 @@
// If plugins should be able to depend on the layout system
// then [`zellij-utils`] could be a proper place.
use crate::{
- data::Direction,
- home::find_default_config_dir,
+ data::{Direction, LayoutInfo},
+ home::{default_layout_dir, find_default_config_dir},
input::{
command::RunCommand,
config::{Config, ConfigError},
@@ -19,6 +19,7 @@ use crate::{
setup::{self},
};
+use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
@@ -819,6 +820,62 @@ impl Default for LayoutParts {
}
impl Layout {
+ // the first layout will either be the default one
+ pub fn list_available_layouts(
+ layout_dir: Option<PathBuf>,
+ default_layout_name: &Option<String>,
+ ) -> Vec<LayoutInfo> {
+ let mut available_layouts = layout_dir
+ .clone()
+ .or_else(|| default_layout_dir())
+ .and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
+ Ok(layout_files) => Some(layout_files),
+ Err(e) => {
+ log::error!("Failed to read layout dir: {:?}", e);
+ None
+ },
+ })
+ .map(|layout_files| {
+ let mut available_layouts = vec![];
+ for file in layout_files {
+ if let Ok(file) = file {
+ if Layout::from_path_or_default_without_config(
+ Some(&file.path()),
+ layout_dir.clone(),
+ )
+ .is_ok()
+ {
+ if let Some(file_name) = file.path().file_stem() {
+ available_layouts
+ .push(LayoutInfo::File(file_name.to_string_lossy().to_string()))
+ }
+ }
+ }
+ }
+ available_layouts
+ })
+ .unwrap_or_else(Default::default);
+ let default_layout_name = default_layout_name
+ .as_ref()
+ .map(|d| d.as_str())
+ .unwrap_or("default");
+ available_layouts.push(LayoutInfo::BuiltIn("default".to_owned()));
+ available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned()));
+ available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned()));
+ available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned()));
+ available_layouts.sort_by(|a, b| {
+ let a_name = a.name();
+ let b_name = b.name();
+ if a_name == default_layout_name {
+ return Ordering::Less;
+ } else if b_name == default_layout_name {
+ return Ordering::Greater;
+ } else {
+ a_name.cmp(&b_name)
+ }
+ });
+ available_layouts
+ }
pub fn stringified_from_path_or_default(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
@@ -861,6 +918,40 @@ impl Layout {
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config))
}
+ pub fn from_path_or_default_without_config(
+ layout_path: Option<&PathBuf>,
+ layout_dir: Option<PathBuf>,
+ ) -> Result<Layout, ConfigError> {
+ let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
+ Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
+ let layout = Layout::from_kdl(
+ &raw_layout,
+ path_to_raw_layout,
+ raw_swap_layouts
+ .as_ref()
+ .map(|(r, f)| (r.as_str(), f.as_str())),
+ None,
+ )?;
+ Ok(layout)
+ }
+ pub fn from_default_assets(
+ layout_name: &Path,
+ _layout_dir: Option<PathBuf>,
+ config: Config,
+ ) -> Result<(Layout, Config), ConfigError> {
+ let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
+ Layout::stringified_from_default_assets(layout_name)?;
+ let layout = Layout::from_kdl(
+ &raw_layout,
+ path_to_raw_layout,
+ raw_swap_layouts
+ .as_ref()
+ .map(|(r, f)| (r.as_str(), f.as_str())),
+ None,
+ )?;
+ let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
+ Ok((layout, config))
+ }
pub fn from_str(
raw: &str,
path_to_raw_layout: String,
@@ -951,6 +1042,11 @@ impl Layout {
Self::stringified_compact_swap_from_assets()?,
)),
)),
+ Some("welcome") => Ok((
+ "Welcome screen layout".into(),
+ Self::stringified_welcome_from_assets()?,
+ None,
+ )),
None | Some(_) => Err(ConfigError::IoPath(
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
path.into(),
@@ -982,6 +1078,10 @@ impl Layout {
Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
}
+ pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> {
+ Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?)
+ }
+
pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
self.template.clone().unwrap_or_default()
}
@@ -1024,24 +1124,10 @@ impl Layout {
swap_layout_path.as_os_str().to_string_lossy().into(),
swap_kdl_layout,
)),
- Err(e) => {
- log::warn!(
- "Failed to read swap layout file: {}. Error: {:?}",
- swap_layout_path.as_os_str().to_string_lossy(),
- e
- );
- None
- },
+ Err(_e) => None,
}
},
- Err(e) => {
- log::warn!(
- "Failed to read swap layout file: {}. Error: {:?}",
- swap_layout_path.as_os_str().to_string_lossy(),
- e
- );
- None
- },
+ Err(_e) => None,
}
}
}
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 71f48fc4..e911f391 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -1,7 +1,7 @@
mod kdl_layout_parser;
use crate::data::{
- Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType,
- Resize, SessionInfo, TabInfo,
+ Direction, InputMode, Key, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest,
+ PermissionType, Resize, SessionInfo, TabInfo,
};
use crate::envs::EnvironmentVariables;
use crate::home::{find_default_config_dir, get_layout_dir};
@@ -1986,6 +1986,31 @@ impl SessionInfo {
.and_then(|p| p.children())
.map(|p| PaneManifest::decode_from_kdl(p))
.ok_or("Failed to parse panes")?;
+ let available_layouts: Vec<LayoutInfo> = kdl_document
+ .get("available_layouts")
+ .and_then(|p| p.children())
+ .map(|e| {
+ e.nodes()
+ .iter()
+ .filter_map(|n| {
+ let layout_name = n.name().value().to_owned();
+ let layout_source = n
+ .entries()
+ .iter()
+ .find(|e| e.name().map(|n| n.value()) == Some("source"))
+ .and_then(|e| e.value().as_string());
+ match layout_source {
+ Some(layout_source) => match layout_source {
+ "built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
+ "file" => Some(LayoutInfo::File(layout_name)),
+ _ => None,
+ },
+ None => None,
+ }
+ })
+ .collect()
+ })
+ .ok_or("Failed to parse available_layouts")?;
let is_current_session = name == current_session_name;
Ok(SessionInfo {
name,
@@ -1993,6 +2018,7 @@ impl SessionInfo {
panes,
connected_clients,
is_current_session,
+ available_layouts,
})
}
pub fn to_string(&self) -> String {
@@ -2017,10 +2043,25 @@ impl SessionInfo {
let mut panes = KdlNode::new("panes");
panes.set_children(self.panes.encode_to_kdl());
+ let mut available_layouts = KdlNode::new("available_layouts");
+ let mut available_layouts_children = KdlDocument::new();
+ for layout_info in &self.available_layouts {
+ let (layout_name, layout_source) = match layout_info {
+ LayoutInfo::File(name) => (name.clone(), "file"),
+ LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
+ };
+ let mut layout_node = KdlNode::new(format!("{}", layout_name));
+ let layout_source = KdlEntry::new_prop("source", layout_source);
+ layout_node.entries_mut().push(layout_source);
+ available_layouts_children.nodes_mut().push(layout_node);
+ }
+ available_layouts.set_children(available_layouts_children);
+
kdl_document.nodes_mut().push(name);
kdl_document.nodes_mut().push(tabs);
kdl_document.nodes_mut().push(panes);
kdl_document.nodes_mut().push(connected_clients);
+ kdl_document.nodes_mut().push(available_layouts);
kdl_document.fmt();
kdl_document.to_string()
}
@@ -2506,6 +2547,11 @@ fn serialize_and_deserialize_session_info_with_data() {
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: false,
+ available_layouts: vec![
+ LayoutInfo::File("layout1".to_owned()),
+ LayoutInfo::BuiltIn("layout2".to_owned()),
+ LayoutInfo::File("layout3".to_owned()),
+ ],
};
let serialized = session_info.to_string();
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap
index 4a231c37..32925ec1 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/kdl/mod.rs
-assertion_line: 2284
+assertion_line: 2459
expression: serialized
---
name ""
@@ -9,4 +9,6 @@ tabs {
panes {
}
connected_clients 0
+available_layouts {
+}
diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap
index 99e3e03f..8244f04a 100644
--- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap
+++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap
@@ -1,6 +1,6 @@
---
source: zellij-utils/src/kdl/mod.rs
-assertion_line: 2377
+assertion_line: 2552
expression: serialized
---
name "my session name"
@@ -78,4 +78,9 @@ panes {
}
}
connected_clients 2
+available_layouts {
+ layout1 source="file"
+ layout2 source="built-in"
+ layout3 source="file"
+}
diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto
index 0d964a35..e26a9c23 100644
--- a/zellij-utils/src/plugin_api/event.proto
+++ b/zellij-utils/src/plugin_api/event.proto
@@ -152,6 +152,12 @@ message SessionManifest {
repeated PaneManifest panes = 3;
uint32 connected_clients = 4;
bool is_current_session = 5;
+ repeated LayoutInfo available_layouts = 6;
+}
+
+message LayoutInfo {
+ string name = 1;
+ string source = 2;
}
message ResurrectableSession {
diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs
index d9e5341d..21ff1eff 100644
--- a/zellij-utils/src/plugin_api/event.rs
+++ b/zellij-utils/src/plugin_api/event.rs
@@ -4,9 +4,9 @@ pub use super::generated_api::api::{
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
- KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
- PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
- ResurrectableSession as ProtobufResurrectableSession,
+ KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
+ ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
+ PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
@@ -14,8 +14,8 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle,
};
use crate::data::{
- CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
- PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
+ CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
+ PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
};
use crate::errors::prelude::*;
@@ -453,6 +453,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest {
.collect(),
connected_clients: session_info.connected_clients as u32,
is_current_session: session_info.is_current_session,
+ available_layouts: session_info
+ .available_layouts
+ .into_iter()
+ .filter_map(|l| ProtobufLayoutInfo::try_from(l).ok())
+ .collect(),
})
}
}
@@ -485,10 +490,42 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo {
panes,
connected_clients: protobuf_session_manifest.connected_clients as usize,
is_current_session: protobuf_session_manifest.is_current_session,
+ available_layouts: protobuf_session_manifest
+ .available_layouts
+ .into_iter()
+ .filter_map(|l| LayoutInfo::try_from(l).ok())
+ .collect(),
})
}
}
+impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
+ type Error = &'static str;
+ fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> {
+ match layout_info {
+ LayoutInfo::File(name) => Ok(ProtobufLayoutInfo {
+ source: "file".to_owned(),
+ name,
+ }),
+ LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo {
+ source: "built-in".to_owned(),
+ name,
+ }),
+ }
+ }
+}
+
+impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
+ type Error = &'static str;
+ fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> {
+ match protobuf_layout_info.source.as_str() {
+ "file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
+ "built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
+ _ => Err("Unknown source for layout"),
+ }
+ }
+}
+
impl TryFrom<CopyDestination> for ProtobufCopyDestination {
type Error = &'static str;
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> {
@@ -1383,6 +1420,11 @@ fn serialize_session_update_event_with_non_default_values() {
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: true,
+ available_layouts: vec![
+ LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::BuiltIn("layout2".to_owned()),
+ LayoutInfo::File("layout3".to_owned()),
+ ],
};
let session_info_2 = SessionInfo {
name: "session 2".to_owned(),
@@ -1392,6 +1434,11 @@ fn serialize_session_update_event_with_non_default_values() {
},
connected_clients: 0,
is_current_session: false,
+ available_layouts: vec![
+ LayoutInfo::File("layout 1".to_owned()),
+ LayoutInfo::BuiltIn("layout2".to_owned()),
+ LayoutInfo::File("layout3".to_owned()),
+ ],
};
let session_infos = vec![session_info_1, session_info_2];
let resurrectable_sessions = vec![];
diff --git a/zellij-utils/src/plugin_api/plugin_command.proto b/zellij-utils/src/plugin_api/plugin_command.proto
index 6ffb0345..b02dd73f 100644
--- a/zellij-utils/src/plugin_api/plugin_command.proto
+++ b/zellij-utils/src/plugin_api/plugin_command.proto
@@ -91,6 +91,8 @@ enum CommandName {
BlockCliPipeInput = 77;
CliPipeOutput = 78;
MessageToPlugin = 79;
+ DisconnectOtherClients = 80;
+ KillSessions = 81;
}
message PluginCommand {
@@ -145,9 +147,14 @@ message PluginCommand {
string block_cli_pipe_input_payload = 48;
CliPipeOutputPayload cli_pipe_output_payload = 49;
MessageToPluginPayload message_to_plugin_payload = 50;
+ KillSessionsPayload kill_sessions_payload = 60;
}
}
+message KillSessionsPayload {
+ repeated string session_names = 1;
+}
+
message CliPipeOutputPayload {
string pipe_name = 1;
string output = 2;
@@ -185,6 +192,7 @@ message SwitchSessionPayload {
optional uint32 tab_position = 2;
optional uint32 pane_id = 3;
optional bool pane_id_is_plugin = 4;
+ optional event.LayoutInfo layout = 5;
}
message RequestPluginPermissionPayload {
diff --git a/zellij-utils/src/plugin_api/plugin_command.rs b/zellij-utils/src/plugin_api/plugin_command.rs
index fa570a26..a5272d42 100644
--- a/zellij-utils/src/plugin_api/plugin_command.rs
+++ b/zellij-utils/src/plugin_api/plugin_command.rs
@@ -4,10 +4,10 @@ pub use super::generated_api::api::{
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable,
- ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload,
- MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload,
- OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType,
- PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
+ ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload,
+ MessageToPluginPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
+ OpenCommandPanePayload, OpenFilePayload, PaneId as ProtobufPaneId,
+ PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
WebRequestPayload,
@@ -574,6 +574,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
name: payload.name,
tab_position: payload.tab_position.map(|p| p as usize),
pane_id,
+ layout: payload.layout.and_then(|l| l.try_into().ok()),
}))
},
_ => Err("Mismatched payload for SwitchSession"),
@@ -727,6 +728,16 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}),
}))
},
+ _ => Err("Mismatched payload for MessageToPlugin"),
+ },
+ Some(CommandName::DisconnectOtherClients) => match protobuf_plugin_command.payload {
+ None => Ok(PluginCommand::DisconnectOtherClients),
+ _ => Err("Mismatched payload for DisconnectOtherClients"),
+ },
+ Some(CommandName::KillSessions) => match protobuf_plugin_command.payload {
+ Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
+ Ok(PluginCommand::KillSessions(session_names))
+ },
_ => Err("Mismatched payload for PipeOutput"),
},
None => Err("Unrecognized plugin command"),
@@ -1082,6 +1093,7 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
tab_position: switch_to_session.tab_position.map(|t| t as u32),
pane_id: switch_to_session.pane_id.map(|p| p.0),
pane_id_is_plugin: switch_to_session.pane_id.map(|p| p.1),
+ layout: switch_to_session.layout.and_then(|l| l.try_into().ok()),
})),
}),
PluginCommand::OpenTerminalInPlace(cwd) => Ok(ProtobufPluginCommand {
@@ -1205,6 +1217,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
})),
})
},
+ PluginCommand::DisconnectOtherClients => Ok(ProtobufPluginCommand {
+ name: CommandName::DisconnectOtherClients as i32,
+ payload: None,
+ }),
+ PluginCommand::KillSessions(session_names) => Ok(ProtobufPluginCommand {
+ name: CommandName::KillSessions as i32,
+ payload: Some(Payload::KillSessionsPayload(KillSessionsPayload {
+ session_names,
+ })),
+ }),
}
}
}
diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs
index 2c40f29d..bf34a894 100644
--- a/zellij-utils/src/setup.rs
+++ b/zellij-utils/src/setup.rs
@@ -167,6 +167,12 @@ pub const COMPACT_BAR_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
"assets/layouts/compact.swap.kdl"
));
+pub const WELCOME_LAYOUT: &[u8] = include_bytes!(concat!(
+ env!("CARGO_MANIFEST_DIR"),
+ "/",
+ "assets/layouts/welcome.kdl"
+));
+
pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
@@ -345,7 +351,9 @@ impl Setup {
/// 2. layout options
/// (`layout.kdl` / `zellij --layout`)
/// 3. config options (`config.kdl`)
- pub fn from_cli_args(cli_args: &CliArgs) -> Result<(Config, Layout, Options), ConfigError> {
+ pub fn from_cli_args(
+ cli_args: &CliArgs,
+ ) -> Result<(Config, Layout, Options, Config, Options), ConfigError> {
// note that this can potentially exit the process
Setup::handle_setup_commands(cli_args);
let config = Config::try_from(cli_args)?;
@@ -355,21 +363,34 @@ impl Setup {
} else {
None
};
+ let mut config_without_layout = config.clone();
let (layout, mut config) =
Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?;
- let config_options = match cli_config_options {
- Some(cli_config_options) => config.options.merge(cli_config_options),
- None => config.options.clone(),
- };
- config.themes = config.themes.merge(get_default_themes());
+ let config_options =
+ apply_themes_to_config(&mut config, cli_config_options.clone(), cli_args)?;
+ let config_options_without_layout =
+ apply_themes_to_config(&mut config_without_layout, cli_config_options, cli_args)?;
+ fn apply_themes_to_config(
+ config: &mut Config,
+ cli_config_options: Option<Options>,
+ cli_args: &CliArgs,
+ ) -> Result<Options, ConfigError> {
+ let config_options = match cli_config_options {
+ Some(cli_config_options) => config.options.merge(cli_config_options),
+ None => config.options.clone(),
+ };
+
+ config.themes = config.themes.merge(get_default_themes());
- let user_theme_dir = config_options.theme_dir.clone().or_else(|| {
- get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir))
- .filter(|dir| dir.exists())
- });
- if let Some(user_theme_dir) = user_theme_dir {
- config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?);
+ let user_theme_dir = config_options.theme_dir.clone().or_else(|| {
+ get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir))
+ .filter(|dir| dir.exists())
+ });
+ if let Some(user_theme_dir) = user_theme_dir {
+ config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?);
+ }
+ Ok(config_options)
}
if let Some(Command::Setup(ref setup)) = &cli_args.command {
@@ -383,7 +404,13 @@ impl Setup {
|_| {},
);
};
- Ok((config, layout, config_options))
+ Ok((
+ config,
+ layout,
+ config_options,
+ config_without_layout,
+ config_options_without_layout,
+ ))
}
/// General setup helpers
@@ -667,7 +694,7 @@ mod setup_test {
#[test]
fn default_config_with_no_cli_arguments() {
let cli_args = CliArgs::default();
- let (config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
assert_snapshot!(format!("{:#?}", layout));
assert_snapshot!(format!("{:#?}", options));
@@ -682,7 +709,7 @@ mod setup_test {
},
..Default::default()
}));
- let (_config, _layout, options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (_config, _layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
}
#[test]
@@ -692,7 +719,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-options.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout));
}
@@ -710,7 +737,7 @@ mod setup_test {
},
..Default::default()
}));
- let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout));
}
@@ -725,7 +752,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-env-vars.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@@ -739,7 +766,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-ui-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@@ -753,7 +780,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-plugins-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@@ -767,7 +794,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-themes-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@@ -781,7 +808,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-keybindings-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
- let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
+ let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
}