feat(sessions): add a session manager to switch between sessions, tabs and panes and create new ones (#2721)

* write/read session metadata to disk for all sessions

* switch session client side

* fix tests

* various adjustments

* fix full screen focus bug in tiled panes

* fix tests

* fix permission sorting issue

* cleanups

* add session manager

* fix tests

* various cleanups

* style(fmt): rustfmt

* clear screen before switching sessions

* I hate you clippy

* truncate controls line to width

* version session cache

* attempt to fix plugin tests

* style(fmt): rustfmt

* another attempt to fix the tests in the ci
This commit is contained in:
Aram Drevekenin 2023-08-24 13:36:24 +02:00 committed by GitHub
parent bf3c072d6d
commit bc628abc12
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
54 changed files with 6397 additions and 370 deletions

15
Cargo.lock generated
View file

@ -2868,6 +2868,17 @@ dependencies = [
"yaml-rust", "yaml-rust",
] ]
[[package]]
name = "session-manager"
version = "0.1.0"
dependencies = [
"ansi_term",
"chrono",
"fuzzy-matcher",
"unicode-width",
"zellij-tile",
]
[[package]] [[package]]
name = "sha-1" name = "sha-1"
version = "0.8.2" version = "0.8.2"
@ -3596,9 +3607,9 @@ checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
[[package]] [[package]]
name = "unsafe-any-ors" name = "unsafe-any-ors"

View file

@ -35,6 +35,7 @@ members = [
"default-plugins/strider", "default-plugins/strider",
"default-plugins/tab-bar", "default-plugins/tab-bar",
"default-plugins/fixture-plugin-for-tests", "default-plugins/fixture-plugin-for-tests",
"default-plugins/session-manager",
"zellij-client", "zellij-client",
"zellij-server", "zellij-server",
"zellij-utils", "zellij-utils",

View file

@ -0,0 +1,2 @@
[build]
target = "wasm32-wasi"

View file

@ -0,0 +1 @@
/target

2809
default-plugins/session-manager/Cargo.lock generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,12 @@
[package]
name = "session-manager"
version = "0.1.0"
authors = ["Aram Drevekenin <aram@poor.dev>"]
edition = "2018"
[dependencies]
ansi_term = "0.12.1"
zellij-tile = { path = "../../zellij-tile" }
chrono = "0.4.0"
fuzzy-matcher = "0.3.7"
unicode-width = "0.1.10"

View file

@ -0,0 +1,208 @@
mod session_list;
mod ui;
use zellij_tile::prelude::*;
use std::collections::BTreeMap;
use ui::{
components::{render_controls_line, render_new_session_line, render_prompt, Colors},
SessionUiInfo,
};
use session_list::SessionList;
#[derive(Default)]
struct State {
session_name: Option<String>,
sessions: SessionList,
selected_index: Option<usize>,
search_term: String,
new_session_name: Option<String>,
colors: Colors,
}
register_plugin!(State);
impl ZellijPlugin for State {
fn load(&mut self, _configuration: BTreeMap<String, String>) {
subscribe(&[
EventType::ModeUpdate,
EventType::SessionUpdate,
EventType::Key,
]);
}
fn update(&mut self, event: Event) -> bool {
let mut should_render = false;
match event {
Event::ModeUpdate(mode_info) => {
self.colors = Colors::new(mode_info.style.colors);
should_render = true;
},
Event::Key(key) => {
should_render = self.handle_key(key);
},
Event::PermissionRequestResult(_result) => {
should_render = true;
},
Event::SessionUpdate(session_infos) => {
self.update_session_infos(session_infos);
should_render = true;
},
_ => (),
};
should_render
}
fn render(&mut self, rows: usize, cols: usize) {
render_prompt(
self.new_session_name.is_some(),
&self.search_term,
self.colors,
);
let room_for_list = rows.saturating_sub(5); // search line and controls
self.sessions.update_rows(room_for_list);
let list = self
.sessions
.render(room_for_list, cols.saturating_sub(7), self.colors); // 7 for various ui
for line in list {
println!("{}", line.render());
}
render_new_session_line(
&self.new_session_name,
self.sessions.is_searching,
self.colors,
);
render_controls_line(self.sessions.is_searching, rows, cols, self.colors);
}
}
impl State {
fn reset_selected_index(&mut self) {
self.selected_index = None;
}
fn handle_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.new_session_name.is_none() {
self.sessions.move_selection_down();
}
should_render = true;
} else if let Key::Up = key {
if self.new_session_name.is_none() {
self.sessions.move_selection_up();
}
should_render = true;
} else if let Key::Char(character) = key {
if character == '\n' {
self.handle_selection();
} else if let Some(new_session_name) = self.new_session_name.as_mut() {
new_session_name.push(character);
} else {
self.search_term.push(character);
self.sessions
.update_search_term(&self.search_term, &self.colors);
}
should_render = true;
} else if let Key::Backspace = 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.pop();
}
} else {
self.search_term.pop();
self.sessions
.update_search_term(&self.search_term, &self.colors);
}
should_render = true;
} else if let Key::Ctrl('w') = key {
if self.sessions.is_searching {
// no-op
} else if self.new_session_name.is_some() {
self.new_session_name = None;
} else {
self.new_session_name = Some(String::new());
}
should_render = true;
} 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 !self.search_term.is_empty() {
self.search_term.clear();
self.sessions
.update_search_term(&self.search_term, &self.colors);
self.reset_selected_index();
} else {
self.reset_selected_index();
hide_self();
}
should_render = true;
} else if let Key::Esc = key {
hide_self();
}
should_render
}
fn handle_selection(&mut self) {
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(selected_session_name) = self.sessions.get_selected_session_name() {
let selected_tab = self.sessions.get_selected_tab_position();
let selected_pane = self.sessions.get_selected_pane_id();
let is_current_session = self.sessions.selected_is_current_session();
if is_current_session {
if let Some((pane_id, is_plugin)) = selected_pane {
if is_plugin {
focus_plugin_pane(pane_id, true);
} else {
focus_terminal_pane(pane_id, true);
}
} else if let Some(tab_position) = selected_tab {
go_to_tab(tab_position as u32);
}
} else {
switch_session_with_focus(&selected_session_name, selected_tab, selected_pane);
}
}
hide_self();
}
fn update_session_infos(&mut self, session_infos: Vec<SessionInfo>) {
let session_infos: Vec<SessionUiInfo> = session_infos
.iter()
.map(|s| SessionUiInfo::from_session_info(s))
.collect();
let current_session_name = session_infos.iter().find_map(|s| {
if s.is_current_session {
Some(s.name.clone())
} else {
None
}
});
if let Some(current_session_name) = current_session_name {
self.session_name = Some(current_session_name);
}
self.sessions.set_sessions(session_infos);
}
}

View file

@ -0,0 +1,381 @@
use fuzzy_matcher::skim::SkimMatcherV2;
use fuzzy_matcher::FuzzyMatcher;
use crate::ui::{
components::{Colors, LineToRender, ListItem},
SessionUiInfo,
};
#[derive(Debug, Default)]
pub struct SessionList {
pub session_ui_infos: Vec<SessionUiInfo>,
pub selected_index: SelectedIndex,
pub selected_search_index: Option<usize>,
pub search_results: Vec<SearchResult>,
pub is_searching: bool,
}
impl SessionList {
pub fn set_sessions(&mut self, mut session_ui_infos: Vec<SessionUiInfo>) {
session_ui_infos.sort_unstable_by(|a, b| {
if a.is_current_session {
std::cmp::Ordering::Less
} else if b.is_current_session {
std::cmp::Ordering::Greater
} else {
a.name.cmp(&b.name)
}
});
self.session_ui_infos = session_ui_infos;
}
pub fn update_search_term(&mut self, search_term: &str, colors: &Colors) {
let mut flattened_assets = self.flatten_assets(colors);
let mut matches = vec![];
let matcher = SkimMatcherV2::default().use_cache(true);
for (list_item, session_name, tab_position, pane_id, is_current_session) in
flattened_assets.drain(..)
{
if let Some((score, indices)) = matcher.fuzzy_indices(&list_item.name, &search_term) {
matches.push(SearchResult::new(
score,
indices,
list_item,
session_name,
tab_position,
pane_id,
is_current_session,
));
}
}
matches.sort_by(|a, b| b.score.cmp(&a.score));
self.search_results = matches;
self.is_searching = !search_term.is_empty();
self.selected_search_index = Some(0);
}
fn flatten_assets(
&self,
colors: &Colors,
) -> Vec<(ListItem, String, Option<usize>, Option<(u32, bool)>, bool)> {
// list_item, session_name, tab_position, (pane_id, is_plugin), is_current_session
let mut list_items = vec![];
for session in &self.session_ui_infos {
let session_name = session.name.clone();
let is_current_session = session.is_current_session;
list_items.push((
ListItem::from_session_info(session, *colors),
session_name.clone(),
None,
None,
is_current_session,
));
for tab in &session.tabs {
let tab_position = tab.position;
list_items.push((
ListItem::from_tab_info(session, tab, *colors),
session_name.clone(),
Some(tab_position),
None,
is_current_session,
));
for pane in &tab.panes {
let pane_id = (pane.pane_id, pane.is_plugin);
list_items.push((
ListItem::from_pane_info(session, tab, pane, *colors),
session_name.clone(),
Some(tab_position),
Some(pane_id),
is_current_session,
));
}
}
}
list_items
}
pub fn get_selected_session_name(&self) -> Option<String> {
if self.is_searching {
self.selected_search_index
.and_then(|i| self.search_results.get(i))
.map(|s| s.session_name.clone())
} else {
self.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.map(|s_i| s_i.name.clone())
}
}
pub fn selected_is_current_session(&self) -> bool {
if self.is_searching {
self.selected_search_index
.and_then(|i| self.search_results.get(i))
.map(|s| s.is_current_session)
.unwrap_or(false)
} else {
self.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.map(|s_i| s_i.is_current_session)
.unwrap_or(false)
}
}
pub fn get_selected_tab_position(&self) -> Option<usize> {
if self.is_searching {
self.selected_search_index
.and_then(|i| self.search_results.get(i))
.and_then(|s| s.tab_position)
} else {
self.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.and_then(|s_i| {
self.selected_index
.1
.and_then(|i| s_i.tabs.get(i))
.map(|t| t.position)
})
}
}
pub fn get_selected_pane_id(&self) -> Option<(u32, bool)> {
// (pane_id, is_plugin)
if self.is_searching {
self.selected_search_index
.and_then(|i| self.search_results.get(i))
.and_then(|s| s.pane_id)
} else {
self.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.and_then(|s_i| {
self.selected_index
.1
.and_then(|i| s_i.tabs.get(i))
.and_then(|t| {
self.selected_index
.2
.and_then(|i| t.panes.get(i))
.map(|p| (p.pane_id, p.is_plugin))
})
})
}
}
pub fn move_selection_down(&mut self) {
if self.is_searching {
match self.selected_search_index.as_mut() {
Some(search_index) => {
*search_index = search_index.saturating_add(1);
},
None => {
if !self.search_results.is_empty() {
self.selected_search_index = Some(0);
}
},
}
} else {
match self.selected_index {
SelectedIndex(None, None, None) => {
if !self.session_ui_infos.is_empty() {
self.selected_index.0 = Some(0);
}
},
SelectedIndex(Some(selected_session), None, None) => {
if self.session_ui_infos.len() > selected_session + 1 {
self.selected_index.0 = Some(selected_session + 1);
} else {
self.selected_index.0 = None;
self.selected_index.1 = None;
self.selected_index.2 = None;
}
},
SelectedIndex(Some(selected_session), Some(selected_tab), None) => {
if self
.get_session(selected_session)
.map(|s| s.tabs.len() > selected_tab + 1)
.unwrap_or(false)
{
self.selected_index.1 = Some(selected_tab + 1);
} else {
self.selected_index.1 = Some(0);
}
},
SelectedIndex(Some(selected_session), Some(selected_tab), Some(selected_pane)) => {
if self
.get_session(selected_session)
.and_then(|s| s.tabs.get(selected_tab))
.map(|t| t.panes.len() > selected_pane + 1)
.unwrap_or(false)
{
self.selected_index.2 = Some(selected_pane + 1);
} else {
self.selected_index.2 = Some(0);
}
},
_ => {},
}
}
}
pub fn move_selection_up(&mut self) {
if self.is_searching {
match self.selected_search_index.as_mut() {
Some(search_index) => {
*search_index = search_index.saturating_sub(1);
},
None => {
if !self.search_results.is_empty() {
self.selected_search_index = Some(0);
}
},
}
} else {
match self.selected_index {
SelectedIndex(None, None, None) => {
if !self.session_ui_infos.is_empty() {
self.selected_index.0 = Some(self.session_ui_infos.len().saturating_sub(1))
}
},
SelectedIndex(Some(selected_session), None, None) => {
if selected_session > 0 {
self.selected_index.0 = Some(selected_session - 1);
} else {
self.selected_index.0 = None;
}
},
SelectedIndex(Some(selected_session), Some(selected_tab), None) => {
if selected_tab > 0 {
self.selected_index.1 = Some(selected_tab - 1);
} else {
let tab_count = self
.get_session(selected_session)
.map(|s| s.tabs.len())
.unwrap_or(0);
self.selected_index.1 = Some(tab_count.saturating_sub(1))
}
},
SelectedIndex(Some(selected_session), Some(selected_tab), Some(selected_pane)) => {
if selected_pane > 0 {
self.selected_index.2 = Some(selected_pane - 1);
} else {
let pane_count = self
.get_session(selected_session)
.and_then(|s| s.tabs.get(selected_tab))
.map(|t| t.panes.len())
.unwrap_or(0);
self.selected_index.2 = Some(pane_count.saturating_sub(1))
}
},
_ => {},
}
}
}
fn get_session(&self, index: usize) -> Option<&SessionUiInfo> {
self.session_ui_infos.get(index)
}
pub fn result_expand(&mut self) {
// we can't move this to SelectedIndex because the borrow checker is mean
match self.selected_index {
SelectedIndex(Some(selected_session), None, None) => {
let selected_session_has_tabs = self
.get_session(selected_session)
.map(|s| !s.tabs.is_empty())
.unwrap_or(false);
if selected_session_has_tabs {
self.selected_index.1 = Some(0);
}
},
SelectedIndex(Some(selected_session), Some(selected_tab), None) => {
let selected_tab_has_panes = self
.get_session(selected_session)
.and_then(|s| s.tabs.get(selected_tab))
.map(|t| !t.panes.is_empty())
.unwrap_or(false);
if selected_tab_has_panes {
self.selected_index.2 = Some(0);
}
},
_ => {},
}
}
pub fn result_shrink(&mut self) {
self.selected_index.result_shrink();
}
pub fn update_rows(&mut self, rows: usize) {
if let Some(search_result_rows_until_selected) = self.selected_search_index.map(|i| {
self.search_results
.iter()
.enumerate()
.take(i + 1)
.fold(0, |acc, s| acc + s.1.lines_to_render())
}) {
if search_result_rows_until_selected > rows
|| self.selected_search_index >= Some(self.search_results.len())
{
self.selected_search_index = None;
}
}
}
}
#[derive(Debug, Clone, Default)]
pub struct SelectedIndex(pub Option<usize>, pub Option<usize>, pub Option<usize>);
impl SelectedIndex {
pub fn tabs_are_visible(&self) -> bool {
self.1.is_some()
}
pub fn panes_are_visible(&self) -> bool {
self.2.is_some()
}
pub fn selected_tab_index(&self) -> Option<usize> {
self.1
}
pub fn session_index_is_selected(&self, index: usize) -> bool {
self.0 == Some(index)
}
pub fn result_shrink(&mut self) {
match self {
SelectedIndex(Some(_selected_session), None, None) => self.0 = None,
SelectedIndex(Some(_selected_session), Some(_selected_tab), None) => self.1 = None,
SelectedIndex(Some(_selected_session), Some(_selected_tab), Some(_selected_pane)) => {
self.2 = None
},
_ => {},
}
}
}
#[derive(Debug)]
pub struct SearchResult {
score: i64,
indices: Vec<usize>,
list_item: ListItem,
session_name: String,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
is_current_session: bool,
}
impl SearchResult {
pub fn new(
score: i64,
indices: Vec<usize>,
list_item: ListItem,
session_name: String,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
is_current_session: bool,
) -> Self {
SearchResult {
score,
indices,
list_item,
session_name,
tab_position,
pane_id,
is_current_session,
}
}
pub fn lines_to_render(&self) -> usize {
self.list_item.line_count()
}
pub fn render(&self, max_width: usize) -> Vec<LineToRender> {
self.list_item.render(Some(self.indices.clone()), max_width)
}
}

View file

@ -0,0 +1,577 @@
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
use zellij_tile::prelude::*;
use crate::ui::{PaneUiInfo, SessionUiInfo, TabUiInfo};
#[derive(Debug)]
pub struct ListItem {
pub name: String,
pub session_name: Option<Vec<UiSpan>>,
pub tab_name: Option<Vec<UiSpan>>,
pub pane_name: Option<Vec<UiSpan>>,
colors: Colors,
}
impl ListItem {
pub fn from_session_info(session_ui_info: &SessionUiInfo, colors: Colors) -> Self {
let session_ui_line = build_session_ui_line(session_ui_info, colors);
ListItem {
name: session_ui_info.name.clone(),
session_name: Some(session_ui_line),
tab_name: None,
pane_name: None,
colors,
}
}
pub fn from_tab_info(
session_ui_info: &SessionUiInfo,
tab_ui_info: &TabUiInfo,
colors: Colors,
) -> Self {
let session_ui_line = build_session_ui_line(session_ui_info, colors);
let tab_ui_line = build_tab_ui_line(tab_ui_info, colors);
ListItem {
name: tab_ui_info.name.clone(),
session_name: Some(session_ui_line),
tab_name: Some(tab_ui_line),
pane_name: None,
colors,
}
}
pub fn from_pane_info(
session_ui_info: &SessionUiInfo,
tab_ui_info: &TabUiInfo,
pane_ui_info: &PaneUiInfo,
colors: Colors,
) -> Self {
let session_ui_line = build_session_ui_line(session_ui_info, colors);
let tab_ui_line = build_tab_ui_line(tab_ui_info, colors);
let pane_ui_line = build_pane_ui_line(pane_ui_info, colors);
ListItem {
name: pane_ui_info.name.clone(),
session_name: Some(session_ui_line),
tab_name: Some(tab_ui_line),
pane_name: Some(pane_ui_line),
colors,
}
}
pub fn line_count(&self) -> usize {
let mut line_count = 0;
if self.session_name.is_some() {
line_count += 1
};
if self.tab_name.is_some() {
line_count += 1
};
if self.pane_name.is_some() {
line_count += 1
};
line_count
}
pub fn render(&self, indices: Option<Vec<usize>>, max_cols: usize) -> Vec<LineToRender> {
let mut lines_to_render = vec![];
if let Some(session_name) = &self.session_name {
let indices = if self.tab_name.is_none() && self.pane_name.is_none() {
indices.clone()
} else {
None
};
let mut line_to_render = LineToRender::new(self.colors);
let mut remaining_cols = max_cols;
for span in session_name {
span.render(
indices
.clone()
.map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
&mut line_to_render,
&mut remaining_cols,
);
}
lines_to_render.push(line_to_render);
}
if let Some(tab_name) = &self.tab_name {
let indices = if self.pane_name.is_none() {
indices.clone()
} else {
None
};
let mut line_to_render = LineToRender::new(self.colors);
let mut remaining_cols = max_cols;
for span in tab_name {
span.render(
indices
.clone()
.map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
&mut line_to_render,
&mut remaining_cols,
);
}
lines_to_render.push(line_to_render);
}
if let Some(pane_name) = &self.pane_name {
let mut line_to_render = LineToRender::new(self.colors);
let mut remaining_cols = max_cols;
for span in pane_name {
span.render(
indices
.clone()
.map(|i| (SpanStyle::ForegroundBold(self.colors.palette.magenta), i)),
&mut line_to_render,
&mut remaining_cols,
);
}
lines_to_render.push(line_to_render);
}
lines_to_render
}
}
#[derive(Debug)]
pub enum UiSpan {
UiSpanTelescope(UiSpanTelescope),
TruncatableUiSpan(TruncatableUiSpan),
}
impl UiSpan {
pub fn render(
&self,
indices: Option<(SpanStyle, Vec<usize>)>,
line_to_render: &mut LineToRender,
remaining_cols: &mut usize,
) {
match self {
UiSpan::UiSpanTelescope(ui_span_telescope) => {
ui_span_telescope.render(line_to_render, remaining_cols)
},
UiSpan::TruncatableUiSpan(truncatable_ui_span) => {
truncatable_ui_span.render(indices, line_to_render, remaining_cols)
},
}
}
}
#[allow(dead_code)] // in the future this will be moved to be its own component
#[derive(Debug)]
pub enum SpanStyle {
None,
Bold,
Foreground(PaletteColor),
ForegroundBold(PaletteColor),
}
impl SpanStyle {
pub fn style_string(&self, to_style: &str) -> String {
match self {
SpanStyle::None => to_style.to_owned(),
SpanStyle::Bold => format!("\u{1b}[1m{}\u{1b}[22m", to_style),
SpanStyle::Foreground(color) => match color {
PaletteColor::EightBit(byte) => {
format!("\u{1b}[38;5;{byte}m{}\u{1b}[39m", to_style)
},
PaletteColor::Rgb((r, g, b)) => {
format!("\u{1b}[38;2;{};{};{}m{}\u{1b}[39m", r, g, b, to_style)
},
},
SpanStyle::ForegroundBold(color) => match color {
PaletteColor::EightBit(byte) => {
format!("\u{1b}[38;5;{byte};1m{}\u{1b}[39;22m", to_style)
},
PaletteColor::Rgb((r, g, b)) => {
format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, to_style)
},
},
}
}
}
impl Default for SpanStyle {
fn default() -> Self {
SpanStyle::None
}
}
#[derive(Debug, Default)]
pub struct TruncatableUiSpan {
text: String,
style: SpanStyle,
}
impl TruncatableUiSpan {
pub fn new(text: String, style: SpanStyle) -> Self {
TruncatableUiSpan { text, style }
}
pub fn render(
&self,
indices: Option<(SpanStyle, Vec<usize>)>,
line_to_render: &mut LineToRender,
remaining_cols: &mut usize,
) {
let mut rendered = String::new();
let truncated = if *remaining_cols >= self.text.width() {
self.text.clone()
} else {
let mut truncated = String::new();
for character in self.text.chars() {
if truncated.width() + character.width().unwrap_or(0) <= *remaining_cols {
truncated.push(character);
} else {
break;
}
}
truncated
};
match indices {
Some((index_style, indices)) => {
for (i, character) in truncated.chars().enumerate() {
// TODO: optimize this by splitting the string up by its indices and only pushing those
// chu8nks
if indices.contains(&i) {
rendered.push_str(&index_style.style_string(&character.to_string()));
} else {
rendered.push_str(&self.style.style_string(&character.to_string()));
}
}
},
None => {
rendered.push_str(&self.style.style_string(&truncated));
},
}
*remaining_cols = remaining_cols.saturating_sub(truncated.width());
line_to_render.append(&rendered);
}
}
#[derive(Debug, Default)]
pub struct UiSpanTelescope(Vec<StringAndLength>);
impl UiSpanTelescope {
pub fn new(string_and_lengths: Vec<StringAndLength>) -> Self {
UiSpanTelescope(string_and_lengths)
}
pub fn render(&self, line_to_render: &mut LineToRender, remaining_cols: &mut usize) {
for string_and_length in &self.0 {
if string_and_length.length < *remaining_cols {
line_to_render.append(&string_and_length.string);
*remaining_cols -= string_and_length.length;
break;
}
}
}
}
#[derive(Debug, Default, Clone)]
pub struct StringAndLength {
pub string: String,
pub length: usize,
}
impl StringAndLength {
pub fn new(string: String, length: usize) -> Self {
StringAndLength { string, length }
}
}
#[derive(Debug, Clone)]
pub struct LineToRender {
line: String,
is_selected: bool,
truncated_result_count: usize,
colors: Colors,
}
impl LineToRender {
pub fn new(colors: Colors) -> Self {
LineToRender {
line: String::default(),
is_selected: false,
truncated_result_count: 0,
colors,
}
}
pub fn append(&mut self, to_append: &str) {
self.line.push_str(to_append)
}
pub fn make_selected(&mut self) {
self.is_selected = true;
match self.colors.palette.gray {
PaletteColor::EightBit(byte) => {
self.line = format!(
"\u{1b}[48;5;{byte}m\u{1b}[K\r\u{1b}[48;5;{byte}m{}",
self.line
);
},
PaletteColor::Rgb((r, g, b)) => {
self.line = format!(
"\u{1b}[48;2;{};{};{}m\u{1b}[K\r\u{1b}[48;5;{};{};{}m{}",
r, g, b, r, g, b, self.line
);
},
}
}
pub fn render(&self) -> String {
let mut line = self.line.clone();
let more = if self.truncated_result_count > 0 {
self.colors
.red(&format!(" [+{}]", self.truncated_result_count))
} else {
String::new()
};
line.push_str(&more);
if self.is_selected {
self.line.clone()
} else {
format!("\u{1b}[49m{}", line)
}
}
pub fn add_truncated_results(&mut self, result_count: usize) {
self.truncated_result_count += result_count;
}
}
pub fn build_session_ui_line(session_ui_info: &SessionUiInfo, colors: Colors) -> Vec<UiSpan> {
let mut ui_spans = vec![];
let tab_count_text = session_ui_info.tabs.len();
let total_pane_count_text = session_ui_info
.tabs
.iter()
.fold(0, |acc, tab| acc + tab.panes.len());
let tab_count = format!("{}", tab_count_text);
let tab_count_styled = colors.cyan(&tab_count);
let total_pane_count = format!("{}", total_pane_count_text);
let total_pane_count_styled = colors.green(&total_pane_count);
let session_name = &session_ui_info.name;
let connected_users = format!("{}", session_ui_info.connected_users);
let connected_users_styled = colors.orange(&connected_users);
let session_bullet_span =
UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
format!(" > "),
3,
)]));
let session_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(
session_name.clone(),
SpanStyle::ForegroundBold(colors.palette.orange),
));
let tab_and_pane_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
StringAndLength::new(
format!(" ({tab_count_styled} tabs, {total_pane_count_styled} panes)"),
2 + tab_count.width() + 7 + total_pane_count.width() + 7,
),
StringAndLength::new(
format!(" ({tab_count_styled}, {total_pane_count_styled})"),
2 + tab_count.width() + 2 + total_pane_count.width() + 3,
),
]));
let connected_users_count = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
StringAndLength::new(
format!(" [{connected_users_styled} connected users]"),
2 + connected_users.width() + 17,
),
StringAndLength::new(
format!(" [{connected_users_styled}]"),
2 + connected_users.width() + 1,
),
]));
ui_spans.push(session_bullet_span);
ui_spans.push(session_name_span);
ui_spans.push(tab_and_pane_count);
ui_spans.push(connected_users_count);
if session_ui_info.is_current_session {
let current_session_indication = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
StringAndLength::new(colors.orange(&format!(" <CURRENT SESSION>")), 18),
StringAndLength::new(colors.orange(&format!(" <CURRENT>")), 10),
StringAndLength::new(colors.orange(&format!(" <C>")), 4),
]));
ui_spans.push(current_session_indication);
}
ui_spans
}
pub fn build_tab_ui_line(tab_ui_info: &TabUiInfo, colors: Colors) -> Vec<UiSpan> {
let mut ui_spans = vec![];
let tab_name = &tab_ui_info.name;
let pane_count_text = tab_ui_info.panes.len();
let pane_count = format!("{}", pane_count_text);
let pane_count_styled = colors.green(&pane_count);
let tab_bullet_span =
UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
format!(" - "),
4,
)]));
let tab_name_span = UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(
tab_name.clone(),
SpanStyle::ForegroundBold(colors.palette.cyan),
));
let connected_users_count_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
StringAndLength::new(
format!(" ({pane_count_styled} panes)"),
2 + pane_count.width() + 7,
),
StringAndLength::new(
format!(" ({pane_count_styled})"),
2 + pane_count.width() + 1,
),
]));
ui_spans.push(tab_bullet_span);
ui_spans.push(tab_name_span);
ui_spans.push(connected_users_count_span);
ui_spans
}
pub fn build_pane_ui_line(pane_ui_info: &PaneUiInfo, colors: Colors) -> Vec<UiSpan> {
let mut ui_spans = vec![];
let pane_name = pane_ui_info.name.clone();
let exit_code = pane_ui_info.exit_code.map(|exit_code_number| {
let exit_code = format!("{}", exit_code_number);
let exit_code = if exit_code_number == 0 {
colors.green(&exit_code)
} else {
colors.red(&exit_code)
};
exit_code
});
let pane_bullet_span =
UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![StringAndLength::new(
format!(" > "),
6,
)]));
ui_spans.push(pane_bullet_span);
let pane_name_span =
UiSpan::TruncatableUiSpan(TruncatableUiSpan::new(pane_name, SpanStyle::Bold));
ui_spans.push(pane_name_span);
if let Some(exit_code) = exit_code {
let pane_name_span = UiSpan::UiSpanTelescope(UiSpanTelescope::new(vec![
StringAndLength::new(
format!(" (EXIT CODE: {exit_code})"),
13 + exit_code.width() + 1,
),
StringAndLength::new(format!(" ({exit_code})"), 2 + exit_code.width() + 1),
]));
ui_spans.push(pane_name_span);
}
ui_spans
}
pub fn minimize_lines(
total_count: usize,
line_count_to_remove: usize,
selected_index: Option<usize>,
) -> (usize, usize, usize, usize) {
// returns: (start_index, anchor_index, end_index, lines_left_to_remove)
let (count_to_render, line_count_to_remove) = if line_count_to_remove > total_count {
(1, line_count_to_remove.saturating_sub(total_count) + 1)
} else {
(total_count.saturating_sub(line_count_to_remove), 0)
};
let anchor_index = selected_index.unwrap_or(0); // 5
let mut start_index = anchor_index.saturating_sub(count_to_render / 2);
let mut end_index = start_index + count_to_render;
if end_index > total_count {
start_index = start_index.saturating_sub(end_index - total_count);
end_index = total_count;
}
(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!("{}\n", prompt);
} else {
println!("\n");
}
}
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
);
},
None => {
println!("\u{1b}[m > {new_session_shortcut} - {new_session}");
},
}
}
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 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 >= 80 {
print!(
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}"
);
} else if max_cols >= 57 {
let navigate = colors.bold("Navigate");
let select = colors.bold("Switch");
print!(
"\u{1b}[m\u{1b}[{row}HHelp: {arrows} - {navigate}, {enter} - {select}, {esc} - {to_hide}"
);
} else if max_cols >= 20 {
print!("\u{1b}[m\u{1b}[{row}H{arrows}/{enter}/{esc}");
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct Colors {
pub palette: Palette,
}
impl Colors {
pub fn new(palette: Palette) -> Self {
Colors { palette }
}
pub fn bold(&self, text: &str) -> String {
format!("\u{1b}[1m{}\u{1b}[22m", text)
}
fn color(&self, color: &PaletteColor, text: &str) -> String {
match color {
PaletteColor::EightBit(byte) => {
format!("\u{1b}[38;5;{};1m{}\u{1b}[39;22m", byte, text)
},
PaletteColor::Rgb((r, g, b)) => {
format!("\u{1b}[38;2;{};{};{};1m{}\u{1b}[39;22m", r, g, b, text)
},
}
}
pub fn orange(&self, text: &str) -> String {
self.color(&self.palette.orange, text)
}
pub fn green(&self, text: &str) -> String {
self.color(&self.palette.green, text)
}
pub fn red(&self, text: &str) -> String {
self.color(&self.palette.red, text)
}
pub fn cyan(&self, text: &str) -> String {
self.color(&self.palette.cyan, text)
}
pub fn magenta(&self, text: &str) -> String {
self.color(&self.palette.magenta, text)
}
}

View file

@ -0,0 +1,357 @@
pub mod components;
use zellij_tile::prelude::*;
use crate::session_list::{SelectedIndex, SessionList};
use components::{
build_pane_ui_line, build_session_ui_line, build_tab_ui_line, minimize_lines, Colors,
LineToRender,
};
macro_rules! render_assets {
($assets:expr, $line_count_to_remove:expr, $selected_index:expr, $to_render_until_selected: expr, $to_render_after_selected:expr, $has_deeper_selected_assets:expr, $max_cols:expr, $colors:expr) => {{
let (start_index, anchor_asset_index, end_index, line_count_to_remove) =
minimize_lines($assets.len(), $line_count_to_remove, $selected_index);
let mut truncated_result_count_above = start_index;
let mut truncated_result_count_below = $assets.len().saturating_sub(end_index);
let mut current_index = 1;
if let Some(assets_to_render_before_selected) = $assets.get(start_index..anchor_asset_index)
{
for asset in assets_to_render_before_selected {
let mut asset: LineToRender =
asset.as_line_to_render(current_index, $max_cols, $colors);
asset.add_truncated_results(truncated_result_count_above);
truncated_result_count_above = 0;
current_index += 1;
$to_render_until_selected.push(asset);
}
}
if let Some(selected_asset) = $assets.get(anchor_asset_index) {
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.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
selected_asset.add_truncated_results(truncated_result_count_below);
}
current_index += 1;
$to_render_until_selected.push(selected_asset);
} else {
$to_render_until_selected.push(selected_asset.as_line_to_render(
current_index,
$max_cols,
$colors,
));
current_index += 1;
}
}
if let Some(assets_to_render_after_selected) =
$assets.get(anchor_asset_index + 1..end_index)
{
for asset in assets_to_render_after_selected.iter().rev() {
let mut asset: LineToRender =
asset.as_line_to_render(current_index, $max_cols, $colors);
asset.add_truncated_results(truncated_result_count_below);
truncated_result_count_below = 0;
current_index += 1;
$to_render_after_selected.insert(0, asset.into());
}
}
line_count_to_remove
}};
}
impl SessionList {
pub fn render(&self, max_rows: usize, max_cols: usize, colors: Colors) -> Vec<LineToRender> {
if self.is_searching {
self.render_search_results(max_rows, max_cols)
} else {
self.render_list(max_rows, max_cols, colors)
}
}
fn render_search_results(&self, max_rows: usize, max_cols: usize) -> Vec<LineToRender> {
let mut lines_to_render = vec![];
for (i, result) in self.search_results.iter().enumerate() {
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 {
for line_to_render in result_lines.iter_mut() {
line_to_render.make_selected();
}
}
lines_to_render.append(&mut result_lines);
} else {
break;
}
}
lines_to_render
}
fn render_list(&self, max_rows: usize, max_cols: usize, colors: Colors) -> Vec<LineToRender> {
let mut lines_to_render_until_selected = vec![];
let mut lines_to_render_after_selected = vec![];
let total_lines_to_render = self.total_lines_to_render();
let line_count_to_remove = total_lines_to_render.saturating_sub(max_rows);
let line_count_to_remove = self.render_sessions(
&mut lines_to_render_until_selected,
&mut lines_to_render_after_selected,
line_count_to_remove,
max_cols,
colors,
);
let line_count_to_remove = self.render_tabs(
&mut lines_to_render_until_selected,
&mut lines_to_render_after_selected,
line_count_to_remove,
max_cols,
colors,
);
self.render_panes(
&mut lines_to_render_until_selected,
&mut lines_to_render_after_selected,
line_count_to_remove,
max_cols,
colors,
);
let mut lines_to_render = lines_to_render_until_selected;
lines_to_render.append(&mut lines_to_render_after_selected);
lines_to_render
}
fn render_sessions(
&self,
to_render_until_selected: &mut Vec<LineToRender>,
to_render_after_selected: &mut Vec<LineToRender>,
line_count_to_remove: usize,
max_cols: usize,
colors: Colors,
) -> usize {
render_assets!(
self.session_ui_infos,
line_count_to_remove,
self.selected_index.0,
to_render_until_selected,
to_render_after_selected,
self.selected_index.1.is_some(),
max_cols,
colors
)
}
fn render_tabs(
&self,
to_render_until_selected: &mut Vec<LineToRender>,
to_render_after_selected: &mut Vec<LineToRender>,
line_count_to_remove: usize,
max_cols: usize,
colors: Colors,
) -> usize {
if self.selected_index.1.is_none() {
return line_count_to_remove;
}
if let Some(tabs_in_session) = self
.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.map(|s| &s.tabs)
{
render_assets!(
tabs_in_session,
line_count_to_remove,
self.selected_index.1,
to_render_until_selected,
to_render_after_selected,
self.selected_index.2.is_some(),
max_cols,
colors
)
} else {
line_count_to_remove
}
}
fn render_panes(
&self,
to_render_until_selected: &mut Vec<LineToRender>,
to_render_after_selected: &mut Vec<LineToRender>,
line_count_to_remove: usize,
max_cols: usize,
colors: Colors,
) -> usize {
if self.selected_index.2.is_none() {
return line_count_to_remove;
}
if let Some(panes_in_session) = self
.selected_index
.0
.and_then(|i| self.session_ui_infos.get(i))
.map(|s| &s.tabs)
.and_then(|tabs| {
self.selected_index
.1
.and_then(|i| tabs.get(i))
.map(|t| &t.panes)
})
{
render_assets!(
panes_in_session,
line_count_to_remove,
self.selected_index.2,
to_render_until_selected,
to_render_after_selected,
false,
max_cols,
colors
)
} else {
line_count_to_remove
}
}
fn total_lines_to_render(&self) -> usize {
self.session_ui_infos
.iter()
.enumerate()
.fold(0, |acc, (index, s)| {
if self.selected_index.session_index_is_selected(index) {
acc + s.line_count(&self.selected_index)
} else {
acc + 1
}
})
}
}
#[derive(Debug, Clone)]
pub struct SessionUiInfo {
pub name: String,
pub tabs: Vec<TabUiInfo>,
pub connected_users: usize,
pub is_current_session: bool,
}
impl SessionUiInfo {
pub fn from_session_info(session_info: &SessionInfo) -> Self {
SessionUiInfo {
name: session_info.name.clone(),
tabs: session_info
.tabs
.iter()
.map(|t| TabUiInfo::new(t, &session_info.panes))
.collect(),
connected_users: session_info.connected_clients,
is_current_session: session_info.is_current_session,
}
}
pub fn line_count(&self, selected_index: &SelectedIndex) -> usize {
let mut line_count = 1; // self
if selected_index.tabs_are_visible() {
match selected_index
.selected_tab_index()
.and_then(|i| self.tabs.get(i))
.map(|t| t.line_count(&selected_index))
{
Some(line_count_of_selected_tab) => {
// we add the line count in the selected tab minus 1 because we will account
// for the selected tab line itself in self.tabs.len() below
line_count += line_count_of_selected_tab.saturating_sub(1);
line_count += self.tabs.len();
},
None => {
line_count += self.tabs.len();
},
}
}
line_count
}
fn as_line_to_render(
&self,
_session_index: u8,
mut max_cols: usize,
colors: Colors,
) -> LineToRender {
let mut line_to_render = LineToRender::new(colors);
let ui_spans = build_session_ui_line(&self, colors);
for span in ui_spans {
span.render(None, &mut line_to_render, &mut max_cols);
}
line_to_render
}
}
#[derive(Debug, Clone)]
pub struct TabUiInfo {
pub name: String,
pub panes: Vec<PaneUiInfo>,
pub position: usize,
}
impl TabUiInfo {
pub fn new(tab_info: &TabInfo, pane_manifest: &PaneManifest) -> Self {
let panes = pane_manifest
.panes
.get(&tab_info.position)
.map(|p| {
p.iter()
.filter_map(|pane_info| {
if pane_info.is_selectable {
Some(PaneUiInfo {
name: pane_info.title.clone(),
exit_code: pane_info.exit_status.clone(),
pane_id: pane_info.id,
is_plugin: pane_info.is_plugin,
})
} else {
None
}
})
.collect()
})
.unwrap_or_default();
TabUiInfo {
name: tab_info.name.clone(),
panes,
position: tab_info.position,
}
}
pub fn line_count(&self, selected_index: &SelectedIndex) -> usize {
let mut line_count = 1; // self
if selected_index.panes_are_visible() {
line_count += self.panes.len()
}
line_count
}
fn as_line_to_render(
&self,
_session_index: u8,
mut max_cols: usize,
colors: Colors,
) -> LineToRender {
let mut line_to_render = LineToRender::new(colors);
let ui_spans = build_tab_ui_line(&self, colors);
for span in ui_spans {
span.render(None, &mut line_to_render, &mut max_cols);
}
line_to_render
}
}
#[derive(Debug, Clone)]
pub struct PaneUiInfo {
pub name: String,
pub exit_code: Option<i32>,
pub pane_id: u32,
pub is_plugin: bool,
}
impl PaneUiInfo {
fn as_line_to_render(
&self,
_session_index: u8,
mut max_cols: usize,
colors: Colors,
) -> LineToRender {
let mut line_to_render = LineToRender::new(colors);
let ui_spans = build_pane_ui_line(&self, colors);
for span in ui_spans {
span.render(None, &mut line_to_render, &mut max_cols);
}
line_to_render
}
}

View file

@ -16,6 +16,7 @@ use zellij_client::{
use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl}; use zellij_server::{os_input_output::get_server_os_input, start_server as start_server_impl};
use zellij_utils::{ use zellij_utils::{
cli::{CliArgs, Command, SessionCommand, Sessions}, cli::{CliArgs, Command, SessionCommand, Sessions},
data::ConnectToSession,
envs, envs,
input::{ input::{
actions::Action, actions::Action,
@ -331,7 +332,33 @@ pub(crate) fn start_client(opts: CliArgs) {
process::exit(1); process::exit(1);
}, },
}; };
let mut reconnect_to_session: Option<ConnectToSession> = None;
let os_input = get_os_input(get_client_os_input); let os_input = get_os_input(get_client_os_input);
loop {
let os_input = os_input.clone();
let config = config.clone();
let layout = layout.clone();
let mut config_options = config_options.clone();
let mut opts = opts.clone();
if let Some(reconnect_to_session) = &reconnect_to_session {
// this is integration code to make session reconnects work with this existing,
// untested and pretty involved function
//
// ideally, we should write tests for this whole function and refctor it
if reconnect_to_session.name.is_some() {
opts.command = Some(Command::Sessions(Sessions::Attach {
session_name: reconnect_to_session.name.clone(),
create: true,
index: None,
options: None,
}));
} else {
opts.command = None;
opts.session = None;
config_options.attach_to_session = None;
}
}
let start_client_plan = |session_name: std::string::String| { let start_client_plan = |session_name: std::string::String| {
assert_session_ne(&session_name); assert_session_ne(&session_name);
@ -345,7 +372,9 @@ pub(crate) fn start_client(opts: CliArgs) {
})) = opts.command.clone() })) = opts.command.clone()
{ {
let config_options = match options.as_deref() { let config_options = match options.as_deref() {
Some(SessionCommand::Options(o)) => config_options.merge_from_cli(o.to_owned().into()), Some(SessionCommand::Options(o)) => {
config_options.merge_from_cli(o.to_owned().into())
},
None => config_options, None => config_options,
}; };
@ -364,8 +393,7 @@ pub(crate) fn start_client(opts: CliArgs) {
if let Ok(val) = std::env::var(envs::SESSION_NAME_ENV_KEY) { if let Ok(val) = std::env::var(envs::SESSION_NAME_ENV_KEY) {
if val == *client.get_session_name() { if val == *client.get_session_name() {
eprintln!("You are trying to attach to the current session (\"{}\"). Zellij does not support nesting a session in itself.", val); panic!("You are trying to attach to the current session (\"{}\"). This is not supported.", val);
process::exit(1);
} }
} }
@ -374,24 +402,34 @@ pub(crate) fn start_client(opts: CliArgs) {
ClientInfo::New(_) => Some(layout), ClientInfo::New(_) => Some(layout),
}; };
start_client_impl( let tab_position_to_focus = reconnect_to_session
.as_ref()
.and_then(|r| r.tab_position.clone());
let pane_id_to_focus = reconnect_to_session
.as_ref()
.and_then(|r| r.pane_id.clone());
reconnect_to_session = start_client_impl(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options, config_options,
client, client,
attach_layout, attach_layout,
tab_position_to_focus,
pane_id_to_focus,
); );
} else { } else {
if let Some(session_name) = opts.session.clone() { if let Some(session_name) = opts.session.clone() {
start_client_plan(session_name.clone()); start_client_plan(session_name.clone());
start_client_impl( reconnect_to_session = start_client_impl(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options, config_options,
ClientInfo::New(session_name), ClientInfo::New(session_name),
Some(layout), Some(layout),
None,
None,
); );
} else { } else {
if let Some(session_name) = config_options.session_name.as_ref() { if let Some(session_name) = config_options.session_name.as_ref() {
@ -419,45 +457,57 @@ pub(crate) fn start_client(opts: CliArgs) {
ClientInfo::Attach(_, _) => None, ClientInfo::Attach(_, _) => None,
ClientInfo::New(_) => Some(layout), ClientInfo::New(_) => Some(layout),
}; };
start_client_impl( reconnect_to_session = start_client_impl(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options, config_options,
client, client,
attach_layout, attach_layout,
None,
None,
); );
}, },
_ => { _ => {
start_client_plan(session_name.clone()); start_client_plan(session_name.clone());
start_client_impl( reconnect_to_session = start_client_impl(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options.clone(), config_options.clone(),
ClientInfo::New(session_name.clone()), ClientInfo::New(session_name.clone()),
Some(layout), Some(layout),
None,
None,
); );
}, },
} }
if reconnect_to_session.is_some() {
continue;
}
// after we detach, this happens and so we need to exit before the rest of the // after we detach, this happens and so we need to exit before the rest of the
// function happens // function happens
// TODO: offload this to a different function
process::exit(0); process::exit(0);
} }
let session_name = generate_unique_session_name(); let session_name = generate_unique_session_name();
start_client_plan(session_name.clone()); start_client_plan(session_name.clone());
start_client_impl( reconnect_to_session = start_client_impl(
Box::new(os_input), Box::new(os_input),
opts, opts,
config, config,
config_options, config_options,
ClientInfo::New(session_name), ClientInfo::New(session_name),
Some(layout), Some(layout),
None,
None,
); );
} }
} }
if reconnect_to_session.is_none() {
break;
}
}
} }
fn generate_unique_session_name() -> String { fn generate_unique_session_name() -> String {

View file

@ -35,6 +35,7 @@ lazy_static::lazy_static! {
WorkspaceMember{crate_name: "default-plugins/strider", build: true}, WorkspaceMember{crate_name: "default-plugins/strider", build: true},
WorkspaceMember{crate_name: "default-plugins/tab-bar", build: true}, WorkspaceMember{crate_name: "default-plugins/tab-bar", build: true},
WorkspaceMember{crate_name: "default-plugins/fixture-plugin-for-tests", build: true}, WorkspaceMember{crate_name: "default-plugins/fixture-plugin-for-tests", build: true},
WorkspaceMember{crate_name: "default-plugins/session-manager", build: true},
WorkspaceMember{crate_name: "zellij-utils", build: false}, WorkspaceMember{crate_name: "zellij-utils", build: false},
WorkspaceMember{crate_name: "zellij-tile-utils", build: false}, WorkspaceMember{crate_name: "zellij-tile-utils", build: false},
WorkspaceMember{crate_name: "zellij-tile", build: false}, WorkspaceMember{crate_name: "zellij-tile", build: false},

View file

@ -157,6 +157,9 @@ impl InputHandler {
.send(ClientInstruction::DoneParsingStdinQuery) .send(ClientInstruction::DoneParsingStdinQuery)
.unwrap(); .unwrap();
}, },
Ok((InputInstruction::Exit, _error_context)) => {
self.should_exit = true;
},
Err(err) => panic!("Encountered read error: {:?}", err), Err(err) => panic!("Encountered read error: {:?}", err),
} }
} }

View file

@ -23,8 +23,8 @@ use crate::{
}; };
use zellij_utils::{ use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext}, channels::{self, ChannelWithContext, SenderWithContext},
consts::ZELLIJ_IPC_PIPE, consts::{set_permissions, ZELLIJ_SOCK_DIR},
data::{ClientId, InputMode, Style}, data::{ClientId, ConnectToSession, InputMode, Style},
envs, envs,
errors::{ClientContext, ContextType, ErrorInstruction}, errors::{ClientContext, ContextType, ErrorInstruction},
input::{config::Config, options::Options}, input::{config::Config, options::Options},
@ -46,6 +46,7 @@ pub(crate) enum ClientInstruction {
StartedParsingStdinQuery, StartedParsingStdinQuery,
DoneParsingStdinQuery, DoneParsingStdinQuery,
Log(Vec<String>), Log(Vec<String>),
SwitchSession(ConnectToSession),
} }
impl From<ServerToClientMsg> for ClientInstruction { impl From<ServerToClientMsg> for ClientInstruction {
@ -60,6 +61,9 @@ impl From<ServerToClientMsg> for ClientInstruction {
ServerToClientMsg::Connected => ClientInstruction::Connected, ServerToClientMsg::Connected => ClientInstruction::Connected,
ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients), ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients),
ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines), ServerToClientMsg::Log(log_lines) => ClientInstruction::Log(log_lines),
ServerToClientMsg::SwitchSession(connect_to_session) => {
ClientInstruction::SwitchSession(connect_to_session)
},
} }
} }
} }
@ -77,6 +81,7 @@ impl From<&ClientInstruction> for ClientContext {
ClientInstruction::Log(_) => ClientContext::Log, ClientInstruction::Log(_) => ClientContext::Log,
ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery, ClientInstruction::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery,
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery, ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
} }
} }
} }
@ -130,6 +135,7 @@ pub(crate) enum InputInstruction {
AnsiStdinInstructions(Vec<AnsiStdinInstruction>), AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
StartedParsing, StartedParsing,
DoneParsing, DoneParsing,
Exit,
} }
pub fn start_client( pub fn start_client(
@ -139,8 +145,12 @@ pub fn start_client(
config_options: Options, config_options: Options,
info: ClientInfo, info: ClientInfo,
layout: Option<Layout>, layout: Option<Layout>,
) { tab_position_to_focus: Option<usize>,
pane_id_to_focus: Option<(u32, bool)>, // (pane_id, is_plugin)
) -> Option<ConnectToSession> {
info!("Starting Zellij client!"); info!("Starting Zellij client!");
let mut reconnect_to_session = None;
let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l"; let clear_client_terminal_attributes = "\u{1b}[?1l\u{1b}=\u{1b}[r\u{1b}[?1000l\u{1b}[?1002l\u{1b}[?1003l\u{1b}[?1005l\u{1b}[?1006l\u{1b}[?12l";
let take_snapshot = "\u{1b}[?1049h"; let take_snapshot = "\u{1b}[?1049h";
let bracketed_paste = "\u{1b}[?2004h"; let bracketed_paste = "\u{1b}[?2004h";
@ -172,28 +182,51 @@ pub fn start_client(
keybinds: config.keybinds.clone(), keybinds: config.keybinds.clone(),
}; };
let first_msg = match info { let create_ipc_pipe = || -> std::path::PathBuf {
ClientInfo::Attach(name, config_options) => { let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
envs::set_session_name(name); std::fs::create_dir_all(&sock_dir).unwrap();
set_permissions(&sock_dir, 0o700).unwrap();
sock_dir.push(envs::get_session_name().unwrap());
sock_dir
};
ClientToServerMsg::AttachClient(client_attributes, config_options) let (first_msg, ipc_pipe) = match info {
ClientInfo::Attach(name, config_options) => {
envs::set_session_name(name.clone());
os_input.update_session_name(name);
let ipc_pipe = create_ipc_pipe();
(
ClientToServerMsg::AttachClient(
client_attributes,
config_options,
tab_position_to_focus,
pane_id_to_focus,
),
ipc_pipe,
)
}, },
ClientInfo::New(name) => { ClientInfo::New(name) => {
envs::set_session_name(name); envs::set_session_name(name.clone());
os_input.update_session_name(name);
let ipc_pipe = create_ipc_pipe();
spawn_server(&*ZELLIJ_IPC_PIPE, opts.debug).unwrap(); spawn_server(&*ipc_pipe, opts.debug).unwrap();
(
ClientToServerMsg::NewClient( ClientToServerMsg::NewClient(
client_attributes, client_attributes,
Box::new(opts), Box::new(opts),
Box::new(config_options.clone()), Box::new(config_options.clone()),
Box::new(layout.unwrap()), Box::new(layout.unwrap()),
Some(config.plugins.clone()), Some(config.plugins.clone()),
),
ipc_pipe,
) )
}, },
}; };
os_input.connect_to_server(&*ZELLIJ_IPC_PIPE); os_input.connect_to_server(&*ipc_pipe);
os_input.send_to_server(first_msg); os_input.send_to_server(first_msg);
let mut command_is_executing = CommandIsExecuting::new(); let mut command_is_executing = CommandIsExecuting::new();
@ -336,7 +369,7 @@ pub fn start_client(
std::process::exit(1); std::process::exit(1);
}; };
let exit_msg: String; let mut exit_msg = String::new();
let mut loading = true; let mut loading = true;
let mut pending_instructions = vec![]; let mut pending_instructions = vec![];
@ -415,13 +448,18 @@ pub fn start_client(
log::info!("{line}"); log::info!("{line}");
} }
}, },
ClientInstruction::SwitchSession(connect_to_session) => {
reconnect_to_session = Some(connect_to_session);
os_input.send_to_server(ClientToServerMsg::ClientExited);
break;
},
_ => {}, _ => {},
} }
} }
router_thread.join().unwrap(); router_thread.join().unwrap();
// cleanup(); if reconnect_to_session.is_none() {
let reset_style = "\u{1b}[m"; let reset_style = "\u{1b}[m";
let show_cursor = "\u{1b}[?25h"; let show_cursor = "\u{1b}[?25h";
let restore_snapshot = "\u{1b}[?1049l"; let restore_snapshot = "\u{1b}[?1049l";
@ -437,6 +475,16 @@ pub fn start_client(
let mut stdout = os_input.get_stdout_writer(); let mut stdout = os_input.get_stdout_writer();
let _ = stdout.write(goodbye_message.as_bytes()).unwrap(); let _ = stdout.write(goodbye_message.as_bytes()).unwrap();
stdout.flush().unwrap(); stdout.flush().unwrap();
} else {
let clear_screen = "\u{1b}[2J";
let mut stdout = os_input.get_stdout_writer();
let _ = stdout.write(clear_screen.as_bytes()).unwrap();
stdout.flush().unwrap();
}
let _ = send_input_instructions.send(InputInstruction::Exit);
reconnect_to_session
} }
#[cfg(test)] #[cfg(test)]

View file

@ -78,6 +78,8 @@ pub struct ClientOsInputOutput {
orig_termios: Option<Arc<Mutex<termios::Termios>>>, orig_termios: Option<Arc<Mutex<termios::Termios>>>,
send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>, send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>,
receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>, receive_instructions_from_server: Arc<Mutex<Option<IpcReceiverWithContext<ServerToClientMsg>>>>,
reading_from_stdin: Arc<Mutex<Option<Vec<u8>>>>,
session_name: Arc<Mutex<Option<String>>>,
} }
/// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that /// The `ClientOsApi` trait represents an abstract interface to the features of an operating system that
@ -94,8 +96,9 @@ pub trait ClientOsApi: Send + Sync {
/// Returns the writer that allows writing to standard output. /// Returns the writer that allows writing to standard output.
fn get_stdout_writer(&self) -> Box<dyn io::Write>; fn get_stdout_writer(&self) -> Box<dyn io::Write>;
fn get_stdin_reader(&self) -> Box<dyn io::Read>; fn get_stdin_reader(&self) -> Box<dyn io::Read>;
fn update_session_name(&mut self, new_session_name: String);
/// Returns the raw contents of standard input. /// Returns the raw contents of standard input.
fn read_from_stdin(&mut self) -> Vec<u8>; fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str>;
/// Returns a [`Box`] pointer to this [`ClientOsApi`] struct. /// Returns a [`Box`] pointer to this [`ClientOsApi`] struct.
fn box_clone(&self) -> Box<dyn ClientOsApi>; fn box_clone(&self) -> Box<dyn ClientOsApi>;
/// Sends a message to the server. /// Sends a message to the server.
@ -135,14 +138,46 @@ impl ClientOsApi for ClientOsInputOutput {
fn box_clone(&self) -> Box<dyn ClientOsApi> { fn box_clone(&self) -> Box<dyn ClientOsApi> {
Box::new((*self).clone()) Box::new((*self).clone())
} }
fn read_from_stdin(&mut self) -> Vec<u8> { fn update_session_name(&mut self, new_session_name: String) {
*self.session_name.lock().unwrap() = Some(new_session_name);
}
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
let session_name_at_calltime = { self.session_name.lock().unwrap().clone() };
// here we wait for a lock in case another thread is holding stdin
// this can happen for example when switching sessions, the old thread will only be
// released once it sees input over STDIN
//
// when this happens, we detect in the other thread that our session is ended (by comparing
// the session name at the beginning of the call and the one after we read from STDIN), and
// so place what we read from STDIN inside a buffer (the "reading_from_stdin" on our state)
// and release the lock
//
// then, another thread will see there's something in the buffer immediately as it acquires
// the lock (without having to wait for STDIN itself) forward this buffer and proceed to
// wait for the "real" STDIN net time it is called
let mut buffered_bytes = self.reading_from_stdin.lock().unwrap();
match buffered_bytes.take() {
Some(buffered_bytes) => Ok(buffered_bytes),
None => {
let stdin = std::io::stdin(); let stdin = std::io::stdin();
let mut stdin = stdin.lock(); let mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap(); let buffer = stdin.fill_buf().unwrap();
let length = buffer.len(); let length = buffer.len();
let read_bytes = Vec::from(buffer); let read_bytes = Vec::from(buffer);
stdin.consume(length); stdin.consume(length);
read_bytes
let session_name_after_reading_from_stdin =
{ self.session_name.lock().unwrap().clone() };
if session_name_at_calltime.is_some()
&& session_name_at_calltime != session_name_after_reading_from_stdin
{
*buffered_bytes = Some(read_bytes);
Err("Session ended")
} else {
Ok(read_bytes)
}
},
}
} }
fn get_stdout_writer(&self) -> Box<dyn io::Write> { fn get_stdout_writer(&self) -> Box<dyn io::Write> {
let stdout = ::std::io::stdout(); let stdout = ::std::io::stdout();
@ -258,19 +293,25 @@ impl Clone for Box<dyn ClientOsApi> {
pub fn get_client_os_input() -> Result<ClientOsInputOutput, nix::Error> { pub fn get_client_os_input() -> Result<ClientOsInputOutput, nix::Error> {
let current_termios = termios::tcgetattr(0)?; let current_termios = termios::tcgetattr(0)?;
let orig_termios = Some(Arc::new(Mutex::new(current_termios))); let orig_termios = Some(Arc::new(Mutex::new(current_termios)));
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput { Ok(ClientOsInputOutput {
orig_termios, orig_termios,
send_instructions_to_server: Arc::new(Mutex::new(None)), send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)), receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
session_name: Arc::new(Mutex::new(None)),
}) })
} }
pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, nix::Error> { pub fn get_cli_client_os_input() -> Result<ClientOsInputOutput, nix::Error> {
let orig_termios = None; // not a terminal let orig_termios = None; // not a terminal
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput { Ok(ClientOsInputOutput {
orig_termios, orig_termios,
send_instructions_to_server: Arc::new(Mutex::new(None)), send_instructions_to_server: Arc::new(Mutex::new(None)),
receive_instructions_from_server: Arc::new(Mutex::new(None)), receive_instructions_from_server: Arc::new(Mutex::new(None)),
reading_from_stdin,
session_name: Arc::new(Mutex::new(None)),
}) })
} }

View file

@ -59,7 +59,8 @@ pub(crate) fn stdin_loop(
} }
let mut ansi_stdin_events = vec![]; let mut ansi_stdin_events = vec![];
loop { loop {
let buf = os_input.read_from_stdin(); match os_input.read_from_stdin() {
Ok(buf) => {
{ {
// here we check if we need to parse specialized ANSI instructions sent over STDIN // here we check if we need to parse specialized ANSI instructions sent over STDIN
// this happens either on startup (see above) or on SIGWINCH // this happens either on startup (see above) or on SIGWINCH
@ -96,7 +97,8 @@ pub(crate) fn stdin_loop(
let event_count = events.len(); let event_count = events.len();
for (i, input_event) in events.into_iter().enumerate() { for (i, input_event) in events.into_iter().enumerate() {
if holding_mouse && is_mouse_press_or_hold(&input_event) && i == event_count - 1 { if holding_mouse && is_mouse_press_or_hold(&input_event) && i == event_count - 1
{
let mut poller = os_input.stdin_poller(); let mut poller = os_input.stdin_poller();
loop { loop {
if poller.ready() { if poller.ready() {
@ -120,6 +122,17 @@ pub(crate) fn stdin_loop(
)) ))
.unwrap(); .unwrap();
} }
},
Err(e) => {
if e == "Session ended" {
log::debug!("Switched sessions, signing this thread off...");
} else {
log::error!("Failed to read from STDIN: {}", e);
}
let _ = send_input_instructions.send(InputInstruction::Exit);
break;
},
}
} }
} }

View file

@ -154,8 +154,9 @@ impl ClientOsApi for FakeClientOsApi {
fn get_stdin_reader(&self) -> Box<dyn io::Read> { fn get_stdin_reader(&self) -> Box<dyn io::Read> {
unimplemented!() unimplemented!()
} }
fn read_from_stdin(&mut self) -> Vec<u8> { fn update_session_name(&mut self, new_session_name: String) {}
self.stdin_buffer.drain(..).collect() fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
Ok(self.stdin_buffer.drain(..).collect())
} }
fn box_clone(&self) -> Box<dyn ClientOsApi> { fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!() unimplemented!()

View file

@ -1,10 +1,16 @@
use zellij_utils::async_std::task; use zellij_utils::async_std::task;
use zellij_utils::consts::{ZELLIJ_SESSION_INFO_CACHE_DIR, ZELLIJ_SOCK_DIR};
use zellij_utils::data::SessionInfo;
use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType}; use zellij_utils::errors::{prelude::*, BackgroundJobContext, ContextType};
use std::collections::HashMap; use std::collections::{BTreeMap, HashMap};
use std::fs;
use std::io::Write;
use std::os::unix::fs::FileTypeExt;
use std::path::PathBuf;
use std::sync::{ use std::sync::{
atomic::{AtomicBool, Ordering}, atomic::{AtomicBool, Ordering},
Arc, Arc, Mutex,
}; };
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
@ -17,6 +23,8 @@ pub enum BackgroundJob {
DisplayPaneError(Vec<PaneId>, String), DisplayPaneError(Vec<PaneId>, String),
AnimatePluginLoading(u32), // u32 - plugin_id AnimatePluginLoading(u32), // u32 - plugin_id
StopPluginLoadingAnimation(u32), // u32 - plugin_id StopPluginLoadingAnimation(u32), // u32 - plugin_id
ReadAllSessionInfosOnMachine, // u32 - plugin_id
ReportSessionInfo(String, SessionInfo), // String - session name
Exit, Exit,
} }
@ -28,6 +36,10 @@ impl From<&BackgroundJob> for BackgroundJobContext {
BackgroundJob::StopPluginLoadingAnimation(..) => { BackgroundJob::StopPluginLoadingAnimation(..) => {
BackgroundJobContext::StopPluginLoadingAnimation BackgroundJobContext::StopPluginLoadingAnimation
}, },
BackgroundJob::ReadAllSessionInfosOnMachine => {
BackgroundJobContext::ReadAllSessionInfosOnMachine
},
BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo,
BackgroundJob::Exit => BackgroundJobContext::Exit, BackgroundJob::Exit => BackgroundJobContext::Exit,
} }
} }
@ -35,11 +47,14 @@ impl From<&BackgroundJob> for BackgroundJobContext {
static FLASH_DURATION_MS: u64 = 1000; static FLASH_DURATION_MS: u64 = 1000;
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500; static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
static SESSION_READ_DURATION: u64 = 1000;
pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> { pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
let err_context = || "failed to write to pty".to_string(); let err_context = || "failed to write to pty".to_string();
let mut running_jobs: HashMap<BackgroundJob, Instant> = HashMap::new(); let mut running_jobs: HashMap<BackgroundJob, Instant> = HashMap::new();
let mut loading_plugins: HashMap<u32, Arc<AtomicBool>> = HashMap::new(); // u32 - plugin_id let mut loading_plugins: HashMap<u32, Arc<AtomicBool>> = HashMap::new(); // u32 - plugin_id
let current_session_name = Arc::new(Mutex::new(String::default()));
let current_session_info = Arc::new(Mutex::new(SessionInfo::default()));
loop { loop {
let (event, mut err_ctx) = bus.recv().with_context(err_context)?; let (event, mut err_ctx) = bus.recv().with_context(err_context)?;
@ -93,10 +108,89 @@ pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
loading_plugin.store(false, Ordering::SeqCst); loading_plugin.store(false, Ordering::SeqCst);
} }
}, },
BackgroundJob::ReportSessionInfo(session_name, session_info) => {
*current_session_name.lock().unwrap() = session_name;
*current_session_info.lock().unwrap() = session_info;
},
BackgroundJob::ReadAllSessionInfosOnMachine => {
// this job should only be run once and it keeps track of other sessions (as well
// as this one's) infos (metadata mostly) and sends it to the screen which in turn
// forwards it to plugins and other places it needs to be
if running_jobs.get(&job).is_some() {
continue;
}
running_jobs.insert(job, Instant::now());
task::spawn({
let senders = bus.senders.clone();
let current_session_info = current_session_info.clone();
let current_session_name = current_session_name.clone();
async move {
loop {
// write state of current session
// write it to disk
let current_session_name =
current_session_name.lock().unwrap().to_string();
let cache_file_name =
session_info_cache_file_name(&current_session_name);
let current_session_info = current_session_info.lock().unwrap().clone();
let _wrote_file =
std::fs::create_dir_all(ZELLIJ_SESSION_INFO_CACHE_DIR.as_path())
.and_then(|_| std::fs::File::create(cache_file_name))
.and_then(|mut f| {
write!(f, "{}", current_session_info.to_string())
});
// start a background job (if not already running) that'll periodically read this and other
// sesion infos and report back
// read state of all sessions
let mut other_session_names = vec![];
let mut session_infos_on_machine = BTreeMap::new();
// we do this so that the session infos will be actual and we're
// reasonably sure their session is running
if let Ok(files) = fs::read_dir(&*ZELLIJ_SOCK_DIR) {
files.for_each(|file| {
if let Ok(file) = file {
if let Ok(file_name) = file.file_name().into_string() {
if file.file_type().unwrap().is_socket() {
other_session_names.push(file_name);
}
}
}
});
}
for session_name in other_session_names {
let session_cache_file_name = ZELLIJ_SESSION_INFO_CACHE_DIR
.join(format!("{}.kdl", session_name));
if let Ok(raw_session_info) =
fs::read_to_string(&session_cache_file_name)
{
if let Ok(session_info) = SessionInfo::from_string(
&raw_session_info,
&current_session_name,
) {
session_infos_on_machine.insert(session_name, session_info);
}
}
}
let _ = senders.send_to_screen(ScreenInstruction::UpdateSessionInfos(
session_infos_on_machine,
));
task::sleep(std::time::Duration::from_millis(SESSION_READ_DURATION))
.await;
}
}
});
},
BackgroundJob::Exit => { BackgroundJob::Exit => {
for loading_plugin in loading_plugins.values() { for loading_plugin in loading_plugins.values() {
loading_plugin.store(false, Ordering::SeqCst); loading_plugin.store(false, Ordering::SeqCst);
} }
let cache_file_name =
session_info_cache_file_name(&current_session_name.lock().unwrap().to_owned());
let _ = std::fs::remove_file(cache_file_name);
return Ok(()); return Ok(());
}, },
} }
@ -122,3 +216,7 @@ fn job_already_running(
}, },
} }
} }
fn session_info_cache_file_name(session_name: &str) -> PathBuf {
ZELLIJ_SESSION_INFO_CACHE_DIR.join(format!("{}.kdl", &session_name))
}

View file

@ -41,7 +41,7 @@ use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext}, channels::{self, ChannelWithContext, SenderWithContext},
cli::CliArgs, cli::CliArgs,
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE}, consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{Event, PluginCapabilities}, data::{ConnectToSession, Event, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext}, errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
input::{ input::{
command::{RunCommand, TerminalAction}, command::{RunCommand, TerminalAction},
@ -74,10 +74,17 @@ pub enum ServerInstruction {
Error(String), Error(String),
KillSession, KillSession,
DetachSession(Vec<ClientId>), DetachSession(Vec<ClientId>),
AttachClient(ClientAttributes, Options, ClientId), AttachClient(
ClientAttributes,
Options,
Option<usize>, // tab position to focus
Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus
ClientId,
),
ConnStatus(ClientId), ConnStatus(ClientId),
ActiveClients(ClientId), ActiveClients(ClientId),
Log(Vec<String>, ClientId), Log(Vec<String>, ClientId),
SwitchSession(ConnectToSession, ClientId),
} }
impl From<&ServerInstruction> for ServerContext { impl From<&ServerInstruction> for ServerContext {
@ -95,6 +102,7 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus, ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus,
ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients, ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients,
ServerInstruction::Log(..) => ServerContext::Log, ServerInstruction::Log(..) => ServerContext::Log,
ServerInstruction::SwitchSession(..) => ServerContext::SwitchSession,
} }
} }
} }
@ -415,7 +423,13 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.send_to_plugin(PluginInstruction::AddClient(client_id)) .send_to_plugin(PluginInstruction::AddClient(client_id))
.unwrap(); .unwrap();
}, },
ServerInstruction::AttachClient(attrs, options, client_id) => { ServerInstruction::AttachClient(
attrs,
options,
tab_position_to_focus,
pane_id_to_focus,
client_id,
) => {
let rlock = session_data.read().unwrap(); let rlock = session_data.read().unwrap();
let session_data = rlock.as_ref().unwrap(); let session_data = rlock.as_ref().unwrap();
session_state session_state
@ -433,7 +447,11 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.unwrap(); .unwrap();
session_data session_data
.senders .senders
.send_to_screen(ScreenInstruction::AddClient(client_id)) .send_to_screen(ScreenInstruction::AddClient(
client_id,
tab_position_to_focus,
pane_id_to_focus,
))
.unwrap(); .unwrap();
session_data session_data
.senders .senders
@ -635,6 +653,41 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
session_state session_state
); );
}, },
ServerInstruction::SwitchSession(connect_to_session, client_id) => {
if let Some(min_size) = session_state.read().unwrap().min_client_terminal_size() {
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::TerminalResize(min_size))
.unwrap();
}
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_screen(ScreenInstruction::RemoveClient(client_id))
.unwrap();
session_data
.write()
.unwrap()
.as_ref()
.unwrap()
.senders
.send_to_plugin(PluginInstruction::RemoveClient(client_id))
.unwrap();
send_to_client!(
client_id,
os_input,
ServerToClientMsg::SwitchSession(connect_to_session),
session_state
);
remove_client!(client_id, os_input, session_state);
},
} }
} }
@ -664,13 +717,11 @@ fn init_session(
plugins, plugins,
} = options; } = options;
SCROLL_BUFFER_SIZE let _ = SCROLL_BUFFER_SIZE.set(
.set(
config_options config_options
.scroll_buffer_size .scroll_buffer_size
.unwrap_or(DEFAULT_SCROLL_BUFFER_SIZE), .unwrap_or(DEFAULT_SCROLL_BUFFER_SIZE),
) );
.unwrap();
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded(); let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
let to_screen = SenderWithContext::new(to_screen); let to_screen = SenderWithContext::new(to_screen);

View file

@ -1351,7 +1351,7 @@ impl Grid {
self.viewport.get(y).unwrap().absolute_character_index(x) self.viewport.get(y).unwrap().absolute_character_index(x)
} }
pub fn move_cursor_forward_until_edge(&mut self, count: usize) { pub fn move_cursor_forward_until_edge(&mut self, count: usize) {
let count_to_move = std::cmp::min(count, self.width - self.cursor.x); let count_to_move = std::cmp::min(count, self.width.saturating_sub(self.cursor.x));
self.cursor.x += count_to_move; self.cursor.x += count_to_move;
} }
pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) { pub fn replace_characters_in_line_after_cursor(&mut self, replace_with: TerminalCharacter) {

View file

@ -523,6 +523,11 @@ impl TiledPanes {
} }
} }
pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) { pub fn focus_pane(&mut self, pane_id: PaneId, client_id: ClientId) {
if self.panes_to_hide.contains(&pane_id) {
// this means there is a fullscreen pane that is not the current pane, let's unset it
// before changing focus
self.unset_fullscreen();
}
if self if self
.panes .panes
.get(&pane_id) .get(&pane_id)
@ -533,7 +538,6 @@ impl TiledPanes {
.expand_pane(&pane_id); .expand_pane(&pane_id);
self.reapply_pane_frames(); self.reapply_pane_frames();
} }
self.active_panes self.active_panes
.insert(client_id, pane_id, &mut self.panes); .insert(client_id, pane_id, &mut self.panes);
if self.session_is_mirrored { if self.session_is_mirrored {

View file

@ -539,6 +539,7 @@ pub fn load_new_plugin_from_hd() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -609,6 +610,7 @@ pub fn plugin_workers() {
// we send a SystemClipboardFailure to trigger the custom handler in the fixture plugin that // we send a SystemClipboardFailure to trigger the custom handler in the fixture plugin that
// will send a message to the worker and in turn back to the plugin to be rendered, so we know // will send a message to the worker and in turn back to the plugin to be rendered, so we know
// that this cycle is working // that this cycle is working
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -682,6 +684,7 @@ pub fn plugin_workers_persist_state() {
// we do this a second time so that the worker will log the first message on its own state and // we do this a second time so that the worker will log the first message on its own state and
// then send us the "received 2 messages" indication we check for below, letting us know it // then send us the "received 2 messages" indication we check for below, letting us know it
// managed to persist its own state and act upon it // managed to persist its own state and act upon it
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -820,6 +823,7 @@ pub fn switch_to_mode_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -887,6 +891,7 @@ pub fn switch_to_mode_plugin_command_permission_denied() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -954,6 +959,7 @@ pub fn new_tabs_with_layout_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1035,6 +1041,7 @@ pub fn new_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1102,6 +1109,7 @@ pub fn go_to_next_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1168,6 +1176,7 @@ pub fn go_to_previous_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1234,6 +1243,7 @@ pub fn resize_focused_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1300,6 +1310,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1366,6 +1377,7 @@ pub fn focus_next_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1432,6 +1444,7 @@ pub fn focus_previous_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1498,6 +1511,7 @@ pub fn move_focus_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1564,6 +1578,7 @@ pub fn move_focus_or_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1630,6 +1645,7 @@ pub fn edit_scrollback_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1696,6 +1712,7 @@ pub fn write_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1762,6 +1779,7 @@ pub fn write_chars_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1828,6 +1846,7 @@ pub fn toggle_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1894,6 +1913,7 @@ pub fn move_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -1960,6 +1980,7 @@ pub fn move_pane_with_direction_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2027,7 +2048,7 @@ pub fn clear_screen_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(100)); std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2095,6 +2116,7 @@ pub fn scroll_up_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2161,6 +2183,7 @@ pub fn scroll_down_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2227,6 +2250,7 @@ pub fn scroll_to_top_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2293,6 +2317,7 @@ pub fn scroll_to_bottom_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2359,6 +2384,7 @@ pub fn page_scroll_up_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2425,6 +2451,7 @@ pub fn page_scroll_down_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2491,6 +2518,7 @@ pub fn toggle_focus_fullscreen_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2557,6 +2585,7 @@ pub fn toggle_pane_frames_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2623,6 +2652,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2689,6 +2719,7 @@ pub fn undo_rename_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2755,6 +2786,7 @@ pub fn close_focus_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2821,6 +2853,7 @@ pub fn toggle_active_tab_sync_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2887,6 +2920,7 @@ pub fn close_focused_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -2953,6 +2987,7 @@ pub fn undo_rename_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3019,6 +3054,7 @@ pub fn previous_swap_layout_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3085,6 +3121,7 @@ pub fn next_swap_layout_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3151,6 +3188,7 @@ pub fn go_to_tab_name_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3217,6 +3255,7 @@ pub fn focus_or_create_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3283,6 +3322,7 @@ pub fn go_to_tab() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3349,6 +3389,7 @@ pub fn start_or_reload_plugin() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3422,6 +3463,7 @@ pub fn quit_zellij_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3495,6 +3537,7 @@ pub fn detach_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3568,6 +3611,7 @@ pub fn open_file_floating_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3641,6 +3685,7 @@ pub fn open_file_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3715,6 +3760,7 @@ pub fn open_file_with_line_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3788,6 +3834,7 @@ pub fn open_file_with_line_floating_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3861,6 +3908,7 @@ pub fn open_terminal_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -3934,6 +3982,7 @@ pub fn open_terminal_floating_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4007,6 +4056,7 @@ pub fn open_command_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4080,6 +4130,7 @@ pub fn open_command_pane_floating_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4146,6 +4197,7 @@ pub fn switch_to_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4207,6 +4259,7 @@ pub fn hide_self_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4268,6 +4321,7 @@ pub fn show_self_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4334,6 +4388,7 @@ pub fn close_terminal_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4400,6 +4455,7 @@ pub fn close_plugin_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4466,6 +4522,7 @@ pub fn focus_terminal_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4532,6 +4589,7 @@ pub fn focus_plugin_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4598,6 +4656,7 @@ pub fn rename_terminal_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4664,6 +4723,7 @@ pub fn rename_plugin_pane_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4730,6 +4790,7 @@ pub fn rename_tab_plugin_command() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4805,6 +4866,7 @@ pub fn send_configuration_to_plugins() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4868,6 +4930,7 @@ pub fn request_plugin_permissions() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -4955,6 +5018,7 @@ pub fn granted_permission_request_result() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),
@ -5040,6 +5104,7 @@ pub fn denied_permission_request_result() {
client_id, client_id,
size, size,
)); ));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![( let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None, None,
Some(client_id), Some(client_id),

View file

@ -1,11 +1,11 @@
--- ---
source: zellij-server/src/plugins/./unit/plugin_tests.rs source: zellij-server/src/plugins/./unit/plugin_tests.rs
assertion_line: 2442 assertion_line: 3306
expression: "format!(\"{:#?}\", new_tab_event)" expression: "format!(\"{:#?}\", new_tab_event)"
--- ---
Some( Some(
GoToTab( GoToTab(
2, 3,
Some( Some(
1, 1,
), ),

View file

@ -782,6 +782,7 @@ fn check_event_permission(
Event::ModeUpdate(..) Event::ModeUpdate(..)
| Event::TabUpdate(..) | Event::TabUpdate(..)
| Event::PaneUpdate(..) | Event::PaneUpdate(..)
| Event::SessionUpdate(..)
| Event::CopyToClipboard(..) | Event::CopyToClipboard(..)
| Event::SystemClipboardFailure | Event::SystemClipboardFailure
| Event::InputReceived => PermissionType::ReadApplicationState, | Event::InputReceived => PermissionType::ReadApplicationState,

View file

@ -2,6 +2,7 @@ use super::PluginInstruction;
use crate::plugins::plugin_map::{PluginEnv, Subscriptions}; use crate::plugins::plugin_map::{PluginEnv, Subscriptions};
use crate::plugins::wasm_bridge::handle_plugin_crash; use crate::plugins::wasm_bridge::handle_plugin_crash;
use crate::route::route_action; use crate::route::route_action;
use crate::ServerInstruction;
use log::{debug, warn}; use log::{debug, warn};
use serde::Serialize; use serde::Serialize;
use std::{ use std::{
@ -15,7 +16,9 @@ use std::{
}; };
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv}; use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
use wasmer_wasi::WasiEnv; use wasmer_wasi::WasiEnv;
use zellij_utils::data::{CommandType, PermissionStatus, PermissionType, PluginPermission}; use zellij_utils::data::{
CommandType, ConnectToSession, PermissionStatus, PermissionType, PluginPermission,
};
use zellij_utils::input::permission::PermissionCache; use zellij_utils::input::permission::PermissionCache;
use url::Url; use url::Url;
@ -205,6 +208,12 @@ fn host_run_plugin_command(env: &ForeignFunctionEnv) {
PluginCommand::RequestPluginPermissions(permissions) => { PluginCommand::RequestPluginPermissions(permissions) => {
request_permission(env, permissions)? request_permission(env, permissions)?
}, },
PluginCommand::SwitchSession(connect_to_session) => switch_session(
env,
connect_to_session.name,
connect_to_session.tab_position,
connect_to_session.pane_id,
)?,
}, },
(PermissionStatus::Denied, permission) => { (PermissionStatus::Denied, permission) => {
log::error!( log::error!(
@ -679,6 +688,31 @@ fn detach(env: &ForeignFunctionEnv) {
apply_action!(action, error_msg, env); apply_action!(action, error_msg, env);
} }
fn switch_session(
env: &ForeignFunctionEnv,
session_name: Option<String>,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
) -> Result<()> {
// pane_id is (id, is_plugin)
let err_context = || format!("Failed to switch session");
let client_id = env.plugin_env.client_id;
let tab_position = tab_position.map(|p| p + 1); // ¯\_()_/¯
let connect_to_session = ConnectToSession {
name: session_name,
tab_position,
pane_id,
};
env.plugin_env
.senders
.send_to_server(ServerInstruction::SwitchSession(
connect_to_session,
client_id,
))
.with_context(err_context)?;
Ok(())
}
fn edit_scrollback(env: &ForeignFunctionEnv) { fn edit_scrollback(env: &ForeignFunctionEnv) {
let action = Action::EditScrollback; let action = Action::EditScrollback;
let error_msg = || format!("Failed to edit scrollback"); let error_msg = || format!("Failed to edit scrollback");
@ -898,7 +932,7 @@ fn go_to_tab(env: &ForeignFunctionEnv, tab_index: u32) {
env.plugin_env.name() env.plugin_env.name()
) )
}; };
let action = Action::GoToTab(tab_index); let action = Action::GoToTab(tab_index + 1);
apply_action!(action, error_msg, env); apply_action!(action, error_msg, env);
} }
@ -1113,7 +1147,6 @@ fn check_command_permission(
_ => return (PermissionStatus::Granted, None), _ => return (PermissionStatus::Granted, None),
}; };
log::info!("plugin permissions: {:?}", plugin_env.permissions);
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() { if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
if permissions.contains(&permission) { if permissions.contains(&permission) {
return (PermissionStatus::Granted, None); return (PermissionStatus::Granted, None);

View file

@ -860,9 +860,19 @@ pub(crate) fn route_thread_main(
.send(new_client_instruction) .send(new_client_instruction)
.with_context(err_context)?; .with_context(err_context)?;
}, },
ClientToServerMsg::AttachClient(client_attributes, opts) => { ClientToServerMsg::AttachClient(
let attach_client_instruction = client_attributes,
ServerInstruction::AttachClient(client_attributes, opts, client_id); opts,
tab_position_to_focus,
pane_id_to_focus,
) => {
let attach_client_instruction = ServerInstruction::AttachClient(
client_attributes,
opts,
tab_position_to_focus,
pane_id_to_focus,
client_id,
);
to_server to_server
.send(attach_client_instruction) .send(attach_client_instruction)
.with_context(err_context)?; .with_context(err_context)?;

View file

@ -6,7 +6,9 @@ use std::path::PathBuf;
use std::rc::Rc; use std::rc::Rc;
use std::str; use std::str;
use zellij_utils::data::{Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy}; use zellij_utils::data::{
Direction, PaneManifest, PluginPermission, Resize, ResizeStrategy, SessionInfo,
};
use zellij_utils::errors::prelude::*; use zellij_utils::errors::prelude::*;
use zellij_utils::input::command::RunCommand; use zellij_utils::input::command::RunCommand;
use zellij_utils::input::options::Clipboard; use zellij_utils::input::options::Clipboard;
@ -242,7 +244,11 @@ pub enum ScreenInstruction {
MouseHoldRight(Position, ClientId), MouseHoldRight(Position, ClientId),
MouseHoldMiddle(Position, ClientId), MouseHoldMiddle(Position, ClientId),
Copy(ClientId), Copy(ClientId),
AddClient(ClientId), AddClient(
ClientId,
Option<usize>, // tab position to focus
Option<(u32, bool)>, // (pane_id, is_plugin) => pane_id to focus
),
RemoveClient(ClientId), RemoveClient(ClientId),
AddOverlay(Overlay, ClientId), AddOverlay(Overlay, ClientId),
RemoveOverlay(ClientId), RemoveOverlay(ClientId),
@ -287,6 +293,7 @@ pub enum ScreenInstruction {
BreakPane(Box<Layout>, Option<TerminalAction>, ClientId), BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
BreakPaneRight(ClientId), BreakPaneRight(ClientId),
BreakPaneLeft(ClientId), BreakPaneLeft(ClientId),
UpdateSessionInfos(BTreeMap<String, SessionInfo>), // String is the session name
} }
impl From<&ScreenInstruction> for ScreenContext { impl From<&ScreenInstruction> for ScreenContext {
@ -460,6 +467,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane, ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane,
ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight, ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight,
ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft, ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft,
ScreenInstruction::UpdateSessionInfos(..) => ScreenContext::UpdateSessionInfos,
} }
} }
} }
@ -524,6 +532,9 @@ pub(crate) struct Screen {
session_is_mirrored: bool, session_is_mirrored: bool,
copy_options: CopyOptions, copy_options: CopyOptions,
debug: bool, debug: bool,
session_name: String,
session_infos_on_machine: BTreeMap<String, SessionInfo>, // String is the session name, can
// also be this session
} }
impl Screen { impl Screen {
@ -539,6 +550,10 @@ impl Screen {
copy_options: CopyOptions, copy_options: CopyOptions,
debug: bool, debug: bool,
) -> Self { ) -> Self {
let session_name = mode_info.session_name.clone().unwrap_or_default();
let session_info = SessionInfo::new(session_name.clone());
let mut session_infos_on_machine = BTreeMap::new();
session_infos_on_machine.insert(session_name.clone(), session_info);
Screen { Screen {
bus, bus,
max_panes, max_panes,
@ -561,6 +576,8 @@ impl Screen {
session_is_mirrored, session_is_mirrored,
copy_options, copy_options,
debug, debug,
session_name,
session_infos_on_machine,
} }
} }
@ -745,8 +762,8 @@ impl Screen {
.non_fatal(); .non_fatal();
} }
self.report_tab_state().with_context(err_context)?; self.log_and_report_session_state()
self.report_pane_state().with_context(err_context)?; .with_context(err_context)?;
return self.render().with_context(err_context); return self.render().with_context(err_context);
}, },
Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
@ -879,8 +896,8 @@ impl Screen {
t.position -= 1; t.position -= 1;
} }
} }
self.report_tab_state().with_context(err_context)?; self.log_and_report_session_state()
self.report_pane_state().with_context(err_context)?; .with_context(err_context)?;
self.render().with_context(err_context) self.render().with_context(err_context)
} }
} }
@ -917,7 +934,8 @@ impl Screen {
.with_context(err_context)?; .with_context(err_context)?;
tab.set_force_render(); tab.set_force_render();
} }
self.report_pane_state().with_context(err_context)?; self.log_and_report_session_state()
.with_context(err_context)?;
self.render().with_context(err_context) self.render().with_context(err_context)
} }
@ -1183,10 +1201,9 @@ impl Screen {
self.add_client(client_id).with_context(err_context)?; self.add_client(client_id).with_context(err_context)?;
} }
self.report_tab_state() self.log_and_report_session_state()
.and_then(|_| self.render()) .and_then(|_| self.render())
.with_context(err_context)?; .with_context(err_context)
self.report_pane_state().with_context(err_context)
} }
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> { pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
@ -1237,13 +1254,38 @@ impl Screen {
self.tab_history.remove(&client_id); self.tab_history.remove(&client_id);
} }
self.connected_clients.borrow_mut().remove(&client_id); self.connected_clients.borrow_mut().remove(&client_id);
self.report_tab_state().with_context(err_context) self.log_and_report_session_state()
.with_context(err_context)
} }
pub fn report_tab_state(&self) -> Result<()> { pub fn generate_and_report_tab_state(&mut self) -> Result<Vec<TabInfo>> {
let mut plugin_updates = vec![]; let mut plugin_updates = vec![];
let mut tab_infos_for_screen_state = BTreeMap::new();
for tab in self.tabs.values() {
let all_focused_clients: Vec<ClientId> = self
.active_tab_indices
.iter()
.filter(|(_c_id, tab_position)| **tab_position == tab.index)
.map(|(c_id, _)| c_id)
.copied()
.collect();
let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info();
let tab_info_for_screen = TabInfo {
position: tab.position,
name: tab.name.clone(),
active: self.active_tab_indices.values().any(|i| i == &tab.index),
panes_to_hide: tab.panes_to_hide_count(),
is_fullscreen_active: tab.is_fullscreen_active(),
is_sync_panes_active: tab.is_sync_panes_active(),
are_floating_panes_visible: tab.are_floating_panes_visible(),
other_focused_clients: all_focused_clients,
active_swap_layout_name,
is_swap_layout_dirty,
};
tab_infos_for_screen_state.insert(tab.position, tab_info_for_screen);
}
for (client_id, active_tab_index) in self.active_tab_indices.iter() { for (client_id, active_tab_index) in self.active_tab_indices.iter() {
let mut tab_data = vec![]; let mut plugin_tab_updates = vec![];
for tab in self.tabs.values() { for tab in self.tabs.values() {
let other_focused_clients: Vec<ClientId> = if self.session_is_mirrored { let other_focused_clients: Vec<ClientId> = if self.session_is_mirrored {
vec![] vec![]
@ -1258,7 +1300,7 @@ impl Screen {
.collect() .collect()
}; };
let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info(); let (active_swap_layout_name, is_swap_layout_dirty) = tab.swap_layout_info();
tab_data.push(TabInfo { let tab_info_for_plugins = TabInfo {
position: tab.position, position: tab.position,
name: tab.name.clone(), name: tab.name.clone(),
active: *active_tab_index == tab.index, active: *active_tab_index == tab.index,
@ -1269,17 +1311,18 @@ impl Screen {
other_focused_clients, other_focused_clients,
active_swap_layout_name, active_swap_layout_name,
is_swap_layout_dirty, is_swap_layout_dirty,
}); };
plugin_tab_updates.push(tab_info_for_plugins);
} }
plugin_updates.push((None, Some(*client_id), Event::TabUpdate(tab_data))); plugin_updates.push((None, Some(*client_id), Event::TabUpdate(plugin_tab_updates)));
} }
self.bus self.bus
.senders .senders
.send_to_plugin(PluginInstruction::Update(plugin_updates)) .send_to_plugin(PluginInstruction::Update(plugin_updates))
.context("failed to update tabs")?; .context("failed to update tabs")?;
Ok(()) Ok(tab_infos_for_screen_state.values().cloned().collect())
} }
fn report_pane_state(&self) -> Result<()> { fn generate_and_report_pane_state(&mut self) -> Result<PaneManifest> {
let mut pane_manifest = PaneManifest::default(); let mut pane_manifest = PaneManifest::default();
for tab in self.tabs.values() { for tab in self.tabs.values() {
pane_manifest.panes.insert(tab.position, tab.pane_infos()); pane_manifest.panes.insert(tab.position, tab.pane_infos());
@ -1289,9 +1332,51 @@ impl Screen {
.send_to_plugin(PluginInstruction::Update(vec![( .send_to_plugin(PluginInstruction::Update(vec![(
None, None,
None, None,
Event::PaneUpdate(pane_manifest), Event::PaneUpdate(pane_manifest.clone()),
)])) )]))
.context("failed to update tabs")?; .context("failed to update tabs")?;
Ok(pane_manifest)
}
fn log_and_report_session_state(&mut self) -> Result<()> {
let err_context = || format!("Failed to log and report session state");
// generate own session info
let pane_manifest = self.generate_and_report_pane_state()?;
let tab_infos = self.generate_and_report_tab_state()?;
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,
};
self.bus
.senders
.send_to_background_jobs(BackgroundJob::ReportSessionInfo(
self.session_name.to_owned(),
session_info,
))
.with_context(err_context)?;
self.bus
.senders
.send_to_background_jobs(BackgroundJob::ReadAllSessionInfosOnMachine)
.with_context(err_context)?;
Ok(())
}
pub fn update_session_infos(
&mut self,
new_session_infos: BTreeMap<String, SessionInfo>,
) -> Result<()> {
self.session_infos_on_machine = new_session_infos;
self.bus
.senders
.send_to_plugin(PluginInstruction::Update(vec![(
None,
None,
Event::SessionUpdate(self.session_infos_on_machine.values().cloned().collect()),
)]))
.context("failed to update session info")?;
Ok(()) Ok(())
} }
@ -1327,7 +1412,8 @@ impl Screen {
} }
}, },
} }
self.report_tab_state().with_context(err_context) self.log_and_report_session_state()
.with_context(err_context)
}, },
Err(err) => { Err(err) => {
Err::<(), _>(err).with_context(err_context).non_fatal(); Err::<(), _>(err).with_context(err_context).non_fatal();
@ -1352,7 +1438,7 @@ impl Screen {
Ok(active_tab) => { Ok(active_tab) => {
if active_tab.name != active_tab.prev_name { if active_tab.name != active_tab.prev_name {
active_tab.name = active_tab.prev_name.clone(); active_tab.name = active_tab.prev_name.clone();
self.report_tab_state() self.log_and_report_session_state()
.context("failed to undo renaming of active tab")?; .context("failed to undo renaming of active tab")?;
} }
}, },
@ -1473,7 +1559,8 @@ impl Screen {
Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
}; };
} }
self.report_pane_state().with_context(err_context)?; self.log_and_report_session_state()
.with_context(err_context)?;
Ok(()) Ok(())
} }
pub fn move_focus_right_or_next_tab(&mut self, client_id: ClientId) -> Result<()> { pub fn move_focus_right_or_next_tab(&mut self, client_id: ClientId) -> Result<()> {
@ -1508,7 +1595,8 @@ impl Screen {
Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(), Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
}; };
} }
self.report_pane_state().with_context(err_context)?; self.log_and_report_session_state()
.with_context(err_context)?;
Ok(()) Ok(())
} }
pub fn toggle_tab(&mut self, client_id: ClientId) -> Result<()> { pub fn toggle_tab(&mut self, client_id: ClientId) -> Result<()> {
@ -1521,8 +1609,8 @@ impl Screen {
.context("failed to toggle tabs")?; .context("failed to toggle tabs")?;
}; };
self.report_tab_state().context("failed to toggle tabs")?; self.log_and_report_session_state()
self.report_pane_state().context("failed to toggle tabs")?; .context("failed to toggle tabs")?;
self.render() self.render()
} }
@ -1550,7 +1638,8 @@ impl Screen {
.with_context(err_context)? .with_context(err_context)?
.focus_pane_with_id(plugin_pane_id, should_float, client_id) .focus_pane_with_id(plugin_pane_id, should_float, client_id)
.context("failed to focus plugin pane")?; .context("failed to focus plugin pane")?;
self.report_pane_state().with_context(err_context)?; self.log_and_report_session_state()
.with_context(err_context)?;
Ok(true) Ok(true)
}, },
None => Ok(false), None => Ok(false),
@ -1684,8 +1773,7 @@ impl Screen {
new_active_tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?; new_active_tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?;
} }
self.report_tab_state()?; self.log_and_report_session_state()?;
self.report_pane_state()?;
} else { } else {
let active_pane_id = { let active_pane_id = {
let active_tab = self.get_active_tab_mut(client_id)?; let active_tab = self.get_active_tab_mut(client_id)?;
@ -1843,8 +1931,7 @@ pub(crate) fn screen_thread_main(
}, },
}; };
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
@ -1852,8 +1939,7 @@ pub(crate) fn screen_thread_main(
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.suppress_active_pane(pid, client_id), ?); .suppress_active_pane(pid, client_id), ?);
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
@ -1861,8 +1947,7 @@ pub(crate) fn screen_thread_main(
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab
.toggle_pane_embed_or_floating(client_id), ?); .toggle_pane_embed_or_floating(client_id), ?);
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
@ -1870,8 +1955,7 @@ pub(crate) fn screen_thread_main(
active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab active_tab_and_connected_client_id!(screen, client_id, |tab: &mut Tab, client_id: ClientId| tab
.toggle_floating_panes(Some(client_id), default_shell), ?); .toggle_floating_panes(Some(client_id), default_shell), ?);
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
@ -1901,8 +1985,7 @@ pub(crate) fn screen_thread_main(
); );
} }
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::VerticalSplit( ScreenInstruction::VerticalSplit(
@ -1931,8 +2014,7 @@ pub(crate) fn screen_thread_main(
); );
} }
screen.unblock_input()?; screen.unblock_input()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::WriteCharacter(bytes, client_id) => { ScreenInstruction::WriteCharacter(bytes, client_id) => {
@ -1953,8 +2035,7 @@ pub(crate) fn screen_thread_main(
? ?
); );
if state_changed { if state_changed {
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
} }
}, },
ScreenInstruction::Resize(client_id, strategy) => { ScreenInstruction::Resize(client_id, strategy) => {
@ -1966,8 +2047,7 @@ pub(crate) fn screen_thread_main(
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
}, },
ScreenInstruction::SwitchFocus(client_id) => { ScreenInstruction::SwitchFocus(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -1977,7 +2057,7 @@ pub(crate) fn screen_thread_main(
); );
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::FocusNextPane(client_id) => { ScreenInstruction::FocusNextPane(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -1996,7 +2076,7 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusLeft(client_id) => { ScreenInstruction::MoveFocusLeft(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2007,13 +2087,13 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => { ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => {
screen.move_focus_left_or_previous_tab(client_id)?; screen.move_focus_left_or_previous_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusDown(client_id) => { ScreenInstruction::MoveFocusDown(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2024,7 +2104,7 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusRight(client_id) => { ScreenInstruction::MoveFocusRight(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2035,13 +2115,13 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusRightOrNextTab(client_id) => { ScreenInstruction::MoveFocusRightOrNextTab(client_id) => {
screen.move_focus_right_or_next_tab(client_id)?; screen.move_focus_right_or_next_tab(client_id)?;
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MoveFocusUp(client_id) => { ScreenInstruction::MoveFocusUp(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2052,7 +2132,7 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ClearScreen(client_id) => { ScreenInstruction::ClearScreen(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2088,7 +2168,7 @@ pub(crate) fn screen_thread_main(
? ?
); );
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ScrollUp(client_id) => { ScreenInstruction::ScrollUp(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2105,10 +2185,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MovePaneBackwards(client_id) => { ScreenInstruction::MovePaneBackwards(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2116,10 +2195,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_backwards(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_backwards(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MovePaneDown(client_id) => { ScreenInstruction::MovePaneDown(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2127,10 +2205,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_down(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_down(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MovePaneUp(client_id) => { ScreenInstruction::MovePaneUp(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2138,10 +2215,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_up(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_up(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MovePaneRight(client_id) => { ScreenInstruction::MovePaneRight(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2149,10 +2225,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_right(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_right(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::MovePaneLeft(client_id) => { ScreenInstruction::MovePaneLeft(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2160,10 +2235,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.move_active_pane_left(client_id) |tab: &mut Tab, client_id: ClientId| tab.move_active_pane_left(client_id)
); );
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ScrollUpAt(point, client_id) => { ScreenInstruction::ScrollUpAt(point, client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2270,10 +2344,9 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ? |tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ?
); );
screen.report_tab_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::SetSelectable(id, selectable, tab_index) => { ScreenInstruction::SetSelectable(id, selectable, tab_index) => {
screen.get_indexed_tab_mut(tab_index).map_or_else( screen.get_indexed_tab_mut(tab_index).map_or_else(
@ -2288,7 +2361,7 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::ClosePane(id, client_id) => { ScreenInstruction::ClosePane(id, client_id) => {
match client_id { match client_id {
@ -2308,9 +2381,8 @@ pub(crate) fn screen_thread_main(
} }
}, },
} }
screen.report_tab_state()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::HoldPane(id, exit_status, run_command, tab_index, client_id) => { ScreenInstruction::HoldPane(id, exit_status, run_command, tab_index, client_id) => {
let is_first_run = false; let is_first_run = false;
@ -2339,9 +2411,8 @@ pub(crate) fn screen_thread_main(
} }
}, },
} }
screen.report_tab_state()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::UpdatePaneName(c, client_id) => { ScreenInstruction::UpdatePaneName(c, client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2351,7 +2422,7 @@ pub(crate) fn screen_thread_main(
); );
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::UndoRenamePane(client_id) => { ScreenInstruction::UndoRenamePane(client_id) => {
active_tab_and_connected_client_id!( active_tab_and_connected_client_id!(
@ -2369,10 +2440,9 @@ pub(crate) fn screen_thread_main(
|tab: &mut Tab, client_id: ClientId| tab |tab: &mut Tab, client_id: ClientId| tab
.toggle_active_pane_fullscreen(client_id) .toggle_active_pane_fullscreen(client_id)
); );
screen.report_tab_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::TogglePaneFrames => { ScreenInstruction::TogglePaneFrames => {
screen.draw_pane_frames = !screen.draw_pane_frames; screen.draw_pane_frames = !screen.draw_pane_frames;
@ -2381,7 +2451,7 @@ pub(crate) fn screen_thread_main(
} }
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::SwitchTabNext(client_id) => { ScreenInstruction::SwitchTabNext(client_id) => {
screen.switch_tab_next(None, true, client_id)?; screen.switch_tab_next(None, true, client_id)?;
@ -2529,7 +2599,7 @@ pub(crate) fn screen_thread_main(
}, },
ScreenInstruction::TerminalResize(new_size) => { ScreenInstruction::TerminalResize(new_size) => {
screen.resize_to_screen(new_size)?; screen.resize_to_screen(new_size)?;
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins screen.log_and_report_session_state()?; // update tabs so that the ui indication will be send to the plugins
screen.render()?; screen.render()?;
}, },
ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => { ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => {
@ -2560,31 +2630,28 @@ pub(crate) fn screen_thread_main(
client_id, client_id,
|tab: &mut Tab, _client_id: ClientId| tab.toggle_sync_panes_is_active() |tab: &mut Tab, _client_id: ClientId| tab.toggle_sync_panes_is_active()
); );
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::LeftClick(point, client_id) => { ScreenInstruction::LeftClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_left_click(&point, client_id), ?); .handle_left_click(&point, client_id), ?);
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::RightClick(point, client_id) => { ScreenInstruction::RightClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_right_click(&point, client_id), ?); .handle_right_click(&point, client_id), ?);
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::MiddleClick(point, client_id) => { ScreenInstruction::MiddleClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_middle_click(&point, client_id), ?); .handle_middle_click(&point, client_id), ?);
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
@ -2632,15 +2699,26 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?; screen.unblock_input()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::AddClient(client_id) => { ScreenInstruction::AddClient(client_id, tab_position_to_focus, pane_id_to_focus) => {
screen.add_client(client_id)?; screen.add_client(client_id)?;
screen.report_tab_state()?; let pane_id = pane_id_to_focus.map(|(pane_id, is_plugin)| {
screen.report_pane_state()?; if is_plugin {
PaneId::Plugin(pane_id)
} else {
PaneId::Terminal(pane_id)
}
});
if let Some(pane_id) = pane_id {
screen.focus_pane_with_id(pane_id, true, client_id)?;
} else if let Some(tab_position_to_focus) = tab_position_to_focus {
screen.go_to_tab(tab_position_to_focus, client_id)?;
}
screen.log_and_report_session_state()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::RemoveClient(client_id) => { ScreenInstruction::RemoveClient(client_id) => {
screen.remove_client(client_id)?; screen.remove_client(client_id)?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::AddOverlay(overlay, _client_id) => { ScreenInstruction::AddOverlay(overlay, _client_id) => {
@ -2755,8 +2833,7 @@ pub(crate) fn screen_thread_main(
? ?
); );
screen.render()?; screen.render()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::NextSwapLayout(client_id) => { ScreenInstruction::NextSwapLayout(client_id) => {
@ -2767,8 +2844,7 @@ pub(crate) fn screen_thread_main(
? ?
); );
screen.render()?; screen.render()?;
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::QueryTabNames(client_id) => { ScreenInstruction::QueryTabNames(client_id) => {
@ -2852,7 +2928,7 @@ pub(crate) fn screen_thread_main(
} else { } else {
log::error!("Tab index not found: {:?}", tab_index); log::error!("Tab index not found: {:?}", tab_index);
} }
screen.report_pane_state()?; screen.log_and_report_session_state()?;
screen.unblock_input()?; screen.unblock_input()?;
}, },
ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => { ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => {
@ -2890,8 +2966,7 @@ pub(crate) fn screen_thread_main(
for tab in all_tabs.values_mut() { for tab in all_tabs.values_mut() {
tab.update_input_modes()?; tab.update_input_modes()?;
} }
screen.report_tab_state()?; screen.log_and_report_session_state()?;
screen.report_pane_state()?;
screen.render()?; screen.render()?;
}, },
ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => { ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => {
@ -2910,7 +2985,7 @@ pub(crate) fn screen_thread_main(
Some((tab_index, client_id)) => { Some((tab_index, client_id)) => {
if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? { if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? {
screen.render()?; screen.render()?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
} else { } else {
screen.bus.senders.send_to_plugin(PluginInstruction::Load( screen.bus.senders.send_to_plugin(PluginInstruction::Load(
Some(should_float), Some(should_float),
@ -2934,12 +3009,11 @@ pub(crate) fn screen_thread_main(
break; break;
} }
} }
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::FocusPaneWithId(pane_id, should_float_if_hidden, client_id) => { ScreenInstruction::FocusPaneWithId(pane_id, should_float_if_hidden, client_id) => {
screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?; screen.focus_pane_with_id(pane_id, should_float_if_hidden, client_id)?;
screen.report_pane_state()?; screen.log_and_report_session_state()?;
screen.report_tab_state()?;
}, },
ScreenInstruction::RenamePane(pane_id, new_name) => { ScreenInstruction::RenamePane(pane_id, new_name) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -2952,7 +3026,7 @@ pub(crate) fn screen_thread_main(
break; break;
} }
} }
screen.report_pane_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::RenameTab(tab_index, new_name) => { ScreenInstruction::RenameTab(tab_index, new_name) => {
match screen.tabs.get_mut(&tab_index.saturating_sub(1)) { match screen.tabs.get_mut(&tab_index.saturating_sub(1)) {
@ -2963,7 +3037,7 @@ pub(crate) fn screen_thread_main(
log::error!("Failed to find tab with index: {:?}", tab_index); log::error!("Failed to find tab with index: {:?}", tab_index);
}, },
} }
screen.report_tab_state()?; screen.log_and_report_session_state()?;
}, },
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => { ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
let all_tabs = screen.get_tabs_mut(); let all_tabs = screen.get_tabs_mut();
@ -2992,6 +3066,9 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::BreakPaneLeft(client_id) => { ScreenInstruction::BreakPaneLeft(client_id) => {
screen.break_pane_to_new_tab(Direction::Left, client_id)?; screen.break_pane_to_new_tab(Direction::Left, client_id)?;
}, },
ScreenInstruction::UpdateSessionInfos(new_session_infos) => {
screen.update_session_infos(new_session_infos)?;
},
} }
} }
Ok(()) Ok(())

View file

@ -3328,6 +3328,7 @@ impl Tab {
// this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true) // this function is to be preferred to directly invoking floating_panes.toggle_show_panes(true)
self.floating_panes.toggle_show_panes(true); self.floating_panes.toggle_show_panes(true);
self.tiled_panes.unfocus_all_panes(); self.tiled_panes.unfocus_all_panes();
self.set_force_render();
} }
pub fn hide_floating_panes(&mut self) { pub fn hide_floating_panes(&mut self) {
@ -3335,6 +3336,7 @@ impl Tab {
// floating_panes.toggle_show_panes(false) // floating_panes.toggle_show_panes(false)
self.floating_panes.toggle_show_panes(false); self.floating_panes.toggle_show_panes(false);
self.tiled_panes.focus_all_panes(); self.tiled_panes.focus_all_panes();
self.set_force_render();
} }
pub fn find_plugin(&self, run_plugin: &RunPlugin) -> Option<PaneId> { pub fn find_plugin(&self, run_plugin: &RunPlugin) -> Option<PaneId> {
@ -3359,6 +3361,7 @@ impl Tab {
// TODO: should error if pane is not selectable // TODO: should error if pane is not selectable
self.tiled_panes self.tiled_panes
.focus_pane_if_exists(pane_id, client_id) .focus_pane_if_exists(pane_id, client_id)
.map(|_| self.hide_floating_panes())
.or_else(|_| { .or_else(|_| {
let focused_floating_pane = let focused_floating_pane =
self.floating_panes.focus_pane_if_exists(pane_id, client_id); self.floating_panes.focus_pane_if_exists(pane_id, client_id);

View file

@ -563,6 +563,34 @@ where
unsafe { host_run_plugin_command() }; unsafe { host_run_plugin_command() };
} }
/// Switch to a session with the given name, create one if no name is given
pub fn switch_session(name: Option<&str>) {
let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
name: name.map(|n| n.to_string()),
..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(
name: &str,
tab_position: Option<usize>,
pane_id: Option<(u32, bool)>,
) {
let plugin_command = PluginCommand::SwitchSession(ConnectToSession {
name: Some(name.to_owned()),
tab_position,
pane_id,
});
let protobuf_plugin_command: ProtobufPluginCommand = plugin_command.try_into().unwrap();
object_to_stdout(&protobuf_plugin_command.encode_to_vec());
unsafe { host_run_plugin_command() };
}
// Utility Functions // Utility Functions
/// Returns the `TabInfo` corresponding to the currently active tab /// Returns the `TabInfo` corresponding to the currently active tab

View file

@ -113,6 +113,12 @@ keybinds {
bind "Ctrl o" { SwitchToMode "Normal"; } bind "Ctrl o" { SwitchToMode "Normal"; }
bind "Ctrl s" { SwitchToMode "Scroll"; } bind "Ctrl s" { SwitchToMode "Scroll"; }
bind "d" { Detach; } bind "d" { Detach; }
bind "w" {
LaunchOrFocusPlugin "zellij:session-manager" {
floating true
};
SwitchToMode "Normal"
}
} }
tmux { tmux {
bind "[" { SwitchToMode "Scroll"; } bind "[" { SwitchToMode "Scroll"; }
@ -181,6 +187,7 @@ plugins {
status-bar { path "status-bar"; } status-bar { path "status-bar"; }
strider { path "strider"; } strider { path "strider"; }
compact-bar { path "compact-bar"; } compact-bar { path "compact-bar"; }
session-manager { path "session-manager"; }
} }
// Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP // Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP

Binary file not shown.

View file

@ -38,6 +38,8 @@ lazy_static! {
.join(format!("{}", Uuid::new_v4())); .join(format!("{}", Uuid::new_v4()));
pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf = pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf =
ZELLIJ_CACHE_DIR.join("permissions.kdl"); ZELLIJ_CACHE_DIR.join("permissions.kdl");
pub static ref ZELLIJ_SESSION_INFO_CACHE_DIR: PathBuf =
ZELLIJ_CACHE_DIR.join(VERSION).join("session_info");
} }
pub const FEATURES: &[&str] = &[ pub const FEATURES: &[&str] = &[
@ -92,6 +94,7 @@ mod not_wasm {
add_plugin!(assets, "status-bar.wasm"); add_plugin!(assets, "status-bar.wasm");
add_plugin!(assets, "tab-bar.wasm"); add_plugin!(assets, "tab-bar.wasm");
add_plugin!(assets, "strider.wasm"); add_plugin!(assets, "strider.wasm");
add_plugin!(assets, "session-manager.wasm");
assets assets
}; };
} }
@ -104,20 +107,13 @@ pub use unix_only::*;
mod unix_only { mod unix_only {
use super::*; use super::*;
use crate::envs; use crate::envs;
use crate::shared::set_permissions; pub use crate::shared::set_permissions;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use nix::unistd::Uid; use nix::unistd::Uid;
use std::{env::temp_dir, fs}; use std::env::temp_dir;
lazy_static! { lazy_static! {
static ref UID: Uid = Uid::current(); static ref UID: Uid = Uid::current();
pub static ref ZELLIJ_IPC_PIPE: PathBuf = {
let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
fs::create_dir_all(&sock_dir).unwrap();
set_permissions(&sock_dir, 0o700).unwrap();
sock_dir.push(envs::get_session_name().unwrap());
sock_dir
};
pub static ref ZELLIJ_TMP_DIR: PathBuf = temp_dir().join(format!("zellij-{}", *UID)); pub static ref ZELLIJ_TMP_DIR: PathBuf = temp_dir().join(format!("zellij-{}", *UID));
pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log"); pub static ref ZELLIJ_TMP_LOG_DIR: PathBuf = ZELLIJ_TMP_DIR.join("zellij-log");
pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("zellij.log"); pub static ref ZELLIJ_TMP_LOG_FILE: PathBuf = ZELLIJ_TMP_LOG_DIR.join("zellij.log");

View file

@ -495,6 +495,7 @@ pub enum Event {
FileSystemDelete(Vec<PathBuf>), FileSystemDelete(Vec<PathBuf>),
/// A Result of plugin permission request /// A Result of plugin permission request
PermissionRequestResult(PermissionStatus), PermissionRequestResult(PermissionStatus),
SessionUpdate(Vec<SessionInfo>),
} }
#[derive( #[derive(
@ -734,6 +735,42 @@ impl ModeInfo {
} }
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct SessionInfo {
pub name: String,
pub tabs: Vec<TabInfo>,
pub panes: PaneManifest,
pub connected_clients: usize,
pub is_current_session: bool,
}
use std::hash::{Hash, Hasher};
#[allow(clippy::derive_hash_xor_eq)]
impl Hash for SessionInfo {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl SessionInfo {
pub fn new(name: String) -> Self {
SessionInfo {
name,
..Default::default()
}
}
pub fn update_tab_info(&mut self, new_tab_info: Vec<TabInfo>) {
self.tabs = new_tab_info;
}
pub fn update_pane_info(&mut self, new_pane_info: PaneManifest) {
self.panes = new_pane_info;
}
pub fn update_connected_clients(&mut self, new_connected_clients: usize) {
self.connected_clients = new_connected_clients;
}
}
/// Contains all the information for a currently opened tab. /// Contains all the information for a currently opened tab.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
pub struct TabInfo { pub struct TabInfo {
@ -921,6 +958,13 @@ impl CommandToRun {
} }
} }
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub struct ConnectToSession {
pub name: Option<String>,
pub tab_position: Option<usize>,
pub pane_id: Option<(u32, bool)>, // (id, is_plugin)
}
#[derive(Debug, Default, Clone)] #[derive(Debug, Default, Clone)]
pub struct PluginMessage { pub struct PluginMessage {
pub name: String, pub name: String,
@ -1016,4 +1060,5 @@ pub enum PluginCommand {
RenameTab(u32, String), // tab index, new name RenameTab(u32, String), // tab index, new name
ReportPanic(String), // stringified panic ReportPanic(String), // stringified panic
RequestPluginPermissions(Vec<PermissionType>), RequestPluginPermissions(Vec<PermissionType>),
SwitchSession(ConnectToSession),
} }

View file

@ -342,6 +342,7 @@ pub enum ScreenContext {
BreakPane, BreakPane,
BreakPaneRight, BreakPaneRight,
BreakPaneLeft, BreakPaneLeft,
UpdateSessionInfos,
} }
/// Stack call representations corresponding to the different types of [`PtyInstruction`]s. /// Stack call representations corresponding to the different types of [`PtyInstruction`]s.
@ -396,6 +397,7 @@ pub enum ClientContext {
OwnClientId, OwnClientId,
StartedParsingStdinQuery, StartedParsingStdinQuery,
DoneParsingStdinQuery, DoneParsingStdinQuery,
SwitchSession,
} }
/// Stack call representations corresponding to the different types of [`ServerInstruction`]s. /// Stack call representations corresponding to the different types of [`ServerInstruction`]s.
@ -413,6 +415,7 @@ pub enum ServerContext {
ConnStatus, ConnStatus,
ActiveClients, ActiveClients,
Log, Log,
SwitchSession,
} }
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
@ -429,6 +432,8 @@ pub enum BackgroundJobContext {
DisplayPaneError, DisplayPaneError,
AnimatePluginLoading, AnimatePluginLoading,
StopPluginLoadingAnimation, StopPluginLoadingAnimation,
ReadAllSessionInfosOnMachine,
ReportSessionInfo,
Exit, Exit,
} }

View file

@ -27,13 +27,17 @@ impl PermissionCache {
pub fn check_permissions( pub fn check_permissions(
&self, &self,
plugin_name: String, plugin_name: String,
permissions: &Vec<PermissionType>, permissions_to_check: &Vec<PermissionType>,
) -> bool { ) -> bool {
if let Some(target) = self.granted.get(&plugin_name) { if let Some(target) = self.granted.get(&plugin_name) {
if target == permissions { let mut all_granted = true;
return true; for permission in permissions_to_check {
if !target.contains(permission) {
all_granted = false;
} }
} }
return all_granted;
}
false false
} }
@ -43,7 +47,10 @@ impl PermissionCache {
let granted = match fs::read_to_string(cache_path.clone()) { let granted = match fs::read_to_string(cache_path.clone()) {
Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(), Ok(raw_string) => PermissionCache::from_string(raw_string).unwrap_or_default(),
Err(_) => GrantedPermission::default(), Err(e) => {
log::error!("Failed to read permission cache file: {}", e);
GrantedPermission::default()
},
}; };
PermissionCache { PermissionCache {

View file

@ -1,7 +1,7 @@
//! IPC stuff for starting to split things into a client and server model. //! IPC stuff for starting to split things into a client and server model.
use crate::{ use crate::{
cli::CliArgs, cli::CliArgs,
data::{ClientId, InputMode, Style}, data::{ClientId, ConnectToSession, InputMode, Style},
errors::{get_current_ctx, prelude::*, ErrorContext}, errors::{get_current_ctx, prelude::*, ErrorContext},
input::keybinds::Keybinds, input::keybinds::Keybinds,
input::{actions::Action, layout::Layout, options::Options, plugins::PluginsConfig}, input::{actions::Action, layout::Layout, options::Options, plugins::PluginsConfig},
@ -65,16 +65,6 @@ impl PixelDimensions {
#[allow(clippy::large_enum_variant)] #[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ClientToServerMsg { pub enum ClientToServerMsg {
/*// List which sessions are available
ListSessions,
// Create a new session
CreateSession,
// Attach to a running session
AttachToSession(SessionId, ClientType),
// Force detach
DetachSession(SessionId),
// Disconnect from the session we're connected to
DisconnectFromSession,*/
DetachSession(Vec<ClientId>), DetachSession(Vec<ClientId>),
TerminalPixelDimensions(PixelDimensions), TerminalPixelDimensions(PixelDimensions),
BackgroundColor(String), BackgroundColor(String),
@ -88,7 +78,12 @@ pub enum ClientToServerMsg {
Box<Layout>, Box<Layout>,
Option<PluginsConfig>, Option<PluginsConfig>,
), ),
AttachClient(ClientAttributes, Options), AttachClient(
ClientAttributes,
Options,
Option<usize>, // tab position to focus
Option<(u32, bool)>, // (pane_id, is_plugin) => pane id to focus
),
Action(Action, Option<ClientId>), Action(Action, Option<ClientId>),
ClientExited, ClientExited,
KillSession, KillSession,
@ -99,10 +94,6 @@ pub enum ClientToServerMsg {
// Types of messages sent from the server to the client // Types of messages sent from the server to the client
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ServerToClientMsg { pub enum ServerToClientMsg {
/*// Info about a particular session
SessionInfo(Session),
// A list of sessions
SessionList(HashSet<Session>),*/
Render(String), Render(String),
UnblockInputThread, UnblockInputThread,
Exit(ExitReason), Exit(ExitReason),
@ -110,6 +101,7 @@ pub enum ServerToClientMsg {
Connected, Connected,
ActiveClients(Vec<ClientId>), ActiveClients(Vec<ClientId>),
Log(Vec<String>), Log(Vec<String>),
SwitchSession(ConnectToSession),
} }
#[derive(Serialize, Deserialize, Debug, Clone)] #[derive(Serialize, Deserialize, Debug, Clone)]

View file

@ -1,5 +1,8 @@
mod kdl_layout_parser; mod kdl_layout_parser;
use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, PermissionType, Resize}; use crate::data::{
Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType,
Resize, SessionInfo, TabInfo,
};
use crate::envs::EnvironmentVariables; use crate::envs::EnvironmentVariables;
use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds; use crate::input::keybinds::Keybinds;
@ -325,7 +328,6 @@ macro_rules! actions_from_kdl {
pub fn kdl_arguments_that_are_strings<'a>( pub fn kdl_arguments_that_are_strings<'a>(
arguments: impl Iterator<Item = &'a KdlEntry>, arguments: impl Iterator<Item = &'a KdlEntry>,
) -> Result<Vec<String>, ConfigError> { ) -> Result<Vec<String>, ConfigError> {
// pub fn kdl_arguments_that_are_strings <'a>(arguments: impl Iterator<Item=&'a KdlValue>) -> Result<Vec<String>, ConfigError> {
let mut args: Vec<String> = vec![]; let mut args: Vec<String> = vec![];
for kdl_entry in arguments { for kdl_entry in arguments {
match kdl_entry.value().as_string() { match kdl_entry.value().as_string() {
@ -1841,6 +1843,419 @@ impl PermissionCache {
} }
} }
impl SessionInfo {
pub fn from_string(raw_session_info: &str, current_session_name: &str) -> Result<Self, String> {
let kdl_document: KdlDocument = raw_session_info
.parse()
.map_err(|e| format!("Failed to parse kdl document: {}", e))?;
let name = kdl_document
.get("name")
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_string())
.map(|s| s.to_owned())
.ok_or("Failed to parse session name")?;
let connected_clients = kdl_document
.get("connected_clients")
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_i64())
.map(|c| c as usize)
.ok_or("Failed to parse connected_clients")?;
let tabs: Vec<TabInfo> = kdl_document
.get("tabs")
.and_then(|t| t.children())
.and_then(|c| {
let mut tab_nodes = vec![];
for tab_node in c.nodes() {
if let Some(tab) = tab_node.children() {
tab_nodes.push(TabInfo::decode_from_kdl(tab).ok()?);
}
}
Some(tab_nodes)
})
.ok_or("Failed to parse tabs")?;
let panes: PaneManifest = kdl_document
.get("panes")
.and_then(|p| p.children())
.map(|p| PaneManifest::decode_from_kdl(p))
.ok_or("Failed to parse panes")?;
let is_current_session = name == current_session_name;
Ok(SessionInfo {
name,
tabs,
panes,
connected_clients,
is_current_session,
})
}
pub fn to_string(&self) -> String {
let mut kdl_document = KdlDocument::new();
let mut name = KdlNode::new("name");
name.push(self.name.clone());
let mut connected_clients = KdlNode::new("connected_clients");
connected_clients.push(self.connected_clients as i64);
let mut tabs = KdlNode::new("tabs");
let mut tab_children = KdlDocument::new();
for tab_info in &self.tabs {
let mut tab = KdlNode::new("tab");
let kdl_tab_info = tab_info.encode_to_kdl();
tab.set_children(kdl_tab_info);
tab_children.nodes_mut().push(tab);
}
tabs.set_children(tab_children);
let mut panes = KdlNode::new("panes");
panes.set_children(self.panes.encode_to_kdl());
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.fmt();
kdl_document.to_string()
}
}
impl TabInfo {
pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<Self, String> {
macro_rules! int_node {
($name:expr, $type:ident) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_i64())
.map(|e| e as $type)
.ok_or(format!("Failed to parse tab {}", $name))?
}};
}
macro_rules! string_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_string())
.map(|s| s.to_owned())
.ok_or(format!("Failed to parse tab {}", $name))?
}};
}
macro_rules! optional_string_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_string())
.map(|s| s.to_owned())
}};
}
macro_rules! bool_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_bool())
.ok_or(format!("Failed to parse tab {}", $name))?
}};
}
let position = int_node!("position", usize);
let name = string_node!("name");
let active = bool_node!("active");
let panes_to_hide = int_node!("panes_to_hide", usize);
let is_fullscreen_active = bool_node!("is_fullscreen_active");
let is_sync_panes_active = bool_node!("is_sync_panes_active");
let are_floating_panes_visible = bool_node!("are_floating_panes_visible");
let mut other_focused_clients = vec![];
if let Some(tab_other_focused_clients) = kdl_document
.get("other_focused_clients")
.map(|n| n.entries())
{
for entry in tab_other_focused_clients {
if let Some(entry_parsed) = entry.value().as_i64() {
other_focused_clients.push(entry_parsed as u16);
}
}
}
let active_swap_layout_name = optional_string_node!("active_swap_layout_name");
let is_swap_layout_dirty = bool_node!("is_swap_layout_dirty");
Ok(TabInfo {
position,
name,
active,
panes_to_hide,
is_fullscreen_active,
is_sync_panes_active,
are_floating_panes_visible,
other_focused_clients,
active_swap_layout_name,
is_swap_layout_dirty,
})
}
pub fn encode_to_kdl(&self) -> KdlDocument {
let mut kdl_doucment = KdlDocument::new();
let mut position = KdlNode::new("position");
position.push(self.position as i64);
kdl_doucment.nodes_mut().push(position);
let mut name = KdlNode::new("name");
name.push(self.name.clone());
kdl_doucment.nodes_mut().push(name);
let mut active = KdlNode::new("active");
active.push(self.active);
kdl_doucment.nodes_mut().push(active);
let mut panes_to_hide = KdlNode::new("panes_to_hide");
panes_to_hide.push(self.panes_to_hide as i64);
kdl_doucment.nodes_mut().push(panes_to_hide);
let mut is_fullscreen_active = KdlNode::new("is_fullscreen_active");
is_fullscreen_active.push(self.is_fullscreen_active);
kdl_doucment.nodes_mut().push(is_fullscreen_active);
let mut is_sync_panes_active = KdlNode::new("is_sync_panes_active");
is_sync_panes_active.push(self.is_sync_panes_active);
kdl_doucment.nodes_mut().push(is_sync_panes_active);
let mut are_floating_panes_visible = KdlNode::new("are_floating_panes_visible");
are_floating_panes_visible.push(self.are_floating_panes_visible);
kdl_doucment.nodes_mut().push(are_floating_panes_visible);
if !self.other_focused_clients.is_empty() {
let mut other_focused_clients = KdlNode::new("other_focused_clients");
for client_id in &self.other_focused_clients {
other_focused_clients.push(*client_id as i64);
}
kdl_doucment.nodes_mut().push(other_focused_clients);
}
if let Some(active_swap_layout_name) = self.active_swap_layout_name.as_ref() {
let mut active_swap_layout = KdlNode::new("active_swap_layout_name");
active_swap_layout.push(active_swap_layout_name.to_string());
kdl_doucment.nodes_mut().push(active_swap_layout);
}
let mut is_swap_layout_dirty = KdlNode::new("is_swap_layout_dirty");
is_swap_layout_dirty.push(self.is_swap_layout_dirty);
kdl_doucment.nodes_mut().push(is_swap_layout_dirty);
kdl_doucment
}
}
impl PaneManifest {
pub fn decode_from_kdl(kdl_doucment: &KdlDocument) -> Self {
let mut panes: HashMap<usize, Vec<PaneInfo>> = HashMap::new();
for node in kdl_doucment.nodes() {
if node.name().to_string() == "pane" {
if let Some(pane_document) = node.children() {
if let Ok((tab_position, pane_info)) = PaneInfo::decode_from_kdl(pane_document)
{
let panes_in_tab_position =
panes.entry(tab_position).or_insert_with(Vec::new);
panes_in_tab_position.push(pane_info);
}
}
}
}
PaneManifest { panes }
}
pub fn encode_to_kdl(&self) -> KdlDocument {
let mut kdl_doucment = KdlDocument::new();
for (tab_position, panes) in &self.panes {
for pane in panes {
let mut pane_node = KdlNode::new("pane");
let mut pane = pane.encode_to_kdl();
let mut position_node = KdlNode::new("tab_position");
position_node.push(*tab_position as i64);
pane.nodes_mut().push(position_node);
pane_node.set_children(pane);
kdl_doucment.nodes_mut().push(pane_node);
}
}
kdl_doucment
}
}
impl PaneInfo {
pub fn decode_from_kdl(kdl_document: &KdlDocument) -> Result<(usize, Self), String> {
// usize is the tab position
macro_rules! int_node {
($name:expr, $type:ident) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_i64())
.map(|e| e as $type)
.ok_or(format!("Failed to parse pane {}", $name))?
}};
}
macro_rules! optional_int_node {
($name:expr, $type:ident) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_i64())
.map(|e| e as $type)
}};
}
macro_rules! bool_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_bool())
.ok_or(format!("Failed to parse pane {}", $name))?
}};
}
macro_rules! string_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_string())
.map(|s| s.to_owned())
.ok_or(format!("Failed to parse pane {}", $name))?
}};
}
macro_rules! optional_string_node {
($name:expr) => {{
kdl_document
.get($name)
.and_then(|n| n.entries().iter().next())
.and_then(|e| e.value().as_string())
.map(|s| s.to_owned())
}};
}
let tab_position = int_node!("tab_position", usize);
let id = int_node!("id", u32);
let is_plugin = bool_node!("is_plugin");
let is_focused = bool_node!("is_focused");
let is_fullscreen = bool_node!("is_fullscreen");
let is_floating = bool_node!("is_floating");
let is_suppressed = bool_node!("is_suppressed");
let title = string_node!("title");
let exited = bool_node!("exited");
let exit_status = optional_int_node!("exit_status", i32);
let is_held = bool_node!("is_held");
let pane_x = int_node!("pane_x", usize);
let pane_content_x = int_node!("pane_content_x", usize);
let pane_y = int_node!("pane_y", usize);
let pane_content_y = int_node!("pane_content_y", usize);
let pane_rows = int_node!("pane_rows", usize);
let pane_content_rows = int_node!("pane_content_rows", usize);
let pane_columns = int_node!("pane_columns", usize);
let pane_content_columns = int_node!("pane_content_columns", usize);
let cursor_coordinates_in_pane = kdl_document
.get("cursor_coordinates_in_pane")
.map(|n| {
let mut entries = n.entries().iter();
(entries.next(), entries.next())
})
.and_then(|(x, y)| {
let x = x.and_then(|x| x.value().as_i64()).map(|x| x as usize);
let y = y.and_then(|y| y.value().as_i64()).map(|y| y as usize);
match (x, y) {
(Some(x), Some(y)) => Some((x, y)),
_ => None,
}
});
let terminal_command = optional_string_node!("terminal_command");
let plugin_url = optional_string_node!("plugin_url");
let is_selectable = bool_node!("is_selectable");
let pane_info = PaneInfo {
id,
is_plugin,
is_focused,
is_fullscreen,
is_floating,
is_suppressed,
title,
exited,
exit_status,
is_held,
pane_x,
pane_content_x,
pane_y,
pane_content_y,
pane_rows,
pane_content_rows,
pane_columns,
pane_content_columns,
cursor_coordinates_in_pane,
terminal_command,
plugin_url,
is_selectable,
};
Ok((tab_position, pane_info))
}
pub fn encode_to_kdl(&self) -> KdlDocument {
let mut kdl_doucment = KdlDocument::new();
macro_rules! int_node {
($name:expr, $val:expr) => {{
let mut att = KdlNode::new($name);
att.push($val as i64);
kdl_doucment.nodes_mut().push(att);
}};
}
macro_rules! bool_node {
($name:expr, $val:expr) => {{
let mut att = KdlNode::new($name);
att.push($val);
kdl_doucment.nodes_mut().push(att);
}};
}
macro_rules! string_node {
($name:expr, $val:expr) => {{
let mut att = KdlNode::new($name);
att.push($val);
kdl_doucment.nodes_mut().push(att);
}};
}
int_node!("id", self.id);
bool_node!("is_plugin", self.is_plugin);
bool_node!("is_focused", self.is_focused);
bool_node!("is_fullscreen", self.is_fullscreen);
bool_node!("is_floating", self.is_floating);
bool_node!("is_suppressed", self.is_suppressed);
string_node!("title", self.title.to_string());
bool_node!("exited", self.exited);
if let Some(exit_status) = self.exit_status {
int_node!("exit_status", exit_status);
}
bool_node!("is_held", self.is_held);
int_node!("pane_x", self.pane_x);
int_node!("pane_content_x", self.pane_content_x);
int_node!("pane_y", self.pane_y);
int_node!("pane_content_y", self.pane_content_y);
int_node!("pane_rows", self.pane_rows);
int_node!("pane_content_rows", self.pane_content_rows);
int_node!("pane_columns", self.pane_columns);
int_node!("pane_content_columns", self.pane_content_columns);
if let Some((cursor_x, cursor_y)) = self.cursor_coordinates_in_pane {
let mut cursor_coordinates_in_pane = KdlNode::new("cursor_coordinates_in_pane");
cursor_coordinates_in_pane.push(cursor_x as i64);
cursor_coordinates_in_pane.push(cursor_y as i64);
kdl_doucment.nodes_mut().push(cursor_coordinates_in_pane);
}
if let Some(terminal_command) = &self.terminal_command {
string_node!("terminal_command", terminal_command.to_string());
}
if let Some(plugin_url) = &self.plugin_url {
string_node!("plugin_url", plugin_url.to_string());
}
bool_node!("is_selectable", self.is_selectable);
kdl_doucment
}
}
pub fn parse_plugin_user_configuration( pub fn parse_plugin_user_configuration(
plugin_block: &KdlNode, plugin_block: &KdlNode,
) -> Result<BTreeMap<String, String>, ConfigError> { ) -> Result<BTreeMap<String, String>, ConfigError> {
@ -1888,3 +2303,104 @@ pub fn parse_plugin_user_configuration(
} }
Ok(configuration) Ok(configuration)
} }
#[test]
fn serialize_and_deserialize_session_info() {
let session_info = SessionInfo::default();
let serialized = session_info.to_string();
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
assert_eq!(session_info, deserealized);
insta::assert_snapshot!(serialized);
}
#[test]
fn serialize_and_deserialize_session_info_with_data() {
let panes_list = vec![
PaneInfo {
id: 1,
is_plugin: false,
is_focused: true,
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
is_held: false,
pane_x: 0,
pane_content_x: 1,
pane_y: 0,
pane_content_y: 1,
pane_rows: 5,
pane_content_rows: 4,
pane_columns: 22,
pane_content_columns: 21,
cursor_coordinates_in_pane: Some((0, 0)),
terminal_command: Some("foo".to_owned()),
plugin_url: None,
is_selectable: true,
},
PaneInfo {
id: 1,
is_plugin: true,
is_focused: true,
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
is_held: false,
pane_x: 0,
pane_content_x: 1,
pane_y: 0,
pane_content_y: 1,
pane_rows: 5,
pane_content_rows: 4,
pane_columns: 22,
pane_content_columns: 21,
cursor_coordinates_in_pane: Some((0, 0)),
terminal_command: None,
plugin_url: Some("i_am_a_fake_plugin".to_owned()),
is_selectable: true,
},
];
let mut panes = HashMap::new();
panes.insert(0, panes_list);
let session_info = SessionInfo {
name: "my session name".to_owned(),
tabs: vec![
TabInfo {
position: 0,
name: "tab 1".to_owned(),
active: true,
panes_to_hide: 1,
is_fullscreen_active: true,
is_sync_panes_active: false,
are_floating_panes_visible: true,
other_focused_clients: vec![2, 3],
active_swap_layout_name: Some("BASE".to_owned()),
is_swap_layout_dirty: true,
},
TabInfo {
position: 1,
name: "tab 2".to_owned(),
active: true,
panes_to_hide: 0,
is_fullscreen_active: false,
is_sync_panes_active: true,
are_floating_panes_visible: true,
other_focused_clients: vec![2, 3],
active_swap_layout_name: None,
is_swap_layout_dirty: false,
},
],
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: false,
};
let serialized = session_info.to_string();
let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap();
assert_eq!(session_info, deserealized);
insta::assert_snapshot!(serialized);
}

View file

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

View file

@ -0,0 +1,81 @@
---
source: zellij-utils/src/kdl/mod.rs
assertion_line: 2377
expression: serialized
---
name "my session name"
tabs {
tab {
position 0
name "tab 1"
active true
panes_to_hide 1
is_fullscreen_active true
is_sync_panes_active false
are_floating_panes_visible true
other_focused_clients 2 3
active_swap_layout_name "BASE"
is_swap_layout_dirty true
}
tab {
position 1
name "tab 2"
active true
panes_to_hide 0
is_fullscreen_active false
is_sync_panes_active true
are_floating_panes_visible true
other_focused_clients 2 3
is_swap_layout_dirty false
}
}
panes {
pane {
id 1
is_plugin false
is_focused true
is_fullscreen true
is_floating false
is_suppressed false
title "pane 1"
exited false
is_held false
pane_x 0
pane_content_x 1
pane_y 0
pane_content_y 1
pane_rows 5
pane_content_rows 4
pane_columns 22
pane_content_columns 21
cursor_coordinates_in_pane 0 0
terminal_command "foo"
is_selectable true
tab_position 0
}
pane {
id 1
is_plugin true
is_focused true
is_fullscreen true
is_floating false
is_suppressed false
title "pane 1"
exited false
is_held false
pane_x 0
pane_content_x 1
pane_y 0
pane_content_y 1
pane_rows 5
pane_content_rows 4
pane_columns 22
pane_content_columns 21
cursor_coordinates_in_pane 0 0
plugin_url "i_am_a_fake_plugin"
is_selectable true
tab_position 0
}
}
connected_clients 2

View file

@ -39,6 +39,7 @@ enum EventType {
/// A file was deleted somewhere in the Zellij CWD folder /// A file was deleted somewhere in the Zellij CWD folder
FileSystemDelete = 14; FileSystemDelete = 14;
PermissionRequestResult = 15; PermissionRequestResult = 15;
SessionUpdate = 16;
} }
message EventNameList { message EventNameList {
@ -59,9 +60,14 @@ message Event {
CustomMessagePayload custom_message_payload = 10; CustomMessagePayload custom_message_payload = 10;
FileListPayload file_list_payload = 11; FileListPayload file_list_payload = 11;
PermissionRequestResultPayload permission_request_result_payload = 12; PermissionRequestResultPayload permission_request_result_payload = 12;
SessionUpdatePayload session_update_payload = 13;
} }
} }
message SessionUpdatePayload {
repeated SessionManifest session_manifests = 1;
}
message PermissionRequestResultPayload { message PermissionRequestResultPayload {
bool granted = 1; bool granted = 1;
} }
@ -111,6 +117,14 @@ message PaneManifest {
repeated PaneInfo panes = 2; repeated PaneInfo panes = 2;
} }
message SessionManifest {
string name = 1;
repeated TabInfo tabs = 2;
repeated PaneManifest panes = 3;
uint32 connected_clients = 4;
bool is_current_session = 5;
}
message PaneInfo { message PaneInfo {
uint32 id = 1; uint32 id = 1;
bool is_plugin = 2; bool is_plugin = 2;

View file

@ -6,7 +6,7 @@ pub use super::generated_api::api::{
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds, EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload, KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
TabInfo as ProtobufTabInfo, *, SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
}, },
input_mode::InputMode as ProtobufInputMode, input_mode::InputMode as ProtobufInputMode,
key::Key as ProtobufKey, key::Key as ProtobufKey,
@ -14,7 +14,7 @@ pub use super::generated_api::api::{
}; };
use crate::data::{ use crate::data::{
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest, CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
PermissionStatus, PluginCapabilities, Style, TabInfo, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
}; };
use crate::errors::prelude::*; use crate::errors::prelude::*;
@ -171,6 +171,18 @@ impl TryFrom<ProtobufEvent> for Event {
}, },
_ => Err("Malformed payload for the file system delete Event"), _ => Err("Malformed payload for the file system delete Event"),
}, },
Some(ProtobufEventType::SessionUpdate) => match protobuf_event.payload {
Some(ProtobufEventPayload::SessionUpdatePayload(
protobuf_session_update_payload,
)) => {
let mut session_infos: Vec<SessionInfo> = vec![];
for protobuf_session_info in protobuf_session_update_payload.session_manifests {
session_infos.push(SessionInfo::try_from(protobuf_session_info)?);
}
Ok(Event::SessionUpdate(session_infos))
},
_ => Err("Malformed payload for the SessionUpdate Event"),
},
None => Err("Unknown Protobuf Event"), None => Err("Unknown Protobuf Event"),
} }
} }
@ -313,7 +325,80 @@ impl TryFrom<Event> for ProtobufEvent {
)), )),
}) })
}, },
Event::SessionUpdate(session_infos) => {
let mut protobuf_session_manifests = vec![];
for session_info in session_infos {
protobuf_session_manifests.push(session_info.try_into()?);
} }
let session_update_payload = SessionUpdatePayload {
session_manifests: protobuf_session_manifests,
};
Ok(ProtobufEvent {
name: ProtobufEventType::SessionUpdate as i32,
payload: Some(event::Payload::SessionUpdatePayload(session_update_payload)),
})
},
}
}
}
impl TryFrom<SessionInfo> for ProtobufSessionManifest {
type Error = &'static str;
fn try_from(session_info: SessionInfo) -> Result<Self, &'static str> {
let mut protobuf_pane_manifests = vec![];
for (tab_index, pane_infos) in session_info.panes.panes {
let mut protobuf_pane_infos = vec![];
for pane_info in pane_infos {
protobuf_pane_infos.push(pane_info.try_into()?);
}
protobuf_pane_manifests.push(ProtobufPaneManifest {
tab_index: tab_index as u32,
panes: protobuf_pane_infos,
});
}
Ok(ProtobufSessionManifest {
name: session_info.name,
panes: protobuf_pane_manifests,
tabs: session_info
.tabs
.iter()
.filter_map(|t| t.clone().try_into().ok())
.collect(),
connected_clients: session_info.connected_clients as u32,
is_current_session: session_info.is_current_session,
})
}
}
impl TryFrom<ProtobufSessionManifest> for SessionInfo {
type Error = &'static str;
fn try_from(protobuf_session_manifest: ProtobufSessionManifest) -> Result<Self, &'static str> {
let mut pane_manifest: HashMap<usize, Vec<PaneInfo>> = HashMap::new();
for protobuf_pane_manifest in protobuf_session_manifest.panes {
let tab_index = protobuf_pane_manifest.tab_index as usize;
let mut panes = vec![];
for protobuf_pane_info in protobuf_pane_manifest.panes {
panes.push(protobuf_pane_info.try_into()?);
}
if pane_manifest.contains_key(&tab_index) {
return Err("Duplicate tab definition in pane manifest");
}
pane_manifest.insert(tab_index, panes);
}
let panes = PaneManifest {
panes: pane_manifest,
};
Ok(SessionInfo {
name: protobuf_session_manifest.name,
tabs: protobuf_session_manifest
.tabs
.iter()
.filter_map(|t| t.clone().try_into().ok())
.collect(),
panes,
connected_clients: protobuf_session_manifest.connected_clients as usize,
is_current_session: protobuf_session_manifest.is_current_session,
})
} }
} }
@ -697,6 +782,7 @@ impl TryFrom<ProtobufEventType> for EventType {
ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate, ProtobufEventType::FileSystemUpdate => EventType::FileSystemUpdate,
ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete, ProtobufEventType::FileSystemDelete => EventType::FileSystemDelete,
ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult, ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
ProtobufEventType::SessionUpdate => EventType::SessionUpdate,
}) })
} }
} }
@ -721,6 +807,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate, EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate,
EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete, EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult, EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
EventType::SessionUpdate => ProtobufEventType::SessionUpdate,
}) })
} }
} }
@ -1083,3 +1170,130 @@ fn serialize_file_system_delete_event() {
"Event properly serialized/deserialized without change" "Event properly serialized/deserialized without change"
); );
} }
#[test]
fn serialize_session_update_event() {
use prost::Message;
let session_update_event = Event::SessionUpdate(Default::default());
let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
Message::decode(serialized_protobuf_event.as_slice()).unwrap();
let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap();
assert_eq!(
session_update_event, deserialized_event,
"Event properly serialized/deserialized without change"
);
}
#[test]
fn serialize_session_update_event_with_non_default_values() {
use prost::Message;
let tab_infos = vec![
TabInfo {
position: 0,
name: "First tab".to_owned(),
active: true,
panes_to_hide: 2,
is_fullscreen_active: true,
is_sync_panes_active: false,
are_floating_panes_visible: true,
other_focused_clients: vec![2, 3, 4],
active_swap_layout_name: Some("my cool swap layout".to_owned()),
is_swap_layout_dirty: false,
},
TabInfo {
position: 1,
name: "Secondtab".to_owned(),
active: false,
panes_to_hide: 5,
is_fullscreen_active: false,
is_sync_panes_active: true,
are_floating_panes_visible: true,
other_focused_clients: vec![1, 5, 111],
active_swap_layout_name: None,
is_swap_layout_dirty: true,
},
TabInfo::default(),
];
let mut panes = HashMap::new();
let panes_list = vec![
PaneInfo {
id: 1,
is_plugin: false,
is_focused: true,
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
is_held: false,
pane_x: 0,
pane_content_x: 1,
pane_y: 0,
pane_content_y: 1,
pane_rows: 5,
pane_content_rows: 4,
pane_columns: 22,
pane_content_columns: 21,
cursor_coordinates_in_pane: Some((0, 0)),
terminal_command: Some("foo".to_owned()),
plugin_url: None,
is_selectable: true,
},
PaneInfo {
id: 1,
is_plugin: true,
is_focused: true,
is_fullscreen: true,
is_floating: false,
is_suppressed: false,
title: "pane 1".to_owned(),
exited: false,
exit_status: None,
is_held: false,
pane_x: 0,
pane_content_x: 1,
pane_y: 0,
pane_content_y: 1,
pane_rows: 5,
pane_content_rows: 4,
pane_columns: 22,
pane_content_columns: 21,
cursor_coordinates_in_pane: Some((0, 0)),
terminal_command: None,
plugin_url: Some("i_am_a_fake_plugin".to_owned()),
is_selectable: true,
},
];
panes.insert(0, panes_list);
let session_info_1 = SessionInfo {
name: "session 1".to_owned(),
tabs: tab_infos,
panes: PaneManifest { panes },
connected_clients: 2,
is_current_session: true,
};
let session_info_2 = SessionInfo {
name: "session 2".to_owned(),
tabs: vec![],
panes: PaneManifest {
panes: HashMap::new(),
},
connected_clients: 0,
is_current_session: false,
};
let session_infos = vec![session_info_1, session_info_2];
let session_update_event = Event::SessionUpdate(session_infos);
let protobuf_event: ProtobufEvent = session_update_event.clone().try_into().unwrap();
let serialized_protobuf_event = protobuf_event.encode_to_vec();
let deserialized_protobuf_event: ProtobufEvent =
Message::decode(serialized_protobuf_event.as_slice()).unwrap();
let deserialized_event: Event = deserialized_protobuf_event.try_into().unwrap();
assert_eq!(
session_update_event, deserialized_event,
"Event properly serialized/deserialized without change"
);
}

View file

@ -78,6 +78,7 @@ enum CommandName {
RenameTab = 64; RenameTab = 64;
ReportCrash = 65; ReportCrash = 65;
RequestPluginPermissions = 66; RequestPluginPermissions = 66;
SwitchSession = 67;
} }
message PluginCommand { message PluginCommand {
@ -120,9 +121,17 @@ message PluginCommand {
IdAndNewName rename_tab_payload = 36; IdAndNewName rename_tab_payload = 36;
string report_crash_payload = 37; string report_crash_payload = 37;
RequestPluginPermissionPayload request_plugin_permission_payload = 38; RequestPluginPermissionPayload request_plugin_permission_payload = 38;
SwitchSessionPayload switch_session_payload = 39;
} }
} }
message SwitchSessionPayload {
optional string name = 1;
optional uint32 tab_position = 2;
optional uint32 pane_id = 3;
optional bool pane_id_is_plugin = 4;
}
message RequestPluginPermissionPayload { message RequestPluginPermissionPayload {
repeated plugin_permission.PermissionType permissions = 1; repeated plugin_permission.PermissionType permissions = 1;
} }

View file

@ -5,13 +5,13 @@ pub use super::generated_api::api::{
OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat, OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload, PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload, RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload,
SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload, SwitchSessionPayload, SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
}, },
plugin_permission::PermissionType as ProtobufPermissionType, plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction, resize::ResizeAction as ProtobufResizeAction,
}; };
use crate::data::{PermissionType, PluginCommand}; use crate::data::{ConnectToSession, PermissionType, PluginCommand};
use std::convert::TryFrom; use std::convert::TryFrom;
@ -500,6 +500,23 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
}, },
_ => Err("Mismatched payload for RequestPluginPermission"), _ => Err("Mismatched payload for RequestPluginPermission"),
}, },
Some(CommandName::SwitchSession) => match protobuf_plugin_command.payload {
Some(Payload::SwitchSessionPayload(payload)) => {
let pane_id = match (payload.pane_id, payload.pane_id_is_plugin) {
(Some(pane_id), Some(is_plugin)) => Some((pane_id, is_plugin)),
(None, None) => None,
_ => {
return Err("Malformed payload for SwitchSession, 'pane_id' and 'is_plugin' must be included together or not at all")
}
};
Ok(PluginCommand::SwitchSession(ConnectToSession {
name: payload.name,
tab_position: payload.tab_position.map(|p| p as usize),
pane_id,
}))
},
_ => Err("Mismatched payload for SwitchSession"),
},
None => Err("Unrecognized plugin command"), None => Err("Unrecognized plugin command"),
} }
} }
@ -846,6 +863,15 @@ impl TryFrom<PluginCommand> for ProtobufPluginCommand {
}, },
)), )),
}), }),
PluginCommand::SwitchSession(switch_to_session) => Ok(ProtobufPluginCommand {
name: CommandName::SwitchSession as i32,
payload: Some(Payload::SwitchSessionPayload(SwitchSessionPayload {
name: switch_to_session.name,
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),
})),
}),
} }
} }
} }

View file

@ -2489,6 +2489,29 @@ Config {
): [ ): [
Detach, Detach,
], ],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt( Alt(
Char( Char(
'+', '+',
@ -3586,6 +3609,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"status-bar", "status-bar",
): PluginConfig { ): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [ ): [
Detach, Detach,
], ],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt( Alt(
Char( Char(
'+', '+',
@ -3586,6 +3609,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"status-bar", "status-bar",
): PluginConfig { ): PluginConfig {

View file

@ -102,6 +102,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"status-bar", "status-bar",
): PluginConfig { ): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [ ): [
Detach, Detach,
], ],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt( Alt(
Char( Char(
'+', '+',
@ -3586,6 +3609,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"some-other-plugin", "some-other-plugin",
): PluginConfig { ): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [ ): [
Detach, Detach,
], ],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt( Alt(
Char( Char(
'+', '+',
@ -3890,6 +3913,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"status-bar", "status-bar",
): PluginConfig { ): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [ ): [
Detach, Detach,
], ],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt( Alt(
Char( Char(
'+', '+',
@ -3586,6 +3609,23 @@ Config {
{}, {},
), ),
}, },
PluginTag(
"session-manager",
): PluginConfig {
path: "session-manager",
run: Pane(
None,
),
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
userspace_configuration: PluginUserConfiguration(
{},
),
},
PluginTag( PluginTag(
"status-bar", "status-bar",
): PluginConfig { ): PluginConfig {