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:
![2022-11-16 12 47 23 AM - style_computer
rs_-_nushell_-_VSCodium](https://user-images.githubusercontent.com/83939/201952558-482de05d-69c7-4bf2-91fc-d0964bf71264.png)
![2022-11-16 12 39 41 AM - style_computer
rs_-_nushell_-_VSCodium](https://user-images.githubusercontent.com/83939/201952580-2384bb86-b680-40fe-8192-71bae396c738.png)
![2022-11-15 09 21 54 PM - run_external
rs_-_nushell_-_VSCodium](https://user-images.githubusercontent.com/83939/201952601-343fc15d-e4a8-4a92-ad89-9a7d17d42748.png)

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:
Leon
2022-12-17 23:07:56 +10:00
committed by GitHub
parent e72cecf457
commit 774769a7ad
39 changed files with 1075 additions and 729 deletions

View File

@ -1,9 +1,9 @@
use fancy_regex::Regex;
use nu_ansi_term::{
Color::{Default, Red, White},
Color::{Red, White},
Style,
};
use nu_color_config::get_color_config;
use nu_color_config::StyleComputer;
use nu_engine::{get_full_help, CallExt};
use nu_protocol::{
ast::Call,
@ -86,13 +86,13 @@ fn help(
let find: Option<Spanned<String>> = call.get_flag(engine_state, stack, "find")?;
let rest: Vec<Spanned<String>> = call.rest(engine_state, stack, 0)?;
let commands = engine_state.get_decl_ids_sorted(false);
let config = engine_state.get_config();
let color_hm = get_color_config(config);
let default_style = Style::new().fg(Default).on(Default);
let string_style = match color_hm.get("string") {
Some(style) => style,
None => &default_style,
};
// 🚩The following two-lines are copied from filters/find.rs:
let style_computer = StyleComputer::from_config(engine_state, stack);
// Currently, search results all use the same style.
// Also note that this sample string is passed into user-written code (the closure that may or may not be
// defined for "string").
let string_style = style_computer.compute("string", &Value::string("search result", head));
if let Some(f) = find {
let org_search_string = f.item.clone();
@ -123,7 +123,7 @@ fn help(
cols.push("name".into());
vals.push(Value::String {
val: if key_match {
highlight_search_string(&key, &org_search_string, string_style)?
highlight_search_string(&key, &org_search_string, &string_style)?
} else {
key
},
@ -142,7 +142,7 @@ fn help(
cols.push("usage".into());
vals.push(Value::String {
val: if usage_match {
highlight_search_string(&usage, &org_search_string, string_style)?
highlight_search_string(&usage, &org_search_string, &string_style)?
} else {
usage
},
@ -172,7 +172,7 @@ fn help(
match highlight_search_string(
term,
&org_search_string,
string_style,
&string_style,
) {
Ok(s) => s,
Err(_) => {

View File

@ -2,8 +2,8 @@ use crate::help::highlight_search_string;
use fancy_regex::Regex;
use lscolors::{Color as LsColors_Color, LsColors, Style as LsColors_Style};
use nu_ansi_term::{Color, Color::Default, Style};
use nu_color_config::get_color_config;
use nu_ansi_term::{Color, Style};
use nu_color_config::StyleComputer;
use nu_engine::{env_to_string, CallExt};
use nu_protocol::{
ast::Call,
@ -308,12 +308,12 @@ fn find_with_rest_and_highlight(
})
.collect::<Vec<Value>>();
let color_hm = get_color_config(&config);
let default_style = Style::new().fg(Default).on(Default);
let string_style = match color_hm.get("string") {
Some(style) => *style,
None => default_style,
};
let style_computer = StyleComputer::from_config(&engine_state, stack);
// Currently, search results all use the same style.
// Also note that this sample string is passed into user-written code (the closure that may or may not be
// defined for "string").
let string_style = style_computer.compute("string", &Value::string("search result", span));
let ls_colors_env_str = match stack.get_env_var(&engine_state, "LS_COLORS") {
Some(v) => Some(env_to_string("LS_COLORS", &v, &engine_state, stack)?),
None => None,

View File

@ -58,7 +58,7 @@ impl Command for External {
// Translate environment variables from Values to Strings
let env_vars_str = env_to_strings(engine_state, stack)?;
fn value_as_spanned(value: Value, name: &String) -> Result<Spanned<String>, ShellError> {
fn value_as_spanned(value: Value) -> Result<Spanned<String>, ShellError> {
let span = value.span()?;
value
@ -66,11 +66,7 @@ impl Command for External {
.map(|item| Spanned { item, span })
.map_err(|_| {
ShellError::ExternalCommand(
format!(
"Cannot convert {} to a string argument for '{}'",
value.get_type(),
name
),
format!("Cannot convert {} to a string", value.get_type()),
"All arguments to an external command need to be string-compatible".into(),
span,
)
@ -87,13 +83,13 @@ impl Command for External {
// Example: one_arg may be something like ["ls" "-a"]
// convert it to "ls" "-a"
for v in vals {
spanned_args.push(value_as_spanned(v, &name.item)?);
spanned_args.push(value_as_spanned(v)?);
// for arguments in list, it's always treated as a whole arguments
arg_keep_raw.push(true);
}
}
val => {
spanned_args.push(value_as_spanned(val, &name.item)?);
spanned_args.push(value_as_spanned(val)?);
match one_arg_expr.expr {
// refer to `parse_dollar_expr` function
// the expression type of $variable_name, $"($variable_name)"

View File

@ -1,7 +1,5 @@
use std::collections::HashMap;
use nu_ansi_term::{Color, Style};
use nu_color_config::{get_color_config, get_color_map};
use nu_color_config::{get_color_map, StyleComputer};
use nu_engine::CallExt;
use nu_explore::{
run_pager,
@ -13,6 +11,7 @@ use nu_protocol::{
engine::{Command, EngineState, Stack},
Category, Example, PipelineData, ShellError, Signature, Span, SyntaxShape, Value,
};
use std::collections::HashMap;
/// A `less` like program to render a [Value] as a table.
#[derive(Clone)]
@ -70,7 +69,7 @@ impl Command for Explore {
let ctrlc = engine_state.ctrlc.clone();
let nu_config = engine_state.get_config();
let color_hm = get_color_config(nu_config);
let style_computer = StyleComputer::from_config(engine_state, stack);
let mut config = nu_config.explore.clone();
prepare_default_config(&mut config);
@ -81,7 +80,7 @@ impl Command for Explore {
let style = style_from_config(&config);
let mut config = PagerConfig::new(nu_config, &color_hm, config);
let mut config = PagerConfig::new(nu_config, &style_computer, config);
config.style = style;
config.reverse = is_reverse;
config.peek_value = peek_value;
@ -89,7 +88,7 @@ impl Command for Explore {
config.exit_esc = exit_esc;
config.show_banner = show_banner;
let result = run_pager(engine_state, stack, ctrlc, input, config);
let result = run_pager(engine_state, &mut stack.clone(), ctrlc, input, config);
match result {
Ok(Some(value)) => Ok(PipelineData::Value(value, None)),
@ -128,6 +127,8 @@ impl Command for Explore {
}
}
// For now, this doesn't use StyleComputer.
// As such, closures can't be given as styles for Explore.
fn is_need_banner(config: &HashMap<String, Value>) -> Option<bool> {
config.get("help_banner").and_then(|v| v.as_bool().ok())
}
@ -170,10 +171,6 @@ fn style_from_config(config: &HashMap<String, Value>) -> StyleConfig {
style.cmd_bar_background = *s;
}
if let Some(s) = colors.get("highlight") {
style.highlight = *s;
}
if let Some(hm) = config.get("status").and_then(create_map) {
let colors = get_color_map(&hm);

View File

@ -1,5 +1,6 @@
use lscolors::{LsColors, Style};
use nu_color_config::{color_from_hex, get_color_config, style_primitive};
use nu_color_config::color_from_hex;
use nu_color_config::{Alignment, StyleComputer, TextStyle};
use nu_engine::{column::get_columns, env_to_string, CallExt};
use nu_protocol::{
ast::{Call, PathMember},
@ -8,11 +9,12 @@ use nu_protocol::{
PipelineData, PipelineMetadata, RawStream, ShellError, Signature, Span, SyntaxShape,
TableIndexMode, Value,
};
use nu_table::{string_width, Alignment, Table as NuTable, TableConfig, TableTheme, TextStyle};
use nu_table::{string_width, Table as NuTable, TableConfig, TableTheme};
use nu_utils::get_ls_colors;
use rayon::prelude::*;
use std::sync::Arc;
use std::time::Instant;
use std::{cmp::max, collections::HashMap, path::PathBuf, sync::atomic::AtomicBool};
use std::{cmp::max, path::PathBuf, sync::atomic::AtomicBool};
use terminal_size::{Height, Width};
use url::Url;
@ -21,7 +23,6 @@ const STREAM_TIMEOUT_CHECK_INTERVAL: usize = 100;
const INDEX_COLUMN_NAME: &str = "index";
type NuText = (String, TextStyle);
type NuColorMap = HashMap<String, nu_ansi_term::Style>;
fn get_width_param(width_param: Option<i64>) -> usize {
if let Some(col) = width_param {
@ -253,6 +254,7 @@ fn handle_table_command(
metadata: None,
trim_end_newline: false,
}),
// None of these two receive a StyleComputer because handle_row_stream() can produce it by itself using engine_state and stack.
PipelineData::Value(Value::List { vals, .. }, metadata) => handle_row_stream(
engine_state,
stack,
@ -272,10 +274,17 @@ fn handle_table_command(
metadata,
),
PipelineData::Value(Value::Record { cols, vals, span }, ..) => {
// Create a StyleComputer to compute styles for each value in the table.
let style_computer = &StyleComputer::from_config(engine_state, stack);
let result = match table_view {
TableView::General => {
build_general_table2(cols, vals, ctrlc.clone(), config, term_width)
}
TableView::General => build_general_table2(
style_computer,
cols,
vals,
ctrlc.clone(),
config,
term_width,
),
TableView::Expanded {
limit,
flatten,
@ -288,13 +297,16 @@ fn handle_table_command(
span,
ctrlc.clone(),
config,
style_computer,
term_width,
limit,
flatten,
sep,
)
}
TableView::Collapsed => build_collapsed_table(cols, vals, config, term_width),
TableView::Collapsed => {
build_collapsed_table(style_computer, cols, vals, config, term_width)
}
}?;
let result = strip_output_color(result, config);
@ -358,6 +370,7 @@ fn supported_table_modes() -> Vec<Value> {
}
fn build_collapsed_table(
style_computer: &StyleComputer,
cols: Vec<String>,
vals: Vec<Value>,
config: &Config,
@ -369,9 +382,16 @@ fn build_collapsed_table(
span: Span::new(0, 0),
};
let color_hm = get_color_config(config);
let theme = load_theme_from_config(config);
let table = nu_table::NuTable::new(value, true, term_width, config, &color_hm, &theme, false);
let table = nu_table::NuTable::new(
value,
true,
term_width,
config,
style_computer,
&theme,
false,
);
let table = table.draw();
@ -379,6 +399,7 @@ fn build_collapsed_table(
}
fn build_general_table2(
style_computer: &StyleComputer,
cols: Vec<String>,
vals: Vec<Value>,
ctrlc: Option<Arc<AtomicBool>>,
@ -400,8 +421,7 @@ fn build_general_table2(
}
let data_len = data.len();
let color_hm = get_color_config(config);
let table_config = create_table_config(config, &color_hm, data_len, false, false, false);
let table_config = create_table_config(config, style_computer, data_len, false, false, false);
let table = NuTable::new(data, (data_len, 2));
@ -410,6 +430,7 @@ fn build_general_table2(
Ok(table)
}
// The table produced by `table -e`
#[allow(clippy::too_many_arguments)]
fn build_expanded_table(
cols: Vec<String>,
@ -417,12 +438,12 @@ fn build_expanded_table(
span: Span,
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
style_computer: &StyleComputer,
term_width: usize,
expand_limit: Option<usize>,
flatten: bool,
flatten_sep: &str,
) -> Result<Option<String>, ShellError> {
let color_hm = get_color_config(config);
let theme = load_theme_from_config(config);
// calculate the width of a key part + the rest of table so we know the rest of the table width available for value.
@ -431,7 +452,7 @@ fn build_expanded_table(
let key_table = NuTable::new(vec![vec![key]], (1, 2));
let key_width = key_table
.draw(
create_table_config(config, &color_hm, 1, false, false, false),
create_table_config(config, style_computer, 1, false, false, false),
usize::MAX,
)
.map(|table| string_width(&table))
@ -454,7 +475,7 @@ fn build_expanded_table(
let is_limited = matches!(expand_limit, Some(0));
let mut is_expanded = false;
let value = if is_limited {
value_to_styled_string(&value, config, &color_hm).0
value_to_styled_string(&value, config, style_computer).0
} else {
let deep = expand_limit.map(|i| i - 1);
@ -466,7 +487,7 @@ fn build_expanded_table(
ctrlc.clone(),
config,
span,
&color_hm,
style_computer,
deep,
flatten,
flatten_sep,
@ -476,14 +497,13 @@ fn build_expanded_table(
match table {
Some((mut table, with_header, with_index)) => {
// controll width via removing table columns.
let theme = load_theme_from_config(config);
table.truncate(remaining_width, &theme);
is_expanded = true;
let table_config = create_table_config(
config,
&color_hm,
style_computer,
table.count_rows(),
with_header,
with_index,
@ -499,7 +519,7 @@ fn build_expanded_table(
None => {
// it means that the list is empty
let value = Value::List { vals, span };
value_to_styled_string(&value, config, &color_hm).0
value_to_styled_string(&value, config, style_computer).0
}
}
}
@ -510,6 +530,7 @@ fn build_expanded_table(
span,
ctrlc.clone(),
config,
style_computer,
remaining_width,
deep,
flatten,
@ -525,7 +546,7 @@ fn build_expanded_table(
let failed_value = value_to_styled_string(
&Value::Record { cols, vals, span },
config,
&color_hm,
style_computer,
);
nu_table::wrap_string(&failed_value.0, remaining_width)
@ -533,7 +554,7 @@ fn build_expanded_table(
}
}
val => {
let text = value_to_styled_string(&val, config, &color_hm).0;
let text = value_to_styled_string(&val, config, style_computer).0;
nu_table::wrap_string(&text, remaining_width)
}
}
@ -555,7 +576,7 @@ fn build_expanded_table(
}
let data_len = data.len();
let table_config = create_table_config(config, &color_hm, data_len, false, false, false);
let table_config = create_table_config(config, style_computer, data_len, false, false, false);
let table = NuTable::new(data, (data_len, 2));
let table_s = table.clone().draw(table_config.clone(), term_width);
@ -700,7 +721,10 @@ fn handle_row_stream(
stdout: Some(RawStream::new(
Box::new(PagingTableCreator {
row_offset,
config: engine_state.get_config().clone(),
// These are passed in as a way to have PagingTable create StyleComputers
// for the values it outputs. Because engine_state is passed in, config doesn't need to.
engine_state: engine_state.clone(),
stack: stack.clone(),
ctrlc: ctrlc.clone(),
head,
stream,
@ -742,18 +766,71 @@ fn make_clickable_link(
}
}
#[allow(clippy::too_many_arguments)]
// convert_to_table() defers all its style computations so that they can be run in parallel using par_extend().
// This structure holds the intermediate computations.
// Currently, the other table forms don't use this.
// Because of how table-specific this is, I don't think this can be pushed into StyleComputer itself.
enum DeferredStyleComputation {
Value { value: Value },
Header { text: String },
RowIndex { text: String },
Empty {},
}
impl DeferredStyleComputation {
// This is only run inside a par_extend().
fn compute(&self, config: &Config, style_computer: &StyleComputer) -> NuText {
match self {
DeferredStyleComputation::Value { value } => {
match value {
// Float precision is required here.
Value::Float { val, .. } => (
format!("{:.prec$}", val, prec = config.float_precision as usize),
style_computer.style_primitive(value),
),
_ => (
value.into_abbreviated_string(config),
style_computer.style_primitive(value),
),
}
}
DeferredStyleComputation::Header { text } => (
text.clone(),
TextStyle::with_style(
Alignment::Center,
style_computer
.compute("header", &Value::string(text.as_str(), Span::unknown())),
),
),
DeferredStyleComputation::RowIndex { text } => (
text.clone(),
TextStyle::with_style(
Alignment::Right,
style_computer
.compute("row_index", &Value::string(text.as_str(), Span::unknown())),
),
),
DeferredStyleComputation::Empty {} => (
"".into(),
TextStyle::with_style(
Alignment::Right,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
),
}
}
}
fn convert_to_table(
row_offset: usize,
input: &[Value],
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
head: Span,
color_hm: &NuColorMap,
style_computer: &StyleComputer,
) -> Result<Option<(NuTable, bool, bool)>, ShellError> {
let mut headers = get_columns(input);
let mut input = input.iter().peekable();
let float_precision = config.float_precision as usize;
let with_index = match config.table_index_mode {
TableIndexMode::Always => true,
TableIndexMode::Never => false,
@ -764,7 +841,9 @@ fn convert_to_table(
return Ok(None);
}
if !headers.is_empty() && with_index {
let with_header = !headers.is_empty();
if with_header && with_index {
headers.insert(0, "#".into());
}
@ -773,26 +852,18 @@ fn convert_to_table(
let headers: Vec<_> = headers
.into_iter()
.filter(|header| header != INDEX_COLUMN_NAME)
.map(|text| {
NuTable::create_cell(
text,
TextStyle {
alignment: Alignment::Center,
color_style: Some(color_hm["header"]),
},
)
})
.map(|text| DeferredStyleComputation::Header { text })
.collect();
let with_header = !headers.is_empty();
let mut count_columns = headers.len();
let mut data: Vec<Vec<_>> = if headers.is_empty() {
let mut data: Vec<Vec<_>> = if !with_header {
Vec::new()
} else {
vec![headers]
};
// Turn each item of each row into a DeferredStyleComputation for that item.
for (row_num, item) in input.enumerate() {
if nu_utils::ctrl_c::was_pressed(&ctrlc) {
return Ok(None);
@ -812,26 +883,36 @@ fn convert_to_table(
}
.unwrap_or_else(|| (row_num + row_offset).to_string());
let value = make_index_string(text, color_hm);
let value = NuTable::create_cell(value.0, value.1);
row.push(value);
row.push(DeferredStyleComputation::RowIndex { text });
}
if !with_header {
let text = item.into_abbreviated_string(config);
let text_type = item.get_type().to_string();
let value = make_styled_string(text, &text_type, color_hm, float_precision);
let value = NuTable::create_cell(value.0, value.1);
row.push(value);
row.push(DeferredStyleComputation::Value {
value: item.clone(),
});
} else {
let skip_num = usize::from(with_index);
// data[0] is used here because headers (the direct reference to it) has been moved.
for header in data[0].iter().skip(skip_num) {
let value =
create_table2_entry_basic(item, header.as_ref(), head, config, color_hm);
let value = NuTable::create_cell(value.0, value.1);
row.push(value);
if let DeferredStyleComputation::Header { text } = header {
row.push(match item {
Value::Record { .. } => {
let path = PathMember::String {
val: text.clone(),
span: head,
};
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => DeferredStyleComputation::Value { value: val },
Err(_) => DeferredStyleComputation::Empty {},
}
}
_ => DeferredStyleComputation::Value {
value: item.clone(),
},
});
}
}
}
@ -840,8 +921,25 @@ fn convert_to_table(
data.push(row);
}
let count_rows = data.len();
let table = NuTable::new(data, (count_rows, count_columns));
// All the computations are parallelised here.
// NOTE: It's currently not possible to Ctrl-C out of this...
let mut cells: Vec<Vec<_>> = Vec::with_capacity(data.len());
data.into_par_iter()
.map(|row| {
let mut new_row = Vec::with_capacity(row.len());
row.into_par_iter()
.map(|deferred| {
let pair = deferred.compute(config, style_computer);
NuTable::create_cell(pair.0, pair.1)
})
.collect_into_vec(&mut new_row);
new_row
})
.collect_into_vec(&mut cells);
let count_rows = cells.len();
let table = NuTable::new(cells, (count_rows, count_columns));
Ok(Some((table, with_header, with_index)))
}
@ -854,7 +952,7 @@ fn convert_to_table2<'a>(
ctrlc: Option<Arc<AtomicBool>>,
config: &Config,
head: Span,
color_hm: &NuColorMap,
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
@ -903,7 +1001,10 @@ fn convert_to_table2<'a>(
let mut column_width = 0;
if with_header {
data[0].push(NuTable::create_cell("#", header_style(color_hm)));
data[0].push(NuTable::create_cell(
"#",
header_style(style_computer, String::from("#")),
));
}
for (row, item) in input.clone().into_iter().enumerate() {
@ -920,7 +1021,7 @@ fn convert_to_table2<'a>(
.then(|| lookup_index_value(item, config).unwrap_or_else(|| index.to_string()))
.unwrap_or_else(|| index.to_string());
let value = make_index_string(text, color_hm);
let value = make_index_string(text, style_computer);
let width = string_width(&value.0);
column_width = max(column_width, width);
@ -952,7 +1053,7 @@ fn convert_to_table2<'a>(
item,
config,
&ctrlc,
color_hm,
style_computer,
deep,
flatten,
flatten_sep,
@ -994,7 +1095,10 @@ fn convert_to_table2<'a>(
let mut column_width = string_width(&header);
data[0].push(NuTable::create_cell(&header, header_style(color_hm)));
data[0].push(NuTable::create_cell(
header.clone(),
header_style(style_computer, header.clone()),
));
for (row, item) in input.clone().into_iter().enumerate() {
if nu_utils::ctrl_c::was_pressed(&ctrlc) {
@ -1007,11 +1111,11 @@ fn convert_to_table2<'a>(
let value = create_table2_entry(
item,
&header,
header.as_str(),
head,
config,
&ctrlc,
color_hm,
style_computer,
deep,
flatten,
flatten_sep,
@ -1039,7 +1143,7 @@ fn convert_to_table2<'a>(
return Ok(None);
}
let value = create_table2_entry_basic(item, &header, head, config, color_hm);
let value = create_table2_entry_basic(item, &header, head, config, style_computer);
let value = wrap_nu_text(value, available_width);
let value_width = string_width(&value.0);
@ -1064,7 +1168,7 @@ fn convert_to_table2<'a>(
return Ok(None);
}
let value = create_table2_entry_basic(item, &header, head, config, color_hm);
let value = create_table2_entry_basic(item, &header, head, config, style_computer);
let value = wrap_nu_text(value, OK_CELL_CONTENT_WIDTH);
let value = NuTable::create_cell(value.0, value.1);
@ -1134,10 +1238,11 @@ fn lookup_index_value(item: &Value, config: &Config) -> Option<String> {
.map(|value| value.into_string("", config))
}
fn header_style(color_hm: &NuColorMap) -> TextStyle {
fn header_style(style_computer: &StyleComputer, header: String) -> TextStyle {
let style = style_computer.compute("header", &Value::string(header.as_str(), Span::unknown()));
TextStyle {
alignment: Alignment::Center,
color_style: Some(color_hm["header"]),
color_style: Some(style),
}
}
@ -1147,7 +1252,7 @@ fn create_table2_entry_basic(
header: &str,
head: Span,
config: &Config,
color_hm: &NuColorMap,
style_computer: &StyleComputer,
) -> NuText {
match item {
Value::Record { .. } => {
@ -1156,11 +1261,11 @@ fn create_table2_entry_basic(
let val = item.clone().follow_cell_path(&[path], false);
match val {
Ok(val) => value_to_styled_string(&val, config, color_hm),
Err(_) => error_sign(color_hm),
Ok(val) => value_to_styled_string(&val, config, style_computer),
Err(_) => error_sign(style_computer),
}
}
_ => value_to_styled_string(item, config, color_hm),
_ => value_to_styled_string(item, config, style_computer),
}
}
@ -1171,7 +1276,7 @@ fn create_table2_entry(
head: Span,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
color_hm: &NuColorMap,
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
@ -1188,20 +1293,20 @@ fn create_table2_entry(
&val,
config,
ctrlc,
color_hm,
style_computer,
deep,
flatten,
flatten_sep,
width,
),
Err(_) => wrap_nu_text(error_sign(color_hm), width),
Err(_) => wrap_nu_text(error_sign(style_computer), width),
}
}
_ => convert_to_table2_entry(
item,
config,
ctrlc,
color_hm,
style_computer,
deep,
flatten,
flatten_sep,
@ -1210,8 +1315,8 @@ fn create_table2_entry(
}
}
fn error_sign(color_hm: &HashMap<String, nu_ansi_term::Style>) -> (String, TextStyle) {
make_styled_string(String::from(""), "empty", color_hm, 0)
fn error_sign(style_computer: &StyleComputer) -> (String, TextStyle) {
make_styled_string(style_computer, String::from(""), None, 0)
}
fn wrap_nu_text(mut text: NuText, width: usize) -> NuText {
@ -1224,7 +1329,9 @@ fn convert_to_table2_entry(
item: &Value,
config: &Config,
ctrlc: &Option<Arc<AtomicBool>>,
color_hm: &NuColorMap,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
deep: Option<usize>,
flatten: bool,
flatten_sep: &str,
@ -1232,13 +1339,13 @@ fn convert_to_table2_entry(
) -> NuText {
let is_limit_reached = matches!(deep, Some(0));
if is_limit_reached {
return wrap_nu_text(value_to_styled_string(item, config, color_hm), width);
return wrap_nu_text(value_to_styled_string(item, config, style_computer), width);
}
match &item {
Value::Record { span, cols, vals } => {
if cols.is_empty() && vals.is_empty() {
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
wrap_nu_text(value_to_styled_string(item, config, style_computer), width)
} else {
let table = convert_to_table2(
0,
@ -1246,7 +1353,7 @@ fn convert_to_table2_entry(
ctrlc.clone(),
config,
*span,
color_hm,
style_computer,
deep.map(|i| i - 1),
flatten,
flatten_sep,
@ -1257,7 +1364,7 @@ fn convert_to_table2_entry(
table.and_then(|(table, with_header, with_index)| {
let table_config = create_table_config(
config,
color_hm,
style_computer,
table.count_rows(),
with_header,
with_index,
@ -1272,7 +1379,7 @@ fn convert_to_table2_entry(
(table, TextStyle::default())
} else {
// error so back down to the default
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
wrap_nu_text(value_to_styled_string(item, config, style_computer), width)
}
}
}
@ -1283,7 +1390,7 @@ fn convert_to_table2_entry(
if flatten && is_simple_list {
wrap_nu_text(
convert_value_list_to_string(vals, config, color_hm, flatten_sep),
convert_value_list_to_string(vals, config, style_computer, flatten_sep),
width,
)
} else {
@ -1293,7 +1400,7 @@ fn convert_to_table2_entry(
ctrlc.clone(),
config,
*span,
color_hm,
style_computer,
deep.map(|i| i - 1),
flatten,
flatten_sep,
@ -1304,7 +1411,7 @@ fn convert_to_table2_entry(
table.and_then(|(table, with_header, with_index)| {
let table_config = create_table_config(
config,
color_hm,
style_computer,
table.count_rows(),
with_header,
with_index,
@ -1320,23 +1427,25 @@ fn convert_to_table2_entry(
} else {
// error so back down to the default
wrap_nu_text(value_to_styled_string(item, config, color_hm), width)
wrap_nu_text(value_to_styled_string(item, config, style_computer), width)
}
}
}
_ => wrap_nu_text(value_to_styled_string(item, config, color_hm), width), // unknown type.
_ => wrap_nu_text(value_to_styled_string(item, config, style_computer), width), // unknown type.
}
}
fn convert_value_list_to_string(
vals: &[Value],
config: &Config,
color_hm: &NuColorMap,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
flatten_sep: &str,
) -> NuText {
let mut buf = Vec::new();
for value in vals {
let (text, _) = value_to_styled_string(value, config, color_hm);
let (text, _) = value_to_styled_string(value, config, style_computer);
buf.push(text);
}
@ -1344,39 +1453,58 @@ fn convert_value_list_to_string(
(text, TextStyle::default())
}
fn value_to_styled_string(value: &Value, config: &Config, color_hm: &NuColorMap) -> NuText {
fn value_to_styled_string(
value: &Value,
config: &Config,
// This is passed in, even though it could be retrieved from config,
// to save reallocation (because it's presumably being used upstream).
style_computer: &StyleComputer,
) -> NuText {
let float_precision = config.float_precision as usize;
make_styled_string(
style_computer,
value.into_abbreviated_string(config),
&value.get_type().to_string(),
color_hm,
Some(value),
float_precision,
)
}
fn make_styled_string(
style_computer: &StyleComputer,
text: String,
text_type: &str,
color_hm: &NuColorMap,
value: Option<&Value>, // None represents table holes.
float_precision: usize,
) -> NuText {
if text_type == "float" {
// set dynamic precision from config
let precise_number = match convert_with_precision(&text, float_precision) {
Ok(num) => num,
Err(e) => e.to_string(),
};
(precise_number, style_primitive(text_type, color_hm))
} else {
(text, style_primitive(text_type, color_hm))
match value {
Some(value) => {
match value {
Value::Float { .. } => {
// set dynamic precision from config
let precise_number = match convert_with_precision(&text, float_precision) {
Ok(num) => num,
Err(e) => e.to_string(),
};
(precise_number, style_computer.style_primitive(value))
}
_ => (text, style_computer.style_primitive(value)),
}
}
None => {
// Though holes are not the same as null, the closure for "empty" is passed a null anyway.
(
text,
TextStyle::with_style(
Alignment::Center,
style_computer.compute("empty", &Value::nothing(Span::unknown())),
),
)
}
}
}
fn make_index_string(text: String, color_hm: &NuColorMap) -> NuText {
let style = TextStyle::new()
.alignment(Alignment::Right)
.style(color_hm["row_index"]);
(text, style)
fn make_index_string(text: String, style_computer: &StyleComputer) -> NuText {
let style = style_computer.compute("row_index", &Value::string(text.as_str(), Span::unknown()));
(text, TextStyle::with_style(Alignment::Right, style))
}
fn convert_with_precision(val: &str, precision: usize) -> Result<String, ShellError> {
@ -1399,8 +1527,9 @@ fn convert_with_precision(val: &str, precision: usize) -> Result<String, ShellEr
struct PagingTableCreator {
head: Span,
stream: ListStream,
engine_state: EngineState,
stack: Stack,
ctrlc: Option<Arc<AtomicBool>>,
config: Config,
row_offset: usize,
width_param: Option<i64>,
view: TableView,
@ -1408,7 +1537,7 @@ struct PagingTableCreator {
impl PagingTableCreator {
fn build_extended(
&self,
&mut self,
batch: &[Value],
limit: Option<usize>,
flatten: bool,
@ -1418,17 +1547,18 @@ impl PagingTableCreator {
return Ok(None);
}
let config = self.engine_state.get_config();
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let term_width = get_width_param(self.width_param);
let color_hm = get_color_config(&self.config);
let theme = load_theme_from_config(&self.config);
let theme = load_theme_from_config(config);
let table = convert_to_table2(
self.row_offset,
batch.iter(),
self.ctrlc.clone(),
&self.config,
config,
self.head,
&color_hm,
&style_computer,
limit,
flatten,
flatten_separator.as_deref().unwrap_or(" "),
@ -1443,8 +1573,8 @@ impl PagingTableCreator {
table.truncate(term_width, &theme);
let table_config = create_table_config(
&self.config,
&color_hm,
config,
&style_computer,
table.count_rows(),
with_header,
with_index,
@ -1476,16 +1606,17 @@ impl PagingTableCreator {
Ok(table)
}
fn build_collapsed(&self, batch: Vec<Value>) -> Result<Option<String>, ShellError> {
fn build_collapsed(&mut self, batch: Vec<Value>) -> Result<Option<String>, ShellError> {
if batch.is_empty() {
return Ok(None);
}
let color_hm = get_color_config(&self.config);
let theme = load_theme_from_config(&self.config);
let config = self.engine_state.get_config();
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let theme = load_theme_from_config(config);
let term_width = get_width_param(self.width_param);
let need_footer = matches!(self.config.footer_mode, FooterMode::RowCount(limit) if batch.len() as u64 > limit)
|| matches!(self.config.footer_mode, FooterMode::Always);
let need_footer = matches!(config.footer_mode, FooterMode::RowCount(limit) if batch.len() as u64 > limit)
|| matches!(config.footer_mode, FooterMode::Always);
let value = Value::List {
vals: batch,
span: Span::new(0, 0),
@ -1495,8 +1626,8 @@ impl PagingTableCreator {
value,
true,
term_width,
&self.config,
&color_hm,
config,
&style_computer,
&theme,
need_footer,
);
@ -1504,17 +1635,17 @@ impl PagingTableCreator {
Ok(table.draw())
}
fn build_general(&self, batch: &[Value]) -> Result<Option<String>, ShellError> {
fn build_general(&mut self, batch: &[Value]) -> Result<Option<String>, ShellError> {
let term_width = get_width_param(self.width_param);
let color_hm = get_color_config(&self.config);
let config = &self.engine_state.get_config();
let style_computer = StyleComputer::from_config(&self.engine_state, &self.stack);
let table = convert_to_table(
self.row_offset,
batch,
self.ctrlc.clone(),
&self.config,
config,
self.head,
&color_hm,
&style_computer,
)?;
let (table, with_header, with_index) = match table {
@ -1523,8 +1654,8 @@ impl PagingTableCreator {
};
let table_config = create_table_config(
&self.config,
&color_hm,
config,
&style_computer,
table.count_rows(),
with_header,
with_index,
@ -1584,8 +1715,8 @@ impl Iterator for PagingTableCreator {
match table {
Ok(Some(table)) => {
let table =
strip_output_color(Some(table), &self.config).expect("must never happen");
let table = strip_output_color(Some(table), self.engine_state.get_config())
.expect("must never happen");
let mut bytes = table.as_bytes().to_vec();
bytes.push(b'\n'); // nu-table tables don't come with a newline on the end
@ -1686,7 +1817,7 @@ fn strip_output_color(output: Option<String>, config: &Config) -> Option<String>
fn create_table_config(
config: &Config,
color_hm: &HashMap<String, nu_ansi_term::Style>,
style_computer: &StyleComputer,
count_records: usize,
with_header: bool,
with_index: bool,
@ -1697,10 +1828,7 @@ fn create_table_config(
let mut table_cfg = TableConfig::new(theme, with_header, with_index, append_footer);
let sep_color = lookup_separator_color(color_hm);
if let Some(color) = sep_color {
table_cfg = table_cfg.splitline_style(color);
}
table_cfg = table_cfg.splitline_style(lookup_separator_color(style_computer));
if expand {
table_cfg = table_cfg.expand();
@ -1709,10 +1837,8 @@ fn create_table_config(
table_cfg.trim(config.trim_strategy.clone())
}
fn lookup_separator_color(
color_hm: &HashMap<String, nu_ansi_term::Style>,
) -> Option<nu_ansi_term::Style> {
color_hm.get("separator").cloned()
fn lookup_separator_color(style_computer: &StyleComputer) -> nu_ansi_term::Style {
style_computer.compute("separator", &Value::nothing(Span::unknown()))
}
fn with_footer(config: &Config, with_header: bool, count_records: usize) -> bool {