guth 0.2.16

Native Rust desktop file manager for fast, bounded local file workflows.
Documentation
use guth_cli::{discover_plugins, PluginManifest};
use std::path::PathBuf;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};

pub const KNOTWORK_NAME: &str = "Knotwork";
pub const MEDIA_PREVIEW_PLUGIN_ID: &str = "knotlook-media";
pub const TEXT_PREVIEW_PLUGIN_ID: &str = "knotread-text";
pub const GUTH_SYNC_PLUGIN_ID: &str = "019f613e-14ad-7205-a36d-73f8ac49c24e";

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PluginSlot {
    Overlay,
}

#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct PluginDescriptor {
    pub id: &'static str,
    pub name: &'static str,
    pub slot: PluginSlot,
    pub shortcut: &'static str,
    pub description: &'static str,
}

#[derive(Clone, Debug, Default)]
pub struct PluginRegistry {
    external: Vec<PluginManifest>,
}

const BUILTIN_PLUGINS: [PluginDescriptor; 2] = [
    PluginDescriptor {
        id: MEDIA_PREVIEW_PLUGIN_ID,
        name: "Knotlook Media",
        slot: PluginSlot::Overlay,
        shortcut: "Space",
        description: "Inline image and video preview using native Guth surfaces.",
    },
    PluginDescriptor {
        id: TEXT_PREVIEW_PLUGIN_ID,
        name: "Knotread Text",
        slot: PluginSlot::Overlay,
        shortcut: "Space",
        description: "Bounded inline text, code, markdown, CSV, and log preview.",
    },
];

impl PluginRegistry {
    pub fn builtin() -> Self {
        Self {
            external: discover_plugins().unwrap_or_default(),
        }
    }

    pub fn all(&self) -> &'static [PluginDescriptor] {
        &BUILTIN_PLUGINS
    }

    pub fn external(&self) -> &[PluginManifest] {
        &self.external
    }

    pub fn refresh_external(&mut self) -> Result<(), String> {
        self.external = discover_plugins().map_err(|error| error.to_string())?;
        Ok(())
    }

    pub fn installed_count(&self) -> usize {
        self.all().len() + self.external.len()
    }

    pub fn media_preview(&self) -> PluginDescriptor {
        BUILTIN_PLUGINS[0]
    }

    pub fn text_preview(&self) -> PluginDescriptor {
        BUILTIN_PLUGINS[1]
    }
}

pub fn install_guth_sync_manifest() -> Result<(), String> {
    let sibling = std::env::current_exe()
        .ok()
        .and_then(|path| path.parent().map(|parent| parent.join("guth-sync")));
    let mut candidates = sibling.into_iter().collect::<Vec<_>>();
    candidates.push(PathBuf::from("guth-sync"));
    let mut last_error = "guth-sync is not installed".to_string();
    let deadline = Instant::now() + Duration::from_secs(15);
    for executable in candidates {
        let child = Command::new(&executable)
            .arg("install-plugin")
            .stdin(Stdio::null())
            .stdout(Stdio::null())
            .stderr(Stdio::null())
            .spawn();
        let mut child = match child {
            Ok(child) => child,
            Err(error) => {
                last_error = error.to_string();
                continue;
            }
        };
        loop {
            match child.try_wait() {
                Ok(Some(status)) if status.success() => return Ok(()),
                Ok(Some(status)) => {
                    last_error = format!("guth-sync exited with {status}");
                    break;
                }
                Ok(None) if Instant::now() < deadline => {
                    thread::sleep(Duration::from_millis(50));
                }
                Ok(None) => {
                    let _ = child.kill();
                    let _ = child.wait();
                    return Err("Could not enable Guth Sync: installer timed out".to_string());
                }
                Err(error) => {
                    let _ = child.kill();
                    let _ = child.wait();
                    last_error = error.to_string();
                    break;
                }
            }
        }
    }
    Err(format!(
        "Could not enable Guth Sync: {last_error}. Install it with `cargo install guth-sync`."
    ))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn builtin_registry_exposes_media_preview_plugin() {
        let registry = PluginRegistry::builtin();
        let plugin = registry.media_preview();
        assert_eq!(plugin.id, MEDIA_PREVIEW_PLUGIN_ID);
        assert_eq!(registry.all()[0], plugin);
        assert_eq!(registry.text_preview().id, TEXT_PREVIEW_PLUGIN_ID);
    }
}