krust-cliplinkd 0.4.0

ClipLink daemon — receive text from mobile and auto-paste on desktop
use anyhow::Context;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Config {
    #[serde(default)]
    pub server: ServerConfig,
    #[serde(default)]
    pub auth: AuthConfig,
    #[serde(default)]
    pub service: ServiceConfig,
    #[serde(default)]
    pub clipboard: ClipboardConfig,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServerConfig {
    #[serde(default = "default_bind")]
    pub bind: String,
    #[serde(default = "default_port")]
    pub port: u16,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthConfig {
    #[serde(default)]
    pub pin: String,
}


#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClipboardConfig {
    /// Max auto-fetch size in bytes. Text below this threshold is fetched
    /// without confirmation. Images and files always require confirmation.
    #[serde(default = "default_max_fetch_size")]
    pub max_fetch_size: usize,
}

fn default_max_fetch_size() -> usize {
    524288 // 512 KB
}

impl Default for ClipboardConfig {
    fn default() -> Self {
        Self {
            max_fetch_size: default_max_fetch_size(),
        }
    }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceConfig {
    #[serde(default = "default_service_name")]
    pub name: String,
}

fn default_bind() -> String {
    "0.0.0.0".into()
}

fn default_port() -> u16 {
    9527
}

fn default_service_name() -> String {
    hostname::get()
        .ok()
        .and_then(|h| h.into_string().ok())
        .unwrap_or_else(|| "ClipLink".into())
}

impl Default for ServerConfig {
    fn default() -> Self {
        Self {
            bind: default_bind(),
            port: default_port(),
        }
    }
}

impl Default for AuthConfig {
    fn default() -> Self {
        Self {
            pin: String::new(),
        }
    }
}

impl Default for ServiceConfig {
    fn default() -> Self {
        Self {
            name: default_service_name(),
        }
    }
}

impl Default for Config {
    fn default() -> Self {
        Self {
            server: ServerConfig::default(),
            auth: AuthConfig::default(),
            service: ServiceConfig::default(),
            clipboard: ClipboardConfig::default(),
        }
    }
}

impl Config {
    /// User config directory under $HOME/.config/cliplinkd/
    fn config_dir() -> PathBuf {
        #[cfg(target_os = "windows")]
        {
            dirs::config_dir()
                .unwrap_or_default()
                .join("cliplinkd")
        }
        #[cfg(not(target_os = "windows"))]
        {
            dirs::home_dir()
                .unwrap_or_default()
                .join(".config")
                .join("cliplinkd")
        }
    }

    /// Load config from the first found path:
    ///   1. ~/.config/cliplinkd/cliplinkd.toml  (user config)
    ///   2. ./cliplinkd.toml                     (local override)
    /// Falls back to defaults if none found.
    pub fn load() -> anyhow::Result<Self> {
        let paths: Vec<PathBuf> = vec![
            Self::config_dir().join("cliplinkd.toml"),
            PathBuf::from("cliplinkd.toml"),
        ];

        for path in &paths {
            if path.exists() {
                let content = std::fs::read_to_string(path).with_context(|| {
                    format!("Failed to read config from {}", path.display())
                })?;
                let config: Config = toml::from_str(&content).with_context(|| {
                    format!("Failed to parse config from {}", path.display())
                })?;
                tracing::info!("Loaded config from {}", path.display());
                return Ok(config);
            }
        }

        tracing::info!("No config file found, using defaults");
        Ok(Config::default())
    }

    /// Write config to ~/.config/cliplinkd/cliplinkd.toml
    pub fn save(&self) -> anyhow::Result<PathBuf> {
        let dir = Self::config_dir();
        std::fs::create_dir_all(&dir)?;
        let path = dir.join("cliplinkd.toml");
        let content = toml::to_string_pretty(self)?;
        std::fs::write(&path, content)?;
        Ok(path)
    }
}