mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 22:47:43 +02:00
color_config now accepts closures as color values (#7141)
# Description Closes #6909. You can now add closures to your `color_config` themes. Whenever a value would be printed with `table`, the closure is run with the value piped-in. The closure must return either a {fg,bg,attr} record or a color name (`'light_red'` etc.). This returned style is used to colour the value. This is entirely backwards-compatible with existing config.nu files. Example code excerpt: ``` let my_theme = { header: green_bold bool: { if $in { 'light_cyan' } else { 'light_red' } } int: purple_bold filesize: { |e| if $e == 0b { 'gray' } else if $e < 1mb { 'purple_bold' } else { 'cyan_bold' } } duration: purple_bold date: { (date now) - $in | if $in > 1wk { 'cyan_bold' } else if $in > 1day { 'green_bold' } else { 'yellow_bold' } } range: yellow_bold string: { if $in =~ '^#\w{6}$' { $in } else { 'white' } } nothing: white ``` Example output with this in effect:    Slightly important notes: * Some color_config names, namely "separator", "empty" and "hints", pipe in `null` instead of a value. * Currently, doing anything non-trivial inside a closure has an understandably big perf hit. I currently do not actually recommend something like `string: { if $in =~ '^#\w{6}$' { $in } else { 'white' } }` for serious work, mainly because of the abundance of string-type data in the world. Nevertheless, lesser-used types like "date" and "duration" work well with this. * I had to do some reorganisation in order to make it possible to call `eval_block()` that late in table rendering. I invented a new struct called "StyleComputer" which holds the engine_state and stack of the initial `table` command (implicit or explicit). * StyleComputer has a `compute()` method which takes a color_config name and a nu value, and always returns the correct Style, so you don't have to worry about A) the color_config value was set at all, B) whether it was set to a closure or not, or C) which default style to use in those cases. * Currently, errors encountered during execution of the closures are thrown in the garbage. Any other ideas are welcome. (Nonetheless, errors result in a huge perf hit when they are encountered. I think what should be done is to assume something terrible happened to the user's config and invalidate the StyleComputer for that `table` run, thus causing subsequent output to just be Style::default().) * More thorough tests are forthcoming - ran into some difficulty using `nu!` to take an alternative config, and for some reason `let-env config =` statements don't seem to work inside `nu!` pipelines(???) * The default config.nu has not been updated to make use of this yet. Do tell if you think I should incorporate that into this. # User-Facing Changes See above. # Tests + Formatting Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace --features=extra -- -D warnings -D clippy::unwrap_used -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace --features=extra` to check that all tests pass # After Submitting If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date.
This commit is contained in:
@ -1,7 +1,6 @@
|
||||
use crate::nu_style::{color_from_hex, color_string_to_nustyle, lookup_style};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use nu_protocol::{Config, Value};
|
||||
use nu_table::{Alignment, TextStyle};
|
||||
use crate::nu_style::{color_from_hex, lookup_style};
|
||||
use nu_ansi_term::Style;
|
||||
use nu_protocol::Value;
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn lookup_ansi_color_style(s: &str) -> Style {
|
||||
@ -10,13 +9,12 @@ pub fn lookup_ansi_color_style(s: &str) -> Style {
|
||||
.ok()
|
||||
.and_then(|c| c.map(|c| c.normal()))
|
||||
.unwrap_or_default()
|
||||
} else if s.starts_with('{') {
|
||||
color_string_to_nustyle(s.to_string())
|
||||
} else {
|
||||
lookup_style(s)
|
||||
}
|
||||
}
|
||||
|
||||
// These two are used only for Explore's very limited color config
|
||||
fn update_hashmap(key: &str, val: &str, hm: &mut HashMap<String, Style>) {
|
||||
// eprintln!("key: {}, val: {}", &key, &val);
|
||||
let color = lookup_ansi_color_style(val);
|
||||
@ -27,49 +25,6 @@ fn update_hashmap(key: &str, val: &str, hm: &mut HashMap<String, Style>) {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_color_config(config: &Config) -> HashMap<String, Style> {
|
||||
let config = config;
|
||||
|
||||
// create the hashmap
|
||||
let mut hm: HashMap<String, Style> = HashMap::new();
|
||||
// set some defaults
|
||||
// hm.insert("primitive_line".to_string(), Color::White.normal());
|
||||
// hm.insert("primitive_pattern".to_string(), Color::White.normal());
|
||||
// hm.insert("primitive_path".to_string(), Color::White.normal());
|
||||
hm.insert("separator".to_string(), Color::White.normal());
|
||||
hm.insert(
|
||||
"leading_trailing_space_bg".to_string(),
|
||||
Style::default().on(Color::Rgb(128, 128, 128)),
|
||||
);
|
||||
hm.insert("header".to_string(), Color::Green.bold());
|
||||
hm.insert("empty".to_string(), Color::Blue.normal());
|
||||
hm.insert("bool".to_string(), Color::White.normal());
|
||||
hm.insert("int".to_string(), Color::White.normal());
|
||||
hm.insert("filesize".to_string(), Color::White.normal());
|
||||
hm.insert("duration".to_string(), Color::White.normal());
|
||||
hm.insert("date".to_string(), Color::White.normal());
|
||||
hm.insert("range".to_string(), Color::White.normal());
|
||||
hm.insert("float".to_string(), Color::White.normal());
|
||||
hm.insert("string".to_string(), Color::White.normal());
|
||||
hm.insert("nothing".to_string(), Color::White.normal());
|
||||
hm.insert("binary".to_string(), Color::White.normal());
|
||||
hm.insert("cellpath".to_string(), Color::White.normal());
|
||||
hm.insert("row_index".to_string(), Color::Green.bold());
|
||||
hm.insert("record".to_string(), Color::White.normal());
|
||||
hm.insert("list".to_string(), Color::White.normal());
|
||||
hm.insert("block".to_string(), Color::White.normal());
|
||||
hm.insert("hints".to_string(), Color::DarkGray.normal());
|
||||
|
||||
for (key, value) in &config.color_config {
|
||||
match value.as_string() {
|
||||
Ok(value) => update_hashmap(key, &value, &mut hm),
|
||||
Err(_) => continue,
|
||||
}
|
||||
}
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
pub fn get_color_map(colors: &HashMap<String, Value>) -> HashMap<String, Style> {
|
||||
let mut hm: HashMap<String, Style> = HashMap::new();
|
||||
|
||||
@ -81,189 +36,3 @@ pub fn get_color_map(colors: &HashMap<String, Value>) -> HashMap<String, Style>
|
||||
|
||||
hm
|
||||
}
|
||||
|
||||
// This function will assign a text style to a primitive, or really any string that's
|
||||
// in the hashmap. The hashmap actually contains the style to be applied.
|
||||
pub fn style_primitive(primitive: &str, color_hm: &HashMap<String, Style>) -> TextStyle {
|
||||
match primitive {
|
||||
"bool" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"int" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Right, *s),
|
||||
None => TextStyle::basic_right(),
|
||||
}
|
||||
}
|
||||
|
||||
"filesize" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Right, *s),
|
||||
None => TextStyle::basic_right(),
|
||||
}
|
||||
}
|
||||
|
||||
"duration" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"date" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"range" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"float" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Right, *s),
|
||||
None => TextStyle::basic_right(),
|
||||
}
|
||||
}
|
||||
|
||||
"string" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"nothing" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
// not sure what to do with error
|
||||
// "error" => {}
|
||||
"binary" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"cellpath" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
"row_index" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Right, *s),
|
||||
None => TextStyle::new()
|
||||
.alignment(Alignment::Right)
|
||||
.fg(Color::Green)
|
||||
.bold(Some(true)),
|
||||
}
|
||||
}
|
||||
|
||||
"record" | "list" | "block" => {
|
||||
let style = color_hm.get(primitive);
|
||||
match style {
|
||||
Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
None => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
// types in nushell but not in engine-q
|
||||
// "Line" => {
|
||||
// let style = color_hm.get("Primitive::Line");
|
||||
// match style {
|
||||
// Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
// None => TextStyle::basic_left(),
|
||||
// }
|
||||
// }
|
||||
// "GlobPattern" => {
|
||||
// let style = color_hm.get("Primitive::GlobPattern");
|
||||
// match style {
|
||||
// Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
// None => TextStyle::basic_left(),
|
||||
// }
|
||||
// }
|
||||
// "FilePath" => {
|
||||
// let style = color_hm.get("Primitive::FilePath");
|
||||
// match style {
|
||||
// Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
// None => TextStyle::basic_left(),
|
||||
// }
|
||||
// }
|
||||
// "BeginningOfStream" => {
|
||||
// let style = color_hm.get("Primitive::BeginningOfStream");
|
||||
// match style {
|
||||
// Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
// None => TextStyle::basic_left(),
|
||||
// }
|
||||
// }
|
||||
// "EndOfStream" => {
|
||||
// let style = color_hm.get("Primitive::EndOfStream");
|
||||
// match style {
|
||||
// Some(s) => TextStyle::with_style(Alignment::Left, *s),
|
||||
// None => TextStyle::basic_left(),
|
||||
// }
|
||||
// }
|
||||
_ => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_hm() {
|
||||
use nu_ansi_term::{Color, Style};
|
||||
|
||||
let mut hm: HashMap<String, Style> = HashMap::new();
|
||||
hm.insert("primitive_int".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_decimal".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_filesize".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_string".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_line".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_columnpath".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_pattern".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_boolean".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_date".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_duration".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_range".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_path".to_string(), Color::White.normal());
|
||||
hm.insert("primitive_binary".to_string(), Color::White.normal());
|
||||
hm.insert("separator".to_string(), Color::White.normal());
|
||||
hm.insert("header_align".to_string(), Color::Green.bold());
|
||||
hm.insert("header".to_string(), Color::Green.bold());
|
||||
hm.insert("header_style".to_string(), Style::default());
|
||||
hm.insert("row_index".to_string(), Color::Green.bold());
|
||||
hm.insert(
|
||||
"leading_trailing_space_bg".to_string(),
|
||||
Style::default().on(Color::Rgb(128, 128, 128)),
|
||||
);
|
||||
|
||||
update_hashmap("primitive_int", "green", &mut hm);
|
||||
|
||||
assert_eq!(hm["primitive_int"], Color::Green.normal());
|
||||
}
|
||||
|
@ -2,8 +2,12 @@ mod color_config;
|
||||
mod matching_brackets_style;
|
||||
mod nu_style;
|
||||
mod shape_color;
|
||||
mod style_computer;
|
||||
mod text_style;
|
||||
|
||||
pub use color_config::*;
|
||||
pub use matching_brackets_style::*;
|
||||
pub use nu_style::*;
|
||||
pub use shape_color::*;
|
||||
pub use style_computer::*;
|
||||
pub use text_style::*;
|
||||
|
@ -1,4 +1,5 @@
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use nu_protocol::Value;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Deserialize, Serialize, PartialEq, Eq, Debug)]
|
||||
@ -83,22 +84,29 @@ pub fn parse_nustyle(nu_style: NuStyle) -> Style {
|
||||
style
|
||||
}
|
||||
|
||||
pub fn color_string_to_nustyle(color_string: String) -> Style {
|
||||
// eprintln!("color_string: {}", &color_string);
|
||||
if color_string.chars().count() < 1 {
|
||||
Style::default()
|
||||
} else {
|
||||
let nu_style = match nu_json::from_str::<NuStyle>(&color_string) {
|
||||
Ok(s) => s,
|
||||
Err(_) => NuStyle {
|
||||
fg: None,
|
||||
bg: None,
|
||||
attr: None,
|
||||
},
|
||||
};
|
||||
// Converts the color_config records, { fg, bg, attr }, into a Style.
|
||||
pub fn color_record_to_nustyle(value: &Value) -> Style {
|
||||
let mut fg = None;
|
||||
let mut bg = None;
|
||||
let mut attr = None;
|
||||
let v = value.as_record();
|
||||
if let Ok((cols, inner_vals)) = v {
|
||||
for (k, v) in cols.iter().zip(inner_vals) {
|
||||
// Because config already type-checked the color_config records, this doesn't bother giving errors
|
||||
// if there are unrecognised keys or bad values.
|
||||
if let Ok(v) = v.as_string() {
|
||||
match k.as_str() {
|
||||
"fg" => fg = Some(v),
|
||||
|
||||
parse_nustyle(nu_style)
|
||||
"bg" => bg = Some(v),
|
||||
|
||||
"attr" => attr = Some(v),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
parse_nustyle(NuStyle { fg, bg, attr })
|
||||
}
|
||||
|
||||
pub fn color_from_hex(
|
||||
|
285
crates/nu-color-config/src/style_computer.rs
Normal file
285
crates/nu-color-config/src/style_computer.rs
Normal file
@ -0,0 +1,285 @@
|
||||
use crate::{color_record_to_nustyle, lookup_ansi_color_style, TextStyle};
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use nu_engine::eval_block;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
CliError, IntoPipelineData, Value,
|
||||
};
|
||||
use tabled::alignment::AlignmentHorizontal;
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fmt::{Debug, Formatter, Result},
|
||||
};
|
||||
|
||||
// ComputableStyle represents the valid user style types: a single color value, or a closure which
|
||||
// takes an input value and produces a color value. The latter represents a value which
|
||||
// is computed at use-time.
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum ComputableStyle {
|
||||
Static(Style),
|
||||
Closure(Value),
|
||||
}
|
||||
|
||||
// macro used for adding initial values to the style hashmap
|
||||
macro_rules! initial {
|
||||
($a:expr, $b:expr) => {
|
||||
($a.to_string(), ComputableStyle::Static($b))
|
||||
};
|
||||
}
|
||||
|
||||
// An alias for the mapping used internally by StyleComputer.
|
||||
pub type StyleMapping = HashMap<String, ComputableStyle>;
|
||||
//
|
||||
// A StyleComputer is an all-in-one way to compute styles. A nu command can
|
||||
// simply create it with from_config(), and then use it with compute().
|
||||
// It stores the engine state and stack needed to run closures that
|
||||
// may be defined as a user style.
|
||||
//
|
||||
pub struct StyleComputer<'a> {
|
||||
engine_state: &'a EngineState,
|
||||
stack: &'a Stack,
|
||||
map: StyleMapping,
|
||||
}
|
||||
|
||||
impl<'a> StyleComputer<'a> {
|
||||
// This is NOT meant to be used in most cases - please use from_config() instead.
|
||||
// This only exists for testing purposes.
|
||||
pub fn new(
|
||||
engine_state: &'a EngineState,
|
||||
stack: &'a Stack,
|
||||
map: StyleMapping,
|
||||
) -> StyleComputer<'a> {
|
||||
StyleComputer {
|
||||
engine_state,
|
||||
stack,
|
||||
map,
|
||||
}
|
||||
}
|
||||
// The main method. Takes a string name which maps to a color_config style name,
|
||||
// and a Nu value to pipe into any closures that may have been defined there.
|
||||
pub fn compute(&self, style_name: &str, value: &Value) -> Style {
|
||||
match self.map.get(style_name) {
|
||||
// Static values require no computation.
|
||||
Some(ComputableStyle::Static(s)) => *s,
|
||||
// Closures are run here.
|
||||
Some(ComputableStyle::Closure(Value::Closure {
|
||||
val: block_id,
|
||||
captures,
|
||||
span,
|
||||
})) => {
|
||||
let block = self.engine_state.get_block(*block_id).clone();
|
||||
// Because captures_to_stack() clones, we don't need to use with_env() here
|
||||
// (contrast with_env() usage in `each` or `do`).
|
||||
let mut stack = self.stack.captures_to_stack(captures);
|
||||
|
||||
// Support 1-argument blocks as well as 0-argument blocks.
|
||||
if let Some(var) = block.signature.get_positional(0) {
|
||||
if let Some(var_id) = &var.var_id {
|
||||
stack.add_var(*var_id, value.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Run the block.
|
||||
match eval_block(
|
||||
self.engine_state,
|
||||
&mut stack,
|
||||
&block,
|
||||
value.clone().into_pipeline_data(),
|
||||
false,
|
||||
false,
|
||||
) {
|
||||
Ok(v) => {
|
||||
let value = v.into_value(*span);
|
||||
// These should be the same color data forms supported by color_config.
|
||||
match value {
|
||||
Value::Record { .. } => color_record_to_nustyle(&value),
|
||||
Value::String { val, .. } => lookup_ansi_color_style(&val),
|
||||
_ => Style::default(),
|
||||
}
|
||||
}
|
||||
// This is basically a copy of nu_cli::report_error(), but that isn't usable due to
|
||||
// dependencies. While crudely spitting out a bunch of errors like this is not ideal,
|
||||
// currently hook closure errors behave roughly the same.
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"Error: {:?}",
|
||||
CliError(&e, &StateWorkingSet::new(self.engine_state))
|
||||
);
|
||||
Style::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
// There should be no other kinds of values (due to create_map() in config.rs filtering them out)
|
||||
// so this is just a fallback.
|
||||
_ => Style::default(),
|
||||
}
|
||||
}
|
||||
|
||||
// Used only by the `table` command.
|
||||
pub fn style_primitive(&self, value: &Value) -> TextStyle {
|
||||
let s = self.compute(&value.get_type().to_string(), value);
|
||||
match *value {
|
||||
Value::Bool { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Int { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::Filesize { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::Duration { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Date { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Range { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Float { .. } => TextStyle::with_style(AlignmentHorizontal::Right, s),
|
||||
|
||||
Value::String { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Nothing { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Binary { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::CellPath { .. } => TextStyle::with_style(AlignmentHorizontal::Left, s),
|
||||
|
||||
Value::Record { .. } | Value::List { .. } | Value::Block { .. } => {
|
||||
TextStyle::with_style(AlignmentHorizontal::Left, s)
|
||||
}
|
||||
_ => TextStyle::basic_left(),
|
||||
}
|
||||
}
|
||||
|
||||
// The main constructor.
|
||||
pub fn from_config(engine_state: &'a EngineState, stack: &'a Stack) -> StyleComputer<'a> {
|
||||
let config = engine_state.get_config();
|
||||
|
||||
// Create the hashmap
|
||||
let mut map: StyleMapping = HashMap::from([
|
||||
initial!("separator", Color::White.normal()),
|
||||
initial!(
|
||||
"leading_trailing_space_bg",
|
||||
Style::default().on(Color::Rgb(128, 128, 128))
|
||||
),
|
||||
initial!("header", Color::White.normal()),
|
||||
initial!("empty", Color::White.normal()),
|
||||
initial!("bool", Color::White.normal()),
|
||||
initial!("int", Color::White.normal()),
|
||||
initial!("filesize", Color::White.normal()),
|
||||
initial!("duration", Color::White.normal()),
|
||||
initial!("date", Color::White.normal()),
|
||||
initial!("range", Color::White.normal()),
|
||||
initial!("float", Color::White.normal()),
|
||||
initial!("string", Color::White.normal()),
|
||||
initial!("nothing", Color::White.normal()),
|
||||
initial!("binary", Color::White.normal()),
|
||||
initial!("cellpath", Color::White.normal()),
|
||||
initial!("row_index", Color::Green.bold()),
|
||||
initial!("record", Color::White.normal()),
|
||||
initial!("list", Color::White.normal()),
|
||||
initial!("block", Color::White.normal()),
|
||||
initial!("hints", Color::DarkGray.normal()),
|
||||
]);
|
||||
|
||||
for (key, value) in &config.color_config {
|
||||
match value {
|
||||
Value::Closure { .. } => {
|
||||
map.insert(key.to_string(), ComputableStyle::Closure(value.clone()));
|
||||
}
|
||||
Value::Record { .. } => {
|
||||
map.insert(
|
||||
key.to_string(),
|
||||
ComputableStyle::Static(color_record_to_nustyle(value)),
|
||||
);
|
||||
}
|
||||
Value::String { val, .. } => {
|
||||
// update the stylemap with the found key
|
||||
let color = lookup_ansi_color_style(val.as_str());
|
||||
if let Some(v) = map.get_mut(key) {
|
||||
*v = ComputableStyle::Static(color);
|
||||
} else {
|
||||
map.insert(key.to_string(), ComputableStyle::Static(color));
|
||||
}
|
||||
}
|
||||
// This should never occur.
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
StyleComputer::new(engine_state, stack, map)
|
||||
}
|
||||
}
|
||||
|
||||
// Because EngineState doesn't have Debug (Dec 2022),
|
||||
// this incomplete representation must be used.
|
||||
impl<'a> Debug for StyleComputer<'a> {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> Result {
|
||||
f.debug_struct("StyleComputer")
|
||||
.field("map", &self.map)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_computable_style_static() {
|
||||
use nu_protocol::Span;
|
||||
|
||||
let style1 = Style::default().italic();
|
||||
let style2 = Style::default().underline();
|
||||
// Create a "dummy" style_computer for this test.
|
||||
let dummy_engine_state = EngineState::new();
|
||||
let mut dummy_stack = Stack::new();
|
||||
let style_computer = StyleComputer::new(
|
||||
&dummy_engine_state,
|
||||
&mut dummy_stack,
|
||||
HashMap::from([
|
||||
("string".into(), ComputableStyle::Static(style1)),
|
||||
("row_index".into(), ComputableStyle::Static(style2)),
|
||||
]),
|
||||
);
|
||||
assert_eq!(
|
||||
style_computer.compute("string", &Value::nothing(Span::unknown())),
|
||||
style1
|
||||
);
|
||||
assert_eq!(
|
||||
style_computer.compute("row_index", &Value::nothing(Span::unknown())),
|
||||
style2
|
||||
);
|
||||
}
|
||||
|
||||
// Because each closure currently runs in a separate environment, checks that the closures have run
|
||||
// must use the filesystem.
|
||||
#[test]
|
||||
fn test_computable_style_closure_basic() {
|
||||
use nu_test_support::{nu, nu_repl_code, playground::Playground};
|
||||
Playground::setup("computable_style_closure_basic", |dirs, _| {
|
||||
let inp = [
|
||||
r#"let-env config = {
|
||||
color_config: {
|
||||
string: {|e| touch ($e + '.obj'); 'red' }
|
||||
}
|
||||
};"#,
|
||||
"[bell book candle] | table | ignore",
|
||||
"ls | get name | to nuon",
|
||||
];
|
||||
let actual_repl = nu!(cwd: dirs.test(), nu_repl_code(&inp));
|
||||
assert_eq!(actual_repl.err, "");
|
||||
assert_eq!(actual_repl.out, "[bell.obj, book.obj, candle.obj]");
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_computable_style_closure_errors() {
|
||||
use nu_test_support::{nu, nu_repl_code};
|
||||
let inp = [
|
||||
r#"let-env config = {
|
||||
color_config: {
|
||||
string: {|e| $e + 2 }
|
||||
}
|
||||
};"#,
|
||||
"[bell] | table",
|
||||
];
|
||||
let actual_repl = nu!(cwd: ".", nu_repl_code(&inp));
|
||||
// Check that the error was printed
|
||||
assert!(actual_repl.err.contains("type mismatch for operator"));
|
||||
// Check that the value was printed
|
||||
assert!(actual_repl.out.contains("bell"));
|
||||
}
|
262
crates/nu-color-config/src/text_style.rs
Normal file
262
crates/nu-color-config/src/text_style.rs
Normal file
@ -0,0 +1,262 @@
|
||||
use nu_ansi_term::{Color, Style};
|
||||
use std::fmt::Display;
|
||||
|
||||
pub type Alignment = tabled::alignment::AlignmentHorizontal;
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct TextStyle {
|
||||
pub alignment: Alignment,
|
||||
pub color_style: Option<Style>,
|
||||
}
|
||||
|
||||
impl TextStyle {
|
||||
pub fn new() -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: Alignment::Left,
|
||||
color_style: Some(Style::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bold(&self, bool_value: Option<bool>) -> TextStyle {
|
||||
let bv = bool_value.unwrap_or(false);
|
||||
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_bold: bv,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_bold(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_bold
|
||||
}
|
||||
|
||||
pub fn dimmed(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_dimmed: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_dimmed(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_dimmed
|
||||
}
|
||||
|
||||
pub fn italic(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_italic: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_italic(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_italic
|
||||
}
|
||||
|
||||
pub fn underline(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_underline: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_underline(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_underline
|
||||
}
|
||||
|
||||
pub fn blink(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_blink: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_blink(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_blink
|
||||
}
|
||||
|
||||
pub fn reverse(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_reverse: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_reverse(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_reverse
|
||||
}
|
||||
|
||||
pub fn hidden(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_hidden: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_hidden(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_hidden
|
||||
}
|
||||
|
||||
pub fn strikethrough(&self) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
is_strikethrough: true,
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_strikethrough(&self) -> bool {
|
||||
self.color_style.unwrap_or_default().is_strikethrough
|
||||
}
|
||||
|
||||
pub fn fg(&self, foreground: Color) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
foreground: Some(foreground),
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn on(&self, background: Color) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
background: Some(background),
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn bg(&self, background: Color) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
background: Some(background),
|
||||
..self.color_style.unwrap_or_default()
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alignment(&self, align: Alignment) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: align,
|
||||
color_style: self.color_style,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn style(&self, style: Style) -> TextStyle {
|
||||
TextStyle {
|
||||
alignment: self.alignment,
|
||||
color_style: Some(Style {
|
||||
foreground: style.foreground,
|
||||
background: style.background,
|
||||
is_bold: style.is_bold,
|
||||
is_dimmed: style.is_dimmed,
|
||||
is_italic: style.is_italic,
|
||||
is_underline: style.is_underline,
|
||||
is_blink: style.is_blink,
|
||||
is_reverse: style.is_reverse,
|
||||
is_hidden: style.is_hidden,
|
||||
is_strikethrough: style.is_strikethrough,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn basic_center() -> TextStyle {
|
||||
TextStyle::new()
|
||||
.alignment(Alignment::Center)
|
||||
.style(Style::default())
|
||||
}
|
||||
|
||||
pub fn basic_right() -> TextStyle {
|
||||
TextStyle::new()
|
||||
.alignment(Alignment::Right)
|
||||
.style(Style::default())
|
||||
}
|
||||
|
||||
pub fn basic_left() -> TextStyle {
|
||||
TextStyle::new()
|
||||
.alignment(Alignment::Left)
|
||||
.style(Style::default())
|
||||
}
|
||||
|
||||
pub fn default_header() -> TextStyle {
|
||||
TextStyle::new()
|
||||
.alignment(Alignment::Center)
|
||||
.fg(Color::Green)
|
||||
.bold(Some(true))
|
||||
}
|
||||
|
||||
pub fn default_field() -> TextStyle {
|
||||
TextStyle::new().fg(Color::Green).bold(Some(true))
|
||||
}
|
||||
|
||||
pub fn with_attributes(bo: bool, al: Alignment, co: Color) -> TextStyle {
|
||||
TextStyle::new().alignment(al).fg(co).bold(Some(bo))
|
||||
}
|
||||
|
||||
pub fn with_style(al: Alignment, style: Style) -> TextStyle {
|
||||
TextStyle::new().alignment(al).style(Style {
|
||||
foreground: style.foreground,
|
||||
background: style.background,
|
||||
is_bold: style.is_bold,
|
||||
is_dimmed: style.is_dimmed,
|
||||
is_italic: style.is_italic,
|
||||
is_underline: style.is_underline,
|
||||
is_blink: style.is_blink,
|
||||
is_reverse: style.is_reverse,
|
||||
is_hidden: style.is_hidden,
|
||||
is_strikethrough: style.is_strikethrough,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for TextStyle {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl tabled::papergrid::Color for TextStyle {
|
||||
fn fmt_prefix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(color) = &self.color_style {
|
||||
color.prefix().fmt(f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn fmt_suffix(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if let Some(color) = &self.color_style {
|
||||
if !color.is_plain() {
|
||||
f.write_str("\u{1b}[0m")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user