Skip to main content

sparcli/output/
kv.rs

1//! Aligned key-value lists.
2
3use crate::core::geometry::Edges;
4use crate::core::render::{Renderable, Rendered};
5use crate::core::style::Style;
6use crate::core::text::{Line, Span, Text};
7use crate::core::theme::theme;
8use crate::core::width::{visible_width, wrap};
9use crate::output::compose::pad;
10
11/// One key-value pair.
12struct Pair {
13    key: String,
14    value: Text,
15}
16
17/// A list of aligned key-value pairs.
18///
19/// # Examples
20///
21/// ```
22/// use sparcli::{KeyValue, Renderable};
23///
24/// let out = KeyValue::new()
25///     .add("name", "sparcli")
26///     .add("license", "MIT")
27///     .render(40);
28/// assert!(out.plain().contains("sparcli"));
29/// ```
30pub struct KeyValue {
31    pairs: Vec<Pair>,
32    separator: String,
33    key_width: Option<u16>,
34    key_style: Style,
35    value_style: Style,
36    item_gap: u16,
37    wrap_values: bool,
38    margin: Edges,
39}
40
41impl Default for KeyValue {
42    fn default() -> Self {
43        Self {
44            pairs: Vec::new(),
45            separator: "  ".to_string(),
46            key_width: None,
47            key_style: Style::new().bold(),
48            value_style: theme().secondary,
49            item_gap: 0,
50            wrap_values: false,
51            margin: Edges::default(),
52        }
53    }
54}
55
56impl KeyValue {
57    /// Creates an empty key-value list.
58    pub fn new() -> Self {
59        Self::default()
60    }
61
62    /// Adds a key-value pair.
63    #[must_use]
64    pub fn add(
65        mut self,
66        key: impl Into<String>,
67        value: impl Into<Text>,
68    ) -> Self {
69        self.pairs.push(Pair {
70            key: key.into(),
71            value: value.into(),
72        });
73        self
74    }
75
76    /// Sets the separator between key and value.
77    #[must_use]
78    pub fn separator(mut self, separator: impl Into<String>) -> Self {
79        self.separator = separator.into();
80        self
81    }
82
83    /// Sets a fixed key column width.
84    #[must_use]
85    pub fn key_width(mut self, width: u16) -> Self {
86        self.key_width = Some(width);
87        self
88    }
89
90    /// Sets the key style.
91    #[must_use]
92    pub fn key_style(mut self, style: Style) -> Self {
93        self.key_style = style;
94        self
95    }
96
97    /// Sets the value style.
98    #[must_use]
99    pub fn value_style(mut self, style: Style) -> Self {
100        self.value_style = style;
101        self
102    }
103
104    /// Sets the number of blank lines between pairs.
105    #[must_use]
106    pub fn item_gap(mut self, gap: u16) -> Self {
107        self.item_gap = gap;
108        self
109    }
110
111    /// Enables wrapping of long values.
112    #[must_use]
113    pub fn wrap_values(mut self, wrap: bool) -> Self {
114        self.wrap_values = wrap;
115        self
116    }
117
118    /// Sets the outer margin.
119    #[must_use]
120    pub fn margin(mut self, margin: Edges) -> Self {
121        self.margin = margin;
122        self
123    }
124
125    /// Returns the effective key column width.
126    fn resolved_key_width(&self) -> usize {
127        match self.key_width {
128            Some(width) => width as usize,
129            None => self
130                .pairs
131                .iter()
132                .map(|pair| visible_width(&pair.key))
133                .max()
134                .unwrap_or(0),
135        }
136    }
137}
138
139impl Renderable for KeyValue {
140    fn render(&self, max_width: u16) -> Rendered {
141        let key_width = self.resolved_key_width();
142        let prefix_width = key_width + visible_width(&self.separator);
143        let value_width =
144            (max_width as usize).saturating_sub(prefix_width).max(1);
145        let mut lines = Vec::new();
146        for (index, pair) in self.pairs.iter().enumerate() {
147            if index > 0 {
148                push_gap(&mut lines, self.item_gap);
149            }
150            self.push_pair(&mut lines, pair, key_width, value_width);
151        }
152        pad(&Rendered::new(lines), self.margin)
153    }
154}
155
156impl KeyValue {
157    /// Renders one pair into `lines`, wrapping the value when enabled.
158    fn push_pair(
159        &self,
160        lines: &mut Vec<Line>,
161        pair: &Pair,
162        key_width: usize,
163        value_width: usize,
164    ) {
165        let value_lines = self.value_lines(pair, value_width);
166        for (row, value_line) in value_lines.into_iter().enumerate() {
167            let key_cell = if row == 0 {
168                pair.key.clone()
169            } else {
170                String::new()
171            };
172            lines.push(self.compose_line(&key_cell, key_width, value_line));
173        }
174    }
175
176    /// Splits a value into one or more display lines.
177    fn value_lines(&self, pair: &Pair, value_width: usize) -> Vec<Line> {
178        if !self.wrap_values {
179            return pair.value.lines.clone();
180        }
181        let mut out = Vec::new();
182        for line in &pair.value.lines {
183            for chunk in wrap(&line.plain(), value_width) {
184                out.push(Line::styled(chunk, self.value_style));
185            }
186        }
187        out
188    }
189
190    /// Composes a key cell, separator and value line into one line.
191    fn compose_line(
192        &self,
193        key: &str,
194        key_width: usize,
195        value_line: Line,
196    ) -> Line {
197        let key_pad = key_width.saturating_sub(visible_width(key));
198        let mut spans = vec![
199            Span::styled(key.to_string(), self.key_style),
200            Span::raw(" ".repeat(key_pad)),
201            Span::raw(self.separator.clone()),
202        ];
203        for mut span in value_line.spans {
204            span.style = self.value_style.patch(span.style);
205            spans.push(span);
206        }
207        Line::new(spans)
208    }
209}
210
211/// Pushes `count` blank lines.
212fn push_gap(lines: &mut Vec<Line>, count: u16) {
213    for _ in 0..count {
214        lines.push(Line::default());
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221
222    fn plain(rendered: &Rendered) -> Vec<String> {
223        rendered.lines.iter().map(Line::plain).collect()
224    }
225
226    #[test]
227    fn aligns_keys_to_the_widest() {
228        let kv = KeyValue::new().add("a", "1").add("name", "2");
229        let lines = plain(&kv.render(40));
230        assert_eq!(lines[0], "a     1");
231        assert_eq!(lines[1], "name  2");
232    }
233
234    #[test]
235    fn wraps_long_values_when_enabled() {
236        let kv = KeyValue::new()
237            .wrap_values(true)
238            .add("k", "one two three four");
239        let lines = plain(&kv.render(10));
240        assert!(lines.len() > 1);
241    }
242}