summaryrefslogtreecommitdiffstats
path: root/zellij-utils
diff options
context:
space:
mode:
authorAram Drevekenin <aram@poor.dev>2023-08-24 13:36:24 +0200
committerGitHub <noreply@github.com>2023-08-24 13:36:24 +0200
commitbc628abc1266cdc0dbce4f19a89727527a3e39a8 (patch)
tree6a0fcac6ec35c71a237bca0128a57a5b40afb0e3 /zellij-utils
parentbf3c072d6dd68da0abd838e95e6004091a4cd331 (diff)
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
Diffstat (limited to 'zellij-utils')
-rw-r--r--zellij-utils/assets/config/default.kdl7
-rwxr-xr-xzellij-utils/assets/plugins/compact-bar.wasmbin806599 -> 818565 bytes
-rwxr-xr-xzellij-utils/assets/plugins/fixture-plugin-for-tests.wasmbin768790 -> 787551 bytes
-rwxr-xr-xzellij-utils/assets/plugins/session-manager.wasmbin0 -> 771931 bytes
-rwxr-xr-xzellij-utils/assets/plugins/status-bar.wasmbin961879 -> 975452 bytes
-rwxr-xr-xzellij-utils/assets/plugins/strider.wasmbin2025837 -> 2033674 bytes
-rwxr-xr-xzellij-utils/assets/plugins/tab-bar.wasmbin781434 -> 792795 bytes
-rw-r--r--zellij-utils/src/consts.rs14
-rw-r--r--zellij-utils/src/data.rs45
-rw-r--r--zellij-utils/src/errors.rs5
-rw-r--r--zellij-utils/src/input/permission.rs15
-rw-r--r--zellij-utils/src/ipc.rs24
-rw-r--r--zellij-utils/src/kdl/mod.rs520
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap12
-rw-r--r--zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap81
-rw-r--r--zellij-utils/src/plugin_api/event.proto14
-rw-r--r--zellij-utils/src/plugin_api/event.rs218
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.proto9
-rw-r--r--zellij-utils/src/plugin_api/plugin_command.rs30
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__default_config_with_no_cli_arguments.snap40
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_env_vars_override_config_env_vars.snap40
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_keybinds_override_config_keybinds.snap17
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_plugins_override_config_plugins.snap40
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_themes_override_config_themes.snap40
-rw-r--r--zellij-utils/src/snapshots/zellij_utils__setup__setup_test__layout_ui_config_overrides_config_ui_config.snap40
25 files changed, 1176 insertions, 35 deletions
diff --git a/zellij-utils/assets/config/default.kdl b/zellij-utils/assets/config/default.kdl
index 32dfb86e0..aea41f274 100644
--- a/zellij-utils/assets/config/default.kdl
+++ b/zellij-utils/assets/config/default.kdl
@@ -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
diff --git a/zellij-utils/assets/plugins/compact-bar.wasm b/zellij-utils/assets/plugins/compact-bar.wasm
index 9e2112cf9..20af974ea 100755
--- a/zellij-utils/assets/plugins/compact-bar.wasm
+++ b/zellij-utils/assets/plugins/compact-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
index ff13cefd0..f3cdb15d0 100755
--- a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
+++ b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm
new file mode 100755
index 000000000..673af58ab
--- /dev/null
+++ b/zellij-utils/assets/plugins/session-manager.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm
index e18d4edf9..e91ee3eb3 100755
--- a/zellij-utils/assets/plugins/status-bar.wasm
+++ b/zellij-utils/assets/plugins/status-bar.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm
index 3aff9cc30..c85bd48a3 100755
--- a/zellij-utils/assets/plugins/strider.wasm
+++ b/zellij-utils/assets/plugins/strider.wasm
Binary files differ
diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm
index ee53ba180..2591e0675 100755
--- a/zellij-utils/assets/plugins/tab-bar.wasm
+++ b/zellij-utils/assets/plugins/tab-bar.wasm
Binary files differ
diff --git a/zellij-utils/src/consts.rs b/zellij-utils/src/consts.rs
index a2c3a42a5..43990b4ba 100644
--- a/zellij-utils/src/consts.rs
+++ b/zellij-utils/src/consts.rs
@@ -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");
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs
index a08e642f9..319191361 100644
--- a/zellij-utils/src/data.rs
+++ b/zellij-utils/src/data.rs
@@ -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),
}
diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs
index 306db448c..677cbc420 100644
--- a/zellij-utils/src/errors.rs
+++ b/zellij-utils/src/errors.rs
@@ -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,
}
diff --git a/zellij-utils/src/input/permission.rs b/zellij-utils/src/input/permission.rs
index 1755fe1e6..9ef7182ad 100644
--- a/zellij-utils/src/input/permission.rs
+++ b/zellij-utils/src/input/permission.rs
@@ -27,12 +27,16 @@ 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 {
diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs
index 149a157ae..0f3415209 100644
--- a/zellij-utils/src/ipc.rs
+++ b/zellij-utils/src/ipc.rs
@@ -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)]
diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs
index 13a03ad15..af49dbb30 100644
--- a/zellij-utils/src/kdl/mod.rs
+++ b/zellij-utils/src/kdl/mod.rs
@@ -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 ac