1use std::fs;
6
7use tern_sdk::{
8 PrintOptions,
9 ui::{self, Column, TableRow, TextAlign, TextCell, View},
10};
11
12fn human(bytes: u64) -> String {
14 const UNITS: [&str; 4] = ["K", "M", "G", "T"];
15 if bytes < 1024 {
16 return bytes.to_string();
17 }
18 let mut size = bytes as f64 / 1024.0;
19 let mut unit = 0;
20 while size >= 1024.0 && unit < UNITS.len() - 1 {
21 size /= 1024.0;
22 unit += 1;
23 }
24 if size < 10.0 {
25 format!("{size:.1}{}", UNITS[unit])
26 } else {
27 format!("{size:.0}{}", UNITS[unit])
28 }
29}
30
31struct Entry {
33 name: String,
35 dir: bool,
37 size: u64,
39}
40
41fn view(entries: &[Entry]) -> View {
44 let rows = entries.iter().map(|e| {
45 let (name, icon) = if e.dir {
46 (format!("{}/", e.name), "folder")
47 } else {
48 (e.name.clone(), "file")
49 };
50 let size = if e.dir { "-".to_owned() } else { human(e.size) };
51 TableRow::new(e.name.clone())
52 .cell("name", TextCell::new(name).icon(icon))
53 .cell("size", size)
54 });
55 View::new().main([ui::table()
56 .col(Column::new("name", "Name").grow(1.0))
57 .col(Column::new("size", "Size").align(TextAlign::End).mono(true))
58 .rows(rows.collect::<Vec<_>>())])
59}
60
61#[cfg(test)]
62mod tests {
63 use super::*;
64
65 #[test]
66 fn a_file_named_head_does_not_collide_with_the_header() {
67 let view = view(&[Entry { name: "head".into(), dir: false, size: 12 }]);
68 assert!(tern_sdk::reconcile::Doc::from_view(view.clone()).is_ok());
69 assert_eq!(tern_sdk::plain(view, 80), "Name Size\nhead 12\n");
70 }
71}
72
73fn main() -> Result<(), tern_sdk::Error> {
74 let mut entries: Vec<Entry> = fs::read_dir(".")?
75 .filter_map(Result::ok)
76 .filter_map(|e| {
77 let meta = e.metadata().ok()?;
78 Some(Entry {
79 name: e.file_name().to_string_lossy().into_owned(),
80 dir: meta.is_dir(),
81 size: meta.len(),
82 })
83 })
84 .collect();
85 entries.sort_by(|a, b| b.dir.cmp(&a.dir).then_with(|| a.name.cmp(&b.name)));
86 tern_sdk::print(view(&entries), PrintOptions::new())
87}