//! 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, /// Model keys (`claude:opus`, `pi:anthropic/claude-sonnet-5-5`), in the /// order they were starred. pub favorites: Vec, /// The model key new threads start on. `None` follows the open thread. pub default_model: Option, /// `default`, an effort level (`high`), `ultracode` or `ultrathink`. /// `None` follows the open thread. pub default_reasoning: Option, /// A permission mode as the CLI spells it. `None` follows the open thread. pub default_access: Option, /// The editor "Open in" uses, by name (`Zed`). pub editor: Option, /// 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, /// Overrides the `pi` binary lookup (`VECTRUM_CODE_PI_BIN` still wins). pub pi_bin: Option, } 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> = Mutex::new(None); pub fn path() -> Option { 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(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() }; }); }