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);
}
}