xaskpass 2.5.2

A lightweight passphrase dialog
use std::io::Write;
use std::path::Path;

use serde::{Deserialize, Deserializer, Serialize, Serializer};
use toml::Value;

use crate::errors::{Context as _, Error, Result};
use crate::{bail, NAME};

pub struct Loader {
    pub xdg_dirs: xdg::BaseDirectories,
}
impl Loader {
    pub fn new() -> Self {
        let xdg_dirs = xdg::BaseDirectories::with_prefix(NAME).unwrap();
        Self { xdg_dirs }
    }

    pub fn load(&self) -> Result<Config> {
        self.xdg_dirs
            .find_config_file(format!("{}.toml", NAME))
            .as_deref()
            .map_or_else(|| Ok(Config::default()), Self::load_path)
    }

    pub fn load_path(path: &Path) -> Result<Config> {
        let data = std::fs::read_to_string(&path).context("Config file")?;
        Ok(toml::from_str(&data).context("Config Toml")?)
    }

    pub fn print(cfg: &Config) -> Result<()> {
        let toml = toml::to_string_pretty(cfg).context("toml serialize")?;
        std::io::stdout()
            .write_all(toml.as_bytes())
            .expect("Unable to write data");
        Ok(())
    }
}

pub fn option_explicit_none<'de, T, D>(deserializer: D) -> std::result::Result<Option<T>, D::Error>
where
    D: Deserializer<'de>,
    T: Deserialize<'de>,
{
    Ok(match Value::deserialize(deserializer)? {
        Value::String(ref value) if value.to_lowercase() == "none" => None,
        value => Some(T::deserialize(value).map_err(serde::de::Error::custom)?),
    })
}

pub fn option_explicit_serialize<T, S>(
    val: &Option<T>,
    serializer: S,
) -> std::result::Result<S::Ok, S::Error>
where
    S: Serializer,
    T: Serialize,
{
    match val {
        None => str::serialize("none", serializer),
        Some(ref val) => T::serialize(val, serializer),
    }
}

#[derive(Debug, Clone, Copy)]
pub struct Rgba {
    pub red: u8,
    pub green: u8,
    pub blue: u8,
    pub alpha: u8,
}

impl Serialize for Rgba {
    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
        let hex = if self.alpha == u8::MAX {
            hex::encode([self.red, self.green, self.blue])
        } else {
            hex::encode([self.red, self.green, self.blue, self.alpha])
        };
        serializer.serialize_str(&format!("#{}", hex))
    }
}

impl<'de> Deserialize<'de> for Rgba {
    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
        let s = String::deserialize(deserializer)?;
        s.parse().map_err(serde::de::Error::custom)
    }
}

impl std::str::FromStr for Rgba {
    type Err = Error;
    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
        log::trace!("rgba::from_str {}", s);
        let without_prefix = s.trim_start_matches('#');
        match without_prefix.len() {
            8 => {
                let mut bytes = [0_u8; 4];
                hex::decode_to_slice(without_prefix, &mut bytes).context("color")?;
                log::trace!("rgba::from_str {:?}", bytes);
                Ok(Rgba {
                    red: bytes[0],
                    green: bytes[1],
                    blue: bytes[2],
                    alpha: bytes[3],
                })
            }
            6 => {
                let mut bytes = [0_u8; 3];
                hex::decode_to_slice(without_prefix, &mut bytes).context("color")?;
                log::trace!("rgba::from_str {:?}", bytes);
                Ok(Rgba {
                    red: bytes[0],
                    green: bytes[1],
                    blue: bytes[2],
                    alpha: u8::MAX,
                })
            }
            n => bail!("invalid hex color length: {}", n),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Config {
    pub title: String,
    pub grab_keyboard: bool,
    pub show_hostname: bool,
    pub resizable: bool,
    pub depth: u8,
    pub dialog: Dialog,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            title: NAME.into(),
            grab_keyboard: false,
            show_hostname: false,
            resizable: false,
            depth: 32,
            dialog: Dialog::default(),
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Dialog {
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub font: Option<String>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub font_file: Option<std::ffi::CString>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub direction: Option<PangoDirection>,
    pub label: String,
    pub alignment: PangoAlignment,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub scale: Option<f64>,
    pub indicator_label: String,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub input_timeout: Option<u64>,
    pub foreground: Rgba,
    pub indicator_label_foreground: Rgba,
    pub background: Rgba,
    pub layout_opts: Layout,
    pub ok_button: TextButton,
    pub cancel_button: TextButton,
    pub clipboard_button: ClipboardButton,
    pub plaintext_button: TextButton,
    pub indicator: Indicator,
}

impl Default for Dialog {
    fn default() -> Self {
        let button = Button::default();
        let ok_button = TextButton {
            label: "OK".into(),
            foreground: "#5c616c".parse().unwrap(),
            button: button.clone(),
        };
        let cancel_button = TextButton {
            label: "Cancel".into(),
            ..ok_button.clone()
        };

        let plaintext_button = TextButton {
            label: "abc".into(),
            ..ok_button.clone()
        };

        Self {
            foreground: "#5c616c".parse().unwrap(),
            indicator_label_foreground: "#5c616c".parse().unwrap(),
            background: "#f5f6f7ee".parse().unwrap(),
            label: "Please enter your authentication passphrase:".into(),
            alignment: PangoAlignment::Left,
            indicator_label: "Secret:".into(),
            input_timeout: Some(30),
            font: Some("mono 11".into()),
            direction: None,
            scale: None,
            font_file: None,
            layout_opts: Layout::default(),
            ok_button,
            cancel_button,
            plaintext_button,
            clipboard_button: ClipboardButton {
                foreground: "#5c616c".parse().unwrap(),
                button,
            },
            indicator: Indicator::default(),
        }
    }
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct ClipboardButton {
    pub foreground: Rgba,
    #[serde(flatten)]
    pub button: Button,
}

impl Default for ClipboardButton {
    fn default() -> Self {
        Self {
            foreground: "#5c616c".parse().unwrap(),
            button: Button::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct TextButton {
    pub label: String,
    pub foreground: Rgba,
    #[serde(flatten)]
    pub button: Button,
}

impl Default for TextButton {
    fn default() -> Self {
        Self {
            label: "label".into(),
            foreground: "#5c616c".parse().unwrap(),
            button: Button::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Button {
    #[serde(deserialize_with = "option_explicit_none")]
    #[serde(serialize_with = "option_explicit_serialize")]
    pub horizontal_spacing: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub vertical_spacing: Option<f64>,
    pub border_width: f64,
    pub radius_x: f64,
    pub radius_y: f64,
    pub pressed_adjustment_x: f64,
    pub pressed_adjustment_y: f64,
    pub background: Rgba,
    pub border_color: Rgba,
    pub border_color_pressed: Rgba,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub background_stop: Option<Rgba>,
    pub background_pressed: Rgba,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub background_pressed_stop: Option<Rgba>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub background_hover_stop: Option<Rgba>,
    pub background_hover: Rgba,
}

impl Default for Button {
    fn default() -> Self {
        Self {
            background: "#fcfdfd".parse().unwrap(),
            background_stop: None,
            background_pressed: "#d3d8e2".parse().unwrap(),
            background_pressed_stop: None,
            background_hover: "#ffffff".parse().unwrap(),
            background_hover_stop: None,
            horizontal_spacing: None,
            vertical_spacing: None,
            border_width: 1.0,
            border_color: "#cfd6e6".parse().unwrap(),
            border_color_pressed: "#b7c0d3".parse().unwrap(),
            radius_x: 2.0,
            radius_y: 2.0,
            pressed_adjustment_x: 1.0,
            pressed_adjustment_y: 1.0,
        }
    }
}

#[derive(Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Layout {
    pub layout: crate::dialog::layout::Layout,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    horizontal_spacing: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    vertical_spacing: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub text_width: Option<u32>,
}

impl Layout {
    pub fn horizontal_spacing(&self, text_height: f64) -> f64 {
        self.horizontal_spacing
            .unwrap_or_else(|| (text_height / 1.7).round())
    }
    pub fn vertical_spacing(&self, text_height: f64) -> f64 {
        self.vertical_spacing
            .unwrap_or_else(|| (text_height / 1.7).round())
    }
}

impl Default for Layout {
    fn default() -> Self {
        Self {
            layout: crate::dialog::layout::Layout::Center,
            horizontal_spacing: None,
            vertical_spacing: None,
            text_width: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[serde(default)]
pub struct IndicatorClassic {
    pub min_count: u16,
    pub max_count: u16,
    pub radius_x: f64,
    pub radius_y: f64,
    #[serde(deserialize_with = "option_explicit_none")]
    #[serde(serialize_with = "option_explicit_serialize")]
    pub horizontal_spacing: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub element_height: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub element_width: Option<f64>,
}

impl Default for IndicatorClassic {
    fn default() -> Self {
        Self {
            min_count: 3,
            max_count: 3,
            radius_x: 2.0,
            radius_y: 2.0,
            horizontal_spacing: None,
            element_height: None,
            element_width: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[serde(default)]
pub struct IndicatorCircle {
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub diameter: Option<f64>,
    pub rotate: bool,
    pub rotation_speed_start: f64,
    pub rotation_speed_gain: f64,
    pub light_up: bool,
    pub spacing_angle: f64,
    pub indicator_count: u32,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub indicator_width: Option<f64>,
    pub lock_color: Rgba,
}

impl Default for IndicatorCircle {
    fn default() -> Self {
        Self {
            diameter: None,
            rotate: true,
            light_up: true,
            rotation_speed_start: 0.10,
            rotation_speed_gain: 1.05,
            spacing_angle: 0.5,
            indicator_count: 3,
            indicator_width: None,
            lock_color: "#ffffff".parse().unwrap(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Disco {
    pub min_count: u16,
    pub max_count: u16,
    pub three_states: bool,
}

impl Default for Disco {
    fn default() -> Self {
        Self {
            min_count: 3,
            max_count: 3,
            three_states: false,
        }
    }
}

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum PangoAlignment {
    Left,
    Center,
    Right,
}

impl From<PangoAlignment> for pango::Alignment {
    fn from(val: PangoAlignment) -> Self {
        match val {
            PangoAlignment::Left => Self::Left,
            PangoAlignment::Center => Self::Center,
            PangoAlignment::Right => Self::Right,
        }
    }
}

#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum PangoDirection {
    Ltr,
    Neutral,
    Rtl,
    WeakLtr,
    WeakRtl,
}

impl From<PangoDirection> for pango::Direction {
    fn from(val: PangoDirection) -> Self {
        match val {
            PangoDirection::Ltr => Self::Ltr,
            PangoDirection::Neutral => Self::Neutral,
            PangoDirection::Rtl => Self::Rtl,
            PangoDirection::WeakLtr => Self::WeakLtr,
            PangoDirection::WeakRtl => Self::WeakRtl,
        }
    }
}

fn strings<'de, D>(d: D) -> std::result::Result<Vec<String>, D::Error>
where
    D: serde::de::Deserializer<'de>,
{
    let arr: Vec<String> = Vec::deserialize(d)?;

    if arr.len() < 2 {
        return Err(serde::de::Error::custom(
            "strings should have at least 2 elements",
        ));
    }

    Ok(arr)
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Custom {
    pub alignment: PangoAlignment,
    pub justify: bool,
    pub randomize: bool,
    #[serde(deserialize_with = "strings")]
    pub strings: Vec<String>,
}

#[allow(clippy::unicode_not_nfc)]
impl Default for Custom {
    fn default() -> Self {
        Self {
            alignment: PangoAlignment::Center,
            justify: false,
            randomize: true,
            strings: vec![
                "pasted 🤯".into(),
                "(っ-̶●̃益●̶̃)っ ,︵‿ ".into(),
                "(⊙.⊙(☉̃ₒ☉)⊙.⊙)".into(),
                "ʕ•́ᴥ•̀ʔっ".into(),
                "ヽ(´ー`)人(´∇`)人(`Д´)ノ".into(),
            ],
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "strings")]
pub enum StringType {
    Disco {
        #[serde(default)]
        disco: Disco,
    },
    Custom {
        #[serde(default)]
        custom: Custom,
    },
    Asterisk {
        #[serde(default)]
        asterisk: Asterisk,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct IndicatorStrings {
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub horizontal_spacing: Option<f64>,
    #[serde(serialize_with = "option_explicit_serialize")]
    #[serde(deserialize_with = "option_explicit_none")]
    pub vertical_spacing: Option<f64>,
    pub radius_x: f64,
    pub radius_y: f64,
    #[serde(flatten)]
    pub strings: StringType,
}

impl Default for IndicatorStrings {
    fn default() -> Self {
        Self {
            horizontal_spacing: None,
            vertical_spacing: None,
            radius_x: 2.0,
            radius_y: 2.0,
            strings: StringType::Asterisk {
                asterisk: Asterisk::default(),
            },
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum IndicatorType {
    Strings {
        #[serde(default)]
        strings: IndicatorStrings,
    },
    Circle {
        #[serde(default)]
        circle: IndicatorCircle,
    },
    Classic {
        #[serde(default)]
        classic: IndicatorClassic,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Indicator {
    #[serde(flatten)]
    pub common: IndicatorCommon,
    #[serde(rename = "type")]
    #[serde(flatten)]
    pub indicator_type: IndicatorType,
}

impl Default for Indicator {
    fn default() -> Self {
        Self {
            indicator_type: IndicatorType::Circle {
                circle: IndicatorCircle::default(),
            },
            common: IndicatorCommon::default(),
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize, Copy)]
#[serde(default)]
pub struct IndicatorCommon {
    pub border_width: f64,
    pub blink: bool,
    pub foreground: Rgba,
    pub background: Rgba,
    #[serde(deserialize_with = "option_explicit_none")]
    #[serde(serialize_with = "option_explicit_serialize")]
    pub background_stop: Option<Rgba>,
    pub border_color: Rgba,
    pub border_color_focused: Rgba,
    pub indicator_color: Rgba,
    #[serde(deserialize_with = "option_explicit_none")]
    #[serde(serialize_with = "option_explicit_serialize")]
    pub indicator_color_stop: Option<Rgba>,
}

impl Default for IndicatorCommon {
    fn default() -> Self {
        Self {
            border_width: 1.0,
            foreground: "#5c616c".parse().unwrap(),
            background: "#ffffff".parse().unwrap(),
            background_stop: None,
            blink: true,
            border_color: "#cfd6e6".parse().unwrap(),
            border_color_focused: "#5294e2".parse().unwrap(),
            indicator_color: "#d3d8e2".parse().unwrap(),
            indicator_color_stop: None,
        }
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Asterisk {
    pub alignment: PangoAlignment,
    pub asterisk: String,
    pub min_count: u16,
    pub max_count: u16,
}

impl Default for Asterisk {
    fn default() -> Self {
        Self {
            alignment: PangoAlignment::Center,
            asterisk: "*".into(),
            min_count: 10,
            max_count: 20,
        }
    }
}