//! 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() };
});
}