1use owo_colors::OwoColorize;
2
3use crate::api::{Device, UnifiClient, format_mac, format_uptime};
4use crate::commands::ports::{self, PortRow};
5use crate::output::{OutputConfig, use_color};
6
7pub struct Pagination {
8 pub limit: usize,
9 pub offset: usize,
10 pub fields: Option<Vec<String>>,
12}
13
14fn render_devices(devices: &[Device], out: &OutputConfig) {
15 if out.is_json() {
16 out.print_data(
17 &serde_json::to_string_pretty(
18 &devices
19 .iter()
20 .map(|d| {
21 serde_json::json!({
22 "name": d.name,
23 "model": d.model,
24 "mac": d.mac_address,
25 "ip": d.ip_address,
26 "state": d.state,
27 "firmware": d.firmware_version,
28 })
29 })
30 .collect::<Vec<_>>(),
31 )
32 .expect("failed to serialize JSON"),
33 );
34 } else {
35 let color = use_color();
36
37 let col = |min: usize, label_len: usize, vals: Vec<usize>| -> usize {
39 vals.into_iter().max().unwrap_or(0).max(label_len).max(min) + 2
40 };
41 let names: Vec<&str> = devices
42 .iter()
43 .map(|d| d.name.as_deref().unwrap_or("-"))
44 .collect();
45 let models: Vec<&str> = devices
46 .iter()
47 .map(|d| d.model.as_deref().unwrap_or("-"))
48 .collect();
49
50 let name_w = col(4, 4, names.iter().map(|n| n.len()).collect());
51 let model_w = col(5, 5, models.iter().map(|m| m.len()).collect());
52 let total_w = name_w + model_w + 19 + 15 + 10 + 10;
53
54 let header = format!(
55 "{:<name_w$} {:<model_w$} {:<19} {:<15} {:<10} {}",
56 "Name", "Model", "MAC", "IP", "State", "Firmware"
57 );
58 if color {
59 println!("{}", header.bold());
60 println!("{}", "-".repeat(total_w).dimmed());
61 } else {
62 println!("{header}");
63 println!("{}", "-".repeat(total_w));
64 }
65
66 for d in devices {
67 let name = d.name.as_deref().unwrap_or("-");
68 let model = d.model.as_deref().unwrap_or("-");
69 let mac = d
70 .mac_address
71 .as_deref()
72 .map(format_mac)
73 .unwrap_or_else(|| "-".into());
74 let ip = d.ip_address.as_deref().unwrap_or("-");
75 let state = d.state.as_deref().unwrap_or("-");
76 let fw = d.firmware_version.as_deref().unwrap_or("-");
77 let name_pad = name_w - 1;
78 let model_pad = model_w;
79
80 if color {
81 println!(
82 " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
83 name.bold(),
84 model,
85 mac.dimmed(),
86 ip,
87 state,
88 fw,
89 );
90 } else {
91 println!(
92 " {:<name_pad$} {:<model_pad$} {:<19} {:<15} {:<10} {}",
93 name, model, mac, ip, state, fw
94 );
95 }
96 }
97 }
98 out.print_message(&format!("\n{} devices", devices.len()));
99}
100
101pub async fn list(
102 client: &mut UnifiClient,
103 out: OutputConfig,
104 watch: Option<u64>,
105 pagination: Pagination,
106) -> Result<(), Box<dyn std::error::Error>> {
107 if let Some(interval) = watch {
108 use crossterm::execute;
109 use crossterm::terminal::EnterAlternateScreen;
110
111 let mut stdout = std::io::stdout();
112 execute!(stdout, EnterAlternateScreen)?;
113
114 loop {
115 execute!(stdout, crossterm::cursor::MoveTo(0, 0))?;
116 execute!(
117 stdout,
118 crossterm::terminal::Clear(crossterm::terminal::ClearType::All)
119 )?;
120 eprintln!("Every {interval}s | devices list (press Ctrl+C to exit)\n");
121 match client.list_devices().await {
122 Ok(devices) => {
123 render_devices(&devices, &out);
124 }
125 Err(e) => {
126 eprintln!("Error: {e}");
127 }
128 }
129 tokio::time::sleep(std::time::Duration::from_secs(interval)).await;
130 }
131 } else {
132 let devices = client.list_devices().await?;
133 let total = devices.len();
134 let paginated: Vec<Device> = devices
135 .into_iter()
136 .skip(pagination.offset)
137 .take(pagination.limit)
138 .collect();
139 if out.is_json() {
140 let items: Vec<serde_json::Value> = paginated
141 .iter()
142 .map(|d| {
143 let mut obj = serde_json::json!({
144 "name": d.name,
145 "model": d.model,
146 "mac": d.mac_address,
147 "ip": d.ip_address,
148 "state": d.state,
149 "firmware": d.firmware_version,
150 });
151 if let Some(ref keep) = pagination.fields {
152 let map = obj.as_object_mut().expect("device is a JSON object");
153 map.retain(|k, _| keep.iter().any(|f| f == k));
154 }
155 obj
156 })
157 .collect();
158 out.print_data(
159 &serde_json::to_string_pretty(&serde_json::json!({
160 "items": items,
161 "total": total,
162 "limit": pagination.limit,
163 "offset": pagination.offset,
164 }))
165 .expect("failed to serialize JSON"),
166 );
167 } else {
168 render_devices(&paginated, &out);
169 }
170 Ok(())
171 }
172}
173
174pub async fn show(
175 client: &UnifiClient,
176 mac: &str,
177 out: OutputConfig,
178) -> Result<(), Box<dyn std::error::Error>> {
179 let d = client.get_device_detail(mac).await?;
180
181 if out.is_json() {
182 out.print_data(&serde_json::to_string_pretty(&serde_json::json!({
183 "name": d.name,
184 "model": d.model,
185 "mac": d.mac,
186 "ip": d.ip,
187 "state": d.state_str(),
188 "firmware": d.version,
189 "version": d.version,
190 "uptime": d.uptime,
191 "num_sta": d.num_sta,
192 }))?);
193 return Ok(());
194 }
195
196 let color = use_color();
197 let label = |l: &str| -> String {
198 if color {
199 format!("{}", l.dimmed())
200 } else {
201 l.to_string()
202 }
203 };
204
205 let name = d.name.as_deref().unwrap_or("Device");
206 if color {
207 println!("{}", name.bold());
208 } else {
209 println!("{name}");
210 }
211
212 println!(
213 " {} {}",
214 label("Model: "),
215 d.model.as_deref().unwrap_or("-")
216 );
217 println!(
218 " {} {}",
219 label("MAC: "),
220 d.mac
221 .as_deref()
222 .map(format_mac)
223 .unwrap_or_else(|| "-".into())
224 );
225 println!(
226 " {} {}",
227 label("IP: "),
228 d.ip.as_deref().unwrap_or("-")
229 );
230 println!(" {} {}", label("State: "), d.state_str());
231
232 if let Some(ref v) = d.version {
233 println!(" {} {v}", label("Firmware:"));
234 }
235 if let Some(uptime) = d.uptime {
236 println!(" {} {}", label("Uptime: "), format_uptime(uptime));
237 }
238 if let Some(num_sta) = d.num_sta {
239 println!(" {} {num_sta}", label("Clients: "));
240 }
241
242 Ok(())
243}
244
245pub async fn restart(
246 client: &UnifiClient,
247 mac: &str,
248 out: OutputConfig,
249) -> Result<(), Box<dyn std::error::Error>> {
250 client.restart_device(mac).await?;
251 out.print_result(
252 &serde_json::json!({"status": "ok", "action": "restart", "mac": format_mac(mac)}),
253 &format!("Restarting {}", format_mac(mac)),
254 );
255 Ok(())
256}
257
258pub async fn ports(
263 client: &UnifiClient,
264 mac: &str,
265 out: OutputConfig,
266) -> Result<(), Box<dyn std::error::Error>> {
267 let device = client.get_device_ports(mac).await?;
268
269 if device.port_table.is_empty() {
270 out.print_message("No port table available for this device (not a switch or router)");
271 if out.is_json() {
272 out.print_data("[]");
273 }
274 return Ok(());
275 }
276
277 let devices = vec![device];
278 let rows = ports::collect_rows_with_fallback(&devices, "Device");
281
282 if out.is_json() {
283 let items: Vec<serde_json::Value> = rows.iter().map(ports::row_json).collect();
284 out.print_data(&serde_json::to_string_pretty(&items)?);
285 } else {
286 let label = &rows[0].device_name;
287 out.print_message(&format!("Ports for {label}:\n"));
288 let refs: Vec<&PortRow> = rows.iter().collect();
289 let dev_w = ports::device_col_width(&refs);
290 ports::render_text(&refs, false, dev_w, &out);
291 }
292 Ok(())
293}
294
295pub async fn upgrade(
296 client: &UnifiClient,
297 mac: &str,
298 out: OutputConfig,
299) -> Result<(), Box<dyn std::error::Error>> {
300 client.upgrade_device(mac).await?;
301 out.print_result(
302 &serde_json::json!({"status": "ok", "action": "upgrade", "mac": format_mac(mac)}),
303 &format!("Upgrading firmware on {}", format_mac(mac)),
304 );
305 Ok(())
306}
307
308pub async fn locate(
309 client: &UnifiClient,
310 mac: &str,
311 off: bool,
312 out: OutputConfig,
313) -> Result<(), Box<dyn std::error::Error>> {
314 client.locate_device(mac, !off).await?;
315 let action = if off { "locate_off" } else { "locate_on" };
316 let msg = if off {
317 format!("Stopped locating {}", format_mac(mac))
318 } else {
319 format!("Locating {} (LED blinking)", format_mac(mac))
320 };
321 out.print_result(
322 &serde_json::json!({"status": "ok", "action": action, "mac": format_mac(mac)}),
323 &msg,
324 );
325 Ok(())
326}