use std::borrow::Cow;
use std::io::{IsTerminal, Write};
use base64::Engine as _;
use base64::engine::general_purpose::STANDARD;
use serde_json::Value;
#[derive(Debug, Clone, Copy)]
pub(crate) struct Output {
pub(crate) json: bool,
}
impl Output {
pub(crate) fn text(&self, text: &str) -> anyhow::Result<()> {
let mut stdout = std::io::stdout().lock();
stdout.write_all(text.as_bytes())?;
if !text.ends_with('\n') {
stdout.write_all(b"\n")?;
}
stdout.flush()?;
Ok(())
}
pub(crate) fn json_value(&self, value: &Value) -> anyhow::Result<()> {
self.text(&value.to_string())
}
pub(crate) fn done(&self, text: &str, json: Value) -> anyhow::Result<()> {
if self.json {
self.json_value(&json)
} else {
self.text(text)
}
}
pub(crate) fn raw_line(&self, bytes: &[u8]) -> anyhow::Result<()> {
let mut stdout = std::io::stdout().lock();
if stdout.is_terminal() {
stdout.write_all(&for_terminal(bytes))?;
} else {
stdout.write_all(bytes)?;
}
stdout.write_all(b"\n")?;
stdout.flush()?;
Ok(())
}
}
pub(crate) fn table(headers: &[&str], rows: Vec<Vec<String>>) -> String {
let mut widths: Vec<usize> = headers.iter().map(|h| h.chars().count()).collect();
for row in &rows {
for (i, cell) in row.iter().enumerate() {
if i < widths.len() {
widths[i] = widths[i].max(cell.chars().count());
}
}
}
let line = |cells: Vec<&str>| -> String {
let last = cells.len().saturating_sub(1);
let mut out = String::new();
for (i, cell) in cells.into_iter().enumerate() {
if i == last {
out.push_str(cell);
} else {
out.push_str(cell);
let pad = widths[i] - cell.chars().count() + 2;
out.extend(std::iter::repeat_n(' ', pad));
}
}
out.trim_end().to_string()
};
let mut out = line(headers.to_vec());
for row in &rows {
out.push('\n');
out.push_str(&line(row.iter().map(String::as_str).collect()));
}
out
}
pub(crate) fn for_terminal(bytes: &[u8]) -> Cow<'_, [u8]> {
let clean = |c: char| !c.is_control() || c == '\t';
if std::str::from_utf8(bytes).is_ok_and(|text| text.chars().all(clean)) {
return Cow::Borrowed(bytes);
}
let mut out = String::with_capacity(bytes.len() * 2);
for chunk in bytes.utf8_chunks() {
for c in chunk.valid().chars() {
if clean(c) {
out.push(c);
} else if c.is_ascii() {
out.push_str(&format!("\\x{:02x}", c as u32));
} else {
out.push_str(&format!("\\u{{{:x}}}", c as u32));
}
}
for b in chunk.invalid() {
out.push_str(&format!("\\x{b:02x}"));
}
}
Cow::Owned(out.into_bytes())
}
pub(crate) fn payload_field(bytes: &[u8]) -> (&'static str, Value) {
match std::str::from_utf8(bytes) {
Ok(text) => ("payload", Value::String(text.to_string())),
Err(_) => ("payload_base64", Value::String(STANDARD.encode(bytes))),
}
}
pub(crate) fn fields(value: &Value) -> String {
let Some(object) = value.as_object() else {
return cell(value);
};
let width = object.keys().map(|k| k.chars().count()).max().unwrap_or(0);
object
.iter()
.map(|(key, value)| format!("{key:width$} {}", cell(value)))
.collect::<Vec<_>>()
.join("\n")
}
pub(crate) fn cell(value: &Value) -> String {
match value {
Value::Null => String::new(),
Value::String(text) => text.clone(),
Value::Array(items) if items.iter().all(Value::is_string) => items
.iter()
.filter_map(Value::as_str)
.collect::<Vec<_>>()
.join(","),
other => other.to_string(),
}
}
#[cfg(test)]
mod tests;