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

View file

@ -35,6 +35,7 @@ members = [
"default-plugins/strider",
"default-plugins/tab-bar",
"default-plugins/fixture-plugin-for-tests",
"default-plugins/session-manager",
"zellij-client",
"zellij-server",
"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_utils::{
cli::{CliArgs, Command, SessionCommand, Sessions},
data::ConnectToSession,
envs,
input::{
actions::Action,
@ -331,7 +332,33 @@ pub(crate) fn start_client(opts: CliArgs) {
process::exit(1);
},
};
let mut reconnect_to_session: Option<ConnectToSession> = None;
let os_input = get_os_input(get_client_os_input);
loop {
let os_input = os_input.clone();
let config = config.clone();
let layout = layout.clone();
let mut config_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| {
assert_session_ne(&session_name);
@ -345,7 +372,9 @@ pub(crate) fn start_client(opts: CliArgs) {
})) = opts.command.clone()
{
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,
};
@ -364,8 +393,7 @@ pub(crate) fn start_client(opts: CliArgs) {
if let Ok(val) = std::env::var(envs::SESSION_NAME_ENV_KEY) {
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);
process::exit(1);
panic!("You are trying to attach to the current session (\"{}\"). This is not supported.", val);
}
}
@ -374,24 +402,34 @@ pub(crate) fn start_client(opts: CliArgs) {
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),
opts,
config,
config_options,
client,
attach_layout,
tab_position_to_focus,
pane_id_to_focus,
);
} else {
if let Some(session_name) = opts.session.clone() {
start_client_plan(session_name.clone());
start_client_impl(
reconnect_to_session = start_client_impl(
Box::new(os_input),
opts,
config,
config_options,
ClientInfo::New(session_name),
Some(layout),
None,
None,
);
} else {
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::New(_) => Some(layout),
};
start_client_impl(
reconnect_to_session = start_client_impl(
Box::new(os_input),
opts,
config,
config_options,
client,
attach_layout,
None,
None,
);
},
_ => {
start_client_plan(session_name.clone());
start_client_impl(
reconnect_to_session = start_client_impl(
Box::new(os_input),
opts,
config,
config_options.clone(),
ClientInfo::New(session_name.clone()),
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
// function happens
// TODO: offload this to a different function
process::exit(0);
}
let session_name = generate_unique_session_name();
start_client_plan(session_name.clone());
start_client_impl(
reconnect_to_session = start_client_impl(
Box::new(os_input),
opts,
config,
config_options,
ClientInfo::New(session_name),
Some(layout),
None,
None,
);
}
}
if reconnect_to_session.is_none() {
break;
}
}
}
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/tab-bar", 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-tile-utils", build: false},
WorkspaceMember{crate_name: "zellij-tile", build: false},

View file

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

View file

@ -23,8 +23,8 @@ use crate::{
};
use zellij_utils::{
channels::{self, ChannelWithContext, SenderWithContext},
consts::ZELLIJ_IPC_PIPE,
data::{ClientId, InputMode, Style},
consts::{set_permissions, ZELLIJ_SOCK_DIR},
data::{ClientId, ConnectToSession, InputMode, Style},
envs,
errors::{ClientContext, ContextType, ErrorInstruction},
input::{config::Config, options::Options},
@ -46,6 +46,7 @@ pub(crate) enum ClientInstruction {
StartedParsingStdinQuery,
DoneParsingStdinQuery,
Log(Vec<String>),
SwitchSession(ConnectToSession),
}
impl From<ServerToClientMsg> for ClientInstruction {
@ -60,6 +61,9 @@ impl From<ServerToClientMsg> for ClientInstruction {
ServerToClientMsg::Connected => ClientInstruction::Connected,
ServerToClientMsg::ActiveClients(clients) => ClientInstruction::ActiveClients(clients),
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::StartedParsingStdinQuery => ClientContext::StartedParsingStdinQuery,
ClientInstruction::DoneParsingStdinQuery => ClientContext::DoneParsingStdinQuery,
ClientInstruction::SwitchSession(..) => ClientContext::SwitchSession,
}
}
}
@ -130,6 +135,7 @@ pub(crate) enum InputInstruction {
AnsiStdinInstructions(Vec<AnsiStdinInstruction>),
StartedParsing,
DoneParsing,
Exit,
}
pub fn start_client(
@ -139,8 +145,12 @@ pub fn start_client(
config_options: Options,
info: ClientInfo,
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!");
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 take_snapshot = "\u{1b}[?1049h";
let bracketed_paste = "\u{1b}[?2004h";
@ -172,28 +182,51 @@ pub fn start_client(
keybinds: config.keybinds.clone(),
};
let first_msg = match info {
ClientInfo::Attach(name, config_options) => {
envs::set_session_name(name);
let create_ipc_pipe = || -> std::path::PathBuf {
let mut sock_dir = ZELLIJ_SOCK_DIR.clone();
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) => {
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(
client_attributes,
Box::new(opts),
Box::new(config_options.clone()),
Box::new(layout.unwrap()),
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);
let mut command_is_executing = CommandIsExecuting::new();
@ -336,7 +369,7 @@ pub fn start_client(
std::process::exit(1);
};
let exit_msg: String;
let mut exit_msg = String::new();
let mut loading = true;
let mut pending_instructions = vec![];
@ -415,13 +448,18 @@ pub fn start_client(
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();
// cleanup();
if reconnect_to_session.is_none() {
let reset_style = "\u{1b}[m";
let show_cursor = "\u{1b}[?25h";
let restore_snapshot = "\u{1b}[?1049l";
@ -437,6 +475,16 @@ pub fn start_client(
let mut stdout = os_input.get_stdout_writer();
let _ = stdout.write(goodbye_message.as_bytes()).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)]

View file

@ -78,6 +78,8 @@ pub struct ClientOsInputOutput {
orig_termios: Option<Arc<Mutex<termios::Termios>>>,
send_instructions_to_server: Arc<Mutex<Option<IpcSenderWithContext<ClientToServerMsg>>>>,
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
@ -94,8 +96,9 @@ pub trait ClientOsApi: Send + Sync {
/// Returns the writer that allows writing to standard output.
fn get_stdout_writer(&self) -> Box<dyn io::Write>;
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.
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.
fn box_clone(&self) -> Box<dyn ClientOsApi>;
/// Sends a message to the server.
@ -135,14 +138,46 @@ impl ClientOsApi for ClientOsInputOutput {
fn box_clone(&self) -> Box<dyn ClientOsApi> {
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 mut stdin = stdin.lock();
let buffer = stdin.fill_buf().unwrap();
let length = buffer.len();
let read_bytes = Vec::from(buffer);
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> {
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> {
let current_termios = termios::tcgetattr(0)?;
let orig_termios = Some(Arc::new(Mutex::new(current_termios)));
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
orig_termios,
send_instructions_to_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> {
let orig_termios = None; // not a terminal
let reading_from_stdin = Arc::new(Mutex::new(None));
Ok(ClientOsInputOutput {
orig_termios,
send_instructions_to_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![];
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
// this happens either on startup (see above) or on SIGWINCH
@ -96,7 +97,8 @@ pub(crate) fn stdin_loop(
let event_count = events.len();
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();
loop {
if poller.ready() {
@ -120,6 +122,17 @@ pub(crate) fn stdin_loop(
))
.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> {
unimplemented!()
}
fn read_from_stdin(&mut self) -> Vec<u8> {
self.stdin_buffer.drain(..).collect()
fn update_session_name(&mut self, new_session_name: String) {}
fn read_from_stdin(&mut self) -> Result<Vec<u8>, &'static str> {
Ok(self.stdin_buffer.drain(..).collect())
}
fn box_clone(&self) -> Box<dyn ClientOsApi> {
unimplemented!()

View file

@ -1,10 +1,16 @@
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 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::{
atomic::{AtomicBool, Ordering},
Arc,
Arc, Mutex,
};
use std::time::{Duration, Instant};
@ -17,6 +23,8 @@ pub enum BackgroundJob {
DisplayPaneError(Vec<PaneId>, String),
AnimatePluginLoading(u32), // u32 - plugin_id
StopPluginLoadingAnimation(u32), // u32 - plugin_id
ReadAllSessionInfosOnMachine, // u32 - plugin_id
ReportSessionInfo(String, SessionInfo), // String - session name
Exit,
}
@ -28,6 +36,10 @@ impl From<&BackgroundJob> for BackgroundJobContext {
BackgroundJob::StopPluginLoadingAnimation(..) => {
BackgroundJobContext::StopPluginLoadingAnimation
},
BackgroundJob::ReadAllSessionInfosOnMachine => {
BackgroundJobContext::ReadAllSessionInfosOnMachine
},
BackgroundJob::ReportSessionInfo(..) => BackgroundJobContext::ReportSessionInfo,
BackgroundJob::Exit => BackgroundJobContext::Exit,
}
}
@ -35,11 +47,14 @@ impl From<&BackgroundJob> for BackgroundJobContext {
static FLASH_DURATION_MS: u64 = 1000;
static PLUGIN_ANIMATION_OFFSET_DURATION_MD: u64 = 500;
static SESSION_READ_DURATION: u64 = 1000;
pub(crate) fn background_jobs_main(bus: Bus<BackgroundJob>) -> Result<()> {
let err_context = || "failed to write to pty".to_string();
let mut running_jobs: HashMap<BackgroundJob, Instant> = HashMap::new();
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 {
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);
}
},
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 => {
for loading_plugin in loading_plugins.values() {
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(());
},
}
@ -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},
cli::CliArgs,
consts::{DEFAULT_SCROLL_BUFFER_SIZE, SCROLL_BUFFER_SIZE},
data::{Event, PluginCapabilities},
data::{ConnectToSession, Event, PluginCapabilities},
errors::{prelude::*, ContextType, ErrorInstruction, FatalError, ServerContext},
input::{
command::{RunCommand, TerminalAction},
@ -74,10 +74,17 @@ pub enum ServerInstruction {
Error(String),
KillSession,
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),
ActiveClients(ClientId),
Log(Vec<String>, ClientId),
SwitchSession(ConnectToSession, ClientId),
}
impl From<&ServerInstruction> for ServerContext {
@ -95,6 +102,7 @@ impl From<&ServerInstruction> for ServerContext {
ServerInstruction::ConnStatus(..) => ServerContext::ConnStatus,
ServerInstruction::ActiveClients(_) => ServerContext::ActiveClients,
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))
.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 session_data = rlock.as_ref().unwrap();
session_state
@ -433,7 +447,11 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
.unwrap();
session_data
.senders
.send_to_screen(ScreenInstruction::AddClient(client_id))
.send_to_screen(ScreenInstruction::AddClient(
client_id,
tab_position_to_focus,
pane_id_to_focus,
))
.unwrap();
session_data
.senders
@ -635,6 +653,41 @@ pub fn start_server(mut os_input: Box<dyn ServerOsApi>, socket_path: PathBuf) {
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,
} = options;
SCROLL_BUFFER_SIZE
.set(
let _ = SCROLL_BUFFER_SIZE.set(
config_options
.scroll_buffer_size
.unwrap_or(DEFAULT_SCROLL_BUFFER_SIZE),
)
.unwrap();
);
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = channels::unbounded();
let to_screen = SenderWithContext::new(to_screen);

View file

@ -1351,7 +1351,7 @@ impl Grid {
self.viewport.get(y).unwrap().absolute_character_index(x)
}
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;
}
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) {
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
.panes
.get(&pane_id)
@ -533,7 +538,6 @@ impl TiledPanes {
.expand_pane(&pane_id);
self.reapply_pane_frames();
}
self.active_panes
.insert(client_id, pane_id, &mut self.panes);
if self.session_is_mirrored {

View file

@ -539,6 +539,7 @@ pub fn load_new_plugin_from_hd() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
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
// 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
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
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
// 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
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -820,6 +823,7 @@ pub fn switch_to_mode_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -887,6 +891,7 @@ pub fn switch_to_mode_plugin_command_permission_denied() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -954,6 +959,7 @@ pub fn new_tabs_with_layout_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1035,6 +1041,7 @@ pub fn new_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1102,6 +1109,7 @@ pub fn go_to_next_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1168,6 +1176,7 @@ pub fn go_to_previous_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1234,6 +1243,7 @@ pub fn resize_focused_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1300,6 +1310,7 @@ pub fn resize_focused_pane_with_direction_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1366,6 +1377,7 @@ pub fn focus_next_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1432,6 +1444,7 @@ pub fn focus_previous_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1498,6 +1511,7 @@ pub fn move_focus_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1564,6 +1578,7 @@ pub fn move_focus_or_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1630,6 +1645,7 @@ pub fn edit_scrollback_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1696,6 +1712,7 @@ pub fn write_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1762,6 +1779,7 @@ pub fn write_chars_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1828,6 +1846,7 @@ pub fn toggle_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1894,6 +1913,7 @@ pub fn move_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -1960,6 +1980,7 @@ pub fn move_pane_with_direction_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2027,7 +2048,7 @@ pub fn clear_screen_plugin_command() {
client_id,
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![(
None,
Some(client_id),
@ -2095,6 +2116,7 @@ pub fn scroll_up_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2161,6 +2183,7 @@ pub fn scroll_down_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2227,6 +2250,7 @@ pub fn scroll_to_top_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2293,6 +2317,7 @@ pub fn scroll_to_bottom_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2359,6 +2384,7 @@ pub fn page_scroll_up_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2425,6 +2451,7 @@ pub fn page_scroll_down_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2491,6 +2518,7 @@ pub fn toggle_focus_fullscreen_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2557,6 +2585,7 @@ pub fn toggle_pane_frames_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2623,6 +2652,7 @@ pub fn toggle_pane_embed_or_eject_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2689,6 +2719,7 @@ pub fn undo_rename_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2755,6 +2786,7 @@ pub fn close_focus_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2821,6 +2853,7 @@ pub fn toggle_active_tab_sync_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2887,6 +2920,7 @@ pub fn close_focused_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -2953,6 +2987,7 @@ pub fn undo_rename_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3019,6 +3054,7 @@ pub fn previous_swap_layout_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3085,6 +3121,7 @@ pub fn next_swap_layout_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3151,6 +3188,7 @@ pub fn go_to_tab_name_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3217,6 +3255,7 @@ pub fn focus_or_create_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3283,6 +3322,7 @@ pub fn go_to_tab() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3349,6 +3389,7 @@ pub fn start_or_reload_plugin() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3422,6 +3463,7 @@ pub fn quit_zellij_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3495,6 +3537,7 @@ pub fn detach_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3568,6 +3611,7 @@ pub fn open_file_floating_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3641,6 +3685,7 @@ pub fn open_file_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3715,6 +3760,7 @@ pub fn open_file_with_line_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3788,6 +3834,7 @@ pub fn open_file_with_line_floating_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3861,6 +3908,7 @@ pub fn open_terminal_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -3934,6 +3982,7 @@ pub fn open_terminal_floating_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4007,6 +4056,7 @@ pub fn open_command_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4080,6 +4130,7 @@ pub fn open_command_pane_floating_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4146,6 +4197,7 @@ pub fn switch_to_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4207,6 +4259,7 @@ pub fn hide_self_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4268,6 +4321,7 @@ pub fn show_self_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4334,6 +4388,7 @@ pub fn close_terminal_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4400,6 +4455,7 @@ pub fn close_plugin_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4466,6 +4522,7 @@ pub fn focus_terminal_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4532,6 +4589,7 @@ pub fn focus_plugin_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4598,6 +4656,7 @@ pub fn rename_terminal_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4664,6 +4723,7 @@ pub fn rename_plugin_pane_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4730,6 +4790,7 @@ pub fn rename_tab_plugin_command() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4805,6 +4866,7 @@ pub fn send_configuration_to_plugins() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4868,6 +4930,7 @@ pub fn request_plugin_permissions() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -4955,6 +5018,7 @@ pub fn granted_permission_request_result() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),
@ -5040,6 +5104,7 @@ pub fn denied_permission_request_result() {
client_id,
size,
));
std::thread::sleep(std::time::Duration::from_millis(500));
let _ = plugin_thread_sender.send(PluginInstruction::Update(vec![(
None,
Some(client_id),

View file

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

View file

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

View file

@ -2,6 +2,7 @@ use super::PluginInstruction;
use crate::plugins::plugin_map::{PluginEnv, Subscriptions};
use crate::plugins::wasm_bridge::handle_plugin_crash;
use crate::route::route_action;
use crate::ServerInstruction;
use log::{debug, warn};
use serde::Serialize;
use std::{
@ -15,7 +16,9 @@ use std::{
};
use wasmer::{imports, Function, ImportObject, Store, WasmerEnv};
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 url::Url;
@ -205,6 +208,12 @@ fn host_run_plugin_command(env: &ForeignFunctionEnv) {
PluginCommand::RequestPluginPermissions(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) => {
log::error!(
@ -679,6 +688,31 @@ fn detach(env: &ForeignFunctionEnv) {
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) {
let action = Action::EditScrollback;
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()
)
};
let action = Action::GoToTab(tab_index);
let action = Action::GoToTab(tab_index + 1);
apply_action!(action, error_msg, env);
}
@ -1113,7 +1147,6 @@ fn check_command_permission(
_ => return (PermissionStatus::Granted, None),
};
log::info!("plugin permissions: {:?}", plugin_env.permissions);
if let Some(permissions) = plugin_env.permissions.lock().unwrap().as_ref() {
if permissions.contains(&permission) {
return (PermissionStatus::Granted, None);

View file

@ -860,9 +860,19 @@ pub(crate) fn route_thread_main(
.send(new_client_instruction)
.with_context(err_context)?;
},
ClientToServerMsg::AttachClient(client_attributes, opts) => {
let attach_client_instruction =
ServerInstruction::AttachClient(client_attributes, opts, client_id);
ClientToServerMsg::AttachClient(
client_attributes,
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
.send(attach_client_instruction)
.with_context(err_context)?;

View file

@ -6,7 +6,9 @@ use std::path::PathBuf;
use std::rc::Rc;
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::input::command::RunCommand;
use zellij_utils::input::options::Clipboard;
@ -242,7 +244,11 @@ pub enum ScreenInstruction {
MouseHoldRight(Position, ClientId),
MouseHoldMiddle(Position, 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),
AddOverlay(Overlay, ClientId),
RemoveOverlay(ClientId),
@ -287,6 +293,7 @@ pub enum ScreenInstruction {
BreakPane(Box<Layout>, Option<TerminalAction>, ClientId),
BreakPaneRight(ClientId),
BreakPaneLeft(ClientId),
UpdateSessionInfos(BTreeMap<String, SessionInfo>), // String is the session name
}
impl From<&ScreenInstruction> for ScreenContext {
@ -460,6 +467,7 @@ impl From<&ScreenInstruction> for ScreenContext {
ScreenInstruction::BreakPane(..) => ScreenContext::BreakPane,
ScreenInstruction::BreakPaneRight(..) => ScreenContext::BreakPaneRight,
ScreenInstruction::BreakPaneLeft(..) => ScreenContext::BreakPaneLeft,
ScreenInstruction::UpdateSessionInfos(..) => ScreenContext::UpdateSessionInfos,
}
}
}
@ -524,6 +532,9 @@ pub(crate) struct Screen {
session_is_mirrored: bool,
copy_options: CopyOptions,
debug: bool,
session_name: String,
session_infos_on_machine: BTreeMap<String, SessionInfo>, // String is the session name, can
// also be this session
}
impl Screen {
@ -539,6 +550,10 @@ impl Screen {
copy_options: CopyOptions,
debug: bool,
) -> 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 {
bus,
max_panes,
@ -561,6 +576,8 @@ impl Screen {
session_is_mirrored,
copy_options,
debug,
session_name,
session_infos_on_machine,
}
}
@ -745,8 +762,8 @@ impl Screen {
.non_fatal();
}
self.report_tab_state().with_context(err_context)?;
self.report_pane_state().with_context(err_context)?;
self.log_and_report_session_state()
.with_context(err_context)?;
return self.render().with_context(err_context);
},
Err(err) => Err::<(), _>(err).with_context(err_context).non_fatal(),
@ -879,8 +896,8 @@ impl Screen {
t.position -= 1;
}
}
self.report_tab_state().with_context(err_context)?;
self.report_pane_state().with_context(err_context)?;
self.log_and_report_session_state()
.with_context(err_context)?;
self.render().with_context(err_context)
}
}
@ -917,7 +934,8 @@ impl Screen {
.with_context(err_context)?;
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)
}
@ -1183,10 +1201,9 @@ impl Screen {
self.add_client(client_id).with_context(err_context)?;
}
self.report_tab_state()
self.log_and_report_session_state()
.and_then(|_| self.render())
.with_context(err_context)?;
self.report_pane_state().with_context(err_context)
.with_context(err_context)
}
pub fn add_client(&mut self, client_id: ClientId) -> Result<()> {
@ -1237,13 +1254,38 @@ impl Screen {
self.tab_history.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 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() {
let mut tab_data = vec![];
let mut plugin_tab_updates = vec![];
for tab in self.tabs.values() {
let other_focused_clients: Vec<ClientId> = if self.session_is_mirrored {
vec![]
@ -1258,7 +1300,7 @@ impl Screen {
.collect()
};
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,
name: tab.name.clone(),
active: *active_tab_index == tab.index,
@ -1269,17 +1311,18 @@ impl Screen {
other_focused_clients,
active_swap_layout_name,
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
.senders
.send_to_plugin(PluginInstruction::Update(plugin_updates))
.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();
for tab in self.tabs.values() {
pane_manifest.panes.insert(tab.position, tab.pane_infos());
@ -1289,9 +1332,51 @@ impl Screen {
.send_to_plugin(PluginInstruction::Update(vec![(
None,
None,
Event::PaneUpdate(pane_manifest),
Event::PaneUpdate(pane_manifest.clone()),
)]))
.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(())
}
@ -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).with_context(err_context).non_fatal();
@ -1352,7 +1438,7 @@ impl Screen {
Ok(active_tab) => {
if active_tab.name != active_tab.prev_name {
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")?;
}
},
@ -1473,7 +1559,8 @@ impl Screen {
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(())
}
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(),
};
}
self.report_pane_state().with_context(err_context)?;
self.log_and_report_session_state()
.with_context(err_context)?;
Ok(())
}
pub fn toggle_tab(&mut self, client_id: ClientId) -> Result<()> {
@ -1521,8 +1609,8 @@ impl Screen {
.context("failed to toggle tabs")?;
};
self.report_tab_state().context("failed to toggle tabs")?;
self.report_pane_state().context("failed to toggle tabs")?;
self.log_and_report_session_state()
.context("failed to toggle tabs")?;
self.render()
}
@ -1550,7 +1638,8 @@ impl Screen {
.with_context(err_context)?
.focus_pane_with_id(plugin_pane_id, should_float, client_id)
.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)
},
None => Ok(false),
@ -1684,8 +1773,7 @@ impl Screen {
new_active_tab.add_tiled_pane(active_pane, active_pane_id, Some(client_id))?;
}
self.report_tab_state()?;
self.report_pane_state()?;
self.log_and_report_session_state()?;
} else {
let active_pane_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.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
@ -1852,8 +1939,7 @@ pub(crate) fn screen_thread_main(
active_tab!(screen, client_id, |tab: &mut Tab| tab
.suppress_active_pane(pid, client_id), ?);
screen.unblock_input()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
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
.toggle_pane_embed_or_floating(client_id), ?);
screen.unblock_input()?;
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
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
.toggle_floating_panes(Some(client_id), default_shell), ?);
screen.unblock_input()?;
screen.report_tab_state()?; // update tabs so that the ui indication will be send to the plugins
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
@ -1901,8 +1985,7 @@ pub(crate) fn screen_thread_main(
);
}
screen.unblock_input()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
ScreenInstruction::VerticalSplit(
@ -1931,8 +2014,7 @@ pub(crate) fn screen_thread_main(
);
}
screen.unblock_input()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
ScreenInstruction::WriteCharacter(bytes, client_id) => {
@ -1953,8 +2035,7 @@ pub(crate) fn screen_thread_main(
?
);
if state_changed {
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
}
},
ScreenInstruction::Resize(client_id, strategy) => {
@ -1966,8 +2047,7 @@ pub(crate) fn screen_thread_main(
);
screen.unblock_input()?;
screen.render()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::SwitchFocus(client_id) => {
active_tab_and_connected_client_id!(
@ -1977,7 +2057,7 @@ pub(crate) fn screen_thread_main(
);
screen.unblock_input()?;
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::FocusNextPane(client_id) => {
active_tab_and_connected_client_id!(
@ -1996,7 +2076,7 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusLeft(client_id) => {
active_tab_and_connected_client_id!(
@ -2007,13 +2087,13 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusLeftOrPreviousTab(client_id) => {
screen.move_focus_left_or_previous_tab(client_id)?;
screen.unblock_input()?;
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusDown(client_id) => {
active_tab_and_connected_client_id!(
@ -2024,7 +2104,7 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusRight(client_id) => {
active_tab_and_connected_client_id!(
@ -2035,13 +2115,13 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusRightOrNextTab(client_id) => {
screen.move_focus_right_or_next_tab(client_id)?;
screen.unblock_input()?;
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MoveFocusUp(client_id) => {
active_tab_and_connected_client_id!(
@ -2052,7 +2132,7 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::ClearScreen(client_id) => {
active_tab_and_connected_client_id!(
@ -2088,7 +2168,7 @@ pub(crate) fn screen_thread_main(
?
);
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::ScrollUp(client_id) => {
active_tab_and_connected_client_id!(
@ -2105,10 +2185,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MovePaneBackwards(client_id) => {
active_tab_and_connected_client_id!(
@ -2116,10 +2195,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MovePaneDown(client_id) => {
active_tab_and_connected_client_id!(
@ -2127,10 +2205,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MovePaneUp(client_id) => {
active_tab_and_connected_client_id!(
@ -2138,10 +2215,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MovePaneRight(client_id) => {
active_tab_and_connected_client_id!(
@ -2149,10 +2225,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::MovePaneLeft(client_id) => {
active_tab_and_connected_client_id!(
@ -2160,10 +2235,9 @@ pub(crate) fn screen_thread_main(
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.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::ScrollUpAt(point, client_id) => {
active_tab_and_connected_client_id!(
@ -2270,10 +2344,9 @@ pub(crate) fn screen_thread_main(
client_id,
|tab: &mut Tab, client_id: ClientId| tab.close_focused_pane(client_id), ?
);
screen.report_tab_state()?;
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::SetSelectable(id, selectable, tab_index) => {
screen.get_indexed_tab_mut(tab_index).map_or_else(
@ -2288,7 +2361,7 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::ClosePane(id, client_id) => {
match client_id {
@ -2308,9 +2381,8 @@ pub(crate) fn screen_thread_main(
}
},
}
screen.report_tab_state()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::HoldPane(id, exit_status, run_command, tab_index, client_id) => {
let is_first_run = false;
@ -2339,9 +2411,8 @@ pub(crate) fn screen_thread_main(
}
},
}
screen.report_tab_state()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::UpdatePaneName(c, client_id) => {
active_tab_and_connected_client_id!(
@ -2351,7 +2422,7 @@ pub(crate) fn screen_thread_main(
);
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::UndoRenamePane(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
.toggle_active_pane_fullscreen(client_id)
);
screen.report_tab_state()?;
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::TogglePaneFrames => {
screen.draw_pane_frames = !screen.draw_pane_frames;
@ -2381,7 +2451,7 @@ pub(crate) fn screen_thread_main(
}
screen.render()?;
screen.unblock_input()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::SwitchTabNext(client_id) => {
screen.switch_tab_next(None, true, client_id)?;
@ -2529,7 +2599,7 @@ pub(crate) fn screen_thread_main(
},
ScreenInstruction::TerminalResize(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()?;
},
ScreenInstruction::TerminalPixelDimensions(pixel_dimensions) => {
@ -2560,31 +2630,28 @@ pub(crate) fn screen_thread_main(
client_id,
|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.unblock_input()?;
},
ScreenInstruction::LeftClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_left_click(&point, client_id), ?);
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
screen.unblock_input()?;
},
ScreenInstruction::RightClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_right_click(&point, client_id), ?);
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
screen.unblock_input()?;
},
ScreenInstruction::MiddleClick(point, client_id) => {
active_tab!(screen, client_id, |tab: &mut Tab| tab
.handle_middle_click(&point, client_id), ?);
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
screen.unblock_input()?;
},
@ -2632,15 +2699,26 @@ pub(crate) fn screen_thread_main(
screen.unblock_input()?;
screen.render()?;
},
ScreenInstruction::AddClient(client_id) => {
ScreenInstruction::AddClient(client_id, tab_position_to_focus, pane_id_to_focus) => {
screen.add_client(client_id)?;
screen.report_tab_state()?;
screen.report_pane_state()?;
let pane_id = pane_id_to_focus.map(|(pane_id, is_plugin)| {
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()?;
},
ScreenInstruction::RemoveClient(client_id) => {
screen.remove_client(client_id)?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
ScreenInstruction::AddOverlay(overlay, _client_id) => {
@ -2755,8 +2833,7 @@ pub(crate) fn screen_thread_main(
?
);
screen.render()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.unblock_input()?;
},
ScreenInstruction::NextSwapLayout(client_id) => {
@ -2767,8 +2844,7 @@ pub(crate) fn screen_thread_main(
?
);
screen.render()?;
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.unblock_input()?;
},
ScreenInstruction::QueryTabNames(client_id) => {
@ -2852,7 +2928,7 @@ pub(crate) fn screen_thread_main(
} else {
log::error!("Tab index not found: {:?}", tab_index);
}
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.unblock_input()?;
},
ScreenInstruction::UpdatePluginLoadingStage(pid, loading_indication) => {
@ -2890,8 +2966,7 @@ pub(crate) fn screen_thread_main(
for tab in all_tabs.values_mut() {
tab.update_input_modes()?;
}
screen.report_tab_state()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
screen.render()?;
},
ScreenInstruction::LaunchOrFocusPlugin(run_plugin, should_float, client_id) => {
@ -2910,7 +2985,7 @@ pub(crate) fn screen_thread_main(
Some((tab_index, client_id)) => {
if screen.focus_plugin_pane(&run_plugin, should_float, client_id)? {
screen.render()?;
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
} else {
screen.bus.senders.send_to_plugin(PluginInstruction::Load(
Some(should_float),
@ -2934,12 +3009,11 @@ pub(crate) fn screen_thread_main(
break;
}
}
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::FocusPaneWithId(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.report_tab_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::RenamePane(pane_id, new_name) => {
let all_tabs = screen.get_tabs_mut();
@ -2952,7 +3026,7 @@ pub(crate) fn screen_thread_main(
break;
}
}
screen.report_pane_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::RenameTab(tab_index, new_name) => {
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);
},
}
screen.report_tab_state()?;
screen.log_and_report_session_state()?;
},
ScreenInstruction::RequestPluginPermissions(plugin_id, plugin_permission) => {
let all_tabs = screen.get_tabs_mut();
@ -2992,6 +3066,9 @@ pub(crate) fn screen_thread_main(
ScreenInstruction::BreakPaneLeft(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(())

View file

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

View file

@ -563,6 +563,34 @@ where
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
/// Returns the `TabInfo` corresponding to the currently active tab

View file

@ -113,6 +113,12 @@ keybinds {
bind "Ctrl o" { SwitchToMode "Normal"; }
bind "Ctrl s" { SwitchToMode "Scroll"; }
bind "d" { Detach; }
bind "w" {
LaunchOrFocusPlugin "zellij:session-manager" {
floating true
};
SwitchToMode "Normal"
}
}
tmux {
bind "[" { SwitchToMode "Scroll"; }
@ -181,6 +187,7 @@ plugins {
status-bar { path "status-bar"; }
strider { path "strider"; }
compact-bar { path "compact-bar"; }
session-manager { path "session-manager"; }
}
// 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()));
pub static ref ZELLIJ_PLUGIN_PERMISSIONS_CACHE: PathBuf =
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] = &[
@ -92,6 +94,7 @@ mod not_wasm {
add_plugin!(assets, "status-bar.wasm");
add_plugin!(assets, "tab-bar.wasm");
add_plugin!(assets, "strider.wasm");
add_plugin!(assets, "session-manager.wasm");
assets
};
}
@ -104,20 +107,13 @@ pub use unix_only::*;
mod unix_only {
use super::*;
use crate::envs;
use crate::shared::set_permissions;
pub use crate::shared::set_permissions;
use lazy_static::lazy_static;
use nix::unistd::Uid;
use std::{env::temp_dir, fs};
use std::env::temp_dir;
lazy_static! {
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_LOG_DIR: PathBuf = ZELLIJ_TMP_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>),
/// A Result of plugin permission request
PermissionRequestResult(PermissionStatus),
SessionUpdate(Vec<SessionInfo>),
}
#[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.
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
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)]
pub struct PluginMessage {
pub name: String,
@ -1016,4 +1060,5 @@ pub enum PluginCommand {
RenameTab(u32, String), // tab index, new name
ReportPanic(String), // stringified panic
RequestPluginPermissions(Vec<PermissionType>),
SwitchSession(ConnectToSession),
}

View file

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

View file

@ -27,13 +27,17 @@ impl PermissionCache {
pub fn check_permissions(
&self,
plugin_name: String,
permissions: &Vec<PermissionType>,
permissions_to_check: &Vec<PermissionType>,
) -> bool {
if let Some(target) = self.granted.get(&plugin_name) {
if target == permissions {
return true;
let mut all_granted = true;
for permission in permissions_to_check {
if !target.contains(permission) {
all_granted = false;
}
}
return all_granted;
}
false
}
@ -43,7 +47,10 @@ impl PermissionCache {
let granted = match fs::read_to_string(cache_path.clone()) {
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 {

View file

@ -1,7 +1,7 @@
//! IPC stuff for starting to split things into a client and server model.
use crate::{
cli::CliArgs,
data::{ClientId, InputMode, Style},
data::{ClientId, ConnectToSession, InputMode, Style},
errors::{get_current_ctx, prelude::*, ErrorContext},
input::keybinds::Keybinds,
input::{actions::Action, layout::Layout, options::Options, plugins::PluginsConfig},
@ -65,16 +65,6 @@ impl PixelDimensions {
#[allow(clippy::large_enum_variant)]
#[derive(Serialize, Deserialize, Debug, Clone)]
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>),
TerminalPixelDimensions(PixelDimensions),
BackgroundColor(String),
@ -88,7 +78,12 @@ pub enum ClientToServerMsg {
Box<Layout>,
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>),
ClientExited,
KillSession,
@ -99,10 +94,6 @@ pub enum ClientToServerMsg {
// Types of messages sent from the server to the client
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ServerToClientMsg {
/*// Info about a particular session
SessionInfo(Session),
// A list of sessions
SessionList(HashSet<Session>),*/
Render(String),
UnblockInputThread,
Exit(ExitReason),
@ -110,6 +101,7 @@ pub enum ServerToClientMsg {
Connected,
ActiveClients(Vec<ClientId>),
Log(Vec<String>),
SwitchSession(ConnectToSession),
}
#[derive(Serialize, Deserialize, Debug, Clone)]

View file

@ -1,5 +1,8 @@
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::input::config::{Config, ConfigError, KdlError};
use crate::input::keybinds::Keybinds;
@ -325,7 +328,6 @@ macro_rules! actions_from_kdl {
pub fn kdl_arguments_that_are_strings<'a>(
arguments: impl Iterator<Item = &'a KdlEntry>,
) -> 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![];
for kdl_entry in arguments {
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(
plugin_block: &KdlNode,
) -> Result<BTreeMap<String, String>, ConfigError> {
@ -1888,3 +2303,104 @@ pub fn parse_plugin_user_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
FileSystemDelete = 14;
PermissionRequestResult = 15;
SessionUpdate = 16;
}
message EventNameList {
@ -59,9 +60,14 @@ message Event {
CustomMessagePayload custom_message_payload = 10;
FileListPayload file_list_payload = 11;
PermissionRequestResultPayload permission_request_result_payload = 12;
SessionUpdatePayload session_update_payload = 13;
}
}
message SessionUpdatePayload {
repeated SessionManifest session_manifests = 1;
}
message PermissionRequestResultPayload {
bool granted = 1;
}
@ -111,6 +117,14 @@ message PaneManifest {
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 {
uint32 id = 1;
bool is_plugin = 2;

View file

@ -6,7 +6,7 @@ pub use super::generated_api::api::{
EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds,
KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload,
PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest,
TabInfo as ProtobufTabInfo, *,
SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *,
},
input_mode::InputMode as ProtobufInputMode,
key::Key as ProtobufKey,
@ -14,7 +14,7 @@ pub use super::generated_api::api::{
};
use crate::data::{
CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest,
PermissionStatus, PluginCapabilities, Style, TabInfo,
PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo,
};
use crate::errors::prelude::*;
@ -171,6 +171,18 @@ impl TryFrom<ProtobufEvent> for 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"),
}
}
@ -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::FileSystemDelete => EventType::FileSystemDelete,
ProtobufEventType::PermissionRequestResult => EventType::PermissionRequestResult,
ProtobufEventType::SessionUpdate => EventType::SessionUpdate,
})
}
}
@ -721,6 +807,7 @@ impl TryFrom<EventType> for ProtobufEventType {
EventType::FileSystemUpdate => ProtobufEventType::FileSystemUpdate,
EventType::FileSystemDelete => ProtobufEventType::FileSystemDelete,
EventType::PermissionRequestResult => ProtobufEventType::PermissionRequestResult,
EventType::SessionUpdate => ProtobufEventType::SessionUpdate,
})
}
}
@ -1083,3 +1170,130 @@ fn serialize_file_system_delete_event() {
"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;
ReportCrash = 65;
RequestPluginPermissions = 66;
SwitchSession = 67;
}
message PluginCommand {
@ -120,9 +121,17 @@ message PluginCommand {
IdAndNewName rename_tab_payload = 36;
string report_crash_payload = 37;
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 {
repeated plugin_permission.PermissionType permissions = 1;
}

View file

@ -5,13 +5,13 @@ pub use super::generated_api::api::{
OpenCommandPanePayload, OpenFilePayload, PaneIdAndShouldFloat,
PluginCommand as ProtobufPluginCommand, PluginMessagePayload,
RequestPluginPermissionPayload, ResizePayload, SetTimeoutPayload, SubscribePayload,
SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
SwitchSessionPayload, SwitchTabToPayload, SwitchToModePayload, UnsubscribePayload,
},
plugin_permission::PermissionType as ProtobufPermissionType,
resize::ResizeAction as ProtobufResizeAction,
};
use crate::data::{PermissionType, PluginCommand};
use crate::data::{ConnectToSession, PermissionType, PluginCommand};
use std::convert::TryFrom;
@ -500,6 +500,23 @@ impl TryFrom<ProtobufPluginCommand> for PluginCommand {
},
_ => 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"),
}
}
@ -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,
],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt(
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(
"status-bar",
): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [
Detach,
],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt(
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(
"status-bar",
): 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(
"status-bar",
): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [
Detach,
],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt(
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(
"some-other-plugin",
): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [
Detach,
],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt(
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(
"status-bar",
): PluginConfig {

View file

@ -2489,6 +2489,29 @@ Config {
): [
Detach,
],
Char(
'w',
): [
LaunchOrFocusPlugin(
RunPlugin {
_allow_exec_host_cmd: false,
location: Zellij(
PluginTag(
"session-manager",
),
),
configuration: PluginUserConfiguration(
{
"floating": "true",
},
),
},
true,
),
SwitchToMode(
Normal,
),
],
Alt(
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(
"status-bar",
): PluginConfig {