use promkit_core::{
crossterm::style::{Attribute, ContentStyle},
grapheme::StyledGraphemes,
};
use super::jsonz::{JsonNode, Row};
use crate::structured::{ContainerNode, ContainerType};
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OverflowMode {
#[default]
Truncate,
Wrap,
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(default))]
#[derive(Clone)]
pub struct Config {
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub curly_brackets_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub square_brackets_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub key_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub string_value_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub number_value_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub boolean_value_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::content_style_serde")
)]
pub null_value_style: ContentStyle,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::attribute_serde")
)]
pub active_item_attribute: Attribute,
#[cfg_attr(
feature = "serde",
serde(with = "termcfg::crossterm_config::attribute_serde")
)]
pub inactive_item_attribute: Attribute,
pub indent: usize,
pub overflow_mode: OverflowMode,
pub lines: Option<usize>,
pub show_line_numbers: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
curly_brackets_style: Default::default(),
square_brackets_style: Default::default(),
key_style: Default::default(),
string_value_style: Default::default(),
number_value_style: Default::default(),
boolean_value_style: Default::default(),
null_value_style: Default::default(),
active_item_attribute: Attribute::NoBold,
inactive_item_attribute: Attribute::NoBold,
indent: Default::default(),
overflow_mode: OverflowMode::default(),
lines: Default::default(),
show_line_numbers: false,
}
}
}
impl Config {
pub fn render_terminal_rows(&self, rows: &[Row], width: u16) -> Vec<StyledGraphemes> {
self.render_rows(rows, 0, Some(width as usize))
}
pub fn render_content_rows(&self, rows: &[Row], active_row: usize) -> Vec<StyledGraphemes> {
self.render_rows(rows, active_row, None)
}
fn render_rows(
&self,
rows: &[Row],
active_row: usize,
width: Option<usize>,
) -> Vec<StyledGraphemes> {
let mut formatted = Vec::new();
for (i, row) in rows.iter().enumerate() {
let indent = StyledGraphemes::from(" ".repeat(self.indent * row.depth));
let mut parts = Vec::new();
if let Some(key) = &row.key {
parts.push(
StyledGraphemes::from(format!("\"{}\"", key)).apply_style(self.key_style),
);
parts.push(StyledGraphemes::from(": "));
}
match &row.node {
JsonNode::Null => {
parts.push(StyledGraphemes::from("null").apply_style(self.null_value_style));
}
JsonNode::Boolean(b) => {
parts.push(
StyledGraphemes::from(b.to_string()).apply_style(self.boolean_value_style),
);
}
JsonNode::Number(n) => {
parts.push(
StyledGraphemes::from(n.to_string()).apply_style(self.number_value_style),
);
}
JsonNode::String(s) => {
let escaped = s.replace('\n', "\\n");
parts.push(
StyledGraphemes::from(format!("\"{}\"", escaped))
.apply_style(self.string_value_style),
);
}
JsonNode::Container(node) => match node {
ContainerNode::Empty { typ } => {
let bracket_style = match typ {
ContainerType::Object => self.curly_brackets_style,
ContainerType::Array => self.square_brackets_style,
};
parts.push(
StyledGraphemes::from(typ.empty_str()).apply_style(bracket_style),
);
}
ContainerNode::Open { typ, collapsed, .. } => {
let bracket_style = match typ {
ContainerType::Object => self.curly_brackets_style,
ContainerType::Array => self.square_brackets_style,
};
if *collapsed {
parts.push(
StyledGraphemes::from(typ.collapsed_preview())
.apply_style(bracket_style),
);
} else {
parts.push(
StyledGraphemes::from(typ.open_str()).apply_style(bracket_style),
);
}
}
ContainerNode::Close { typ, .. } => {
let bracket_style = match typ {
ContainerType::Object => self.curly_brackets_style,
ContainerType::Array => self.square_brackets_style,
};
parts.push(
StyledGraphemes::from(typ.close_str()).apply_style(bracket_style),
);
}
},
}
if i + 1 < rows.len() {
let next_is_close = matches!(
&rows[i + 1].node,
JsonNode::Container(ContainerNode::Close { .. })
);
let current_is_open = matches!(
&rows[i].node,
JsonNode::Container(ContainerNode::Open {
collapsed: false,
..
})
);
if !next_is_close && !current_is_open {
parts.push(StyledGraphemes::from(","));
}
}
let mut content: StyledGraphemes = parts.into_iter().collect();
content = content.apply_attribute(if i == active_row {
self.active_item_attribute
} else {
self.inactive_item_attribute
});
let mut line: StyledGraphemes = vec![indent, content].into_iter().collect();
if let Some(width) = width {
match self.overflow_mode {
OverflowMode::Truncate => {
line =
line.truncated_line_with_ellipsis(width, &StyledGraphemes::from("…"));
formatted.push(line);
}
OverflowMode::Wrap => {
formatted.extend(line.wrapped_lines(width));
}
}
} else {
formatted.push(line);
}
}
formatted
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
mod config {
use super::*;
mod render_terminal_rows {
use super::*;
use crate::structured::json::jsonz::create_rows;
#[test]
fn truncate_mode_appends_an_ellipsis() {
let value = json!({
"very_long_key": "abcdefghijklmnopqrstuvwxyz",
});
let rows = create_rows([&value]);
let width = 12;
let lines = Config {
indent: 2,
overflow_mode: OverflowMode::Truncate,
..Default::default()
}
.render_terminal_rows(&rows, width);
assert_eq!(lines.len(), rows.len());
assert!(lines.iter().all(|line| line.widths() <= width as usize));
assert!(
lines
.iter()
.any(|line| line.chars().last().is_some_and(|ch| *ch == '…'))
);
}
#[test]
fn wrap_mode_wraps_without_an_ellipsis() {
let value = json!({
"very_long_key": "abcdefghijklmnopqrstuvwxyz",
});
let rows = create_rows([&value]);
let width = 12;
let lines = Config {
indent: 2,
overflow_mode: OverflowMode::Wrap,
..Default::default()
}
.render_terminal_rows(&rows, width);
assert!(lines.len() > rows.len());
assert!(lines.iter().all(|line| line.widths() <= width as usize));
assert!(
lines
.iter()
.all(|line| !matches!(line.chars().last(), Some('…')))
);
}
}
#[cfg(feature = "serde")]
mod deserialize {
use super::*;
use promkit_core::crossterm::style::{Attributes, Color};
#[test]
fn missing_new_fields_are_filled_by_default() {
let mut value = serde_json::to_value(Config {
indent: 4,
..Default::default()
})
.unwrap();
let obj = value.as_object_mut().unwrap();
obj.remove("active_item_attribute");
obj.remove("inactive_item_attribute");
obj.remove("overflow_mode");
obj.remove("lines");
obj.remove("show_line_numbers");
let formatter: Config = serde_json::from_value(value).unwrap();
assert_eq!(formatter.indent, 4);
assert_eq!(formatter.active_item_attribute, Attribute::NoBold);
assert_eq!(formatter.inactive_item_attribute, Attribute::NoBold);
assert_eq!(formatter.overflow_mode, OverflowMode::Truncate);
assert_eq!(formatter.lines, None);
assert!(!formatter.show_line_numbers);
}
#[test]
fn loads_all_fields_from_toml() {
let input = r#"
indent = 4
lines = 7
show_line_numbers = true
curly_brackets_style = "attr=bold"
square_brackets_style = "attr=bold"
key_style = "fg=cyan"
string_value_style = "fg=green"
number_value_style = "fg=yellow"
boolean_value_style = "fg=magenta"
null_value_style = "fg=grey"
active_item_attribute = "underlined"
inactive_item_attribute = "dim"
overflow_mode = "Wrap"
"#;
let formatter: Config = toml::from_str(input).unwrap();
assert_eq!(formatter.indent, 4);
assert_eq!(formatter.lines, Some(7));
assert!(formatter.show_line_numbers);
assert_eq!(
formatter.curly_brackets_style.attributes,
Attributes::from(Attribute::Bold),
);
assert_eq!(
formatter.square_brackets_style.attributes,
Attributes::from(Attribute::Bold),
);
assert_eq!(formatter.key_style.foreground_color, Some(Color::Cyan));
assert_eq!(
formatter.string_value_style.foreground_color,
Some(Color::Green),
);
assert_eq!(
formatter.number_value_style.foreground_color,
Some(Color::Yellow)
);
assert_eq!(
formatter.boolean_value_style.foreground_color,
Some(Color::Magenta),
);
assert_eq!(
formatter.null_value_style.foreground_color,
Some(Color::Grey)
);
assert_eq!(formatter.active_item_attribute, Attribute::Underlined);
assert_eq!(formatter.inactive_item_attribute, Attribute::Dim);
assert_eq!(formatter.overflow_mode, OverflowMode::Wrap);
}
}
}
}