Files
lsd/src/config_file.rs
2025-12-09 10:37:22 +08:00

463 lines
14 KiB
Rust

//! This module provides methods to handle the program's config files and
//! operations related to this.
use crate::flags::HyperlinkOption;
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::{ColorOption, ThemeOption};
use crate::print_error;
use std::path::{Path, PathBuf};
use serde::Deserialize;
use std::fs;
use std::io;
/// 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<bool>,
pub blocks: Option<Vec<String>>,
pub color: Option<Color>,
pub date: Option<String>,
pub dereference: Option<bool>,
pub display: Option<Display>,
pub icons: Option<Icons>,
pub ignore_globs: Option<Vec<String>>,
pub indicators: Option<bool>,
pub layout: Option<Layout>,
pub recursion: Option<Recursion>,
pub size: Option<SizeFlag>,
pub permission: Option<PermissionFlag>,
pub sorting: Option<Sorting>,
pub no_symlink: Option<bool>,
pub total_size: Option<bool>,
pub symlink_arrow: Option<String>,
pub hyperlink: Option<HyperlinkOption>,
pub header: Option<bool>,
pub literal: Option<bool>,
pub truncate_owner: Option<TruncateOwner>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct Color {
pub when: Option<ColorOption>,
pub theme: Option<ThemeOption>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct Icons {
pub when: Option<IconOption>,
pub theme: Option<IconTheme>,
pub separator: Option<String>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct Recursion {
pub enabled: Option<bool>,
pub depth: Option<usize>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Sorting {
pub column: Option<SortColumn>,
pub reverse: Option<bool>,
pub dir_grouping: Option<DirGrouping>,
}
#[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct TruncateOwner {
pub after: Option<usize>,
pub marker: Option<String>,
}
/// 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<P: AsRef<Path>>(path: P) -> Option<PathBuf> {
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 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,
literal: None,
truncate_owner: None,
}
}
/// This constructs a Config struct with a passed file path.
pub fn from_file<P: AsRef<Path>>(file: P) -> Option<Self> {
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<Self, serde_yaml::Error> {
serde_yaml::from_str::<Self>(yaml)
}
/// Config paths for non-Windows platforms will be read from
/// `$XDG_CONFIG_HOME/lsd` or `$HOME/.config/lsd`
/// (usually, those are the same) in that order.
/// The default paths for Windows will be read from
/// `%APPDATA%\lsd` or `%USERPROFILE%\.config\lsd` in that order.
/// This will apply both to the config file and the theme file.
pub fn config_paths() -> impl Iterator<Item = PathBuf> {
#[cfg(not(windows))]
use xdg::BaseDirectories;
[
dirs::home_dir().map(|h| h.join(".config")),
dirs::config_dir(),
#[cfg(not(windows))]
BaseDirectories::with_prefix("")
.ok()
.map(|p| p.get_config_home()),
]
.iter()
.filter_map(|p| p.as_ref().map(|p| p.join("lsd")))
.collect::<Vec<_>>()
.into_iter()
}
}
impl Default for Config {
/// Try to find either config.yaml or config.yml in the config directories
/// and use the first one that is found. If none are found, or the parsing fails,
/// use the default from DEFAULT_CONFIG.
fn default() -> Self {
Config::config_paths()
.find_map(|p| {
let yaml = p.join("config.yaml");
let yml = p.join("config.yml");
if yaml.is_file() {
Config::from_file(yaml)
} else if yml.is_file() {
Config::from_file(yml)
} else {
None
}
})
.or(Self::from_yaml(DEFAULT_CONFIG).ok())
.expect("Failed to read both config file and default config")
}
}
pub 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, links, git
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, custom
# When "custom" is set, lsd will look in the config directory for `colors.yaml`.
theme: default
# == Date ==
# This specifies the date format for the date column. The freeform format
# accepts a strftime like string.
# When "classic" is set, this is set to "date".
# Possible values: date, locale, relative, '+<date_format>'
# `date_format` will be a `strftime` formatted value. e.g. `date: '+%d %b %y %X'` will give you a date like this: 17 Jun 21 20:14:55
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
# Separator between icon and the name
# Default to 1 space
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, attributes (windows only), disable
# 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 ==
# Attach hyperlink to filenames
# Possible values: always, auto, never
hyperlink: never
# == Symlink arrow ==
# Specifies how the symlink arrow display, chars in both ascii and utf8
symlink-arrow: ⇒
# == Header ==
# Whether to display block headers.
# Possible values: false, true
header: false
# == Literal ==
# Whether to show quotes on filenames.
# Possible values: false, true
literal: false
# == Truncate owner ==
# How to truncate the username and group names for a file if they exceed a certain
# number of characters.
truncate-owner:
# Number of characters to keep. By default, no truncation is done (empty value).
after:
# String to be appended to a name if truncated.
marker: ""
"#;
#[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::HyperlinkOption;
use crate::flags::color::{ColorOption, ThemeOption};
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(),
]),
color: Some(config_file::Color {
when: Some(ColorOption::Auto),
theme: Some(ThemeOption::Default)
}),
date: Some("date".to_string()),
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: None,
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: Some(false),
literal: Some(false),
truncate_owner: Some(config_file::TruncateOwner {
after: None,
marker: Some("".to_string()),
}),
},
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())
}
}