improve CLI and refactors
This commit is contained in:
parent
b14a7a50fd
commit
2acbb468ad
|
@ -2,7 +2,7 @@ use std::{fs, rc::Rc, thread};
|
||||||
|
|
||||||
use crossbeam_channel::{bounded, Sender};
|
use crossbeam_channel::{bounded, Sender};
|
||||||
use libupgr::config::{Config, Step};
|
use libupgr::config::{Config, Step};
|
||||||
use miette::{miette, WrapErr, IntoDiagnostic};
|
use miette::{miette, IntoDiagnostic, WrapErr};
|
||||||
use mlua::Lua;
|
use mlua::Lua;
|
||||||
|
|
||||||
pub fn start() -> miette::Result<LuaActorHandle> {
|
pub fn start() -> miette::Result<LuaActorHandle> {
|
||||||
|
|
|
@ -48,7 +48,7 @@ pub enum IntoCommandError {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CfgCommand {
|
impl CfgCommand {
|
||||||
pub fn into_command(self, shell: &[String]) -> Result<Command, IntoCommandError> {
|
pub fn as_command(&self, shell: &[String]) -> Result<Command, IntoCommandError> {
|
||||||
match self {
|
match self {
|
||||||
Self::Args(args) => {
|
Self::Args(args) => {
|
||||||
let mut cmd = Command::new(args.first().ok_or(IntoCommandError::EmptyArgs)?);
|
let mut cmd = Command::new(args.first().ok_or(IntoCommandError::EmptyArgs)?);
|
||||||
|
|
|
@ -8,10 +8,12 @@ edition = "2018"
|
||||||
[dependencies]
|
[dependencies]
|
||||||
clap = { version = "3.1.6", features = ["derive"] }
|
clap = { version = "3.1.6", features = ["derive"] }
|
||||||
dirs = "4.0.0"
|
dirs = "4.0.0"
|
||||||
|
humantime = "2.1.0"
|
||||||
libupgr = { path = "../libupgr" }
|
libupgr = { path = "../libupgr" }
|
||||||
miette = { version = "4.2.1", features = ["fancy"] }
|
miette = { version = "4.2.1", features = ["fancy"] }
|
||||||
mlua = { version = "0.7.4", features = ["luajit", "serialize"] }
|
mlua = { version = "0.7.4", features = ["luajit", "serialize"] }
|
||||||
owo-colors = "3.3.0"
|
owo-colors = "3.3.0"
|
||||||
sled = "0.34.7"
|
sled = "0.34.7"
|
||||||
|
terminal_size = "0.1.17"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|
|
@ -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 ┃
|
|
||||||
┗━━━━━━━━━━━━━━━━━━━━━━━━━━┛",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::info_table::{CustomTableData, InfoTable, ToInfoData};
|
use crate::output::{info_table::InfoTable, FormattedCfgCommand};
|
||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use libupgr::{
|
use libupgr::{
|
||||||
config::{CfgCommand, Config},
|
config::{CfgCommand, Config},
|
||||||
|
@ -7,9 +7,16 @@ use libupgr::{
|
||||||
use miette::{miette, Context, IntoDiagnostic};
|
use miette::{miette, Context, IntoDiagnostic};
|
||||||
use mlua::{prelude::LuaFunction, Lua};
|
use mlua::{prelude::LuaFunction, Lua};
|
||||||
use owo_colors::OwoColorize;
|
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)]
|
#[derive(Parser)]
|
||||||
struct Opt {
|
struct Opt {
|
||||||
|
@ -86,7 +93,7 @@ pub fn exec_steps(conf: Config, uninteractive: bool) -> miette::Result<()> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exec_cmd(
|
pub fn exec_cmd(
|
||||||
cmd: CfgCommand,
|
cfg_cmd: CfgCommand,
|
||||||
shell: &[String],
|
shell: &[String],
|
||||||
when: Option<LuaFunction>,
|
when: Option<LuaFunction>,
|
||||||
workdir: Option<PathBuf>,
|
workdir: Option<PathBuf>,
|
||||||
|
@ -98,63 +105,59 @@ pub fn exec_cmd(
|
||||||
.wrap_err("Failed to call when function")?;
|
.wrap_err("Failed to call when function")?;
|
||||||
|
|
||||||
if !val {
|
if !val {
|
||||||
println!("Skipping {}", cmd.to_info_data());
|
println!(
|
||||||
|
"Skipping {}",
|
||||||
|
FormattedCfgCommand::new(&cfg_cmd, Some(shell))
|
||||||
|
);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut table = InfoTable::new_with_title("Running");
|
let mut table = InfoTable::with_title("Running");
|
||||||
table.row("Command", &cmd);
|
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 {
|
if let Some(workdir) = workdir {
|
||||||
table.row("Workdir", &workdir.to_string_lossy().as_ref());
|
table.row("Workdir", &workdir.to_string_lossy().as_ref());
|
||||||
cmd.current_dir(workdir);
|
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 start_time = Instant::now();
|
||||||
let exit = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
|
let exit = cmd.spawn().into_diagnostic()?.wait().into_diagnostic()?;
|
||||||
let total_time = Instant::now() - start_time;
|
let total_time = Instant::now() - start_time;
|
||||||
|
|
||||||
let mut data = String::new();
|
InfoTable::with_title("Summary")
|
||||||
println!(
|
.row("Command", FormattedCfgCommand::new(&cfg_cmd, Some(shell)))
|
||||||
"{}",
|
.row(
|
||||||
InfoTable::new_with_title("Summary")
|
"Time",
|
||||||
.row("Time", &total_time)
|
humantime::format_duration(
|
||||||
.row(
|
// only be precise to milliseconds
|
||||||
"Exit Code",
|
Duration::from_millis(total_time.as_millis() as u64),
|
||||||
&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))
|
|
||||||
)
|
)
|
||||||
.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(())
|
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,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
39
upgr/src/output/info_table.rs
Normal file
39
upgr/src/output/info_table.rs
Normal 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
86
upgr/src/output/mod.rs
Normal 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(())
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue