promkit_widgets/jsonstream/
config.rs1use promkit_core::{
2 crossterm::style::{Attribute, ContentStyle},
3 grapheme::StyledGraphemes,
4};
5
6use super::jsonz::{ContainerType, Row, Value};
7
8#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
12pub enum OverflowMode {
13 #[default]
14 Truncate,
17 Wrap,
20}
21
22#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
23#[cfg_attr(feature = "serde", serde(default))]
24#[derive(Clone)]
25pub struct Config {
26 #[cfg_attr(
28 feature = "serde",
29 serde(with = "termcfg::crossterm_config::content_style_serde")
30 )]
31 pub curly_brackets_style: ContentStyle,
32 #[cfg_attr(
34 feature = "serde",
35 serde(with = "termcfg::crossterm_config::content_style_serde")
36 )]
37 pub square_brackets_style: ContentStyle,
38 #[cfg_attr(
40 feature = "serde",
41 serde(with = "termcfg::crossterm_config::content_style_serde")
42 )]
43 pub key_style: ContentStyle,
44 #[cfg_attr(
46 feature = "serde",
47 serde(with = "termcfg::crossterm_config::content_style_serde")
48 )]
49 pub string_value_style: ContentStyle,
50 #[cfg_attr(
52 feature = "serde",
53 serde(with = "termcfg::crossterm_config::content_style_serde")
54 )]
55 pub number_value_style: ContentStyle,
56 #[cfg_attr(
58 feature = "serde",
59 serde(with = "termcfg::crossterm_config::content_style_serde")
60 )]
61 pub boolean_value_style: ContentStyle,
62 #[cfg_attr(
64 feature = "serde",
65 serde(with = "termcfg::crossterm_config::content_style_serde")
66 )]
67 pub null_value_style: ContentStyle,
68
69 #[cfg_attr(
71 feature = "serde",
72 serde(with = "termcfg::crossterm_config::attribute_serde")
73 )]
74 pub active_item_attribute: Attribute,
75 #[cfg_attr(
77 feature = "serde",
78 serde(with = "termcfg::crossterm_config::attribute_serde")
79 )]
80 pub inactive_item_attribute: Attribute,
81
82 pub indent: usize,
87
88 pub overflow_mode: OverflowMode,
90 pub lines: Option<usize>,
92}
93
94impl Default for Config {
95 fn default() -> Self {
96 Self {
97 curly_brackets_style: Default::default(),
98 square_brackets_style: Default::default(),
99 key_style: Default::default(),
100 string_value_style: Default::default(),
101 number_value_style: Default::default(),
102 boolean_value_style: Default::default(),
103 null_value_style: Default::default(),
104 active_item_attribute: Attribute::NoBold,
105 inactive_item_attribute: Attribute::NoBold,
106 indent: Default::default(),
107 overflow_mode: OverflowMode::default(),
108 lines: Default::default(),
109 }
110 }
111}
112
113impl Config {
114 fn truncate_line_with_ellipsis(line: StyledGraphemes, width: usize) -> StyledGraphemes {
115 if line.widths() <= width {
116 return line;
117 }
118
119 if width == 0 {
120 return StyledGraphemes::default();
121 }
122
123 let ellipsis: StyledGraphemes = StyledGraphemes::from("…");
124 let ellipsis_width = ellipsis.widths();
125 if width <= ellipsis_width {
126 return ellipsis;
127 }
128
129 let mut truncated = StyledGraphemes::default();
130 let mut current_width = 0;
131 for g in line.iter() {
132 if current_width + g.width() + ellipsis_width > width {
133 break;
134 }
135 truncated.push_back(g.clone());
136 current_width += g.width();
137 }
138
139 vec![truncated, ellipsis].into_iter().collect()
140 }
141
142 fn wrap_line(line: StyledGraphemes, width: usize) -> Vec<StyledGraphemes> {
143 let mut wrapped = vec![StyledGraphemes::default()];
144 let mut current_width = 0;
145
146 for g in line.iter() {
147 if g.width() > width {
148 continue;
149 }
150 if current_width + g.width() > width {
151 wrapped.push(StyledGraphemes::default());
152 current_width = 0;
153 }
154 wrapped
155 .last_mut()
156 .expect("wrapped always contains at least one row")
157 .push_back(g.clone());
158 current_width += g.width();
159 }
160
161 wrapped
162 }
163
164 pub fn format_for_terminal_display(&self, rows: &[Row], width: u16) -> Vec<StyledGraphemes> {
166 let mut formatted = Vec::new();
167 let width = width as usize;
168
169 for (i, row) in rows.iter().enumerate() {
170 let indent = StyledGraphemes::from(" ".repeat(self.indent * row.depth));
171 let mut parts = Vec::new();
172
173 if let Some(key) = &row.k {
174 parts.push(
175 StyledGraphemes::from(format!("\"{}\"", key)).apply_style(self.key_style),
176 );
177 parts.push(StyledGraphemes::from(": "));
178 }
179
180 match &row.v {
181 Value::Null => {
182 parts.push(StyledGraphemes::from("null").apply_style(self.null_value_style));
183 }
184 Value::Boolean(b) => {
185 parts.push(
186 StyledGraphemes::from(b.to_string()).apply_style(self.boolean_value_style),
187 );
188 }
189 Value::Number(n) => {
190 parts.push(
191 StyledGraphemes::from(n.to_string()).apply_style(self.number_value_style),
192 );
193 }
194 Value::String(s) => {
195 let escaped = s.replace('\n', "\\n");
196 parts.push(
197 StyledGraphemes::from(format!("\"{}\"", escaped))
198 .apply_style(self.string_value_style),
199 );
200 }
201 Value::Empty { typ } => {
202 let bracket_style = match typ {
203 ContainerType::Object => self.curly_brackets_style,
204 ContainerType::Array => self.square_brackets_style,
205 };
206 parts.push(StyledGraphemes::from(typ.empty_str()).apply_style(bracket_style));
207 }
208 Value::Open { typ, collapsed, .. } => {
209 let bracket_style = match typ {
210 ContainerType::Object => self.curly_brackets_style,
211 ContainerType::Array => self.square_brackets_style,
212 };
213 if *collapsed {
214 parts.push(
215 StyledGraphemes::from(typ.collapsed_preview())
216 .apply_style(bracket_style),
217 );
218 } else {
219 parts
220 .push(StyledGraphemes::from(typ.open_str()).apply_style(bracket_style));
221 }
222 }
223 Value::Close { typ, .. } => {
224 let bracket_style = match typ {
225 ContainerType::Object => self.curly_brackets_style,
226 ContainerType::Array => self.square_brackets_style,
227 };
228 parts.push(StyledGraphemes::from(typ.close_str()).apply_style(bracket_style));
232 }
233 }
234
235 if i + 1 < rows.len() {
236 if let Value::Close { .. } = rows[i + 1].v {
237 } else if let Value::Open {
238 collapsed: false, ..
239 } = rows[i].v
240 {
241 } else {
242 parts.push(StyledGraphemes::from(","));
243 }
244 }
245
246 let mut content: StyledGraphemes = parts.into_iter().collect();
247
248 content = content.apply_attribute(if i == 0 {
252 self.active_item_attribute
253 } else {
254 self.inactive_item_attribute
255 });
256
257 let mut line: StyledGraphemes = vec![indent, content].into_iter().collect();
258
259 match self.overflow_mode {
260 OverflowMode::Truncate => {
261 line = Self::truncate_line_with_ellipsis(line, width);
262 formatted.push(line);
263 }
264 OverflowMode::Wrap => {
265 formatted.extend(Self::wrap_line(line, width));
266 }
267 }
268 }
269
270 formatted
271 }
272
273 pub fn format_raw_json(&self, rows: &[Row]) -> String {
275 let mut result = String::new();
276 let mut first_in_container = true;
277
278 for (i, row) in rows.iter().enumerate() {
279 if !matches!(row.v, Value::Close { .. }) {
281 if !result.is_empty() {
282 result.push('\n');
283 }
284 result.push_str(&" ".repeat(self.indent * row.depth));
285 }
286
287 if let Some(key) = &row.k {
289 result.push('"');
290 result.push_str(key);
291 result.push_str("\": ");
292 }
293
294 match &row.v {
296 Value::Null => result.push_str("null"),
297 Value::Boolean(b) => result.push_str(&b.to_string()),
298 Value::Number(n) => result.push_str(&n.to_string()),
299 Value::String(s) => {
300 result.push('"');
301 result.push_str(&s.replace('\n', "\\n"));
302 result.push('"');
303 }
304 Value::Empty { typ } => {
305 result.push_str(match typ {
306 ContainerType::Object => "{}",
307 ContainerType::Array => "[]",
308 });
309 }
310 Value::Open { typ, .. } => {
311 result.push(match typ {
312 ContainerType::Object => '{',
313 ContainerType::Array => '[',
314 });
315 }
316 Value::Close { typ, .. } => {
317 if !first_in_container {
318 result.push('\n');
319 result.push_str(&" ".repeat(self.indent * row.depth));
320 }
321 result.push(match typ {
322 ContainerType::Object => '}',
323 ContainerType::Array => ']',
324 });
325 }
326 }
327
328 if i + 1 < rows.len() {
330 if let Value::Close { .. } = rows[i + 1].v {
331 } else if let Value::Open { .. } = rows[i].v {
333 } else {
335 result.push(',');
336 }
337 }
338
339 if let Value::Open { .. } = row.v {
340 first_in_container = true;
341 } else {
342 first_in_container = false;
343 }
344 }
345
346 result
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use serde_json::json;
354
355 mod format_raw_json {
356 use std::str::FromStr;
357
358 use super::*;
359
360 use crate::jsonstream::jsonz::create_rows;
361
362 #[test]
363 fn test() {
364 let expected = r#"
365{
366 "array": [
367 {
368 "key": "value"
369 },
370 [
371 1,
372 2,
373 3
374 ],
375 {
376 "nested": true
377 }
378 ],
379 "object": {
380 "array": [
381 1,
382 2,
383 3
384 ],
385 "nested": {
386 "value": "test"
387 }
388 }
389}"#
390 .trim();
391
392 assert_eq!(
393 Config {
394 indent: 4,
395 ..Default::default()
396 }
397 .format_raw_json(&create_rows([
398 &serde_json::Value::from_str(&expected).unwrap()
399 ])),
400 expected,
401 );
402 }
403 }
404
405 mod format_for_terminal_display {
406 use super::*;
407
408 use crate::jsonstream::jsonz::create_rows;
409
410 #[test]
411 fn test_ellipsis_mode_truncates_with_ellipsis() {
412 let value = json!({
413 "very_long_key": "abcdefghijklmnopqrstuvwxyz",
414 });
415 let rows = create_rows([&value]);
416 let width = 12;
417
418 let lines = Config {
419 indent: 2,
420 overflow_mode: OverflowMode::Truncate,
421 ..Default::default()
422 }
423 .format_for_terminal_display(&rows, width);
424
425 assert_eq!(lines.len(), rows.len());
426 assert!(lines.iter().all(|line| line.widths() <= width as usize));
427 assert!(
428 lines
429 .iter()
430 .any(|line| line.chars().last().is_some_and(|ch| *ch == '…'))
431 );
432 }
433
434 #[test]
435 fn test_linewrap_mode_wraps_without_ellipsis() {
436 let value = json!({
437 "very_long_key": "abcdefghijklmnopqrstuvwxyz",
438 });
439 let rows = create_rows([&value]);
440 let width = 12;
441
442 let lines = Config {
443 indent: 2,
444 overflow_mode: OverflowMode::Wrap,
445 ..Default::default()
446 }
447 .format_for_terminal_display(&rows, width);
448
449 assert!(lines.len() > rows.len());
450 assert!(lines.iter().all(|line| line.widths() <= width as usize));
451 assert!(
452 lines
453 .iter()
454 .all(|line| !matches!(line.chars().last(), Some('…')))
455 );
456 }
457 }
458
459 #[cfg(feature = "serde")]
460 mod serde_compatibility {
461 use super::*;
462 use promkit_core::crossterm::style::{Attributes, Color};
463
464 #[test]
465 fn missing_new_fields_are_filled_by_default() {
466 let mut value = serde_json::to_value(Config {
467 indent: 4,
468 ..Default::default()
469 })
470 .unwrap();
471 let obj = value.as_object_mut().unwrap();
472 obj.remove("active_item_attribute");
473 obj.remove("inactive_item_attribute");
474 obj.remove("overflow_mode");
475 obj.remove("lines");
476
477 let formatter: Config = serde_json::from_value(value).unwrap();
478
479 assert_eq!(formatter.indent, 4);
480 assert_eq!(formatter.active_item_attribute, Attribute::NoBold);
481 assert_eq!(formatter.inactive_item_attribute, Attribute::NoBold);
482 assert_eq!(formatter.overflow_mode, OverflowMode::Truncate);
483 assert_eq!(formatter.lines, None);
484 }
485
486 #[test]
487 fn config_fields_are_fully_loaded_from_toml() {
488 let input = r#"
489indent = 4
490lines = 7
491curly_brackets_style = "attr=bold"
492square_brackets_style = "attr=bold"
493key_style = "fg=cyan"
494string_value_style = "fg=green"
495number_value_style = "fg=yellow"
496boolean_value_style = "fg=magenta"
497null_value_style = "fg=grey"
498active_item_attribute = "underlined"
499inactive_item_attribute = "dim"
500overflow_mode = "Wrap"
501"#;
502
503 let formatter: Config = toml::from_str(input).unwrap();
504
505 assert_eq!(formatter.indent, 4);
506 assert_eq!(formatter.lines, Some(7));
507 assert_eq!(
508 formatter.curly_brackets_style.attributes,
509 Attributes::from(Attribute::Bold),
510 );
511 assert_eq!(
512 formatter.square_brackets_style.attributes,
513 Attributes::from(Attribute::Bold),
514 );
515 assert_eq!(formatter.key_style.foreground_color, Some(Color::Cyan));
516 assert_eq!(
517 formatter.string_value_style.foreground_color,
518 Some(Color::Green),
519 );
520 assert_eq!(
521 formatter.number_value_style.foreground_color,
522 Some(Color::Yellow)
523 );
524 assert_eq!(
525 formatter.boolean_value_style.foreground_color,
526 Some(Color::Magenta),
527 );
528 assert_eq!(
529 formatter.null_value_style.foreground_color,
530 Some(Color::Grey)
531 );
532 assert_eq!(formatter.active_item_attribute, Attribute::Underlined);
533 assert_eq!(formatter.inactive_item_attribute, Attribute::Dim);
534 assert_eq!(formatter.overflow_mode, OverflowMode::Wrap);
535 }
536 }
537}