diff options
author | Aram Drevekenin <aram@poor.dev> | 2024-02-06 14:26:14 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-02-06 14:26:14 +0100 |
commit | 6b20a958f4e8db5614fffb27fe5d32f07ddfe855 (patch) | |
tree | c8383fb7941b5e0c23cb348a655f2ea0fafd8668 /zellij-utils/src | |
parent | 286f7ccc28e3a091e2d3ef5663ae7878056551e6 (diff) |
feat(sessions): welcome screen (#3112)
* prototype - can send layout name for new session from session-manager
* feat(sessions): ui for selecting layout for new session in the session-manager
* fix: send available layouts to plugins
* make tests compile
* fix tests
* improve ui
* fix: respect built-in layouts
* ui for built-in layouts
* some cleanups
* style(fmt): rustfmt
* welcome screen ui
* fix: make sure layout config is not shared between sessions
* allow disconnecting other users from current session and killing other sessions
* fix: respect default layout
* add welcome screen layout
* tests(plugins): new api methods
* fix(session-manager): do not quit welcome screen on esc and break
* fix(plugins): adjust permissions
* style(fmt): rustfmt
* style(fmt): fix warnings
Diffstat (limited to 'zellij-utils/src')
-rw-r--r-- | zellij-utils/src/data.rs | 45 | ||||
-rw-r--r-- | zellij-utils/src/errors.rs | 1 | ||||
-rw-r--r-- | zellij-utils/src/input/config.rs | 9 | ||||
-rw-r--r-- | zellij-utils/src/input/keybinds.rs | 11 | ||||
-rw-r--r-- | zellij-utils/src/input/layout.rs | 122 | ||||
-rw-r--r-- | zellij-utils/src/kdl/mod.rs | 50 | ||||
-rw-r--r-- | zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap | 4 | ||||
-rw-r--r-- | zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap | 7 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.proto | 6 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/event.rs | 57 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.proto | 8 | ||||
-rw-r--r-- | zellij-utils/src/plugin_api/plugin_command.rs | 30 | ||||
-rw-r--r-- | zellij-utils/src/setup.rs | 71 |
13 files changed, 363 insertions, 58 deletions
diff --git a/zellij-utils/src/data.rs b/zellij-utils/src/data.rs index f1e01115d..d001b75fb 100644 --- a/zellij-utils/src/data.rs +++ b/zellij-utils/src/data.rs @@ -765,6 +765,28 @@ pub struct SessionInfo { pub panes: PaneManifest, pub connected_clients: usize, pub is_current_session: bool, + pub available_layouts: Vec<LayoutInfo>, +} + +#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)] +pub enum LayoutInfo { + BuiltIn(String), + File(String), +} + +impl LayoutInfo { + pub fn name(&self) -> &str { + match self { + LayoutInfo::BuiltIn(name) => &name, + LayoutInfo::File(name) => &name, + } + } + pub fn is_builtin(&self) -> bool { + match self { + LayoutInfo::BuiltIn(_name) => true, + LayoutInfo::File(_name) => false, + } + } } use std::hash::{Hash, Hasher}; @@ -1032,12 +1054,12 @@ impl MessageToPlugin { self } pub fn new_plugin_instance_should_float(mut self, should_float: bool) -> Self { - let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); new_plugin_args.should_float = Some(should_float); self } pub fn new_plugin_instance_should_replace_pane(mut self, pane_id: PaneId) -> Self { - let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); new_plugin_args.pane_id_to_replace = Some(pane_id); self } @@ -1045,17 +1067,17 @@ impl MessageToPlugin { mut self, pane_title: impl Into<String>, ) -> Self { - let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); new_plugin_args.pane_title = Some(pane_title.into()); self } pub fn new_plugin_instance_should_have_cwd(mut self, cwd: PathBuf) -> Self { - let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); new_plugin_args.cwd = Some(cwd); self } pub fn new_plugin_instance_should_skip_cache(mut self) -> Self { - let mut new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); + let new_plugin_args = self.new_plugin_args.get_or_insert_with(Default::default); new_plugin_args.skip_cache = true; self } @@ -1066,6 +1088,17 @@ pub struct ConnectToSession { pub name: Option<String>, pub tab_position: Option<usize>, pub pane_id: Option<(u32, bool)>, // (id, is_plugin) + pub layout: Option<LayoutInfo>, +} + +impl ConnectToSession { + pub fn apply_layout_dir(&mut self, layout_dir: &PathBuf) { + if let Some(LayoutInfo::File(file_path)) = self.layout.as_mut() { + *file_path = Path::join(layout_dir, &file_path) + .to_string_lossy() + .to_string(); + } + } } #[derive(Debug, Default, Clone)] @@ -1228,4 +1261,6 @@ pub enum PluginCommand { BlockCliPipeInput(String), // String => pipe name CliPipeOutput(String, String), // String => pipe name, String => output MessageToPlugin(MessageToPlugin), + DisconnectOtherClients, + KillSessions(Vec<String>), // one or more session names } diff --git a/zellij-utils/src/errors.rs b/zellij-utils/src/errors.rs index 26ad521c3..5b3233998 100644 --- a/zellij-utils/src/errors.rs +++ b/zellij-utils/src/errors.rs @@ -442,6 +442,7 @@ pub enum ServerContext { UnblockCliPipeInput, CliPipeOutput, AssociatePipeWithClient, + DisconnectAllClientsExcept, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 23c05d4f8..eb8fe521d 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -222,6 +222,15 @@ impl Config { Err(e) => Err(ConfigError::IoPath(e, path.into())), } } + pub fn merge(&mut self, other: Config) -> Result<(), ConfigError> { + self.options = self.options.merge(other.options); + self.keybinds.merge(other.keybinds.clone()); + self.themes = self.themes.merge(other.themes); + self.plugins = self.plugins.merge(other.plugins); + self.ui = self.ui.merge(other.ui); + self.env = self.env.merge(other.env); + Ok(()) + } } #[cfg(test)] diff --git a/zellij-utils/src/input/keybinds.rs b/zellij-utils/src/input/keybinds.rs index eaed03fd4..ab13f711f 100644 --- a/zellij-utils/src/input/keybinds.rs +++ b/zellij-utils/src/input/keybinds.rs @@ -65,6 +65,17 @@ impl Keybinds { } ret } + pub fn merge(&mut self, mut other: Keybinds) { + for (other_input_mode, mut other_input_mode_keybinds) in other.0.drain() { + let input_mode_keybinds = self + .0 + .entry(other_input_mode) + .or_insert_with(|| Default::default()); + for (other_action, other_action_keybinds) in other_input_mode_keybinds.drain() { + input_mode_keybinds.insert(other_action, other_action_keybinds); + } + } + } } // The unit test location. diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 82dab15ee..7f65d6054 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -9,8 +9,8 @@ // If plugins should be able to depend on the layout system // then [`zellij-utils`] could be a proper place. use crate::{ - data::Direction, - home::find_default_config_dir, + data::{Direction, LayoutInfo}, + home::{default_layout_dir, find_default_config_dir}, input::{ command::RunCommand, config::{Config, ConfigError}, @@ -19,6 +19,7 @@ use crate::{ setup::{self}, }; +use std::cmp::Ordering; use std::fmt::{Display, Formatter}; use std::str::FromStr; @@ -819,6 +820,62 @@ impl Default for LayoutParts { } impl Layout { + // the first layout will either be the default one + pub fn list_available_layouts( + layout_dir: Option<PathBuf>, + default_layout_name: &Option<String>, + ) -> Vec<LayoutInfo> { + let mut available_layouts = layout_dir + .clone() + .or_else(|| default_layout_dir()) + .and_then(|layout_dir| match std::fs::read_dir(layout_dir) { + Ok(layout_files) => Some(layout_files), + Err(e) => { + log::error!("Failed to read layout dir: {:?}", e); + None + }, + }) + .map(|layout_files| { + let mut available_layouts = vec![]; + for file in layout_files { + if let Ok(file) = file { + if Layout::from_path_or_default_without_config( + Some(&file.path()), + layout_dir.clone(), + ) + .is_ok() + { + if let Some(file_name) = file.path().file_stem() { + available_layouts + .push(LayoutInfo::File(file_name.to_string_lossy().to_string())) + } + } + } + } + available_layouts + }) + .unwrap_or_else(Default::default); + let default_layout_name = default_layout_name + .as_ref() + .map(|d| d.as_str()) + .unwrap_or("default"); + available_layouts.push(LayoutInfo::BuiltIn("default".to_owned())); + available_layouts.push(LayoutInfo::BuiltIn("strider".to_owned())); + available_layouts.push(LayoutInfo::BuiltIn("disable-status-bar".to_owned())); + available_layouts.push(LayoutInfo::BuiltIn("compact".to_owned())); + available_layouts.sort_by(|a, b| { + let a_name = a.name(); + let b_name = b.name(); + if a_name == default_layout_name { + return Ordering::Less; + } else if b_name == default_layout_name { + return Ordering::Greater; + } else { + a_name.cmp(&b_name) + } + }); + available_layouts + } pub fn stringified_from_path_or_default( layout_path: Option<&PathBuf>, layout_dir: Option<PathBuf>, @@ -861,6 +918,40 @@ impl Layout { let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with Ok((layout, config)) } + pub fn from_path_or_default_without_config( + layout_path: Option<&PathBuf>, + layout_dir: Option<PathBuf>, + ) -> Result<Layout, ConfigError> { + let (path_to_raw_layout, raw_layout, raw_swap_layouts) = + Layout::stringified_from_path_or_default(layout_path, layout_dir)?; + let layout = Layout::from_kdl( + &raw_layout, + path_to_raw_layout, + raw_swap_layouts + .as_ref() + .map(|(r, f)| (r.as_str(), f.as_str())), + None, + )?; + Ok(layout) + } + pub fn from_default_assets( + layout_name: &Path, + _layout_dir: Option<PathBuf>, + config: Config, + ) -> Result<(Layout, Config), ConfigError> { + let (path_to_raw_layout, raw_layout, raw_swap_layouts) = + Layout::stringified_from_default_assets(layout_name)?; + let layout = Layout::from_kdl( + &raw_layout, + path_to_raw_layout, + raw_swap_layouts + .as_ref() + .map(|(r, f)| (r.as_str(), f.as_str())), + None, + )?; + let config = Config::from_kdl(&raw_layout, Some(config))?; // this merges the two config, with + Ok((layout, config)) + } pub fn from_str( raw: &str, path_to_raw_layout: String, @@ -951,6 +1042,11 @@ impl Layout { Self::stringified_compact_swap_from_assets()?, )), )), + Some("welcome") => Ok(( + "Welcome screen layout".into(), + Self::stringified_welcome_from_assets()?, + None, + )), None | Some(_) => Err(ConfigError::IoPath( std::io::Error::new(std::io::ErrorKind::Other, "The layout was not found"), path.into(), @@ -982,6 +1078,10 @@ impl Layout { Ok(String::from_utf8(setup::COMPACT_BAR_SWAP_LAYOUT.to_vec())?) } + pub fn stringified_welcome_from_assets() -> Result<String, ConfigError> { + Ok(String::from_utf8(setup::WELCOME_LAYOUT.to_vec())?) + } + pub fn new_tab(&self) -> (TiledPaneLayout, Vec<FloatingPaneLayout>) { self.template.clone().unwrap_or_default() } @@ -1024,24 +1124,10 @@ impl Layout { swap_layout_path.as_os_str().to_string_lossy().into(), swap_kdl_layout, )), - Err(e) => { - log::warn!( - "Failed to read swap layout file: {}. Error: {:?}", - swap_layout_path.as_os_str().to_string_lossy(), - e - ); - None - }, + Err(_e) => None, } }, - Err(e) => { - log::warn!( - "Failed to read swap layout file: {}. Error: {:?}", - swap_layout_path.as_os_str().to_string_lossy(), - e - ); - None - }, + Err(_e) => None, } } } diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index 71f48fc4f..e911f391e 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -1,7 +1,7 @@ mod kdl_layout_parser; use crate::data::{ - Direction, InputMode, Key, Palette, PaletteColor, PaneInfo, PaneManifest, PermissionType, - Resize, SessionInfo, TabInfo, + Direction, InputMode, Key, LayoutInfo, Palette, PaletteColor, PaneInfo, PaneManifest, + PermissionType, Resize, SessionInfo, TabInfo, }; use crate::envs::EnvironmentVariables; use crate::home::{find_default_config_dir, get_layout_dir}; @@ -1986,6 +1986,31 @@ impl SessionInfo { .and_then(|p| p.children()) .map(|p| PaneManifest::decode_from_kdl(p)) .ok_or("Failed to parse panes")?; + let available_layouts: Vec<LayoutInfo> = kdl_document + .get("available_layouts") + .and_then(|p| p.children()) + .map(|e| { + e.nodes() + .iter() + .filter_map(|n| { + let layout_name = n.name().value().to_owned(); + let layout_source = n + .entries() + .iter() + .find(|e| e.name().map(|n| n.value()) == Some("source")) + .and_then(|e| e.value().as_string()); + match layout_source { + Some(layout_source) => match layout_source { + "built-in" => Some(LayoutInfo::BuiltIn(layout_name)), + "file" => Some(LayoutInfo::File(layout_name)), + _ => None, + }, + None => None, + } + }) + .collect() + }) + .ok_or("Failed to parse available_layouts")?; let is_current_session = name == current_session_name; Ok(SessionInfo { name, @@ -1993,6 +2018,7 @@ impl SessionInfo { panes, connected_clients, is_current_session, + available_layouts, }) } pub fn to_string(&self) -> String { @@ -2017,10 +2043,25 @@ impl SessionInfo { let mut panes = KdlNode::new("panes"); panes.set_children(self.panes.encode_to_kdl()); + let mut available_layouts = KdlNode::new("available_layouts"); + let mut available_layouts_children = KdlDocument::new(); + for layout_info in &self.available_layouts { + let (layout_name, layout_source) = match layout_info { + LayoutInfo::File(name) => (name.clone(), "file"), + LayoutInfo::BuiltIn(name) => (name.clone(), "built-in"), + }; + let mut layout_node = KdlNode::new(format!("{}", layout_name)); + let layout_source = KdlEntry::new_prop("source", layout_source); + layout_node.entries_mut().push(layout_source); + available_layouts_children.nodes_mut().push(layout_node); + } + available_layouts.set_children(available_layouts_children); + kdl_document.nodes_mut().push(name); kdl_document.nodes_mut().push(tabs); kdl_document.nodes_mut().push(panes); kdl_document.nodes_mut().push(connected_clients); + kdl_document.nodes_mut().push(available_layouts); kdl_document.fmt(); kdl_document.to_string() } @@ -2506,6 +2547,11 @@ fn serialize_and_deserialize_session_info_with_data() { panes: PaneManifest { panes }, connected_clients: 2, is_current_session: false, + available_layouts: vec![ + LayoutInfo::File("layout1".to_owned()), + LayoutInfo::BuiltIn("layout2".to_owned()), + LayoutInfo::File("layout3".to_owned()), + ], }; let serialized = session_info.to_string(); let deserealized = SessionInfo::from_string(&serialized, "not this session").unwrap(); diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap index 4a231c37b..32925ec11 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/kdl/mod.rs -assertion_line: 2284 +assertion_line: 2459 expression: serialized --- name "" @@ -9,4 +9,6 @@ tabs { panes { } connected_clients 0 +available_layouts { +} diff --git a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap index 99e3e03fe..8244f04ad 100644 --- a/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap +++ b/zellij-utils/src/kdl/snapshots/zellij_utils__kdl__serialize_and_deserialize_session_info_with_data.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/kdl/mod.rs -assertion_line: 2377 +assertion_line: 2552 expression: serialized --- name "my session name" @@ -78,4 +78,9 @@ panes { } } connected_clients 2 +available_layouts { + layout1 source="file" + layout2 source="built-in" + layout3 source="file" +} diff --git a/zellij-utils/src/plugin_api/event.proto b/zellij-utils/src/plugin_api/event.proto index 0d964a354..e26a9c23a 100644 --- a/zellij-utils/src/plugin_api/event.proto +++ b/zellij-utils/src/plugin_api/event.proto @@ -152,6 +152,12 @@ message SessionManifest { repeated PaneManifest panes = 3; uint32 connected_clients = 4; bool is_current_session = 5; + repeated LayoutInfo available_layouts = 6; +} + +message LayoutInfo { + string name = 1; + string source = 2; } message ResurrectableSession { diff --git a/zellij-utils/src/plugin_api/event.rs b/zellij-utils/src/plugin_api/event.rs index d9e5341d6..21ff1eff1 100644 --- a/zellij-utils/src/plugin_api/event.rs +++ b/zellij-utils/src/plugin_api/event.rs @@ -4,9 +4,9 @@ pub use super::generated_api::api::{ event::Payload as ProtobufEventPayload, CopyDestination as ProtobufCopyDestination, Event as ProtobufEvent, EventNameList as ProtobufEventNameList, EventType as ProtobufEventType, InputModeKeybinds as ProtobufInputModeKeybinds, - KeyBind as ProtobufKeyBind, ModeUpdatePayload as ProtobufModeUpdatePayload, - PaneInfo as ProtobufPaneInfo, PaneManifest as ProtobufPaneManifest, - ResurrectableSession as ProtobufResurrectableSession, + KeyBind as ProtobufKeyBind, LayoutInfo as ProtobufLayoutInfo, + ModeUpdatePayload as ProtobufModeUpdatePayload, PaneInfo as ProtobufPaneInfo, + PaneManifest as ProtobufPaneManifest, ResurrectableSession as ProtobufResurrectableSession, SessionManifest as ProtobufSessionManifest, TabInfo as ProtobufTabInfo, *, }, input_mode::InputMode as ProtobufInputMode, @@ -14,8 +14,8 @@ pub use super::generated_api::api::{ style::Style as ProtobufStyle, }; use crate::data::{ - CopyDestination, Event, EventType, InputMode, Key, ModeInfo, Mouse, PaneInfo, PaneManifest, - PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo, + CopyDestination, Event, EventType, InputMode, Key, LayoutInfo, ModeInfo, Mouse, PaneInfo, + PaneManifest, PermissionStatus, PluginCapabilities, SessionInfo, Style, TabInfo, }; use crate::errors::prelude::*; @@ -453,6 +453,11 @@ impl TryFrom<SessionInfo> for ProtobufSessionManifest { .collect(), connected_clients: session_info.connected_clients as u32, is_current_session: session_info.is_current_session, + available_layouts: session_info + .available_layouts + .into_iter() + .filter_map(|l| ProtobufLayoutInfo::try_from(l).ok()) + .collect(), }) } } @@ -485,10 +490,42 @@ impl TryFrom<ProtobufSessionManifest> for SessionInfo { panes, connected_clients: protobuf_session_manifest.connected_clients as usize, is_current_session: protobuf_session_manifest.is_current_session, + available_layouts: protobuf_session_manifest + .available_layouts + .into_iter() + .filter_map(|l| LayoutInfo::try_from(l).ok()) + .collect(), }) } } +impl TryFrom<LayoutInfo> for ProtobufLayoutInfo { + type Error = &'static str; + fn try_from(layout_info: LayoutInfo) -> Result<Self, &'static str> { + match layout_info { + LayoutInfo::File(name) => Ok(ProtobufLayoutInfo { + source: "file".to_owned(), + name, + }), + LayoutInfo::BuiltIn(name) => Ok(ProtobufLayoutInfo { + source: "built-in".to_owned(), + name, + }), + } + } +} + +impl TryFrom<ProtobufLayoutInfo> for LayoutInfo { + type Error = &'static str; + fn try_from(protobuf_layout_info: ProtobufLayoutInfo) -> Result<Self, &'static str> { + match protobuf_layout_info.source.as_str() { + "file" => Ok(LayoutInfo::File(protobuf_layout_info.name)), + "built-in" => Ok(LayoutInfo::BuiltIn(protobuf_layout_info.name)), + _ => Err("Unknown source for layout"), + } + } +} + impl TryFrom<CopyDestination> for ProtobufCopyDestination { type Error = &'static str; fn try_from(copy_destination: CopyDestination) -> Result<Self, &'static str> { @@ -1383,6 +1420,11 @@ fn serialize_session_update_event_with_non_default_values() { panes: PaneManifest { panes }, connected_clients: 2, is_current_session: true, + available_layouts: vec![ + LayoutInfo::File("layout 1".to_owned()), + LayoutInfo::BuiltIn("layout2".to_owned()), |