diff options
author | Aram Drevekenin <aram@poor.dev> | 2023-07-25 10:04:12 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-07-25 10:04:12 +0200 |
commit | c95d0e769f31b21f5e2d4aaf6465468344f1bfd6 (patch) | |
tree | 9589f0875b91b73460b807e90817907bf3d7d8c6 /zellij-utils | |
parent | 6cf795a7df6c83b65a4535b6af0338b4a0b1742f (diff) |
feat(plugins): make plugins configurable (#2646)
* work
* make every plugin entry point configurable
* make integration tests pass
* make e2e tests pass
* add test for plugin configuration
* add test snapshot
* add plugin config parsing test
* cleanups
* style(fmt): rustfmt
* style(comment): remove commented code
Diffstat (limited to 'zellij-utils')
16 files changed, 371 insertions, 52 deletions
diff --git a/zellij-utils/src/cli.rs b/zellij-utils/src/cli.rs index 6639f810b..fd2bdcda7 100644 --- a/zellij-utils/src/cli.rs +++ b/zellij-utils/src/cli.rs @@ -2,7 +2,7 @@ use crate::data::{Direction, InputMode, Resize}; use crate::setup::Setup; use crate::{ consts::{ZELLIJ_CONFIG_DIR_ENV, ZELLIJ_CONFIG_FILE_ENV}, - input::options::CliOptions, + input::{layout::PluginUserConfiguration, options::CliOptions}, }; use clap::{Parser, Subcommand}; use serde::{Deserialize, Serialize}; @@ -292,6 +292,8 @@ pub enum CliAction { requires("command") )] start_suspended: bool, + #[clap(short, long, value_parser)] + configuration: Option<PluginUserConfiguration>, }, /// Open the specified file in a new zellij pane with your default EDITOR Edit { @@ -376,10 +378,14 @@ pub enum CliAction { QueryTabNames, StartOrReloadPlugin { url: String, + #[clap(short, long, value_parser)] + configuration: Option<PluginUserConfiguration>, }, LaunchOrFocusPlugin { #[clap(short, long, value_parser)] floating: bool, url: Url, + #[clap(short, long, value_parser)] + configuration: Option<PluginUserConfiguration>, }, } diff --git a/zellij-utils/src/input/actions.rs b/zellij-utils/src/input/actions.rs index c2071846e..568c0a2c2 100644 --- a/zellij-utils/src/input/actions.rs +++ b/zellij-utils/src/input/actions.rs @@ -230,8 +230,8 @@ pub enum Action { /// Query all tab names QueryTabNames, /// Open a new tiled (embedded, non-floating) plugin pane - NewTiledPluginPane(RunPluginLocation, Option<String>), // String is an optional name - NewFloatingPluginPane(RunPluginLocation, Option<String>), // String is an optional name + NewTiledPluginPane(RunPlugin, Option<String>), // String is an optional name + NewFloatingPluginPane(RunPlugin, Option<String>), // String is an optional name StartOrReloadPlugin(RunPlugin), CloseTerminalPane(u32), ClosePluginPane(u32), @@ -292,21 +292,24 @@ impl Action { name, close_on_exit, start_suspended, + configuration, } => { let current_dir = get_current_dir(); let cwd = cwd .map(|cwd| current_dir.join(cwd)) .or_else(|| Some(current_dir)); + let user_configuration = configuration.unwrap_or_default(); if let Some(plugin) = plugin { + let location = RunPluginLocation::parse(&plugin, cwd) + .map_err(|e| format!("Failed to parse plugin loction {plugin}: {}", e))?; + let plugin = RunPlugin { + _allow_exec_host_cmd: false, + location, + configuration: user_configuration, + }; if floating { - let plugin = RunPluginLocation::parse(&plugin, cwd).map_err(|e| { - format!("Failed to parse plugin loction {plugin}: {}", e) - })?; Ok(vec![Action::NewFloatingPluginPane(plugin, name)]) } else { - let plugin = RunPluginLocation::parse(&plugin, cwd).map_err(|e| { - format!("Failed to parse plugin location {plugin}: {}", e) - })?; // it is intentional that a new tiled plugin pane cannot include a // direction // this is because the cli client opening a tiled plugin pane is a @@ -480,23 +483,29 @@ impl Action { CliAction::PreviousSwapLayout => Ok(vec![Action::PreviousSwapLayout]), CliAction::NextSwapLayout => Ok(vec![Action::NextSwapLayout]), CliAction::QueryTabNames => Ok(vec![Action::QueryTabNames]), - CliAction::StartOrReloadPlugin { url } => { + CliAction::StartOrReloadPlugin { url, configuration } => { let current_dir = get_current_dir(); let run_plugin_location = RunPluginLocation::parse(&url, Some(current_dir)) .map_err(|e| format!("Failed to parse plugin location: {}", e))?; let run_plugin = RunPlugin { location: run_plugin_location, _allow_exec_host_cmd: false, + configuration: configuration.unwrap_or_default(), }; Ok(vec![Action::StartOrReloadPlugin(run_plugin)]) }, - CliAction::LaunchOrFocusPlugin { url, floating } => { + CliAction::LaunchOrFocusPlugin { + url, + floating, + configuration, + } => { let current_dir = get_current_dir(); let run_plugin_location = RunPluginLocation::parse(url.as_str(), Some(current_dir)) .map_err(|e| format!("Failed to parse plugin location: {}", e))?; let run_plugin = RunPlugin { location: run_plugin_location, _allow_exec_host_cmd: false, + configuration: configuration.unwrap_or_default(), }; Ok(vec![Action::LaunchOrFocusPlugin(run_plugin, floating)]) }, diff --git a/zellij-utils/src/input/config.rs b/zellij-utils/src/input/config.rs index 4ad4ef89d..55170aec2 100644 --- a/zellij-utils/src/input/config.rs +++ b/zellij-utils/src/input/config.rs @@ -600,6 +600,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -609,6 +610,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -618,6 +620,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("strider")), _allow_exec_host_cmd: true, + userspace_configuration: Default::default(), }, ); expected_plugin_configuration.insert( @@ -627,6 +630,7 @@ mod config_test { run: PluginType::Pane(None), location: RunPluginLocation::Zellij(PluginTag::new("compact-bar")), _allow_exec_host_cmd: false, + userspace_configuration: Default::default(), }, ); assert_eq!( diff --git a/zellij-utils/src/input/layout.rs b/zellij-utils/src/input/layout.rs index c1d95a125..55e5fc69f 100644 --- a/zellij-utils/src/input/layout.rs +++ b/zellij-utils/src/input/layout.rs @@ -212,6 +212,35 @@ pub struct RunPlugin { #[serde(default)] pub _allow_exec_host_cmd: bool, pub location: RunPluginLocation, + pub configuration: PluginUserConfiguration, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] +pub struct PluginUserConfiguration(BTreeMap<String, String>); + +impl PluginUserConfiguration { + pub fn new(configuration: BTreeMap<String, String>) -> Self { + PluginUserConfiguration(configuration) + } + pub fn inner(&self) -> &BTreeMap<String, String> { + &self.0 + } +} + +impl FromStr for PluginUserConfiguration { + type Err = &'static str; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + let mut ret = BTreeMap::new(); + let configs = s.split(','); + for config in configs { + let mut config = config.split('='); + let key = config.next().ok_or("invalid configuration key")?.to_owned(); + let value = config.map(|c| c.to_owned()).collect::<Vec<_>>().join("="); + ret.insert(key, value); + } + Ok(PluginUserConfiguration(ret)) + } } #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, Hash)] diff --git a/zellij-utils/src/input/plugins.rs b/zellij-utils/src/input/plugins.rs index 47cc3aa74..f12aba2a2 100644 --- a/zellij-utils/src/input/plugins.rs +++ b/zellij-utils/src/input/plugins.rs @@ -8,7 +8,7 @@ use thiserror::Error; use serde::{Deserialize, Serialize}; use url::Url; -use super::layout::{RunPlugin, RunPluginLocation}; +use super::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation}; #[cfg(not(target_family = "wasm"))] use crate::consts::ASSET_MAP; pub use crate::data::PluginTag; @@ -48,9 +48,11 @@ impl PluginsConfig { run: PluginType::Pane(None), _allow_exec_host_cmd: run._allow_exec_host_cmd, location: run.location.clone(), + userspace_configuration: run.configuration.clone(), }), RunPluginLocation::Zellij(tag) => self.0.get(tag).cloned().map(|plugin| PluginConfig { _allow_exec_host_cmd: run._allow_exec_host_cmd, + userspace_configuration: run.configuration.clone(), ..plugin }), } @@ -86,6 +88,8 @@ pub struct PluginConfig { pub _allow_exec_host_cmd: bool, /// Original location of the pub location: RunPluginLocation, + /// Custom configuration for this plugin + pub userspace_configuration: PluginUserConfiguration, } impl PluginConfig { diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index fee88aae4..038161a49 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -504,9 +504,18 @@ fn layout_with_plugin_panes() { pane { plugin location="file:/path/to/my/plugin.wasm" } + pane { + plugin location="zellij:status-bar" { + config_key_1 "config_value_1" + "2" true + } + } } "#; let layout = Layout::from_kdl(kdl_layout, "layout_file_name".into(), None, None).unwrap(); + let mut expected_plugin_configuration = BTreeMap::new(); + expected_plugin_configuration.insert("config_key_1".to_owned(), "config_value_1".to_owned()); + expected_plugin_configuration.insert("2".to_owned(), "true".to_owned()); let expected_layout = Layout { template: Some(( TiledPaneLayout { @@ -515,6 +524,7 @@ fn layout_with_plugin_panes() { run: Some(Run::Plugin(RunPlugin { location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), _allow_exec_host_cmd: false, + configuration: Default::default(), })), ..Default::default() }, @@ -524,6 +534,15 @@ fn layout_with_plugin_panes() { "/path/to/my/plugin.wasm", )), _allow_exec_host_cmd: false, + configuration: Default::default(), + })), + ..Default::default() + }, + TiledPaneLayout { + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + configuration: PluginUserConfiguration(expected_plugin_configuration), })), ..Default::default() }, @@ -2016,6 +2035,7 @@ fn run_plugin_location_parsing() { run: Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + configuration: Default::default(), })), ..Default::default() }, @@ -2025,6 +2045,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "/path/to/my/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2032,6 +2053,7 @@ fn run_plugin_location_parsing() { run: Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd: false, location: RunPluginLocation::File(PathBuf::from("plugin.wasm")), + configuration: Default::default(), })), ..Default::default() }, @@ -2041,6 +2063,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "relative/with space/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2050,6 +2073,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "/absolute/with space/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, @@ -2059,6 +2083,7 @@ fn run_plugin_location_parsing() { location: RunPluginLocation::File(PathBuf::from( "c:/absolute/windows/plugin.wasm", )), + configuration: Default::default(), })), ..Default::default() }, diff --git a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap index 98d4fef5a..e9f849b7f 100644 --- a/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap +++ b/zellij-utils/src/input/unit/snapshots/zellij_utils__input__layout__layout_test__can_load_swap_layouts_from_a_different_file.snap @@ -1,6 +1,6 @@ --- source: zellij-utils/src/input/./unit/layout_test.rs -assertion_line: 1850 +assertion_line: 1863 expression: "format!(\"{:#?}\", layout)" --- Layout { @@ -66,6 +66,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -150,6 +153,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -194,6 +200,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -331,6 +340,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -375,6 +387,9 @@ Layout { "tab-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), @@ -578,6 +593,9 @@ Layout { "status-bar", ), ), + configuration: PluginUserConfiguration( + {}, + ), }, ), ), diff --git a/zellij-utils/src/kdl/kdl_layout_parser.rs b/zellij-utils/src/kdl/kdl_layout_parser.rs index a5cab4b1b..9cee47988 100644 --- a/zellij-utils/src/kdl/kdl_layout_parser.rs +++ b/zellij-utils/src/kdl/kdl_layout_parser.rs @@ -2,9 +2,9 @@ use crate::input::{ command::RunCommand, config::ConfigError, layout::{ - FloatingPaneLayout, Layout, LayoutConstraint, PercentOrFixed, Run, RunPlugin, - RunPluginLocation, SplitDirection, SplitSize, SwapFloatingLayout, SwapTiledLayout, - TiledPaneLayout, + FloatingPaneLayout, Layout, LayoutConstraint, PercentOrFixed, PluginUserConfiguration, Run, + RunPlugin, RunPluginLocation, SplitDirection, SplitSize, SwapFloatingLayout, + SwapTiledLayout, TiledPaneLayout, }, }; @@ -14,7 +14,8 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::str::FromStr; use crate::{ - kdl_child_with_name, kdl_children_nodes, kdl_get_bool_property_or_child_value, + kdl_child_with_name, kdl_children_nodes, kdl_first_entry_as_bool, kdl_first_entry_as_i64, + kdl_first_entry_as_string, kdl_get_bool_property_or_child_value, kdl_get_bool_property_or_child_value_with_error, kdl_get_child, kdl_get_int_property_or_child_value, kdl_get_property_or_child, kdl_get_string_property_or_child_value, kdl_get_string_property_or_child_value_with_error, @@ -121,6 +122,11 @@ impl<'a> KdlLayoutParser<'a> { || property_name == "min_panes" || property_name == "exact_panes" } + pub fn is_a_reserved_plugin_property(property_name: &str) -> bool { + property_name == "location" + || property_name == "_allow_exec_host_cmd" + || property_name == "path" + } fn assert_legal_node_name(&self, name: &str, kdl_node: &KdlNode) -> Result<(), ConfigError> { if name.contains(char::is_whitespace) { Err(ConfigError::new_layout_kdl_error( @@ -305,11 +311,62 @@ impl<'a> KdlLayoutParser<'a> { url_node.span().len(), ) })?; + let configuration = KdlLayoutParser::parse_plugin_user_configuration(&plugin_block)?; Ok(Some(Run::Plugin(RunPlugin { _allow_exec_host_cmd, location, + configuration, }))) } + pub fn parse_plugin_user_configuration( + plugin_block: &KdlNode, + ) -> Result<PluginUserConfiguration, ConfigError> { + let mut configuration = BTreeMap::new(); + for user_configuration_entry in plugin_block.entries() { + let name = user_configuration_entry.name(); + let value = user_configuration_entry.value(); + if let Some(name) = name { + let name = name.to_string(); + if KdlLayoutParser::is_a_reserved_plugin_property(&name) { + continue; + } + configuration.insert(name, value.to_string()); + } + // we ignore "bare" (eg. `plugin i_am_a_bare_true_argument { arg_one 1; }`) entries + // to prevent diverging behaviour with the keybindings config + } + if let Some(user_config) = kdl_children_nodes!(plugin_block) { + for user_configuration_entry in user_config { + let config_entry_name = kdl_name!(user_configuration_entry); + if KdlLayoutParser::is_a_reserved_plugin_property(&config_entry_name) { + continue; + } + let config_entry_str_value = kdl_first_entry_as_string!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_int_value = kdl_first_entry_as_i64!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_bool_value = kdl_first_entry_as_bool!(user_configuration_entry) + .map(|s| format!("{}", s.to_string())); + let config_entry_children = user_configuration_entry + .children() + .map(|s| format!("{}", s.to_string().trim())); + let config_entry_value = config_entry_str_value + .or(config_entry_int_value) + .or(config_entry_bool_value) + .or(config_entry_children) + .ok_or(ConfigError::new_kdl_error( + format!( + "Failed to parse plugin block configuration: {:?}", + user_configuration_entry + ), + plugin_block.span().offset(), + plugin_block.span().len(), + ))?; + configuration.insert(config_entry_name.into(), config_entry_value); + } + } + Ok(PluginUserConfiguration::new(configuration)) + } fn parse_args(&self, pane_node: &KdlNode) -> Result<Option<Vec<String>>, ConfigError> { match kdl_get_child!(pane_node, "args") { Some(kdl_args) => { diff --git a/zellij-utils/src/kdl/mod.rs b/zellij-utils/src/kdl/mod.rs index a75292900..341415c1c 100644 --- a/zellij-utils/src/kdl/mod.rs +++ b/zellij-utils/src/kdl/mod.rs @@ -3,13 +3,13 @@ use crate::data::{Direction, InputMode, Key, Palette, PaletteColor, Resize}; use crate::envs::EnvironmentVariables; use crate::input::config::{Config, ConfigError, KdlError}; use crate::input::keybinds::Keybinds; -use crate::input::layout::{Layout, RunPlugin, RunPluginLocation}; +use crate::input::layout::{Layout, PluginUserConfiguration, RunPlugin, RunPluginLocation}; use crate::input::options::{Clipboard, OnForceClose, Options}; use crate::input::plu |