1use crate::model::{Row, Value};
4
5pub fn format_results(
6 rows: &[Row],
7 columns: Option<&[String]>,
8 output_format: &str,
9 truncate: usize,
10) -> String {
11 if rows.is_empty() {
12 return "No results.".to_string();
13 }
14
15 let cols: Vec<String> = match columns {
16 Some(c) => c.to_vec(),
17 None => {
18 let mut seen: Vec<String> = Vec::new();
19 let mut set = std::collections::HashSet::new();
20 for r in rows {
21 for k in r.keys() {
22 if set.insert(k.clone()) {
23 seen.push(k.clone());
24 }
25 }
26 }
27 seen
28 }
29 };
30
31 match output_format {
32 "json" => format_json(rows, &cols),
33 "csv" => format_csv(rows, &cols),
34 _ => format_table(rows, &cols, truncate),
35 }
36}
37
38fn format_json(rows: &[Row], columns: &[String]) -> String {
39 let projected: Vec<serde_json::Value> = rows
40 .iter()
41 .map(|r| {
42 let mut obj = serde_json::Map::new();
43 for c in columns {
44 let val = r.get(c).unwrap_or(&Value::Null);
45 obj.insert(c.clone(), value_to_json(val));
46 }
47 serde_json::Value::Object(obj)
48 })
49 .collect();
50
51 serde_json::to_string_pretty(&projected).unwrap_or_default()
52}
53
54fn value_to_json(val: &Value) -> serde_json::Value {
55 match val {
56 Value::Null => serde_json::Value::Null,
57 Value::String(s) => serde_json::Value::String(s.clone()),
58 Value::Int(n) => serde_json::json!(*n),
59 Value::Float(f) => serde_json::json!(*f),
60 Value::Bool(b) => serde_json::Value::Bool(*b),
61 Value::Date(d) => serde_json::Value::String(d.format("%Y-%m-%d").to_string()),
62 Value::DateTime(dt) => serde_json::Value::String(dt.format("%Y-%m-%dT%H:%M:%S").to_string()),
63 Value::List(items) => {
64 serde_json::Value::Array(items.iter().map(|s| serde_json::Value::String(s.clone())).collect())
65 }
66 Value::Dict(map) => {
67 let obj: serde_json::Map<String, serde_json::Value> = map.iter()
68 .map(|(k, v)| (k.clone(), value_to_json(v)))
69 .collect();
70 serde_json::Value::Object(obj)
71 }
72 }
73}
74
75fn format_csv(rows: &[Row], columns: &[String]) -> String {
76 let mut out = String::new();
77 out.push_str(&columns.join(","));
79 out.push('\n');
80 for r in rows {
82 let vals: Vec<String> = columns
83 .iter()
84 .map(|c| {
85 let val = r.get(c).unwrap_or(&Value::Null);
86 csv_value(val)
87 })
88 .collect();
89 out.push_str(&vals.join(","));
90 out.push('\n');
91 }
92 out
93}
94
95fn csv_value(val: &Value) -> String {
96 match val {
97 Value::Null => String::new(),
98 Value::List(items) => items.join(";"),
99 Value::Date(d) => d.format("%Y-%m-%d").to_string(),
100 Value::DateTime(dt) => dt.format("%Y-%m-%dT%H:%M:%S").to_string(),
101 Value::Dict(map) => {
102 let obj: serde_json::Map<String, serde_json::Value> = map.iter()
103 .map(|(k, v)| (k.clone(), value_to_json(v)))
104 .collect();
105 serde_json::to_string(&serde_json::Value::Object(obj)).unwrap_or_default()
106 }
107 other => other.to_display_string(),
108 }
109}
110
111fn format_table(rows: &[Row], columns: &[String], truncate: usize) -> String {
112 let ncols = columns.len();
113 if ncols == 0 {
114 return "No results.".to_string();
115 }
116
117 let mut natural_widths: Vec<usize> = columns.iter().map(|c| c.chars().count()).collect();
119
120 let raw_cells: Vec<Vec<String>> = rows
121 .iter()
122 .map(|r| {
123 columns
124 .iter()
125 .enumerate()
126 .map(|(i, c)| {
127 let val = r.get(c).unwrap_or(&Value::Null);
128 let s = val.to_display_string().replace('\n', " ");
129 let s = s.trim().to_string();
130 let char_len = s.chars().count();
131 if char_len > natural_widths[i] {
132 natural_widths[i] = char_len;
133 }
134 s
135 })
136 .collect()
137 })
138 .collect();
139
140 let gap = 2; let total_gap = gap * (ncols.saturating_sub(1));
143
144 let effective_widths = if truncate > 0 {
145 natural_widths.iter().map(|&w| w.min(truncate)).collect::<Vec<_>>()
147 } else {
148 let term_width = terminal_width().unwrap_or(120);
150 fit_columns_to_width(&natural_widths, term_width.saturating_sub(total_gap))
151 };
152
153 let cell_data: Vec<Vec<String>> = raw_cells
155 .iter()
156 .map(|row_cells| {
157 row_cells
158 .iter()
159 .enumerate()
160 .map(|(i, s)| truncate_str(s, effective_widths[i]))
161 .collect()
162 })
163 .collect();
164
165 let truncated_headers: Vec<String> = columns
166 .iter()
167 .enumerate()
168 .map(|(i, c)| truncate_str(c, effective_widths[i]))
169 .collect();
170
171 let mut display_widths: Vec<usize> = truncated_headers.iter().map(|h| h.chars().count()).collect();
173 for row_cells in &cell_data {
174 for (i, s) in row_cells.iter().enumerate() {
175 let w = s.chars().count();
176 if w > display_widths[i] {
177 display_widths[i] = w;
178 }
179 }
180 }
181
182 let mut out = String::new();
183
184 let header: Vec<String> = truncated_headers
186 .iter()
187 .enumerate()
188 .map(|(i, h)| format!("{:<width$}", h, width = display_widths[i]))
189 .collect();
190 out.push_str(&header.join(" "));
191 out.push('\n');
192
193 let sep: Vec<String> = display_widths.iter().map(|w| "-".repeat(*w)).collect();
195 out.push_str(&sep.join(" "));
196 out.push('\n');
197
198 for cells in &cell_data {
200 let line: Vec<String> = cells
201 .iter()
202 .enumerate()
203 .map(|(i, c)| {
204 if rows
206 .first()
207 .and_then(|r| r.get(&columns[i]))
208 .map_or(false, |v| matches!(v, Value::Int(_) | Value::Float(_)))
209 {
210 format!("{:>width$}", c, width = display_widths[i])
211 } else {
212 format!("{:<width$}", c, width = display_widths[i])
213 }
214 })
215 .collect();
216 out.push_str(&line.join(" "));
217 out.push('\n');
218 }
219
220 out.trim_end_matches('\n').to_string()
222}
223
224fn terminal_width() -> Option<usize> {
226 if let Ok(cols) = std::env::var("COLUMNS") {
228 if let Ok(w) = cols.parse::<usize>() {
229 if w > 0 {
230 return Some(w);
231 }
232 }
233 }
234
235 #[cfg(unix)]
238 {
239 use std::mem::zeroed;
240 for fd in [2, 1, 0] {
241 unsafe {
242 let mut ws: libc::winsize = zeroed();
243 if libc::ioctl(fd, libc::TIOCGWINSZ, &mut ws) == 0 && ws.ws_col > 0 {
244 return Some(ws.ws_col as usize);
245 }
246 }
247 }
248 }
249
250 None
251}
252
253fn fit_columns_to_width(natural: &[usize], available: usize) -> Vec<usize> {
255 let total_natural: usize = natural.iter().sum();
256 if total_natural <= available {
257 return natural.to_vec();
258 }
259
260 let min_col = 4;
263 let mut widths: Vec<usize> = natural
264 .iter()
265 .map(|&w| {
266 let share = ((w as f64 / total_natural as f64) * available as f64) as usize;
267 share.max(min_col)
268 })
269 .collect();
270
271 let mut total: usize = widths.iter().sum();
273 while total > available {
274 if let Some(i) = widths.iter().enumerate()
275 .filter(|(_, &w)| w > min_col)
276 .max_by_key(|(_, &w)| w)
277 .map(|(i, _)| i)
278 {
279 widths[i] -= 1;
280 total -= 1;
281 } else {
282 break;
283 }
284 }
285
286 widths
287}
288
289fn truncate_str(s: &str, max_len: usize) -> String {
290 let s = s.replace('\n', " ");
291 let s = s.trim();
292 if s.chars().count() > max_len {
293 let truncated: String = s.chars().take(max_len - 3).collect();
294 format!("{}...", truncated)
295 } else {
296 s.to_string()
297 }
298}