dotlock-bin 0.1.1

Encrypted project-local environment variables manager
use crate::{
    domain::{
        error::DotLockError,
        model::{Alg, DotLockResult},
    },
    storage::secrets_lock::SecretRecord,
};
use colored::Colorize;
use std::io::IsTerminal;

pub fn parse_alg(alg: &str) -> DotLockResult<Alg> {
    match alg {
        "xchacha20-poly1305" => Ok(Alg::XChaCha20Poly1305),
        other => Err(DotLockError::UnsupportedAlgorithm {
            alg: other.to_string(),
        }),
    }
}

pub fn normalize_var_name(name: &str) -> DotLockResult<String> {
    let mut chars = name.chars();

    let first = chars
        .next()
        .ok_or_else(|| DotLockError::InvalidVariableName {
            name: name.to_string(),
        })?;

    if !(first.is_ascii_alphabetic() || first == '_') {
        return Err(DotLockError::InvalidVariableName {
            name: name.to_string(),
        });
    }

    if !chars.all(|c| c.is_ascii_alphanumeric() || c == '_') {
        return Err(DotLockError::InvalidVariableName {
            name: name.to_string(),
        });
    }

    Ok(name.to_ascii_uppercase())
}

pub fn print_get_result(name: &str, id: &str, value: &str) {
    if !std::io::stdout().is_terminal() {
        println!("{}", value);
        return;
    }

    let short = short_uuid(id);
    let title = name.to_string();
    let id_line = format!("id: {}", short);
    let value_lines: Vec<&str> = if value.is_empty() {
        vec![""]
    } else {
        value.lines().collect()
    };

    let center = |s: &str, w: usize| {
        let len = s.chars().count();
        if len >= w {
            s.to_string()
        } else {
            let total = w - len;
            let left = total / 2;
            let right = total - left;
            format!("{}{}{}", " ".repeat(left), s, " ".repeat(right))
        }
    };
    let content_w = title.chars().count().max(id_line.chars().count()).max(
        value_lines
            .iter()
            .map(|line| line.chars().count())
            .max()
            .unwrap_or(0),
    );
    let inner_w = content_w + 2;

    println!();
    println!(
        "  {}{}{}",
        "".dimmed(),
        "".repeat(inner_w).dimmed(),
        "".dimmed()
    );
    println!(
        "  {}{}{}",
        "".dimmed(),
        center(&title, content_w).bold(),
        "".dimmed()
    );
    println!(
        "  {}{}{}",
        "".dimmed(),
        center(&id_line, content_w).yellow(),
        "".dimmed()
    );
    println!(
        "  {}{}{}",
        "".dimmed(),
        " ".repeat(content_w),
        "".dimmed()
    );
    for line in value_lines {
        println!(
            "  {}{}{}",
            "".dimmed(),
            center(line, content_w),
            "".dimmed()
        );
    }
    println!(
        "  {}{}{}",
        "".dimmed(),
        "".repeat(content_w + 2).dimmed(),
        "".dimmed()
    );
    println!();
    println!(
        "  {} pipe to a command to read the value (e.g. `dl get {} | pbcopy`)",
        "hint:".cyan().bold(),
        name
    );
    println!();
}

pub fn print_secrets_table(entries: &[SecretRecord]) {
    if entries.is_empty() {
        println!("{} no secrets stored", "info:".cyan().bold());
        return;
    }

    let id_header = "ID";
    let name_header = "NAME";

    let id_w = id_header.len().max(8);
    let name_w = entries
        .iter()
        .map(|e| e.name.chars().count())
        .max()
        .unwrap_or(0)
        .max(name_header.len());

    let pad = |s: &str, w: usize| {
        let len = s.chars().count();
        if len >= w {
            s.to_string()
        } else {
            format!("{}{}", s, " ".repeat(w - len))
        }
    };

    println!();
    println!(
        "  {}  {}",
        pad(id_header, id_w).dimmed().bold(),
        pad(name_header, name_w).dimmed().bold()
    );
    println!(
        "  {}  {}",
        "".repeat(id_w).dimmed(),
        "".repeat(name_w).dimmed()
    );
    for entry in entries {
        let short = short_uuid(&entry.id);
        println!(
            "  {}  {}",
            pad(&short, id_w).yellow(),
            pad(&entry.name, name_w).bold()
        );
    }
    println!();
    println!(
        "  {} {}",
        "total:".cyan().bold(),
        format!(
            "{} secret{}",
            entries.len(),
            if entries.len() == 1 { "" } else { "s" }
        )
        .bold()
    );
    println!();
}

pub fn short_uuid(id: &str) -> String {
    id.chars().take(8).collect()
}

pub fn report_error(err: &DotLockError) {
    eprintln!("{} {}", "error:".red().bold(), err);
    if let Some(hint) = err.hint() {
        eprintln!("{} {}", "hint: ".cyan().bold(), hint);
    }
}