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,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());
}

View File

@ -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::*;

View File

@ -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(

View 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"));
}

View 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(())
}
}