Skip to main content

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 initial_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                initial_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                    || tag == "layout-manager"
71                    || tag == "link"
72                {
73                    Some(PluginConfig {
74                        path: PathBuf::from(&tag),
75                        _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
76                        location: RunPluginLocation::parse(&format!("zellij:{}", tag), None)
77                            .ok()?,
78                        initial_userspace_configuration: run_plugin.configuration.clone(),
79                        initial_cwd: run_plugin.initial_cwd.clone(),
80                    })
81                } else {
82                    None
83                }
84            },
85            RunPluginLocation::Remote(_) => Some(PluginConfig {
86                path: PathBuf::new(),
87                _allow_exec_host_cmd: run_plugin._allow_exec_host_cmd,
88                location: run_plugin.location.clone(),
89                initial_userspace_configuration: run_plugin.configuration.clone(),
90                initial_cwd: run_plugin.initial_cwd.clone(),
91            }),
92        }
93    }
94    /// Resolve wasm plugin bytes for the plugin path and given plugin directory.
95    ///
96    /// If zellij was built without the 'disable_automatic_asset_installation' feature, builtin
97    /// plugins (Starting with 'zellij:' in the layout file) are loaded directly from the
98    /// binary-internal asset map. Otherwise:
99    ///
100    /// Attempts to first resolve the plugin path as an absolute path, then adds a ".wasm"
101    /// extension to the path and resolves that, finally we use the plugin directory joined with
102    /// the path with an appended ".wasm" extension. So if our path is "tab-bar" and the given
103    /// plugin dir is "/home/bob/.zellij/plugins" the lookup chain will be this:
104    ///
105    /// ```bash
106    ///   /tab-bar
107    ///   /tab-bar.wasm
108    /// ```
109    ///
110    pub fn resolve_wasm_bytes(&self, plugin_dir: &Path) -> Result<Vec<u8>> {
111        let err_context =
112            |err: std::io::Error, path: &PathBuf| format!("{}: '{}'", err, path.display());
113
114        // Locations we check for valid plugins
115        let paths_arr = [
116            &self.path,
117            &self.path.with_extension("wasm"),
118            &plugin_dir.join(&self.path).with_extension("wasm"),
119        ];
120        // Throw out dupes, because it's confusing to read that zellij checked the same plugin
121        // location multiple times. Do NOT sort the vector here, because it will break the lookup!
122        let mut paths = paths_arr.to_vec();
123        paths.dedup();
124
125        // This looks weird and usually we would handle errors like this differently, but in this
126        // case it's helpful for users and developers alike. This way we preserve all the lookup
127        // errors and can report all of them back. We must initialize `last_err` with something,
128        // and since the user will only get to see it when loading a plugin failed, we may as well
129        // spell it out right here.
130        let mut last_err: Result<Vec<u8>> = Err(anyhow!("failed to load plugin from disk"));
131        for path in paths {
132            // Check if the plugin path matches an entry in the asset map. If so, load it directly
133            // from memory, don't bother with the disk.
134            #[cfg(not(target_family = "wasm"))]
135            if !cfg!(feature = "disable_automatic_asset_installation") && self.is_builtin() {
136                let asset_path = PathBuf::from("plugins").join(path);
137                if let Some(bytes) = ASSET_MAP.get(&asset_path) {
138                    log::debug!("Loaded plugin '{}' from internal assets", path.display());
139
140                    if plugin_dir.join(path).with_extension("wasm").exists() {
141                        log::info!(
142                            "Plugin '{}' exists in the 'PLUGIN DIR' at '{}' but is being ignored",
143                            path.display(),
144                            plugin_dir.display()
145                        );
146                    }
147
148                    return Ok(bytes.to_vec());
149                }
150            }
151
152            // Try to read from disk
153            match fs::read(&path) {
154                Ok(val) => {
155                    log::debug!("Loaded plugin '{}' from disk", path.display());
156                    return Ok(val);
157                },
158                Err(err) => {
159                    last_err = last_err.with_context(|| err_context(err, &path));
160                },
161            }
162        }
163
164        // Not reached if a plugin is found!
165        #[cfg(not(target_family = "wasm"))]
166        if self.is_builtin() {
167            // Layout requested a builtin plugin that wasn't found
168            let plugin_path = self.path.with_extension("wasm");
169
170            if cfg!(feature = "disable_automatic_asset_installation")
171                && ASSET_MAP.contains_key(&PathBuf::from("plugins").join(&plugin_path))
172            {
173                return Err(ZellijError::BuiltinPluginMissing {
174                    plugin_path,
175                    plugin_dir: plugin_dir.to_owned(),
176                    source: last_err.unwrap_err(),
177                })
178                .context("failed to load a plugin");
179            } else {
180                return Err(ZellijError::BuiltinPluginNonexistent {
181                    plugin_path,
182                    source: last_err.unwrap_err(),
183                })
184                .context("failed to load a plugin");
185            }
186        }
187
188        return last_err;
189    }
190
191    pub fn is_builtin(&self) -> bool {
192        matches!(self.location, RunPluginLocation::Zellij(_))
193    }
194}
195
196#[derive(Error, Debug, PartialEq)]
197pub enum PluginsConfigError {
198    #[error("Duplication in plugin tag names is not allowed: '{}'", String::from(.0.clone()))]
199    DuplicatePlugins(PluginTag),
200    #[error("Failed to parse url: {0:?}")]
201    InvalidUrl(#[from] url::ParseError),
202    #[error("Only 'file:', 'http(s):' and 'zellij:' url schemes are supported for plugin lookup. '{0}' does not match either.")]
203    InvalidUrlScheme(Url),
204    #[error("Could not find plugin at the path: '{0:?}'")]
205    InvalidPluginLocation(PathBuf),
206}