Skip to main content

table/
table.rs

1//! `print`: the current directory as a table, sizes right-aligned by a
2//! stylesheet. In Tern it stays in the scrollback as a native table;
3//! anywhere else it prints as aligned plain text.
4
5use std::fs;
6
7use tern_sdk::{
8	PrintOptions,
9	ui::{View, html},
10};
11
12/// The table's look: header muted, sizes right-aligned.
13const CSS: &str = ".ls{border-spacing:0} .ls th{text-align:left;padding:0 18px 3px \
14                   0;color:var(--sf-c-muted);font-weight:500} .ls td{padding:1px 18px 1px 0} .ls \
15                   .size{text-align:right} .ls .dir{color:var(--sf-c-accent)}";
16
17/// A size as `312`, `2.1K`, `14K`, `3.4M`.
18fn human(bytes: u64) -> String {
19	const UNITS: [&str; 4] = ["K", "M", "G", "T"];
20	if bytes < 1024 {
21		return bytes.to_string();
22	}
23	let mut size = bytes as f64 / 1024.0;
24	let mut unit = 0;
25	while size >= 1024.0 && unit < UNITS.len() - 1 {
26		size /= 1024.0;
27		unit += 1;
28	}
29	if size < 10.0 {
30		format!("{size:.1}{}", UNITS[unit])
31	} else {
32		format!("{size:.0}{}", UNITS[unit])
33	}
34}
35
36/// One entry: its name, whether it is a directory, and its size.
37struct Entry {
38	/// The file name.
39	name: String,
40	/// A directory.
41	dir:  bool,
42	/// The size in bytes.
43	size: u64,
44}
45
46/// The directory listing as a table.
47fn view(entries: &[Entry]) -> View {
48	let head = html::tr()
49		.child(html::th("Name"))
50		.child(html::th("Size").class("size"));
51	let rows = entries.iter().map(|e| {
52		let name = if e.dir {
53			format!("{}/", e.name)
54		} else {
55			e.name.clone()
56		};
57		let size = if e.dir { "-".to_owned() } else { human(e.size) };
58		html::tr()
59			.child(html::td(name).class(if e.dir { "dir" } else { "file" }))
60			.child(html::td(size).class("size"))
61	});
62	View::new().main([html::table()
63		.class("ls")
64		.child(html::thead().child(head))
65		.child(html::tbody().children(rows))])
66}
67
68#[cfg(test)]
69mod tests {
70	use super::*;
71
72	#[test]
73	fn a_file_named_head_does_not_collide_with_the_header() {
74		let view = view(&[Entry { name: "head".into(), dir: false, size: 12 }]);
75		assert!(tern_sdk::reconcile::Doc::from_view(view.clone()).is_ok());
76		assert_eq!(tern_sdk::plain(view, 80), "Name  Size\nhead  12\n");
77	}
78}
79
80fn main() -> Result<(), tern_sdk::Error> {
81	let mut entries: Vec<Entry> = fs::read_dir(".")?
82		.filter_map(Result::ok)
83		.filter_map(|e| {
84			let meta = e.metadata().ok()?;
85			Some(Entry {
86				name: e.file_name().to_string_lossy().into_owned(),
87				dir:  meta.is_dir(),
88				size: meta.len(),
89			})
90		})
91		.collect();
92	entries.sort_by(|a, b| b.dir.cmp(&a.dir).then_with(|| a.name.cmp(&b.name)));
93	tern_sdk::print(view(&entries), PrintOptions::new().css(CSS))
94}