improve CLI and refactors

This commit is contained in:
LordMZTE 2022-03-25 01:05:19 +01:00
parent b14a7a50fd
commit 2acbb468ad
7 changed files with 177 additions and 207 deletions

View File

@ -2,7 +2,7 @@ use std::{fs, rc::Rc, thread};
use crossbeam_channel::{bounded, Sender};
use libupgr::config::{Config, Step};
use miette::{miette, WrapErr, IntoDiagnostic};
use miette::{miette, IntoDiagnostic, WrapErr};
use mlua::Lua;
pub fn start() -> miette::Result<LuaActorHandle> {

View File

@ -48,7 +48,7 @@ pub enum IntoCommandError {
}
impl CfgCommand {
pub fn into_command(self, shell: &[String]) -> Result<Command, IntoCommandError> {
pub fn as_command(&self, shell: &[String]) -> Result<Command, IntoCommandError> {
match self {
Self::Args(args) => {
let mut cmd = Command::new(args.first().ok_or(IntoCommandError::EmptyArgs)?);

View File

@ -8,10 +8,12 @@ edition = "2018"
[dependencies]
clap = { version = "3.1.6", features = ["derive"] }
dirs = "4.0.0"
humantime = "2.1.0"
libupgr = { path = "../libupgr" }
miette = { version = "4.2.1", features = ["fancy"] }
mlua = { version = "0.7.4", features = ["luajit", "serialize"] }
owo-colors = "3.3.0"
sled = "0.34.7"
terminal_size = "0.1.17"
[features]

View File

@ -1,160 +0,0 @@
use std::time::Duration;
use owo_colors::OwoColorize;
pub struct InfoTable {
rows: Vec<(String, String, usize)>,
title: String,
width: usize,
}
impl InfoTable {
pub fn new_with_title<T>(title: T) -> Self
where
T: ToString,
{
let title = title.to_string();
Self {
rows: Vec::new(),
width: title.len(),
title,
}
}
pub fn row<K, V>(&mut self, key: K, value: &V) -> &mut Self
where
K: ToString,
V: ToInfoData,
{
let key = key.to_string();
let info_data = value.to_info_data();
let width = value
.info_width()
.or_else(|| info_data.lines().map(|s| s.len()).max())
.unwrap_or(0) +
key.len();
self.rows.push((key, info_data, width));
if self.width < width {
self.width = width;
}
self
}
}
impl ToString for InfoTable {
fn to_string(&self) -> String {
let mut buf = String::new();
buf += &format!("{:━<width$}\n", &self.title, width = self.width + 4);
for &(ref key, ref val, width) in &self.rows {
buf += &format!(
"┃ {}: {}{: <width$} ┃\n",
key,
val,
"",
width = self.width - width,
);
}
buf += &format!("{:━<width$}", "", width = self.width + 4);
buf
}
}
/// A trait semilar to ToString, but exclusively used by InfoTable. This is an
/// additional trait so coloring can be supported easily
pub trait ToInfoData {
/// The String that should be displayed in the data column of the InfoTable.
fn to_info_data(&self) -> String;
/// The width that the data returned by to_info_data has. Useful when using
/// colors. If this returns None, the len of to_info_data is used.
fn info_width(&self) -> Option<usize> {
None
}
}
impl ToInfoData for &str {
fn to_info_data(&self) -> String {
self.to_string()
}
}
impl ToInfoData for bool {
fn to_info_data(&self) -> String {
if *self {
"yes".green().to_string()
} else {
"no".red().to_string()
}
}
fn info_width(&self) -> Option<usize> {
Some(if *self { 3 } else { 2 })
}
}
impl ToInfoData for Duration {
fn to_info_data(&self) -> String {
format!("{}ms", self.as_millis().purple())
}
fn info_width(&self) -> Option<usize> {
Some(self.as_millis().to_string().len() + 2)
}
}
pub struct CustomTableData<'a>(pub &'a str, pub usize);
impl<'a> ToInfoData for CustomTableData<'a> {
fn to_info_data(&self) -> String {
self.0.to_string()
}
fn info_width(&self) -> Option<usize> {
Some(self.1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn info_table_simple() {
let s = InfoTable::new_with_title("Test Title")
.row("Test Key", &"Test Value")
.to_string();
assert_eq!(
s,
"\
Test Title
Test Key: Test Value
",
)
}
#[test]
fn info_table_multi() {
let s = InfoTable::new_with_title("Test Title")
.row("Test Key", &"Test Value")
.row("Test Key 2", &"Test Value 2")
.to_string();
assert_eq!(
s,
"\
Test Title
Test Key: Test Value
Test Key 2: Test Value 2
",
)
}
}

View File

@ -1,4 +1,4 @@
use crate::info_table::{CustomTableData, InfoTable, ToInfoData};
use crate::output::{info_table::InfoTable, FormattedCfgCommand};
use clap::Parser;
use libupgr::{
config::{CfgCommand, Config},
@ -7,9 +7,16 @@ use libupgr::{
use miette::{miette, Context, IntoDiagnostic};
use mlua::{prelude::LuaFunction, Lua};
use owo_colors::OwoColorize;
use std::{fs::File, io::Read, path::PathBuf, rc::Rc, time::Instant};
use std::{
fs::File,
io::{self, Read},
path::PathBuf,
rc::Rc,
time::{Duration, Instant},
};
use terminal_size::terminal_size;
mod info_table;
mod output;
#[derive(Parser)]
struct Opt {
@ -86,7 +93,7 @@ pub fn exec_steps(conf: Config, uninteractive: bool) -> miette::Result<()> {
}
pub fn exec_cmd(
cmd: CfgCommand,
cfg_cmd: CfgCommand,
shell: &[String],
when: Option<LuaFunction>,
workdir: Option<PathBuf>,
@ -98,63 +105,59 @@ pub fn exec_cmd(
.wrap_err("Failed to call when function")?;
if !val {
println!("Skipping {}", cmd.to_info_data());
println!(
"Skipping {}",
FormattedCfgCommand::new(&cfg_cmd, Some(shell))
);
return Ok(());
}
}
let mut table = InfoTable::new_with_title("Running");
table.row("Command", &cmd);
let mut table = InfoTable::with_title("Running");
table.row("Command", FormattedCfgCommand::new(&cfg_cmd, Some(shell)));
let mut cmd = cmd.into_command(shell)?;
let mut cmd = cfg_cmd.as_command(shell)?;
if let Some(workdir) = workdir {
table.row("Workdir", &workdir.to_string_lossy().as_ref());
cmd.current_dir(workdir);
}
println!("{}", table.to_string());
let (terminal_size::Width(width), _) =
terminal_size().ok_or_else(|| miette!("Couldn't get terminal size"))?;
table
.write_to(io::stdout(), width as usize)
.into_diagnostic()?;
let start_time = Instant::now();
let exit = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
let total_time = Instant::now() - start_time;
let mut data = String::new();
println!(
"{}",
InfoTable::new_with_title("Summary")
.row("Time", &total_time)
.row(
"Exit Code",
&exit
.code()
.map(|s| if s == 0 {
data = 0.green().to_string();
CustomTableData(&data, 1)
} else {
let s = s.to_string();
data = s.red().to_string();
CustomTableData(&data, s.len())
})
.unwrap_or(CustomTableData("Unknown", 7))
InfoTable::with_title("Summary")
.row("Command", FormattedCfgCommand::new(&cfg_cmd, Some(shell)))
.row(
"Time",
humantime::format_duration(
// only be precise to milliseconds
Duration::from_millis(total_time.as_millis() as u64),
)
.to_string()
);
.blue(),
)
.row(
"Exit Code",
&exit
.code()
.map(|s| {
if s == 0 {
0.green().to_string()
} else {
s.red().to_string()
}
})
.unwrap_or_else(|| "Unknown".blue().to_string()),
)
.write_to(io::stdout(), width as usize)
.into_diagnostic()?;
Ok(())
}
impl ToInfoData for CfgCommand {
fn to_info_data(&self) -> String {
match self {
Self::Shell(s) => s.green().to_string(),
Self::Args(args) => args.join(" ").green().to_string(),
}
}
fn info_width(&self) -> Option<usize> {
Some(match self {
Self::Shell(s) => s.len(),
Self::Args(a) => a.iter().map(String::len).sum::<usize>() + a.len() - 1,
})
}
}

View File

@ -0,0 +1,39 @@
use std::io::{self, Write};
pub struct InfoTable {
rows: Vec<(String, String)>,
title: String,
}
impl InfoTable {
pub fn with_title<T>(title: T) -> Self
where
T: ToString,
{
let title = title.to_string();
Self {
rows: Vec::new(),
title,
}
}
pub fn row(&mut self, key: impl ToString, value: impl ToString) -> &mut Self {
self.rows.push((key.to_string(), value.to_string()));
self
}
pub fn write_to(&self, mut writer: impl Write, width: usize) -> io::Result<()> {
writeln!(writer, "━{:━<width$}", &self.title, width = width - 1)?;
for &(ref key, ref val) in &self.rows {
writeln!(writer, "{}: {}", key, val)?;
}
writer.write_all("".repeat(width).as_bytes())?;
writer.write_all(b"\n")?;
Ok(())
}
}

86
upgr/src/output/mod.rs Normal file
View File

@ -0,0 +1,86 @@
use std::{
cell::Cell,
fmt::{Display, Write},
};
use libupgr::config::CfgCommand;
use owo_colors::OwoColorize;
pub mod info_table;
pub struct FormattedCfgCommand<'a, Shell> {
command: &'a CfgCommand,
shell: Cell<Option<Shell>>,
}
impl<'a, Shell, S> FormattedCfgCommand<'a, Shell>
where
Shell: IntoIterator<Item = S>,
S: Display,
{
pub fn new(command: &'a CfgCommand, shell: Option<Shell>) -> Self {
Self {
command,
shell: Cell::new(shell),
}
}
}
impl<'a, Shell, S> Display for FormattedCfgCommand<'a, Shell>
where
Shell: IntoIterator<Item = S>,
S: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.command {
CfgCommand::Args(args) => {
FormattedEscapedCommand::new(args).cyan().fmt(f)?;
},
CfgCommand::Shell(cmd) => {
if let Some(shell) = self.shell.take() {
write!(f, "[{}] ", FormattedEscapedCommand::new(shell).red())?;
}
cmd.cyan().fmt(f)?;
},
}
Ok(())
}
}
struct FormattedEscapedCommand<Shell>(Cell<Option<Shell>>);
impl<Shell> FormattedEscapedCommand<Shell> {
fn new(s: Shell) -> Self {
Self(Cell::new(Some(s)))
}
}
impl<Shell, S> Display for FormattedEscapedCommand<Shell>
where
Shell: IntoIterator<Item = S>,
S: Display,
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut first = true;
for a in self
.0
.take()
.expect("Attempt to format FormattedEscapedCommand twice")
{
let a = a.to_string();
if !first {
f.write_char(' ')?;
}
first = false;
if a.contains(' ') {
write!(f, r#""{}""#, a)?;
} else {
f.write_str(&a)?;
}
}
Ok(())
}
}