///! This module provides methods to handle the program's config files and operations related to ///! this. use crate::flags::color::ColorOption; use crate::flags::display::Display; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::print_error; use std::path::PathBuf; use serde::Deserialize; use std::fs; const CONF_DIR: &str = "lsd"; const CONF_FILE_NAME: &str = "config"; const YAML_LONG_EXT: &str = "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 sorting: Option, pub no_symlink: Option, pub total_size: Option, pub symlink_arrow: Option, } #[derive(Eq, PartialEq, Debug, Deserialize)] pub struct Color { pub when: ColorOption, } #[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, sorting: None, no_symlink: None, total_size: None, symlink_arrow: None, } } /// This constructs a Config struct with a passed file path [String]. pub fn from_file(file: String) -> Option { 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, e); None } }, Err(e) => { match e.kind() { std::io::ErrorKind::NotFound => {} _ => print_error!("Can not open config file {}: {}.", &file, 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))] fn config_file_path() -> Option { use xdg::BaseDirectories; match BaseDirectories::with_prefix(CONF_DIR) { Ok(p) => { if let Ok(p) = p.place_config_file([CONF_FILE_NAME, YAML_LONG_EXT].join(".")) { return Some(p); } } 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)] fn config_file_path() -> Option { if let Some(p) = dirs::config_dir() { return Some( p.join(CONF_DIR) .join(CONF_FILE_NAME) .with_extension(YAML_LONG_EXT), ); } None } } impl Default for Config { fn default() -> Self { if let Some(p) = Self::config_file_path() { if let Some(c) = Self::from_file(p.to_string_lossy().to_string()) { 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, size, size_value, 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 # == 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 # == 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 # == 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; use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::layout::Layout; use crate::flags::size::SizeFlag; use crate::flags::sorting::{DirGrouping, SortColumn}; #[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(), ] .into() ), color: Some(config_file::Color { when: ColorOption::Auto, }), 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), 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()), }, 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".to_string()); assert!(c.is_none()) } #[test] fn test_read_bad_display() { assert!(Config::from_yaml("display: bad").is_err()) } }