From ab22619f4a20d5e89afc67bf4f7f139acbc32078 Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Mon, 15 Nov 2021 14:09:17 -0600 Subject: [PATCH] enable ls_colors for the `ls` command (#340) * enable ls_colors for the `ls` command * added wrapping with ansi-cut so the ansi sequences don't bleed over * clippy --- Cargo.lock | 117 ++++++++++++++++++++++++- crates/nu-command/src/filesystem/ls.rs | 57 ++++++++---- crates/nu-protocol/src/config.rs | 5 ++ crates/nu-table/Cargo.toml | 3 +- crates/nu-table/src/table.rs | 14 +-- crates/nu-table/src/wrap.rs | 61 ++++++++----- 6 files changed, 210 insertions(+), 47 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 299fb969ed..3847f83f02 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,6 +36,26 @@ dependencies = [ "memchr", ] +[[package]] +name = "ansi-cut" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "116f67a17a90942bc30b3fe78b800941f8276234386844b3e0dee3f9d2782839" +dependencies = [ + "ansi-parser", + "strip-ansi-escapes", +] + +[[package]] +name = "ansi-parser" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bcb2392079bf27198570d6af79ecbd9ec7d8f16d3ec6b60933922fdb66287127" +dependencies = [ + "heapless", + "nom 4.2.3", +] + [[package]] name = "ansi_term" version = "0.12.1" @@ -51,6 +71,18 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" +[[package]] +name = "as-slice" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45403b49e3954a4b8428a0ac21a4b7afadccf92bfd96273f1a58cd4812496ae0" +dependencies = [ + "generic-array 0.12.4", + "generic-array 0.13.3", + "generic-array 0.14.4", + "stable_deref_trait", +] + [[package]] name = "assert_cmd" version = "1.0.8" @@ -133,6 +165,12 @@ dependencies = [ "utf8-width", ] +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + [[package]] name = "bytesize" version = "1.1.0" @@ -493,6 +531,34 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "generic-array" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffdf9f34f1447443d37393cc6c2b8313aebddcd96906caf34e54c68d8e57d7bd" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f797e67af32588215eaaab8327027ee8e71b9dd0b2b26996aedf20c030fce309" +dependencies = [ + "typenum", +] + +[[package]] +name = "generic-array" +version = "0.14.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +dependencies = [ + "typenum", + "version_check 0.9.3", +] + [[package]] name = "getrandom" version = "0.2.3" @@ -516,12 +582,33 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574" +[[package]] +name = "hash32" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4041af86e63ac4298ce40e5cca669066e75b6f1aa3390fe2561ffa5e1d9f4cc" +dependencies = [ + "byteorder", +] + [[package]] name = "hashbrown" version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +[[package]] +name = "heapless" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74911a68a1658cfcfb61bc0ccfbd536e3b6e906f8c2f7883ee50157e3e2184f1" +dependencies = [ + "as-slice", + "generic-array 0.13.3", + "hash32", + "stable_deref_trait", +] + [[package]] name = "hermit-abi" version = "0.1.19" @@ -542,7 +629,7 @@ dependencies = [ "rand_xoshiro", "sized-chunks", "typenum", - "version_check", + "version_check 0.9.3", ] [[package]] @@ -664,7 +751,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f79496a5651c8d57cd033c5add8ca7ee4e3d5f7587a4777484640d9cb60392d9" dependencies = [ "fnv", - "nom", + "nom 1.2.4", ] [[package]] @@ -748,6 +835,16 @@ version = "1.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a5b8c256fd9471521bcb84c3cdba98921497f1a331cbc15b8030fc63b82050ce" +[[package]] +name = "nom" +version = "4.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ad2a91a8e869eeb30b9cb3119ae87773a8f4ae617f41b1eb9c154b2905f7bd6" +dependencies = [ + "memchr", + "version_check 0.1.5", +] + [[package]] name = "ntapi" version = "0.3.6" @@ -887,8 +984,10 @@ dependencies = [ name = "nu-table" version = "0.36.0" dependencies = [ + "ansi-cut", "nu-ansi-term", "regex", + "strip-ansi-escapes", "unicode-width", ] @@ -1428,6 +1527,12 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "strip-ansi-escapes" version = "0.1.1" @@ -1610,7 +1715,7 @@ version = "0.9.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baeed7327e25054889b9bd4f975f32e5f4c5d434042d59ab6cd4142c0a76ed0" dependencies = [ - "version_check", + "version_check 0.9.3", ] [[package]] @@ -1652,6 +1757,12 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "936e4b492acfd135421d8dca4b1aa80a7bfc26e702ef3af710e0752684df5372" +[[package]] +name = "version_check" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914b1a6776c4c929a602fafd8bc742e06365d4bcbe48c30f9cca5824f70dc9dd" + [[package]] name = "version_check" version = "0.9.3" diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index ea4907b466..3dfc6975fa 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use lscolors::{LsColors, Style}; use nu_engine::eval_expression; use nu_protocol::ast::Call; use nu_protocol::engine::{Command, EngineState, Stack}; @@ -32,6 +33,7 @@ impl Command for Ls { call: &Call, _input: PipelineData, ) -> Result { + let config = stack.get_config()?; let pattern = if let Some(expr) = call.positional.get(0) { let result = eval_expression(engine_state, stack, expr)?; let mut result = result.as_string()?; @@ -51,6 +53,7 @@ impl Command for Ls { let call_span = call.head; let glob = glob::glob(&pattern).unwrap(); + let ls_colors = LsColors::from_env().unwrap_or_default(); Ok(glob .into_iter() @@ -60,13 +63,22 @@ impl Command for Ls { let is_file = metadata.is_file(); let is_dir = metadata.is_dir(); let filesize = metadata.len(); - let mut cols = vec!["name".into(), "type".into(), "size".into()]; + let style = ls_colors.style_for_path(path.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let use_ls_colors = config.use_ls_colors; let mut vals = vec![ - Value::String { - val: path.to_string_lossy().to_string(), - span: call_span, + if use_ls_colors { + Value::String { + val: ansi_style.apply(path.to_string_lossy()).to_string(), + span: call_span, + } + } else { + Value::String { + val: path.to_string_lossy().to_string(), + span: call_span, + } }, if is_file { Value::string("file", call_span) @@ -97,18 +109,31 @@ impl Command for Ls { span: call_span, } } - Err(_) => Value::Record { - cols: vec!["name".into(), "type".into(), "size".into()], - vals: vec![ - Value::String { - val: path.to_string_lossy().to_string(), - span: call_span, - }, - Value::Nothing { span: call_span }, - Value::Nothing { span: call_span }, - ], - span: call_span, - }, + Err(_) => { + let style = ls_colors.style_for_path(path.clone()); + let ansi_style = style.map(Style::to_crossterm_style).unwrap_or_default(); + let use_ls_colors = config.use_ls_colors; + + Value::Record { + cols: vec!["name".into(), "type".into(), "size".into()], + vals: vec![ + if use_ls_colors { + Value::String { + val: ansi_style.apply(path.to_string_lossy()).to_string(), + span: call_span, + } + } else { + Value::String { + val: path.to_string_lossy().to_string(), + span: call_span, + } + }, + Value::Nothing { span: call_span }, + Value::Nothing { span: call_span }, + ], + span: call_span, + } + } }, _ => Value::Nothing { span: call_span }, }) diff --git a/crates/nu-protocol/src/config.rs b/crates/nu-protocol/src/config.rs index ff9cb506cf..babefc366c 100644 --- a/crates/nu-protocol/src/config.rs +++ b/crates/nu-protocol/src/config.rs @@ -6,6 +6,7 @@ use crate::{ShellError, Value}; pub struct Config { pub filesize_metric: bool, pub table_mode: String, + pub use_ls_colors: bool, } impl Default for Config { @@ -13,6 +14,7 @@ impl Default for Config { Config { filesize_metric: false, table_mode: "rounded".into(), + use_ls_colors: true, } } } @@ -31,6 +33,9 @@ impl Value { "table_mode" => { config.table_mode = value.as_string()?; } + "use_ls_colors" => { + config.use_ls_colors = value.as_bool()?; + } _ => {} } } diff --git a/crates/nu-table/Cargo.toml b/crates/nu-table/Cargo.toml index 1cba0794b2..108cee5a36 100644 --- a/crates/nu-table/Cargo.toml +++ b/crates/nu-table/Cargo.toml @@ -13,6 +13,7 @@ path = "src/main.rs" [dependencies] nu-ansi-term = "0.39.0" - regex = "1.4" unicode-width = "0.1.8" +strip-ansi-escapes = "0.1.1" +ansi-cut = "0.1.1" \ No newline at end of file diff --git a/crates/nu-table/src/table.rs b/crates/nu-table/src/table.rs index 0d731a6a1e..65ac0c6c02 100644 --- a/crates/nu-table/src/table.rs +++ b/crates/nu-table/src/table.rs @@ -611,15 +611,15 @@ impl Table { } #[derive(Debug)] -pub struct ProcessedTable<'a> { - pub headers: Vec>, - pub data: Vec>>, +pub struct ProcessedTable { + pub headers: Vec, + pub data: Vec>, pub theme: Theme, } #[derive(Debug)] -pub struct ProcessedCell<'a> { - pub contents: Vec>>, +pub struct ProcessedCell { + pub contents: Vec>, pub style: TextStyle, } @@ -995,7 +995,7 @@ pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedT processed_table.headers.push(ProcessedCell { contents: vec![vec![Subline { - subline: "...", + subline: "...".to_string(), width: 3, }]], style: TextStyle::basic_center(), @@ -1004,7 +1004,7 @@ pub fn maybe_truncate_columns(termwidth: usize, processed_table: &mut ProcessedT for entry in processed_table.data.iter_mut() { entry.push(ProcessedCell { contents: vec![vec![Subline { - subline: "...", + subline: "...".to_string(), width: 3, }]], style: TextStyle::basic_center(), diff --git a/crates/nu-table/src/wrap.rs b/crates/nu-table/src/wrap.rs index 2bdb97edaa..3d8977292b 100644 --- a/crates/nu-table/src/wrap.rs +++ b/crates/nu-table/src/wrap.rs @@ -1,8 +1,9 @@ use crate::table::TextStyle; +use ansi_cut::AnsiCut; use nu_ansi_term::Style; use std::collections::HashMap; use std::{fmt::Display, iter::Iterator}; -use unicode_width::UnicodeWidthStr; +use unicode_width::{UnicodeWidthChar, UnicodeWidthStr}; #[derive(Debug, Clone, Copy)] pub enum Alignment { @@ -12,14 +13,14 @@ pub enum Alignment { } #[derive(Debug)] -pub struct Subline<'a> { - pub subline: &'a str, +pub struct Subline { + pub subline: String, pub width: usize, } #[derive(Debug)] -pub struct Line<'a> { - pub sublines: Vec>, +pub struct Line { + pub sublines: Vec, pub width: usize, } @@ -37,7 +38,7 @@ pub struct WrappedCell { pub style: TextStyle, } -impl<'a> Display for Line<'a> { +impl Display for Line { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let mut first = true; for subline in &self.sublines { @@ -52,21 +53,42 @@ impl<'a> Display for Line<'a> { } } +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 unicode_width_strip_ansi(astring: &str) -> usize { + let stripped_string: String = { + if let Ok(bytes) = strip_ansi_escapes::strip(astring) { + String::from_utf8_lossy(&bytes).to_string() + } else { + astring.to_string() + } + }; + + UnicodeWidthStr::width(&stripped_string[..]) +} + pub fn split_sublines(input: &str) -> Vec> { input .split_terminator('\n') .map(|line| { line.split_terminator(' ') .map(|x| Subline { - subline: x, + subline: x.to_string(), width: { // We've tried UnicodeWidthStr::width(x), UnicodeSegmentation::graphemes(x, true).count() // and x.chars().count() with all types of combinations. Currently, it appears that // getting the max of char count and Unicode width seems to produce the best layout. // However, it's not perfect. - let c = x.chars().count(); - let u = UnicodeWidthStr::width(x); - std::cmp::max(c, u) + // let c = x.chars().count(); + // let u = UnicodeWidthStr::width(x); + // std::cmp::min(c, u) + unicode_width_strip_ansi(x) }, }) .collect::>() @@ -101,19 +123,18 @@ pub fn column_width(input: &[Vec]) -> usize { } fn split_word(cell_width: usize, word: &str) -> Vec { - use unicode_width::UnicodeWidthChar; - let mut output = vec![]; let mut current_width = 0; let mut start_index = 0; let mut end_index; - for c in word.char_indices() { + let word_no_ansi = strip_ansi(word); + for c in word_no_ansi.char_indices() { if let Some(width) = c.1.width() { end_index = c.0; if current_width + width > cell_width { output.push(Subline { - subline: &word[start_index..end_index], + subline: word.cut(start_index..end_index), width: current_width, }); @@ -127,7 +148,7 @@ fn split_word(cell_width: usize, word: &str) -> Vec { if start_index != word.len() { output.push(Subline { - subline: &word[start_index..], + subline: word.cut(start_index..), width: current_width, }); } @@ -135,9 +156,9 @@ fn split_word(cell_width: usize, word: &str) -> Vec { output } -pub fn wrap<'a>( +pub fn wrap( cell_width: usize, - mut input: impl Iterator>, + mut input: impl Iterator, color_hm: &HashMap, re_leading: ®ex::Regex, re_trailing: ®ex::Regex, @@ -165,7 +186,7 @@ pub fn wrap<'a>( // If this is a really long single word, we need to split the word if current_line.len() == 1 && current_width > cell_width { max_width = cell_width; - let sublines = split_word(cell_width, current_line[0].subline); + let sublines = split_word(cell_width, ¤t_line[0].subline); for subline in sublines { let width = subline.width; lines.push(Line { @@ -200,7 +221,7 @@ pub fn wrap<'a>( None => { if current_width > cell_width { // We need to break up the last word - let sublines = split_word(cell_width, current_line[0].subline); + let sublines = split_word(cell_width, ¤t_line[0].subline); for subline in sublines { let width = subline.width; lines.push(Line { @@ -235,7 +256,7 @@ pub fn wrap<'a>( first = false; current_line_width = subline.width; } - current_line.push_str(subline.subline); + current_line.push_str(&subline.subline); } if current_line_width > current_max {