nushell/crates/nu-protocol/src/config/table.rs
Ian Manske 68fcd71898
Add Value::coerce_str (#11885)
# Description
Following #11851, this PR adds one final conversion function for
`Value`. `Value::coerce_str` takes a `&Value` and converts it to a
`Cow<str>`, creating an owned `String` for types that needed converting.
Otherwise, it returns a borrowed `str` for `String` and `Binary`
`Value`s which avoids a clone/allocation. Where possible, `coerce_str`
and `coerce_into_string` should be used instead of `coerce_string`,
since `coerce_string` always allocates a new `String`.
2024-02-18 17:47:10 +01:00

334 lines
11 KiB
Rust

use super::helper::ReconstructVal;
use crate::{record, Config, ShellError, Span, Value};
use serde::{Deserialize, Serialize};
use std::str::FromStr;
#[derive(Serialize, Deserialize, Clone, Copy, Debug, Default)]
pub enum TableMode {
Basic,
Thin,
Light,
Compact,
WithLove,
CompactDouble,
#[default]
Rounded,
Reinforced,
Heavy,
None,
Psql,
Markdown,
Dots,
Restructured,
AsciiRounded,
BasicCompact,
}
impl FromStr for TableMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"basic" => Ok(Self::Basic),
"thin" => Ok(Self::Thin),
"light" => Ok(Self::Light),
"compact" => Ok(Self::Compact),
"with_love" => Ok(Self::WithLove),
"compact_double" => Ok(Self::CompactDouble),
"default" => Ok(TableMode::default()),
"rounded" => Ok(Self::Rounded),
"reinforced" => Ok(Self::Reinforced),
"heavy" => Ok(Self::Heavy),
"none" => Ok(Self::None),
"psql" => Ok(Self::Psql),
"markdown" => Ok(Self::Markdown),
"dots" => Ok(Self::Dots),
"restructured" => Ok(Self::Restructured),
"ascii_rounded" => Ok(Self::AsciiRounded),
"basic_compact" => Ok(Self::BasicCompact),
_ => Err("expected either 'basic', 'thin', 'light', 'compact', 'with_love', 'compact_double', 'rounded', 'reinforced', 'heavy', 'none', 'psql', 'markdown', 'dots', 'restructured', 'ascii_rounded', or 'basic_compact'"),
}
}
}
impl ReconstructVal for TableMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
TableMode::Basic => "basic",
TableMode::Thin => "thin",
TableMode::Light => "light",
TableMode::Compact => "compact",
TableMode::WithLove => "with_love",
TableMode::CompactDouble => "compact_double",
TableMode::Rounded => "rounded",
TableMode::Reinforced => "reinforced",
TableMode::Heavy => "heavy",
TableMode::None => "none",
TableMode::Psql => "psql",
TableMode::Markdown => "markdown",
TableMode::Dots => "dots",
TableMode::Restructured => "restructured",
TableMode::AsciiRounded => "ascii_rounded",
TableMode::BasicCompact => "basic_compact",
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum FooterMode {
/// Never show the footer
Never,
/// Always show the footer
Always,
/// Only show the footer if there are more than RowCount rows
RowCount(u64),
/// Calculate the screen height, calculate row count, if display will be bigger than screen, add the footer
Auto,
}
impl FromStr for FooterMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"always" => Ok(FooterMode::Always),
"never" => Ok(FooterMode::Never),
"auto" => Ok(FooterMode::Auto),
x => {
if let Ok(count) = x.parse() {
Ok(FooterMode::RowCount(count))
} else {
Err("expected either 'never', 'always', 'auto' or a row count")
}
}
}
}
}
impl ReconstructVal for FooterMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
FooterMode::Always => "always".to_string(),
FooterMode::Never => "never".to_string(),
FooterMode::Auto => "auto".to_string(),
FooterMode::RowCount(c) => c.to_string(),
},
span,
)
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TableIndexMode {
/// Always show indexes
Always,
/// Never show indexes
Never,
/// Show indexes when a table has "index" column
Auto,
}
impl FromStr for TableIndexMode {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_ascii_lowercase().as_str() {
"always" => Ok(TableIndexMode::Always),
"never" => Ok(TableIndexMode::Never),
"auto" => Ok(TableIndexMode::Auto),
_ => Err("expected either 'never', 'always' or 'auto'"),
}
}
}
impl ReconstructVal for TableIndexMode {
fn reconstruct_value(&self, span: Span) -> Value {
Value::string(
match self {
TableIndexMode::Always => "always",
TableIndexMode::Never => "never",
TableIndexMode::Auto => "auto",
},
span,
)
}
}
/// A Table view configuration, for a situation where
/// we need to limit cell width in order to adjust for a terminal size.
#[derive(Serialize, Deserialize, Clone, Debug)]
pub enum TrimStrategy {
/// Wrapping strategy.
///
/// It it's similar to original nu_table, strategy.
Wrap {
/// A flag which indicates whether is it necessary to try
/// to keep word boundaries.
try_to_keep_words: bool,
},
/// Truncating strategy, where we just cut the string.
/// And append the suffix if applicable.
Truncate {
/// Suffix which can be appended to a truncated string after being cut.
///
/// It will be applied only when there's enough room for it.
/// For example in case where a cell width must be 12 chars, but
/// the suffix takes 13 chars it won't be used.
suffix: Option<String>,
},
}
impl TrimStrategy {
pub fn wrap(dont_split_words: bool) -> Self {
Self::Wrap {
try_to_keep_words: dont_split_words,
}
}
pub fn truncate(suffix: Option<String>) -> Self {
Self::Truncate { suffix }
}
}
impl Default for TrimStrategy {
fn default() -> Self {
TrimStrategy::Wrap {
try_to_keep_words: true,
}
}
}
pub(super) fn try_parse_trim_strategy(
value: &Value,
errors: &mut Vec<ShellError>,
) -> Result<TrimStrategy, ShellError> {
let map = value.as_record().map_err(|e| ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim is not a record".into(),
span: Some(value.span()),
help: Some("Please consult the documentation for configuring Nushell.".into()),
inner: vec![e],
})?;
let mut methodology = match map.get("methodology") {
Some(value) => match try_parse_trim_methodology(value) {
Some(methodology) => methodology,
None => return Ok(TrimStrategy::default()),
},
None => {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.methodology was not provided".into(),
span: Some(value.span()),
help: Some("Please consult the documentation for configuring Nushell.".into()),
inner: vec![],
});
return Ok(TrimStrategy::default());
}
};
match &mut methodology {
TrimStrategy::Wrap { try_to_keep_words } => {
if let Some(value) = map.get("wrapping_try_keep_words") {
if let Ok(b) = value.as_bool() {
*try_to_keep_words = b;
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.wrapping_try_keep_words is not a bool".into(),
span: Some(value.span()),
help: Some(
"Please consult the documentation for configuring Nushell.".into(),
),
inner: vec![],
});
}
}
}
TrimStrategy::Truncate { suffix } => {
if let Some(value) = map.get("truncating_suffix") {
if let Ok(v) = value.coerce_string() {
*suffix = Some(v);
} else {
errors.push(ShellError::GenericError {
error: "Error while applying config changes".into(),
msg: "$env.config.table.trim.truncating_suffix is not a string".into(),
span: Some(value.span()),
help: Some(
"Please consult the documentation for configuring Nushell.".into(),
),
inner: vec![],
});
}
}
}
}
Ok(methodology)
}
fn try_parse_trim_methodology(value: &Value) -> Option<TrimStrategy> {
if let Ok(value) = value.coerce_str() {
match value.to_lowercase().as_str() {
"wrapping" => {
return Some(TrimStrategy::Wrap {
try_to_keep_words: false,
});
}
"truncating" => return Some(TrimStrategy::Truncate { suffix: None }),
_ => eprintln!("unrecognized $config.table.trim.methodology value; expected either 'truncating' or 'wrapping'"),
}
} else {
eprintln!("$env.config.table.trim.methodology is not a string")
}
None
}
pub(super) fn reconstruct_trim_strategy(config: &Config, span: Span) -> Value {
match &config.trim_strategy {
TrimStrategy::Wrap { try_to_keep_words } => Value::record(
record! {
"methodology" => Value::string("wrapping", span),
"wrapping_try_keep_words" => Value::bool(*try_to_keep_words, span),
},
span,
),
TrimStrategy::Truncate { suffix } => Value::record(
match suffix {
Some(s) => record! {
"methodology" => Value::string("truncating", span),
"truncating_suffix" => Value::string(s.clone(), span),
},
None => record! {
"methodology" => Value::string("truncating", span),
"truncating_suffix" => Value::nothing(span),
},
},
span,
),
}
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct TableIndent {
pub left: usize,
pub right: usize,
}
pub(super) fn reconstruct_padding(config: &Config, span: Span) -> Value {
// For better completions always reconstruct the record version even though unsigned int would
// be supported, `as` conversion is sane as it came from an i64 original
Value::record(
record!(
"left" => Value::int(config.table_indent.left as i64, span),
"right" => Value::int(config.table_indent.right as i64, span),
),
span,
)
}