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
This commit is contained in:
Aram Drevekenin 2024-02-06 14:26:14 +01:00 committed by GitHub
parent 286f7ccc28
commit 6b20a958f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1941 additions and 400 deletions

View file

@ -280,6 +280,16 @@ impl ZellijPlugin for State {
context, 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) => { Event::CustomMessage(message, payload) => {

View file

@ -1,3 +1,4 @@
mod new_session_info;
mod resurrectable_sessions; mod resurrectable_sessions;
mod session_list; mod session_list;
mod ui; mod ui;
@ -5,34 +6,58 @@ use zellij_tile::prelude::*;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use new_session_info::NewSessionInfo;
use ui::{ use ui::{
components::{ components::{
render_controls_line, render_error, render_new_session_line, render_prompt, render_controls_line, render_error, render_new_session_block, render_prompt,
render_renaming_session_screen, render_resurrection_toggle, Colors, render_renaming_session_screen, render_screen_toggle, Colors,
}, },
welcome_screen::{render_banner, render_welcome_boundaries},
SessionUiInfo, SessionUiInfo,
}; };
use resurrectable_sessions::ResurrectableSessions; use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList; use session_list::SessionList;
#[derive(Clone, Debug, Copy)]
enum ActiveScreen {
NewSession,
AttachToSession,
ResurrectSession,
}
impl Default for ActiveScreen {
fn default() -> Self {
ActiveScreen::AttachToSession
}
}
#[derive(Default)] #[derive(Default)]
struct State { struct State {
session_name: Option<String>, session_name: Option<String>,
sessions: SessionList, sessions: SessionList,
resurrectable_sessions: ResurrectableSessions, resurrectable_sessions: ResurrectableSessions,
search_term: String, search_term: String,
new_session_name: Option<String>, new_session_info: NewSessionInfo,
renaming_session_name: Option<String>, renaming_session_name: Option<String>,
error: Option<String>, error: Option<String>,
browsing_resurrection_sessions: bool, active_screen: ActiveScreen,
colors: Colors, colors: Colors,
is_welcome_screen: bool,
show_kill_all_sessions_warning: bool,
} }
register_plugin!(State); register_plugin!(State);
impl ZellijPlugin for 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(&[ subscribe(&[
EventType::ModeUpdate, EventType::ModeUpdate,
EventType::SessionUpdate, EventType::SessionUpdate,
@ -55,6 +80,12 @@ impl ZellijPlugin for State {
should_render = true; should_render = true;
}, },
Event::SessionUpdate(session_infos, resurrectable_session_list) => { 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 self.resurrectable_sessions
.update(resurrectable_session_list); .update(resurrectable_session_list);
self.update_session_infos(session_infos); self.update_session_infos(session_infos);
@ -66,36 +97,53 @@ impl ZellijPlugin for State {
} }
fn render(&mut self, rows: usize, cols: usize) { fn render(&mut self, rows: usize, cols: usize) {
if self.browsing_resurrection_sessions { let (x, y, width, height) = self.main_menu_size(rows, cols);
self.resurrectable_sessions.render(rows, cols);
return; if self.is_welcome_screen {
} else if let Some(new_session_name) = self.renaming_session_name.as_ref() { render_banner(x, 0, rows.saturating_sub(height), width);
render_renaming_session_screen(&new_session_name, rows, cols);
return;
} }
render_resurrection_toggle(cols, false); render_screen_toggle(self.active_screen, x, y, width.saturating_sub(2));
render_prompt(
self.new_session_name.is_some(), match self.active_screen {
&self.search_term, ActiveScreen::NewSession => {
self.colors, render_new_session_block(
); &self.new_session_info,
let room_for_list = rows.saturating_sub(5); // search line and controls self.colors,
self.sessions.update_rows(room_for_list); height,
let list = self width,
.sessions x,
.render(room_for_list, cols.saturating_sub(7), self.colors); // 7 for various ui y + 2,
for line in list { );
println!("{}", line.render()); },
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() { if let Some(error) = self.error.as_ref() {
render_error(&error, rows, cols); render_error(&error, height, width, x, y);
} else { } 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; self.error = None;
return true; 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; let mut should_render = false;
if let Key::Right = key { if let Key::Down = key {
if self.new_session_name.is_none() { self.new_session_info.handle_key(key);
self.sessions.result_expand();
}
should_render = true;
} else if let Key::Left = key {
if self.new_session_name.is_none() {
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();
}
should_render = true; should_render = true;
} else if let Key::Up = key { } else if let Key::Up = key {
if self.browsing_resurrection_sessions { self.new_session_info.handle_key(key);
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();
}
should_render = true; should_render = true;
} else if let Key::Char(character) = key { } else if let Key::Char(character) = key {
if character == '\n' { if character == '\n' {
self.handle_selection(); self.handle_selection();
} else if let Some(new_session_name) = self.new_session_name.as_mut() { } else {
self.new_session_info.handle_key(key);
}
should_render = true;
} 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 {
self.sessions.move_selection_down();
should_render = true;
} else if let Key::Up = key {
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.renaming_session_name.as_mut() {
new_session_name.push(character); 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 { } else {
self.search_term.push(character); self.search_term.push(character);
self.sessions self.sessions
@ -150,20 +235,12 @@ impl State {
} }
should_render = true; should_render = true;
} else if let Key::Backspace = key { } 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() { if new_session_name.is_empty() {
self.new_session_name = None; self.renaming_session_name = None;
} else { } else {
new_session_name.pop(); 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();
}
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_backspace();
} else { } else {
self.search_term.pop(); self.search_term.pop();
self.sessions self.sessions
@ -171,136 +248,165 @@ impl State {
} }
should_render = true; should_render = true;
} else if let Key::Ctrl('w') = key { } else if let Key::Ctrl('w') = key {
if self.sessions.is_searching || self.browsing_resurrection_sessions { self.active_screen = ActiveScreen::NewSession;
// no-op
} else if self.new_session_name.is_some() {
self.new_session_name = None;
} else {
self.new_session_name = Some(String::new());
}
should_render = true; should_render = true;
} else if let Key::Ctrl('r') = key { } else if let Key::Ctrl('r') = key {
if self.sessions.is_searching || self.browsing_resurrection_sessions { self.renaming_session_name = Some(String::new());
// no-op should_render = true;
} else if self.renaming_session_name.is_some() { } else if let Key::Delete = key {
self.renaming_session_name = None; 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 { } else {
self.renaming_session_name = Some(String::new()); self.show_error("Must select session before killing it.");
} }
should_render = true; should_render = true;
} 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.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 { } else if let Key::Ctrl('c') = key {
if let Some(new_session_name) = self.new_session_name.as_mut() { if !self.search_term.is_empty() {
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() {
self.search_term.clear(); self.search_term.clear();
self.sessions self.sessions
.update_search_term(&self.search_term, &self.colors); .update_search_term(&self.search_term, &self.colors);
self.reset_selected_index(); self.reset_selected_index();
} else { } else if !self.is_welcome_screen {
self.reset_selected_index(); self.reset_selected_index();
hide_self(); hide_self();
} }
should_render = true; should_render = true;
} else if let Key::BackTab = key { } else if let Key::BackTab = key {
self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions; self.toggle_active_screen();
should_render = true; 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 { } else if let Key::Esc = key {
if self.renaming_session_name.is_some() { if self.renaming_session_name.is_some() {
self.renaming_session_name = None; self.renaming_session_name = None;
should_render = true; should_render = true;
} else if self.new_session_name.is_some() { } else if !self.is_welcome_screen {
self.new_session_name = None; hide_self();
should_render = true; }
}
should_render
}
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 { } else {
self.resurrectable_sessions.handle_character(character);
}
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(); hide_self();
} }
} }
should_render should_render
} }
fn handle_selection(&mut self) { fn handle_selection(&mut self) {
if self.browsing_resurrection_sessions { match self.active_screen {
if let Some(session_name_to_resurrect) = ActiveScreen::NewSession => {
self.resurrectable_sessions.get_selected_session_name() self.new_session_info.handle_selection(&self.session_name);
{ },
switch_session(Some(&session_name_to_resurrect)); ActiveScreen::AttachToSession => {
} if let Some(renaming_session_name) = &self.renaming_session_name.take() {
} else if let Some(new_session_name) = &self.new_session_name { if renaming_session_name.is_empty() {
if new_session_name.is_empty() { self.show_error("New name must not be empty.");
switch_session(None); return; // so that we don't hide self
} else if self.session_name.as_ref() == Some(new_session_name) { } else if self.session_name.as_ref() == Some(renaming_session_name) {
// noop - we're already here! // noop - we're already called that!
self.new_session_name = None; return; // so that we don't hide self
} else { } else if self.sessions.has_session(&renaming_session_name) {
switch_session(Some(new_session_name)); self.show_error("A session by this name already exists.");
} return; // so that we don't hide self
} else if let Some(renaming_session_name) = &self.renaming_session_name.take() { } else if self
if renaming_session_name.is_empty() { .resurrectable_sessions
// TODO: implement these, then implement the error UI, then implement the renaming .has_session(&renaming_session_name)
// session screen, then test it {
self.show_error("New name must not be empty."); self.show_error("A resurrectable session by this name already exists.");
return; // s that we don't hide self 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
}
} 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);
} else { } 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 { if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
switch_session_with_focus(&selected_session_name, selected_tab, selected_pane); 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; fn toggle_active_screen(&mut self) {
self.search_term.clear(); self.active_screen = match self.active_screen {
self.sessions ActiveScreen::NewSession => ActiveScreen::AttachToSession,
.update_search_term(&self.search_term, &self.colors); ActiveScreen::AttachToSession => ActiveScreen::ResurrectSession,
hide_self(); ActiveScreen::ResurrectSession => ActiveScreen::NewSession,
};
} }
fn show_error(&mut self, error_text: &str) { fn show_error(&mut self, error_text: &str) {
self.error = Some(error_text.to_owned()); self.error = Some(error_text.to_owned());
@ -329,4 +435,53 @@ impl State {
} }
self.sessions.set_sessions(session_infos); 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,
);
}
} }

View file

@ -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>,
}

View file

@ -2,8 +2,6 @@ use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use humantime::format_duration; use humantime::format_duration;
use crate::ui::components::render_resurrection_toggle;
use std::time::Duration; use std::time::Duration;
use zellij_tile::shim::*; use zellij_tile::shim::*;
@ -24,23 +22,22 @@ impl ResurrectableSessions {
list.sort_by(|a, b| a.1.cmp(&b.1)); list.sort_by(|a, b| a.1.cmp(&b.1));
self.all_resurrectable_sessions = list; 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 { 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; return;
} }
render_resurrection_toggle(columns, true); let search_indication =
let search_indication = Text::new(format!("> {}_", self.search_term)).color_range(1, ..); Text::new(format!("Search: {}_", self.search_term)).color_range(2, ..7);
let table_rows = rows.saturating_sub(3); let table_rows = rows.saturating_sub(5); // search row, toggle row and some padding
let table_columns = columns; let table_columns = columns;
let table = if self.is_searching { let table = if self.is_searching {
self.render_search_results(table_rows, columns) self.render_search_results(table_rows, columns)
} else { } else {
self.render_all_entries(table_rows, columns) self.render_all_entries(table_rows, columns)
}; };
print_text_with_coordinates(search_indication, 0, 0, None, None); print_text_with_coordinates(search_indication, x.saturating_sub(1), y + 2, None, None);
print_table_with_coordinates(table, 0, 1, Some(table_columns), Some(table_rows)); print_table_with_coordinates(table, x, y + 3, Some(table_columns), Some(table_rows));
self.render_controls_line(rows);
} }
fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table { fn render_search_results(&self, table_rows: usize, _table_columns: usize) -> Table {
let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row let mut table = Table::new().add_row(vec![" ", " ", " "]); // skip the title row
@ -103,7 +100,7 @@ impl ResurrectableSessions {
} }
table 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 { if rows == 0 || columns == 0 {
return; return;
} }
@ -112,11 +109,12 @@ impl ResurrectableSessions {
let warning_description_text = let warning_description_text =
format!("This will delete {} resurrectable sessions", session_count,); format!("This will delete {} resurrectable sessions", session_count,);
let confirmation_text = "Are you sure? (y/n)"; let confirmation_text = "Are you sure? (y/n)";
let warning_y_location = (rows / 2).saturating_sub(1); let warning_y_location = y + (rows / 2).saturating_sub(1);
let confirmation_y_location = (rows / 2) + 1; let confirmation_y_location = y + (rows / 2) + 1;
let warning_x_location = let warning_x_location =
columns.saturating_sub(warning_description_text.chars().count()) / 2; x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
let confirmation_x_location = columns.saturating_sub(confirmation_text.chars().count()) / 2; let confirmation_x_location =
x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
print_text_with_coordinates( print_text_with_coordinates(
Text::new(warning_description_text).color_range(0, 17..18 + session_count_len), Text::new(warning_description_text).color_range(0, 17..18 + session_count_len),
warning_x_location, warning_x_location,
@ -200,15 +198,6 @@ impl ResurrectableSessions {
Text::new(" ") 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) { pub fn move_selection_down(&mut self) {
if self.is_searching { if self.is_searching {
if let Some(selected_index) = self.selected_search_index.as_mut() { if let Some(selected_index) = self.selected_search_index.as_mut() {

View file

@ -323,6 +323,18 @@ impl SessionList {
.find(|s| s.name == old_name) .find(|s| s.name == old_name)
.map(|s| s.name = new_name.to_owned()); .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)] #[derive(Debug, Clone, Default)]

View file

@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo}; use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
use crate::{ActiveScreen, NewSessionInfo};
#[derive(Debug)] #[derive(Debug)]
pub struct ListItem { pub struct ListItem {
@ -292,18 +293,45 @@ impl LineToRender {
pub fn append(&mut self, to_append: &str) { pub fn append(&mut self, to_append: &str) {
self.line.push_str(to_append) 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; self.is_selected = true;
let arrows = if add_arrows {
self.colors.magenta(" <↓↑> ")
} else {
" ".to_owned()
};
match self.colors.palette.bg { match self.colors.palette.bg {
PaletteColor::EightBit(byte) => { PaletteColor::EightBit(byte) => {
self.line = format!( 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 self.line
); );
}, },
PaletteColor::Rgb((r, g, b)) => { PaletteColor::Rgb((r, g, b)) => {
self.line = format!( 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
);
},
}
}
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\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 r, g, b, r, g, b, self.line
); );
}, },
@ -323,7 +351,7 @@ impl LineToRender {
if self.is_selected { if self.is_selected {
self.line.clone() self.line.clone()
} else { } else {
format!("\u{1b}[49m{}", line) format!("\u{1b}[49m {}", line)
} }
} }
pub fn add_truncated_results(&mut self, result_count: usize) { 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) (start_index, anchor_index, end_index, line_count_to_remove)
} }
pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) { pub fn render_prompt(search_term: &str, colors: Colors, x: usize, y: usize) {
if !typing_session_name { let prompt = colors.green(&format!("Search:"));
let prompt = colors.bold(&format!("> {}_", search_term)); let search_term = colors.bold(&format!("{}_", search_term));
println!("\u{1b}[H{}\n", prompt); println!("\u{1b}[{};{}H{} {}\n", y + 1, x, prompt, search_term);
} else {
println!("\n");
}
} }
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 key_indication_text = "<TAB>";
let running_sessions_text = "Running"; let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 {
let exited_sessions_text = "Exited"; ("New Session", "Attach to Session", "Resurrect Session")
} else {
("New", "Attach", "Resurrect")
};
let key_indication_len = key_indication_text.chars().count() + 1; let key_indication_len = key_indication_text.chars().count() + 1;
let first_ribbon_length = running_sessions_text.chars().count() + 4; let first_ribbon_length = new_session_text.chars().count() + 4;
let second_ribbon_length = exited_sessions_text.chars().count() + 4; let second_ribbon_length = running_sessions_text.chars().count() + 4;
let key_indication_x = let third_ribbon_length = exited_sessions_text.chars().count() + 4;
cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length); 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 first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length; 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( print_text_with_coordinates(
Text::new(key_indication_text).color_range(3, ..), Text::new(key_indication_text).color_range(3, ..),
key_indication_x, key_indication_x,
0, y,
None, None,
None, None,
); );
if resurrection_screen_is_active { print_ribbon_with_coordinates(new_session_text, first_ribbon_x, y, None, None);
print_ribbon_with_coordinates( print_ribbon_with_coordinates(running_sessions_text, second_ribbon_x, y, None, None);
Text::new(running_sessions_text), print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
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,
);
}
} }
pub fn render_new_session_line(session_name: &Option<String>, is_searching: bool, colors: Colors) { pub fn render_new_session_block(
if is_searching { new_session_info: &NewSessionInfo,
return; colors: Colors,
} max_rows_of_new_session_block: usize,
let new_session_shortcut_text = "<Ctrl w>"; max_cols_of_new_session_block: usize,
let new_session_shortcut = colors.magenta(new_session_shortcut_text); x: usize,
let new_session = colors.bold("New session"); y: usize,
) {
let enter = colors.magenta("<ENTER>"); let enter = colors.magenta("<ENTER>");
match session_name { if new_session_info.entering_new_session_name() {
Some(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!( println!(
"\u{1b}[m > {}_ ({}, {} when done)", "\u{1b}[m{}{} {}_ ({} {})",
colors.orange(session_name), format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.bold("Type optional name"), colors.green(prompt),
enter colors.orange(&new_session_name),
enter,
long_instruction,
); );
}, } else {
None => { println!(
println!("\u{1b}[m > {new_session_shortcut} - {new_session}"); "\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( print_text_with_coordinates(
Text::new(format!("Error: {}", error_text)).color_range(3, ..), Text::new(format!("Error: {}", error_text)).color_range(3, ..),
0, x,
rows, y + rows,
Some(columns), Some(columns),
None, 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 { if rows == 0 || columns == 0 {
return; return;
} }
let prompt_text = "NEW NAME FOR CURRENT SESSION"; let text = Text::new(format!(
let new_session_name = format!("{}_", new_session_name); "New name for current session: {}_ (<ENTER> when done)",
let prompt_y_location = (rows / 2).saturating_sub(1); new_session_name
let session_name_y_location = (rows / 2) + 1; ))
let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2; .color_range(2, ..29)
let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2; .color_range(
print_text_with_coordinates( 3,
Text::new(prompt_text).color_range(0, ..), 33 + new_session_name.width()..40 + new_session_name.width(),
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,
); );
print_text_with_coordinates(text, x, y, None, None);
} }
pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) { pub fn render_controls_line(
let (arrows, navigate) = if is_searching { active_screen: ActiveScreen,
(colors.magenta("<↓↑>"), colors.bold("Navigate")) max_cols: usize,
} else { colors: Colors,
(colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand")) x: usize,
}; y: usize,
let rename = colors.magenta("<Ctrl r>"); ) {
let rename_text = colors.bold("Rename session"); match active_screen {
let enter = colors.magenta("<ENTER>"); ActiveScreen::NewSession => {
let select = colors.bold("Switch to selected"); if max_cols >= 50 {
let esc = colors.magenta("<ESC>"); print!(
let to_hide = colors.bold("Hide"); "\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 { if max_cols > 90 {
print!( print!(
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" "\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 >= 73 { } else if max_cols >= 28 {
let navigate = colors.bold("Navigate"); print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}");
let select = colors.bold("Switch"); }
let rename_text = colors.bold("Rename"); },
print!( ActiveScreen::ResurrectSession => {
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}" let arrows = colors.magenta("<↓↑>");
); let navigate = colors.bold("Navigate");
} else if max_cols >= 28 { let enter = colors.magenta("<ENTER>");
print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}"); 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}");
}
},
} }
} }

View file

@ -1,4 +1,5 @@
pub mod components; pub mod components;
pub mod welcome_screen;
use zellij_tile::prelude::*; use zellij_tile::prelude::*;
use crate::session_list::{SelectedIndex, SessionList}; use crate::session_list::{SelectedIndex, SessionList};
@ -29,7 +30,7 @@ macro_rules! render_assets {
if $selected_index.is_some() && !$has_deeper_selected_assets { if $selected_index.is_some() && !$has_deeper_selected_assets {
let mut selected_asset: LineToRender = let mut selected_asset: LineToRender =
selected_asset.as_line_to_render(current_index, $max_cols, $colors); 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); selected_asset.add_truncated_results(truncated_result_count_above);
if anchor_asset_index + 1 >= end_index { if anchor_asset_index + 1 >= end_index {
// no more results below, let's add the more indication if we need to // 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 { if lines_to_render.len() + result.lines_to_render() <= max_rows {
let mut result_lines = result.render(max_cols); let mut result_lines = result.render(max_cols);
if Some(i) == self.selected_search_index { if Some(i) == self.selected_search_index {
let mut render_arrows = true;
for line_to_render in result_lines.iter_mut() { 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); lines_to_render.append(&mut result_lines);

View file

@ -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 ~

 "#;

View file

@ -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_server::{os_input_output::get_server_os_input, start_server as start_server_impl};
use zellij_utils::{ use zellij_utils::{
cli::{CliArgs, Command, SessionCommand, Sessions}, cli::{CliArgs, Command, SessionCommand, Sessions},
data::ConnectToSession, data::{ConnectToSession, LayoutInfo},
envs, envs,
input::{ input::{
actions::Action, actions::Action,
config::{Config, ConfigError}, config::{Config, ConfigError},
layout::Layout,
options::Options, options::Options,
}, },
miette::{Report, Result}, miette::{Report, Result},
nix, nix,
setup::Setup, setup::{find_default_config_dir, get_layout_dir, Setup},
}; };
pub(crate) use crate::sessions::list_sessions; pub(crate) use crate::sessions::list_sessions;
@ -383,24 +384,25 @@ fn attach_with_session_name(
pub(crate) fn start_client(opts: CliArgs) { pub(crate) fn start_client(opts: CliArgs) {
// look for old YAML config/layout/theme files and convert them to KDL // look for old YAML config/layout/theme files and convert them to KDL
convert_old_yaml_files(&opts); convert_old_yaml_files(&opts);
let (config, layout, config_options) = match Setup::from_cli_args(&opts) { let (config, layout, config_options, config_without_layout, config_options_without_layout) =
Ok(results) => results, match Setup::from_cli_args(&opts) {
Err(e) => { Ok(results) => results,
if let ConfigError::KdlError(error) = e { Err(e) => {
let report: Report = error.into(); if let ConfigError::KdlError(error) = e {
eprintln!("{:?}", report); let report: Report = error.into();
} else { eprintln!("{:?}", report);
eprintln!("{}", e); } else {
} eprintln!("{}", e);
process::exit(1); }
}, process::exit(1);
}; },
};
let mut reconnect_to_session: Option<ConnectToSession> = None; let mut reconnect_to_session: Option<ConnectToSession> = None;
let os_input = get_os_input(get_client_os_input); let os_input = get_os_input(get_client_os_input);
loop { loop {
let os_input = os_input.clone(); let os_input = os_input.clone();
let config = config.clone(); let mut config = config.clone();
let layout = layout.clone(); let mut layout = layout.clone();
let mut config_options = config_options.clone(); let mut config_options = config_options.clone();
let mut opts = opts.clone(); let mut opts = opts.clone();
let mut is_a_reconnect = false; let mut is_a_reconnect = false;
@ -423,6 +425,43 @@ pub(crate) fn start_client(opts: CliArgs) {
opts.session = None; opts.session = None;
config_options.attach_to_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; is_a_reconnect = true;
} }

View file

@ -52,8 +52,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
session_layout_cache_file_name(&folder_name.display().to_string()); session_layout_cache_file_name(&folder_name.display().to_string());
let raw_layout = match std::fs::read_to_string(&layout_file_name) { let raw_layout = match std::fs::read_to_string(&layout_file_name) {
Ok(raw_layout) => raw_layout, Ok(raw_layout) => raw_layout,
Err(e) => { Err(_e) => {
log::error!("Failed to read resurrection layout file: {:?}", e);
return None; return None;
}, },
}; };
@ -61,13 +60,7 @@ pub(crate) fn get_resurrectable_sessions() -> Vec<(String, Duration, Layout)> {
.and_then(|metadata| metadata.created()) .and_then(|metadata| metadata.created())
{ {
Ok(created) => Some(created), Ok(created) => Some(created),
Err(e) => { Err(_e) => None,
log::error!(
"Failed to read created stamp of resurrection file: {:?}",
e
);
None
},
}; };
let layout = match Layout::from_kdl( let layout = match Layout::from_kdl(
&raw_layout, &raw_layout,

View file

@ -44,7 +44,7 @@ use zellij_utils::{
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{ConnectToSession, Event, PluginCapabilities}, data::{ConnectToSession, Event, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
home::get_default_data_dir, home::{default_layout_dir, get_default_data_dir},
input::{ input::{
command::{RunCommand, TerminalAction}, command::{RunCommand, TerminalAction},
get_mode_info, get_mode_info,
@ -93,6 +93,7 @@ pub enum ServerInstruction {
pipe_id: String, pipe_id: String,
client_id: ClientId, client_id: ClientId,
}, },
DisconnectAllClientsExcept(ClientId),
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
@ -117,6 +118,9 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::AssociatePipeWithClient { .. } => { ServerInstruction::AssociatePipeWithClient { .. } => {
ServerContext::AssociatePipeWithClient ServerContext::AssociatePipeWithClient
}, },
ServerInstruction::DisconnectAllClientsExcept(..) => {
ServerContext::DisconnectAllClientsExcept
},
} }
} }
} }
@ -133,6 +137,7 @@ pub(crate) struct SessionMetaData {
pub client_attributes: ClientAttributes, pub client_attributes: ClientAttributes,
pub default_shell: Option<TerminalAction>, pub default_shell: Option<TerminalAction>,
pub layout: Box<Layout>, pub layout: Box<Layout>,
pub config_options: Box<Options>,
screen_thread: Option<thread::JoinHandle<()>>, screen_thread: Option<thread::JoinHandle<()>>,
pty_thread: Option<thread::JoinHandle<()>>, pty_thread: Option<thread::JoinHandle<()>>,
plugin_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; 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) => { ServerInstruction::DetachSession(client_ids) => {
for client_id in client_ids { for client_id in client_ids {
let _ = os_input let _ = os_input
@ -749,7 +769,16 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
session_state 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() { if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
session_data session_data
.write() .write()
@ -906,6 +935,7 @@ fn init_session(
let client_attributes_clone = client_attributes.clone(); let client_attributes_clone = client_attributes.clone();
let debug = opts.debug; let debug = opts.debug;
let layout = layout.clone(); let layout = layout.clone();
let config_options = config_options.clone();
move || { move || {
screen_thread_main( screen_thread_main(
screen_bus, screen_bus,
@ -1006,6 +1036,7 @@ fn init_session(
default_shell, default_shell,
client_attributes, client_attributes,
layout, layout,
config_options: config_options.clone(),
screen_thread: Some(screen_thread), screen_thread: Some(screen_thread),
pty_thread: Some(pty_thread), pty_thread: Some(pty_thread),
plugin_thread: Some(plugin_thread), plugin_thread: Some(plugin_thread),

View file

@ -5970,3 +5970,243 @@ pub fn pipe_message_to_plugin_plugin_command() {
}); });
assert_snapshot!(format!("{:#?}", plugin_bytes_event)); 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));
}

View file

@ -0,0 +1,10 @@
---
source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 6131
expression: "format!(\"{:#?}\", switch_session_event)"
---
Some(
DisconnectAllClientsExcept(
1,
),
)

View file

@ -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,
),
)

View file

@ -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,
),
)

View file

@ -18,10 +18,14 @@ use std::{
use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports}; use wasmer::{imports, AsStoreMut, Function, FunctionEnv, FunctionEnvMut, Imports};
use wasmer_wasi::WasiEnv; use wasmer_wasi::WasiEnv;
use zellij_utils::data::{ use zellij_utils::data::{
CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType, CommandType, ConnectToSession, HttpVerb, LayoutInfo, MessageToPlugin, PermissionStatus,
PluginPermission, PermissionType, PluginPermission,
}; };
use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::permission::PermissionCache;
use zellij_utils::{
interprocess::local_socket::LocalSocketStream,
ipc::{ClientToServerMsg, IpcSenderWithContext},
};
use url::Url; use url::Url;
@ -225,6 +229,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
connect_to_session.name, connect_to_session.name,
connect_to_session.tab_position, connect_to_session.tab_position,
connect_to_session.pane_id, connect_to_session.pane_id,
connect_to_session.layout,
)?, )?,
PluginCommand::DeleteDeadSession(session_name) => { PluginCommand::DeleteDeadSession(session_name) => {
delete_dead_session(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)? cli_pipe_output(env, pipe_name, output)?
}, },
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?, 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) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -900,6 +907,7 @@ fn switch_session(
session_name: Option<String>, session_name: Option<String>,
tab_position: Option<usize>, tab_position: Option<usize>,
pane_id: Option<(u32, bool)>, pane_id: Option<(u32, bool)>,
layout: Option<LayoutInfo>,
) -> Result<()> { ) -> Result<()> {
// pane_id is (id, is_plugin) // pane_id is (id, is_plugin)
let err_context = || format!("Failed to switch session"); let err_context = || format!("Failed to switch session");
@ -909,6 +917,7 @@ fn switch_session(
name: session_name, name: session_name,
tab_position, tab_position,
pane_id, pane_id,
layout,
}; };
env.plugin_env env.plugin_env
.senders .senders
@ -1278,6 +1287,30 @@ fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) {
apply_action!(action, error_msg, env); 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. // Custom panic handler for plugins.
// //
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the // 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::DeleteDeadSession(..)
| PluginCommand::DeleteAllDeadSessions | PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameSession(..) | PluginCommand::RenameSession(..)
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState, | PluginCommand::RenameTab(..)
| PluginCommand::DisconnectOtherClients
| PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..) PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..) | PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes, | PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,

View file

@ -577,6 +577,8 @@ pub(crate) struct Screen {
default_shell: Option<PathBuf>, default_shell: Option<PathBuf>,
styled_underlines: bool, styled_underlines: bool,
arrow_fonts: bool, arrow_fonts: bool,
layout_dir: Option<PathBuf>,
default_layout_name: Option<String>,
} }
impl Screen { impl Screen {
@ -592,12 +594,14 @@ impl Screen {
copy_options: CopyOptions, copy_options: CopyOptions,
debug: bool, debug: bool,
default_layout: Box<Layout>, default_layout: Box<Layout>,
default_layout_name: Option<String>,
default_shell: Option<PathBuf>, default_shell: Option<PathBuf>,
session_serialization: bool, session_serialization: bool,
serialize_pane_viewport: bool, serialize_pane_viewport: bool,
scrollback_lines_to_serialize: Option<usize>, scrollback_lines_to_serialize: Option<usize>,
styled_underlines: bool, styled_underlines: bool,
arrow_fonts: bool, arrow_fonts: bool,
layout_dir: Option<PathBuf>,
) -> Self { ) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default(); let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone()); let session_info = SessionInfo::new(session_name.clone());
@ -629,6 +633,7 @@ impl Screen {
session_name, session_name,
session_infos_on_machine, session_infos_on_machine,
default_layout, default_layout,
default_layout_name,
default_shell, default_shell,
session_serialization, session_serialization,
serialize_pane_viewport, serialize_pane_viewport,
@ -636,6 +641,7 @@ impl Screen {
styled_underlines, styled_underlines,
arrow_fonts, arrow_fonts,
resurrectable_sessions, resurrectable_sessions,
layout_dir,
} }
} }
@ -1412,12 +1418,21 @@ impl Screen {
// generate own session info // generate own session info
let pane_manifest = self.generate_and_report_pane_state()?; let pane_manifest = self.generate_and_report_pane_state()?;
let tab_infos = self.generate_and_report_tab_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 { let session_info = SessionInfo {
name: self.session_name.clone(), name: self.session_name.clone(),
tabs: tab_infos, tabs: tab_infos,
panes: pane_manifest, panes: pane_manifest,
connected_clients: self.active_tab_indices.keys().len(), connected_clients: self.active_tab_indices.keys().len(),
is_current_session: true, is_current_session: true,
available_layouts,
}; };
self.bus self.bus
.senders .senders
@ -2101,7 +2116,11 @@ pub(crate) fn screen_thread_main(
let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false); let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false);
let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize; let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize;
let session_is_mirrored = config_options.mirror_session.unwrap_or(false); 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_shell = config_options.default_shell;
let default_layout_name = config_options
.default_layout
.map(|l| format!("{}", l.display()));
let copy_options = CopyOptions::new( let copy_options = CopyOptions::new(
config_options.copy_command, config_options.copy_command,
config_options.copy_clipboard.unwrap_or_default(), config_options.copy_clipboard.unwrap_or_default(),
@ -2128,12 +2147,14 @@ pub(crate) fn screen_thread_main(
copy_options, copy_options,
debug, debug,
default_layout, default_layout,
default_layout_name,
default_shell, default_shell,
session_serialization, session_serialization,
serialize_pane_viewport, serialize_pane_viewport,
scrollback_lines_to_serialize, scrollback_lines_to_serialize,
styled_underlines, styled_underlines,
arrow_fonts, arrow_fonts,
layout_dir,
); );
let mut pending_tab_ids: HashSet<usize> = HashSet::new(); let mut pending_tab_ids: HashSet<usize> = HashSet::new();

View file

@ -847,7 +847,7 @@ impl Tab {
pub fn rename_session(&mut self, new_session_name: String) -> Result<()> { pub fn rename_session(&mut self, new_session_name: String) -> Result<()> {
{ {
let mode_infos = &mut self.mode_info.borrow_mut(); 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()); mode_info.session_name = Some(new_session_name.clone());
} }
self.default_mode_info.session_name = Some(new_session_name); self.default_mode_info.session_name = Some(new_session_name);

View file

@ -241,10 +241,12 @@ fn create_new_screen(size: Size) -> Screen {
let session_is_mirrored = true; let session_is_mirrored = true;
let copy_options = CopyOptions::default(); let copy_options = CopyOptions::default();
let default_layout = Box::new(Layout::default()); let default_layout = Box::new(Layout::default());
let default_layout_name = None;
let default_shell = None; let default_shell = None;
let session_serialization = true; let session_serialization = true;
let serialize_pane_viewport = false; let serialize_pane_viewport = false;
let scrollback_lines_to_serialize = None; let scrollback_lines_to_serialize = None;
let layout_dir = None;
let debug = false; let debug = false;
let styled_underlines = true; let styled_underlines = true;
@ -260,12 +262,14 @@ fn create_new_screen(size: Size) -> Screen {
copy_options, copy_options,
debug, debug,
default_layout, default_layout,
default_layout_name,
default_shell, default_shell,
session_serialization, session_serialization,
serialize_pane_viewport, serialize_pane_viewport,
scrollback_lines_to_serialize, scrollback_lines_to_serialize,
styled_underlines, styled_underlines,
arrow_fonts, arrow_fonts,
layout_dir,
); );
screen screen
} }
@ -425,6 +429,7 @@ impl MockScreen {
plugin_thread: None, plugin_thread: None,
pty_writer_thread: None, pty_writer_thread: None,
background_jobs_thread: None, background_jobs_thread: None,
config_options: Default::default(),
layout, layout,
} }
} }
@ -481,6 +486,7 @@ impl MockScreen {
plugin_thread: None, plugin_thread: None,
pty_writer_thread: None, pty_writer_thread: None,
background_jobs_thread: None, background_jobs_thread: None,
config_options: Default::default(),
layout, layout,
}; };
@ -2869,17 +2875,17 @@ pub fn screen_can_break_floating_pane_to_a_new_tab() {
1, 1,
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 // move back to make sure the other pane is in the previous tab
let _ = mock_screen let _ = mock_screen
.to_screen .to_screen
.send(ScreenInstruction::MoveFocusLeftOrPreviousTab(1)); .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 // move forward to make sure the broken pane is in the previous tab
let _ = mock_screen let _ = mock_screen
.to_screen .to_screen
.send(ScreenInstruction::MoveFocusRightOrNextTab(1)); .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]); mock_screen.teardown(vec![server_thread, screen_thread]);

View file

@ -653,6 +653,18 @@ pub fn switch_session(name: Option<&str>) {
unsafe { host_run_plugin_command() }; 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 /// Switch to a session with the given name, focusing either the provided pane_id or the provided
/// tab position (in that order) /// tab position (in that order)
pub fn switch_session_with_focus( pub fn switch_session_with_focus(
@ -664,6 +676,7 @@ pub fn switch_session_with_focus(
name: Some(name.to_owned()), name: Some(name.to_owned()),
tab_position, tab_position,
pane_id, pane_id,
..Default::default()
}); });
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap(); let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec()); 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() }; 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 // Utility Functions
#[allow(unused)] #[allow(unused)]

View file

@ -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

View file

@ -172,6 +172,16 @@ pub struct SessionManifest {
pub connected_clients: u32, pub connected_clients: u32,
#[prost(bool, tag = "5")] #[prost(bool, tag = "5")]
pub is_current_session: bool, 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32, pub name: i32,
#[prost( #[prost(
oneof = "plugin_command::Payload", 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>, pub payload: ::core::option::Option<plugin_command::Payload>,
} }
@ -112,10 +112,18 @@ pub mod plugin_command {
CliPipeOutputPayload(super::CliPipeOutputPayload), CliPipeOutputPayload(super::CliPipeOutputPayload),
#[prost(message, tag = "50")] #[prost(message, tag = "50")]
MessageToPluginPayload(super::MessageToPluginPayload), MessageToPluginPayload(super::MessageToPluginPayload),
#[prost(message, tag = "60")]
KillSessionsPayload(super::KillSessionsPayload),
} }
} }
#[allow(clippy::derive_partial_eq_without_eq)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[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 { pub struct CliPipeOutputPayload {
#[prost(string, tag = "1")] #[prost(string, tag = "1")]
pub pipe_name: ::prost::alloc::string::String, pub pipe_name: ::prost::alloc::string::String,
@ -171,6 +179,8 @@ pub struct SwitchSessionPayload {
pub pane_id: ::core::option::Option<u32>, pub pane_id: ::core::option::Option<u32>,
#[prost(bool, optional, tag = "4")] #[prost(bool, optional, tag = "4")]
pub pane_id_is_plugin: ::core::option::Option<bool>, 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)] #[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)] #[derive(Clone, PartialEq, ::prost::Message)]
@ -376,6 +386,8 @@ pub enum CommandName {
BlockCliPipeInput = 77, BlockCliPipeInput = 77,
CliPipeOutput = 78, CliPipeOutput = 78,
MessageToPlugin = 79, MessageToPlugin = 79,
DisconnectOtherClients = 80,
KillSessions = 81,
} }
impl CommandName { impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition. /// String value of the enum field names used in the ProtoBuf definition.
@ -464,6 +476,8 @@ impl CommandName {
CommandName::BlockCliPipeInput => "BlockCliPipeInput", CommandName::BlockCliPipeInput => "BlockCliPipeInput",
CommandName::CliPipeOutput => "CliPipeOutput", CommandName::CliPipeOutput => "CliPipeOutput",
CommandName::MessageToPlugin => "MessageToPlugin", CommandName::MessageToPlugin => "MessageToPlugin",
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
CommandName::KillSessions => "KillSessions",
} }
} }
/// Creates an enum from field names used in the ProtoBuf definition. /// Creates an enum from field names used in the ProtoBuf definition.
@ -549,6 +563,8 @@ impl CommandName {
"BlockCliPipeInput" => Some(Self::BlockCliPipeInput), "BlockCliPipeInput" => Some(Self::BlockCliPipeInput),
"CliPipeOutput" => Some(Self::CliPipeOutput), "CliPipeOutput" => Some(Self::CliPipeOutput),
"MessageToPlugin" => Some(Self::MessageToPlugin), "MessageToPlugin" => Some(Self::MessageToPlugin),
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
"KillSessions" => Some(Self::KillSessions),
_ => None, _ => None,
} }
} }

View file

@ -765,6 +765,28 @@ pub struct SessionInfo {
pub panes: PaneManifest, pub panes: PaneManifest,
pub connected_clients: usize, pub connected_clients: usize,
pub is_current_session: bool, 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}; use std::hash::{Hash, Hasher};
@ -1032,12 +1054,12 @@ impl MessageToPlugin {
self self
} }
pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> 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); new_plugin_args.should_float = Some(should_float);
self self
} }
pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> 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); new_plugin_args.pane_id_to_replace = Some(pane_id);
self self
} }
@ -1045,17 +1067,17 @@ impl MessageToPlugin {
mut self, mut self,
pane_title: impl Into<String>, pane_title: impl Into<String>,
) -> 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.pane_title = Some(pane_title.into()); new_plugin_args.pane_title = Some(pane_title.into());
self self
} }
pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> 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); new_plugin_args.cwd = Some(cwd);
self self
} }
pub fn new_plugin_instance_should_skip_cache(mut self) -> 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; new_plugin_args.skip_cache = true;
self self
} }
@ -1066,6 +1088,17 @@ pub struct ConnectToSession {
pub name: Option<String>, pub name: Option<String>,
pub tab_position: Option<usize>, pub tab_position: Option<usize>,
pub pane_id: Option<(u32, bool)>, // (id, is_plugin) 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)] #[derive(Debug, Default, Clone)]
@ -1228,4 +1261,6 @@ pub enum PluginCommand {
BlockCliPipeInput(String), // String => pipe name BlockCliPipeInput(String), // String => pipe name
CliPipeOutput(String, String), // String => pipe name, String => output CliPipeOutput(String, String), // String => pipe name, String => output
MessageToPlugin(MessageToPlugin), MessageToPlugin(MessageToPlugin),
DisconnectOtherClients,
KillSessions(Vec<String>), // one or more session names
} }

View file

@ -442,6 +442,7 @@ pub enum ServerContext {
UnblockCliPipeInput, UnblockCliPipeInput,
CliPipeOutput, CliPipeOutput,
AssociatePipeWithClient, AssociatePipeWithClient,
DisconnectAllClientsExcept,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]

View file

@ -222,6 +222,15 @@ impl Config {
Err(e) => Err(ConfigError::IoPath(e, path.into())), 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)] #[cfg(test)]

View file

@ -65,6 +65,17 @@ impl Keybinds {
} }
ret 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. // The unit test location.

View file

@ -9,8 +9,8 @@
// If plugins should be able to depend on the layout system // If plugins should be able to depend on the layout system
// then [`zellij-utils`] could be a proper place. // then [`zellij-utils`] could be a proper place.
use crate::{ use crate::{
data::Direction, data::{Direction, LayoutInfo},
home::find_default_config_dir, home::{default_layout_dir, find_default_config_dir},
input::{ input::{
command::RunCommand, command::RunCommand,
config::{Config, ConfigError}, config::{Config, ConfigError},
@ -19,6 +19,7 @@ use crate::{
setup::{self}, setup::{self},
}; };
use std::cmp::Ordering;
use std::fmt::{Display, Formatter}; use std::fmt::{Display, Formatter};
use std::str::FromStr; use std::str::FromStr;
@ -819,6 +820,62 @@ impl Default for LayoutParts {
} }
impl Layout { 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( pub fn stringified_from_path_or_default(
layout_path: Option<&PathBuf>, layout_path: Option<&PathBuf>,
layout_dir: 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 let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config)) 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( pub fn from_str(
raw: &str, raw: &str,
path_to_raw_layout: String, path_to_raw_layout: String,
@ -951,6 +1042,11 @@ impl Layout {
Self::stringified_compact_swap_from_assets()?, Self::stringified_compact_swap_from_assets()?,
)), )),
)), )),
Some("welcome") => Ok((
"Welcome screen layout".into(),
Self::stringified_welcome_from_assets()?,
None,
)),
None | Some(_) => Err(ConfigError::IoPath( None | Some(_) => Err(ConfigError::IoPath(
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"), std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
path.into(), path.into(),
@ -982,6 +1078,10 @@ impl Layout {
Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?) 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>) { pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
self.template.clone().unwrap_or_default() self.template.clone().unwrap_or_default()
} }
@ -1024,24 +1124,10 @@ impl Layout {
swap_layout_path.as_os_str().to_string_lossy().into(), swap_layout_path.as_os_str().to_string_lossy().into(),
swap_kdl_layout, swap_kdl_layout,
)), )),
Err(e) => { Err(_e) => None,
log::warn!(
"Failed to read swap layout file: {}. Error: {:?}",
swap_layout_path.as_os_str().to_string_lossy(),
e
);
None
},
} }
}, },
Err(e) => { Err(_e) => None,
log::warn!(
"Failed to read swap layout file: {}. Error: {:?}",
swap_layout_path.as_os_str().to_string_lossy(),
e
);
None
},
} }
} }
} }

View file

@ -1,7 +1,7 @@
mod kdl_layout_parser; mod kdl_layout_parser;
use crate::data::{ use crate::data::{
Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, Direction, InputMode, Key, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest,
Resize, SessionInfo, TabInfo, PermissionType, Resize, SessionInfo, TabInfo,
}; };
use crate::envs::EnvironmentVariables; use crate::envs::EnvironmentVariables;
use crate::home::{find_default_config_dir, get_layout_dir}; use crate::home::{find_default_config_dir, get_layout_dir};
@ -1986,6 +1986,31 @@ impl SessionInfo {
.and_then(|p| p.children()) .and_then(|p| p.children())
.map(|p| PaneManifest::decode_from_kdl(p)) .map(|p| PaneManifest::decode_from_kdl(p))
.ok_or("Failed to parse panes")?; .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; let is_current_session = name == current_session_name;
Ok(SessionInfo { Ok(SessionInfo {
name, name,
@ -1993,6 +2018,7 @@ impl SessionInfo {
panes, panes,
connected_clients, connected_clients,
is_current_session, is_current_session,
available_layouts,
}) })
} }
pub fn to_string(&self) -> String { pub fn to_string(&self) -> String {
@ -2017,10 +2043,25 @@ impl SessionInfo {
let mut panes = KdlNode::new("panes"); let mut panes = KdlNode::new("panes");
panes.set_children(self.panes.encode_to_kdl()); 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(name);
kdl_document.nodes_mut().push(tabs); kdl_document.nodes_mut().push(tabs);
kdl_document.nodes_mut().push(panes); kdl_document.nodes_mut().push(panes);
kdl_document.nodes_mut().push(connected_clients); kdl_document.nodes_mut().push(connected_clients);
kdl_document.nodes_mut().push(available_layouts);
kdl_document.fmt(); kdl_document.fmt();
kdl_document.to_string() kdl_document.to_string()
} }
@ -2506,6 +2547,11 @@ fn serialize_and_deserialize_session_info_with_data() {
panes: PaneManifest { panes }, panes: PaneManifest { panes },
connected_clients: 2, connected_clients: 2,
is_current_session: false, 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 serialized = session_info.to_string();
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap(); let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 2284 assertion_line: 2459
expression: serialized expression: serialized
--- ---
name "" name ""
@ -9,4 +9,6 @@ tabs {
panes { panes {
} }
connected_clients 0 connected_clients 0
available_layouts {
}

View file

@ -1,6 +1,6 @@
--- ---
source: zellij-utils/src/kdl/mod.rs source: zellij-utils/src/kdl/mod.rs
assertion_line: 2377 assertion_line: 2552
expression: serialized expression: serialized
--- ---
name "my session name" name "my session name"
@ -78,4 +78,9 @@ panes {
} }
} }
connected_clients 2 connected_clients 2
available_layouts {
layout1 source="file"
layout2 source="built-in"
layout3 source="file"
}

View file

@ -152,6 +152,12 @@ message SessionManifest {
repeated PaneManifest panes = 3; repeated PaneManifest panes = 3;
uint32 connected_clients = 4; uint32 connected_clients = 4;
bool is_current_session = 5; bool is_current_session = 5;
repeated LayoutInfo available_layouts = 6;
}
message LayoutInfo {
string name = 1;
string source = 2;
} }
message ResurrectableSession { message ResurrectableSession {

View file

@ -4,9 +4,9 @@ pub use super::generated_api::api::{
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination, event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList, Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds, EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload, KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
ResurrectableSession as ProtobufResurrectableSession, PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *, SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
}, },
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
@ -14,8 +14,8 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle, style::Style as ProtobufStyle,
}; };
use crate::data::{ use crate::data::{
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest, CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo, PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
}; };
use crate::errors::prelude::*; use crate::errors::prelude::*;
@ -453,6 +453,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest {
.collect(), .collect(),
connected_clients: session_info.connected_clients as u32, connected_clients: session_info.connected_clients as u32,
is_current_session: session_info.is_current_session, 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, panes,
connected_clients: protobuf_session_manifest.connected_clients as usize, connected_clients: protobuf_session_manifest.connected_clients as usize,
is_current_session: protobuf_session_manifest.is_current_session, 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 { impl TryFrom<CopyDestination> for ProtobufCopyDestination {
type Error = &'static str; type Error = &'static str;
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'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 }, panes: PaneManifest { panes },
connected_clients: 2, connected_clients: 2,
is_current_session: true, 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 { let session_info_2 = SessionInfo {
name: "session 2".to_owned(), name: "session 2".to_owned(),
@ -1392,6 +1434,11 @@ fn serialize_session_update_event_with_non_default_values() {
}, },
connected_clients: 0, connected_clients: 0,
is_current_session: false, 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 session_infos = vec![session_info_1, session_info_2];
let resurrectable_sessions = vec![]; let resurrectable_sessions = vec![];

View file

@ -91,6 +91,8 @@ enum CommandName {
BlockCliPipeInput = 77; BlockCliPipeInput = 77;
CliPipeOutput = 78; CliPipeOutput = 78;
MessageToPlugin = 79; MessageToPlugin = 79;
DisconnectOtherClients = 80;
KillSessions = 81;
} }
message PluginCommand { message PluginCommand {
@ -145,9 +147,14 @@ message PluginCommand {
string block_cli_pipe_input_payload = 48; string block_cli_pipe_input_payload = 48;
CliPipeOutputPayload cli_pipe_output_payload = 49; CliPipeOutputPayload cli_pipe_output_payload = 49;
MessageToPluginPayload message_to_plugin_payload = 50; MessageToPluginPayload message_to_plugin_payload = 50;
KillSessionsPayload kill_sessions_payload = 60;
} }
} }
message KillSessionsPayload {
repeated string session_names = 1;
}
message CliPipeOutputPayload { message CliPipeOutputPayload {
string pipe_name = 1; string pipe_name = 1;
string output = 2; string output = 2;
@ -185,6 +192,7 @@ message SwitchSessionPayload {
optional uint32 tab_position = 2; optional uint32 tab_position = 2;
optional uint32 pane_id = 3; optional uint32 pane_id = 3;
optional bool pane_id_is_plugin = 4; optional bool pane_id_is_plugin = 4;
optional event.LayoutInfo layout = 5;
} }
message RequestPluginPermissionPayload { message RequestPluginPermissionPayload {

View file

@ -4,10 +4,10 @@ pub use super::generated_api::api::{
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
plugin_command::{ plugin_command::{
plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable, plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable,
ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload, ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload,
MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload, MessageToPluginPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType, OpenCommandPanePayload, OpenFilePayload, PaneId as ProtobufPaneId,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload, RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload, SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
WebRequestPayload, WebRequestPayload,
@ -574,6 +574,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
name: payload.name, name: payload.name,
tab_position: payload.tab_position.map(|p| p as usize), tab_position: payload.tab_position.map(|p| p as usize),
pane_id, pane_id,
layout: payload.layout.and_then(|l| l.try_into().ok()),
})) }))
}, },
_ => Err("Mismatched payload for SwitchSession"), _ => 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"), _ => Err("Mismatched payload for PipeOutput"),
}, },
None => Err("Unrecognized plugin command"), 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), 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: switch_to_session.pane_id.map(|p| p.0),
pane_id_is_plugin: switch_to_session.pane_id.map(|p| p.1), 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 { 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,
})),
}),
} }
} }
} }

View file

@ -167,6 +167,12 @@ pub const COMPACT_BAR_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
"assets/layouts/compact.swap.kdl" "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!( pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"), env!("CARGO_MANIFEST_DIR"),
"/", "/",
@ -345,7 +351,9 @@ impl Setup {
/// 2. layout options /// 2. layout options
/// (`layout.kdl` / `zellij --layout`) /// (`layout.kdl` / `zellij --layout`)
/// 3. config options (`config.kdl`) /// 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 // note that this can potentially exit the process
Setup::handle_setup_commands(cli_args); Setup::handle_setup_commands(cli_args);
let config = Config::try_from(cli_args)?; let config = Config::try_from(cli_args)?;
@ -355,21 +363,34 @@ impl Setup {
} else { } else {
None None
}; };
let mut config_without_layout = config.clone();
let (layout, mut config) = let (layout, mut config) =
Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?; 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(),
};
let user_theme_dir = config_options.theme_dir.clone().or_else(|| { config.themes = config.themes.merge(get_default_themes());
get_theme_dir(cli_args.config_dir.clone().or_else(find_default_config_dir))
.filter(|dir| dir.exists()) 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))
if let Some(user_theme_dir) = user_theme_dir { .filter(|dir| dir.exists())
config.themes = config.themes.merge(Themes::from_dir(user_theme_dir)?); });
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 { 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 /// General setup helpers
@ -667,7 +694,7 @@ mod setup_test {
#[test] #[test]
fn default_config_with_no_cli_arguments() { fn default_config_with_no_cli_arguments() {
let cli_args = CliArgs::default(); 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!("{:#?}", config));
assert_snapshot!(format!("{:#?}", layout)); assert_snapshot!(format!("{:#?}", layout));
assert_snapshot!(format!("{:#?}", options)); assert_snapshot!(format!("{:#?}", options));
@ -682,7 +709,7 @@ mod setup_test {
}, },
..Default::default() ..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!("{:#?}", options));
} }
#[test] #[test]
@ -692,7 +719,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-options.kdl", "{}/src/test-fixtures/layout-with-options.kdl",
env!("CARGO_MANIFEST_DIR") 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!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout)); assert_snapshot!(format!("{:#?}", layout));
} }
@ -710,7 +737,7 @@ mod setup_test {
}, },
..Default::default() ..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!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout)); assert_snapshot!(format!("{:#?}", layout));
} }
@ -725,7 +752,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-env-vars.kdl", "{}/src/test-fixtures/layout-with-env-vars.kdl",
env!("CARGO_MANIFEST_DIR") 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)); assert_snapshot!(format!("{:#?}", config));
} }
#[test] #[test]
@ -739,7 +766,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-ui-config.kdl", "{}/src/test-fixtures/layout-with-ui-config.kdl",
env!("CARGO_MANIFEST_DIR") 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)); assert_snapshot!(format!("{:#?}", config));
} }
#[test] #[test]
@ -753,7 +780,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-plugins-config.kdl", "{}/src/test-fixtures/layout-with-plugins-config.kdl",
env!("CARGO_MANIFEST_DIR") 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)); assert_snapshot!(format!("{:#?}", config));
} }
#[test] #[test]
@ -767,7 +794,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-themes-config.kdl", "{}/src/test-fixtures/layout-with-themes-config.kdl",
env!("CARGO_MANIFEST_DIR") 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)); assert_snapshot!(format!("{:#?}", config));
} }
#[test] #[test]
@ -781,7 +808,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-keybindings-config.kdl", "{}/src/test-fixtures/layout-with-keybindings-config.kdl",
env!("CARGO_MANIFEST_DIR") 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)); assert_snapshot!(format!("{:#?}", config));
} }
} }