Vectrum
Sign In
114 lines · 3.9 KB · Rust
//! Preferences that outlive the app, kept in
//! `$XDG_CONFIG_HOME/vectrum-code/settings.json` and edited in the settings
//! view. They're read once and cached, so rendering can consult them freely.

use std::{path::PathBuf, sync::Mutex};

use serde::{Deserialize, Serialize};

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct Settings {
    /// `bone`, `auto` or `graphite`.
    pub colorway: Option<String>,
    /// Model keys (`claude:opus`, `pi:anthropic/claude-sonnet-5-5`), in the
    /// order they were starred.
    pub favorites: Vec<String>,
    /// The model key new threads start on. `None` follows the open thread.
    pub default_model: Option<String>,
    /// `default`, an effort level (`high`), `ultracode` or `ultrathink`.
    /// `None` follows the open thread.
    pub default_reasoning: Option<String>,
    /// A permission mode as the CLI spells it. `None` follows the open thread.
    pub default_access: Option<String>,
    /// The editor "Open in" uses, by name (`Zed`).
    pub editor: Option<String>,
    /// Fold a finished turn's tool calls into one "Worked for" row.
    pub fold_turns: bool,
    /// Show what threads and turns cost.
    pub show_costs: bool,
    /// One-line thread cards in the sidebar.
    pub compact_threads: bool,
    /// Send with Ctrl+Enter, so Enter starts a new line.
    pub send_with_ctrl_enter: bool,
    /// A desktop notification when a thread finishes or needs you while the
    /// window is in the background.
    pub notify: bool,
    /// Overrides the `claude` binary lookup (`VECTRUM_CODE_CLAUDE_BIN` still wins).
    pub claude_bin: Option<String>,
    /// Overrides the `pi` binary lookup (`VECTRUM_CODE_PI_BIN` still wins).
    pub pi_bin: Option<String>,
}

impl Default for Settings {
    fn default() -> Self {
        Self {
            colorway: None,
            favorites: Vec::new(),
            default_model: None,
            default_reasoning: None,
            default_access: None,
            editor: None,
            fold_turns: true,
            show_costs: true,
            compact_threads: false,
            send_with_ctrl_enter: false,
            notify: false,
            claude_bin: None,
            pi_bin: None,
        }
    }
}

static CACHE: Mutex<Option<Settings>> = Mutex::new(None);

pub fn path() -> Option<PathBuf> {
    let base = std::env::var_os("XDG_CONFIG_HOME")
        .map(PathBuf::from)
        .or_else(|| std::env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?;
    Some(base.join("vectrum-code").join("settings.json"))
}

fn read_file() -> Settings {
    path()
        .and_then(|path| std::fs::read_to_string(path).ok())
        .and_then(|text| serde_json::from_str(&text).ok())
        .unwrap_or_default()
}

/// Reads one value out of the settings.
pub fn get<T>(read: impl FnOnce(&Settings) -> T) -> T {
    let mut cache = CACHE.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
    read(cache.get_or_insert_with(read_file))
}

pub fn load() -> Settings {
    get(Settings::clone)
}

/// Applies `edit` to the settings and writes them back.
pub fn update(edit: impl FnOnce(&mut Settings)) {
    let settings = {
        let mut cache = CACHE.lock().unwrap_or_else(|poisoned| poisoned.into_inner());
        let settings = cache.get_or_insert_with(read_file);
        edit(settings);
        settings.clone()
    };
    let Some(path) = path() else { return };
    let written = path
        .parent()
        .map(std::fs::create_dir_all)
        .unwrap_or(Ok(()))
        .and_then(|_| std::fs::write(&path, serde_json::to_string_pretty(&settings).unwrap_or_default()));
    if let Err(err) = written {
        eprintln!("vectrum-code: couldn't save {}: {err}", path.display());
    }
}

/// Puts everything but favorites back to its default.
pub fn restore_defaults() {
    update(|settings| {
        let favorites = std::mem::take(&mut settings.favorites);
        *settings = Settings { favorites, ..Settings::default() };
    });
}