diff --git a/Cargo.lock b/Cargo.lock index d89c2f0..950f4e8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -300,13 +300,25 @@ dependencies = [ "unicode-width", ] +[[package]] +name = "comfy-table" +version = "6.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1090f39f45786ec6dc6286f8ea9c75d0a7ef0a0d3cda674cef0c3af7b307fbc2" +dependencies = [ + "crossterm", + "strum", + "strum_macros", + "unicode-width", +] + [[package]] name = "console" version = "0.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c050367d967ced717c04b65d8c619d863ef9292ce0c5760028655a2fb298718c" dependencies = [ - "encode_unicode 0.3.6", + "encode_unicode", "lazy_static", "libc", "terminal_size", @@ -411,6 +423,31 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossterm" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +dependencies = [ + "bitflags", + "crossterm_winapi", + "libc", + "mio", + "parking_lot", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c" +dependencies = [ + "winapi", +] + [[package]] name = "crypto-common" version = "0.1.6" @@ -573,16 +610,6 @@ dependencies = [ "dirs-sys", ] -[[package]] -name = "dirs-next" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" -dependencies = [ - "cfg-if", - "dirs-sys-next", -] - [[package]] name = "dirs-sys" version = "0.3.7" @@ -594,17 +621,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "dirs-sys-next" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" -dependencies = [ - "libc", - "redox_users", - "winapi", -] - [[package]] name = "either" version = "1.8.0" @@ -617,12 +633,6 @@ version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" -[[package]] -name = "encode_unicode" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" - [[package]] name = "encoding_rs" version = "0.8.31" @@ -1150,6 +1160,16 @@ dependencies = [ "cc", ] +[[package]] +name = "lock_api" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "435011366fe56583b16cf956f9df0095b405b82d76425bc8981c0e22e60ec4df" +dependencies = [ + "autocfg", + "scopeguard", +] + [[package]] name = "log" version = "0.4.17" @@ -1316,6 +1336,29 @@ dependencies = [ "num_cpus", ] +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dc9e0dc2adc1c69d09143aff38d3d30c5c3f0df0dad82e6d25547af174ebec0" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-sys 0.42.0", +] + [[package]] name = "path-dedot" version = "3.0.18" @@ -1375,19 +1418,6 @@ version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" -[[package]] -name = "prettytable-rs" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f375cb74c23b51d23937ffdeb48b1fbf5b6409d4b9979c1418c1de58bc8f801" -dependencies = [ - "atty", - "encode_unicode 1.0.0", - "lazy_static", - "term", - "unicode-width", -] - [[package]] name = "proc-macro-error" version = "1.0.4" @@ -1676,6 +1706,7 @@ dependencies = [ "chrono", "clap", "clap_complete", + "comfy-table", "crossbeam-channel", "derivative", "derive-getters", @@ -1698,7 +1729,6 @@ dependencies = [ "nix", "pariter", "path-dedot", - "prettytable-rs", "quickcheck", "quickcheck_macros", "rand", @@ -1926,6 +1956,36 @@ dependencies = [ "digest", ] +[[package]] +name = "signal-hook" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a253b5e89e2698464fc26b545c9edceb338e18a89effeeecfea192c3025be29d" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29ad2e15f37ec9a6cc544097b78a1ec90001e9f71b81338ca39f430adaca99af" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + +[[package]] +name = "signal-hook-registry" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +dependencies = [ + "libc", +] + [[package]] name = "simplelog" version = "0.12.0" @@ -1946,6 +2006,12 @@ dependencies = [ "autocfg", ] +[[package]] +name = "smallvec" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" + [[package]] name = "socket2" version = "0.4.7" @@ -1968,6 +2034,25 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +[[package]] +name = "strum" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" + +[[package]] +name = "strum_macros" +version = "0.24.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e385be0d24f186b4ce2f9982191e7101bb737312ad61c1f2f984f34bcf85d59" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + [[package]] name = "subtle" version = "2.4.1" @@ -2010,17 +2095,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "term" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" -dependencies = [ - "dirs-next", - "rustversion", - "winapi", -] - [[package]] name = "termcolor" version = "1.1.3" diff --git a/Cargo.toml b/Cargo.toml index cb27a51..069755b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -77,7 +77,6 @@ toml = "0.5" merge = "0.1" serde_with = "2.1" rpassword = "7" -prettytable-rs = {version = "0.9", default-features = false } bytesize = "1" indicatif = "0.17" path-dedot = "3" @@ -86,6 +85,7 @@ humantime = "2" users = "0.11" itertools = "0.10" simplelog = "0.12" +comfy-table = "6.1.2" [dev-dependencies] rstest = "0.15" diff --git a/src/commands/forget.rs b/src/commands/forget.rs index 186ac71..07fce9e 100644 --- a/src/commands/forget.rs +++ b/src/commands/forget.rs @@ -5,11 +5,10 @@ use chrono::{DateTime, Datelike, Duration, Local, Timelike}; use clap::{AppSettings, Parser}; use derivative::Derivative; use merge::Merge; -use prettytable::{format, row, Table}; use serde::Deserialize; use serde_with::{serde_as, DisplayFromStr}; -use super::{progress_counter, prune, RusticConfig}; +use super::{progress_counter, prune, table_with_titles, RusticConfig}; use crate::backend::{Cache, DecryptFullBackend, FileType}; use crate::repo::{ ConfigFile, SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, StringList, @@ -89,7 +88,8 @@ pub(super) fn execute( snapshots.sort_unstable_by(|sn1, sn2| sn1.cmp(sn2).reverse()); let latest_time = snapshots[0].time; let mut group_keep = opts.config.keep.clone(); - let mut table = Table::new(); + let mut table = + table_with_titles(["ID", "Time", "Host", "Tags", "Paths", "Action", "Reason"]); let mut iter = snapshots.iter().peekable(); let mut last = None; @@ -123,18 +123,22 @@ pub(super) fn execute( let tags = sn.tags.formatln(); let paths = sn.paths.formatln(); - let time = sn.time.format("%Y-%m-%d %H:%M:%S"); - table.add_row(row![sn.id, time, sn.hostname, tags, paths, action, reason]); + let time = sn.time.format("%Y-%m-%d %H:%M:%S").to_string(); + table.add_row([ + sn.id.to_string(), + time, + sn.hostname.to_string(), + tags, + paths, + action.to_string(), + reason, + ]); last = Some(sn); } - table.set_titles( - row![b->"ID", b->"Time", b->"Host", b->"Tags", b->"Paths", b->"Action", br->"Reason"], - ); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); println!(); - table.printstd(); + println!("{table}"); println!(); } @@ -290,7 +294,7 @@ impl KeepOptions { latest_time: DateTime, ) -> Option { let mut keep = false; - let mut reason = String::new(); + let mut reason = Vec::new(); if self .keep_ids @@ -298,12 +302,12 @@ impl KeepOptions { .any(|id| sn.id.to_hex().starts_with(id)) { keep = true; - reason.push_str("id\n"); + reason.push("id"); } if !self.keep_tags.is_empty() && sn.tags.matches(&self.keep_tags) { keep = true; - reason.push_str("tags\n"); + reason.push("tags"); } let keep_checks = [ @@ -356,17 +360,15 @@ impl KeepOptions { if *counter > 0 { *counter -= 1; keep = true; - reason.push_str(reason1); - reason.push('\n'); + reason.push(reason1); } if sn.time + Duration::from_std(*within).unwrap() > latest_time { keep = true; - reason.push_str(reason2); - reason.push('\n'); + reason.push(reason2); } } } - keep.then_some(reason) + keep.then_some(reason.join("\n")) } } diff --git a/src/commands/helpers.rs b/src/commands/helpers.rs index a018d4c..d70b9a8 100644 --- a/src/commands/helpers.rs +++ b/src/commands/helpers.rs @@ -6,6 +6,9 @@ use std::time::Duration; use anyhow::{bail, Result}; use bytesize::ByteSize; +use comfy_table::{ + presets::ASCII_MARKDOWN, Attribute, Cell, CellAlignment, ContentArrangement, Table, +}; use indicatif::HumanDuration; use indicatif::{ProgressBar, ProgressState, ProgressStyle}; use log::*; @@ -149,3 +152,34 @@ pub fn wait(d: Option) { p.finish(); } } + +// Helpers for table output + +pub fn bold_cell(s: T) -> Cell { + Cell::new(s).add_attribute(Attribute::Bold) +} + +pub fn table() -> Table { + let mut table = Table::new(); + table + .load_preset(ASCII_MARKDOWN) + .set_content_arrangement(ContentArrangement::Dynamic); + table +} + +pub fn table_with_titles, T: ToString>(titles: I) -> Table { + let mut table = table(); + table.set_header(titles.into_iter().map(bold_cell)); + table +} + +pub fn table_right_from, T: ToString>(start: usize, titles: I) -> Table { + let mut table = table_with_titles(titles); + // set alignment of all rows except first start row + table + .column_iter_mut() + .skip(start) + .for_each(|c| c.set_cell_alignment(CellAlignment::Right)); + + table +} diff --git a/src/commands/repoinfo.rs b/src/commands/repoinfo.rs index c975cc1..c1f6b88 100644 --- a/src/commands/repoinfo.rs +++ b/src/commands/repoinfo.rs @@ -2,9 +2,8 @@ use anyhow::Result; use clap::Parser; use derive_more::Add; use log::*; -use prettytable::{format, row, Table}; -use super::{bytes, progress_counter}; +use super::{bytes, progress_counter, table_right_from}; use crate::backend::{DecryptReadBackend, ReadBackend, ALL_FILE_TYPES}; use crate::blob::{BlobType, BlobTypeMap, Sum}; use crate::index::IndexEntry; @@ -75,33 +74,56 @@ pub(super) fn execute( } p.finish_with_message("done"); - let mut table = Table::new(); + let mut table = table_right_from( + 1, + ["Blob type", "Count", "Total Size", "Total Size in Packs"], + ); for (blob_type, info) in &info { - table.add_row(row![format!("{blob_type:?}"),r->info.count,r->bytes(info.data_size), r->bytes(info.size) ]); + table.add_row([ + format!("{blob_type:?}"), + info.count.to_string(), + bytes(info.data_size), + bytes(info.size), + ]); } for (blob_type, info_delete) in &info_delete { if info_delete.count > 0 { - table.add_row(row![format!("{blob_type:?} to delete"),r->info_delete.count,r->bytes(info_delete.data_size),r->bytes(info_delete.size)]); + table.add_row([ + format!("{blob_type:?} to delete"), + info_delete.count.to_string(), + bytes(info_delete.data_size), + bytes(info_delete.size), + ]); } } let total = info.sum() + info_delete.sum(); - table.add_row(row!["Total",r->total.count,r->bytes(total.data_size),r->bytes(total.size)]); + table.add_row([ + "Total".to_string(), + total.count.to_string(), + bytes(total.data_size), + bytes(total.size), + ]); - table.set_titles(row![b->"Blob type", br->"Count", br->"Total Size",br->"Total Size in Packs"]); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); println!(); - table.printstd(); + println!("{table}"); + + let mut table = table_right_from( + 1, + ["Blob type", "Pack Count", "Minimum Size", "Maximum Size"], + ); - let mut table = Table::new(); for (blob_type, info) in info { - table.add_row(row![format!("{blob_type:?} packs"), r->info.pack_count, r->bytes(info.min_pack_size), r->bytes(info.max_pack_size)]); + table.add_row([ + format!("{blob_type:?} packs"), + info.pack_count.to_string(), + bytes(info.min_pack_size), + bytes(info.max_pack_size), + ]); } - table.set_titles(row![b->"Blob type", br->"Pack Count", br->"Minimum Size",br->"Maximum Size"]); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); println!(); - table.printstd(); + println!("{table}"); Ok(()) } @@ -109,24 +131,26 @@ pub(super) fn execute( fn fileinfo(text: &str, be: &impl ReadBackend) -> Result<()> { info!("scanning files..."); - let mut table = Table::new(); + let mut table = table_right_from(1, ["File type", "Count", "Total Size"]); let mut total_count = 0; let mut total_size = 0; for tpe in ALL_FILE_TYPES { let list = be.list_with_size(tpe)?; let count = list.len(); let size = list.iter().map(|f| f.1 as u64).sum(); - table.add_row(row![format!("{:?}", tpe), r->count, r->bytes(size)]); + table.add_row([format!("{:?}", tpe), count.to_string(), bytes(size)]); total_count += count; total_size += size; } println!("{}", text); - table.add_row(row!["Total",r->total_count,r->bytes(total_size)]); + table.add_row([ + "Total".to_string(), + total_count.to_string(), + bytes(total_size), + ]); - table.set_titles(row![b->"File type", br->"Count", br->"Total Size"]); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); println!(); - table.printstd(); + println!("{table}"); println!(); Ok(()) } diff --git a/src/commands/snapshots.rs b/src/commands/snapshots.rs index 09c2a06..c67f98f 100644 --- a/src/commands/snapshots.rs +++ b/src/commands/snapshots.rs @@ -2,11 +2,11 @@ use std::time::Duration; use anyhow::Result; use clap::Parser; +use comfy_table::Cell; use humantime::format_duration; use itertools::Itertools; -use prettytable::{format, row, Table}; -use super::{bytes, RusticConfig}; +use super::{bold_cell, bytes, table, table_right_from, RusticConfig}; use crate::backend::DecryptReadBackend; use crate::repo::{ DeleteOption, SnapshotFile, SnapshotFilter, SnapshotGroup, SnapshotGroupCriterion, @@ -104,21 +104,34 @@ pub(super) fn execute( 0 => format!("{}", sn.id), count => format!("{} (+{})", sn.id, count), }; - row![id, time, sn.hostname, tags, paths, r->files, r->dirs, r->size] + [ + id, + time.to_string(), + sn.hostname, + tags, + paths, + files, + dirs, + size, + ] }; - let mut table: Table = snapshots + let mut table = table_right_from( + 5, + [ + "ID", "Time", "Host", "Tags", "Paths", "Files", "Dirs", "Size", + ], + ); + + let snapshots: Vec<_> = snapshots .into_iter() .group_by(|sn| if opts.all { sn.id } else { sn.tree }) .into_iter() .map(|(_, mut g)| (g.next().unwrap(), g.count())) .map(snap_to_table) .collect(); - table.set_titles( - row![b->"ID", b->"Time", b->"Host", b->"Tags", b->"Paths", br->"Files",br->"Dirs", br->"Size"], - ); - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.printstd(); + table.add_rows(snapshots); + println!("{table}"); } println!("{} snapshot(s)", count); } @@ -127,31 +140,35 @@ pub(super) fn execute( } fn display_snap(sn: SnapshotFile) { - let mut table = Table::new(); + let mut table = table(); - table.add_row(row![b->"Snapshot", b->sn.id.to_hex()]); + let mut add_entry = |title: &str, value: String| { + table.add_row([bold_cell(title), Cell::new(value)]); + }; + + add_entry("Snapshot", sn.id.to_hex()); // note that if original was not set, it is set to sn.id by the load process if sn.original != Some(sn.id) { - table.add_row(row![b->"Original ID", sn.original.unwrap().to_hex()]); + add_entry("Original ID", sn.original.unwrap().to_hex()); } - table.add_row(row![b->"Time", sn.time.format("%Y-%m-%d %H:%M:%S")]); - table.add_row(row![b->"Host", sn.hostname]); - table.add_row(row![b->"Tags", sn.tags.formatln()]); + add_entry("Time", sn.time.format("%Y-%m-%d %H:%M:%S").to_string()); + add_entry("Host", sn.hostname); + add_entry("Tags", sn.tags.formatln()); let delete = match sn.delete { DeleteOption::NotSet => "not set".to_string(), DeleteOption::Never => "never".to_string(), DeleteOption::After(t) => format!("after {}", t.format("%Y-%m-%d %H:%M:%S")), }; - table.add_row(row![b->"Delete", delete]); - table.add_row(row![b->"Paths", sn.paths.formatln()]); + add_entry("Delete", delete); + add_entry("Paths", sn.paths.formatln()); let parent = match sn.parent { None => "no parent snapshot".to_string(), Some(p) => p.to_hex(), }; - table.add_row(row![b->"Parent", parent]); + add_entry("Parent", parent); if let Some(summary) = sn.summary { - table.add_row(row![]); - table.add_row(row![b->"Command", summary.command]); + add_entry("", "".to_string()); + add_entry("Command", summary.command); let source = format!( "files: {} / dirs: {} / size: {}", @@ -159,23 +176,21 @@ fn display_snap(sn: SnapshotFile) { summary.total_dirs_processed, bytes(summary.total_bytes_processed) ); - table.add_row(row![b->"Source", source]); - - table.add_row(row![]); + add_entry("Source", source); + add_entry("", "".to_string()); let files = format!( "new: {:>10} / changed: {:>10} / unchanged: {:>10}", summary.files_new, summary.files_changed, summary.files_unmodified, ); - table.add_row(row![b->"Files", files]); + add_entry("Files", files); let trees = format!( "new: {:>10} / changed: {:>10} / unchanged: {:>10}", summary.dirs_new, summary.dirs_changed, summary.dirs_unmodified, ); - table.add_row(row![b->"Dirs", trees]); - - table.add_row(row![]); + add_entry("Dirs", trees); + add_entry("", "".to_string()); let written = format!( "data: {:>10} blobs / raw: {:>10} / packed: {:>10}\n\ @@ -191,7 +206,7 @@ fn display_snap(sn: SnapshotFile) { bytes(summary.data_added), bytes(summary.data_added_packed), ); - table.add_row(row![b->"Added to repo", written]); + add_entry("Added to repo", written); let duration = format!( "backup start: {} / backup end: {} / backup duration: {}\n\ @@ -201,9 +216,8 @@ fn display_snap(sn: SnapshotFile) { format_duration(Duration::from_secs_f64(summary.backup_duration)), format_duration(Duration::from_secs_f64(summary.total_duration)) ); - table.add_row(row![b->"Duration", duration]); + add_entry("Duration", duration); } - table.set_format(*format::consts::FORMAT_NO_BORDER_LINE_SEPARATOR); - table.printstd(); + println!("{table}"); println!(); } diff --git a/src/repo/snapshotfile.rs b/src/repo/snapshotfile.rs index f7ae12c..f4c4695 100644 --- a/src/repo/snapshotfile.rs +++ b/src/repo/snapshotfile.rs @@ -451,9 +451,6 @@ impl StringList { } pub fn formatln(&self) -> String { - self.0 - .iter() - .map(|p| p.to_string() + "\n") - .collect::() + self.0.join("\n") } }