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