use super::{Renderer, Writer};
use crate::output::Verbosity;
const KEY_WIDTH_CAP: usize = 24;
const KEY_VALUE_GAP: &str = " ";
impl Renderer {
pub(crate) fn render_kv(&self, key: &str, value: &str) {
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
s.kv_buffer.push((key.into(), value.into()));
}
pub(crate) fn render_kv_block_no_flush(
&self,
w: &dyn Writer,
depth: usize,
pairs: &[(String, String)],
) {
if self.verbosity == Verbosity::Quiet || pairs.is_empty() {
return;
}
self.flush_pending_section_headers(w);
let effective_depth = {
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
let bump = depth == 0 && s.section_stack.is_empty() && s.last_was_top_heading;
s.last_was_top_heading = false;
if s.leading {
s.leading = false;
s.blank_pending = false;
} else if s.blank_pending && !bump {
w.write_line("");
s.blank_pending = false;
} else if bump {
s.blank_pending = false;
}
if bump { depth + 1 } else { depth }
};
let prefix = " ".repeat(effective_depth);
let key_col = pairs
.iter()
.map(|(k, _)| k.len())
.max()
.unwrap_or(0)
.min(KEY_WIDTH_CAP);
for (k, v) in pairs {
if k.len() <= KEY_WIDTH_CAP {
let key = self
.theme
.header
.apply_to(format!("{:<width$}", k, width = key_col));
w.write_line(&format!("{}{}{}{}", prefix, key, KEY_VALUE_GAP, v));
} else {
let key = self.theme.header.apply_to(k);
w.write_line(&format!("{}{}", prefix, key));
w.write_line(&format!("{} {}", prefix, v));
}
}
self.mark_top_level_blank_if_at_root();
}
pub(crate) fn render_kv_block(&self, w: &dyn Writer, depth: usize, pairs: &[(String, String)]) {
self.render_kv_block_no_flush(w, depth, pairs);
}
pub(crate) fn flush_kv_buffer(&self, w: &dyn Writer) {
let (pairs, depth) = {
let mut s = self.state.lock().unwrap_or_else(|e| e.into_inner());
if s.kv_buffer.is_empty() {
return;
}
(std::mem::take(&mut s.kv_buffer), s.indent_depth)
};
self.render_kv_block(w, depth, &pairs);
}
}
#[cfg(test)]
mod tests {
use std::sync::{Arc, Mutex};
use super::super::{Renderer, StringSink};
use crate::output::strip_ansi;
use crate::output::{Theme, Verbosity};
fn capture() -> (Renderer, StringSink, Arc<Mutex<String>>) {
let buf = Arc::new(Mutex::new(String::new()));
let sink = StringSink(buf.clone());
let r = Renderer::new(Theme::default(), Verbosity::Normal);
(r, sink, buf)
}
#[test]
fn kv_block_aligns_to_max_key_in_block() {
let (r, sink, buf) = capture();
r.render_kv_block(
&sink,
0,
&[("Foo".into(), "1".into()), ("LongerKey".into(), "2".into())],
);
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Foo 1"), "got: {out:?}");
assert!(out.contains("LongerKey 2"), "got: {out:?}");
}
#[test]
fn buffered_kvs_coalesce_into_one_block() {
let (r, sink, buf) = capture();
r.render_kv("Foo", "1");
r.render_kv("LongerKey", "2");
r.flush_kv_buffer(&sink);
let out = strip_ansi(&buf.lock().unwrap());
assert!(out.contains("Foo 1"), "got: {out:?}");
assert!(out.contains("LongerKey 2"), "got: {out:?}");
}
#[test]
fn long_key_wraps_value_to_next_line() {
let (r, sink, buf) = capture();
let long = "x".repeat(30);
r.render_kv_block(&sink, 0, &[(long.clone(), "value".into())]);
let out = strip_ansi(&buf.lock().unwrap());
let lines: Vec<&str> = out.lines().collect();
assert!(lines.len() >= 2, "expected wrapped output, got {out:?}");
assert_eq!(lines[0], long);
assert!(lines[1].starts_with(" value"), "got line: {:?}", lines[1]);
}
#[test]
fn kv_quiet_suppressed() {
let buf = Arc::new(Mutex::new(String::new()));
let sink = StringSink(buf.clone());
let r = Renderer::new(Theme::default(), Verbosity::Quiet);
r.render_kv_block(&sink, 0, &[("Foo".into(), "1".into())]);
assert!(buf.lock().unwrap().is_empty());
}
}