From ce4d9dc7c6df985dc8049459dc4f2d4526585548 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Mon, 29 Nov 2021 14:37:09 -0600 Subject: [PATCH] allow icons to be used in `grid -c` (#378) * add icons to grid output. still needs cleanup * working but adds a dependency on ansi_term - need to fix that * update styling, added lots of green code to icons * clippy * add config point for grid icons --- Cargo.lock | 3 + crates/nu-command/Cargo.toml | 7 +- crates/nu-command/src/viewers/griddle.rs | 72 ++- crates/nu-command/src/viewers/icons.rs | 580 +++++++++++++++++++++++ crates/nu-command/src/viewers/mod.rs | 1 + crates/nu-protocol/src/config.rs | 5 + 6 files changed, 651 insertions(+), 17 deletions(-) create mode 100644 crates/nu-command/src/viewers/icons.rs diff --git a/Cargo.lock b/Cargo.lock index ed8819785e..ed8cfd3ba5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1434,6 +1434,7 @@ dependencies = [ "chrono", "chrono-humanize", "chrono-tz", + "crossterm", "csv", "dialoguer", "eml-parser", @@ -1441,6 +1442,7 @@ dependencies = [ "ical", "indexmap", "itertools", + "lazy_static", "lscolors", "meval", "nu-ansi-term 0.39.0", @@ -1461,6 +1463,7 @@ dependencies = [ "serde_ini", "serde_urlencoded", "serde_yaml", + "strip-ansi-escapes", "sysinfo", "terminal_size", "thiserror", diff --git a/crates/nu-command/Cargo.toml b/crates/nu-command/Cargo.toml index c61526801d..ca96af7837 100644 --- a/crates/nu-command/Cargo.toml +++ b/crates/nu-command/Cargo.toml @@ -14,8 +14,6 @@ nu-table = { path = "../nu-table" } nu-term-grid = { path = "../nu-term-grid" } nu-parser = { path = "../nu-parser" } nu-ansi-term = { path = "../nu-ansi-term" } -trash = { version = "1.3.0", optional = true } -unicode-segmentation = "1.8.0" # Potential dependencies for extras csv = "1.1.3" @@ -46,6 +44,11 @@ ical = "0.7.0" calamine = "0.18.0" roxmltree = "0.14.0" rand = "0.8" +trash = { version = "1.3.0", optional = true } +unicode-segmentation = "1.8.0" +lazy_static = "1.4.0" +strip-ansi-escapes = "0.1.1" +crossterm = "0.22.1" num = {version="0.4.0", optional=true} diff --git a/crates/nu-command/src/viewers/griddle.rs b/crates/nu-command/src/viewers/griddle.rs index 6ad5c282c0..a04457f310 100644 --- a/crates/nu-command/src/viewers/griddle.rs +++ b/crates/nu-command/src/viewers/griddle.rs @@ -1,3 +1,5 @@ +// use super::icons::{icon_for_file, iconify_style_ansi_to_nu}; +use super::icons::icon_for_file; use lscolors::{LsColors, Style}; use nu_engine::CallExt; use nu_protocol::{ @@ -57,23 +59,23 @@ prints out the list properly."# let width_param: Option = call.get_flag(engine_state, stack, "width")?; let color_param: bool = call.has_flag("color"); let separator_param: Option = call.get_flag(engine_state, stack, "separator")?; - let config = stack.get_config()?; - let env_str = stack.get_env_var("LS_COLORS"); + let use_grid_icons = config.use_grid_icons; match input { PipelineData::Value(Value::List { vals, .. }) => { // dbg!("value::list"); - let data = convert_to_list2(vals, &config); + let data = convert_to_list(vals, &config); if let Some(items) = data { - Ok(create_grid_output2( + Ok(create_grid_output( items, call, width_param, color_param, separator_param, env_str, + use_grid_icons, )) } else { Ok(PipelineData::new(call.head)) @@ -81,15 +83,16 @@ prints out the list properly."# } PipelineData::Stream(stream) => { // dbg!("value::stream"); - let data = convert_to_list2(stream, &config); + let data = convert_to_list(stream, &config); if let Some(items) = data { - Ok(create_grid_output2( + Ok(create_grid_output( items, call, width_param, color_param, separator_param, env_str, + use_grid_icons, )) } else { // dbg!(data); @@ -104,13 +107,14 @@ prints out the list properly."# items.push((i, c, v.into_string(", ", &config))) } - Ok(create_grid_output2( + Ok(create_grid_output( items, call, width_param, color_param, separator_param, env_str, + use_grid_icons, )) } x => { @@ -122,13 +126,22 @@ prints out the list properly."# } } -fn create_grid_output2( +fn strip_ansi(astring: &str) -> String { + if let Ok(bytes) = strip_ansi_escapes::strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } +} + +fn create_grid_output( items: Vec<(usize, String, String)>, call: &Call, width_param: Option, color_param: bool, separator_param: Option, env_str: Option, + use_grid_icons: bool, ) -> PipelineData { let ls_colors = match env_str { Some(s) => LsColors::from_string(&s), @@ -157,14 +170,43 @@ fn create_grid_output2( // only output value if the header name is 'name' if header == "name" { if color_param { - let style = ls_colors.style_for_path(value.clone()); - let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); - let mut cell = Cell::from(ansi_style.apply(value).to_string()); - cell.alignment = Alignment::Right; - grid.add(cell); + if use_grid_icons { + let no_ansi = strip_ansi(&value); + let path = std::path::Path::new(&no_ansi); + let icon = icon_for_file(path); + let ls_colors_style = ls_colors.style_for_path(path); + // eprintln!("ls_colors_style: {:?}", &ls_colors_style); + + let icon_style = match ls_colors_style { + Some(c) => c.to_crossterm_style(), + None => crossterm::style::ContentStyle::default(), + }; + // eprintln!("icon_style: {:?}", &icon_style); + + let ansi_style = ls_colors_style + .map(Style::to_crossterm_style) + .unwrap_or_default(); + // eprintln!("ansi_style: {:?}", &ansi_style); + + let item = format!( + "{} {}", + icon_style.apply(icon).to_string(), + ansi_style.apply(value).to_string() + ); + + let mut cell = Cell::from(item); + cell.alignment = Alignment::Left; + grid.add(cell); + } else { + let style = ls_colors.style_for_path(value.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let mut cell = Cell::from(ansi_style.apply(value).to_string()); + cell.alignment = Alignment::Left; + grid.add(cell); + } } else { let mut cell = Cell::from(value); - cell.alignment = Alignment::Right; + cell.alignment = Alignment::Left; grid.add(cell); } } @@ -184,7 +226,7 @@ fn create_grid_output2( .into_pipeline_data() } -fn convert_to_list2( +fn convert_to_list( iter: impl IntoIterator, config: &Config, ) -> Option> { diff --git a/crates/nu-command/src/viewers/icons.rs b/crates/nu-command/src/viewers/icons.rs new file mode 100644 index 0000000000..e504e10741 --- /dev/null +++ b/crates/nu-command/src/viewers/icons.rs @@ -0,0 +1,580 @@ +use lazy_static::lazy_static; +use std::collections::HashMap; +use std::path::Path; + +// Attribution: Thanks exa. Most of this file is taken from around here +// https://github.com/ogham/exa/blob/dbd11d38042284cc890fdd91760c2f93b65e8553/src/output/icons.rs + +pub trait FileIcon { + fn icon_file(&self, file: &Path) -> Option; +} + +#[derive(Copy, Clone)] +pub enum Icons { + Audio, + Image, + Video, +} + +impl Icons { + pub fn value(self) -> char { + match self { + Self::Audio => '\u{f001}', + Self::Image => '\u{f1c5}', + Self::Video => '\u{f03d}', + } + } +} + +// keeping this for now in case we have to revert to ansi style instead of crossterm style +// Helper function to convert ansi_term style to nu_ansi_term. unfortunately +// this is necessary because ls_colors has a dependency on ansi_term vs nu_ansi_term +// double unfortunately, now we have a dependency on both. we may have to bring +// in ls_colors crate to nushell +// pub fn iconify_style_ansi_to_nu<'a>(style: ansi_term::Style) -> nu_ansi_term::Style { +// let bg = match style.background { +// Some(c) => match c { +// ansi_term::Color::Black => Some(nu_ansi_term::Color::Black), +// ansi_term::Color::Red => Some(nu_ansi_term::Color::Red), +// ansi_term::Color::Green => Some(nu_ansi_term::Color::Green), +// ansi_term::Color::Yellow => Some(nu_ansi_term::Color::Yellow), +// ansi_term::Color::Blue => Some(nu_ansi_term::Color::Blue), +// ansi_term::Color::Purple => Some(nu_ansi_term::Color::Purple), +// ansi_term::Color::Cyan => Some(nu_ansi_term::Color::Cyan), +// ansi_term::Color::White => Some(nu_ansi_term::Color::White), +// ansi_term::Color::Fixed(f) => Some(nu_ansi_term::Color::Fixed(f)), +// ansi_term::Color::RGB(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)), +// }, +// None => None, +// }; + +// let fg = match style.foreground { +// Some(c) => match c { +// ansi_term::Color::Black => Some(nu_ansi_term::Color::Black), +// ansi_term::Color::Red => Some(nu_ansi_term::Color::Red), +// ansi_term::Color::Green => Some(nu_ansi_term::Color::Green), +// ansi_term::Color::Yellow => Some(nu_ansi_term::Color::Yellow), +// ansi_term::Color::Blue => Some(nu_ansi_term::Color::Blue), +// ansi_term::Color::Purple => Some(nu_ansi_term::Color::Purple), +// ansi_term::Color::Cyan => Some(nu_ansi_term::Color::Cyan), +// ansi_term::Color::White => Some(nu_ansi_term::Color::White), +// ansi_term::Color::Fixed(f) => Some(nu_ansi_term::Color::Fixed(f)), +// ansi_term::Color::RGB(r, g, b) => Some(nu_ansi_term::Color::Rgb(r, g, b)), +// }, +// None => None, +// }; + +// let nu_style = nu_ansi_term::Style { +// foreground: fg, +// background: bg, +// is_blink: style.is_blink, +// is_bold: style.is_bold, +// is_dimmed: style.is_dimmed, +// is_hidden: style.is_hidden, +// is_italic: style.is_italic, +// is_underline: style.is_underline, +// is_reverse: style.is_reverse, +// is_strikethrough: style.is_strikethrough, +// }; + +// nu_style +// .background +// .or(nu_style.foreground) +// .map(nu_ansi_term::Style::from) +// .unwrap_or_default() +// } + +lazy_static! { + static ref MAP_BY_NAME: HashMap<&'static str, char> = { + let mut m = HashMap::new(); + m.insert(".Trash", '\u{f1f8}'); //  + m.insert(".atom", '\u{e764}'); //  + m.insert(".bashprofile", '\u{e615}'); //  + m.insert(".bashrc", '\u{f489}'); //  + m.insert(".git", '\u{f1d3}'); //  + m.insert(".gitattributes", '\u{f1d3}'); //  + m.insert(".gitconfig", '\u{f1d3}'); //  + m.insert(".github", '\u{f408}'); //  + m.insert(".gitignore", '\u{f1d3}'); //  + m.insert(".gitmodules", '\u{f1d3}'); //  + m.insert(".rvm", '\u{e21e}'); //  + m.insert(".vimrc", '\u{e62b}'); //  + m.insert(".vscode", '\u{e70c}'); //  + m.insert(".zshrc", '\u{f489}'); //  + m.insert("Cargo.lock", '\u{e7a8}'); //  + m.insert("bin", '\u{e5fc}'); //  + m.insert("config", '\u{e5fc}'); //  + m.insert("docker-compose.yml", '\u{f308}'); //  + m.insert("Dockerfile", '\u{f308}'); //  + m.insert("ds_store", '\u{f179}'); //  + m.insert("gitignore_global", '\u{f1d3}'); //  + m.insert("gradle", '\u{e70e}'); //  + m.insert("gruntfile.coffee", '\u{e611}'); //  + m.insert("gruntfile.js", '\u{e611}'); //  + m.insert("gruntfile.ls", '\u{e611}'); //  + m.insert("gulpfile.coffee", '\u{e610}'); //  + m.insert("gulpfile.js", '\u{e610}'); //  + m.insert("gulpfile.ls", '\u{e610}'); //  + m.insert("hidden", '\u{f023}'); //  + m.insert("include", '\u{e5fc}'); //  + m.insert("lib", '\u{f121}'); //  + m.insert("localized", '\u{f179}'); //  + m.insert("Makefile", '\u{e779}'); //  + m.insert("node_modules", '\u{e718}'); //  + m.insert("npmignore", '\u{e71e}'); //  + m.insert("rubydoc", '\u{e73b}'); //  + m.insert("yarn.lock", '\u{e718}'); //  + + m + }; +} + +pub fn icon_for_file(file_path: &Path) -> char { + let extensions = Box::new(FileExtensions); + let fp = format!("{}", file_path.display()); + + if let Some(icon) = MAP_BY_NAME.get(&fp[..]) { + *icon + } else if file_path.is_dir() { + match file_path.file_name().unwrap().to_str().unwrap() { + "bin" => '\u{e5fc}', //  + ".git" => '\u{f1d3}', //  + ".idea" => '\u{e7b5}', //  + _ => '\u{f115}', //  + } + } else if let Some(icon) = extensions.icon_file(file_path) { + icon + } else if let Some(ext) = file_path.extension().as_ref() { + match ext.to_str().unwrap() { + "ai" => '\u{e7b4}', //  + "android" => '\u{e70e}', //  + "apk" => '\u{e70e}', //  + "apple" => '\u{f179}', //  + "avi" => '\u{f03d}', //  + "avro" => '\u{e60b}', //  + "awk" => '\u{f489}', //  + "bash" => '\u{f489}', //  + "bash_history" => '\u{f489}', //  + "bash_profile" => '\u{f489}', //  + "bashrc" => '\u{f489}', //  + "bat" => '\u{f17a}', //  + "bmp" => '\u{f1c5}', //  + "bz" => '\u{f410}', //  + "bz2" => '\u{f410}', //  + "c" => '\u{e61e}', //  + "c++" => '\u{e61d}', //  + "cab" => '\u{e70f}', //  + "cc" => '\u{e61d}', //  + "cfg" => '\u{e615}', //  + "class" => '\u{e256}', //  + "clj" => '\u{e768}', //  + "cljs" => '\u{e76a}', //  + "cls" => '\u{e600}', //  + "cmd" => '\u{e70f}', //  + "coffee" => '\u{f0f4}', //  + "conf" => '\u{e615}', //  + "cp" => '\u{e61d}', //  + "cpp" => '\u{e61d}', //  + "cs" => '\u{f81a}', //  + "csh" => '\u{f489}', //  + "cshtml" => '\u{f1fa}', //  + "csproj" => '\u{f81a}', //  + "css" => '\u{e749}', //  + "csv" => '\u{f1c3}', //  + "csx" => '\u{f81a}', //  + "cxx" => '\u{e61d}', //  + "d" => '\u{e7af}', //  + "dart" => '\u{e798}', //  + "db" => '\u{f1c0}', //  + "deb" => '\u{e77d}', //  + "diff" => '\u{f440}', //  + "djvu" => '\u{f02d}', //  + "dll" => '\u{e70f}', //  + "doc" => '\u{f1c2}', //  + "docx" => '\u{f1c2}', //  + "ds_store" => '\u{f179}', //  + "DS_store" => '\u{f179}', //  + "dump" => '\u{f1c0}', //  + "ebook" => '\u{e28b}', //  + "editorconfig" => '\u{e615}', //  + "ejs" => '\u{e618}', //  + "elm" => '\u{e62c}', //  + "env" => '\u{f462}', //  + "eot" => '\u{f031}', //  + "epub" => '\u{e28a}', //  + "erb" => '\u{e73b}', //  + "erl" => '\u{e7b1}', //  + "ex" => '\u{e62d}', //  + "exe" => '\u{f17a}', //  + "exs" => '\u{e62d}', //  + "fish" => '\u{f489}', //  + "flac" => '\u{f001}', //  + "flv" => '\u{f03d}', //  + "font" => '\u{f031}', //  + "gdoc" => '\u{f1c2}', //  + "gem" => '\u{e21e}', //  + "gemfile" => '\u{e21e}', //  + "gemspec" => '\u{e21e}', //  + "gform" => '\u{f298}', //  + "gif" => '\u{f1c5}', //  + "git" => '\u{f1d3}', //  + "gitattributes" => '\u{f1d3}', //  + "gitignore" => '\u{f1d3}', //  + "gitmodules" => '\u{f1d3}', //  + "go" => '\u{e626}', //  + "gradle" => '\u{e70e}', //  + "groovy" => '\u{e775}', //  + "gsheet" => '\u{f1c3}', //  + "gslides" => '\u{f1c4}', //  + "guardfile" => '\u{e21e}', //  + "gz" => '\u{f410}', //  + "h" => '\u{f0fd}', //  + "hbs" => '\u{e60f}', //  + "hpp" => '\u{f0fd}', //  + "hs" => '\u{e777}', //  + "htm" => '\u{f13b}', //  + "html" => '\u{f13b}', //  + "hxx" => '\u{f0fd}', //  + "ico" => '\u{f1c5}', //  + "image" => '\u{f1c5}', //  + "iml" => '\u{e7b5}', //  + "ini" => '\u{f17a}', //  + "ipynb" => '\u{e606}', //  + "iso" => '\u{e271}', //  + "jad" => '\u{e256}', //  + "jar" => '\u{e204}', //  + "java" => '\u{e204}', //  + "jpeg" => '\u{f1c5}', //  + "jpg" => '\u{f1c5}', //  + "js" => '\u{e74e}', //  + "json" => '\u{e60b}', //  + "jsx" => '\u{e7ba}', //  + "ksh" => '\u{f489}', //  + "latex" => '\u{e600}', //  + "less" => '\u{e758}', //  + "lhs" => '\u{e777}', //  + "license" => '\u{f718}', //  + "localized" => '\u{f179}', //  + "lock" => '\u{f023}', //  + "log" => '\u{f18d}', //  + "lua" => '\u{e620}', //  + "lz" => '\u{f410}', //  + "lzh" => '\u{f410}', //  + "lzma" => '\u{f410}', //  + "lzo" => '\u{f410}', //  + "m" => '\u{e61e}', //  + "mm" => '\u{e61d}', //  + "m4a" => '\u{f001}', //  + "markdown" => '\u{f48a}', //  + "md" => '\u{f48a}', //  + "mjs" => '\u{e74e}', //  + "mkd" => '\u{f48a}', //  + "mkv" => '\u{f03d}', //  + "mobi" => '\u{e28b}', //  + "mov" => '\u{f03d}', //  + "mp3" => '\u{f001}', //  + "mp4" => '\u{f03d}', //  + "msi" => '\u{e70f}', //  + "mustache" => '\u{e60f}', //  + "nix" => '\u{f313}', //  + "node" => '\u{f898}', //  + "npmignore" => '\u{e71e}', //  + "odp" => '\u{f1c4}', //  + "ods" => '\u{f1c3}', //  + "odt" => '\u{f1c2}', //  + "ogg" => '\u{f001}', //  + "ogv" => '\u{f03d}', //  + "otf" => '\u{f031}', //  + "patch" => '\u{f440}', //  + "pdf" => '\u{f1c1}', //  + "php" => '\u{e73d}', //  + "pl" => '\u{e769}', //  + "png" => '\u{f1c5}', //  + "ppt" => '\u{f1c4}', //  + "pptx" => '\u{f1c4}', //  + "procfile" => '\u{e21e}', //  + "properties" => '\u{e60b}', //  + "ps1" => '\u{f489}', //  + "psd" => '\u{e7b8}', //  + "pxm" => '\u{f1c5}', //  + "py" => '\u{e606}', //  + "pyc" => '\u{e606}', //  + "r" => '\u{f25d}', //  + "rakefile" => '\u{e21e}', //  + "rar" => '\u{f410}', //  + "razor" => '\u{f1fa}', //  + "rb" => '\u{e21e}', //  + "rdata" => '\u{f25d}', //  + "rdb" => '\u{e76d}', //  + "rdoc" => '\u{f48a}', //  + "rds" => '\u{f25d}', //  + "readme" => '\u{f48a}', //  + "rlib" => '\u{e7a8}', //  + "rmd" => '\u{f48a}', //  + "rpm" => '\u{e7bb}', //  + "rs" => '\u{e7a8}', //  + "rspec" => '\u{e21e}', //  + "rspec_parallel" => '\u{e21e}', //  + "rspec_status" => '\u{e21e}', //  + "rss" => '\u{f09e}', //  + "rtf" => '\u{f718}', //  + "ru" => '\u{e21e}', //  + "rubydoc" => '\u{e73b}', //  + "sass" => '\u{e603}', //  + "scala" => '\u{e737}', //  + "scss" => '\u{e749}', //  + "sh" => '\u{f489}', //  + "shell" => '\u{f489}', //  + "slim" => '\u{e73b}', //  + "sln" => '\u{e70c}', //  + "so" => '\u{f17c}', //  + "sql" => '\u{f1c0}', //  + "sqlite3" => '\u{e7c4}', //  + "styl" => '\u{e600}', //  + "stylus" => '\u{e600}', //  + "svg" => '\u{f1c5}', //  + "swift" => '\u{e755}', //  + "tar" => '\u{f410}', //  + "taz" => '\u{f410}', //  + "tbz" => '\u{f410}', //  + "tbz2" => '\u{f410}', //  + "tex" => '\u{e600}', //  + "tiff" => '\u{f1c5}', //  + "toml" => '\u{e615}', //  + "ts" => '\u{e628}', //  + "tsv" => '\u{f1c3}', //  + "tsx" => '\u{e7ba}', //  + "ttf" => '\u{f031}', //  + "twig" => '\u{e61c}', //  + "txt" => '\u{f15c}', //  + "tz" => '\u{f410}', //  + "tzo" => '\u{f410}', //  + "video" => '\u{f03d}', //  + "vim" => '\u{e62b}', //  + "vue" => '\u{fd42}', // ﵂ + "war" => '\u{e256}', //  + "wav" => '\u{f001}', //  + "webm" => '\u{f03d}', //  + "webp" => '\u{f1c5}', //  + "windows" => '\u{f17a}', //  + "woff" => '\u{f031}', //  + "woff2" => '\u{f031}', //  + "xhtml" => '\u{f13b}', //  + "xls" => '\u{f1c3}', //  + "xlsx" => '\u{f1c3}', //  + "xml" => '\u{fabf}', // 謹 + "xul" => '\u{fabf}', // 謹 + "xz" => '\u{f410}', //  + "yaml" => '\u{f481}', //  + "yml" => '\u{f481}', //  + "zip" => '\u{f410}', //  + "zsh" => '\u{f489}', //  + "zsh-theme" => '\u{f489}', //  + "zshrc" => '\u{f489}', //  + _ => '\u{f15b}', //  + } + } else { + '\u{f016}' + } +} + +/// Whether this file’s extension is any of the strings that get passed in. +/// +/// This will always return `false` if the file has no extension. +pub fn extension_is_one_of(path: &Path, choices: &[&str]) -> bool { + match path.extension() { + Some(os_ext) => match os_ext.to_str() { + Some(ext) => choices.contains(&ext), + None => false, + }, + None => false, + } +} + +/// Whether this file’s name, including extension, is any of the strings +/// that get passed in. +// pub fn name_is_one_of(name: &str, choices: &[&str]) -> bool { +// choices.contains(&&name[..]) +// } + +#[derive(Debug, Default, PartialEq)] +pub struct FileExtensions; + +// TODO: We may want to re-add these FileExtensions impl fns back. I have disabled +// it now because it's hard coding colors which kind of defeats the LS_COLORS +// functionality. We may want to enable and augment at some point. + +impl FileExtensions { + // /// An “immediate” file is something that can be run or activated somehow + // /// in order to kick off the build of a project. It’s usually only present + // /// in directories full of source code. + // #[allow(clippy::case_sensitive_file_extension_comparisons)] + // #[allow(dead_code)] + // fn is_immediate(&self, file_path: &Path) -> bool { + // file_path + // .file_name() + // .unwrap() + // .to_str() + // .unwrap() + // .to_lowercase() + // .starts_with("readme") + // || file_path + // .file_name() + // .unwrap() + // .to_str() + // .unwrap() + // .ends_with(".ninja") + // || name_is_one_of( + // file_path.file_name().unwrap().to_str().unwrap(), + // &[ + // "Makefile", + // "Cargo.toml", + // "SConstruct", + // "CMakeLists.txt", + // "build.gradle", + // "pom.xml", + // "Rakefile", + // "package.json", + // "Gruntfile.js", + // "Gruntfile.coffee", + // "BUILD", + // "BUILD.bazel", + // "WORKSPACE", + // "build.xml", + // "Podfile", + // "webpack.config.js", + // "meson.build", + // "composer.json", + // "RoboFile.php", + // "PKGBUILD", + // "Justfile", + // "Procfile", + // "Dockerfile", + // "Containerfile", + // "Vagrantfile", + // "Brewfile", + // "Gemfile", + // "Pipfile", + // "build.sbt", + // "mix.exs", + // "bsconfig.json", + // "tsconfig.json", + // ], + // ) + // } + + fn is_image(&self, file: &Path) -> bool { + extension_is_one_of( + file, + &[ + "png", "jfi", "jfif", "jif", "jpe", "jpeg", "jpg", "gif", "bmp", "tiff", "tif", + "ppm", "pgm", "pbm", "pnm", "webp", "raw", "arw", "svg", "stl", "eps", "dvi", "ps", + "cbr", "jpf", "cbz", "xpm", "ico", "cr2", "orf", "nef", "heif", "avif", "jxl", + ], + ) + } + + fn is_video(&self, file: &Path) -> bool { + extension_is_one_of( + file, + &[ + "avi", "flv", "m2v", "m4v", "mkv", "mov", "mp4", "mpeg", "mpg", "ogm", "ogv", + "vob", "wmv", "webm", "m2ts", "heic", + ], + ) + } + + fn is_music(&self, file: &Path) -> bool { + extension_is_one_of(file, &["aac", "m4a", "mp3", "ogg", "wma", "mka", "opus"]) + } + + // Lossless music, rather than any other kind of data... + fn is_lossless(&self, file: &Path) -> bool { + extension_is_one_of(file, &["alac", "ape", "flac", "wav"]) + } + + // #[allow(dead_code)] + // fn is_crypto(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &["asc", "enc", "gpg", "pgp", "sig", "signature", "pfx", "p12"], + // ) + // } + + // #[allow(dead_code)] + // fn is_document(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &[ + // "djvu", "doc", "docx", "dvi", "eml", "eps", "fotd", "key", "keynote", "numbers", + // "odp", "odt", "pages", "pdf", "ppt", "pptx", "rtf", "xls", "xlsx", + // ], + // ) + // } + + // #[allow(dead_code)] + // fn is_compressed(&self, file: &Path) -> bool { + // extension_is_one_of( + // file, + // &[ + // "zip", "tar", "Z", "z", "gz", "bz2", "a", "ar", "7z", "iso", "dmg", "tc", "rar", + // "par", "tgz", "xz", "txz", "lz", "tlz", "lzma", "deb", "rpm", "zst", "lz4", + // ], + // ) + // } + + // #[allow(dead_code)] + // fn is_temp(&self, file: &Path) -> bool { + // file.file_name().unwrap().to_str().unwrap().ends_with('~') + // || (file.file_name().unwrap().to_str().unwrap().starts_with('#') + // && file.file_name().unwrap().to_str().unwrap().ends_with('#')) + // || extension_is_one_of(file, &["tmp", "swp", "swo", "swn", "bak", "bkp", "bk"]) + // } + + // #[allow(dead_code)] + // fn is_compiled(&self, file: &Path) -> bool { + // if extension_is_one_of(file, &["class", "elc", "hi", "o", "pyc", "zwc", "ko"]) { + // true + // // } else if let Some(dir) = file.parent() { + // // file.get_source_files() + // // .iter() + // // .any(|path| dir.contains(path)) + // } else { + // false + // } + // } + // } + + // impl FileColours for FileExtensions { + // fn colour_file(&self, file: &Path) -> Option