hyprshell_exec_lib/
plugin.rs

1use anyhow::{Context, bail};
2use config_lib::Modifier;
3use hyprland::ctl::plugin;
4use hyprland_plugin::PluginConfig;
5use std::path::Path;
6use std::sync::OnceLock;
7use tracing::{debug, debug_span, info, trace};
8
9// info: trying to load a plugin causes hyprland to issue a reload
10// this will cause hyprshell to restart.
11// this second restart wont reload the plugin as the plugin config didnt change
12// if the plugin fails to load it however tries again which the triggers the next reload
13static PLUGIN_COULD_BE_BUILD: OnceLock<bool> = OnceLock::new();
14
15pub fn load_plugin(
16    switch: Option<(Modifier, Box<str>)>,
17    overview: Option<(Modifier, Box<str>)>,
18) -> anyhow::Result<()> {
19    let _span = debug_span!("load_plugin").entered();
20
21    if PLUGIN_COULD_BE_BUILD.get() == Some(&false) {
22        bail!("plugin could not be built last, skipping to prevent reload loop");
23    }
24
25    let config = PluginConfig {
26        xkb_key_switch_mod: switch
27            .as_ref()
28            .map(|(r#mod, _)| Box::from(mod_to_xkb_key(*r#mod))),
29        xkb_key_switch_key: switch.map(|(_, key)| key),
30        xkb_key_overview_mod: overview
31            .as_ref()
32            .map(|(r#mod, _)| Box::from(r#mod.to_string())),
33        xkb_key_overview_key: overview.map(|(_, key)| key),
34    };
35
36    if check_new_plugin_needed(&config) {
37        unload().context("unable to unload old plugin")?;
38        info!("Building plugin, this may take a while, please wait");
39        hyprland_plugin::generate(&config).context("unable to generate plugin")?;
40        trace!(
41            "generated plugin at {:?}",
42            hyprland_plugin::PLUGIN_OUTPUT_PATH
43        );
44        if let Err(err) = plugin::load(Path::new(hyprland_plugin::PLUGIN_OUTPUT_PATH)) {
45            PLUGIN_COULD_BE_BUILD.get_or_init(|| false);
46            trace!("plugin failed to load, disabling plugin");
47            bail!("unable to load plugin: {err:?}")
48        }
49        trace!("loaded plugin");
50    } else {
51        debug!("plugin already loaded, skipping");
52    }
53
54    Ok(())
55}
56
57pub fn check_new_plugin_needed(config: &PluginConfig) -> bool {
58    let plugins = plugin::list().unwrap_or_default();
59    trace!("plugins: {plugins:?}");
60    for plugin in plugins {
61        if plugin.name == hyprland_plugin::PLUGIN_NAME {
62            let Some(desc) = plugin.description.split(" - ").last() else {
63                continue;
64            };
65            if desc == config.to_string() {
66                // config didn't change, no need to reload
67                return false;
68            }
69        }
70    }
71    true
72}
73
74pub fn unload() -> anyhow::Result<()> {
75    let plugins = plugin::list().unwrap_or_default();
76    for plugin in plugins {
77        if plugin.name == hyprland_plugin::PLUGIN_NAME {
78            debug!("plugin loaded, unloading it");
79            plugin::unload(Path::new(hyprland_plugin::PLUGIN_OUTPUT_PATH)).with_context(|| {
80                format!(
81                    "unable to unload old plugin at: {}",
82                    hyprland_plugin::PLUGIN_OUTPUT_PATH
83                )
84            })?;
85            debug!("plugin unloaded");
86        }
87    }
88    Ok(())
89}
90
91#[allow(clippy::must_use_candidate)]
92pub const fn mod_to_xkb_key(r#mod: Modifier) -> &'static str {
93    match r#mod {
94        Modifier::Alt => "XKB_KEY_Alt",
95        Modifier::Ctrl => "XKB_KEY_Control",
96        Modifier::Super => "XKB_KEY_Super",
97        Modifier::None => "XKB_KEY_NoSymbol",
98    }
99}