color: add parse theme file

Signed-off-by: zwPapEr <zw.paper@gmail.com>
This commit is contained in:
zwPapEr 2020-11-25 15:52:54 +08:00 committed by Abin Simon
parent ec77d91952
commit b553d07faa
8 changed files with 374 additions and 123 deletions

1
Cargo.lock generated
View file

@ -24,6 +24,7 @@ version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
dependencies = [ dependencies = [
"serde",
"winapi", "winapi",
] ]

View file

@ -20,7 +20,7 @@ clap = "2.33.*"
version_check = "0.9.*" version_check = "0.9.*"
[dependencies] [dependencies]
ansi_term = "0.12.*" ansi_term = { version = "0.12.*", features = ["derive_serde_style"] }
dirs = "3.0.*" dirs = "3.0.*"
libc = "0.2.*" libc = "0.2.*"
human-sort = "0.2.2" human-sort = "0.2.2"

View file

@ -1,6 +1,11 @@
mod theme;
use theme::Theme;
use crate::flags::color::ThemeOption;
use ansi_term::{ANSIString, Colour, Style}; use ansi_term::{ANSIString, Colour, Style};
use lscolors::{Indicator, LsColors}; use lscolors::{Indicator, LsColors};
use std::collections::HashMap;
use std::path::Path; use std::path::Path;
#[allow(dead_code)] #[allow(dead_code)]
@ -60,37 +65,81 @@ impl Elem {
pub fn has_suid(&self) -> bool { pub fn has_suid(&self) -> bool {
matches!(self, Elem::Dir { uid: true } | Elem::File { uid: true, .. }) matches!(self, Elem::Dir { uid: true } | Elem::File { uid: true, .. })
} }
pub fn get_color(&self, theme: &theme::Theme) -> Colour {
match self {
Elem::File {
exec: true,
uid: true,
} => theme.file_type.file.exec_uid,
Elem::File {
exec: false,
uid: true,
} => theme.file_type.file.uid_no_exec,
Elem::File {
exec: true,
uid: false,
} => theme.file_type.file.exec_no_uid,
Elem::File {
exec: false,
uid: false,
} => theme.file_type.file.no_exec_no_uid,
Elem::SymLink => theme.file_type.symlink.default,
Elem::BrokenSymLink => theme.file_type.symlink.broken,
Elem::Dir { uid: true } => theme.file_type.dir.uid,
Elem::Dir { uid: false } => theme.file_type.dir.no_uid,
Elem::Pipe => theme.file_type.pipe,
Elem::BlockDevice => theme.file_type.block_device,
Elem::CharDevice => theme.file_type.char_device,
Elem::Socket => theme.file_type.socket,
Elem::Special => theme.file_type.special,
Elem::Read => theme.permissions.read,
Elem::Write => theme.permissions.write,
Elem::Exec => theme.permissions.exec,
Elem::ExecSticky => theme.permissions.exec_sticky,
Elem::NoAccess => theme.permissions.no_access,
Elem::DayOld => theme.modified.day_old,
Elem::HourOld => theme.modified.hour_old,
Elem::Older => theme.modified.older,
Elem::User => theme.user,
Elem::Group => theme.group,
Elem::NonFile => theme.size.none,
Elem::FileLarge => theme.size.large,
Elem::FileMedium => theme.size.medium,
Elem::FileSmall => theme.size.small,
Elem::INode { valid: false } => theme.inode.valid,
Elem::INode { valid: true } => theme.inode.invalid,
}
}
} }
pub type ColoredString<'a> = ANSIString<'a>; pub type ColoredString<'a> = ANSIString<'a>;
#[allow(dead_code)]
#[derive(Debug, Copy, Clone)]
pub enum Theme {
NoColor,
Default,
NoLscolors,
}
pub struct Colors { pub struct Colors {
colors: Option<HashMap<Elem, Colour>>, theme: Option<Theme>,
lscolors: Option<LsColors>, lscolors: Option<LsColors>,
} }
impl Colors { impl Colors {
pub fn new(theme: Theme) -> Self { pub fn new(t: ThemeOption) -> Self {
let colors = match theme { let theme = match t {
Theme::NoColor => None, ThemeOption::NoColor => None,
Theme::Default => Some(Self::get_light_theme_colour_map()), ThemeOption::Default => Some(Theme::default_dark()),
Theme::NoLscolors => Some(Self::get_light_theme_colour_map()), ThemeOption::NoLscolors => Some(Theme::default_dark()),
ThemeOption::Custom(ref file) => {
Some(Theme::from_path(file).unwrap_or_else(Theme::default_dark))
}
}; };
let lscolors = match theme { let lscolors = match t {
Theme::NoColor => None, ThemeOption::Default => Some(LsColors::from_env().unwrap_or_default()),
Theme::Default => Some(LsColors::from_env().unwrap_or_default()), _ => None,
Theme::NoLscolors => None,
}; };
Self { colors, lscolors } Self { theme, lscolors }
} }
pub fn colorize<'a>(&self, input: String, elem: &Elem) -> ColoredString<'a> { pub fn colorize<'a>(&self, input: String, elem: &Elem) -> ColoredString<'a> {
@ -135,8 +184,8 @@ impl Colors {
} }
fn style_default(&self, elem: &Elem) -> Style { fn style_default(&self, elem: &Elem) -> Style {
if let Some(ref colors) = self.colors { if let Some(t) = &self.theme {
let style_fg = Style::default().fg(colors[elem]); let style_fg = Style::default().fg(elem.get_color(&t));
if elem.has_suid() { if elem.has_suid() {
style_fg.on(Colour::Fixed(124)) // Red3 style_fg.on(Colour::Fixed(124)) // Red3
} else { } else {
@ -183,81 +232,4 @@ impl Colors {
None => None, None => None,
} }
} }
// You can find the table for each color, code, and display at:
//
//https://jonasjacek.github.io/colors/
fn get_light_theme_colour_map() -> HashMap<Elem, Colour> {
let mut m = HashMap::new();
// User / Group
m.insert(Elem::User, Colour::Fixed(230)); // Cornsilk1
m.insert(Elem::Group, Colour::Fixed(187)); // LightYellow3
// Permissions
m.insert(Elem::Read, Colour::Green);
m.insert(Elem::Write, Colour::Yellow);
m.insert(Elem::Exec, Colour::Red);
m.insert(Elem::ExecSticky, Colour::Purple);
m.insert(Elem::NoAccess, Colour::Fixed(245)); // Grey
// File Types
m.insert(
Elem::File {
exec: false,
uid: false,
},
Colour::Fixed(184),
); // Yellow3
m.insert(
Elem::File {
exec: false,
uid: true,
},
Colour::Fixed(184),
); // Yellow3
m.insert(
Elem::File {
exec: true,
uid: false,
},
Colour::Fixed(40),
); // Green3
m.insert(
Elem::File {
exec: true,
uid: true,
},
Colour::Fixed(40),
); // Green3
m.insert(Elem::Dir { uid: true }, Colour::Fixed(33)); // DodgerBlue1
m.insert(Elem::Dir { uid: false }, Colour::Fixed(33)); // DodgerBlue1
m.insert(Elem::Pipe, Colour::Fixed(44)); // DarkTurquoise
m.insert(Elem::SymLink, Colour::Fixed(44)); // DarkTurquoise
m.insert(Elem::BrokenSymLink, Colour::Fixed(124)); // Red3
m.insert(Elem::BlockDevice, Colour::Fixed(44)); // DarkTurquoise
m.insert(Elem::CharDevice, Colour::Fixed(172)); // Orange3
m.insert(Elem::Socket, Colour::Fixed(44)); // DarkTurquoise
m.insert(Elem::Special, Colour::Fixed(44)); // DarkTurquoise
// Last Time Modified
m.insert(Elem::HourOld, Colour::Fixed(40)); // Green3
m.insert(Elem::DayOld, Colour::Fixed(42)); // SpringGreen2
m.insert(Elem::Older, Colour::Fixed(36)); // DarkCyan
// Last Time Modified
m.insert(Elem::NonFile, Colour::Fixed(245)); // Grey
m.insert(Elem::FileSmall, Colour::Fixed(229)); // Wheat1
m.insert(Elem::FileMedium, Colour::Fixed(216)); // LightSalmon1
m.insert(Elem::FileLarge, Colour::Fixed(172)); // Orange3
// INode
m.insert(Elem::INode { valid: true }, Colour::Fixed(13)); // Pink
m.insert(Elem::INode { valid: false }, Colour::Fixed(245)); // Grey
m.insert(Elem::Links { valid: true }, Colour::Fixed(13));
m.insert(Elem::Links { valid: false }, Colour::Fixed(245));
// TODO add this after we can use file to configure theme
// m.insert(Elem::TreeEdge, Colour::Fixed(44)); // DarkTurquoise
m
}
} }

188
src/color/theme.rs Normal file
View file

@ -0,0 +1,188 @@
///! This module provides methods to create theme from files and operations related to
///! this.
use crate::config_file;
use crate::print_error;
use ansi_term::Colour;
use serde::Deserialize;
use std::fs;
use std::path::Path;
/// A struct holding the theme configuration
/// Color table: https://upload.wikimedia.org/wikipedia/commons/1/15/Xterm_256color_chart.avg
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Theme {
pub user: Colour,
pub group: Colour,
pub permissions: Permissions,
pub file_type: FileType,
pub modified: Modified,
pub size: Size,
pub inode: INode,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Permissions {
pub read: Colour,
pub write: Colour,
pub exec: Colour,
pub exec_sticky: Colour,
pub no_access: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct FileType {
pub file: File,
pub dir: Dir,
pub pipe: Colour,
pub symlink: Symlink,
pub block_device: Colour,
pub char_device: Colour,
pub socket: Colour,
pub special: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct File {
pub exec_uid: Colour,
pub uid_no_exec: Colour,
pub exec_no_uid: Colour,
pub no_exec_no_uid: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Dir {
pub uid: Colour,
pub no_uid: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Symlink {
pub default: Colour,
pub broken: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Modified {
pub hour_old: Colour,
pub day_old: Colour,
pub older: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct Size {
pub none: Colour,
pub small: Colour,
pub medium: Colour,
pub large: Colour,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(deny_unknown_fields)]
pub struct INode {
pub valid: Colour,
pub invalid: Colour,
}
impl Theme {
/// This read theme from file,
/// use the file path if it is absolute
/// prefix the config_file dir to it if it is not
pub fn from_path(file: &str) -> Option<Self> {
let real = if let Some(path) = config_file::Config::expand_home(file) {
path
} else {
print_error!("Bad theme file path: {}.", &file);
return None;
};
let path = if Path::new(&real).is_absolute() {
real
} else {
config_file::Config::config_file_path().unwrap().join(real)
};
match fs::read(&path) {
Ok(f) => Self::with_yaml(&String::from_utf8_lossy(&f)),
Err(e) => {
print_error!("bad theme file: {}, {}\n", path.to_string_lossy(), e);
None
}
}
}
/// This constructs a Theme struct with a passed [Yaml] str.
fn with_yaml(yaml: &str) -> Option<Self> {
match serde_yaml::from_str::<Self>(yaml) {
Ok(c) => Some(c),
Err(e) => {
print_error!("theme file format error, {}\n\n", e);
None
}
}
}
pub fn default_dark() -> Self {
Theme {
user: Colour::Fixed(230), // Cornsilk1
group: Colour::Fixed(187), // LightYellow3
permissions: Permissions {
read: Colour::Green,
write: Colour::Yellow,
exec: Colour::Red,
exec_sticky: Colour::Purple,
no_access: Colour::Fixed(245), // Grey
},
file_type: FileType {
file: File {
exec_uid: Colour::Fixed(40), // Green3
uid_no_exec: Colour::Fixed(184), // Yellow3
exec_no_uid: Colour::Fixed(40), // Green3
no_exec_no_uid: Colour::Fixed(184), // Yellow3
},
dir: Dir {
uid: Colour::Fixed(33), // DodgerBlue1
no_uid: Colour::Fixed(33), // DodgerBlue1
},
pipe: Colour::Fixed(44), // DarkTurquoise
symlink: Symlink {
default: Colour::Fixed(44), // DarkTurquoise
broken: Colour::Fixed(124), // Red3
},
block_device: Colour::Fixed(44), // DarkTurquoise
char_device: Colour::Fixed(172), // Orange3
socket: Colour::Fixed(44), // DarkTurquoise
special: Colour::Fixed(44), // DarkTurquoise
},
modified: Modified {
hour_old: Colour::Fixed(40), // Green3
day_old: Colour::Fixed(42), // SpringGreen2
older: Colour::Fixed(36), // DarkCyan
},
size: Size {
none: Colour::Fixed(245), // Grey
small: Colour::Fixed(229), // Wheat1
medium: Colour::Fixed(216), // LightSalmon1
large: Colour::Fixed(172), // Orange3
},
inode: INode {
valid: Colour::Fixed(13), // Pink
invalid: Colour::Fixed(245), // Grey
},
}
}
}

View file

@ -1,14 +1,14 @@
///! 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::display::Display;
use crate::flags::icons::{IconOption, IconTheme}; use crate::flags::icons::{IconOption, IconTheme};
use crate::flags::layout::Layout; use crate::flags::layout::Layout;
use crate::flags::size::SizeFlag; use crate::flags::size::SizeFlag;
use crate::flags::sorting::{DirGrouping, SortColumn}; use crate::flags::sorting::{DirGrouping, SortColumn};
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 crate::print_error;
use std::path::PathBuf; use std::path::{Path, PathBuf};
use serde::Deserialize; use serde::Deserialize;
@ -44,7 +44,8 @@ pub struct Config {
#[derive(Eq, PartialEq, Debug, Deserialize)] #[derive(Eq, PartialEq, Debug, Deserialize)]
pub struct Color { pub struct Color {
pub when: ColorOption, pub when: Option<ColorOption>,
pub theme: Option<ThemeOption>,
} }
#[derive(Eq, PartialEq, Debug, Deserialize)] #[derive(Eq, PartialEq, Debug, Deserialize)]
@ -120,13 +121,11 @@ impl Config {
/// This provides the path for a configuration file, according to the XDG_BASE_DIRS specification. /// This provides the path for a configuration file, according to the XDG_BASE_DIRS specification.
/// return None if error like PermissionDenied /// return None if error like PermissionDenied
#[cfg(not(windows))] #[cfg(not(windows))]
fn config_file_path() -> Option<PathBuf> { pub fn config_file_path() -> Option<PathBuf> {
use xdg::BaseDirectories; use xdg::BaseDirectories;
match BaseDirectories::with_prefix(CONF_DIR) { match BaseDirectories::with_prefix(CONF_DIR) {
Ok(p) => { Ok(p) => {
if let Ok(p) = p.place_config_file([CONF_FILE_NAME, YAML_LONG_EXT].join(".")) { return Some(p.get_config_home());
return Some(p);
}
} }
Err(e) => print_error!("Can not open config file: {}.", e), Err(e) => print_error!("Can not open config file: {}.", e),
} }
@ -136,22 +135,47 @@ impl Config {
/// This provides the path for a configuration file, inside the %APPDATA% directory. /// This provides the path for a configuration file, inside the %APPDATA% directory.
/// return None if error like PermissionDenied /// return None if error like PermissionDenied
#[cfg(windows)] #[cfg(windows)]
fn config_file_path() -> Option<PathBuf> { pub fn config_file_path() -> Option<PathBuf> {
if let Some(p) = dirs::config_dir() { if let Some(p) = dirs::config_dir() {
return Some( return Some(p.join(CONF_DIR));
p.join(CONF_DIR)
.join(CONF_FILE_NAME)
.with_extension(YAML_LONG_EXT),
);
} }
None None
} }
/// 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 Default for Config { impl Default for Config {
fn default() -> Self { fn default() -> Self {
if let Some(p) = Self::config_file_path() { if let Some(p) = Self::config_file_path() {
if let Some(c) = Self::from_file(p.to_string_lossy().to_string()) { if let Some(c) = Self::from_file(
p.join([CONF_FILE_NAME, YAML_LONG_EXT].join("."))
.to_string_lossy()
.to_string(),
) {
return c; return c;
} }
} }

View file

@ -1,6 +1,8 @@
use crate::color::{self, Colors}; use crate::color::Colors;
use crate::display; use crate::display;
use crate::flags::{ColorOption, Display, Flags, IconOption, IconTheme, Layout, SortOrder}; use crate::flags::{
ColorOption, Display, Flags, IconOption, IconTheme, Layout, SortOrder, ThemeOption,
};
use crate::icon::{self, Icons}; use crate::icon::{self, Icons};
use crate::meta::Meta; use crate::meta::Meta;
use crate::{print_error, print_output, sort}; use crate::{print_error, print_output, sort};
@ -41,8 +43,8 @@ impl Core {
let mut inner_flags = flags.clone(); let mut inner_flags = flags.clone();
let color_theme = match (tty_available && console_color_ok, flags.color.when) { let color_theme = match (tty_available && console_color_ok, flags.color.when) {
(_, ColorOption::Never) | (false, ColorOption::Auto) => color::Theme::NoColor, (_, ColorOption::Never) | (false, ColorOption::Auto) => ThemeOption::NoColor,
_ => color::Theme::Default, _ => flags.color.theme.clone(),
}; };
let icon_theme = match (tty_available, flags.icons.when, flags.icons.theme) { let icon_theme = match (tty_available, flags.icons.when, flags.icons.theme) {

View file

@ -17,7 +17,7 @@ pub mod total_size;
pub use blocks::Block; pub use blocks::Block;
pub use blocks::Blocks; pub use blocks::Blocks;
pub use color::Color; pub use color::Color;
pub use color::ColorOption; pub use color::{ColorOption, ThemeOption};
pub use date::DateFlag; pub use date::DateFlag;
pub use dereference::Dereference; pub use dereference::Dereference;
pub use display::Display; pub use display::Display;

View file

@ -7,14 +7,17 @@ use crate::config_file::Config;
use crate::print_error; use crate::print_error;
use clap::ArgMatches; use clap::ArgMatches;
use serde::de::{self, Deserializer, Visitor};
use serde::Deserialize; use serde::Deserialize;
use std::env; use std::env;
use std::fmt;
/// A collection of flags on how to use colors. /// A collection of flags on how to use colors.
#[derive(Clone, Debug, Copy, PartialEq, Eq, Default)] #[derive(Clone, Debug, Default)]
pub struct Color { pub struct Color {
/// When to use color. /// When to use color.
pub when: ColorOption, pub when: ColorOption,
pub theme: ThemeOption,
} }
impl Color { impl Color {
@ -23,7 +26,68 @@ impl Color {
/// The [ColorOption] is configured with their respective [Configurable] implementation. /// The [ColorOption] is configured with their respective [Configurable] implementation.
pub fn configure_from(matches: &ArgMatches, config: &Config) -> Self { pub fn configure_from(matches: &ArgMatches, config: &Config) -> Self {
let when = ColorOption::configure_from(matches, config); let when = ColorOption::configure_from(matches, config);
Self { when } let theme = ThemeOption::from_config(config);
Self { when, theme }
}
}
/// ThemeOption could be one of the following:
/// Custom(*.yaml): use the YAML theme file as theme file
/// if error happened, use the default theme
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum ThemeOption {
NoColor,
Default,
NoLscolors,
Custom(String),
}
impl ThemeOption {
fn from_config(config: &Config) -> ThemeOption {
if let Some(c) = &config.color {
if let Some(t) = &c.theme {
return t.clone();
}
}
ThemeOption::default()
}
}
impl Default for ThemeOption {
fn default() -> Self {
ThemeOption::Default
}
}
impl<'de> de::Deserialize<'de> for ThemeOption {
fn deserialize<D>(deserializer: D) -> Result<ThemeOption, D::Error>
where
D: Deserializer<'de>,
{
struct ThemeOptionVisitor;
impl<'de> Visitor<'de> for ThemeOptionVisitor {
type Value = ThemeOption;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("`no-color`, `default`, `no-lscolors` or <theme-file-path>")
}
fn visit_str<E>(self, value: &str) -> Result<ThemeOption, E>
where
E: de::Error,
{
match value {
"no-color" => Ok(ThemeOption::NoColor),
"default" => Ok(ThemeOption::Default),
"no-lscolors" => Ok(ThemeOption::NoLscolors),
str => Ok(ThemeOption::Custom(str.to_string())),
}
}
}
deserializer.deserialize_identifier(ThemeOptionVisitor)
} }
} }