Skip to main content

fur_cli/commands/
id.rs

1//! Who this machine writes as: `fur id`.
2//!
3//! Attribution, not authentication. There is no password, no verification, and
4//! no server-side identity — a person states who they are and the archive
5//! records it honestly. Institutional identity is a deployment concern.
6//!
7//! Resolution order, first hit wins:
8//!   1. `.fur/identity`                 — this project, this checkout
9//!   2. `$XDG_CONFIG_HOME/fur/identity` — this user, every project
10//!   3. `avatars.json` → `main`         — the pre-existing behaviour
11//!
12//! Rule 3 is why nothing breaks: a machine that never runs `fur id` resolves
13//! exactly as it did before this command existed.
14
15use std::fs;
16use std::path::{Path, PathBuf};
17
18use clap::Parser;
19use colored::*;
20use serde_json::json;
21
22use crate::avatars::{get_random_emoji_for_name, load_avatars, save_avatars};
23
24#[derive(Parser, Debug)]
25pub struct IdArgs {
26    /// Avatar to write as. Omit to show the current identity.
27    pub name: Option<String>,
28
29    /// Apply to this project only, instead of this user
30    #[arg(long)]
31    pub project: bool,
32
33    /// Remove the override
34    #[arg(long)]
35    pub clear: bool,
36}
37
38pub fn user_identity_path() -> Option<PathBuf> {
39    let base = std::env::var_os("XDG_CONFIG_HOME")
40        .map(PathBuf::from)
41        .or_else(|| std::env::var_os("HOME").map(|h| PathBuf::from(h).join(".config")))
42        .or_else(|| std::env::var_os("APPDATA").map(PathBuf::from))?;
43
44    Some(base.join("fur").join("identity"))
45}
46
47fn project_identity_path() -> PathBuf {
48    Path::new(".fur").join("identity")
49}
50
51fn read_identity(path: &Path) -> Option<String> {
52    let text = fs::read_to_string(path).ok()?;
53    let name = text.trim().to_string();
54    if name.is_empty() {
55        None
56    } else {
57        Some(name)
58    }
59}
60
61/// The avatar this machine writes as, and where that came from.
62pub fn current_identity() -> Option<(String, &'static str)> {
63    if let Some(name) = read_identity(&project_identity_path()) {
64        return Some((name, "proyecto (.fur/identity)"));
65    }
66    if let Some(name) = user_identity_path().as_deref().and_then(read_identity) {
67        return Some((name, "usuario (config)"));
68    }
69    load_avatars()
70        .get("main")
71        .and_then(|v| v.as_str())
72        .map(|name| (name.to_string(), "avatars.json (main)"))
73}
74
75pub fn run_id(args: IdArgs) {
76    if args.clear {
77        let path = if args.project {
78            project_identity_path()
79        } else {
80            match user_identity_path() {
81                Some(p) => p,
82                None => return eprintln!("❌ No config directory available."),
83            }
84        };
85        let _ = fs::remove_file(&path);
86        println!("✔ Identidad eliminada ({})", path.display());
87        return show();
88    }
89
90    let Some(name) = args.name else {
91        return show();
92    };
93
94    // Create the avatar rather than failing: stating who you are should not
95    // require a separate setup step.
96    let mut avatars = load_avatars();
97    if avatars.get(&name).is_none() {
98        let emoji = get_random_emoji_for_name(&name);
99        avatars[name.clone()] = json!(emoji);
100        save_avatars(&avatars);
101        println!("[OK] Avatar '{}' creado {}", name, emoji);
102    }
103
104    let path = if args.project {
105        project_identity_path()
106    } else {
107        match user_identity_path() {
108            Some(p) => p,
109            None => return eprintln!("❌ No config directory available."),
110        }
111    };
112
113    if let Some(parent) = path.parent() {
114        let _ = fs::create_dir_all(parent);
115    }
116
117    match fs::write(&path, format!("{}\n", name)) {
118        Ok(_) => println!(
119            "✔ Escribiendo como {} ({})",
120            name.bright_yellow().bold(),
121            path.display()
122        ),
123        Err(e) => eprintln!("❌ No se pudo escribir {}: {}", path.display(), e),
124    }
125}
126
127fn show() {
128    match current_identity() {
129        Some((name, source)) => println!(
130            "{} {}\n  {}",
131            "Escribiendo como".bright_black(),
132            name.bright_yellow().bold(),
133            source.bright_black()
134        ),
135        None => println!("Sin identidad. Usa `fur id <nombre>`."),
136    }
137}