Skip to main content

clickup_cli/
output.rs

1use chrono::DateTime;
2use comfy_table::{ContentArrangement, Table};
3
4pub struct OutputConfig {
5    pub mode: String,
6    pub fields: Option<Vec<String>>,
7    pub no_header: bool,
8    pub quiet: bool,
9}
10
11impl OutputConfig {
12    pub fn from_cli(mode: &str, fields: &Option<String>, no_header: bool, quiet: bool) -> Self {
13        Self {
14            mode: mode.to_string(),
15            fields: fields
16                .as_ref()
17                .map(|f| f.split(',').map(|s| s.trim().to_string()).collect()),
18            no_header,
19            quiet,
20        }
21    }
22
23    pub fn print_items(
24        &self,
25        items: &[serde_json::Value],
26        default_fields: &[&str],
27        id_field: &str,
28    ) {
29        if self.quiet {
30            for item in items {
31                if let Some(id) = item.get(id_field).and_then(|v| v.as_str()) {
32                    println!("{}", id);
33                }
34            }
35            return;
36        }
37
38        let fields: Vec<&str> = match &self.fields {
39            Some(f) => f.iter().map(|s| s.as_str()).collect(),
40            None => default_fields.to_vec(),
41        };
42
43        match self.mode.as_str() {
44            "json" => {
45                println!("{}", serde_json::to_string_pretty(items).unwrap());
46            }
47            "json-compact" => {
48                let filtered = compact_items(items, &fields);
49                println!("{}", serde_json::to_string_pretty(&filtered).unwrap());
50            }
51            "csv" => {
52                if !self.no_header {
53                    println!("{}", fields.join(","));
54                }
55                for item in items {
56                    let row: Vec<String> =
57                        fields.iter().map(|&f| flatten_value(item.get(f))).collect();
58                    println!("{}", row.join(","));
59                }
60            }
61            _ => {
62                // table (default)
63                let mut table = Table::new();
64                table.set_content_arrangement(ContentArrangement::Dynamic);
65                if !self.no_header {
66                    table.set_header(fields.iter().map(|f| f.to_string()).collect::<Vec<_>>());
67                }
68                for item in items {
69                    let row: Vec<String> =
70                        fields.iter().map(|&f| flatten_value(item.get(f))).collect();
71                    table.add_row(row);
72                }
73                println!("{}", table);
74            }
75        }
76    }
77
78    pub fn print_single(&self, item: &serde_json::Value, default_fields: &[&str], id_field: &str) {
79        self.print_items(std::slice::from_ref(item), default_fields, id_field);
80    }
81
82    pub fn print_message(&self, message: &str) {
83        if self.mode == "json" {
84            println!("{}", serde_json::json!({ "message": message }));
85        } else {
86            println!("{}", message);
87        }
88    }
89}
90
91/// Flatten a list of items to only include the specified fields with flattened values.
92/// Returns a JSON array. Used by MCP server for token-efficient responses.
93pub fn compact_items(items: &[serde_json::Value], fields: &[&str]) -> serde_json::Value {
94    let compacted: Vec<serde_json::Value> = items
95        .iter()
96        .map(|item| {
97            let mut obj = serde_json::Map::new();
98            for &field in fields {
99                let val = flatten_value(item.get(field));
100                obj.insert(field.to_string(), serde_json::Value::String(val));
101            }
102            serde_json::Value::Object(obj)
103        })
104        .collect();
105    serde_json::Value::Array(compacted)
106}
107
108pub fn flatten_value(value: Option<&serde_json::Value>) -> String {
109    match value {
110        None | Some(serde_json::Value::Null) => "-".to_string(),
111        Some(serde_json::Value::String(s)) => {
112            // Try to parse as Unix millisecond timestamp
113            if let Ok(ms) = s.parse::<i64>() {
114                if ms > 1_000_000_000_000 && ms < 10_000_000_000_000 {
115                    if let Some(dt) = DateTime::from_timestamp_millis(ms) {
116                        return dt.format("%Y-%m-%d").to_string();
117                    }
118                }
119            }
120            s.clone()
121        }
122        Some(serde_json::Value::Number(n)) => n.to_string(),
123        Some(serde_json::Value::Bool(b)) => b.to_string(),
124        Some(serde_json::Value::Array(arr)) => {
125            // For arrays of objects with "username" field (assignees)
126            let items: Vec<String> = arr
127                .iter()
128                .map(|v| {
129                    if let Some(username) = v.get("username").and_then(|u| u.as_str()) {
130                        username.to_string()
131                    } else if let Some(s) = v.as_str() {
132                        s.to_string()
133                    } else {
134                        v.to_string()
135                    }
136                })
137                .collect();
138            if items.is_empty() {
139                "-".to_string()
140            } else {
141                items.join(", ")
142            }
143        }
144        Some(serde_json::Value::Object(obj)) => {
145            // Flatten nested objects: status.status, priority.priority
146            if let Some(inner) = obj.get("status").and_then(|v| v.as_str()) {
147                inner.to_string()
148            } else if let Some(inner) = obj.get("priority").and_then(|v| v.as_str()) {
149                inner.to_string()
150            } else if let Some(name) = obj.get("name").and_then(|v| v.as_str()) {
151                name.to_string()
152            } else if let Some(username) = obj.get("username").and_then(|v| v.as_str()) {
153                username.to_string()
154            } else {
155                serde_json::to_string(&serde_json::Value::Object(obj.clone())).unwrap()
156            }
157        }
158    }
159}