diff options
author | spacemaison <tuchsen@protonmail.com> | 2021-09-22 10:13:21 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-09-22 18:13:21 +0100 |
commit | c9372212f68fed52d45590b2dac271ab6270d943 (patch) | |
tree | 031f384ab012817656876b5db020b9e9c6a41a1f /zellij-utils | |
parent | c39f02181052f1c0948001d4ae419009fc0df677 (diff) |
feat(plugin): add manifest to allow for plugin configuration (#660)
* feat(plugins-manifest): Add a plugins manifest to allow for more configuration of plugins
* refactor(plugins-manifest): Better storage of plugin metadata in wasm_vm
* fix(plugins-manifest): Inherit permissions from run configuration
* refactor(plugins-manifest): Rename things for more clarity
- The Plugins/Plugin structs had "Config" appended to them to clarify
that they're metadata about plugins, and not the plugins themselves.
- The PluginType::OncePerPane variant was renamed to be just
PluginType::Pane, and the documentation clarified to explain what it
is.
- The "service" nomenclature was completely removed in favor of
"headless".
* refactor(plugins-manifest): Move security warning into start plugin
* refactor(plugins-manifest): Remove hack in favor of standard method
* refactor(plugins-manifest): Change display of plugin location
The only time that a plugin location is displayed in Zellij is the
border of the pane. Having `zellij:strider` display instead of just
`strider` was a little annoying, so we're stripping out the scheme
information from a locations display.
* refactor(plugins-manifest): Add a little more documentation
* fix(plugins-manifest): Formatting
Co-authored-by: Jesse Tuchsen <not@disclosing>
Diffstat (limited to 'zellij-utils')
-rw-r--r-- | zellij-utils/Cargo.toml | 2 | ||||
-rw-r--r-- | zellij-utils/assets/config/default.yaml | 7 | ||||
-rw-r--r-- | zellij-utils/assets/layouts/default.yaml | 4 | ||||
-rw-r--r-- | zellij-utils/assets/layouts/disable-status-bar.yaml | 2 | ||||
-rw-r--r-- | zellij-utils/assets/layouts/strider.yaml | 6 | ||||
-rw-r--r-- | zellij-utils/src/input/config.rs | 29 | ||||
-rw-r--r-- | zellij-utils/src/input/layout.rs | 150 | ||||
-rw-r--r-- | zellij-utils/src/input/mod.rs | 1 | ||||
-rw-r--r-- | zellij-utils/src/input/plugins.rs | 315 | ||||
-rw-r--r-- | zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml | 5 | ||||
-rw-r--r-- | zellij-utils/src/input/unit/layout_test.rs | 89 | ||||
-rw-r--r-- | zellij-utils/src/ipc.rs | 10 | ||||
-rw-r--r-- | zellij-utils/src/setup.rs | 1 |
13 files changed, 535 insertions, 86 deletions
diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 66e8c1cf4..8751edbd8 100644 --- a/zellij-utils/Cargo.toml +++ b/zellij-utils/Cargo.toml @@ -21,11 +21,13 @@ nix = "0.19.1" once_cell = "1.7.2" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8" +serde_json = "1.0" signal-hook = "0.3" strip-ansi-escapes = "0.1.0" structopt = "0.3" strum = "0.20.0" termion = "1.5.0" +url = { version = "2.2.2", features = ["serde"] } vte = "0.10.1" zellij-tile = { path = "../zellij-tile/", version = "0.18.0" } log = "0.4.14" diff --git a/zellij-utils/assets/config/default.yaml b/zellij-utils/assets/config/default.yaml index 60b4069eb..c00fdfbed 100644 --- a/zellij-utils/assets/config/default.yaml +++ b/zellij-utils/assets/config/default.yaml @@ -248,6 +248,13 @@ keybinds: key: [Ctrl: 'q',] - action: [Detach,] key: [Char: 'd',] +plugins: + - path: tab-bar + tag: tab-bar + - path: status-bar + tag: status-bar + - path: strider + tag: strider # Choose what to do when zellij receives SIGTERM, SIGINT, SIGQUIT or SIGHUP # eg. when terminal window with an active zellij session is closed diff --git a/zellij-utils/assets/layouts/default.yaml b/zellij-utils/assets/layouts/default.yaml index 549dea24b..0688e54bb 100644 --- a/zellij-utils/assets/layouts/default.yaml +++ b/zellij-utils/assets/layouts/default.yaml @@ -8,7 +8,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true - direction: Vertical @@ -17,6 +17,6 @@ template: Fixed: 2 run: plugin: - path: status-bar + location: "zellij:status-bar" tabs: - direction: Vertical diff --git a/zellij-utils/assets/layouts/disable-status-bar.yaml b/zellij-utils/assets/layouts/disable-status-bar.yaml index e97bb8f1e..107793980 100644 --- a/zellij-utils/assets/layouts/disable-status-bar.yaml +++ b/zellij-utils/assets/layouts/disable-status-bar.yaml @@ -8,6 +8,6 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true diff --git a/zellij-utils/assets/layouts/strider.yaml b/zellij-utils/assets/layouts/strider.yaml index ccb2a5748..26e1eba4f 100644 --- a/zellij-utils/assets/layouts/strider.yaml +++ b/zellij-utils/assets/layouts/strider.yaml @@ -8,7 +8,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Vertical body: true - direction: Vertical @@ -17,7 +17,7 @@ template: Fixed: 2 run: plugin: - path: status-bar + location: "zellij:status-bar" tabs: - direction: Vertical parts: @@ -26,5 +26,5 @@ tabs: Percent: 20 run: plugin: - path: strider + location: "zellij:strider" - direction: Horizontal diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 941efa1d9..e55798e75 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -5,15 +5,16 @@ use std::fs::File; use std::io::{self, Read}; use std::path::{Path, PathBuf}; +use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; + use super::keybinds::{Keybinds, KeybindsFromYaml}; use super::options::Options; +use super::plugins::{PluginsConfig, PluginsConfigError, PluginsConfigFromYaml}; use super::theme::ThemesFromYaml; use crate::cli::{CliArgs, Command}; use crate::setup; -use serde::{Deserialize, Serialize}; -use std::convert::TryFrom; - const DEFAULT_CONFIG_FILE_NAME: &str = "config.yaml"; type ConfigResult = Result<Config, ConfigError>; @@ -25,6 +26,8 @@ pub struct ConfigFromYaml { pub options: Option<Options>, pub keybinds: Option<KeybindsFromYaml>, pub themes: Option<ThemesFromYaml>, + #[serde(default)] + pub plugins: PluginsConfigFromYaml, } /// Main configuration. @@ -33,6 +36,7 @@ pub struct Config { pub keybinds: Keybinds, pub options: Options, pub themes: Option<ThemesFromYaml>, + pub plugins: PluginsConfig, } #[derive(Debug)] @@ -47,6 +51,8 @@ pub enum ConfigError { FromUtf8(std::string::FromUtf8Error), // Naming a part in a tab is unsupported LayoutNameInTab(LayoutNameInTabError), + // Plugins have a semantic error, usually trying to parse two of the same tag + PluginsError(PluginsConfigError), } impl Default for Config { @@ -54,11 +60,13 @@ impl Default for Config { let keybinds = Keybinds::default(); let options = Options::default(); let themes = None; + let plugins = PluginsConfig::default(); Config { keybinds, options, themes, + plugins, } } } @@ -106,9 +114,11 @@ impl Config { let keybinds = Keybinds::get_default_keybinds_with_config(config.keybinds); let options = Options::from_yaml(config.options); let themes = config.themes; + let plugins = PluginsConfig::get_plugins_with_default(config.plugins.try_into()?); Ok(Config { keybinds, options, + plugins, themes, }) } @@ -129,10 +139,11 @@ impl Config { } /// Gets default configuration from assets - // TODO Deserialize the Configuration from bytes &[u8], + // TODO Deserialize the Config from bytes &[u8], // once serde-yaml supports zero-copy pub fn from_default_assets() -> ConfigResult { - Self::from_yaml(String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?.as_str()) + let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec())?; + Self::from_yaml(cfg.as_str()) } } @@ -179,6 +190,7 @@ impl Display for ConfigError { ConfigError::LayoutNameInTab(ref err) => { write!(formatter, "There was an error in the layout file, {}", err) } + ConfigError::PluginsError(ref err) => write!(formatter, "PluginsError: {}", err), } } } @@ -191,6 +203,7 @@ impl std::error::Error for ConfigError { ConfigError::Serde(ref err) => Some(err), ConfigError::FromUtf8(ref err) => Some(err), ConfigError::LayoutNameInTab(ref err) => Some(err), + ConfigError::PluginsError(ref err) => Some(err), } } } @@ -219,6 +232,12 @@ impl From<LayoutNameInTabError> for ConfigError { } } +impl From<PluginsConfigError> for ConfigError { + fn from(err: PluginsConfigError) -> ConfigError { + ConfigError::PluginsError(err) + } +} + // The unit test location. #[cfg(test)] mod config_test { diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index 9dc8cd47b..033614151 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -18,14 +18,18 @@ use crate::{ }; use crate::{serde, serde_yaml}; +use super::plugins::{PluginTag, PluginsConfigError}; use serde::{Deserialize, Serialize}; +use std::convert::{TryFrom, TryInto}; use std::vec::Vec; use std::{ cmp::max, + fmt, fs, ops::Not, path::{Path, PathBuf}, }; use std::{fs::File, io::prelude::*}; +use url::Url; #[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy)] #[serde(crate = "self::serde")] @@ -56,17 +60,68 @@ pub enum SplitSize { #[serde(crate = "self::serde")] pub enum Run { #[serde(rename = "plugin")] - Plugin(Option<RunPlugin>), + Plugin(RunPlugin), #[serde(rename = "command")] Command(RunCommand), } -#[derive(Debug, Default, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub enum RunFromYaml { + #[serde(rename = "plugin")] + Plugin(RunPluginFromYaml), + #[serde(rename = "command")] + Command(RunCommand), +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub struct RunPluginFromYaml { + #[serde(default)] + pub _allow_exec_host_cmd: bool, + pub location: Url, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] #[serde(crate = "self::serde")] pub struct RunPlugin { - pub path: PathBuf, #[serde(default)] pub _allow_exec_host_cmd: bool, + pub location: RunPluginLocation, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] +#[serde(crate = "self::serde")] +pub enum RunPluginLocation { + File(PathBuf), + Zellij(PluginTag), +} + +impl From<&RunPluginLocation> for Url { + fn from(location: &RunPluginLocation) -> Self { + let url = match location { + RunPluginLocation::File(path) => format!( + "file:{}", + path.clone().into_os_string().into_string().unwrap() + ), + RunPluginLocation::Zellij(tag) => format!("zellij:{}", tag), + }; + Self::parse(&url).unwrap() + } +} + +impl fmt::Display for RunPluginLocation { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + match self { + Self::File(path) => write!( + f, + "{}", + path.clone().into_os_string().into_string().unwrap() + ), + + Self::Zellij(tag) => write!(f, "{}", tag), + } + } } // The layout struct ultimately used to build the layouts. @@ -193,7 +248,7 @@ pub struct LayoutTemplate { #[serde(default)] pub body: bool, pub split_size: Option<SplitSize>, - pub run: Option<Run>, + pub run: Option<RunFromYaml>, } impl LayoutTemplate { @@ -235,9 +290,9 @@ pub struct TabLayout { #[serde(default)] pub parts: Vec<TabLayout>, pub split_size: Option<SplitSize>, - pub run: Option<Run>, #[serde(default)] pub name: String, + pub run: Option<RunFromYaml>, } impl TabLayout { @@ -291,25 +346,23 @@ impl Layout { split_space(space, self) } - pub fn merge_tab_layout(&mut self, tab: TabLayout) { - self.parts.push(tab.into()); - } - pub fn merge_layout_parts(&mut self, mut parts: Vec<Layout>) { self.parts.append(&mut parts); } - fn from_vec_tab_layout(tab_layout: Vec<TabLayout>) -> Vec<Self> { + fn from_vec_tab_layout(tab_layout: Vec<TabLayout>) -> Result<Vec<Self>, ConfigError> { tab_layout .iter() - .map(|tab_layout| Layout::from(tab_layout.to_owned())) + .map(|tab_layout| Layout::try_from(tab_layout.to_owned())) .collect() } - fn from_vec_template_layout(layout_template: Vec<LayoutTemplate>) -> Vec<Self> { + fn from_vec_template_layout( + layout_template: Vec<LayoutTemplate>, + ) -> Result<Vec<Self>, ConfigError> { layout_template .iter() - .map(|layout_template| Layout::from(layout_template.to_owned())) + .map(|layout_template| Layout::try_from(layout_template.to_owned())) .collect() } } @@ -408,15 +461,55 @@ fn split_space(space_to_split: &PaneGeom, layout: &Layout) -> Vec<(Layout, PaneG pane_positions } -impl From<TabLayout> for Layout { - fn from(tab: TabLayout) -> Self { - Layout { +impl TryFrom<Url> for RunPluginLocation { + type Error = PluginsConfigError; + + fn try_from(url: Url) -> Result<Self, Self::Error> { + match url.scheme() { + "zellij" => Ok(Self::Zellij(PluginTag::new(url.path()))), + "file" => { + let path = PathBuf::from(url.path()); + let canonicalize = |p: &Path| { + fs::canonicalize(p) + .map_err(|_| PluginsConfigError::InvalidPluginLocation(p.to_owned())) + }; + canonicalize(&path) + .or_else(|_| match path.strip_prefix("/") { + Ok(path) => canonicalize(path), + Err(_) => Err(PluginsConfigError::InvalidPluginLocation(path.to_owned())), + }) + .map(Self::File) + } + _ => Err(PluginsConfigError::InvalidUrl(url)), + } + } +} + +impl TryFrom<RunFromYaml> for Run { + type Error = PluginsConfigError; + + fn try_from(run: RunFromYaml) -> Result<Self, Self::Error> { + match run { + RunFromYaml::Command(command) => Ok(Run::Command(command)), + RunFromYaml::Plugin(plugin) => Ok(Run::Plugin(RunPlugin { + _allow_exec_host_cmd: plugin._allow_exec_host_cmd, + location: plugin.location.try_into()?, + })), + } + } +} + +impl TryFrom<TabLayout> for Layout { + type Error = ConfigError; + + fn try_from(tab: TabLayout) -> Result<Self, Self::Error> { + Ok(Layout { direction: tab.direction, borderless: tab.borderless, - parts: Self::from_vec_tab_layout(tab.parts), + parts: Self::from_vec_tab_layout(tab.parts)?, split_size: tab.split_size, - run: tab.run, - } + run: tab.run.map(Run::try_from).transpose()?, + }) } } @@ -433,15 +526,22 @@ impl From<TabLayout> for LayoutTemplate { } } -impl From<LayoutTemplate> for Layout { - fn from(template: LayoutTemplate) -> Self { - Layout { +impl TryFrom<LayoutTemplate> for Layout { + type Error = ConfigError; + + fn try_from(template: LayoutTemplate) -> Result<Self, Self::Error> { + Ok(Layout { direction: template.direction, borderless: template.borderless, - parts: Self::from_vec_template_layout(template.parts), + parts: Self::from_vec_template_layout(template.parts)?, split_size: template.split_size, - run: template.run, - } + run: template + .run + .map(Run::try_from) + // FIXME: This is just Result::transpose but that method is unstable, when it + // stabalizes we should swap this out. + .map_or(Ok(None), |r| r.map(Some))?, + }) } } diff --git a/zellij-utils/src/input/mod.rs b/zellij-utils/src/input/mod.rs index 68aa9dd79..409c9afae 100644 --- a/zellij-utils/src/input/mod.rs +++ b/zellij-utils/src/input/mod.rs @@ -7,6 +7,7 @@ pub mod keybinds; pub mod layout; pub mod mouse; pub mod options; +pub mod plugins; pub mod theme; use termion::input::TermRead; diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs new file mode 100644 index 000000000..931d9f780 --- /dev/null +++ b/zellij-utils/src/input/plugins.rs @@ -0,0 +1,315 @@ +//! Plugins configuration metadata +use std::borrow::Borrow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt::{self, Display}; +use std::fs; +use std::path::{Path, PathBuf}; + +use lazy_static::lazy_static; +use serde::{Deserialize, Serialize}; +use url::Url; + +use super::config::ConfigFromYaml; +use super::layout::{RunPlugin, RunPluginLocation}; +use crate::setup; +pub use zellij_tile::data::PluginTag; + +lazy_static! { + static ref DEFAULT_CONFIG_PLUGINS: PluginsConfig = { + let cfg = String::from_utf8(setup::DEFAULT_CONFIG.to_vec()).unwrap(); + let cfg_yaml: ConfigFromYaml = serde_yaml::from_str(cfg.as_str()).unwrap(); + PluginsConfig::try_from(cfg_yaml.plugins).unwrap() + }; +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct PluginsConfigFromYaml(Vec<PluginConfigFromYaml>); + +/// Used in the config struct for plugin metadata +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PluginsConfig(HashMap<PluginTag, PluginConfig>); + +impl PluginsConfig { + pub fn new() -> Self { + Self(HashMap::new()) + } + + /// Entrypoint from the config module + pub fn get_plugins_with_default(user_plugins: Self) -> Self { + let mut base_plugins = DEFAULT_CONFIG_PLUGINS.clone(); + base_plugins.0.extend(user_plugins.0); + base_plugins + } + + /// Get plugin config from run configuration specified in layout files. + pub fn get(&self, run: impl Borrow<RunPlugin>) -> Option<PluginConfig> { + let run = run.borrow(); + match &run.location { + RunPluginLocation::File(path) => Some(PluginConfig { + path: path.clone(), + run: PluginType::Pane(None), + _allow_exec_host_cmd: run._allow_exec_host_cmd, + location: run.location.clone(), + }), + RunPluginLocation::Zellij(tag) => self.0.get(tag).cloned().map(|plugin| PluginConfig { + _allow_exec_host_cmd: run._allow_exec_host_cmd, + ..plugin + }), + } + } + + pub fn iter(&self) -> impl Iterator<Item = &PluginConfig> { + self.0.values() + } +} + +impl Default for PluginsConfig { + fn default() -> Self { + Self::get_plugins_with_default(PluginsConfig::new()) + } +} + +impl TryFrom<PluginsConfigFromYaml> for PluginsConfig { + type Error = PluginsConfigError; + + fn try_from(yaml: PluginsConfigFromYaml) -> Result<Self, PluginsConfigError> { + let mut plugins = HashMap::new(); + for plugin in yaml.0 { + if plugins.contains_key(&plugin.tag) { + return Err(PluginsConfigError::DuplicatePlugins(plugin.tag)); + } + plugins.insert(plugin.tag.clone(), plugin.into()); + } + + Ok(PluginsConfig(plugins)) + } +} + +impl From<PluginConfigFromYaml> for PluginConfig { + fn from(plugin: PluginConfigFromYaml) -> Self { + PluginConfig { + path: plugin.path, + run: match plugin.run { + PluginTypeFromYaml::Pane => PluginType::Pane(None), + PluginTypeFromYaml::Headless => PluginType::Headless, + }, + _allow_exec_host_cmd: plugin._allow_exec_host_cmd, + location: RunPluginLocation::Zellij(plugin.tag), + } + } +} + +/// Plugin metadata +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PluginConfig { + /// Path of the plugin, see resolve_wasm_bytes for resolution semantics + pub path: PathBuf, + /// Plugin type + pub run: PluginType, + /// Allow command execution from plugin + pub _allow_exec_host_cmd: bool, + /// Original location of the + pub location: RunPluginLocation, +} + +impl PluginConfig { + /// Resolve wasm plugin bytes for the plugin path and given plugin directory. Attempts to first + /// resolve the plugin path as an absolute path, then adds a ".wasm" extension to the path and + /// resolves that, finally we use the plugin directoy joined with the path with an appended + /// ".wasm" extension. So if our path is "tab-bar" and the given plugin dir is + /// "/home/bob/.zellij/plugins" the lookup chain will be this: + /// + /// ```bash + /// /tab-bar + /// /tab-bar.wasm + /// /home/bob/.zellij/plugins/tab-bar.wasm + /// ``` + /// + pub fn resolve_wasm_bytes(&self, plugin_dir: &Path) -> Option<Vec<u8>> { + fs::read(&self.path) + .or_else(|_| fs::read(&self.path.with_extension("wasm"))) + .or_else(|_| fs::read(plugin_dir.join(&self.path).with_extension("wasm"))) + .ok() + } + + /// Sets the tab index inside of the plugin type of the run field. + pub fn set_tab_index(&mut self, tab_index: usize) { + match self.run { + PluginType::Pane(..) => { + self.run = PluginType::Pane(Some(tab_index)); + } + PluginType::Headless => {} + } + } +} + +/// Type of the plugin. Defaults to Pane. +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PluginType { + // TODO: A plugin with output thats cloned across every pane in a tab, or across the entire + // application might be useful + // Tab + // Static + /// Starts immediately when Zellij is started and runs without a visible pane + Headless, + /// Runs once per pane declared inside a layout file + Pane(Option<usize>), // tab_index +} + +impl Default for PluginType { + fn default() -> Self { + Self::Pane(None) + } +} + +#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)] +pub struct PluginConfigFromYaml { + pub path: PathBuf, |