use crossterm::style::Color;
use crossterm::style::{Attribute, ContentStyle, StyledContent, Stylize};
use lscolors::{Indicator, LsColors};
use std::path::Path;
pub use crate::flags::color::ThemeOption;
use crate::git::GitStatus;
use crate::print_output;
use crate::theme::{color::ColorTheme, Theme};
#[allow(dead_code)]
#[derive(Hash, Debug, Eq, PartialEq, Clone)]
pub enum Elem {
File {
exec: bool,
uid: bool,
},
SymLink,
BrokenSymLink,
MissingSymLinkTarget,
Dir {
uid: bool,
},
Pipe,
BlockDevice,
CharDevice,
Socket,
Special,
Read,
Write,
Exec,
ExecSticky,
NoAccess,
Octal,
Acl,
Context,
Archive,
AttributeRead,
Hidden,
System,
DayOld,
HourOld,
Older,
User,
Group,
NonFile,
FileLarge,
FileMedium,
FileSmall,
INode {
valid: bool,
},
Links {
valid: bool,
},
TreeEdge,
GitStatus {
status: GitStatus,
},
}
impl Elem {
fn has_suid(&self) -> bool {
matches!(self, Elem::Dir { uid: true } | Elem::File { uid: true, .. })
}
pub fn get_color(&self, theme: &ColorTheme) -> Color {
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::MissingSymLinkTarget => theme.file_type.symlink.missing_target,
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.permission.read,
Elem::Write => theme.permission.write,
Elem::Exec => theme.permission.exec,
Elem::ExecSticky => theme.permission.exec_sticky,
Elem::NoAccess => theme.permission.no_access,
Elem::Octal => theme.permission.octal,
Elem::Acl => theme.permission.acl,
Elem::Context => theme.permission.context,
Elem::Archive => theme.attributes.archive,
Elem::AttributeRead => theme.attributes.read,
Elem::Hidden => theme.attributes.hidden,
Elem::System => theme.attributes.system,
Elem::DayOld => theme.date.day_old,
Elem::HourOld => theme.date.hour_old,
Elem::Older => theme.date.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: true } => theme.inode.valid,
Elem::INode { valid: false } => theme.inode.invalid,
Elem::TreeEdge => theme.tree_edge,
Elem::Links { valid: false } => theme.links.invalid,
Elem::Links { valid: true } => theme.links.valid,
Elem::GitStatus {
status: GitStatus::Default,
} => theme.git_status.default,
Elem::GitStatus {
status: GitStatus::Unmodified,
} => theme.git_status.unmodified,
Elem::GitStatus {
status: GitStatus::Ignored,
} => theme.git_status.ignored,
Elem::GitStatus {
status: GitStatus::NewInIndex,
} => theme.git_status.new_in_index,
Elem::GitStatus {
status: GitStatus::NewInWorkdir,
} => theme.git_status.new_in_workdir,
Elem::GitStatus {
status: GitStatus::Typechange,
} => theme.git_status.typechange,
Elem::GitStatus {
status: GitStatus::Deleted,
} => theme.git_status.deleted,
Elem::GitStatus {
status: GitStatus::Renamed,
} => theme.git_status.renamed,
Elem::GitStatus {
status: GitStatus::Modified,
} => theme.git_status.modified,
Elem::GitStatus {
status: GitStatus::Conflicted,
} => theme.git_status.conflicted,
}
}
}
pub type ColoredString = StyledContent<String>;
pub struct Colors {
theme: Option<ColorTheme>,
lscolors: Option<LsColors>,
}
impl Colors {
pub fn new(t: ThemeOption) -> Self {
let theme = match t {
ThemeOption::NoColor => None,
ThemeOption::Default | ThemeOption::NoLscolors => Some(Theme::default().color),
ThemeOption::Custom => Some(
Theme::from_path::<ColorTheme>(Path::new("colors").to_str().unwrap())
.unwrap_or_default(),
),
ThemeOption::CustomLegacy(ref file) => {
print_output!(
"Warning: the 'themes' directory is deprecated, use 'colors.yaml' instead.\n\n"
);
Some(
Theme::from_path::<ColorTheme>(
Path::new("themes").join(file).to_str().unwrap_or(file),
)
.unwrap_or_default(),
)
}
};
let lscolors = match t {
ThemeOption::Default | ThemeOption::Custom | ThemeOption::CustomLegacy(_) => {
Some(LsColors::from_env().unwrap_or_default())
}
_ => None,
};
Self { theme, lscolors }
}
pub fn colorize<S: Into<String>>(&self, input: S, elem: &Elem) -> ColoredString {
self.style(elem).apply(input.into())
}
pub fn colorize_using_path(&self, input: String, path: &Path, elem: &Elem) -> ColoredString {
let style_from_path = self.style_from_path(path);
match style_from_path {
Some(style_from_path) => style_from_path.apply(input),
None => self.colorize(input, elem),
}
}
pub fn default_style() -> ContentStyle {
ContentStyle::default()
}
fn style_from_path(&self, path: &Path) -> Option<ContentStyle> {
match &self.lscolors {
Some(lscolors) => lscolors.style_for_path(path).map(to_content_style),
None => None,
}
}
fn style(&self, elem: &Elem) -> ContentStyle {
match &self.lscolors {
Some(lscolors) => match self.get_indicator_from_elem(elem) {
Some(style) => {
let style = lscolors.style_for_indicator(style);
style.map(to_content_style).unwrap_or_default()
}
None => self.style_default(elem),
},
None => self.style_default(elem),
}
}
fn style_default(&self, elem: &Elem) -> ContentStyle {
if let Some(t) = &self.theme {
let style_fg = ContentStyle::default().with(elem.get_color(t));
if elem.has_suid() {
style_fg.on(Color::AnsiValue(124)) } else {
style_fg
}
} else {
ContentStyle::default()
}
}
fn get_indicator_from_elem(&self, elem: &Elem) -> Option<Indicator> {
let indicator_string = match elem {
Elem::File { exec, uid } => match (exec, uid) {
(_, true) => None,
(true, false) => Some("ex"),
(false, false) => Some("fi"),
},
Elem::Dir { uid } => {
if *uid {
None
} else {
Some("di")
}
}
Elem::SymLink => Some("ln"),
Elem::Pipe => Some("pi"),
Elem::Socket => Some("so"),
Elem::BlockDevice => Some("bd"),
Elem::CharDevice => Some("cd"),
Elem::BrokenSymLink => Some("or"),
Elem::MissingSymLinkTarget => Some("mi"),
_ => None,
};
match indicator_string {
Some(ids) => Indicator::from(ids),
None => None,
}
}
}
fn to_content_style(ls: &lscolors::Style) -> ContentStyle {
let to_crossterm_color = |c: &lscolors::Color| match c {
lscolors::style::Color::RGB(r, g, b) => Color::Rgb {
r: *r,
g: *g,
b: *b,
},
lscolors::style::Color::Fixed(n) => Color::AnsiValue(*n),
lscolors::style::Color::Black => Color::Black,
lscolors::style::Color::Red => Color::DarkRed,
lscolors::style::Color::Green => Color::DarkGreen,
lscolors::style::Color::Yellow => Color::DarkYellow,
lscolors::style::Color::Blue => Color::DarkBlue,
lscolors::style::Color::Magenta => Color::DarkMagenta,
lscolors::style::Color::Cyan => Color::DarkCyan,
lscolors::style::Color::White => Color::Grey,
lscolors::style::Color::BrightBlack => Color::DarkGrey,
lscolors::style::Color::BrightRed => Color::Red,
lscolors::style::Color::BrightGreen => Color::Green,
lscolors::style::Color::BrightYellow => Color::Yellow,
lscolors::style::Color::BrightBlue => Color::Blue,
lscolors::style::Color::BrightMagenta => Color::Magenta,
lscolors::style::Color::BrightCyan => Color::Cyan,
lscolors::style::Color::BrightWhite => Color::White,
};
let mut style = ContentStyle {
foreground_color: ls.foreground.as_ref().map(to_crossterm_color),
background_color: ls.background.as_ref().map(to_crossterm_color),
..ContentStyle::default()
};
if ls.font_style.bold {
style.attributes.set(Attribute::Bold);
}
if ls.font_style.dimmed {
style.attributes.set(Attribute::Dim);
}
if ls.font_style.italic {
style.attributes.set(Attribute::Italic);
}
if ls.font_style.underline {
style.attributes.set(Attribute::Underlined);
}
if ls.font_style.rapid_blink {
style.attributes.set(Attribute::RapidBlink);
}
if ls.font_style.slow_blink {
style.attributes.set(Attribute::SlowBlink);
}
if ls.font_style.reverse {
style.attributes.set(Attribute::Reverse);
}
if ls.font_style.hidden {
style.attributes.set(Attribute::Hidden);
}
if ls.font_style.strikethrough {
style.attributes.set(Attribute::CrossedOut);
}
style
}
#[cfg(test)]
mod tests {
use super::Colors;
use crate::color::ThemeOption;
use crate::theme::color::ColorTheme;
#[test]
fn test_color_new_no_color_theme() {
assert!(Colors::new(ThemeOption::NoColor).theme.is_none());
}
#[test]
fn test_color_new_custom_theme() {
assert_eq!(
Colors::new(ThemeOption::Custom).theme,
Some(ColorTheme::default_dark()),
);
}
#[test]
fn test_color_new_custom_no_file_theme() {
assert_eq!(
Colors::new(ThemeOption::Custom).theme,
Some(ColorTheme::default_dark()),
);
}
#[test]
fn test_color_new_bad_legacy_custom_theme() {
assert_eq!(
Colors::new(ThemeOption::CustomLegacy("not-existed".to_string())).theme,
Some(ColorTheme::default_dark()),
);
}
}
#[cfg(test)]
mod elem {
use super::Elem;
use crate::theme::{color, color::ColorTheme};
use crossterm::style::Color;
#[cfg(test)]
fn test_theme() -> ColorTheme {
ColorTheme {
user: Color::AnsiValue(230), group: Color::AnsiValue(187), permission: color::Permission {
read: Color::Green,
write: Color::Yellow,
exec: Color::Red,
exec_sticky: Color::Magenta,
no_access: Color::AnsiValue(245), octal: Color::AnsiValue(6),
acl: Color::DarkCyan,
context: Color::Cyan,
},
attributes: color::Attributes {
read: Color::Green,
archive: Color::Yellow,
hidden: Color::Red,
system: Color::Magenta,
},
file_type: color::FileType {
file: color::File {
exec_uid: Color::AnsiValue(40), uid_no_exec: Color::AnsiValue(184), exec_no_uid: Color::AnsiValue(40), no_exec_no_uid: Color::AnsiValue(184), },
dir: color::Dir {
uid: Color::AnsiValue(33), no_uid: Color::AnsiValue(33), },
pipe: Color::AnsiValue(44), symlink: color::Symlink {
default: Color::AnsiValue(44), broken: Color::AnsiValue(124), missing_target: Color::AnsiValue(124), },
block_device: Color::AnsiValue(44), char_device: Color::AnsiValue(172), socket: Color::AnsiValue(44), special: Color::AnsiValue(44), },
date: color::Date {
hour_old: Color::AnsiValue(40), day_old: Color::AnsiValue(42), older: Color::AnsiValue(36), },
size: color::Size {
none: Color::AnsiValue(245), small: Color::AnsiValue(229), medium: Color::AnsiValue(216), large: Color::AnsiValue(172), },
inode: color::INode {
valid: Color::AnsiValue(13), invalid: Color::AnsiValue(245), },
links: color::Links {
valid: Color::AnsiValue(13), invalid: Color::AnsiValue(245), },
tree_edge: Color::AnsiValue(245), git_status: Default::default(),
}
}
#[test]
fn test_default_theme_color() {
assert_eq!(
Elem::File {
exec: true,
uid: true
}
.get_color(&test_theme()),
Color::AnsiValue(40),
);
assert_eq!(
Elem::File {
exec: false,
uid: true
}
.get_color(&test_theme()),
Color::AnsiValue(184),
);
assert_eq!(
Elem::File {
exec: true,
uid: false
}
.get_color(&test_theme()),
Color::AnsiValue(40),
);
assert_eq!(
Elem::File {
exec: false,
uid: false
}
.get_color(&test_theme()),
Color::AnsiValue(184),
);
}
}