diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-08-24 13:36:24 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-24 13:36:24 +0200 |
commit | bc628abc1266cdc0dbce4f19a89727527a3e39a8 (patch) | |
tree | 6a0fcac6ec35c71a237bca0128a57a5b40afb0e3 /zellij-utils | |
parent | bf3c072d6dd68da0abd838e95e6004091a4cd331 (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')
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 Binary files differindex 9e2112cf9..20af974ea 100755 --- a/zellij-utils/assets/plugins/compact-bar.wasm +++ b/zellij-utils/assets/plugins/compact-bar.wasm diff --git a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm Binary files differindex ff13cefd0..f3cdb15d0 100755 --- a/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm +++ b/zellij-utils/assets/plugins/fixture-plugin-for-tests.wasm diff --git a/zellij-utils/assets/plugins/session-manager.wasm b/zellij-utils/assets/plugins/session-manager.wasm Binary files differnew file mode 100755 index 000000000..673af58ab --- /dev/null +++ b/zellij-utils/assets/plugins/session-manager.wasm diff --git a/zellij-utils/assets/plugins/status-bar.wasm b/zellij-utils/assets/plugins/status-bar.wasm Binary files differindex e18d4edf9..e91ee3eb3 100755 --- a/zellij-utils/assets/plugins/status-bar.wasm +++ b/zellij-utils/assets/plugins/status-bar.wasm diff --git a/zellij-utils/assets/plugins/strider.wasm b/zellij-utils/assets/plugins/strider.wasm Binary files differindex 3aff9cc30..c85bd48a3 100755 --- a/zellij-utils/assets/plugins/strider.wasm +++ b/zellij-utils/assets/plugins/strider.wasm diff --git a/zellij-utils/assets/plugins/tab-bar.wasm b/zellij-utils/assets/plugins/tab-bar.wasm Binary files differindex ee53ba180..2591e0675 100755 --- a/zellij-utils/assets/plugins/tab-bar.wasm +++ b/zellij-utils/assets/plugins/tab-bar.wasm 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 |