From c9372212f68fed52d45590b2dac271ab6270d943 Mon Sep 17 00:00:00 2001 From: spacemaison Date: Wed, 22 Sep 2021 10:13:21 -0700 Subject: [PATCH] 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 --- Cargo.lock | 79 +++++ zellij-client/src/lib.rs | 1 + zellij-server/Cargo.toml | 1 + zellij-server/src/lib.rs | 49 ++- zellij-server/src/pty.rs | 7 +- zellij-server/src/tab.rs | 13 +- zellij-server/src/unit/screen_tests.rs | 3 +- zellij-server/src/unit/tab_tests.rs | 7 +- zellij-server/src/wasm_vm.rs | 203 +++++++---- zellij-tile/src/data.rs | 23 ++ zellij-tile/src/lib.rs | 4 +- zellij-tile/src/shim.rs | 6 +- zellij-utils/Cargo.toml | 2 + zellij-utils/assets/config/default.yaml | 7 + zellij-utils/assets/layouts/default.yaml | 4 +- .../assets/layouts/disable-status-bar.yaml | 2 +- zellij-utils/assets/layouts/strider.yaml | 6 +- zellij-utils/src/input/config.rs | 29 +- zellij-utils/src/input/layout.rs | 152 +++++++-- zellij-utils/src/input/mod.rs | 1 + zellij-utils/src/input/plugins.rs | 315 ++++++++++++++++++ ...ee-panes-with-tab-and-default-plugins.yaml | 5 +- zellij-utils/src/input/unit/layout_test.rs | 89 ++--- zellij-utils/src/ipc.rs | 10 +- zellij-utils/src/setup.rs | 1 - 25 files changed, 833 insertions(+), 186 deletions(-) create mode 100644 zellij-utils/src/input/plugins.rs diff --git a/Cargo.lock b/Cargo.lock index 66a0b3cb..bf4ae0fd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -748,6 +748,16 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "form_urlencoded" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" +dependencies = [ + "matches", + "percent-encoding", +] + [[package]] name = "fuchsia-cprng" version = "0.1.1" @@ -970,6 +980,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "idna" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +dependencies = [ + "matches", + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "indexmap" version = "1.7.0" @@ -1206,6 +1227,12 @@ dependencies = [ "libc", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "memchr" version = "2.3.4" @@ -1432,6 +1459,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "percent-encoding" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" + [[package]] name = "pin-project-lite" version = "0.2.7" @@ -2107,6 +2140,21 @@ dependencies = [ "winapi", ] +[[package]] +name = "tinyvec" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" + [[package]] name = "tracing" version = "0.1.26" @@ -2179,6 +2227,21 @@ dependencies = [ "syn", ] +[[package]] +name = "unicode-bidi" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" + +[[package]] +name = "unicode-normalization" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +dependencies = [ + "tinyvec", +] + [[package]] name = "unicode-segmentation" version = "1.8.0" @@ -2206,6 +2269,19 @@ dependencies = [ "traitobject", ] +[[package]] +name = "url" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", + "serde", +] + [[package]] name = "utf8parse" version = "0.1.1" @@ -2680,6 +2756,7 @@ dependencies = [ "serde_json", "typetag", "unicode-width", + "url", "wasmer", "wasmer-wasi", "zellij-utils", @@ -2720,6 +2797,7 @@ dependencies = [ "nix", "once_cell", "serde", + "serde_json", "serde_yaml", "signal-hook 0.3.9", "strip-ansi-escapes", @@ -2728,6 +2806,7 @@ dependencies = [ "tempfile", "termion", "unicode-width", + "url", "vte 0.10.1", "zellij-tile", ] diff --git a/zellij-client/src/lib.rs b/zellij-client/src/lib.rs index bd2c6376..b4d63d27 100644 --- a/zellij-client/src/lib.rs +++ b/zellij-client/src/lib.rs @@ -138,6 +138,7 @@ pub fn start_client( Box::new(opts), Box::new(config_options.clone()), layout.unwrap(), + Some(config.plugins.clone()), ) } }; diff --git a/zellij-server/Cargo.toml b/zellij-server/Cargo.toml index bc0abe31..c9f98ecf 100644 --- a/zellij-server/Cargo.toml +++ b/zellij-server/Cargo.toml @@ -16,6 +16,7 @@ byteorder = "1.4.3" daemonize = "0.4.1" serde_json = "1.0" unicode-width = "0.1.8" +url = "2.2.2" wasmer = "1.0.0" wasmer-wasi = "1.0.0" cassowary = "0.3.0" diff --git a/zellij-server/src/lib.rs b/zellij-server/src/lib.rs index 419aae4f..36569522 100644 --- a/zellij-server/src/lib.rs +++ b/zellij-server/src/lib.rs @@ -38,6 +38,7 @@ use zellij_utils::{ get_mode_info, layout::LayoutFromYaml, options::Options, + plugins::PluginsConfig, }, ipc::{ClientAttributes, ClientToServerMsg, ExitReason, ServerToClientMsg}, setup::get_default_data_dir, @@ -46,7 +47,13 @@ use zellij_utils::{ /// Instructions related to server-side application #[derive(Debug, Clone)] pub(crate) enum ServerInstruction { - NewClient(ClientAttributes, Box, Box, LayoutFromYaml), + NewClient( + ClientAttributes, + Box, + Box, + LayoutFromYaml, + Option, + ), Render(Option), UnblockInputThread, ClientExit, @@ -58,8 +65,8 @@ pub(crate) enum ServerInstruction { impl From for ServerInstruction { fn from(instruction: ClientToServerMsg) -> Self { match instruction { - ClientToServerMsg::NewClient(attrs, opts, options, layout) => { - ServerInstruction::NewClient(attrs, opts, options, layout) + ClientToServerMsg::NewClient(attrs, opts, options, layout, plugins) => { + ServerInstruction::NewClient(attrs, opts, options, layout, plugins) } ClientToServerMsg::AttachClient(attrs, force, options) => { ServerInstruction::AttachClient(attrs, force, options) @@ -193,15 +200,24 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { let (instruction, mut err_ctx) = server_receiver.recv().unwrap(); err_ctx.add_call(ContextType::IPCServer((&instruction).into())); match instruction { - ServerInstruction::NewClient(client_attributes, opts, config_options, layout) => { + ServerInstruction::NewClient( + client_attributes, + opts, + config_options, + layout, + plugins, + ) => { let session = init_session( os_input.clone(), - opts, - config_options.clone(), to_server.clone(), client_attributes, session_state.clone(), - layout.clone(), + SessionOptions { + opts, + layout: layout.clone(), + plugins, + config_options: config_options.clone(), + }, ); *session_data.write().unwrap() = Some(session); *session_state.write().unwrap() = SessionState::Attached; @@ -302,15 +318,26 @@ pub fn start_server(os_input: Box, socket_path: PathBuf) { drop(std::fs::remove_file(&socket_path)); } +pub struct SessionOptions { + pub opts: Box, + pub config_options: Box, + pub layout: LayoutFromYaml, + pub plugins: Option, +} + fn init_session( os_input: Box, - opts: Box, - config_options: Box, to_server: SenderWithContext, client_attributes: ClientAttributes, session_state: Arc>, - layout: LayoutFromYaml, + options: SessionOptions, ) -> SessionMetaData { + let SessionOptions { + opts, + config_options, + layout, + plugins, + } = options; let (to_screen, screen_receiver): ChannelWithContext = channels::unbounded(); let to_screen = SenderWithContext::new(to_screen); @@ -394,7 +421,7 @@ fn init_session( ); let store = Store::default(); - move || wasm_thread_main(plugin_bus, store, data_dir) + move || wasm_thread_main(plugin_bus, store, data_dir, plugins.unwrap_or_default()) }) .unwrap(); SessionMetaData { diff --git a/zellij-server/src/pty.rs b/zellij-server/src/pty.rs index e37e6062..761b1702 100644 --- a/zellij-server/src/pty.rs +++ b/zellij-server/src/pty.rs @@ -65,6 +65,8 @@ pub(crate) struct Pty { task_handles: HashMap>, } +use std::convert::TryFrom; + pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) { loop { let (event, mut err_ctx) = pty.bus.recv().expect("failed to receive event on channel"); @@ -104,7 +106,10 @@ pub(crate) fn pty_thread_main(mut pty: Pty, layout: LayoutFromYaml) { }); let merged_layout = layout.template.clone().insert_tab_layout(tab_layout); - pty.spawn_terminals_for_layout(merged_layout.into(), terminal_action.clone()); + let layout: Layout = + Layout::try_from(merged_layout).unwrap_or_else(|err| panic!("{}", err)); + + pty.spawn_terminals_for_layout(layout, terminal_action.clone()); if let Some(tab_name) = tab_name { // clear current name at first diff --git a/zellij-server/src/tab.rs b/zellij-server/src/tab.rs index 62e0a2cb..fa62cfce 100644 --- a/zellij-server/src/tab.rs +++ b/zellij-server/src/tab.rs @@ -322,23 +322,18 @@ impl Tab { for (layout, position_and_size) in positions_and_size { // A plugin pane - if let Some(Run::Plugin(Some(plugin))) = &layout.run { + if let Some(Run::Plugin(run)) = layout.run.clone() { let (pid_tx, pid_rx) = channel(); + let pane_title = run.location.to_string(); self.senders - .send_to_plugin(PluginInstruction::Load( - pid_tx, - plugin.path.clone(), - tab_index, - plugin._allow_exec_host_cmd, - )) + .send_to_plugin(PluginInstruction::Load(pid_tx, run, tab_index)) .unwrap(); let pid = pid_rx.recv().unwrap(); - let title = String::from(plugin.path.as_path().as_os_str().to_string_lossy()); let mut new_plugin = PluginPane::new( pid, *position_and_size, self.senders.to_plugin.as_ref().unwrap().clone(), - title, + pane_title, ); new_plugin.set_borderless(layout.borderless); self.panes.insert(PaneId::Plugin(pid), Box::new(new_plugin)); diff --git a/zellij-server/src/unit/screen_tests.rs b/zellij-server/src/unit/screen_tests.rs index aeea671b..e1df2c64 100644 --- a/zellij-server/src/unit/screen_tests.rs +++ b/zellij-server/src/unit/screen_tests.rs @@ -5,6 +5,7 @@ use crate::{ thread_bus::Bus, SessionState, }; +use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::command::TerminalAction; @@ -101,7 +102,7 @@ fn create_new_screen(size: Size) -> Screen { } fn new_tab(screen: &mut Screen, pid: i32) { - screen.apply_layout(LayoutTemplate::default().into(), vec![pid]); + screen.apply_layout(LayoutTemplate::default().try_into().unwrap(), vec![pid]); } #[test] diff --git a/zellij-server/src/unit/tab_tests.rs b/zellij-server/src/unit/tab_tests.rs index de5c1067..7f0fa71f 100644 --- a/zellij-server/src/unit/tab_tests.rs +++ b/zellij-server/src/unit/tab_tests.rs @@ -6,6 +6,7 @@ use crate::{ thread_bus::ThreadSenders, SessionState, }; +use std::convert::TryInto; use std::path::PathBuf; use std::sync::{Arc, RwLock}; use zellij_utils::input::layout::LayoutTemplate; @@ -101,7 +102,11 @@ fn create_new_tab(size: Size) -> Tab { session_state, true, // draw pane frames ); - tab.apply_layout(LayoutTemplate::default().into(), vec![1], index); + tab.apply_layout( + LayoutTemplate::default().try_into().unwrap(), + vec![1], + index, + ); tab } diff --git a/zellij-server/src/wasm_vm.rs b/zellij-server/src/wasm_vm.rs index 40967c5a..1d67e847 100644 --- a/zellij-server/src/wasm_vm.rs +++ b/zellij-server/src/wasm_vm.rs @@ -1,7 +1,7 @@ -use log::{info, warn}; +use log::{debug, info, warn}; use std::collections::{HashMap, HashSet}; use std::fs; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use std::process; use std::str::FromStr; use std::sync::{mpsc::Sender, Arc, Mutex}; @@ -9,6 +9,7 @@ use std::thread; use std::time::{Duration, Instant}; use serde::{de::DeserializeOwned, Serialize}; +use url::Url; use wasmer::{ imports, ChainableNamedResolver, Function, ImportObject, Instance, Module, Store, Value, WasmerEnv, @@ -24,12 +25,17 @@ use crate::{ thread_bus::{Bus, ThreadSenders}, }; use zellij_utils::errors::{ContextType, PluginContext}; -use zellij_utils::{input::command::TerminalAction, serde, zellij_tile}; +use zellij_utils::{ + input::command::TerminalAction, + input::layout::RunPlugin, + input::plugins::{PluginConfig, PluginType, PluginsConfig}, + serde, zellij_tile, +}; #[derive(Clone, Debug)] pub(crate) enum PluginInstruction { - Load(Sender, PathBuf, usize, bool), // tx_pid, path_of_plugin , tab_index, allow_exec_host_cmd - Update(Option, Event), // Focused plugin / broadcast, event data + Load(Sender, RunPlugin, usize), // tx_pid, plugin metadata, tab_index + Update(Option, Event), // Focused plugin / broadcast, event data Render(Sender, u32, usize, usize), // String buffer, plugin id, rows, cols Unload(u32), Exit, @@ -50,83 +56,62 @@ impl From<&PluginInstruction> for PluginContext { #[derive(WasmerEnv, Clone)] pub(crate) struct PluginEnv { pub plugin_id: u32, - pub tab_index: usize, + pub plugin: PluginConfig, pub senders: ThreadSenders, pub wasi_env: WasiEnv, pub subscriptions: Arc>>, - // FIXME: Once permission system is ready, this could be removed - pub _allow_exec_host_cmd: bool, plugin_own_data_dir: PathBuf, } // Thread main -------------------------------------------------------------------------------------------------------- -pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_dir: PathBuf) { +pub(crate) fn wasm_thread_main( + bus: Bus, + store: Store, + data_dir: PathBuf, + plugins: PluginsConfig, +) { info!("Wasm main thread starts"); + let mut plugin_id = 0; let mut plugin_map = HashMap::new(); let plugin_dir = data_dir.join("plugins/"); let plugin_global_data_dir = plugin_dir.join("data"); fs::create_dir_all(plugin_global_data_dir.as_path()).unwrap(); + for plugin in plugins.iter() { + if let PluginType::Headless = plugin.run { + let (instance, plugin_env) = start_plugin( + plugin_id, + plugin, + 0, + &bus, + &store, + &data_dir, + &plugin_global_data_dir, + ); + plugin_map.insert(plugin_id, (instance, plugin_env)); + plugin_id += 1; + } + } + loop { let (event, mut err_ctx) = bus.recv().expect("failed to receive event on channel"); err_ctx.add_call(ContextType::Plugin((&event).into())); match event { - PluginInstruction::Load(pid_tx, path, tab_index, _allow_exec_host_cmd) => { - let wasm_bytes = fs::read(&path) - .or_else(|_| fs::read(&path.with_extension("wasm"))) - .or_else(|_| fs::read(&plugin_dir.join(&path).with_extension("wasm"))) - .unwrap_or_else(|_| panic!("cannot find plugin {}", &path.display())); + PluginInstruction::Load(pid_tx, run, tab_index) => { + let plugin = plugins + .get(&run) + .unwrap_or_else(|| panic!("Plugin {:?} could not be resolved", run)); - // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that - let module = Module::new(&store, &wasm_bytes).unwrap(); - - let output = Pipe::new(); - let input = Pipe::new(); - let stderr = LoggingPipe::new( - path.as_path().file_name().unwrap().to_str().unwrap(), - plugin_id, - ); - - let plugin_name = path.as_path().file_stem().unwrap(); - let plugin_own_data_dir = plugin_global_data_dir.join(plugin_name); - fs::create_dir_all(&plugin_own_data_dir).unwrap(); - - let mut wasi_env = WasiState::new("Zellij") - .env("CLICOLOR_FORCE", "1") - .map_dir("/host", ".") - .unwrap() - .map_dir("/data", plugin_own_data_dir.as_path()) - .unwrap() - .stdin(Box::new(input)) - .stdout(Box::new(output)) - .stderr(Box::new(stderr)) - .finalize() - .unwrap(); - - let wasi = wasi_env.import_object(&module).unwrap(); - - if _allow_exec_host_cmd { - info!("Plugin({:?}) is able to run any host command, this may lead to some security issues!", path); - } - - let plugin_env = PluginEnv { + let (instance, plugin_env) = start_plugin( plugin_id, + &plugin, tab_index, - senders: bus.senders.clone(), - wasi_env, - subscriptions: Arc::new(Mutex::new(HashSet::new())), - _allow_exec_host_cmd, - plugin_own_data_dir, - }; - - let zellij = zellij_exports(&store, &plugin_env); - let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); - - let start = instance.exports.get_function("_start").unwrap(); - - // This eventually calls the `.load()` method - start.call(&[]).unwrap(); + &bus, + &store, + &data_dir, + &plugin_global_data_dir, + ); plugin_map.insert(plugin_id, (instance, plugin_env)); pid_tx.send(plugin_id).unwrap(); @@ -150,7 +135,6 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d buf_tx.send(String::new()).unwrap(); } else { let (instance, plugin_env) = plugin_map.get(&pid).unwrap(); - let render = instance.exports.get_function("render").unwrap(); render @@ -172,6 +156,71 @@ pub(crate) fn wasm_thread_main(bus: Bus, store: Store, data_d fs::remove_dir_all(plugin_global_data_dir.as_path()).unwrap(); } +fn start_plugin( + plugin_id: u32, + plugin: &PluginConfig, + tab_index: usize, + bus: &Bus, + store: &Store, + data_dir: &Path, + plugin_global_data_dir: &Path, +) -> (Instance, PluginEnv) { + if plugin._allow_exec_host_cmd { + info!( + "Plugin({:?}) is able to run any host command, this may lead to some security issues!", + plugin.path + ); + } + + let wasm_bytes = plugin + .resolve_wasm_bytes(&data_dir.join("plugins/")) + .unwrap_or_else(|| panic!("Cannot resolve wasm bytes for plugin {:?}", plugin)); + + // FIXME: Cache this compiled module on disk. I could use `(de)serialize_to_file()` for that + let module = Module::new(store, &wasm_bytes).unwrap(); + + let output = Pipe::new(); + let input = Pipe::new(); + let stderr = LoggingPipe::new(&plugin.location.to_string(), plugin_id); + let plugin_own_data_dir = plugin_global_data_dir.join(Url::from(&plugin.location).to_string()); + fs::create_dir_all(&plugin_own_data_dir).unwrap(); + + let mut wasi_env = WasiState::new("Zellij") + .env("CLICOLOR_FORCE", "1") + .map_dir("/host", ".") + .unwrap() + .map_dir("/data", plugin_own_data_dir.as_path()) + .unwrap() + .stdin(Box::new(input)) + .stdout(Box::new(output)) + .stderr(Box::new(stderr)) + .finalize() + .unwrap(); + + let wasi = wasi_env.import_object(&module).unwrap(); + let mut plugin = plugin.clone(); + plugin.set_tab_index(tab_index); + + let plugin_env = PluginEnv { + plugin_id, + plugin, + senders: bus.senders.clone(), + wasi_env, + subscriptions: Arc::new(Mutex::new(HashSet::new())), + plugin_own_data_dir, + }; + + let zellij = zellij_exports(store, &plugin_env); + let instance = Instance::new(&module, &zellij.chain_back(wasi)).unwrap(); + + let start = instance.exports.get_function("_start").unwrap(); + + // This eventually calls the `.load()` method + start.call(&[]).unwrap(); + + (instance, plugin_env) +} + // Plugin API --------------------------------------------------------------------------------------------------------- pub(crate) fn zellij_exports(store: &Store, plugin_env: &PluginEnv) -> ImportObject { @@ -210,15 +259,25 @@ fn host_unsubscribe(plugin_env: &PluginEnv) { } fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) { - let selectable = selectable != 0; - plugin_env - .senders - .send_to_screen(ScreenInstruction::SetSelectable( - PaneId::Plugin(plugin_env.plugin_id), - selectable, - plugin_env.tab_index, - )) - .unwrap() + match plugin_env.plugin.run { + PluginType::Pane(Some(tab_index)) => { + let selectable = selectable != 0; + plugin_env + .senders + .send_to_screen(ScreenInstruction::SetSelectable( + PaneId::Plugin(plugin_env.plugin_id), + selectable, + tab_index, + )) + .unwrap() + } + _ => { + debug!( + "{} - Calling method 'host_set_selectable' does nothing for headless plugins", + plugin_env.plugin.location + ) + } + } } fn host_get_plugin_ids(plugin_env: &PluginEnv) { @@ -273,7 +332,7 @@ fn host_exec_cmd(plugin_env: &PluginEnv) { let command = cmdline.remove(0); // Bail out if we're forbidden to run command - if !plugin_env._allow_exec_host_cmd { + if !plugin_env.plugin._allow_exec_host_cmd { warn!("This plugin isn't allow to run command in host side, skip running this command: '{cmd} {args}'.", cmd = command, args = cmdline.join(" ")); return; diff --git a/zellij-tile/src/data.rs b/zellij-tile/src/data.rs index 44708627..6ca70c28 100644 --- a/zellij-tile/src/data.rs +++ b/zellij-tile/src/data.rs @@ -1,4 +1,5 @@ use serde::{Deserialize, Serialize}; +use std::fmt; use std::str::FromStr; use strum_macros::{EnumDiscriminants, EnumIter, EnumString, ToString}; @@ -169,6 +170,28 @@ pub struct PluginIds { pub zellij_pid: u32, } +/// Tag used to identify the plugin in layout and config yaml files +#[derive(Debug, Default, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] +pub struct PluginTag(String); + +impl PluginTag { + pub fn new(url: impl Into) -> Self { + PluginTag(url.into()) + } +} + +impl From for String { + fn from(tag: PluginTag) -> Self { + tag.0 + } +} + +impl fmt::Display for PluginTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{}", self.0) + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)] pub struct PluginCapabilities { pub arrow_fonts: bool, diff --git a/zellij-tile/src/lib.rs b/zellij-tile/src/lib.rs index af7d8ca1..96483c3c 100644 --- a/zellij-tile/src/lib.rs +++ b/zellij-tile/src/lib.rs @@ -27,7 +27,9 @@ macro_rules! register_plugin { #[no_mangle] pub fn update() { STATE.with(|state| { - state.borrow_mut().update($crate::shim::object_from_stdin()); + state + .borrow_mut() + .update($crate::shim::object_from_stdin().unwrap()); }); } diff --git a/zellij-tile/src/shim.rs b/zellij-tile/src/shim.rs index bdd6f4fb..6904d3f7 100644 --- a/zellij-tile/src/shim.rs +++ b/zellij-tile/src/shim.rs @@ -24,7 +24,7 @@ pub fn set_selectable(selectable: bool) { // Query Functions pub fn get_plugin_ids() -> PluginIds { unsafe { host_get_plugin_ids() }; - object_from_stdin() + object_from_stdin().unwrap() } // Host Functions @@ -45,10 +45,10 @@ pub fn exec_cmd(cmd: &[&str]) { // Internal Functions #[doc(hidden)] -pub fn object_from_stdin() -> T { +pub fn object_from_stdin() -> Result { let mut json = String::new(); io::stdin().read_line(&mut json).unwrap(); - serde_json::from_str(&json).unwrap() + serde_json::from_str(&json) } #[doc(hidden)] diff --git a/zellij-utils/Cargo.toml b/zellij-utils/Cargo.toml index 66e8c1cf..8751edbd 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 60b4069e..c00fdfbe 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 549dea24..0688e54b 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 e97bb8f1..10779398 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 ccb2a574..26e1eba4 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 941efa1d..e55798e7 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; @@ -25,6 +26,8 @@ pub struct ConfigFromYaml { pub options: Option, pub keybinds: Option, pub themes: Option, + #[serde(default)] + pub plugins: PluginsConfigFromYaml, } /// Main configuration. @@ -33,6 +36,7 @@ pub struct Config { pub keybinds: Keybinds, pub options: Options, pub themes: Option, + 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 for ConfigError { } } +impl From 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 9dc8cd47..03361415 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), + 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 struct RunPlugin { - pub path: PathBuf, +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 { + #[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, - pub run: Option, + pub run: Option, } impl LayoutTemplate { @@ -235,9 +290,9 @@ pub struct TabLayout { #[serde(default)] pub parts: Vec, pub split_size: Option, - pub run: Option, #[serde(default)] pub name: String, + pub run: Option, } 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) { self.parts.append(&mut parts); } - fn from_vec_tab_layout(tab_layout: Vec) -> Vec { + fn from_vec_tab_layout(tab_layout: Vec) -> Result, 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) -> Vec { + fn from_vec_template_layout( + layout_template: Vec, + ) -> Result, 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 for Layout { - fn from(tab: TabLayout) -> Self { - Layout { +impl TryFrom for RunPluginLocation { + type Error = PluginsConfigError; + + fn try_from(url: Url) -> Result { + 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 for Run { + type Error = PluginsConfigError; + + fn try_from(run: RunFromYaml) -> Result { + 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 for Layout { + type Error = ConfigError; + + fn try_from(tab: TabLayout) -> Result { + 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 for LayoutTemplate { } } -impl From for Layout { - fn from(template: LayoutTemplate) -> Self { - Layout { +impl TryFrom for Layout { + type Error = ConfigError; + + fn try_from(template: LayoutTemplate) -> Result { + 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 68aa9dd7..409c9afa 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 00000000..931d9f78 --- /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); + +/// Used in the config struct for plugin metadata +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +pub struct PluginsConfig(HashMap); + +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) -> Option { + 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 { + self.0.values() + } +} + +impl Default for PluginsConfig { + fn default() -> Self { + Self::get_plugins_with_default(PluginsConfig::new()) + } +} + +impl TryFrom for PluginsConfig { + type Error = PluginsConfigError; + + fn try_from(yaml: PluginsConfigFromYaml) -> Result { + 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 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> { + 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), // 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, + pub tag: PluginTag, + #[serde(default)] + pub run: PluginTypeFromYaml, + #[serde(default)] + pub config: serde_yaml::Value, + #[serde(default)] + pub _allow_exec_host_cmd: bool, +} + +#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[serde(rename_all = "kebab-case")] +pub enum PluginTypeFromYaml { + Headless, + Pane, +} + +impl Default for PluginTypeFromYaml { + fn default() -> Self { + Self::Pane + } +} + +#[derive(Debug, PartialEq)] +pub enum PluginsConfigError { + DuplicatePlugins(PluginTag), + InvalidUrl(Url), + InvalidPluginLocation(PathBuf), +} + +impl std::error::Error for PluginsConfigError {} +impl Display for PluginsConfigError { + fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + match self { + PluginsConfigError::DuplicatePlugins(tag) => write!( + formatter, + "Duplication in plugin tag names is not allowed: '{}'", + String::from(tag.clone()) + ), + PluginsConfigError::InvalidUrl(url) => write!( + formatter, + "Only 'file:' and 'zellij:' url schemes are supported for plugin lookup. '{}' does not match either.", + url + ), + PluginsConfigError::InvalidPluginLocation(path) => write!( + formatter, + "Could not find plugin at the path: '{:?}'", path + ), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::input::config::ConfigError; + use std::convert::TryInto; + + #[test] + fn run_plugin_permissions_are_inherited() -> Result<(), ConfigError> { + let yaml_plugins: PluginsConfigFromYaml = serde_yaml::from_str( + " + - path: boo.wasm + tag: boo + _allow_exec_host_cmd: false + ", + )?; + let plugins = PluginsConfig::try_from(yaml_plugins)?; + + assert_eq!( + plugins.get(RunPlugin { + _allow_exec_host_cmd: true, + location: RunPluginLocation::Zellij(PluginTag::new("boo")) + }), + Some(PluginConfig { + _allow_exec_host_cmd: true, + path: PathBuf::from("boo.wasm"), + location: RunPluginLocation::Zellij(PluginTag::new("boo")), + run: PluginType::Pane(None), + }) + ); + + Ok(()) + } + + #[test] + fn try_from_yaml_fails_when_duplicate_tag_names_are_present() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: /foo/bar/baz.wasm + tag: boo + - path: /foo/bar/boo.wasm + tag: boo + ", + )?; + + assert_eq!( + PluginsConfig::try_from(plugins), + Err(PluginsConfigError::DuplicatePlugins(PluginTag::new("boo"))) + ); + + Ok(()) + } + + #[test] + fn default_plugins() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: boo.wasm + tag: boo + ", + )?; + let plugins = PluginsConfig::get_plugins_with_default(plugins.try_into()?); + + assert_eq!(plugins.iter().collect::>().len(), 4); + Ok(()) + } + + #[test] + fn default_plugins_allow_overriding() -> Result<(), ConfigError> { + let ConfigFromYaml { plugins, .. } = serde_yaml::from_str( + " + plugins: + - path: boo.wasm + tag: tab-bar + ", + )?; + let plugins = PluginsConfig::get_plugins_with_default(plugins.try_into()?); + + assert_eq!( + plugins.get(RunPlugin { + _allow_exec_host_cmd: false, + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")) + }), + Some(PluginConfig { + _allow_exec_host_cmd: false, + path: PathBuf::from("boo.wasm"), + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + run: PluginType::Pane(None), + }) + ); + + Ok(()) + } +} diff --git a/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml index ae54a0c9..8148fd20 100644 --- a/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml +++ b/zellij-utils/src/input/unit/fixtures/layouts/three-panes-with-tab-and-default-plugins.yaml @@ -7,7 +7,7 @@ template: Fixed: 1 run: plugin: - path: tab-bar + location: "zellij:tab-bar" - direction: Horizontal body: true - direction: Vertical @@ -15,8 +15,7 @@ template: Fixed: 2 run: plugin: - path: status-bar - + location: "zellij:status-bar" tabs: - direction: Vertical parts: diff --git a/zellij-utils/src/input/unit/layout_test.rs b/zellij-utils/src/input/unit/layout_test.rs index 1b696b0a..dc896925 100644 --- a/zellij-utils/src/input/unit/layout_test.rs +++ b/zellij-utils/src/input/unit/layout_test.rs @@ -1,4 +1,5 @@ use super::super::layout::*; +use std::convert::TryInto; fn layout_test_dir(layout: String) -> PathBuf { let root = Path::new(env!("CARGO_MANIFEST_DIR")); @@ -45,10 +46,10 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Vertical, @@ -62,16 +63,16 @@ fn default_layout_merged_correctly() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -89,10 +90,10 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Horizontal, @@ -106,16 +107,16 @@ fn default_layout_new_tab_correct() { borderless: true, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -207,7 +208,7 @@ fn three_panes_with_tab_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -229,7 +230,7 @@ fn three_panes_with_tab_new_tab_is_correct() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -265,10 +266,10 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Vertical, @@ -312,16 +313,16 @@ fn three_panes_with_tab_and_default_plugins_merged_correctly() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -339,10 +340,10 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(1)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "tab-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("tab-bar")), + _allow_exec_host_cmd: false, + })), }, Layout { direction: Direction::Horizontal, @@ -356,16 +357,16 @@ fn three_panes_with_tab_and_default_plugins_new_tab_is_correct() { borderless: false, parts: vec![], split_size: Some(SplitSize::Fixed(2)), - run: Some(Run::Plugin(Some(RunPlugin { - path: "status-bar".into(), - ..Default::default() - }))), + run: Some(Run::Plugin(RunPlugin { + location: RunPluginLocation::Zellij(PluginTag::new("status-bar")), + _allow_exec_host_cmd: false, + })), }, ], split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -473,7 +474,7 @@ fn deeply_nested_tab_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -523,7 +524,7 @@ fn three_tabs_tab_one_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -573,7 +574,7 @@ fn three_tabs_tab_two_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -622,7 +623,7 @@ fn three_tabs_tab_three_merged_correctly() { split_size: None, run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -660,7 +661,7 @@ fn no_tabs_merged_correctly() { run: None, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } #[test] @@ -716,5 +717,5 @@ fn no_layout_template_merged_correctly() { borderless: false, }; - assert_eq!(merged_layout, tab_layout.into()); + assert_eq!(merged_layout, tab_layout.try_into().unwrap()); } diff --git a/zellij-utils/src/ipc.rs b/zellij-utils/src/ipc.rs index a7cda977..1b883d2a 100644 --- a/zellij-utils/src/ipc.rs +++ b/zellij-utils/src/ipc.rs @@ -3,7 +3,7 @@ use crate::{ cli::CliArgs, errors::{get_current_ctx, ErrorContext}, - input::{actions::Action, layout::LayoutFromYaml, options::Options}, + input::{actions::Action, layout::LayoutFromYaml, options::Options, plugins::PluginsConfig}, pane_size::Size, }; use interprocess::local_socket::LocalSocketStream; @@ -58,7 +58,13 @@ pub enum ClientToServerMsg { // Disconnect from the session we're connected to DisconnectFromSession,*/ TerminalResize(Size), - NewClient(ClientAttributes, Box, Box, LayoutFromYaml), + NewClient( + ClientAttributes, + Box, + Box, + LayoutFromYaml, + Option, + ), AttachClient(ClientAttributes, bool, Options), Action(Action), ClientExited, diff --git a/zellij-utils/src/setup.rs b/zellij-utils/src/setup.rs index 0d9855ab..73001461 100644 --- a/zellij-utils/src/setup.rs +++ b/zellij-utils/src/setup.rs @@ -188,7 +188,6 @@ impl Setup { return Err(e); } }; - //.map(|layout| layout.template); if let Some(Command::Setup(ref setup)) = &opts.command { setup.from_cli(opts, &config_options).map_or_else(