1use std::io::{self, IsTerminal, Write};
7
8use serde_json::Value;
9
10use crate::coerce::apply_head;
11use crate::spool::{self, pointer_json, write_spool};
12
13#[derive(Debug, Clone, Default)]
14pub struct OutputOptions {
15 pub pretty: bool,
16 pub raw: bool,
17 pub toon: bool,
18 pub head: Option<usize>,
19 pub json_output: bool,
20 pub max_bytes: Option<usize>,
22 pub inline: bool,
24}
25
26pub fn strip_ansi(s: &str) -> String {
28 let mut out = String::with_capacity(s.len());
29 let mut chars = s.chars().peekable();
30 while let Some(c) = chars.next() {
31 if c == '\u{1b}' {
32 match chars.peek() {
33 Some('[') => {
34 chars.next();
35 for c2 in chars.by_ref() {
36 if c2.is_ascii_uppercase() || c2.is_ascii_lowercase() {
37 break;
38 }
39 }
40 }
41 Some(']') => {
42 chars.next();
43 for c2 in chars.by_ref() {
44 if c2 == '\u{7}' || c2 == '\u{1b}' {
45 break;
46 }
47 }
48 }
49 Some(_) => {
50 chars.next();
51 }
52 None => {}
53 }
54 } else if c == '\0' {
55 } else {
57 out.push(c);
58 }
59 }
60 out
61}
62
63fn emit_json(data: &Value, pretty: bool) -> io::Result<()> {
64 let stdout = io::stdout();
65 let mut out = stdout.lock();
66 if pretty || io::stdout().is_terminal() {
67 serde_json::to_writer_pretty(&mut out, data)?;
68 writeln!(out)?;
69 } else {
70 serde_json::to_writer(&mut out, data)?;
71 writeln!(out)?;
72 }
73 Ok(())
74}
75
76fn render_json_bytes(data: &Value, pretty: bool) -> io::Result<Vec<u8>> {
77 if pretty {
78 let mut v = serde_json::to_vec_pretty(data)?;
79 v.push(b'\n');
80 Ok(v)
81 } else {
82 let mut v = serde_json::to_vec(data)?;
83 v.push(b'\n');
84 Ok(v)
85 }
86}
87
88fn encode_toon(data: &Value) -> Result<String, String> {
89 toon_format::encode(data, &toon_format::EncodeOptions::default()).map_err(|e| e.to_string())
90}
91
92fn should_spill(opts: &OutputOptions, len: usize) -> bool {
93 if opts.inline {
94 return false;
95 }
96 match opts.max_bytes {
97 Some(0) | None => false,
98 Some(n) => len > n,
99 }
100}
101
102fn spill_and_print_pointer(bytes: &[u8], kind: &str, preview: &str) -> io::Result<()> {
103 spool::maybe_clean_expired();
104 let path = write_spool(bytes, kind).map_err(io::Error::other)?;
105 eprintln!(
106 "skiff: output {} bytes exceeded limit; spooled to {}",
107 bytes.len(),
108 path.display()
109 );
110 let ptr = pointer_json(&path, bytes, preview);
111 let stdout = io::stdout();
113 let mut out = stdout.lock();
114 serde_json::to_writer(&mut out, &ptr)?;
115 writeln!(out)?;
116 Ok(())
117}
118
119pub fn output_result(data: Value, opts: &OutputOptions) -> io::Result<()> {
121 let mut data = data;
122
123 if let Value::String(s) = &data {
124 if opts.json_output || opts.toon {
125 if let Ok(parsed) = serde_json::from_str::<Value>(s) {
126 data = parsed;
127 }
128 } else if !opts.raw {
129 match serde_json::from_str::<Value>(s) {
130 Ok(parsed) => data = parsed,
131 Err(_) => {
132 let text = if io::stdout().is_terminal() {
133 strip_ansi(s)
134 } else {
135 s.clone()
136 };
137 if should_spill(opts, text.len()) {
138 return spill_and_print_pointer(text.as_bytes(), "txt", &text);
139 }
140 println!("{text}");
141 return Ok(());
142 }
143 }
144 }
145 }
146
147 if opts.raw {
148 match data {
149 Value::String(s) => {
150 if should_spill(opts, s.len()) {
151 return spill_and_print_pointer(s.as_bytes(), "txt", &s);
152 }
153 println!("{s}");
154 return Ok(());
155 }
156 other => {
157 let s = serde_json::to_string(&other)?;
158 if should_spill(opts, s.len()) {
159 return spill_and_print_pointer(s.as_bytes(), "json", &s);
160 }
161 println!("{s}");
162 return Ok(());
163 }
164 }
165 }
166
167 if let Some(n) = opts.head {
168 data = apply_head(data, n);
169 }
170
171 if opts.toon {
172 match encode_toon(&data) {
173 Ok(toon) => {
174 let mut bytes = toon.into_bytes();
175 bytes.push(b'\n');
176 if should_spill(opts, bytes.len()) {
177 let preview = String::from_utf8_lossy(&bytes).into_owned();
178 return spill_and_print_pointer(&bytes, "toon", &preview);
179 }
180 let stdout = io::stdout();
181 let mut out = stdout.lock();
182 out.write_all(&bytes)?;
183 return Ok(());
184 }
185 Err(e) => {
186 eprintln!("Warning: --toon encode failed ({e}); falling back to JSON");
187 }
189 }
190 }
191
192 if opts.json_output || opts.toon || !matches!(&data, Value::String(_)) {
194 let pretty = opts.pretty;
195 let use_pretty = pretty || (io::stdout().is_terminal() && !opts.json_output && !opts.toon);
197 let bytes = render_json_bytes(&data, use_pretty)?;
198 if should_spill(opts, bytes.len()) {
199 let preview = String::from_utf8_lossy(&bytes).into_owned();
200 return spill_and_print_pointer(&bytes, "json", &preview);
201 }
202 let stdout = io::stdout();
203 let mut out = stdout.lock();
204 out.write_all(&bytes)?;
205 return Ok(());
206 }
207
208 emit_json(&data, opts.pretty)
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214 use serde_json::json;
215
216 #[test]
217 fn strip_ansi_removes_csi() {
218 let s = "\u{1b}[31mred\u{1b}[0m plain";
219 assert_eq!(strip_ansi(s), "red plain");
220 }
221
222 #[test]
223 fn encode_toon_uniform_array() {
224 let data = json!([{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]);
225 let t = encode_toon(&data).unwrap();
226 assert!(t.contains("id") && t.contains("name"));
227 }
228
229 #[test]
230 fn options_construct() {
231 let opts = OutputOptions {
232 pretty: true,
233 head: Some(2),
234 max_bytes: Some(100),
235 ..Default::default()
236 };
237 assert_eq!(opts.head, Some(2));
238 assert_eq!(opts.max_bytes, Some(100));
239 }
240}