From bb4aa6a5a6a6c17a18aaeb4508d498a6f574aa98 Mon Sep 17 00:00:00 2001 From: uncenter <47499684+uncenter@users.noreply.github.com> Date: Tue, 10 Dec 2024 18:42:18 -0500 Subject: [PATCH] Support ~ and $HOME components in path option (#278) * Support ~ and $HOME components in path option * Clean up error handling, canonicalize after replacing components * Clean up error handling further --- src/config.rs | 30 +++++++++++++++++++++--------- 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/config.rs b/src/config.rs index 3381f43..575f9f2 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,9 +1,10 @@ //! This module contains the config specification and functionality for creating a config. +use anyhow::Result; use clap::ValueEnum; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use std::{env, fs, io}; +use std::path::{Path, PathBuf}; +use std::{env, fs}; /// This struct is the actual config type consumed through the codebase. It is boostrapped via its /// public methods and uses [`EntryConfig`], a private struct, under the hood in order to @@ -22,7 +23,7 @@ impl Config { /// This method tries to deserialize the config file (empty, non-existent, partial or complete) /// and uses [`EntryConfig`] as an intermediary struct. This is the primary method used when /// creating a config. - pub fn try_config() -> anyhow::Result { + pub fn try_config() -> Result { // Within this method, we check if the config file is empty before deserializing it. Users // should be able to proceed with empty config files. If empty or not found, then we fall // back to the "EntryConfig" default before conversion. @@ -37,7 +38,7 @@ impl Config { let path = match paths.into_iter().find(|p| p.exists()) { Some(path) => path, - None => return Ok(Self::try_config_default()?), + None => return Self::try_config_default(), }; let contents = fs::read_to_string(path)?; @@ -46,25 +47,25 @@ impl Config { } else { toml::from_str(&contents)? }; - Ok(Self::from_entry_config(&entry_config)?) + Self::from_entry_config(&entry_config) } /// This method does not look for the config file and uses [`EntryConfig`]'s defaults instead. /// Use this method when the user wishes to skip config file lookup. - pub fn try_config_default() -> io::Result { + pub fn try_config_default() -> Result { Self::from_entry_config(&EntryConfig::default()) } /// This method prints the full config (merged with config file, as needed) as valid, pretty TOML. - pub fn print(self) -> std::result::Result<(), toml::ser::Error> { + pub fn print(self) -> Result<(), toml::ser::Error> { print!("{}", toml::to_string_pretty(&self)?); Ok(()) } - fn from_entry_config(entry_config: &EntryConfig) -> io::Result { + fn from_entry_config(entry_config: &EntryConfig) -> Result { Ok(Config { path: match &entry_config.path { - Some(path) => path.clone(), + Some(path) => normalize_path(path)?, None => env::current_dir()?.canonicalize()?, }, display_mode: match &entry_config.display_mode { @@ -79,6 +80,17 @@ impl Config { } } +fn normalize_path(path: &Path) -> Result { + Ok(match path + .strip_prefix("~") + .or_else(|_| path.strip_prefix("$HOME")) + { + Ok(stripped) => user_dirs::home_dir()?.join(stripped), + Err(_) => path.to_path_buf(), + } + .canonicalize()?) +} + /// This struct is a reflection of [`Config`] with its fields wrapped with [`Option`], which /// ensures that we can deserialize from partial config file contents and populate empty fields /// with defaults. Moreover, enum fields cannot set defaults values currently, so we need to