use crate::theme::ThemeFileType as FileType; use crate::theme::*; use nu_ansi_term::{Color, Style}; use serde::{Deserialize, Deserializer, Serialize}; use serde_yaml; use std::collections::HashMap; use std::path::PathBuf; #[derive(Debug, Default, Eq, PartialEq)] pub struct ThemeConfig { // This is rather bare for now, will be expanded with config file location: ConfigLoc, } #[derive(Debug, Default, PartialEq, Eq)] pub enum ConfigLoc { #[default] Default, // $XDG_CONFIG_HOME/eza/config|theme.yml Env(PathBuf), // $EZA_CONFIG_DIR } trait FromOverride: Sized { fn from(value: T, default: Self) -> Self; } impl FromOverride> for Option where T: FromOverride + Default, { fn from(value: Option, default: Option) -> Option { match (value, default) { (Some(value), Some(default)) => Some(FromOverride::from(value, default)), (Some(value), None) => Some(FromOverride::from(value, T::default())), (None, Some(default)) => Some(default), (None, None) => None, } } } #[rustfmt::skip] fn color_from_str(s: &str) -> Option { use Color::*; match s { // nothing "" | "none" | "None" => None, // hardcoded colors "default" | "Default" => Some(Default), "black" | "Black" => Some(Black), "darkgray" | "DarkGray" => Some(DarkGray), "red" | "Red" => Some(Red), "lightred" | "LightRed" => Some(LightRed), "green" | "Green" => Some(Green), "lightgreen" | "LightGreen" => Some(LightGreen), "yellow" | "Yellow" => Some(Yellow), "lightyellow" | "LightYellow" => Some(LightYellow), "blue" | "Blue" => Some(Blue), "lightblue" | "LightBlue" => Some(LightBlue), "purple" | "Purple" => Some(Purple), "lightpurple" | "LightPurple" => Some(LightPurple), "magenta" | "Magenta" => Some(Magenta), "lightmagenta" | "LightMagenta" => Some(LightMagenta), "cyan" | "Cyan" => Some(Cyan), "lightcyan" | "LightCyan" => Some(LightCyan), "white" | "White" => Some(White), "lightgray" | "LightGray" => Some(LightGray), // some other string s => match s.chars().collect::>()[..] { // #rrggbb hex color ['#', r1, r2, g1, g2, b1, b2] => { let Ok(r) = u8::from_str_radix(&format!("{}{}", r1, r2), 16) else { return None }; let Ok(g) = u8::from_str_radix(&format!("{}{}", g1, g2), 16) else { return None }; let Ok(b) = u8::from_str_radix(&format!("{}{}", b1, b2), 16) else { return None }; Some(Rgb(r, g, b)) }, // #rgb shorthand hex color ['#', r, g, b] => { let Ok(r) = u8::from_str_radix(&format!("{}{}", r, r), 16) else { return None }; let Ok(g) = u8::from_str_radix(&format!("{}{}", g, g), 16) else { return None }; let Ok(b) = u8::from_str_radix(&format!("{}{}", b, b), 16) else { return None }; Some(Rgb(r, g, b)) }, // 0-255 color code [c1, c2] => { let Ok(c) = u8::from_str_radix(&format!("{}{}", c1, c2), 10) else { return None }; Some(Fixed(c)) }, // unknown format _ => None, } } } #[rustfmt::skip] fn deserialize_color<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de> { Ok(color_from_str(&String::deserialize(deserializer)?)) } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize, Default)] pub struct StyleOverride { /// The style's foreground color, if it has one. #[serde(alias = "fg", deserialize_with = "deserialize_color", default)] pub foreground: Option, /// The style's background color, if it has one. #[serde(alias = "bg", deserialize_with = "deserialize_color", default)] pub background: Option, /// Whether this style is bold. #[serde(alias = "bold")] pub is_bold: Option, /// Whether this style is dimmed. #[serde(alias = "dimmed")] pub is_dimmed: Option, /// Whether this style is italic. #[serde(alias = "italic")] pub is_italic: Option, /// Whether this style is underlined. #[serde(alias = "underline")] pub is_underline: Option, /// Whether this style is blinking. #[serde(alias = "blink")] pub is_blink: Option, /// Whether this style has reverse colors. #[serde(alias = "reverse")] pub is_reverse: Option, /// Whether this style is hidden. #[serde(alias = "hidden")] pub is_hidden: Option, /// Whether this style is struckthrough. #[serde(alias = "strikethrough")] pub is_strikethrough: Option, /// Wether this style is always displayed starting with a reset code to clear any remaining style artifacts #[serde(alias = "prefix_reset")] pub prefix_with_reset: Option, } impl FromOverride for Style { fn from(value: StyleOverride, default: Self) -> Self { let mut style = default; if value.foreground.is_some() { style.foreground = value.foreground; } if value.background.is_some() { style.background = value.background; } if let Some(bold) = value.is_bold { style.is_bold = bold; } if let Some(dimmed) = value.is_dimmed { style.is_dimmed = dimmed; } if let Some(italic) = value.is_italic { style.is_italic = italic; } if let Some(underline) = value.is_underline { style.is_underline = underline; } if let Some(blink) = value.is_blink { style.is_blink = blink; } if let Some(reverse) = value.is_reverse { style.is_reverse = reverse; } if let Some(hidden) = value.is_hidden { style.is_hidden = hidden; } if let Some(strikethrough) = value.is_strikethrough { style.is_strikethrough = strikethrough; } if let Some(reset) = value.prefix_with_reset { style.prefix_with_reset = reset; } style } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct IconStyleOverride { pub glyph: Option, pub style: Option, } impl FromOverride for char { fn from(value: char, _default: char) -> char { value } } impl FromOverride for IconStyle { fn from(value: IconStyleOverride, default: Self) -> Self { IconStyle { glyph: FromOverride::from(value.glyph, default.glyph), style: FromOverride::from(value.style, default.style), } } } #[derive(Debug, PartialEq, Eq, Clone, Copy, Serialize, Deserialize)] pub struct FileNameStyleOverride { pub icon: Option, pub filename: Option, } impl FromOverride for FileNameStyle { fn from(value: FileNameStyleOverride, default: Self) -> Self { FileNameStyle { icon: FromOverride::from(value.icon, default.icon), filename: FromOverride::from(value.filename, default.filename), } } } impl FromOverride> for HashMap where T: FromOverride, R: Clone + Eq + std::hash::Hash, T: Clone + Eq + Default, { fn from(value: HashMap, default: HashMap) -> HashMap { let mut result = default.clone(); for (r, s) in value { let t = match default.get(&r) { Some(t) => t.clone(), None => T::default(), }; result.insert(r, FromOverride::from(s, t)); } result } } #[rustfmt::skip] #[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct FileKindsOverride { pub normal: Option, // fi pub directory: Option, // di pub symlink: Option, // ln pub pipe: Option, // pi pub block_device: Option, // bd pub char_device: Option, // cd pub socket: Option, // so pub special: Option, // sp pub executable: Option, // ex pub mount_point: Option, // mp } impl FromOverride for FileKinds { fn from(value: FileKindsOverride, default: Self) -> Self { FileKinds { normal: FromOverride::from(value.normal, default.normal), directory: FromOverride::from(value.directory, default.directory), symlink: FromOverride::from(value.symlink, default.symlink), pipe: FromOverride::from(value.pipe, default.pipe), block_device: FromOverride::from(value.block_device, default.block_device), char_device: FromOverride::from(value.char_device, default.char_device), socket: FromOverride::from(value.socket, default.socket), special: FromOverride::from(value.special, default.special), executable: FromOverride::from(value.executable, default.executable), mount_point: FromOverride::from(value.mount_point, default.mount_point), } } } #[rustfmt::skip] #[derive(Clone, Copy,Eq, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct PermissionsOverride { pub user_read: Option, // ur pub user_write: Option, // uw pub user_execute_file: Option, // ux pub user_execute_other: Option, // ue pub group_read: Option, // gr pub group_write: Option, // gw pub group_execute: Option, // gx pub other_read: Option, // tr pub other_write: Option, // tw pub other_execute: Option, // tx pub special_user_file: Option, // su pub special_other: Option, // sf pub attribute: Option, // xa } impl FromOverride for Permissions { fn from(value: PermissionsOverride, default: Self) -> Self { Permissions { user_read: FromOverride::from(value.user_read, default.user_read), user_write: FromOverride::from(value.user_write, default.user_write), user_execute_file: FromOverride::from( value.user_execute_file, default.user_execute_file, ), user_execute_other: FromOverride::from( value.user_execute_other, default.user_execute_other, ), group_read: FromOverride::from(value.group_read, default.group_read), group_write: FromOverride::from(value.group_write, default.group_write), group_execute: FromOverride::from(value.group_execute, default.group_execute), other_read: FromOverride::from(value.other_read, default.other_read), other_write: FromOverride::from(value.other_write, default.other_write), other_execute: FromOverride::from(value.other_execute, default.other_execute), special_user_file: FromOverride::from( value.special_user_file, default.special_user_file, ), special_other: FromOverride::from(value.special_other, default.special_other), attribute: FromOverride::from(value.attribute, default.attribute), } } } #[rustfmt::skip] #[derive(Clone, Copy, Eq, Debug, Default, PartialEq, Serialize, Deserialize)] pub struct SizeOverride { pub major: Option, // df pub minor: Option, // ds pub number_byte: Option, // sn nb pub number_kilo: Option, // sn nk pub number_mega: Option, // sn nm pub number_giga: Option, // sn ng pub number_huge: Option, // sn nt pub unit_byte: Option, // sb ub pub unit_kilo: Option, // sb uk pub unit_mega: Option, // sb um pub unit_giga: Option, // sb ug pub unit_huge: Option, // sb ut } impl FromOverride for Size { fn from(value: SizeOverride, default: Self) -> Self { Size { major: FromOverride::from(value.major, default.major), minor: FromOverride::from(value.minor, default.minor), number_byte: FromOverride::from(value.number_byte, default.number_byte), number_kilo: FromOverride::from(value.number_kilo, default.number_kilo), number_mega: FromOverride::from(value.number_mega, default.number_mega), number_giga: FromOverride::from(value.number_giga, default.number_giga), number_huge: FromOverride::from(value.number_huge, default.number_huge), unit_byte: FromOverride::from(value.unit_byte, default.unit_byte), unit_kilo: FromOverride::from(value.unit_kilo, default.unit_kilo), unit_mega: FromOverride::from(value.unit_mega, default.unit_mega), unit_giga: FromOverride::from(value.unit_giga, default.unit_giga), unit_huge: FromOverride::from(value.unit_huge, default.unit_huge), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug,Eq, Default, PartialEq, Serialize, Deserialize)] pub struct UsersOverride { pub user_you: Option, // uu pub user_root: Option, // uR pub user_other: Option, // un pub group_yours: Option, // gu pub group_other: Option, // gn pub group_root: Option, // gR } impl FromOverride for Users { fn from(value: UsersOverride, default: Self) -> Self { Users { user_you: FromOverride::from(value.user_you, default.user_you), user_root: FromOverride::from(value.user_root, default.user_root), user_other: FromOverride::from(value.user_other, default.user_other), group_yours: FromOverride::from(value.group_yours, default.group_yours), group_other: FromOverride::from(value.group_other, default.group_other), group_root: FromOverride::from(value.group_root, default.group_root), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct LinksOverride { pub normal: Option, // lc pub multi_link_file: Option, // lm } impl FromOverride for Links { fn from(value: LinksOverride, default: Self) -> Self { Links { normal: FromOverride::from(value.normal, default.normal), multi_link_file: FromOverride::from(value.multi_link_file, default.multi_link_file), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug,Eq, PartialEq, Serialize, Deserialize)] pub struct GitOverride { pub new: Option, // ga pub modified: Option, // gm pub deleted: Option, // gd pub renamed: Option, // gv pub typechange: Option, // gt pub ignored: Option, // gi pub conflicted: Option, // gc } impl FromOverride for Git { fn from(value: GitOverride, default: Self) -> Self { Git { new: FromOverride::from(value.new, default.new), modified: FromOverride::from(value.modified, default.modified), deleted: FromOverride::from(value.deleted, default.deleted), renamed: FromOverride::from(value.renamed, default.renamed), typechange: FromOverride::from(value.typechange, default.typechange), ignored: FromOverride::from(value.ignored, default.ignored), conflicted: FromOverride::from(value.conflicted, default.conflicted), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct GitRepoOverride { pub branch_main: Option, //Gm pub branch_other: Option, //Go pub git_clean: Option, //Gc pub git_dirty: Option, //Gd } impl FromOverride for GitRepo { fn from(value: GitRepoOverride, default: Self) -> Self { GitRepo { branch_main: FromOverride::from(value.branch_main, default.branch_main), branch_other: FromOverride::from(value.branch_other, default.branch_other), git_clean: FromOverride::from(value.git_clean, default.git_clean), git_dirty: FromOverride::from(value.git_dirty, default.git_dirty), } } } #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct SELinuxContextOverride { pub colon: Option, pub user: Option, // Su pub role: Option, // Sr pub typ: Option, // St pub range: Option, // Sl } impl FromOverride for SELinuxContext { fn from(value: SELinuxContextOverride, default: Self) -> Self { SELinuxContext { colon: FromOverride::from(value.colon, default.colon), user: FromOverride::from(value.user, default.user), role: FromOverride::from(value.role, default.role), typ: FromOverride::from(value.typ, default.typ), range: FromOverride::from(value.range, default.range), } } } #[rustfmt::skip] #[derive(Clone, Eq, Copy, Debug, PartialEq, Serialize, Deserialize)] pub struct SecurityContextOverride { pub none: Option, // Sn pub selinux: Option, } impl FromOverride for SecurityContext { fn from(value: SecurityContextOverride, default: Self) -> Self { SecurityContext { none: FromOverride::from(value.none, default.none), selinux: FromOverride::from(value.selinux, default.selinux), } } } #[rustfmt::skip] #[derive(Clone, Copy, Debug, Eq, Default, PartialEq, Serialize, Deserialize)] pub struct FileTypeOverride { pub image: Option, // im - image file pub video: Option, // vi - video file pub music: Option, // mu - lossy music pub lossless: Option, // lo - lossless music pub crypto: Option, // cr - related to cryptography pub document: Option, // do - document file pub compressed: Option, // co - compressed file pub temp: Option, // tm - temporary file pub compiled: Option, // cm - compilation artifact pub build: Option, // bu - file that is used to build a project pub source: Option, // sc - source code } impl FromOverride for FileType { fn from(value: FileTypeOverride, default: Self) -> Self { FileType { image: FromOverride::from(value.image, default.image), video: FromOverride::from(value.video, default.video), music: FromOverride::from(value.music, default.music), lossless: FromOverride::from(value.lossless, default.lossless), crypto: FromOverride::from(value.crypto, default.crypto), document: FromOverride::from(value.document, default.document), compressed: FromOverride::from(value.compressed, default.compressed), temp: FromOverride::from(value.temp, default.temp), compiled: FromOverride::from(value.compiled, default.compiled), build: FromOverride::from(value.build, default.build), source: FromOverride::from(value.source, default.source), } } } #[rustfmt::skip] #[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct UiStylesOverride { pub colourful: Option, pub filekinds: Option, pub perms: Option, pub size: Option, pub users: Option, pub links: Option, pub git: Option, pub git_repo: Option, pub security_context: Option, pub file_type: Option, pub punctuation: Option, // xx pub date: Option, // da pub inode: Option, // in pub blocks: Option, // bl pub header: Option, // hd pub octal: Option, // oc pub flags: Option, // ff pub symlink_path: Option, // lp pub control_char: Option, // cc pub broken_symlink: Option, // or pub broken_path_overlay: Option, // bO pub filenames: Option>, pub extensions: Option>, } impl FromOverride for UiStyles { fn from(value: UiStylesOverride, default: Self) -> Self { UiStyles { colourful: value.colourful, filekinds: FromOverride::from(value.filekinds, default.filekinds), perms: FromOverride::from(value.perms, default.perms), size: FromOverride::from(value.size, default.size), users: FromOverride::from(value.users, default.users), links: FromOverride::from(value.links, default.links), git: FromOverride::from(value.git, default.git), git_repo: FromOverride::from(value.git_repo, default.git_repo), security_context: FromOverride::from(value.security_context, default.security_context), file_type: FromOverride::from(value.file_type, default.file_type), punctuation: FromOverride::from(value.punctuation, default.punctuation), date: FromOverride::from(value.date, default.date), inode: FromOverride::from(value.inode, default.inode), blocks: FromOverride::from(value.blocks, default.blocks), header: FromOverride::from(value.header, default.header), octal: FromOverride::from(value.octal, default.octal), flags: FromOverride::from(value.flags, default.flags), symlink_path: FromOverride::from(value.symlink_path, default.symlink_path), control_char: FromOverride::from(value.control_char, default.control_char), broken_symlink: FromOverride::from(value.broken_symlink, default.broken_symlink), broken_path_overlay: FromOverride::from( value.broken_path_overlay, default.broken_path_overlay, ), filenames: FromOverride::from(value.filenames, default.filenames), extensions: FromOverride::from(value.extensions, default.extensions), } } } impl ThemeConfig { pub fn from_path(path: &str) -> Self { let path = PathBuf::from(path); ThemeConfig { location: ConfigLoc::Env(path), } } pub fn to_theme(&self) -> Option { let ui_styles_override: Option = match &self.location { ConfigLoc::Default => { let path = dirs::config_dir()?.join("eza").join("theme.yml"); let file = std::fs::File::open(path).ok()?; serde_yaml::from_reader(&file).ok() } ConfigLoc::Env(path) => { let file = std::fs::File::open(path).ok()?; serde_yaml::from_reader(&file).ok() } }; FromOverride::from(ui_styles_override, Some(UiStyles::default())) } } #[cfg(test)] mod tests { use super::*; #[test] fn parse_none_color_from_string() { ["", "none", "None"].iter().for_each(|case| { assert_eq!(color_from_str(case), None); }); } #[test] fn parse_default_color_from_string() { ["default", "Default"].iter().for_each(|case| { assert_eq!(color_from_str(case), Some(Color::Default)); }); } #[test] fn parse_fixed_color_from_string() { ["black", "Black"].iter().for_each(|case| { assert_eq!(color_from_str(case), Some(Color::Black)); }); } #[test] fn parse_long_hex_color_from_string() { ["#ff00ff", "#FF00FF"].iter().for_each(|case| { assert_eq!(color_from_str(case), Some(Color::Rgb(255, 0, 255))); }); } #[test] fn parse_short_hex_color_from_string() { ["#f0f", "#F0F"].iter().for_each(|case| { assert_eq!(color_from_str(case), Some(Color::Rgb(255, 0, 255))); }); } #[test] fn parse_color_code_from_string() { [("10", 10), ("01", 1)].iter().for_each(|(s, c)| { assert_eq!(color_from_str(s), Some(Color::Fixed(*c))); }); } }