use std::path::PathBuf;
use std::sync::Mutex;
use std::sync::atomic::{AtomicU8, Ordering};
enum PathOverride {
Unset,
Empty,
Set(PathBuf),
}
static HOME: Mutex<PathOverride> = Mutex::new(PathOverride::Unset);
static DB_PATH: Mutex<PathOverride> = Mutex::new(PathOverride::Unset);
const DEBUG_UNSET: u8 = 0;
const DEBUG_OFF: u8 = 1;
const DEBUG_ON: u8 = 2;
static DEBUG: AtomicU8 = AtomicU8::new(DEBUG_UNSET);
pub fn init_from_env() {
let home = std::env::var("TOKF_HOME")
.ok()
.filter(|s| !s.is_empty())
.map(PathBuf::from);
set_home(home);
let db = std::env::var("TOKF_DB_PATH").ok().map(PathBuf::from);
set_db_path(db);
let debug = std::env::var("TOKF_DEBUG")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true"));
set_debug(debug);
}
pub fn set_home(path: Option<PathBuf>) {
let mut guard = HOME
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = path.map_or(PathOverride::Empty, PathOverride::Set);
}
pub fn reset_home() {
let mut guard = HOME
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = PathOverride::Unset;
}
pub fn set_db_path(path: Option<PathBuf>) {
let mut guard = DB_PATH
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = path.map_or(PathOverride::Empty, PathOverride::Set);
}
pub fn reset_db_path() {
let mut guard = DB_PATH
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
*guard = PathOverride::Unset;
}
pub fn set_debug(enabled: bool) {
DEBUG.store(
if enabled { DEBUG_ON } else { DEBUG_OFF },
Ordering::Relaxed,
);
}
pub fn reset_debug() {
DEBUG.store(DEBUG_UNSET, Ordering::Relaxed);
}
pub fn debug_enabled() -> bool {
match DEBUG.load(Ordering::Relaxed) {
DEBUG_ON => true,
DEBUG_OFF => false,
_ => std::env::var("TOKF_DEBUG")
.ok()
.is_some_and(|v| v == "1" || v.eq_ignore_ascii_case("true")),
}
}
pub fn db_path_override() -> Option<PathBuf> {
let guard = DB_PATH
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &*guard {
PathOverride::Set(p) => return Some(p.clone()),
PathOverride::Empty => return None,
PathOverride::Unset => {}
}
drop(guard);
std::env::var("TOKF_DB_PATH").ok().map(PathBuf::from)
}
fn resolve_user_path(dirs_fallback: Option<PathBuf>) -> Option<PathBuf> {
let guard = HOME
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner);
match &*guard {
PathOverride::Set(p) => return Some(p.clone()),
PathOverride::Empty => return dirs_fallback,
PathOverride::Unset => {}
}
drop(guard);
if let Ok(home) = std::env::var("TOKF_HOME")
&& !home.is_empty()
{
return Some(PathBuf::from(home));
}
dirs_fallback
}
pub fn user_dir() -> Option<PathBuf> {
resolve_user_path(dirs::config_dir().map(|d| d.join("tokf")))
}
pub fn user_data_dir() -> Option<PathBuf> {
resolve_user_path(dirs::data_local_dir().map(|d| d.join("tokf")))
}
pub fn user_cache_dir() -> Option<PathBuf> {
resolve_user_path(dirs::cache_dir().map(|d| d.join("tokf")))
}
pub fn shims_dir() -> Option<PathBuf> {
user_cache_dir().map(|d| d.join("shims"))
}
#[cfg(any(test, feature = "test-keyring"))]
pub struct HomeGuard(());
#[cfg(any(test, feature = "test-keyring"))]
impl HomeGuard {
pub fn new(path: Option<PathBuf>) -> Self {
set_home(path);
Self(())
}
pub fn set(path: impl Into<PathBuf>) -> Self {
Self::new(Some(path.into()))
}
}
#[cfg(any(test, feature = "test-keyring"))]
impl Drop for HomeGuard {
fn drop(&mut self) {
reset_home();
}
}
#[cfg(any(test, feature = "test-keyring"))]
pub struct DbPathGuard(());
#[cfg(any(test, feature = "test-keyring"))]
impl DbPathGuard {
pub fn new(path: Option<PathBuf>) -> Self {
set_db_path(path);
Self(())
}
pub fn set(path: impl Into<PathBuf>) -> Self {
Self::new(Some(path.into()))
}
}
#[cfg(any(test, feature = "test-keyring"))]
impl Drop for DbPathGuard {
fn drop(&mut self) {
reset_db_path();
}
}
#[cfg(any(test, feature = "test-keyring"))]
pub struct DebugGuard(());
#[cfg(any(test, feature = "test-keyring"))]
impl DebugGuard {
pub fn new(enabled: bool) -> Self {
set_debug(enabled);
Self(())
}
}
#[cfg(any(test, feature = "test-keyring"))]
impl Drop for DebugGuard {
fn drop(&mut self) {
reset_debug();
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used)]
mod tests {
use serial_test::serial;
use super::*;
#[test]
#[serial]
fn user_dir_uses_tokf_home_when_set() {
let _guard = HomeGuard::set("/custom/tokf/home");
let result = user_dir();
assert_eq!(result, Some(PathBuf::from("/custom/tokf/home")));
}
#[test]
#[serial]
fn user_dir_ignores_empty_tokf_home() {
let _guard = HomeGuard::new(None);
let result = user_dir();
if let Some(p) = result {
assert_ne!(p, PathBuf::from(""));
}
}
#[test]
#[serial]
fn user_data_dir_uses_tokf_home_when_set() {
let _guard = HomeGuard::set("/custom/tokf/home");
let result = user_data_dir();
assert_eq!(result, Some(PathBuf::from("/custom/tokf/home")));
}
#[test]
#[serial]
fn user_cache_dir_uses_tokf_home_when_set() {
let _guard = HomeGuard::set("/custom/tokf/home");
let result = user_cache_dir();
assert_eq!(result, Some(PathBuf::from("/custom/tokf/home")));
}
#[test]
#[serial]
fn user_dir_fallback_matches_dirs_crate() {
let _guard = HomeGuard::new(None);
let via_paths = user_dir();
let via_dirs = dirs::config_dir().map(|d| d.join("tokf"));
assert_eq!(via_paths, via_dirs);
}
#[test]
#[serial]
fn all_three_dirs_agree_when_tokf_home_set() {
let _guard = HomeGuard::set("/unified/home");
let config = user_dir();
let data = user_data_dir();
let cache = user_cache_dir();
assert_eq!(config, data);
assert_eq!(data, cache);
assert_eq!(config, Some(PathBuf::from("/unified/home")));
}
#[test]
#[serial]
fn shims_dir_uses_tokf_home_when_set() {
let _guard = HomeGuard::set("/custom/tokf/home");
let result = shims_dir();
assert_eq!(result, Some(PathBuf::from("/custom/tokf/home/shims")));
}
#[test]
#[serial]
fn db_path_override_returns_set_value() {
let _guard = DbPathGuard::set("/custom/db.sqlite");
let result = db_path_override();
assert_eq!(result, Some(PathBuf::from("/custom/db.sqlite")));
}
#[test]
#[serial]
fn db_path_override_returns_none_when_cleared() {
let _guard = DbPathGuard::new(None);
let result = db_path_override();
assert!(result.is_none());
}
}