Skip to main content

yuki_cli/
output.rs

1use comfy_table::{Cell, Color, Table, presets::UTF8_FULL_CONDENSED};
2use serde_json::{Map, Value, json};
3
4/// Pagination and field-selection options shared by all list commands.
5#[derive(Default)]
6pub struct ListOptions<'a> {
7    pub limit: Option<usize>,
8    pub offset: Option<usize>,
9    pub fields: Option<&'a str>,
10}
11
12/// Apply offset then limit to a row slice in place.
13pub fn apply_pagination(rows: &mut Vec<Vec<String>>, opts: &ListOptions<'_>) {
14    if let Some(off) = opts.offset {
15        if off >= rows.len() {
16            rows.clear();
17            return;
18        }
19        rows.drain(..off);
20    }
21    if let Some(lim) = opts.limit {
22        rows.truncate(lim);
23    }
24}
25
26pub enum OutputFormat {
27    Table,
28    Json,
29}
30
31impl OutputFormat {
32    pub fn from_flag(flag: Option<&str>, is_tty: bool) -> Self {
33        match flag {
34            Some("json") => Self::Json,
35            // "text" and "table" both map to table output
36            Some("text") | Some("table") => Self::Table,
37            // Explicit "auto" or no flag: defer to TTY detection
38            Some("auto") | None => {
39                if is_tty {
40                    Self::Table
41                } else {
42                    Self::Json
43                }
44            }
45            // Any other unrecognized value: treat as auto
46            Some(_) => {
47                if is_tty {
48                    Self::Table
49                } else {
50                    Self::Json
51                }
52            }
53        }
54    }
55}
56
57/// Format rows as a clispec v0.2 items envelope: `{"items": [...], "total": N}`.
58pub fn format_json(headers: &[String], rows: &[Vec<String>]) -> String {
59    let items: Vec<Value> = rows
60        .iter()
61        .map(|row| {
62            let mut map = Map::new();
63            for (i, header) in headers.iter().enumerate() {
64                let val = row.get(i).cloned().unwrap_or_default();
65                map.insert(header.clone(), Value::String(val));
66            }
67            Value::Object(map)
68        })
69        .collect();
70    let total = items.len();
71    serde_json::to_string_pretty(&json!({
72        "items": items,
73        "total": total
74    }))
75    .unwrap_or_else(|_| r#"{"items":[],"total":0}"#.into())
76}
77
78pub fn format_table(headers: &[String], rows: &[Vec<String>]) -> String {
79    let mut table = Table::new();
80    table.load_preset(UTF8_FULL_CONDENSED);
81    let header_cells: Vec<Cell> = headers
82        .iter()
83        .map(|h| {
84            Cell::new(h)
85                .fg(Color::White)
86                .add_attribute(comfy_table::Attribute::Bold)
87        })
88        .collect();
89    table.set_header(header_cells);
90    for row in rows {
91        table.add_row(row);
92    }
93    table.to_string()
94}
95
96/// Format a structured error as the clispec v0.2 envelope.
97///
98/// The spec requires the last line of stderr to be:
99/// `{"error": {"kind": "<kind>", "message": "<message>"}}`
100pub fn format_error_json(message: &str, kind: &str) -> String {
101    serde_json::to_string(&json!({
102        "error": {
103            "kind": kind,
104            "message": message
105        }
106    }))
107    .unwrap_or_else(|_| format!(r#"{{"error":{{"kind":"{kind}","message":"{message}"}}}}"#))
108}
109
110pub fn is_tty() -> bool {
111    std::io::IsTerminal::is_terminal(&std::io::stdout())
112}