wayglance 0.3.3

A file clipboard for Wayland — watches directories for new files and shows a transient Waybar widget with drag-and-drop, open, edit, and copy actions
use anyhow::Result;
use serde::Deserialize;
use std::path::PathBuf;

fn default_watch_dirs() -> Vec<String> {
    vec![
        "~/Pictures/Screenshots".into(),
        "~/Downloads".into(),
    ]
}
fn default_signal_number() -> u8 {
    8
}
fn default_dismiss_seconds() -> u64 {
    10
}
fn default_ignore_suffixes() -> Vec<String> {
    vec![".part".into(), ".crdownload".into(), ".tmp".into()]
}
fn default_bar_height() -> i32 {
    57
}
fn default_history_size() -> usize {
    5
}
fn default_editor() -> String {
    "swappy -f".into()
}
fn default_actions() -> Vec<String> {
    vec!["drag".into(), "open".into(), "edit".into(), "copy".into()]
}
fn default_menu_dismiss_seconds() -> u64 {
    8
}

#[derive(Debug, Clone, Deserialize)]
pub struct MenuStyle {
    #[serde(default = "MenuStyle::default_background")]
    pub background: String,
    #[serde(default = "MenuStyle::default_text_color")]
    pub text_color: String,
    #[serde(default = "MenuStyle::default_secondary_color")]
    pub secondary_color: String,
    #[serde(default = "MenuStyle::default_button_background")]
    pub button_background: String,
    #[serde(default = "MenuStyle::default_button_hover")]
    pub button_hover: String,
    #[serde(default = "MenuStyle::default_border_radius")]
    pub border_radius: i32,
}

impl MenuStyle {
    fn default_background() -> String { "rgba(30,30,46,0.95)".into() }
    fn default_text_color() -> String { "#cdd6f4".into() }
    fn default_secondary_color() -> String { "#a6adc8".into() }
    fn default_button_background() -> String { "rgba(255,255,255,0.08)".into() }
    fn default_button_hover() -> String { "rgba(255,255,255,0.15)".into() }
    fn default_border_radius() -> i32 { 12 }
}

impl Default for MenuStyle {
    fn default() -> Self {
        Self {
            background: Self::default_background(),
            text_color: Self::default_text_color(),
            secondary_color: Self::default_secondary_color(),
            button_background: Self::default_button_background(),
            button_hover: Self::default_button_hover(),
            border_radius: Self::default_border_radius(),
        }
    }
}

#[derive(Debug, Clone, Deserialize)]
pub struct Config {
    #[serde(default = "default_watch_dirs")]
    pub watch_dirs: Vec<String>,
    #[serde(default = "default_signal_number")]
    pub signal_number: u8,
    #[serde(default = "default_dismiss_seconds")]
    pub dismiss_seconds: u64,
    #[serde(default = "default_ignore_suffixes")]
    pub ignore_suffixes: Vec<String>,
    #[serde(default = "default_bar_height")]
    pub bar_height: i32,
    #[serde(default = "default_history_size")]
    pub history_size: usize,
    #[serde(default = "default_editor")]
    pub editor: String,
    #[serde(default = "default_actions")]
    pub actions: Vec<String>,
    #[serde(default = "default_menu_dismiss_seconds")]
    pub menu_dismiss_seconds: u64,
    #[serde(default)]
    pub menu_style: MenuStyle,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            watch_dirs: default_watch_dirs(),
            signal_number: default_signal_number(),
            dismiss_seconds: default_dismiss_seconds(),
            ignore_suffixes: default_ignore_suffixes(),
            bar_height: default_bar_height(),
            history_size: default_history_size(),
            editor: default_editor(),
            actions: default_actions(),
            menu_dismiss_seconds: default_menu_dismiss_seconds(),
            menu_style: MenuStyle::default(),
        }
    }
}

impl Config {
    pub fn load() -> Result<Self> {
        let config_path = config_path();
        if !config_path.exists() {
            return Ok(Self::default());
        }
        let content = std::fs::read_to_string(&config_path)?;
        let mut cfg: Config = toml::from_str(&content)?;
        cfg.watch_dirs = cfg
            .watch_dirs
            .into_iter()
            .map(|d| shellexpand::tilde(&d).into_owned())
            .collect();
        Ok(cfg)
    }

    pub fn has_action(&self, name: &str) -> bool {
        self.actions.iter().any(|a| a == name)
    }

    pub fn state_file() -> PathBuf {
        runtime_dir().join("glance-latest.json")
    }

    pub fn pid_file() -> PathBuf {
        runtime_dir().join("glance.pid")
    }
}

fn runtime_dir() -> PathBuf {
    PathBuf::from(
        std::env::var("XDG_RUNTIME_DIR").unwrap_or_else(|_| "/tmp".into()),
    )
}

fn config_path() -> PathBuf {
    let base = std::env::var("XDG_CONFIG_HOME")
        .map(PathBuf::from)
        .unwrap_or_else(|_| {
            PathBuf::from(std::env::var("HOME").unwrap_or_else(|_| "/tmp".into()))
                .join(".config")
        });
    base.join("glance/config.toml")
}