alopex_cli/tui/
renderer.rs1use crate::cli::OutputFormat;
4use crate::error::Result;
5use crate::models::{Column, Row, Value};
6use crate::output::formatter::create_formatter;
7
8use super::TuiApp;
9
10#[allow(clippy::too_many_arguments)]
11pub fn render_output<'a>(
12 columns: Vec<Column>,
13 rows: Vec<Row>,
14 connection_label: impl Into<String>,
15 context_message: Option<String>,
16 processing: bool,
17 status_message: Option<String>,
18 output_format: OutputFormat,
19 admin_launcher: Option<Box<dyn FnMut() -> Result<()> + 'a>>,
20) -> Result<()> {
21 let fallback_columns = columns.clone();
22 let fallback_rows = rows.clone();
23 let output_status = status_message_for(&columns, &rows);
24 let status_message = output_status.clone().or(status_message);
25 let rows = if output_status.is_some() {
26 Vec::new()
27 } else {
28 rows
29 };
30 let mut app = TuiApp::new(columns, rows, connection_label, processing)
31 .with_context_message(context_message)
32 .with_admin_launcher(admin_launcher);
33 if let Some(message) = status_message {
34 app = app.with_status_message(message);
35 }
36 if app.run().is_err() {
37 let mut formatter = create_formatter(output_format);
38 let mut writer = std::io::stdout().lock();
39 formatter.write_header(&mut writer, &fallback_columns)?;
40 for row in &fallback_rows {
41 formatter.write_row(&mut writer, row)?;
42 }
43 formatter.write_footer(&mut writer)?;
44 return Ok(());
45 }
46 Ok(())
47}
48
49fn status_message_for(columns: &[Column], rows: &[Row]) -> Option<String> {
50 let row = rows.first()?;
51 if rows.len() != 1 || columns.len() != 2 {
52 return None;
53 }
54 if !column_name_eq(&columns[0].name, "status") || !column_name_eq(&columns[1].name, "message") {
55 return None;
56 }
57 let status = row.columns.first().map(value_to_string)?;
58 let message = row.columns.get(1).map(value_to_string)?;
59 Some(format!("{status}: {message}"))
60}
61
62fn column_name_eq(name: &str, expected: &str) -> bool {
63 name.eq_ignore_ascii_case(expected)
64}
65
66fn value_to_string(value: &Value) -> String {
67 match value {
68 Value::Null => "NULL".to_string(),
69 Value::Bool(b) => b.to_string(),
70 Value::Int(i) => i.to_string(),
71 Value::Float(f) => format!("{f:.6}"),
72 Value::Text(text) => text.clone(),
73 Value::Bytes(bytes) => {
74 let hex: String = bytes
75 .iter()
76 .take(32)
77 .map(|byte| format!("{byte:02x}"))
78 .collect();
79 if bytes.len() > 32 {
80 format!("{hex}...")
81 } else {
82 hex
83 }
84 }
85 Value::Vector(values) => {
86 if values.len() <= 4 {
87 format!(
88 "[{}]",
89 values
90 .iter()
91 .map(|value| format!("{value:.4}"))
92 .collect::<Vec<_>>()
93 .join(", ")
94 )
95 } else {
96 format!(
97 "[{}, ... ({} dims)]",
98 values
99 .iter()
100 .take(3)
101 .map(|value| format!("{value:.4}"))
102 .collect::<Vec<_>>()
103 .join(", "),
104 values.len()
105 )
106 }
107 }
108 }
109}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114 use crate::models::DataType;
115
116 #[test]
117 fn detects_status_message() {
118 let columns = vec![
119 Column::new("status", DataType::Text),
120 Column::new("message", DataType::Text),
121 ];
122 let rows = vec![Row::new(vec![
123 Value::Text("OK".to_string()),
124 Value::Text("Updated key".to_string()),
125 ])];
126
127 let message = status_message_for(&columns, &rows);
128 assert_eq!(message, Some("OK: Updated key".to_string()));
129 }
130
131 #[test]
132 fn ignores_non_status_output() {
133 let columns = vec![Column::new("id", DataType::Int)];
134 let rows = vec![Row::new(vec![Value::Int(1)])];
135
136 let message = status_message_for(&columns, &rows);
137 assert_eq!(message, None);
138 }
139
140 #[test]
141 fn keeps_rich_health_rows_visible() {
142 let columns = vec![
143 Column::new("status", DataType::Text),
144 Column::new("message", DataType::Text),
145 Column::new("degraded", DataType::Bool),
146 ];
147 let rows = vec![Row::new(vec![
148 Value::Text("degraded".to_string()),
149 Value::Text("cluster status degraded".to_string()),
150 Value::Bool(true),
151 ])];
152
153 let message = status_message_for(&columns, &rows);
154 assert_eq!(message, None);
155 }
156}