use crate::flags::display::Display; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::permission::PermissionFlag; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::flags::HyperlinkOption; use crate::flags::{ColorOption, ThemeOption}; ///! This module provides methods to handle the program's config files and operations related to ///! this. use crate::print_error; use std::path::{Path, PathBuf}; use serde::Deserialize; use std::fs; use std::io; const CONF_DIR: &str = "lsd"; const CONF_FILE_NAME: &str = "config.yaml"; /// A struct to hold an optional configuration items, and provides methods /// around error handling in a config file. #[derive(Eq, PartialEq, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] #[serde(deny_unknown_fields)] pub struct Config { pub classic: Option, pub blocks: Option>, pub color: Option, pub date: Option, pub dereference: Option, pub display: Option, pub icons: Option, pub ignore_globs: Option>, pub indicators: Option, pub layout: Option, pub recursion: Option, pub size: Option, pub permission: Option, pub sorting: Option, pub no_symlink: Option, pub total_size: Option, pub symlink_arrow: Option, pub hyperlink: Option, pub header: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Color { pub when: Option, pub theme: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Icons { pub when: Option, pub theme: Option, pub separator: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Recursion { pub enabled: Option, pub depth: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] #[serde(rename_all = "kebab-case")] pub struct Sorting { pub column: Option, pub reverse: Option, pub dir_grouping: Option, } impl Config { /// This constructs a Config struct with all None pub fn with_none() -> Self { Self { classic: None, blocks: None, color: None, date: None, dereference: None, display: None, icons: None, ignore_globs: None, indicators: None, layout: None, recursion: None, size: None, permission: None, sorting: None, no_symlink: None, total_size: None, symlink_arrow: None, hyperlink: None, header: None, } } /// This constructs a Config struct with a passed file path. pub fn from_file>(file: P) -> Option { let file = file.as_ref(); match fs::read(file) { Ok(f) => match Self::from_yaml(&String::from_utf8_lossy(&f)) { Ok(c) => Some(c), Err(e) => { print_error!( "Configuration file {} format error, {}.", file.to_string_lossy(), e ); None } }, Err(e) => { if e.kind() != io::ErrorKind::NotFound { print_error!( "Can not open config file {}: {}.", file.to_string_lossy(), e ); } None } } } /// This constructs a Config struct with a passed [Yaml] str. /// If error happened, return the [serde_yaml::Error]. fn from_yaml(yaml: &str) -> Result { serde_yaml::from_str::(yaml) } /// This provides the path for a configuration file, according to the XDG_BASE_DIRS specification. /// return None if error like PermissionDenied #[cfg(not(windows))] pub fn config_file_path() -> Option { use xdg::BaseDirectories; match BaseDirectories::with_prefix(CONF_DIR) { Ok(p) => Some(p.get_config_home()), Err(e) => { print_error!("Can not open config file: {}.", e); None } } } /// This provides the path for a configuration file, inside the %APPDATA% directory. /// return None if error like PermissionDenied #[cfg(windows)] pub fn config_file_path() -> Option { dirs::config_dir().map(|x| x.join(CONF_DIR)) } /// This expand the `~` in path to HOME dir /// returns the origin one if no `~` found; /// returns None if error happened when getting home dir /// /// Implementing this to reuse the `dirs` dependency, avoid adding new one pub fn expand_home>(path: P) -> Option { let p = path.as_ref(); if !p.starts_with("~") { return Some(p.to_path_buf()); } if p == Path::new("~") { return dirs::home_dir(); } dirs::home_dir().map(|mut h| { if h == Path::new("/") { // Corner case: `h` root directory; // don't prepend extra `/`, just drop the tilde. p.strip_prefix("~").unwrap().to_path_buf() } else { h.push(p.strip_prefix("~/").unwrap()); h } }) } } impl Default for Config { fn default() -> Self { if let Some(p) = Self::config_file_path() { if let Some(c) = Self::from_file(p.join(CONF_FILE_NAME)) { return c; } } Self::from_yaml(DEFAULT_CONFIG).unwrap() } } const DEFAULT_CONFIG: &str = r#"--- # == Classic == # This is a shorthand to override some of the options to be backwards compatible # with `ls`. It affects the "color"->"when", "sorting"->"dir-grouping", "date" # and "icons"->"when" options. # Possible values: false, true classic: false # == Blocks == # This specifies the columns and their order when using the long and the tree # layout. # Possible values: permission, user, group, context, size, date, name, inode blocks: - permission - user - group - size - date - name # == Color == # This has various color options. (Will be expanded in the future.) color: # When to colorize the output. # When "classic" is set, this is set to "never". # Possible values: never, auto, always when: auto # How to colorize the output. # When "classic" is set, this is set to "no-color". # Possible values: default, no-color, no-lscolors, # when specifying , lsd will look up theme file in # XDG Base Directory if relative # The file path if absolute theme: default # == Date == # This specifies the date format for the date column. The freeform format # accepts an strftime like string. # When "classic" is set, this is set to "date". # Possible values: date, relative, + # date: date # == Dereference == # Whether to dereference symbolic links. # Possible values: false, true dereference: false # == Display == # What items to display. Do not specify this for the default behavior. # Possible values: all, almost-all, directory-only # display: all # == Icons == icons: # When to use icons. # When "classic" is set, this is set to "never". # Possible values: always, auto, never when: auto # Which icon theme to use. # Possible values: fancy, unicode theme: fancy # The string between the icons and the name. # Possible values: any string (eg: " |") separator: " " # == Ignore Globs == # A list of globs to ignore when listing. # ignore-globs: # - .git # == Indicators == # Whether to add indicator characters to certain listed files. # Possible values: false, true indicators: false # == Layout == # Which layout to use. "oneline" might be a bit confusing here and should be # called "one-per-line". It might be changed in the future. # Possible values: grid, tree, oneline layout: grid # == Recursion == recursion: # Whether to enable recursion. # Possible values: false, true enabled: false # How deep the recursion should go. This has to be a positive integer. Leave # it unspecified for (virtually) infinite. # depth: 3 # == Size == # Specifies the format of the size column. # Possible values: default, short, bytes size: default # == Permission == # Specify the format of the permission column. # Possible value: rwx, octal permission: rwx # == Sorting == sorting: # Specify what to sort by. # Possible values: extension, name, time, size, version column: name # Whether to reverse the sorting. # Possible values: false, true reverse: false # Whether to group directories together and where. # When "classic" is set, this is set to "none". # Possible values: first, last, none dir-grouping: none # == No Symlink == # Whether to omit showing symlink targets # Possible values: false, true no-symlink: false # == Total size == # Whether to display the total size of directories. # Possible values: false, true total-size: false # == Hyperlink == # Whether to display the total size of directories. # Possible values: always, auto, never hyperlink: never # == Symlink arrow == # Specifies how the symlink arrow display, chars in both ascii and utf8 symlink-arrow: ⇒ "#; #[cfg(test)] impl Config { pub fn builtin() -> Self { Self::from_yaml(DEFAULT_CONFIG).unwrap() } } #[cfg(test)] mod tests { use super::Config; use crate::config_file; use crate::flags::color::{ColorOption, ThemeOption}; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::permission::PermissionFlag; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::flags::HyperlinkOption; #[test] fn test_read_default() { let c = Config::from_yaml(config_file::DEFAULT_CONFIG).unwrap(); assert_eq!( Config { classic: Some(false), blocks: Some(vec![ "permission".into(), "user".into(), "group".into(), "size".into(), "date".into(), "name".into(), ]), color: Some(config_file::Color { when: Some(ColorOption::Auto), theme: Some(ThemeOption::Default) }), date: None, dereference: Some(false), display: None, icons: Some(config_file::Icons { when: Some(IconOption::Auto), theme: Some(IconTheme::Fancy), separator: Some(" ".to_string()), }), ignore_globs: None, indicators: Some(false), layout: Some(Layout::Grid), recursion: Some(config_file::Recursion { enabled: Some(false), depth: None, }), size: Some(SizeFlag::Default), permission: Some(PermissionFlag::Rwx), sorting: Some(config_file::Sorting { column: Some(SortColumn::Name), reverse: Some(false), dir_grouping: Some(DirGrouping::None), }), no_symlink: Some(false), total_size: Some(false), symlink_arrow: Some("⇒".into()), hyperlink: Some(HyperlinkOption::Never), header: None }, c ); } #[test] fn test_read_config_ok() { let c = Config::from_yaml("classic: true").unwrap(); assert!(c.classic.unwrap()) } #[test] fn test_read_config_bad_bool() { let c = Config::from_yaml("classic: notbool"); assert!(c.is_err()) } #[test] fn test_read_config_file_not_found() { let c = Config::from_file("not-existed"); assert!(c.is_none()) } #[test] fn test_read_bad_display() { assert!(Config::from_yaml("display: bad").is_err()) } }