1use crate::error::YukiError;
2use comfy_table::{Cell, Color, Table, presets::UTF8_FULL_CONDENSED};
3use serde_json::{Map, Value, json};
4
5#[derive(Default)]
7pub struct ListOptions<'a> {
8 pub limit: Option<usize>,
9 pub offset: Option<usize>,
10 pub fields: Option<&'a str>,
11}
12
13pub fn apply_pagination(rows: &mut Vec<Vec<String>>, opts: &ListOptions<'_>) {
15 if let Some(off) = opts.offset {
16 if off >= rows.len() {
17 rows.clear();
18 return;
19 }
20 rows.drain(..off);
21 }
22 if let Some(lim) = opts.limit {
23 rows.truncate(lim);
24 }
25}
26
27pub fn select_fields(
33 headers: &mut Vec<String>,
34 rows: &mut [Vec<String>],
35 opts: &ListOptions<'_>,
36) -> Result<(), YukiError> {
37 let Some(spec) = opts.fields else {
38 return Ok(());
39 };
40
41 let mut indices = Vec::new();
42 let mut selected = Vec::new();
43 for requested in spec.split(',').map(str::trim).filter(|s| !s.is_empty()) {
44 let position = headers
45 .iter()
46 .position(|h| h.eq_ignore_ascii_case(requested))
47 .ok_or_else(|| {
48 YukiError::Config(format!(
49 "unknown field: {requested} (available: {})",
50 headers.join(", ")
51 ))
52 })?;
53 indices.push(position);
54 selected.push(headers[position].clone());
55 }
56 if indices.is_empty() {
57 return Err(YukiError::Config(
58 "--fields was given no column names".to_string(),
59 ));
60 }
61
62 for row in rows.iter_mut() {
63 *row = indices
64 .iter()
65 .map(|&i| row.get(i).cloned().unwrap_or_default())
66 .collect();
67 }
68 *headers = selected;
69 Ok(())
70}
71
72pub enum OutputFormat {
73 Table,
74 Json,
75}
76
77impl OutputFormat {
78 pub fn from_flag(flag: Option<&str>, is_tty: bool) -> Self {
79 match flag {
80 Some("json") => Self::Json,
81 Some("text") | Some("table") => Self::Table,
83 Some("auto") | None => {
85 if is_tty {
86 Self::Table
87 } else {
88 Self::Json
89 }
90 }
91 Some(_) => {
93 if is_tty {
94 Self::Table
95 } else {
96 Self::Json
97 }
98 }
99 }
100 }
101}
102
103pub fn format_json(headers: &[String], rows: &[Vec<String>]) -> String {
105 let items: Vec<Value> = rows
106 .iter()
107 .map(|row| {
108 let mut map = Map::new();
109 for (i, header) in headers.iter().enumerate() {
110 let val = row.get(i).cloned().unwrap_or_default();
111 map.insert(header.clone(), Value::String(val));
112 }
113 Value::Object(map)
114 })
115 .collect();
116 let total = items.len();
117 serde_json::to_string_pretty(&json!({
118 "items": items,
119 "total": total
120 }))
121 .unwrap_or_else(|_| r#"{"items":[],"total":0}"#.into())
122}
123
124pub fn format_table(headers: &[String], rows: &[Vec<String>]) -> String {
125 let mut table = Table::new();
126 table.load_preset(UTF8_FULL_CONDENSED);
127 let header_cells: Vec<Cell> = headers
128 .iter()
129 .map(|h| {
130 Cell::new(h)
131 .fg(Color::White)
132 .add_attribute(comfy_table::Attribute::Bold)
133 })
134 .collect();
135 table.set_header(header_cells);
136 for row in rows {
137 table.add_row(row);
138 }
139 table.to_string()
140}
141
142pub fn format_error_json(message: &str, kind: &str) -> String {
147 serde_json::to_string(&json!({
148 "error": {
149 "kind": kind,
150 "message": message
151 }
152 }))
153 .unwrap_or_else(|_| format!(r#"{{"error":{{"kind":"{kind}","message":"{message}"}}}}"#))
154}
155
156pub fn is_tty() -> bool {
157 std::io::IsTerminal::is_terminal(&std::io::stdout())
158}