From 784cbef9530b400c09f698d5fd77e0e64fe86019 Mon Sep 17 00:00:00 2001 From: Ayose Date: Sun, 22 Aug 2021 03:33:13 +0100 Subject: [PATCH] Setting to render list as a table. --- Cargo.lock | 7 +++ Cargo.toml | 1 + src/format/mod.rs | 2 + src/format/tables.rs | 87 +++++++++++++++++++++++++++ src/lib.rs | 55 +++++++++++++---- src/tests/shell/change-config.test.sh | 9 +-- src/tests/shell/tables.test.sh | 16 +++++ 7 files changed, 162 insertions(+), 15 deletions(-) create mode 100644 src/format/tables.rs create mode 100644 src/tests/shell/tables.test.sh diff --git a/Cargo.lock b/Cargo.lock index a0a624c..f700479 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -196,8 +196,15 @@ dependencies = [ "plthook", "serde", "serde_json", + "unicode-width", ] +[[package]] +name = "unicode-width" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" + [[package]] name = "unicode-xid" version = "0.2.2" diff --git a/Cargo.toml b/Cargo.toml index 2f0c566..772425b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ once_cell = "1.8.0" plthook = "0" serde = { version = "1", features = ["derive"] } serde_json = "1" +unicode-width = "0.1.8" [build-dependencies] generator = { path = "generator" } diff --git a/src/format/mod.rs b/src/format/mod.rs index b247633..90fdfb1 100644 --- a/src/format/mod.rs +++ b/src/format/mod.rs @@ -5,6 +5,8 @@ use std::io::{self, Write}; use std::os::unix::ffi::OsStrExt; use std::{fmt, mem}; +pub mod tables; + pub const HELP: &[u8] = include_bytes!(concat!(env!("OUT_DIR"), "/doc.txt")); /// Render a format string with data from a `Entry` instance. diff --git a/src/format/tables.rs b/src/format/tables.rs new file mode 100644 index 0000000..3de8a6b --- /dev/null +++ b/src/format/tables.rs @@ -0,0 +1,87 @@ +//! Render a multi-line string as a table. +//! +//! Rows as separated by `\n`, and columns by `\t`. + +use std::io::{self, Write}; +use std::{mem, str}; +use unicode_width::UnicodeWidthStr; + +/// Padding between columns. +const PADDING: usize = 2; + +pub struct TableWriter { + output: T, + contents: Vec, +} + +impl TableWriter { + pub fn new(output: T) -> Self { + TableWriter { + output, + contents: Vec::new(), + } + } +} + +impl Write for TableWriter { + fn write(&mut self, buf: &[u8]) -> io::Result { + self.contents.extend_from_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + let contents = mem::take(&mut self.contents); + let lines: Vec<&str> = match str::from_utf8(&contents) { + Ok(c) => c.trim_end().split('\n').collect(), + + Err(_) => return self.output.write_all(&contents), + }; + + // Compute the width for every column. + let mut widths = Vec::new(); + + for line in &lines { + for (n, column) in line.split('\t').enumerate() { + let width = UnicodeWidthStr::width(column) + PADDING; + match widths.get_mut(n) { + Some(col) if *col < width => *col = width, + None => widths.push(width), + _ => (), + } + } + } + + // The last column does not need the width value. + if let Some(last) = widths.last_mut() { + *last = 0; + } + + // Print table. + for line in &lines { + for (column, width) in line.split('\t').zip(&widths) { + write!(self.output, "{:1$}", column, width)?; + } + + self.output.write_all(&[b'\n'])?; + } + + Ok(()) + } +} + +#[test] +fn render_test() { + let mut buf = vec![]; + let mut table = TableWriter::new(&mut buf); + + write!(&mut table, "aaa\tb\tcc\na\t\tc\na\tbbbb\na\tb\tcccc").unwrap(); + table.flush().unwrap(); + + assert_eq!( + String::from_utf8(buf).unwrap(), + "aaa b cc\n\ + a c\n\ + a bbbb \n\ + a b cccc\n" + ); +} diff --git a/src/lib.rs b/src/lib.rs index ad64248..869d131 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,7 +7,7 @@ use std::io::{self, BufWriter, Write}; builtin_metadata!( name = "timehistory", try_create = TimeHistory::new, - short_doc = "timehistory [-f FMT | -v | -j] [ | +] | -s SET | -R", + short_doc = "timehistory [-f FMT | -v | -j] [ | +] | -s | -s SET | -R", long_doc = " Displays information about the resources used by programs executed in the running shell. @@ -34,11 +34,12 @@ builtin_metadata!( format\tDefault format string. header\tShow a header with the labels of every resource. limit\tHistory limit. + table\tRender the history list as a table. To change a setting, use '-s name=value', where 'name' is any of the previous values. Use one '-s' for every setting to change. - To see the current values use '-c show'. + '-s' with no argument shows the current settings. ", ); @@ -61,6 +62,9 @@ struct TimeHistory { /// Show header with field labels. show_header: bool, + + /// Render lists as a table. + render_table: bool, } #[derive(BuiltinOptions)] @@ -78,7 +82,7 @@ enum Opt<'a> { Reset, #[opt = 's'] - Setting(&'a str), + Setting(Option<&'a str>), #[cfg(feature = "option-for-panics")] #[opt = 'P'] @@ -112,14 +116,16 @@ impl TimeHistory { Ok(TimeHistory { default_format: DEFAULT_FORMAT.into(), show_header: false, + render_table: false, }) } } impl Builtin for TimeHistory { fn call(&mut self, args: &mut Args) -> BuiltinResult<()> { + let mut table_writer; let stdout_handle = io::stdout(); - let mut output = BufWriter::new(stdout_handle.lock()); + let mut output = &mut BufWriter::new(stdout_handle.lock()) as &mut dyn Write; let mut history = match crate::ipc::events::collect_events(true) { Some(history) => history, @@ -158,22 +164,34 @@ impl Builtin for TimeHistory { Opt::Reset => action = Action::Reset, - Opt::Setting("show") => { + Opt::Setting(None) => { self.print_config(&mut output, &history)?; exit_after_options = true; } - Opt::Setting(setting) => { + Opt::Setting(Some(setting)) => { let mut parts = setting.splitn(2, '='); match (parts.next(), parts.next()) { (Some("limit"), Some(value)) => { history.set_size(value.parse()?); } + (Some("header"), None) => { + self.show_header = true; + } + (Some("header"), Some(value)) => { self.show_header = value.parse()?; } + (Some("table"), Some(value)) => { + self.render_table = value.parse()?; + } + + (Some("table"), None) => { + self.render_table = true; + } + (Some("format"), Some(value)) => { self.default_format = if value.is_empty() { DEFAULT_FORMAT.into() @@ -228,7 +246,16 @@ impl Builtin for TimeHistory { Some(Output::Json) => None, }; - if self.show_header { + // Use headers/tables. + let decorate = matches!(&output_format, None | Some(Output::Format(_))); + + // Render output as a table. + if decorate && self.render_table { + table_writer = format::tables::TableWriter::new(output); + output = &mut table_writer as &mut dyn Write; + } + + if decorate && self.show_header { if let Some(fmt) = &format { format::labels(fmt, &mut output)?; output.write_all(b"\n")?; @@ -276,20 +303,26 @@ impl Builtin for TimeHistory { } } + output.flush()?; + Ok(()) } } impl TimeHistory { fn print_config(&self, mut output: impl Write, history: &history::History) -> io::Result<()> { - writeln!( + write!( &mut output, - "format={}\n\ - header={}\n\ - limit={}", + "\ + format = {}\n\ + header = {}\n\ + limit = {}\n\ + table = {}\n\ + ", self.default_format, self.show_header, history.size(), + self.render_table, )?; Ok(()) diff --git a/src/tests/shell/change-config.test.sh b/src/tests/shell/change-config.test.sh index e06fa1d..1d814de 100644 --- a/src/tests/shell/change-config.test.sh +++ b/src/tests/shell/change-config.test.sh @@ -6,11 +6,12 @@ load_builtin timehistory -s limit=5000 -s format='%n\t%P\t%C' ASSERT_OUTPUT \ - "timehistory -s show" \ + "timehistory -s" \ <<-'ITEMS' - format=%n\t%P\t%C - header=false - limit=5000 + format = %n\t%P\t%C + header = false + limit = 5000 + table = false ITEMS timehistory -s format='> %C' diff --git a/src/tests/shell/tables.test.sh b/src/tests/shell/tables.test.sh new file mode 100644 index 0000000..2149881 --- /dev/null +++ b/src/tests/shell/tables.test.sh @@ -0,0 +1,16 @@ +# Test to use the shell session after removing the builtin. + +load_builtin + +/bin/true 1 +/bin/true 2 + +timehistory -s header=true -s table=true + +ASSERT_OUTPUT \ + "timehistory -f '%n\t%C'" \ + <<-ITEMS + NUMBER COMMAND + 1 /bin/true 1 + 2 /bin/true 2 +ITEMS