use std::ffi::OsString;
use std::path::{Path, PathBuf};
pub fn config_dir() -> Option<PathBuf> {
resolve_dir(
std::env::var_os("TMPRL_CONFIG_DIR"),
std::env::var_os("XDG_CONFIG_HOME"),
std::env::var_os("HOME"),
)
}
fn resolve_dir(
explicit: Option<OsString>,
xdg: Option<OsString>,
home: Option<OsString>,
) -> Option<PathBuf> {
if let Some(dir) = explicit {
return Some(PathBuf::from(dir));
}
if let Some(dir) = xdg {
return Some(PathBuf::from(dir).join("tmprl"));
}
home.map(|h| PathBuf::from(h).join(".config").join("tmprl"))
}
pub const CONFIG_FILES: [&str; 3] = ["config.toml", "keys.toml", "views.toml"];
pub fn describe_paths(explicit_temporal_config: Option<&str>) -> String {
let mut out = String::new();
match tmprl_client::config_file_in_use(explicit_temporal_config) {
None => out.push_str("profiles: nowhere (no HOME)\n"),
Some(path) => {
let mark = if path.is_file() { "present" } else { "absent" };
out.push_str(&format!("profiles: {} ({mark})\n", path.display()));
}
}
match config_dir() {
None => out.push_str(
"config: nowhere (neither TMPRL_CONFIG_DIR, XDG_CONFIG_HOME nor HOME is set)\n",
),
Some(dir) => {
out.push_str(&format!("config: {}\n", dir.display()));
for name in CONFIG_FILES {
let path = dir.join(name);
let mark = if path.is_file() { "present" } else { "absent" };
out.push_str(&format!(" {name:<12} {mark}\n"));
}
}
}
out.push_str(&format!("audit: {}\n", audit_path_display()));
out
}
fn audit_path_display() -> String {
match audit_dir() {
Ok(dir) => dir.join("audit.jsonl").display().to_string(),
Err(e) => e,
}
}
pub fn read(name: &str) -> Result<Option<String>, String> {
match config_dir() {
Some(dir) => read_from(&dir, name),
None => Ok(None),
}
}
fn read_from(dir: &Path, name: &str) -> Result<Option<String>, String> {
let path = dir.join(name);
match std::fs::read_to_string(&path) {
Ok(s) => Ok(Some(s)),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
Err(e) => Err(format!("could not read {}: {e}", path.display())),
}
}
fn audit_dir() -> Result<PathBuf, String> {
match std::env::var_os("XDG_STATE_HOME") {
Some(d) => Ok(PathBuf::from(d).join("tmprl")),
None => match std::env::var_os("HOME") {
Some(h) => Ok(PathBuf::from(h).join(".local").join("state").join("tmprl")),
None => Err("no HOME to write an audit log under".into()),
},
}
}
pub fn append_audit(line: &str) -> Result<(), String> {
use std::io::Write;
let dir = audit_dir()?;
std::fs::create_dir_all(&dir)
.map_err(|e| format!("could not create {}: {e}", dir.display()))?;
let path = dir.join("audit.jsonl");
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&path)
.map_err(|e| format!("could not open {}: {e}", path.display()))?;
writeln!(file, "{line}").map_err(|e| format!("could not write {}: {e}", path.display()))
}
#[cfg(test)]
mod tests {
use super::*;
fn os(s: &str) -> Option<OsString> {
Some(OsString::from(s))
}
#[test]
fn an_explicit_directory_wins() {
assert_eq!(
resolve_dir(os("/explicit"), os("/xdg"), os("/home/someone")),
Some(PathBuf::from("/explicit"))
);
}
#[test]
fn xdg_is_used_when_there_is_no_explicit_override() {
assert_eq!(
resolve_dir(None, os("/xdg"), os("/home/someone")),
Some(PathBuf::from("/xdg/tmprl"))
);
}
#[test]
fn home_is_the_fallback() {
assert_eq!(
resolve_dir(None, None, os("/home/someone")),
Some(PathBuf::from("/home/someone/.config/tmprl"))
);
}
#[test]
fn with_no_environment_at_all_there_is_no_config_dir() {
assert_eq!(resolve_dir(None, None, None), None);
}
#[test]
fn an_absent_file_is_not_an_error() {
let dir = PathBuf::from("/nonexistent-tmprl-config-dir");
assert_eq!(read_from(&dir, "keys.toml"), Ok(None));
}
#[test]
fn a_file_that_exists_is_read() {
let dir = std::env::temp_dir().join("tmprl-config-read-test");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("views.toml"), "# hello\n").unwrap();
assert_eq!(read_from(&dir, "views.toml"), Ok(Some("# hello\n".into())));
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn describe_paths_names_every_file_and_the_audit_log() {
let out = describe_paths(None);
for name in CONFIG_FILES {
assert!(out.contains(name), "{name} missing from:\n{out}");
}
assert!(out.contains("audit.jsonl"), "{out}");
assert!(out.contains("config:"), "{out}");
}
#[test]
fn describe_paths_marks_a_file_that_exists() {
let dir = std::env::temp_dir().join("tmprl-describe-test");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("config.toml"), "").unwrap();
assert!(dir.join("config.toml").is_file());
assert!(!dir.join("keys.toml").is_file());
let _ = std::fs::remove_dir_all(&dir);
}
}