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