zellij_utils/input/
plugins.rs

1//! Plugins configuration metadata
2use std::collections::BTreeMap;
3use std::fs;
4use std::path::{Path, PathBuf};
5use thiserror::Error;
6
7use serde::{Deserialize, Serialize};
8use url::Url;
9
10use super::layout::{PluginUserConfiguration, RunPlugin, RunPluginLocation};
11#[cfg(not(target_family = "wasm"))]
12use crate::consts::ASSET_MAP;
13pub use crate::data::PluginTag;
14use crate::errors::prelude::*;
15
16#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize, Serialize)]
17pub struct PluginAliases {
18    pub aliases: BTreeMap<String, RunPlugin>,
19}
20
21impl PluginAliases {
22    pub fn merge(&mut self, other: Self) {
23        self.aliases.extend(other.aliases);
24    }
25    pub fn from_data(aliases: BTreeMap<String, RunPlugin>) -> Self {
26        PluginAliases { aliases }
27    }
28    pub fn list(&self) -> Vec<String> {
29        self.aliases.keys().cloned().collect()
30    }
31}
32
33/// Plugin metadata
34#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
35pub struct PluginConfig {
36    /// Path of the plugin, see resolve_wasm_bytes for resolution semantics
37    pub path: PathBuf,
38    /// Allow command execution from plugin
39    pub _allow_exec_host_cmd: bool,
40    /// Original location of the
41    pub location: RunPluginLocation,
42    /// Custom configuration for this plugin
43    pub userspace_configuration: PluginUserConfiguration,
44    /// plugin initial working directory
45    pub initial_cwd: Option<PathBuf>,
46}
47
48impl PluginConfig {
49    pub fn from_run_plugin(run_plugin: &RunPlugin) -> Option<PluginConfig> {
50        match &run_plugin.location {
51            RunPluginLocation::File(path) => Some(PluginConfig {
52                path: path.clone(),
53                _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
54                location: run_plugin.location.clone(),
55                userspace_configuration: run_plugin.configuration.clone(),
56                initial_cwd: run_plugin.initial_cwd.clone(),
57            }),
58            RunPluginLocation::Zellij(tag) => {
59                let tag = tag.to_string();
60                if tag == "status-bar"
61                    || tag == "tab-bar"
62                    || tag == "compact-bar"
63                    || tag == "strider"
64                    || tag == "session-manager"
65                    || tag == "configuration"
66                    || tag == "plugin-manager"
67                    || tag == "about"
68                    || tag == "share"
69                    || tag == "multiple-select"
70                {
71                    Some(PluginConfig {
72                        path: PathBuf::from(&tag),
73                        _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
74                        location: RunPluginLocation::parse(&format!("zellij:{}", tag), None)
75                            .ok()?,
76                        userspace_configuration: run_plugin.configuration.clone(),
77                        initial_cwd: run_plugin.initial_cwd.clone(),
78                    })
79                } else {
80                    None
81                }
82            },
83            RunPluginLocation::Remote(_) => Some(PluginConfig {
84                path: PathBuf::new(),
85                _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
86                location: run_plugin.location.clone(),
87                userspace_configuration: run_plugin.configuration.clone(),
88                initial_cwd: run_plugin.initial_cwd.clone(),
89            }),
90        }
91    }
92    /// Resolve wasm plugin bytes for the plugin path and given plugin directory.
93    ///
94    /// If zellij was built without the 'disable_automatic_asset_installation' feature, builtin
95    /// plugins (Starting with 'zellij:' in the layout file) are loaded directly from the
96    /// binary-internal asset map. Otherwise:
97    ///
98    /// Attempts to first resolve the plugin path as an absolute path, then adds a ".wasm"
99    /// extension to the path and resolves that, finally we use the plugin directory joined with
100    /// the path with an appended ".wasm" extension. So if our path is "tab-bar" and the given
101    /// plugin dir is "/home/bob/.zellij/plugins" the lookup chain will be this:
102    ///
103    /// ```bash
104    ///   /tab-bar
105    ///   /tab-bar.wasm
106    /// ```
107    ///
108    pub fn resolve_wasm_bytes(&self, plugin_dir: &Path) -> Result<Vec<u8>> {
109        let err_context =
110            |err: std::io::Error, path: &PathBuf| format!("{}: '{}'", err, path.display());
111
112        // Locations we check for valid plugins
113        let paths_arr = [
114            &self.path,
115            &self.path.with_extension("wasm"),
116            &plugin_dir.join(&self.path).with_extension("wasm"),
117        ];
118        // Throw out dupes, because it's confusing to read that zellij checked the same plugin
119        // location multiple times. Do NOT sort the vector here, because it will break the lookup!
120        let mut paths = paths_arr.to_vec();
121        paths.dedup();
122
123        // This looks weird and usually we would handle errors like this differently, but in this
124        // case it's helpful for users and developers alike. This way we preserve all the lookup
125        // errors and can report all of them back. We must initialize `last_err` with something,
126        // and since the user will only get to see it when loading a plugin failed, we may as well
127        // spell it out right here.
128        let mut last_err: Result<Vec<u8>> = Err(anyhow!("failed to load plugin from disk"));
129        for path in paths {
130            // Check if the plugin path matches an entry in the asset map. If so, load it directly
131            // from memory, don't bother with the disk.
132            #[cfg(not(target_family = "wasm"))]
133            if !cfg!(feature = "disable_automatic_asset_installation") && self.is_builtin() {
134                let asset_path = PathBuf::from("plugins").join(path);
135                if let Some(bytes) = ASSET_MAP.get(&asset_path) {
136                    log::debug!("Loaded plugin '{}' from internal assets", path.display());
137
138                    if plugin_dir.join(path).with_extension("wasm").exists() {
139                        log::info!(
140                            "Plugin '{}' exists in the 'PLUGIN DIR' at '{}' but is being ignored",
141                            path.display(),
142                            plugin_dir.display()
143                        );
144                    }
145
146                    return Ok(bytes.to_vec());
147                }
148            }
149
150            // Try to read from disk
151            match fs::read(&path) {
152                Ok(val) => {
153                    log::debug!("Loaded plugin '{}' from disk", path.display());
154                    return Ok(val);
155                },
156                Err(err) => {
157                    last_err = last_err.with_context(|| err_context(err, &path));
158                },
159            }
160        }
161
162        // Not reached if a plugin is found!
163        #[cfg(not(target_family = "wasm"))]
164        if self.is_builtin() {
165            // Layout requested a builtin plugin that wasn't found
166            let plugin_path = self.path.with_extension("wasm");
167
168            if cfg!(feature = "disable_automatic_asset_installation")
169                && ASSET_MAP.contains_key(&PathBuf::from("plugins").join(&plugin_path))
170            {
171                return Err(ZellijError::BuiltinPluginMissing {
172                    plugin_path,
173                    plugin_dir: plugin_dir.to_owned(),
174                    source: last_err.unwrap_err(),
175                })
176                .context("failed to load a plugin");
177            } else {
178                return Err(ZellijError::BuiltinPluginNonexistent {
179                    plugin_path,
180                    source: last_err.unwrap_err(),
181                })
182                .context("failed to load a plugin");
183            }
184        }
185
186        return last_err;
187    }
188
189    pub fn is_builtin(&self) -> bool {
190        matches!(self.location, RunPluginLocation::Zellij(_))
191    }
192}
193
194#[derive(Error, Debug, PartialEq)]
195pub enum PluginsConfigError {
196    #[error("Duplication in plugin tag names is not allowed: '{}'", String::from(.0.clone()))]
197    DuplicatePlugins(PluginTag),
198    #[error("Failed to parse url: {0:?}")]
199    InvalidUrl(#[from] url::ParseError),
200    #[error("Only 'file:', 'http(s):' and 'zellij:' url schemes are supported for plugin lookup. '{0}' does not match either.")]
201    InvalidUrlScheme(Url),
202    #[error("Could not find plugin at the path: '{0:?}'")]
203    InvalidPluginLocation(PathBuf),
204}