Skip to content

Commit 39c018f

Browse files
Add display module and table output
Introduce client/src/display.rs providing table printing, styled headers/values, and a short fingerprint summary helper. Replace ad-hoc prints in main.rs for listing ID files and known hosts with sorted table output using display::print_table, and integrate fingerprints_summary for compact fingerprint display. Also add terminal detection (IsTerminal) to color_choice so colors are disabled when TERM=dumb or stdout is not a TTY.
1 parent 8a12a59 commit 39c018f

2 files changed

Lines changed: 148 additions & 8 deletions

File tree

client/src/display.rs

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use clap::ColorChoice;
2+
3+
const RESET: &str = "\x1b[0m";
4+
const BOLD: &str = "\x1b[1m";
5+
const DIM: &str = "\x1b[2m";
6+
const UNDERLINE: &str = "\x1b[4m";
7+
const GREEN: &str = "\x1b[32m";
8+
const YELLOW: &str = "\x1b[33m";
9+
const CYAN: &str = "\x1b[36m";
10+
11+
#[derive(Debug, Clone)]
12+
struct TableCell {
13+
text: String,
14+
visible_len: usize,
15+
}
16+
17+
impl TableCell {
18+
fn new(text: String, visible_len: usize) -> Self {
19+
Self { text, visible_len }
20+
}
21+
}
22+
23+
pub fn print_table(headers: &[&str], rows: &[Vec<String>], color_choice: ColorChoice) {
24+
let mut widths: Vec<usize> = headers.iter().map(|h| h.len()).collect();
25+
for row in rows {
26+
for (i, cell) in row.iter().enumerate() {
27+
if i >= widths.len() {
28+
widths.push(cell.len());
29+
} else {
30+
widths[i] = widths[i].max(cell.len());
31+
}
32+
}
33+
}
34+
35+
let header_cells: Vec<TableCell> = headers
36+
.iter()
37+
.map(|h| TableCell::new(styled_header(h, color_choice), h.len()))
38+
.collect();
39+
println!("{}", format_table_row(&header_cells, &widths));
40+
41+
for row in rows {
42+
let cells: Vec<TableCell> = row
43+
.iter()
44+
.enumerate()
45+
.map(|(i, v)| TableCell::new(styled_value(i, v, color_choice), v.len()))
46+
.collect();
47+
println!("{}", format_table_row(&cells, &widths));
48+
}
49+
}
50+
51+
pub fn fingerprints_summary(fingerprints: &[Vec<u8>]) -> String {
52+
if fingerprints.is_empty() {
53+
return "-".to_string();
54+
}
55+
56+
let first = hex_fingerprint_short(&fingerprints[0], 16);
57+
if fingerprints.len() == 1 {
58+
first
59+
} else {
60+
format!("{} (+{})", first, fingerprints.len() - 1)
61+
}
62+
}
63+
64+
fn format_table_row(cells: &[TableCell], widths: &[usize]) -> String {
65+
let mut out = String::new();
66+
for (i, cell) in cells.iter().enumerate() {
67+
if i > 0 {
68+
out.push_str(" ");
69+
}
70+
let width = widths.get(i).copied().unwrap_or(0);
71+
out.push_str(&cell.text);
72+
let padding = width.saturating_sub(cell.visible_len);
73+
for _ in 0..padding {
74+
out.push(' ');
75+
}
76+
}
77+
out
78+
}
79+
80+
fn styled_header(text: &str, color_choice: ColorChoice) -> String {
81+
if matches!(color_choice, ColorChoice::Never) {
82+
return text.to_string();
83+
}
84+
format!("{BOLD}{UNDERLINE}{YELLOW}{text}{RESET}")
85+
}
86+
87+
fn styled_value(col: usize, text: &str, color_choice: ColorChoice) -> String {
88+
if matches!(color_choice, ColorChoice::Never) {
89+
return text.to_string();
90+
}
91+
92+
if text == "-" {
93+
return format!("{DIM}{text}{RESET}");
94+
}
95+
96+
let color = match col {
97+
0 => CYAN,
98+
1 => GREEN,
99+
_ => YELLOW,
100+
};
101+
format!("{color}{text}{RESET}")
102+
}
103+
104+
fn hex_fingerprint_short(bytes: &[u8], max_bytes: usize) -> String {
105+
let take = bytes.len().min(max_bytes);
106+
let mut parts = Vec::with_capacity(take);
107+
for b in &bytes[..take] {
108+
parts.push(format!("{:02X}", b));
109+
}
110+
let mut s = parts.join(":");
111+
if bytes.len() > take {
112+
s.push_str(":...");
113+
}
114+
s
115+
}

client/src/main.rs

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@ use libgsh::{
1717
},
1818
},
1919
};
20+
use std::io::IsTerminal;
2021
use std::process::exit;
2122

2223
mod auth;
2324
mod client;
2425
mod config;
26+
mod display;
2527
mod network;
2628

2729
#[derive(Parser, Debug)]
@@ -113,10 +115,18 @@ async fn main() {
113115
println!("ID file created at {} for {}", path.display(), name);
114116
}
115117
IdCommand::List => {
116-
println!("ID files:");
117-
for (id_name, id_file) in id_files.files() {
118-
println!("- {}: {}", id_name, id_file.display());
119-
}
118+
let mut rows: Vec<(String, String)> = id_files
119+
.files()
120+
.into_iter()
121+
.map(|(name, path)| (name, path.display().to_string()))
122+
.collect();
123+
rows.sort_by(|a, b| a.0.cmp(&b.0));
124+
125+
let table_rows: Vec<Vec<String>> = rows
126+
.into_iter()
127+
.map(|(name, path)| vec![name, path])
128+
.collect();
129+
display::print_table(&["NAME", "PATH"], &table_rows, color_choice);
120130
}
121131
IdCommand::Verify { name } => {
122132
const MESSAGE: &[u8] = b"test";
@@ -141,10 +151,22 @@ async fn main() {
141151
},
142152
Command::Host { command } => match command {
143153
HostCommand::List => {
144-
println!("Known hosts:");
145-
for host in known_hosts.hosts {
146-
println!("Host: {}, Fingerprints: {:?}", host.host, host.fingerprints);
147-
}
154+
let mut rows: Vec<(String, String, String)> = known_hosts
155+
.hosts
156+
.iter()
157+
.map(|h| {
158+
let fp = display::fingerprints_summary(&h.fingerprints);
159+
let id = h.id_file_ref.clone().unwrap_or_else(|| "-".to_string());
160+
(h.host.clone(), id, fp)
161+
})
162+
.collect();
163+
rows.sort_by(|a, b| a.0.cmp(&b.0));
164+
165+
let table_rows: Vec<Vec<String>> = rows
166+
.into_iter()
167+
.map(|(host, id, fp)| vec![host, id, fp])
168+
.collect();
169+
display::print_table(&["HOST", "ID", "FINGERPRINT"], &table_rows, color_choice);
148170
}
149171
},
150172
}
@@ -222,6 +244,9 @@ fn color_choice() -> ColorChoice {
222244
if matches!(std::env::var("TERM").as_deref(), Ok("dumb")) {
223245
return ColorChoice::Never;
224246
}
247+
if !std::io::stdout().is_terminal() {
248+
return ColorChoice::Never;
249+
}
225250
ColorChoice::Always
226251
}
227252

0 commit comments

Comments
 (0)