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,
);
},
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) => {

View file

@ -1,3 +1,4 @@
mod new_session_info;
mod resurrectable_sessions;
mod session_list;
mod ui;
@ -5,34 +6,58 @@ use zellij_tile::prelude::*;
use std::collections::BTreeMap;
use new_session_info::NewSessionInfo;
use ui::{
components::{
render_controls_line, render_error, render_new_session_line, render_prompt,
render_renaming_session_screen, render_resurrection_toggle, Colors,
render_controls_line, render_error, render_new_session_block, render_prompt,
render_renaming_session_screen, render_screen_toggle, Colors,
},
welcome_screen::{render_banner, render_welcome_boundaries},
SessionUiInfo,
};
use resurrectable_sessions::ResurrectableSessions;
use session_list::SessionList;
#[derive(Clone, Debug, Copy)]
enum ActiveScreen {
NewSession,
AttachToSession,
ResurrectSession,
}
impl Default for ActiveScreen {
fn default() -> Self {
ActiveScreen::AttachToSession
}
}
#[derive(Default)]
struct State {
session_name: Option<String>,
sessions: SessionList,
resurrectable_sessions: ResurrectableSessions,
search_term: String,
new_session_name: Option<String>,
new_session_info: NewSessionInfo,
renaming_session_name: Option<String>,
error: Option<String>,
browsing_resurrection_sessions: bool,
active_screen: ActiveScreen,
colors: Colors,
is_welcome_screen: bool,
show_kill_all_sessions_warning: bool,
}
register_plugin!(State);
impl ZellijPlugin for State {
fn load(&mut self, _configuration: BTreeMap<String, String>) {
fn load(&mut self, configuration: BTreeMap<String, String>) {
self.is_welcome_screen = configuration
.get("welcome_screen")
.map(|v| v == "true")
.unwrap_or(false);
if self.is_welcome_screen {
self.active_screen = ActiveScreen::NewSession;
}
subscribe(&[
EventType::ModeUpdate,
EventType::SessionUpdate,
@ -55,6 +80,12 @@ impl ZellijPlugin for State {
should_render = true;
},
Event::SessionUpdate(session_infos, resurrectable_session_list) => {
for session_info in &session_infos {
if session_info.is_current_session {
self.new_session_info
.update_layout_list(session_info.available_layouts.clone());
}
}
self.resurrectable_sessions
.update(resurrectable_session_list);
self.update_session_infos(session_infos);
@ -66,36 +97,53 @@ impl ZellijPlugin for State {
}
fn render(&mut self, rows: usize, cols: usize) {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions.render(rows, cols);
return;
} else if let Some(new_session_name) = self.renaming_session_name.as_ref() {
render_renaming_session_screen(&new_session_name, rows, cols);
return;
let (x, y, width, height) = self.main_menu_size(rows, cols);
if self.is_welcome_screen {
render_banner(x, 0, rows.saturating_sub(height), width);
}
render_resurrection_toggle(cols, false);
render_prompt(
self.new_session_name.is_some(),
&self.search_term,
render_screen_toggle(self.active_screen, x, y, width.saturating_sub(2));
match self.active_screen {
ActiveScreen::NewSession => {
render_new_session_block(
&self.new_session_info,
self.colors,
height,
width,
x,
y + 2,
);
let room_for_list = rows.saturating_sub(5); // search line and controls
self.sessions.update_rows(room_for_list);
let list = self
.sessions
.render(room_for_list, cols.saturating_sub(7), self.colors); // 7 for various ui
for line in list {
println!("{}", line.render());
}
render_new_session_line(
&self.new_session_name,
self.sessions.is_searching,
self.colors,
);
if let Some(error) = self.error.as_ref() {
render_error(&error, rows, cols);
},
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_controls_line(self.sessions.is_searching, rows, cols, self.colors);
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);
},
}
if let Some(error) = self.error.as_ref() {
render_error(&error, height, width, x, y);
} else {
render_controls_line(self.active_screen, width, self.colors, x + 1, rows);
}
if self.is_welcome_screen {
render_welcome_boundaries(rows, cols); // explicitly done in the end to override some
// stuff, see comment in function
}
}
}
@ -109,40 +157,77 @@ impl State {
self.error = None;
return true;
}
match self.active_screen {
ActiveScreen::NewSession => self.handle_new_session_key(key),
ActiveScreen::AttachToSession => self.handle_attach_to_session(key),
ActiveScreen::ResurrectSession => self.handle_resurrect_session_key(key),
}
}
fn handle_new_session_key(&mut self, key: Key) -> bool {
let mut should_render = false;
if let Key::Right = key {
if self.new_session_name.is_none() {
self.sessions.result_expand();
}
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();
}
if let Key::Down = key {
self.new_session_info.handle_key(key);
should_render = true;
} else if let Key::Up = key {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions.move_selection_up();
} else if self.new_session_name.is_none() && self.renaming_session_name.is_none() {
self.sessions.move_selection_up();
}
self.new_session_info.handle_key(key);
should_render = true;
} else if let Key::Char(character) = key {
if character == '\n' {
self.handle_selection();
} else if let Some(new_session_name) = self.new_session_name.as_mut() {
} else {
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);
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
renaming_session_name.push(character);
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_character(character);
} else {
self.search_term.push(character);
self.sessions
@ -150,20 +235,12 @@ impl State {
}
should_render = true;
} else if let Key::Backspace = key {
if let Some(new_session_name) = self.new_session_name.as_mut() {
if let Some(new_session_name) = self.renaming_session_name.as_mut() {
if new_session_name.is_empty() {
self.new_session_name = None;
self.renaming_session_name = None;
} else {
new_session_name.pop();
}
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
if renaming_session_name.is_empty() {
self.renaming_session_name = None;
} else {
renaming_session_name.pop();
}
} else if self.browsing_resurrection_sessions {
self.resurrectable_sessions.handle_backspace();
} else {
self.search_term.pop();
self.sessions
@ -171,101 +248,110 @@ impl State {
}
should_render = true;
} else if let Key::Ctrl('w') = key {
if self.sessions.is_searching || self.browsing_resurrection_sessions {
// no-op
} else if self.new_session_name.is_some() {
self.new_session_name = None;
} else {
self.new_session_name = Some(String::new());
}
self.active_screen = ActiveScreen::NewSession;
should_render = true;
} else if let Key::Ctrl('r') = key {
if self.sessions.is_searching || self.browsing_resurrection_sessions {
// no-op
} else if self.renaming_session_name.is_some() {
self.renaming_session_name = None;
} else {
self.renaming_session_name = Some(String::new());
should_render = true;
} else if let Key::Delete = key {
if let Some(selected_session_name) = self.sessions.get_selected_session_name() {
kill_sessions(&[selected_session_name]);
self.reset_selected_index();
self.search_term.clear();
self.sessions
.update_search_term(&self.search_term, &self.colors);
} else {
self.show_error("Must select session before killing it.");
}
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 {
if let Some(new_session_name) = self.new_session_name.as_mut() {
if new_session_name.is_empty() {
self.new_session_name = None;
} else {
new_session_name.clear()
}
} else if let Some(renaming_session_name) = self.renaming_session_name.as_mut() {
if renaming_session_name.is_empty() {
self.renaming_session_name = None;
} else {
renaming_session_name.clear()
}
} else if !self.search_term.is_empty() {
if !self.search_term.is_empty() {
self.search_term.clear();
self.sessions
.update_search_term(&self.search_term, &self.colors);
self.reset_selected_index();
} else {
} else if !self.is_welcome_screen {
self.reset_selected_index();
hide_self();
}
should_render = true;
} else if let Key::BackTab = key {
self.browsing_resurrection_sessions = !self.browsing_resurrection_sessions;
self.toggle_active_screen();
should_render = true;
} else if let Key::Delete = key {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions.delete_selected_session();
should_render = true;
}
} else if let Key::Ctrl('d') = key {
if self.browsing_resurrection_sessions {
self.resurrectable_sessions
.show_delete_all_sessions_warning();
should_render = true;
}
} else if let Key::Esc = key {
if self.renaming_session_name.is_some() {
self.renaming_session_name = None;
should_render = true;
} else if self.new_session_name.is_some() {
self.new_session_name = None;
} else if !self.is_welcome_screen {
hide_self();
}
}
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 {
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();
}
}
should_render
}
fn handle_selection(&mut self) {
if self.browsing_resurrection_sessions {
if let Some(session_name_to_resurrect) =
self.resurrectable_sessions.get_selected_session_name()
{
switch_session(Some(&session_name_to_resurrect));
}
} else if let Some(new_session_name) = &self.new_session_name {
if new_session_name.is_empty() {
switch_session(None);
} else if self.session_name.as_ref() == Some(new_session_name) {
// noop - we're already here!
self.new_session_name = None;
} else {
switch_session(Some(new_session_name));
}
} else if let Some(renaming_session_name) = &self.renaming_session_name.take() {
match self.active_screen {
ActiveScreen::NewSession => {
self.new_session_info.handle_selection(&self.session_name);
},
ActiveScreen::AttachToSession => {
if let Some(renaming_session_name) = &self.renaming_session_name.take() {
if renaming_session_name.is_empty() {
// TODO: implement these, then implement the error UI, then implement the renaming
// session screen, then test it
self.show_error("New name must not be empty.");
return; // s that we don't hide self
return; // so that we don't hide self
} else if self.session_name.as_ref() == Some(renaming_session_name) {
// noop - we're already called that!
return; // s that we don't hide self
return; // so that we don't hide self
} else if self.sessions.has_session(&renaming_session_name) {
self.show_error("A session by this name already exists.");
return; // s that we don't hide self
return; // so that we don't hide self
} else if self
.resurrectable_sessions
.has_session(&renaming_session_name)
@ -277,7 +363,8 @@ impl State {
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() {
}
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();
@ -292,15 +379,34 @@ impl State {
go_to_tab(tab_position as u32);
}
} else {
switch_session_with_focus(&selected_session_name, selected_tab, selected_pane);
switch_session_with_focus(
&selected_session_name,
selected_tab,
selected_pane,
);
}
}
self.reset_selected_index();
self.new_session_name = None;
self.search_term.clear();
self.sessions
.update_search_term(&self.search_term, &self.colors);
hide_self();
},
ActiveScreen::ResurrectSession => {
if let Some(session_name_to_resurrect) =
self.resurrectable_sessions.get_selected_session_name()
{
switch_session(Some(&session_name_to_resurrect));
}
},
}
}
fn toggle_active_screen(&mut self) {
self.active_screen = match self.active_screen {
ActiveScreen::NewSession => ActiveScreen::AttachToSession,
ActiveScreen::AttachToSession => ActiveScreen::ResurrectSession,
ActiveScreen::ResurrectSession => ActiveScreen::NewSession,
};
}
fn show_error(&mut self, error_text: &str) {
self.error = Some(error_text.to_owned());
@ -329,4 +435,53 @@ impl State {
}
self.sessions.set_sessions(session_infos);
}
fn main_menu_size(&self, rows: usize, cols: usize) -> (usize, usize, usize, usize) {
// x, y, width, height
let width = if self.is_welcome_screen {
std::cmp::min(cols, 101)
} else {
cols
};
let x = if self.is_welcome_screen {
(cols.saturating_sub(width) as f64 / 2.0).floor() as usize + 2
} else {
0
};
let y = if self.is_welcome_screen {
(rows.saturating_sub(15) as f64 / 2.0).floor() as usize
} else {
0
};
let height = rows.saturating_sub(y);
(x, y, width, height)
}
fn render_kill_all_sessions_warning(&self, rows: usize, columns: usize, x: usize, y: usize) {
if rows == 0 || columns == 0 {
return;
}
let session_count = self.sessions.all_other_sessions().len();
let session_count_len = session_count.to_string().chars().count();
let warning_description_text = format!("This will kill {} active sessions", session_count);
let confirmation_text = "Are you sure? (y/n)";
let warning_y_location = y + (rows / 2).saturating_sub(1);
let confirmation_y_location = y + (rows / 2) + 1;
let warning_x_location =
x + columns.saturating_sub(warning_description_text.chars().count()) / 2;
let confirmation_x_location =
x + columns.saturating_sub(confirmation_text.chars().count()) / 2;
print_text_with_coordinates(
Text::new(warning_description_text).color_range(0, 15..16 + session_count_len),
warning_x_location,
warning_y_location,
None,
None,
);
print_text_with_coordinates(
Text::new(confirmation_text).color_indices(2, vec![15, 17]),
confirmation_x_location,
confirmation_y_location,
None,
None,
);
}
}

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

View file

@ -323,6 +323,18 @@ impl SessionList {
.find(|s| s.name == old_name)
.map(|s| s.name = new_name.to_owned());
}
pub fn all_other_sessions(&self) -> Vec<String> {
self.session_ui_infos
.iter()
.filter_map(|s| {
if !s.is_current_session {
Some(s.name.clone())
} else {
None
}
})
.collect()
}
}
#[derive(Debug, Clone, Default)]

View file

@ -3,6 +3,7 @@ use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
use crate::{ActiveScreen, NewSessionInfo};
#[derive(Debug)]
pub struct ListItem {
@ -292,18 +293,45 @@ impl LineToRender {
pub fn append(&mut self, to_append: &str) {
self.line.push_str(to_append)
}
pub fn make_selected(&mut self) {
pub fn make_selected_as_search(&mut self, add_arrows: bool) {
self.is_selected = true;
let arrows = if add_arrows {
self.colors.magenta(" <↓↑> ")
} else {
" ".to_owned()
};
match self.colors.palette.bg {
PaletteColor::EightBit(byte) => {
self.line = format!(
"\u{1b}[48;5;{byte}m\u{1b}[K\r\u{1b}[48;5;{byte}m{}",
"\u{1b}[48;5;{byte}m\u{1b}[K\u{1b}[48;5;{byte}m{arrows}{}",
self.line
);
},
PaletteColor::Rgb((r, g, b)) => {
self.line = format!(
"\u{1b}[48;2;{};{};{}m\u{1b}[K\r\u{1b}[48;2;{};{};{}m{}",
"\u{1b}[48;2;{};{};{}m\u{1b}[K\u{1b}[48;2;{};{};{}m{arrows}{}",
r, g, b, r, g, b, self.line
);
},
}
}
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
);
},
@ -475,151 +503,275 @@ pub fn minimize_lines(
(start_index, anchor_index, end_index, line_count_to_remove)
}
pub fn render_prompt(typing_session_name: bool, search_term: &str, colors: Colors) {
if !typing_session_name {
let prompt = colors.bold(&format!("> {}_", search_term));
println!("\u{1b}[H{}\n", prompt);
} else {
println!("\n");
}
pub fn render_prompt(search_term: &str, colors: Colors, x: usize, y: usize) {
let prompt = colors.green(&format!("Search:"));
let search_term = colors.bold(&format!("{}_", search_term));
println!("\u{1b}[{};{}H{} {}\n", y + 1, x, prompt, search_term);
}
pub fn render_resurrection_toggle(cols: usize, resurrection_screen_is_active: bool) {
pub fn render_screen_toggle(active_screen: ActiveScreen, x: usize, y: usize, max_cols: usize) {
let key_indication_text = "<TAB>";
let running_sessions_text = "Running";
let exited_sessions_text = "Exited";
let (new_session_text, running_sessions_text, exited_sessions_text) = if max_cols > 66 {
("New Session", "Attach to Session", "Resurrect Session")
} else {
("New", "Attach", "Resurrect")
};
let key_indication_len = key_indication_text.chars().count() + 1;
let first_ribbon_length = running_sessions_text.chars().count() + 4;
let second_ribbon_length = exited_sessions_text.chars().count() + 4;
let key_indication_x =
cols.saturating_sub(key_indication_len + first_ribbon_length + second_ribbon_length);
let first_ribbon_length = new_session_text.chars().count() + 4;
let second_ribbon_length = running_sessions_text.chars().count() + 4;
let third_ribbon_length = exited_sessions_text.chars().count() + 4;
let total_len =
key_indication_len + first_ribbon_length + second_ribbon_length + third_ribbon_length;
let key_indication_x = x;
let first_ribbon_x = key_indication_x + key_indication_len;
let second_ribbon_x = first_ribbon_x + first_ribbon_length;
let third_ribbon_x = second_ribbon_x + second_ribbon_length;
let mut new_session_text = Text::new(new_session_text);
let mut running_sessions_text = Text::new(running_sessions_text);
let mut exited_sessions_text = Text::new(exited_sessions_text);
match active_screen {
ActiveScreen::NewSession => {
new_session_text = new_session_text.selected();
},
ActiveScreen::AttachToSession => {
running_sessions_text = running_sessions_text.selected();
},
ActiveScreen::ResurrectSession => {
exited_sessions_text = exited_sessions_text.selected();
},
}
print_text_with_coordinates(
Text::new(key_indication_text).color_range(3, ..),
key_indication_x,
0,
y,
None,
None,
);
if resurrection_screen_is_active {
print_ribbon_with_coordinates(
Text::new(running_sessions_text),
first_ribbon_x,
0,
None,
None,
);
print_ribbon_with_coordinates(
Text::new(exited_sessions_text).selected(),
second_ribbon_x,
0,
None,
None,
print_ribbon_with_coordinates(new_session_text, first_ribbon_x, y, None, None);
print_ribbon_with_coordinates(running_sessions_text, second_ribbon_x, y, None, None);
print_ribbon_with_coordinates(exited_sessions_text, third_ribbon_x, y, None, None);
}
pub fn render_new_session_block(
new_session_info: &NewSessionInfo,
colors: Colors,
max_rows_of_new_session_block: usize,
max_cols_of_new_session_block: usize,
x: usize,
y: usize,
) {
let enter = colors.magenta("<ENTER>");
if new_session_info.entering_new_session_name() {
let prompt = "New session name:";
let long_instruction = "when done, blank for random";
let new_session_name = new_session_info.name();
if max_cols_of_new_session_block
> prompt.width() + long_instruction.width() + new_session_name.width() + 15
{
println!(
"\u{1b}[m{}{} {}_ ({} {})",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(prompt),
colors.orange(&new_session_name),
enter,
long_instruction,
);
} 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) {
if is_searching {
return;
}
let new_session_shortcut_text = "<Ctrl w>";
let new_session_shortcut = colors.magenta(new_session_shortcut_text);
let new_session = colors.bold("New session");
let enter = colors.magenta("<ENTER>");
match session_name {
Some(session_name) => {
println!(
"\u{1b}[m > {}_ ({}, {} when done)",
colors.orange(session_name),
colors.bold("Type optional name"),
enter
"\u{1b}[m{}{} {}_ {}",
format!("\u{1b}[{};{}H", y + 1, x + 1),
colors.green(prompt),
colors.orange(&new_session_name),
enter,
);
}
} 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,
);
},
None => {
println!("\u{1b}[m > {new_session_shortcut} - {new_session}");
},
}
}
pub fn render_error(error_text: &str, rows: usize, columns: usize) {
pub fn render_layout_selection_list(
new_session_info: &NewSessionInfo,
max_rows_of_new_session_block: usize,
max_cols_of_new_session_block: usize,
x: usize,
y: usize,
) {
let layout_search_term = new_session_info.layout_search_term();
let search_term_len = layout_search_term.width();
let layout_indication_line = if max_cols_of_new_session_block > 73 + search_term_len {
Text::new(format!(
"New session layout: {}_ (Search and select from list, <ENTER> when done)",
layout_search_term
))
.color_range(2, ..20 + search_term_len)
.color_range(3, 20..20 + search_term_len)
.color_range(3, 52 + search_term_len..59 + search_term_len)
} else {
Text::new(format!(
"New session layout: {}_ <ENTER>",
layout_search_term
))
.color_range(2, ..20 + search_term_len)
.color_range(3, 20..20 + search_term_len)
.color_range(3, 22 + search_term_len..)
};
print_text_with_coordinates(layout_indication_line, x, y + 1, None, None);
println!();
let mut table = Table::new();
for (i, (layout_info, indices, is_selected)) in
new_session_info.layouts_to_render().into_iter().enumerate()
{
let layout_name = layout_info.name();
let layout_name_len = layout_name.width();
let is_builtin = layout_info.is_builtin();
if i > max_rows_of_new_session_block {
break;
} else {
let mut layout_cell = if is_builtin {
Text::new(format!("{} (built-in)", layout_name))
.color_range(1, 0..layout_name_len)
.color_range(0, layout_name_len + 1..)
.color_indices(3, indices)
} else {
Text::new(format!("{}", layout_name))
.color_range(1, ..)
.color_indices(3, indices)
};
if is_selected {
layout_cell = layout_cell.selected();
}
let arrow_cell = if is_selected {
Text::new(format!("<↓↑>")).selected().color_range(3, ..)
} else {
Text::new(format!(" ")).color_range(3, ..)
};
table = table.add_styled_row(vec![arrow_cell, layout_cell]);
}
}
print_table_with_coordinates(table, x, y + 3, None, None);
}
pub fn render_error(error_text: &str, rows: usize, columns: usize, x: usize, y: usize) {
print_text_with_coordinates(
Text::new(format!("Error: {}", error_text)).color_range(3, ..),
0,
rows,
x,
y + rows,
Some(columns),
None,
);
}
pub fn render_renaming_session_screen(new_session_name: &str, rows: usize, columns: usize) {
pub fn render_renaming_session_screen(
new_session_name: &str,
rows: usize,
columns: usize,
x: usize,
y: usize,
) {
if rows == 0 || columns == 0 {
return;
}
let prompt_text = "NEW NAME FOR CURRENT SESSION";
let new_session_name = format!("{}_", new_session_name);
let prompt_y_location = (rows / 2).saturating_sub(1);
let session_name_y_location = (rows / 2) + 1;
let prompt_x_location = columns.saturating_sub(prompt_text.chars().count()) / 2;
let session_name_x_location = columns.saturating_sub(new_session_name.chars().count()) / 2;
print_text_with_coordinates(
Text::new(prompt_text).color_range(0, ..),
prompt_x_location,
prompt_y_location,
None,
None,
);
print_text_with_coordinates(
Text::new(new_session_name).color_range(3, ..),
session_name_x_location,
session_name_y_location,
None,
None,
let text = Text::new(format!(
"New name for current session: {}_ (<ENTER> when done)",
new_session_name
))
.color_range(2, ..29)
.color_range(
3,
33 + new_session_name.width()..40 + new_session_name.width(),
);
print_text_with_coordinates(text, x, y, None, None);
}
pub fn render_controls_line(is_searching: bool, row: usize, max_cols: usize, colors: Colors) {
let (arrows, navigate) = if is_searching {
(colors.magenta("<↓↑>"), colors.bold("Navigate"))
} else {
(colors.magenta("<←↓↑→>"), colors.bold("Navigate and Expand"))
};
let rename = colors.magenta("<Ctrl r>");
let rename_text = colors.bold("Rename session");
let enter = colors.magenta("<ENTER>");
let select = colors.bold("Switch to selected");
let esc = colors.magenta("<ESC>");
let to_hide = colors.bold("Hide");
if max_cols >= 104 {
pub fn render_controls_line(
active_screen: ActiveScreen,
max_cols: usize,
colors: Colors,
x: usize,
y: usize,
) {
match active_screen {
ActiveScreen::NewSession => {
if max_cols >= 50 {
print!(
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {rename} - {rename_text}, {esc} - {to_hide}"
"\u{1b}[m\u{1b}[{y};{x}H\u{1b}[1mHelp: Fill in the form to start a new session."
);
} else if max_cols >= 73 {
}
},
ActiveScreen::AttachToSession => {
let arrows = colors.magenta("<←↓↑→>");
let navigate = colors.bold("Navigate");
let select = colors.bold("Switch");
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 > 90 {
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 >= 28 {
print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{rename}/{esc}");
print!("\u{1b}[m\u{1b}[{y};{x}H{rename}/{disconnect}/{kill}/{kill_all}");
}
},
ActiveScreen::ResurrectSession => {
let arrows = colors.magenta("<↓↑>");
let navigate = colors.bold("Navigate");
let enter = colors.magenta("<ENTER>");
let select = colors.bold("Resurrect");
let del = colors.magenta("<DEL>");
let del_text = colors.bold("Delete");
let del_all = colors.magenta("<Ctrl d>");
let del_all_text = colors.bold("Delete all");
if max_cols > 83 {
print!(
"\u{1b}[m\u{1b}[{y};{x}HHelp: {arrows} - {navigate}, {enter} - {select}, {del} - {del_text}, {del_all} - {del_all_text}"
);
} else if max_cols >= 28 {
print!("\u{1b}[m\u{1b}[{y};{x}H{arrows}/{enter}/{del}/{del_all}");
}
},
}
}

View file

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

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_utils::{
cli::{CliArgs, Command, SessionCommand, Sessions},
data::ConnectToSession,
data::{ConnectToSession, LayoutInfo},
envs,
input::{
actions::Action,
config::{Config, ConfigError},
layout::Layout,
options::Options,
},
miette::{Report, Result},
nix,
setup::Setup,
setup::{find_default_config_dir, get_layout_dir, Setup},
};
pub(crate) use crate::sessions::list_sessions;
@ -383,7 +384,8 @@ fn attach_with_session_name(
pub(crate) fn start_client(opts: CliArgs) {
// look for old YAML config/layout/theme files and convert them to KDL
convert_old_yaml_files(&opts);
let (config, layout, config_options) = match Setup::from_cli_args(&opts) {
let (config, layout, config_options, config_without_layout, config_options_without_layout) =
match Setup::from_cli_args(&opts) {
Ok(results) => results,
Err(e) => {
if let ConfigError::KdlError(error) = e {
@ -399,8 +401,8 @@ pub(crate) fn start_client(opts: CliArgs) {
let os_input = get_os_input(get_client_os_input);
loop {
let os_input = os_input.clone();
let config = config.clone();
let layout = layout.clone();
let mut config = config.clone();
let mut layout = layout.clone();
let mut config_options = config_options.clone();
let mut opts = opts.clone();
let mut is_a_reconnect = false;
@ -423,6 +425,43 @@ pub(crate) fn start_client(opts: CliArgs) {
opts.session = None;
config_options.attach_to_session = None;
}
if let Some(reconnect_layout) = &reconnect_to_session.layout {
let layout_dir = config.options.layout_dir.clone().or_else(|| {
get_layout_dir(opts.config_dir.clone().or_else(find_default_config_dir))
});
let new_session_layout = match reconnect_layout {
LayoutInfo::BuiltIn(layout_name) => Layout::from_default_assets(
&PathBuf::from(layout_name),
layout_dir.clone(),
config_without_layout.clone(),
),
LayoutInfo::File(layout_name) => Layout::from_path_or_default(
Some(&PathBuf::from(layout_name)),
layout_dir.clone(),
config_without_layout.clone(),
),
};
match new_session_layout {
Ok(new_session_layout) => {
// here we make sure to override both the layout and the config, but we do
// this with an instance of the config before it was merged with the
// layout configuration of the previous iteration of the loop, since we do
// not want it to mix with the config of this session
let (new_layout, new_layout_config) = new_session_layout;
layout = new_layout;
let mut new_config = config_without_layout.clone();
let _ = new_config.merge(new_layout_config.clone());
config = new_config;
config_options =
config_options_without_layout.merge(new_layout_config.options);
},
Err(e) => {
log::error!("Failed to parse new session layout: {:?}", e);
},
}
}
is_a_reconnect = true;
}

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

View file

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

View file

@ -5970,3 +5970,243 @@ pub fn pipe_message_to_plugin_plugin_command() {
});
assert_snapshot!(format!("{:#?}", plugin_bytes_event));
}
#[test]
#[ignore]
pub fn switch_session_plugin_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
};
let tab_index = 1;
let client_id = 1;
let size = Size {
cols: 121,
rows: 20,
};
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
received_screen_instructions,
ScreenInstruction::Exit,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);
let received_server_instruction = Arc::new(Mutex::new(vec![]));
let server_thread = log_actions_in_thread!(
received_server_instruction,
ServerInstruction::SwitchSession,
server_receiver,
1
);
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
tab_index,
None,
client_id,
size,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::Key(Key::Ctrl('5')), // this triggers the enent in the fixture plugin
)]));
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
server_thread.join().unwrap(); // this might take a while if the cache is cold
let switch_session_event = received_server_instruction
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ServerInstruction::SwitchSession(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", switch_session_event));
}
#[test]
#[ignore]
pub fn switch_session_with_layout_plugin_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
};
let tab_index = 1;
let client_id = 1;
let size = Size {
cols: 121,
rows: 20,
};
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
received_screen_instructions,
ScreenInstruction::Exit,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);
let received_server_instruction = Arc::new(Mutex::new(vec![]));
let server_thread = log_actions_in_thread!(
received_server_instruction,
ServerInstruction::SwitchSession,
server_receiver,
1
);
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
tab_index,
None,
client_id,
size,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::Key(Key::Ctrl('7')), // this triggers the enent in the fixture plugin
)]));
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
server_thread.join().unwrap(); // this might take a while if the cache is cold
let switch_session_event = received_server_instruction
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ServerInstruction::SwitchSession(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", switch_session_event));
}
#[test]
#[ignore]
pub fn disconnect_other_clients_plugins_command() {
let temp_folder = tempdir().unwrap(); // placed explicitly in the test scope because its
// destructor removes the directory
let plugin_host_folder = PathBuf::from(temp_folder.path());
let cache_path = plugin_host_folder.join("permissions_test.kdl");
let (plugin_thread_sender, server_receiver, screen_receiver, teardown) =
create_plugin_thread_with_server_receiver(Some(plugin_host_folder));
let plugin_should_float = Some(false);
let plugin_title = Some("test_plugin".to_owned());
let run_plugin = RunPlugin {
_allow_exec_host_cmd: false,
location: RunPluginLocation::File(PathBuf::from(&*PLUGIN_FIXTURE)),
configuration: Default::default(),
};
let tab_index = 1;
let client_id = 1;
let size = Size {
cols: 121,
rows: 20,
};
let received_screen_instructions = Arc::new(Mutex::new(vec![]));
let _screen_thread = grant_permissions_and_log_actions_in_thread_naked_variant!(
received_screen_instructions,
ScreenInstruction::Exit,
screen_receiver,
1,
&PermissionType::ChangeApplicationState,
cache_path,
plugin_thread_sender,
client_id
);
let received_server_instruction = Arc::new(Mutex::new(vec![]));
let server_thread = log_actions_in_thread!(
received_server_instruction,
ServerInstruction::DisconnectAllClientsExcept,
server_receiver,
1
);
let _ = plugin_thread_sender.send(PluginInstruction::AddClient(client_id));
let _ = plugin_thread_sender.send(PluginInstruction::Load(
plugin_should_float,
false,
plugin_title,
run_plugin,
tab_index,
None,
client_id,
size,
None,
false,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
Event::Key(Key::Ctrl('6')), // this triggers the enent in the fixture plugin
)]));
std::thread::sleep(std::time::Duration::from_millis(500));
teardown();
server_thread.join().unwrap(); // this might take a while if the cache is cold
let switch_session_event = received_server_instruction
.lock()
.unwrap()
.iter()
.rev()
.find_map(|i| {
if let ServerInstruction::DisconnectAllClientsExcept(..) = i {
Some(i.clone())
} else {
None
}
})
.clone();
assert_snapshot!(format!("{:#?}", switch_session_event));
}

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_wasi::WasiEnv;
use zellij_utils::data::{
CommandType, ConnectToSession, HttpVerb, MessageToPlugin, PermissionStatus, PermissionType,
PluginPermission,
CommandType, ConnectToSession, HttpVerb, LayoutInfo, MessageToPlugin, PermissionStatus,
PermissionType, PluginPermission,
};
use zellij_utils::input::permission::PermissionCache;
use zellij_utils::{
interprocess::local_socket::LocalSocketStream,
ipc::{ClientToServerMsg, IpcSenderWithContext},
};
use url::Url;
@ -225,6 +229,7 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
connect_to_session.name,
connect_to_session.tab_position,
connect_to_session.pane_id,
connect_to_session.layout,
)?,
PluginCommand::DeleteDeadSession(session_name) => {
delete_dead_session(session_name)?
@ -252,6 +257,8 @@ fn host_run_plugin_command(env: FunctionEnvMut<ForeignFunctionEnv>) {
cli_pipe_output(env, pipe_name, output)?
},
PluginCommand::MessageToPlugin(message) => message_to_plugin(env, message)?,
PluginCommand::DisconnectOtherClients => disconnect_other_clients(env),
PluginCommand::KillSessions(session_list) => kill_sessions(session_list),
},
(PermissionStatus::Denied, permission) => {
log::error!(
@ -900,6 +907,7 @@ fn switch_session(
session_name: Option<String>,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
layout: Option<LayoutInfo>,
) -> Result<()> {
// pane_id is (id, is_plugin)
let err_context = || format!("Failed to switch session");
@ -909,6 +917,7 @@ fn switch_session(
name: session_name,
tab_position,
pane_id,
layout,
};
env.plugin_env
.senders
@ -1278,6 +1287,30 @@ fn rename_session(env: &ForeignFunctionEnv, new_session_name: String) {
apply_action!(action, error_msg, env);
}
fn disconnect_other_clients(env: &ForeignFunctionEnv) {
let _ = env
.plugin_env
.senders
.send_to_server(ServerInstruction::DisconnectAllClientsExcept(
env.plugin_env.client_id,
))
.context("failed to send disconnect other clients instruction");
}
fn kill_sessions(session_names: Vec<String>) {
for session_name in session_names {
let path = &*ZELLIJ_SOCK_DIR.join(&session_name);
match LocalSocketStream::connect(path) {
Ok(stream) => {
let _ = IpcSenderWithContext::new(stream).send(ClientToServerMsg::KillSession);
},
Err(e) => {
log::error!("Failed to kill session {}: {:?}", session_name, e);
},
};
}
}
// Custom panic handler for plugins.
//
// This is called when a panic occurs in a plugin. Since most panics will likely originate in the
@ -1406,7 +1439,9 @@ fn check_command_permission(
| PluginCommand::DeleteDeadSession(..)
| PluginCommand::DeleteAllDeadSessions
| PluginCommand::RenameSession(..)
| PluginCommand::RenameTab(..) => PermissionType::ChangeApplicationState,
| PluginCommand::RenameTab(..)
| PluginCommand::DisconnectOtherClients
| PluginCommand::KillSessions(..) => PermissionType::ChangeApplicationState,
PluginCommand::UnblockCliPipeInput(..)
| PluginCommand::BlockCliPipeInput(..)
| PluginCommand::CliPipeOutput(..) => PermissionType::ReadCliPipes,

View file

@ -577,6 +577,8 @@ pub(crate) struct Screen {
default_shell: Option<PathBuf>,
styled_underlines: bool,
arrow_fonts: bool,
layout_dir: Option<PathBuf>,
default_layout_name: Option<String>,
}
impl Screen {
@ -592,12 +594,14 @@ impl Screen {
copy_options: CopyOptions,
debug: bool,
default_layout: Box<Layout>,
default_layout_name: Option<String>,
default_shell: Option<PathBuf>,
session_serialization: bool,
serialize_pane_viewport: bool,
scrollback_lines_to_serialize: Option<usize>,
styled_underlines: bool,
arrow_fonts: bool,
layout_dir: Option<PathBuf>,
) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone());
@ -629,6 +633,7 @@ impl Screen {
session_name,
session_infos_on_machine,
default_layout,
default_layout_name,
default_shell,
session_serialization,
serialize_pane_viewport,
@ -636,6 +641,7 @@ impl Screen {
styled_underlines,
arrow_fonts,
resurrectable_sessions,
layout_dir,
}
}
@ -1412,12 +1418,21 @@ impl Screen {
// generate own session info
let pane_manifest = self.generate_and_report_pane_state()?;
let tab_infos = self.generate_and_report_tab_state()?;
// in the context of unit/integration tests, we don't need to list available layouts
// because this is mostly about HD access - it does however throw off the timing in the
// tests and causes them to flake, which is why we skip it here
#[cfg(not(test))]
let available_layouts =
Layout::list_available_layouts(self.layout_dir.clone(), &self.default_layout_name);
#[cfg(test)]
let available_layouts = vec![];
let session_info = SessionInfo {
name: self.session_name.clone(),
tabs: tab_infos,
panes: pane_manifest,
connected_clients: self.active_tab_indices.keys().len(),
is_current_session: true,
available_layouts,
};
self.bus
.senders
@ -2101,7 +2116,11 @@ pub(crate) fn screen_thread_main(
let serialize_pane_viewport = config_options.serialize_pane_viewport.unwrap_or(false);
let scrollback_lines_to_serialize = config_options.scrollback_lines_to_serialize;
let session_is_mirrored = config_options.mirror_session.unwrap_or(false);
let layout_dir = config_options.layout_dir;
let default_shell = config_options.default_shell;
let default_layout_name = config_options
.default_layout
.map(|l| format!("{}", l.display()));
let copy_options = CopyOptions::new(
config_options.copy_command,
config_options.copy_clipboard.unwrap_or_default(),
@ -2128,12 +2147,14 @@ pub(crate) fn screen_thread_main(
copy_options,
debug,
default_layout,
default_layout_name,
default_shell,
session_serialization,
serialize_pane_viewport,
scrollback_lines_to_serialize,
styled_underlines,
arrow_fonts,
layout_dir,
);
let mut pending_tab_ids: HashSet<usize> = HashSet::new();

View file

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

View file

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

View file

@ -653,6 +653,18 @@ pub fn switch_session(name: Option<&str>) {
unsafe { host_run_plugin_command() };
}
/// Switch to a session with the given name, create one if no name is given
pub fn switch_session_with_layout(name: Option<&str>, layout: LayoutInfo) {
let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
name: name.map(|n| n.to_string()),
layout: Some(layout),
..Default::default()
});
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Switch to a session with the given name, focusing either the provided pane_id or the provided
/// tab position (in that order)
pub fn switch_session_with_focus(
@ -664,6 +676,7 @@ pub fn switch_session_with_focus(
name: Some(name.to_owned()),
tab_position,
pane_id,
..Default::default()
});
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
@ -726,6 +739,26 @@ pub fn pipe_message_to_plugin(message_to_plugin: MessageToPlugin) {
unsafe { host_run_plugin_command() };
}
/// Disconnect all other clients from the current session
pub fn disconnect_other_clients() {
let plugin_command = PluginCommand::DisconnectOtherClients;
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
/// Kill all Zellij sessions in the list
pub fn kill_sessions<S: AsRef<str>>(session_names: &[S])
where
S: ToString,
{
let plugin_command =
PluginCommand::KillSessions(session_names.into_iter().map(|s| s.to_string()).collect());
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions
#[allow(unused)]

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,
#[prost(bool, tag = "5")]
pub is_current_session: bool,
#[prost(message, repeated, tag = "6")]
pub available_layouts: ::prost::alloc::vec::Vec<LayoutInfo>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct LayoutInfo {
#[prost(string, tag = "1")]
pub name: ::prost::alloc::string::String,
#[prost(string, tag = "2")]
pub source: ::prost::alloc::string::String,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]

View file

@ -5,7 +5,7 @@ pub struct PluginCommand {
pub name: i32,
#[prost(
oneof = "plugin_command::Payload",
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50"
tags = "2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 60"
)]
pub payload: ::core::option::Option<plugin_command::Payload>,
}
@ -112,10 +112,18 @@ pub mod plugin_command {
CliPipeOutputPayload(super::CliPipeOutputPayload),
#[prost(message, tag = "50")]
MessageToPluginPayload(super::MessageToPluginPayload),
#[prost(message, tag = "60")]
KillSessionsPayload(super::KillSessionsPayload),
}
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct KillSessionsPayload {
#[prost(string, repeated, tag = "1")]
pub session_names: ::prost::alloc::vec::Vec<::prost::alloc::string::String>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
pub struct CliPipeOutputPayload {
#[prost(string, tag = "1")]
pub pipe_name: ::prost::alloc::string::String,
@ -171,6 +179,8 @@ pub struct SwitchSessionPayload {
pub pane_id: ::core::option::Option<u32>,
#[prost(bool, optional, tag = "4")]
pub pane_id_is_plugin: ::core::option::Option<bool>,
#[prost(message, optional, tag = "5")]
pub layout: ::core::option::Option<super::event::LayoutInfo>,
}
#[allow(clippy::derive_partial_eq_without_eq)]
#[derive(Clone, PartialEq, ::prost::Message)]
@ -376,6 +386,8 @@ pub enum CommandName {
BlockCliPipeInput = 77,
CliPipeOutput = 78,
MessageToPlugin = 79,
DisconnectOtherClients = 80,
KillSessions = 81,
}
impl CommandName {
/// String value of the enum field names used in the ProtoBuf definition.
@ -464,6 +476,8 @@ impl CommandName {
CommandName::BlockCliPipeInput => "BlockCliPipeInput",
CommandName::CliPipeOutput => "CliPipeOutput",
CommandName::MessageToPlugin => "MessageToPlugin",
CommandName::DisconnectOtherClients => "DisconnectOtherClients",
CommandName::KillSessions => "KillSessions",
}
}
/// Creates an enum from field names used in the ProtoBuf definition.
@ -549,6 +563,8 @@ impl CommandName {
"BlockCliPipeInput" => Some(Self::BlockCliPipeInput),
"CliPipeOutput" => Some(Self::CliPipeOutput),
"MessageToPlugin" => Some(Self::MessageToPlugin),
"DisconnectOtherClients" => Some(Self::DisconnectOtherClients),
"KillSessions" => Some(Self::KillSessions),
_ => None,
}
}

View file

@ -765,6 +765,28 @@ pub struct SessionInfo {
pub panes: PaneManifest,
pub connected_clients: usize,
pub is_current_session: bool,
pub available_layouts: Vec<LayoutInfo>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub enum LayoutInfo {
BuiltIn(String),
File(String),
}
impl LayoutInfo {
pub fn name(&self) -> &str {
match self {
LayoutInfo::BuiltIn(name) => &name,
LayoutInfo::File(name) => &name,
}
}
pub fn is_builtin(&self) -> bool {
match self {
LayoutInfo::BuiltIn(_name) => true,
LayoutInfo::File(_name) => false,
}
}
}
use std::hash::{Hash, Hasher};
@ -1032,12 +1054,12 @@ impl MessageToPlugin {
self
}
pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self {
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.should_float = Some(should_float);
self
}
pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self {
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.pane_id_to_replace = Some(pane_id);
self
}
@ -1045,17 +1067,17 @@ impl MessageToPlugin {
mut self,
pane_title: impl Into<String>,
) -> Self {
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.pane_title = Some(pane_title.into());
self
}
pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self {
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.cwd = Some(cwd);
self
}
pub fn new_plugin_instance_should_skip_cache(mut self) -> Self {
let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default);
new_plugin_args.skip_cache = true;
self
}
@ -1066,6 +1088,17 @@ pub struct ConnectToSession {
pub name: Option<String>,
pub tab_position: Option<usize>,
pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
pub layout: Option<LayoutInfo>,
}
impl ConnectToSession {
pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) {
if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() {
*file_path = Path::join(layout_dir, &file_path)
.to_string_lossy()
.to_string();
}
}
}
#[derive(Debug, Default, Clone)]
@ -1228,4 +1261,6 @@ pub enum PluginCommand {
BlockCliPipeInput(String), // String => pipe name
CliPipeOutput(String, String), // String => pipe name, String => output
MessageToPlugin(MessageToPlugin),
DisconnectOtherClients,
KillSessions(Vec<String>), // one or more session names
}

View file

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

View file

@ -222,6 +222,15 @@ impl Config {
Err(e) => Err(ConfigError::IoPath(e, path.into())),
}
}
pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> {
self.options = self.options.merge(other.options);
self.keybinds.merge(other.keybinds.clone());
self.themes = self.themes.merge(other.themes);
self.plugins = self.plugins.merge(other.plugins);
self.ui = self.ui.merge(other.ui);
self.env = self.env.merge(other.env);
Ok(())
}
}
#[cfg(test)]

View file

@ -65,6 +65,17 @@ impl Keybinds {
}
ret
}
pub fn merge(&mut self, mut other: Keybinds) {
for (other_input_mode, mut other_input_mode_keybinds) in other.0.drain() {
let input_mode_keybinds = self
.0
.entry(other_input_mode)
.or_insert_with(|| Default::default());
for (other_action, other_action_keybinds) in other_input_mode_keybinds.drain() {
input_mode_keybinds.insert(other_action, other_action_keybinds);
}
}
}
}
// The unit test location.

View file

@ -9,8 +9,8 @@
// If plugins should be able to depend on the layout system
// then [`zellij-utils`] could be a proper place.
use crate::{
data::Direction,
home::find_default_config_dir,
data::{Direction, LayoutInfo},
home::{default_layout_dir, find_default_config_dir},
input::{
command::RunCommand,
config::{Config, ConfigError},
@ -19,6 +19,7 @@ use crate::{
setup::{self},
};
use std::cmp::Ordering;
use std::fmt::{Display, Formatter};
use std::str::FromStr;
@ -819,6 +820,62 @@ impl Default for LayoutParts {
}
impl Layout {
// the first layout will either be the default one
pub fn list_available_layouts(
layout_dir: Option<PathBuf>,
default_layout_name: &Option<String>,
) -> Vec<LayoutInfo> {
let mut available_layouts = layout_dir
.clone()
.or_else(|| default_layout_dir())
.and_then(|layout_dir| match std::fs::read_dir(layout_dir) {
Ok(layout_files) => Some(layout_files),
Err(e) => {
log::error!("Failed to read layout dir: {:?}", e);
None
},
})
.map(|layout_files| {
let mut available_layouts = vec![];
for file in layout_files {
if let Ok(file) = file {
if Layout::from_path_or_default_without_config(
Some(&file.path()),
layout_dir.clone(),
)
.is_ok()
{
if let Some(file_name) = file.path().file_stem() {
available_layouts
.push(LayoutInfo::File(file_name.to_string_lossy().to_string()))
}
}
}
}
available_layouts
})
.unwrap_or_else(Default::default);
let default_layout_name = default_layout_name
.as_ref()
.map(|d| d.as_str())
.unwrap_or("default");
available_layouts.push(LayoutInfo::BuiltIn("default".to_owned()));
available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned()));
available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned()));
available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned()));
available_layouts.sort_by(|a, b| {
let a_name = a.name();
let b_name = b.name();
if a_name == default_layout_name {
return Ordering::Less;
} else if b_name == default_layout_name {
return Ordering::Greater;
} else {
a_name.cmp(&b_name)
}
});
available_layouts
}
pub fn stringified_from_path_or_default(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
@ -861,6 +918,40 @@ impl Layout {
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config))
}
pub fn from_path_or_default_without_config(
layout_path: Option<&PathBuf>,
layout_dir: Option<PathBuf>,
) -> Result<Layout, ConfigError> {
let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
Layout::stringified_from_path_or_default(layout_path, layout_dir)?;
let layout = Layout::from_kdl(
&raw_layout,
path_to_raw_layout,
raw_swap_layouts
.as_ref()
.map(|(r, f)| (r.as_str(), f.as_str())),
None,
)?;
Ok(layout)
}
pub fn from_default_assets(
layout_name: &Path,
_layout_dir: Option<PathBuf>,
config: Config,
) -> Result<(Layout, Config), ConfigError> {
let (path_to_raw_layout, raw_layout, raw_swap_layouts) =
Layout::stringified_from_default_assets(layout_name)?;
let layout = Layout::from_kdl(
&raw_layout,
path_to_raw_layout,
raw_swap_layouts
.as_ref()
.map(|(r, f)| (r.as_str(), f.as_str())),
None,
)?;
let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with
Ok((layout, config))
}
pub fn from_str(
raw: &str,
path_to_raw_layout: String,
@ -951,6 +1042,11 @@ impl Layout {
Self::stringified_compact_swap_from_assets()?,
)),
)),
Some("welcome") => Ok((
"Welcome screen layout".into(),
Self::stringified_welcome_from_assets()?,
None,
)),
None | Some(_) => Err(ConfigError::IoPath(
std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"),
path.into(),
@ -982,6 +1078,10 @@ impl Layout {
Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?)
}
pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> {
Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?)
}
pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) {
self.template.clone().unwrap_or_default()
}
@ -1024,24 +1124,10 @@ impl Layout {
swap_layout_path.as_os_str().to_string_lossy().into(),
swap_kdl_layout,
)),
Err(e) => {
log::warn!(
"Failed to read swap layout file: {}. Error: {:?}",
swap_layout_path.as_os_str().to_string_lossy(),
e
);
None
},
Err(_e) => None,
}
},
Err(e) => {
log::warn!(
"Failed to read swap layout file: {}. Error: {:?}",
swap_layout_path.as_os_str().to_string_lossy(),
e
);
None
},
Err(_e) => None,
}
}
}

View file

@ -1,7 +1,7 @@
mod kdl_layout_parser;
use crate::data::{
Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType,
Resize, SessionInfo, TabInfo,
Direction, InputMode, Key, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest,
PermissionType, Resize, SessionInfo, TabInfo,
};
use crate::envs::EnvironmentVariables;
use crate::home::{find_default_config_dir, get_layout_dir};
@ -1986,6 +1986,31 @@ impl SessionInfo {
.and_then(|p| p.children())
.map(|p| PaneManifest::decode_from_kdl(p))
.ok_or("Failed to parse panes")?;
let available_layouts: Vec<LayoutInfo> = kdl_document
.get("available_layouts")
.and_then(|p| p.children())
.map(|e| {
e.nodes()
.iter()
.filter_map(|n| {
let layout_name = n.name().value().to_owned();
let layout_source = n
.entries()
.iter()
.find(|e| e.name().map(|n| n.value()) == Some("source"))
.and_then(|e| e.value().as_string());
match layout_source {
Some(layout_source) => match layout_source {
"built-in" => Some(LayoutInfo::BuiltIn(layout_name)),
"file" => Some(LayoutInfo::File(layout_name)),
_ => None,
},
None => None,
}
})
.collect()
})
.ok_or("Failed to parse available_layouts")?;
let is_current_session = name == current_session_name;
Ok(SessionInfo {
name,
@ -1993,6 +2018,7 @@ impl SessionInfo {
panes,
connected_clients,
is_current_session,
available_layouts,
})
}
pub fn to_string(&self) -> String {
@ -2017,10 +2043,25 @@ impl SessionInfo {
let mut panes = KdlNode::new("panes");
panes.set_children(self.panes.encode_to_kdl());
let mut available_layouts = KdlNode::new("available_layouts");
let mut available_layouts_children = KdlDocument::new();
for layout_info in &self.available_layouts {
let (layout_name, layout_source) = match layout_info {
LayoutInfo::File(name) => (name.clone(), "file"),
LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"),
};
let mut layout_node = KdlNode::new(format!("{}", layout_name));
let layout_source = KdlEntry::new_prop("source", layout_source);
layout_node.entries_mut().push(layout_source);
available_layouts_children.nodes_mut().push(layout_node);
}
available_layouts.set_children(available_layouts_children);
kdl_document.nodes_mut().push(name);
kdl_document.nodes_mut().push(tabs);
kdl_document.nodes_mut().push(panes);
kdl_document.nodes_mut().push(connected_clients);
kdl_document.nodes_mut().push(available_layouts);
kdl_document.fmt();
kdl_document.to_string()
}
@ -2506,6 +2547,11 @@ fn serialize_and_deserialize_session_info_with_data() {
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: false,
available_layouts: vec![
LayoutInfo::File("layout1".to_owned()),
LayoutInfo::BuiltIn("layout2".to_owned()),
LayoutInfo::File("layout3".to_owned()),
],
};
let serialized = session_info.to_string();
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();

View file

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

View file

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

View file

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

View file

@ -4,9 +4,9 @@ pub use super::generated_api::api::{
event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination,
Event as ProtobufEvent, EventNameList as ProtobufEventNameList,
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
ResurrectableSession as ProtobufResurrectableSession,
KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo,
ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo,
PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
@ -14,8 +14,8 @@ pub use super::generated_api::api::{
style::Style as ProtobufStyle,
};
use crate::data::{
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo,
PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
};
use crate::errors::prelude::*;
@ -453,6 +453,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest {
.collect(),
connected_clients: session_info.connected_clients as u32,
is_current_session: session_info.is_current_session,
available_layouts: session_info
.available_layouts
.into_iter()
.filter_map(|l| ProtobufLayoutInfo::try_from(l).ok())
.collect(),
})
}
}
@ -485,10 +490,42 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo {
panes,
connected_clients: protobuf_session_manifest.connected_clients as usize,
is_current_session: protobuf_session_manifest.is_current_session,
available_layouts: protobuf_session_manifest
.available_layouts
.into_iter()
.filter_map(|l| LayoutInfo::try_from(l).ok())
.collect(),
})
}
}
impl TryFrom<LayoutInfo> for ProtobufLayoutInfo {
type Error = &'static str;
fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> {
match layout_info {
LayoutInfo::File(name) => Ok(ProtobufLayoutInfo {
source: "file".to_owned(),
name,
}),
LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo {
source: "built-in".to_owned(),
name,
}),
}
}
}
impl TryFrom<ProtobufLayoutInfo> for LayoutInfo {
type Error = &'static str;
fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> {
match protobuf_layout_info.source.as_str() {
"file" => Ok(LayoutInfo::File(protobuf_layout_info.name)),
"built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)),
_ => Err("Unknown source for layout"),
}
}
}
impl TryFrom<CopyDestination> for ProtobufCopyDestination {
type Error = &'static str;
fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> {
@ -1383,6 +1420,11 @@ fn serialize_session_update_event_with_non_default_values() {
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: true,
available_layouts: vec![
LayoutInfo::File("layout 1".to_owned()),
LayoutInfo::BuiltIn("layout2".to_owned()),
LayoutInfo::File("layout3".to_owned()),
],
};
let session_info_2 = SessionInfo {
name: "session 2".to_owned(),
@ -1392,6 +1434,11 @@ fn serialize_session_update_event_with_non_default_values() {
},
connected_clients: 0,
is_current_session: false,
available_layouts: vec![
LayoutInfo::File("layout 1".to_owned()),
LayoutInfo::BuiltIn("layout2".to_owned()),
LayoutInfo::File("layout3".to_owned()),
],
};
let session_infos = vec![session_info_1, session_info_2];
let resurrectable_sessions = vec![];

View file

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

View file

@ -4,10 +4,10 @@ pub use super::generated_api::api::{
input_mode::InputMode as ProtobufInputMode,
plugin_command::{
plugin_command::Payload, CliPipeOutputPayload, CommandName, ContextItem, EnvVariable,
ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, MessageToPluginPayload,
MovePayload, NewPluginArgs as ProtobufNewPluginArgs, OpenCommandPanePayload,
OpenFilePayload, PaneId as ProtobufPaneId, PaneType as ProtobufPaneType,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
ExecCmdPayload, HttpVerb as ProtobufHttpVerb, IdAndNewName, KillSessionsPayload,
MessageToPluginPayload, MovePayload, NewPluginArgs as ProtobufNewPluginArgs,
OpenCommandPanePayload, OpenFilePayload, PaneId as ProtobufPaneId,
PaneType as ProtobufPaneType, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, RunCommandPayload, SetTimeoutPayload,
SubscribePayload, SwitchSessionPayload, SwitchTabToPayload, UnsubscribePayload,
WebRequestPayload,
@ -574,6 +574,7 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
name: payload.name,
tab_position: payload.tab_position.map(|p| p as usize),
pane_id,
layout: payload.layout.and_then(|l| l.try_into().ok()),
}))
},
_ => Err("Mismatched payload for SwitchSession"),
@ -727,6 +728,16 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}),
}))
},
_ => Err("Mismatched payload for MessageToPlugin"),
},
Some(CommandName::DisconnectOtherClients) => match protobuf_plugin_command.payload {
None => Ok(PluginCommand::DisconnectOtherClients),
_ => Err("Mismatched payload for DisconnectOtherClients"),
},
Some(CommandName::KillSessions) => match protobuf_plugin_command.payload {
Some(Payload::KillSessionsPayload(KillSessionsPayload { session_names })) => {
Ok(PluginCommand::KillSessions(session_names))
},
_ => Err("Mismatched payload for PipeOutput"),
},
None => Err("Unrecognized plugin command"),
@ -1082,6 +1093,7 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
tab_position: switch_to_session.tab_position.map(|t| t as u32),
pane_id: switch_to_session.pane_id.map(|p| p.0),
pane_id_is_plugin: switch_to_session.pane_id.map(|p| p.1),
layout: switch_to_session.layout.and_then(|l| l.try_into().ok()),
})),
}),
PluginCommand::OpenTerminalInPlace(cwd) => Ok(ProtobufPluginCommand {
@ -1205,6 +1217,16 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
})),
})
},
PluginCommand::DisconnectOtherClients => Ok(ProtobufPluginCommand {
name: CommandName::DisconnectOtherClients as i32,
payload: None,
}),
PluginCommand::KillSessions(session_names) => Ok(ProtobufPluginCommand {
name: CommandName::KillSessions as i32,
payload: Some(Payload::KillSessionsPayload(KillSessionsPayload {
session_names,
})),
}),
}
}
}

View file

@ -167,6 +167,12 @@ pub const COMPACT_BAR_SWAP_LAYOUT: &[u8] = include_bytes!(concat!(
"assets/layouts/compact.swap.kdl"
));
pub const WELCOME_LAYOUT: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
"assets/layouts/welcome.kdl"
));
pub const FISH_EXTRA_COMPLETION: &[u8] = include_bytes!(concat!(
env!("CARGO_MANIFEST_DIR"),
"/",
@ -345,7 +351,9 @@ impl Setup {
/// 2. layout options
/// (`layout.kdl` / `zellij --layout`)
/// 3. config options (`config.kdl`)
pub fn from_cli_args(cli_args: &CliArgs) -> Result<(Config, Layout, Options), ConfigError> {
pub fn from_cli_args(
cli_args: &CliArgs,
) -> Result<(Config, Layout, Options, Config, Options), ConfigError> {
// note that this can potentially exit the process
Setup::handle_setup_commands(cli_args);
let config = Config::try_from(cli_args)?;
@ -355,8 +363,19 @@ impl Setup {
} else {
None
};
let mut config_without_layout = config.clone();
let (layout, mut config) =
Setup::parse_layout_and_override_config(cli_config_options.as_ref(), config, cli_args)?;
let config_options =
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(),
@ -371,6 +390,8 @@ impl Setup {
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 {
setup
@ -383,7 +404,13 @@ impl Setup {
|_| {},
);
};
Ok((config, layout, config_options))
Ok((
config,
layout,
config_options,
config_without_layout,
config_options_without_layout,
))
}
/// General setup helpers
@ -667,7 +694,7 @@ mod setup_test {
#[test]
fn default_config_with_no_cli_arguments() {
let cli_args = CliArgs::default();
let (config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
assert_snapshot!(format!("{:#?}", layout));
assert_snapshot!(format!("{:#?}", options));
@ -682,7 +709,7 @@ mod setup_test {
},
..Default::default()
}));
let (_config, _layout, options) = Setup::from_cli_args(&cli_args).unwrap();
let (_config, _layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
}
#[test]
@ -692,7 +719,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-options.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout));
}
@ -710,7 +737,7 @@ mod setup_test {
},
..Default::default()
}));
let (_config, layout, options) = Setup::from_cli_args(&cli_args).unwrap();
let (_config, layout, options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", options));
assert_snapshot!(format!("{:#?}", layout));
}
@ -725,7 +752,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-env-vars.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@ -739,7 +766,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-ui-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@ -753,7 +780,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-plugins-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@ -767,7 +794,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-themes-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
#[test]
@ -781,7 +808,7 @@ mod setup_test {
"{}/src/test-fixtures/layout-with-keybindings-config.kdl",
env!("CARGO_MANIFEST_DIR")
)));
let (config, _layout, _options) = Setup::from_cli_args(&cli_args).unwrap();
let (config, _layout, _options, _, _) = Setup::from_cli_args(&cli_args).unwrap();
assert_snapshot!(format!("{:#?}", config));
}
}