use super::theme::Theme;
use serde::{Deserialize, Serialize};
use std::{
env, fs,
path::{Path, PathBuf},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default)]
pub struct Look {
pub theme: Theme,
pub icons: bool,
pub keep_going: bool,
pub snapshots: bool,
pub set_up: bool,
}
impl Default for Look {
fn default() -> Self {
Self {
theme: Theme::Solid,
icons: true,
keep_going: false,
snapshots: true,
set_up: false,
}
}
}
pub fn path() -> Option<PathBuf> {
let base = env::var_os("XDG_CONFIG_HOME")
.map(PathBuf::from)
.filter(|path| path.is_absolute())
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".config")))?;
Some(base.join("helix-toolbox/settings.toml"))
}
pub fn load(path: &Path) -> Look {
let Some(table) = fs::read_to_string(path)
.ok()
.and_then(|text| text.parse::<toml::Table>().ok())
else {
return Look::default();
};
let mut look = Look {
set_up: true,
..Look::default()
};
let get = |name: &str| table.get(name).cloned();
if let Some(theme) = get("theme").and_then(|value| value.try_into().ok()) {
look.theme = theme;
}
for (name, field) in [
("icons", &mut look.icons),
("keep_going", &mut look.keep_going),
("snapshots", &mut look.snapshots),
("set_up", &mut look.set_up),
] {
if let Some(value) = get(name).and_then(|value| value.as_bool()) {
*field = value;
}
}
look
}
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(default)]
struct Favorites {
entries: Vec<String>,
}
const OLD_FAVORITES: &str = "favourites.toml";
pub fn favorites_path(settings: &Path) -> PathBuf {
settings.with_file_name("favorites.toml")
}
pub fn load_favorites(path: &Path) -> Vec<String> {
let old = path.with_file_name(OLD_FAVORITES);
let path = match (modified(&old), modified(path)) {
(Some(old_time), Some(time)) if old_time > time => &old,
(Some(_), None) => &old,
_ => path,
};
fs::read_to_string(path)
.ok()
.and_then(|text| toml::from_str::<Favorites>(&text).ok())
.map(|favorites| favorites.entries)
.unwrap_or_default()
}
fn modified(path: &Path) -> Option<std::time::SystemTime> {
fs::symlink_metadata(path)
.ok()
.filter(|metadata| metadata.is_file())
.and_then(|metadata| metadata.modified().ok())
}
pub fn save_favorites(path: &Path, entries: &[String]) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let text = toml::to_string(&Favorites {
entries: entries.to_vec(),
})
.map_err(std::io::Error::other)?;
let staging = path.with_extension("toml.new");
fs::write(
&staging,
format!("# Entries pinned in Helix Toolbox with *.\n{text}"),
)?;
fs::rename(&staging, path)?;
let old = path.with_file_name(OLD_FAVORITES);
if fs::symlink_metadata(&old).is_ok() {
let link = path.with_extension("toml.link");
let _ = fs::remove_file(&link);
std::os::unix::fs::symlink(path.file_name().unwrap_or_default(), &link)?;
fs::rename(&link, &old)?;
}
Ok(())
}
pub fn save(path: &Path, look: &Look) -> std::io::Result<()> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)?;
}
let text = toml::to_string(look).map_err(std::io::Error::other)?;
let staging = path.with_extension("toml.new");
fs::write(
&staging,
format!("# Helix Toolbox interface settings; Settings in the menu changes them.\n{text}"),
)?;
fs::rename(&staging, path)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn favorites_move_from_the_old_file_and_leave_a_link() {
let directory = tempfile::tempdir().unwrap();
let path = favorites_path(&directory.path().join("settings.toml"));
let old = directory.path().join(OLD_FAVORITES);
assert!(load_favorites(&path).is_empty());
fs::write(&old, "entries = [\"ripgrep\"]\n").unwrap();
assert_eq!(load_favorites(&path), ["ripgrep"]);
save_favorites(&path, &["ripgrep".into(), "fzf".into()]).unwrap();
assert!(fs::symlink_metadata(&old).unwrap().is_symlink());
assert_eq!(
fs::read_to_string(&old).unwrap(),
fs::read_to_string(&path).unwrap()
);
assert_eq!(load_favorites(&path), ["ripgrep", "fzf"]);
fs::remove_file(&old).unwrap();
std::thread::sleep(std::time::Duration::from_millis(20));
fs::write(&old, "entries = [\"bat\"]\n").unwrap();
assert_eq!(load_favorites(&path), ["bat"]);
let fresh = tempfile::tempdir().unwrap();
let path = favorites_path(&fresh.path().join("settings.toml"));
save_favorites(&path, &["fzf".into()]).unwrap();
assert!(!fresh.path().join(OLD_FAVORITES).exists());
}
#[test]
fn the_look_round_trips_and_falls_back_to_the_default() {
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("helix-toolbox/settings.toml");
assert_eq!(load(&path), Look::default());
assert_eq!(Look::default().theme, Theme::Solid);
let look = Look {
theme: Theme::Transparent,
icons: false,
keep_going: true,
snapshots: false,
set_up: false,
};
save(&path, &look).unwrap();
assert_eq!(load(&path), look);
assert!(
fs::read_to_string(&path)
.unwrap()
.contains("theme = \"transparent\"")
);
fs::write(&path, "theme = \"default\"\n").unwrap();
assert_eq!(load(&path).theme, Theme::Transparent);
fs::write(&path, "theme = 3\n").unwrap();
assert_eq!(
load(&path),
Look {
set_up: true,
..Look::default()
}
);
assert!(
!Look::default().set_up,
"with no settings at all, it is the first run"
);
fs::write(
&path,
"theme = \"aurora\"\nicons = false\nanimations = true\n[future]\nx = 1\n",
)
.unwrap();
assert_eq!(
load(&path),
Look {
icons: false,
set_up: true,
..Look::default()
}
);
}
}