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>
This commit is contained in:
spacemaison 2021-09-22 10:13:21 -07:00 committed by GitHub
parent c39f021810
commit c9372212f6
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 833 additions and 186 deletions

79
Cargo.lock generated
View file

@ -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",
]

View file

@ -138,6 +138,7 @@ pub fn start_client(
Box::new(opts),
Box::new(config_options.clone()),
layout.unwrap(),
Some(config.plugins.clone()),
)
}
};

View file

@ -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"

View file

@ -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<CliArgs>, Box<Options>, LayoutFromYaml),
NewClient(
ClientAttributes,
Box<CliArgs>,
Box<Options>,
LayoutFromYaml,
Option<PluginsConfig>,
),
Render(Option<String>),
UnblockInputThread,
ClientExit,
@ -58,8 +65,8 @@ pub(crate) enum ServerInstruction {
impl From<ClientToServerMsg> 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<dyn ServerOsApi>, 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<dyn ServerOsApi>, socket_path: PathBuf) {
drop(std::fs::remove_file(&socket_path));
}
pub struct SessionOptions {
pub opts: Box<CliArgs>,
pub config_options: Box<Options>,
pub layout: LayoutFromYaml,
pub plugins: Option<PluginsConfig>,
}
fn init_session(
os_input: Box<dyn ServerOsApi>,
opts: Box<CliArgs>,
config_options: Box<Options>,
to_server: SenderWithContext<ServerInstruction>,
client_attributes: ClientAttributes,
session_state: Arc<RwLock<SessionState>>,
layout: LayoutFromYaml,
options: SessionOptions,
) -> SessionMetaData {
let SessionOptions {
opts,
config_options,
layout,
plugins,
} = options;
let (to_screen, screen_receiver): ChannelWithContext<ScreenInstruction> = 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 {

View file

@ -65,6 +65,8 @@ pub(crate) struct Pty {
task_handles: HashMap<RawFd, JoinHandle<()>>,
}
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

View file

@ -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));

View file

@ -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]

View file

@ -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
}

View file

@ -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,11 +25,16 @@ 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<u32>, PathBuf, usize, bool), // tx_pid, path_of_plugin , tab_index, allow_exec_host_cmd
Load(Sender<u32>, RunPlugin, usize), // tx_pid, plugin metadata, tab_index
Update(Option<u32>, Event), // Focused plugin / broadcast, event data
Render(Sender<String>, u32, usize, usize), // String buffer, plugin id, rows, cols
Unload(u32),
@ -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<Mutex<HashSet<EventType>>>,
// 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<PluginInstruction>, store: Store, data_dir: PathBuf) {
pub(crate) fn wasm_thread_main(
bus: Bus<PluginInstruction>,
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<PluginInstruction>, 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<PluginInstruction>, 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<PluginInstruction>,
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,16 +259,26 @@ fn host_unsubscribe(plugin_env: &PluginEnv) {
}
fn host_set_selectable(plugin_env: &PluginEnv, selectable: i32) {
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,
plugin_env.tab_index,
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) {
let ids = PluginIds {
@ -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;

View file

@ -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<String>) -> Self {
PluginTag(url.into())
}
}
impl From<PluginTag> 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,

View file

@ -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());
});
}

View file

@ -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: DeserializeOwned>() -> T {
pub fn object_from_stdin<T: DeserializeOwned>() -> Result<T, serde_json::Error> {
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)]

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -8,6 +8,6 @@ template:
Fixed: 1
run:
plugin:
path: tab-bar
location: "zellij:tab-bar"
- direction: Vertical
body: true

View file

@ -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

View file

@ -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 {

View file

@ -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 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<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))?,
})
}
}

View file

@ -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;

View file

@ -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,
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::<Vec<_>>().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(())
}
}

View file

@ -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:

View file

@ -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());
}

View file

@ -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<CliArgs>, Box<Options>, LayoutFromYaml),
NewClient(
ClientAttributes,
Box<CliArgs>,
Box<Options>,
LayoutFromYaml,
Option<PluginsConfig>,
),
AttachClient(ClientAttributes, bool, Options),
Action(Action),
ClientExited,

View file

@ -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(