diff --git a/Cargo.toml b/Cargo.toml index ead0648a1..7232cd273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ rayon = "1.0" serde = {version="1.0", features = ["rc"]} serde_derive = "1.0" structopt = "0.2" +tabout = { path = "tabout" } term = { path = "term" } termwiz = { path = "termwiz"} toml = "0.4" diff --git a/src/main.rs b/src/main.rs index 083a00c8c..fef575d82 100644 --- a/src/main.rs +++ b/src/main.rs @@ -5,6 +5,7 @@ use failure::Error; use log::{error, info}; use std::ffi::OsString; use structopt::StructOpt; +use tabout::{tabulate_output, Alignment, Column}; use std::rc::Rc; use std::sync::Arc; @@ -115,7 +116,17 @@ enum SubCommand { } #[derive(Debug, StructOpt, Clone)] -struct CliCommand {} +struct CliCommand { + #[structopt(subcommand)] + sub: CliSubCommand, +} + +#[derive(Debug, StructOpt, Clone)] +enum CliSubCommand { + #[structopt(name = "list", about = "list windows and tabs")] + #[structopt(raw(setting = "structopt::clap::AppSettings::ColoredHelp"))] + List, +} fn run_terminal_gui(config: Arc, opts: &StartCommand) -> Result<(), Error> { let font_system = opts.font_system.unwrap_or(config.font_system); @@ -193,23 +204,36 @@ fn main() -> Result<(), Error> { error!("Using configuration: {:#?}\nopts: {:#?}", config, opts); run_terminal_gui(config, &start) } - SubCommand::Cli(_) => { - use crate::server::codec::*; + SubCommand::Cli(cli) => { let mut client = Client::new(&config)?; - info!("ping: {:?}", client.ping()?); - let tabs = client.list_tabs()?; - for entry in tabs.tabs.iter() { - info!("tab {} {}: {}", entry.window_id, entry.tab_id, entry.title); + match cli.sub { + CliSubCommand::List => { + let cols = vec![ + Column { + name: "WINID".to_string(), + alignment: Alignment::Right, + }, + Column { + name: "TABID".to_string(), + alignment: Alignment::Right, + }, + Column { + name: "TITLE".to_string(), + alignment: Alignment::Left, + }, + ]; + let mut data = vec![]; + let tabs = client.list_tabs()?; + for entry in tabs.tabs.iter() { + data.push(vec![ + entry.window_id.to_string(), + entry.tab_id.to_string(), + entry.title.clone(), + ]); + } + tabulate_output(&cols, &data, &mut std::io::stdout().lock())?; + } } - error!( - "spawn: {:?}", - client.spawn(Spawn { - domain_id: 0, - size: PtySize::default(), - command: None, - window_id: None, - }) - ); Ok(()) } } diff --git a/tabout/Cargo.toml b/tabout/Cargo.toml new file mode 100644 index 000000000..e84880d89 --- /dev/null +++ b/tabout/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "tabout" +version = "0.1.0" +authors = ["Wez Furlong "] +edition = "2018" +repository = "https://github.com/wez/wezterm" +description = "Tabulate output for CLI programs" +license = "MIT" +documentation = "https://docs.rs/tabout" + +[dependencies] +termwiz = { path = "../termwiz"} diff --git a/tabout/src/lib.rs b/tabout/src/lib.rs new file mode 100644 index 000000000..8c9597edb --- /dev/null +++ b/tabout/src/lib.rs @@ -0,0 +1,151 @@ +//! This crate provides some helpers to automatically tabulate data +//! so that it is presented reasonably nicely for humans to read, +//! without requiring that each column be hard coded to particular +//! widths in the code beforehand. + +/// Describes the alignment of a column +#[derive(Debug, Clone, Copy)] +pub enum Alignment { + Left, + Center, + Right, +} + +/// Describes a column +#[derive(Debug)] +pub struct Column { + /// The name of the column; this is the column header text + pub name: String, + /// How the column should be aligned + pub alignment: Alignment, +} + +fn emit_column( + text: &str, + max_width: usize, + alignment: Alignment, + output: &mut W, +) -> Result<(), std::io::Error> { + let (left_pad, right_pad) = match alignment { + Alignment::Left => (0, max_width - text.len()), + Alignment::Center => { + let left_pad = (max_width - text.len()) / 2; + // for odd-length columns, take care to use the remaining + // length rather than just assuming that the right_pad + // will have the same value as the left_pad + let right_pad = max_width - (text.len() + left_pad); + (left_pad, right_pad) + } + Alignment::Right => (max_width - text.len(), 0), + }; + + for _ in 0..left_pad { + write!(output, " ")?; + } + write!(output, "{}", text)?; + for _ in 0..right_pad { + write!(output, " ")?; + } + + Ok(()) +} + +/// Given a set of column headers and the row content, +/// automatically compute the column widths and then format +/// the data to the output stream. +/// If a given row has more columns than are defined in the +/// columns slice, then a left aligned column with no label +/// will be assumed. +pub fn tabulate_output( + columns: &[Column], + rows: &Vec>, + output: &mut W, +) -> Result<(), std::io::Error> { + let mut col_widths: Vec = columns.iter().map(|c| c.name.len()).collect(); + + let mut display_rows: Vec> = vec![]; + for src_row in rows { + let dest_row: Vec = src_row.iter().map(|col| col.to_string()).collect(); + for (idx, col) in dest_row.iter().enumerate() { + if let Some(width) = col_widths.get_mut(idx) { + *width = (*width).max(col.len()); + } else { + col_widths.push(col.len()); + } + } + display_rows.push(dest_row); + } + + for (idx, col) in columns.iter().enumerate() { + if idx > 0 { + write!(output, " ")?; + } + + emit_column(&col.name, col_widths[idx], col.alignment, output)?; + } + write!(output, "\n")?; + + for row in &display_rows { + for (idx, col) in row.iter().enumerate() { + let max_width = col_widths.get(idx).cloned().unwrap_or(col.len()); + let alignment = columns + .get(idx) + .map(|c| c.alignment) + .unwrap_or(Alignment::Left); + + if idx > 0 { + write!(output, " ")?; + } + + emit_column(col, max_width, alignment, output)?; + } + write!(output, "\n")?; + } + + Ok(()) +} + +/// A convenience around `tabulate_output` that returns a String holding +/// the formatted data. +pub fn tabulate_output_as_string( + columns: &[Column], + rows: &Vec>, +) -> Result { + let mut output: Vec = vec![]; + tabulate_output(columns, rows, &mut output)?; + String::from_utf8(output) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, format!("{}", e))) +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn basics() { + let cols = vec![ + Column { + name: "hello".to_string(), + alignment: Alignment::Left, + }, + Column { + name: "middle-of-me".to_string(), + alignment: Alignment::Center, + }, + Column { + name: "world".to_string(), + alignment: Alignment::Right, + }, + ]; + let data = vec![vec!["one", "i", "two"], vec!["longer", "boo", "again"]]; + + let output = tabulate_output_as_string(&cols, &data).unwrap(); + eprintln!("output is:\n{}", output); + assert_eq!( + output, + "hello middle-of-me world\n\ + one i two\n\ + longer boo again\n" + ); + } +}