fur-cli 2.0.0

Turn your AI chats into a durable, local-first diary. Save messages, attach notes, organize conversations, and stop losing context every time the model forgets you exist.
Documentation
use serde_json::Value;
use std::collections::HashMap;
use std::fs;
use std::path::Path;

use crate::avatars::emojis::pick_emoji;
use crate::avatars::{get_random_emoji_for_name, load_avatars, save_avatars, MAIN_EMOJI};
use crate::commands::utils::input::{ask_string, ask_yes_no, default_yes};
use crate::renderer::table::render_table;
use colored::*;
use serde_json::json;

pub fn count_messages_per_avatar() -> HashMap<String, usize> {
    let mut counts = HashMap::new();
    let messages_dir = Path::new(".fur/messages");

    if !messages_dir.exists() {
        return counts;
    }

    if let Ok(entries) = fs::read_dir(messages_dir) {
        for entry in entries.flatten() {
            if let Ok(content) = fs::read_to_string(entry.path()) {
                if let Ok(json) = serde_json::from_str::<Value>(&content) {
                    if let Some(avatar) = json["avatar"].as_str() {
                        *counts.entry(avatar.to_string()).or_insert(0) += 1;
                    }
                }
            }
        }
    }

    counts
}

pub fn run_avatar_view() {
    let avatars = load_avatars();

    if let Some(map) = avatars.as_object() {
        if map.is_empty() {
            println!("(no avatars yet)");
            return;
        }

        let mut rows = Vec::new();
        let mut active_idx = None;

        let msg_counts = count_messages_per_avatar();

        for (i, (name, val)) in map.iter().enumerate() {
            if name == "main" {
                if let Some(target) = val.as_str() {
                    let count = msg_counts.get(target).copied().unwrap_or(0);
                    rows.push(vec![
                        "⭐ main".to_string(),
                        target.to_string(),
                        count.to_string(),
                    ]);
                    active_idx = Some(i);
                }
            } else {
                let emoji = val.as_str().unwrap_or("🐾");
                let count = msg_counts.get(name).copied().unwrap_or(0);
                rows.push(vec![name.to_string(), emoji.to_string(), count.to_string()]);
            }
        }

        render_table("Avatars", &["Role", "Emoji", "Messages"], rows, active_idx);
    }
}

pub fn run_avatar_onboarding() {
    let mut avatars = load_avatars();

    println!("\n{}", "== Create Avatar ==".bright_magenta().bold());
    println!(
        "{}",
        "A secondary avatar is anyone who isn’t you (the main user).\n\
         Examples: an AI, your boss, your therapist, or your cat. \n\
         If you choose [n], you’re creating or replacing the *main* avatar."
            .bright_cyan()
    );

    let is_secondary = ask_yes_no("Secondary avatar? [Y/n]: ", default_yes);

    if is_secondary {
        create_secondary_avatar(&mut avatars);
    } else {
        create_main_avatar(&mut avatars);
    }

    save_avatars(&avatars);
    println!("✅ Avatar creation complete. Use `fur avatar --view` to list all avatars.");
}

fn create_main_avatar(avatars: &mut serde_json::Value) {
    let name = ask_string("Main avatar name [me]: ", Some("me"));

    avatars["main"] = json!(name);
    avatars[name.clone()] = json!(MAIN_EMOJI);
    println!("[OK] Main avatar set: {}", name);
}

fn create_secondary_avatar(avatars: &mut serde_json::Value) {
    let name = ask_string("Choose name [ai]: ", Some("ai"));

    let suggested = get_random_emoji_for_name(&name);

    let skip = ask_yes_no(
        &format!("Use suggested emoji {}? [Y/n]: ", suggested),
        default_yes,
    );

    // viceroy: the picker moved to `avatars::emojis::pick_emoji` so onboarding
    // can use the same one instead of a hardcoded lookup table.
    let emoji = if skip {
        suggested
    } else {
        pick_emoji("Your choice: ", None)
    };

    avatars[name.clone()] = json!(emoji);

    println!(
        "[OK] Other avatar '{}' created with emoji '{}'",
        name, emoji
    );
}