1 #![forbid(unsafe_code)]2 #![allow(dead_code)]3 //! Output: compact JSON, --jq passthrough, thin human view.4 //! JSON is serde_json::to_string (no spaces). Empty is [] or {}.5 //! No ANSI color when stdout is not a tty.6 7 use serde::Serialize;8 use std::io::IsTerminal;9 use std::process::{Command, Stdio};10 11 /// Safe stdout write: BrokenPipe is clean end-of-output (exit 0, no panic/trace).12 /// All root-path prints must go through here. Errors stay on stderr.13 pub fn out(s: &str) {14 use std::io::Write;15 let r = (|| -> std::io::Result<()> {16 let mut o = std::io::stdout().lock();17 o.write_all(s.as_bytes())?;18 o.flush()?;19 Ok(())20 })();21 if let Err(e) = r {22 if e.kind() == std::io::ErrorKind::BrokenPipe {23 std::process::exit(0);24 }25 // Other stdout errors: no panic, no trace.26 }27 }28 29 /// Compact one-line JSON.30 pub fn json_line<T: Serialize>(v: &T) -> String {31 serde_json::to_string(v).unwrap_or_else(|_| "{}".to_string())32 }33 34 /// Print pre-rendered compact JSON, applying --jq when given.35 pub fn emit_str(json: &str, jq: Option<&str>) {36 match jq {37 Some(expr) => match run_jq(json, expr) {38 Ok(s) => out(&s),39 Err(note) => {40 eprintln!("{note}");41 out(&format!("{json}\n"));42 }43 },44 None => out(&format!("{json}\n")),45 }46 }47 48 /// Print a value as compact JSON.49 pub fn emit_json<T: Serialize>(v: &T, jq: Option<&str>) {50 emit_str(&json_line(v), jq);51 }52 53 /// Bulk pre-rendered lines: one write when raw (fast path for big lists),54 /// per-line passthrough when --jq is set (jq consumes one doc per call).55 pub fn emit_bulk(lines: &str, jq: Option<&str>) {56 match jq {57 Some(_) => {58 for line in lines.lines() {59 emit_str(line, jq);60 }61 }62 None => out(lines),63 }64 }65 66 /// Empty results: no prose.67 pub fn emit_empty_array(jq: Option<&str>) {68 emit_str("[]", jq);69 }70 71 /// Empty object result.72 pub fn emit_empty_object(jq: Option<&str>) {73 emit_str("{}", jq);74 }75 76 /// M8 one-hop migration note: one line on stderr naming the file, keep77 /// going. None (current/absent schema) prints nothing.78 pub fn warn_schema(note: &Option<String>) {79 if let Some(n) = note {80 eprintln!("warning: {n}");81 }82 }83 84 /// Print plain text (help, skill), applying --jq only when it looks85 /// like JSON; else ignore jq (skill text is not JSON).86 pub fn emit_text(text: &str, jq: Option<&str>) {87 let t = text.trim_end_matches('\n');88 if jq.is_some() && (t.starts_with('{') || t.starts_with('[')) {89 emit_str(t, jq);90 } else {91 out(text);92 if !text.ends_with('\n') {93 out("\n");94 }95 }96 }97 98 /// Shell out to system jq via argv array (SEC-04). Missing jq ->99 /// one-line note, never a hand-rolled fallback parser.100 pub fn run_jq(json: &str, expr: &str) -> Result<String, String> {101 let mut child = Command::new("jq")102 .arg(expr)103 .stdin(Stdio::piped())104 .stdout(Stdio::piped())105 .stderr(Stdio::null())106 .spawn()107 .map_err(|_| "jq not found; showing raw json".to_string())?;108 use std::io::Write;109 if let Some(mut stdin) = child.stdin.take() {110 let _ = stdin.write_all(json.as_bytes());111 }112 let out = child.wait_with_output().map_err(|_| "jq failed; showing raw json".to_string())?;113 if !out.status.success() {114 return Err("jq failed; showing raw json".to_string());115 }116 String::from_utf8(out.stdout).map_err(|_| "jq failed; showing raw json".to_string())117 }118 119 /// True when stdout is a tty (color allowed; default off for agents).120 pub fn is_tty() -> bool {121 std::io::stdout().is_terminal()122 }123 124 /// Thin aligned human view of rows. No ANSI when not a tty.125 pub fn align(rows: &[Vec<String>]) -> String {126 if rows.is_empty() {127 return String::new();128 }129 let cols = rows.iter().map(Vec::len).max().unwrap_or(0);130 let mut width = vec![0usize; cols];131 for r in rows {132 for (i, c) in r.iter().enumerate() {133 width[i] = width[i].max(c.len());134 }135 }136 rows.iter()137 .map(|r| {138 r.iter()139 .enumerate()140 .map(|(i, c)| {141 if i + 1 < r.len() {142 format!("{:<w$} ", c, w = width[i])143 } else {144 c.clone()145 }146 })147 .collect::<String>()148 })149 .collect::<Vec<_>>()150 .join("\n")151 }