Skip to content

Commit 8b1b5c4

Browse files
author
Jonas Bertossa
committed
refactored formats
1 parent aeb27cc commit 8b1b5c4

31 files changed

Lines changed: 1080 additions & 230 deletions

Cargo.lock

Lines changed: 305 additions & 21 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ zip = { version = "0.6.6", default-features = false, features = [
4747
"aes-crypto",
4848
] }
4949
zstd = { version = "0.13.2", default-features = false, features = ["zstdmt"] }
50+
phf = { version = "0.13", features = ["macros"] }
51+
comfy-table = { version = "7.1", features = ["tty", "custom_styling"] }
52+
terminal_size = "0.4"
53+
unicode-width = "0.2"
54+
strip-ansi-escapes = "0.2"
5055

5156
[target.'cfg(not(unix))'.dependencies]
5257
is_executable = "1.0.1"

src/check.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ pub fn check_missing_formats_when_decompressing(files: &[PathBuf], formats: &[Ve
159159
}
160160

161161
error = error.detail("Decompression formats are detected automatically from file extension");
162-
error = error.hint_all_supported_formats();
162+
error = error.hint_supported_formats_for_decompress();
163163

164164
// If there's exactly one file, give a suggestion to use `--format`
165165
if let &[path] = files_with_broken_extension.as_slice() {
@@ -187,6 +187,7 @@ pub fn check_first_format_when_compressing<'a>(formats: &'a [Extension], output_
187187
.hint("")
188188
.hint("Alternatively, you can overwrite this option by using the '--format' flag:")
189189
.hint(format!(" ouch compress <FILES>... {output_path} --format tar.gz"))
190+
.hint_supported_formats_for_compress()
190191
.into()
191192
})
192193
}
@@ -246,7 +247,8 @@ pub fn check_invalid_compression_with_non_archive_format(
246247
.detail("Formats that bundle files into an archive are tar and zip.")
247248
.hint(format!("Try inserting 'tar.' or 'zip.' before '{first_format}'."))
248249
.hint(from_hint)
249-
.hint(to_hint);
250+
.hint(to_hint)
251+
.hint_supported_formats_for_compress();
250252

251253
Err(error.into())
252254
}

src/cli/args.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,9 @@ pub enum Subcommand {
116116
#[arg(short, long)]
117117
tree: bool,
118118
},
119+
/// List supported formats and their capabilities
120+
#[command(name = "list-formats", visible_aliases = ["formats", "support"])]
121+
ListFormats,
119122
}
120123

121124
#[cfg(test)]

src/cli/mod.rs

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,10 +28,19 @@ impl CliArgs {
2828

2929
set_accessible(args.accessible);
3030

31-
let (Subcommand::Compress { files, .. }
32-
| Subcommand::Decompress { files, .. }
33-
| Subcommand::List { archives: files, .. }) = &mut args.cmd;
34-
*files = canonicalize_files(files)?;
31+
match &mut args.cmd {
32+
// These subcommands receive file paths and must be canonicalized
33+
Subcommand::Compress { files, .. } | Subcommand::Decompress { files, .. } => {
34+
*files = canonicalize_files(files)?;
35+
}
36+
Subcommand::List { archives: files, .. } => {
37+
*files = canonicalize_files(files)?;
38+
}
39+
// New subcommand: no paths to canonicalize
40+
Subcommand::ListFormats => {
41+
// No-op
42+
}
43+
}
3544

3645
let skip_questions_positively = match (args.yes, args.no) {
3746
(false, false) => QuestionPolicy::Ask,

src/commands/compress.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,9 @@ pub fn compress_files(
106106
let win_size = 22; // default to 2^22 = 4 MiB window size
107107
Box::new(brotli::CompressorWriter::new(encoder, BUFFER_CAPACITY, level, win_size))
108108
}
109-
Tar | Zip | Rar | SevenZip => unreachable!(),
109+
Tar | Zip | SevenZip => unreachable!(),
110+
#[cfg(feature = "unrar")]
111+
Rar => unreachable!(),
110112
};
111113
Ok(encoder)
112114
};

src/commands/decompress.rs

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,9 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
140140
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
141141
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
142142
Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)),
143-
Tar | Zip | Rar | SevenZip => decoder,
143+
Tar | Zip | SevenZip => decoder,
144+
#[cfg(feature = "unrar")]
145+
Rar => decoder,
144146
};
145147
Ok(decoder)
146148
};
@@ -250,10 +252,7 @@ pub fn decompress_file(options: DecompressOptions) -> crate::Result<()> {
250252
return Ok(());
251253
}
252254
}
253-
#[cfg(not(feature = "unrar"))]
254-
Rar => {
255-
return Err(crate::archive::rar_stub::no_support());
256-
}
255+
257256
SevenZip => {
258257
if options.formats.len() > 1 {
259258
// Locking necessary to guarantee that warning and question

src/commands/list.rs

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -69,14 +69,16 @@ pub fn list_archive_contents(
6969
Snappy => Box::new(snap::read::FrameDecoder::new(decoder)),
7070
Zstd => Box::new(zstd::stream::Decoder::new(decoder)?),
7171
Brotli => Box::new(brotli::Decompressor::new(decoder, BUFFER_CAPACITY)),
72-
Tar | Zip | Rar | SevenZip => unreachable!("should be treated by caller"),
72+
Tar | Zip | SevenZip => unreachable!("should be treated by caller"),
73+
#[cfg(feature = "unrar")]
74+
Rar => unreachable!("should be treated by caller"),
7375
};
7476
Ok(decoder)
7577
};
7678

7779
let mut misplaced_archive_format = None;
7880
for &format in formats.iter().skip(1).rev() {
79-
if format.archive_format() {
81+
if format.is_archive() {
8082
misplaced_archive_format = Some(format);
8183
break;
8284
}
@@ -114,10 +116,7 @@ pub fn list_archive_contents(
114116
Box::new(crate::archive::rar::list_archive(archive_path, password)?)
115117
}
116118
}
117-
#[cfg(not(feature = "unrar"))]
118-
Rar => {
119-
return Err(crate::archive::rar_stub::no_support());
120-
}
119+
121120
SevenZip => {
122121
if formats.len() > 1 {
123122
// Locking necessary to guarantee that warning and question

src/commands/mod.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,5 +262,10 @@ pub fn run(
262262

263263
Ok(())
264264
}
265+
Subcommand::ListFormats => {
266+
// Print a concise explanation + the capability table + the shorthand table.
267+
println!("{}", crate::formats::render_capabilities_and_shorthands(crate::formats::SupportedOp::Any));
268+
Ok(())
269+
}
265270
}
266271
}

src/error.rs

Lines changed: 77 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,16 @@
22
//!
33
//! All usage errors will pass through the Error enum, a lot of them in the Error::Custom.
44
5+
use crate::formats::{self, SupportedOp};
6+
use crate::utils::colors::{BLUE, GREEN, RESET, YELLOW};
7+
use crate::{accessible::is_running_in_accessible_mode, utils::os_str_to_str};
58
use std::{
69
borrow::Cow,
710
ffi::OsString,
811
fmt::{self, Display},
912
io,
1013
};
1114

12-
use crate::{
13-
accessible::is_running_in_accessible_mode,
14-
extension::{PRETTY_SUPPORTED_ALIASES, PRETTY_SUPPORTED_EXTENSIONS},
15-
utils::os_str_to_str,
16-
};
17-
1815
/// All errors that can be generated by `ouch`
1916
#[derive(Debug, Clone)]
2017
pub enum Error {
@@ -43,7 +40,7 @@ pub enum Error {
4340
/// From sevenz_rust::Error
4441
SevenzipError { reason: String },
4542
/// Recognised but unsupported format
46-
// currently only RAR when built without the `unrar` feature
43+
/// (currently only RAR when built without the `unrar` feature)
4744
UnsupportedFormat { reason: String },
4845
/// Invalid password provided
4946
InvalidPassword { reason: String },
@@ -58,11 +55,11 @@ pub type CowStr = Cow<'static, str>;
5855
/// Pretty final error message for end users, crashing the program after display.
5956
#[derive(Clone, Debug, Default, PartialEq, Eq)]
6057
pub struct FinalError {
61-
/// Should be made of just one line, appears after the "\[ERROR\]" part
58+
/// Single line shown after `[ERROR]`
6259
title: CowStr,
63-
/// Shown as a unnumbered list in yellow
60+
/// Bullet list in yellow
6461
details: Vec<CowStr>,
65-
/// Shown as green at the end to give hints on how to work around this error, if it's fixable
62+
/// Hints in green at the end
6663
hints: Vec<CowStr>,
6764
}
6865

@@ -71,8 +68,6 @@ impl Display for FinalError {
7168
use crate::utils::colors::*;
7269

7370
// Title
74-
//
75-
// When in ACCESSIBLE mode, the square brackets are suppressed
7671
if is_running_in_accessible_mode() {
7772
write!(f, "{}ERROR{}: {}", *RED, *RESET, self.title)?;
7873
} else {
@@ -88,9 +83,8 @@ impl Display for FinalError {
8883
if !self.hints.is_empty() {
8984
// Separate by one blank line.
9085
writeln!(f)?;
91-
// to reduce redundant output for text-to-speech systems, braille
92-
// displays and so on, only print "hints" once in ACCESSIBLE mode
9386
if is_running_in_accessible_mode() {
87+
// In ACCESSIBLE mode, print the "hints" header once.
9488
write!(f, "\n{}hints:{}", *GREEN, *RESET)?;
9589
for hint in &self.hints {
9690
write!(f, "\n{hint}")?;
@@ -107,7 +101,7 @@ impl Display for FinalError {
107101
}
108102

109103
impl FinalError {
110-
/// Only constructor
104+
/// Constructor with title only.
111105
#[must_use]
112106
pub fn with_title(title: impl Into<CowStr>) -> Self {
113107
Self {
@@ -117,30 +111,88 @@ impl FinalError {
117111
}
118112
}
119113

120-
/// Add one detail line, can have multiple
114+
/// Adds one detail line (can be called multiple times).
121115
#[must_use]
122116
pub fn detail(mut self, detail: impl Into<CowStr>) -> Self {
123117
self.details.push(detail.into());
124118
self
125119
}
126120

127-
/// Add one hint line, can have multiple
121+
/// Adds one hint line (can be called multiple times).
128122
#[must_use]
129123
pub fn hint(mut self, hint: impl Into<CowStr>) -> Self {
130124
self.hints.push(hint.into());
131125
self
132126
}
133127

134-
/// Adds all supported formats as hints.
128+
// ---------------------------------------------------------------------
129+
// Compact, colored lists of supported formats (DRY helper + 3 public APIs)
130+
// ---------------------------------------------------------------------
131+
132+
/// Internal helper to add compact, colored supported-lists.
135133
///
136-
/// This is what it looks like:
137-
/// ```
138-
/// hint: Supported extensions are: tar, zip, bz, bz2, gz, lz4, xz, lzma, lz, sz, zst
139-
/// hint: Supported aliases are: tgz, tbz, tlz4, txz, tlzma, tsz, tzst, tlz
140-
/// ```
134+
/// - If `show_compress` is true, prints the compress-only list.
135+
/// - If `show_decompress` is true, prints the decompress-only list.
136+
/// - Always appends a colored hint for `ouch list-formats`.
137+
fn hint_supported_lists(mut self, show_compress: bool, show_decompress: bool) -> Self {
138+
// Color each token in a comma-separated list.
139+
let color_csv = |csv: String| -> String {
140+
csv.split(", ")
141+
.map(|t| format!("{}{}{}", *GREEN, t, *RESET))
142+
.collect::<Vec<_>>()
143+
.join(", ")
144+
};
145+
146+
if show_compress {
147+
let compress = formats::compact_extensions(SupportedOp::Compress);
148+
let compress_colored = color_csv(compress);
149+
self = self.hint(format!(
150+
"Supported extensions for {}compression{} are: {}",
151+
*YELLOW, *RESET, compress_colored
152+
));
153+
}
154+
155+
if show_decompress {
156+
let decompress = formats::compact_extensions(SupportedOp::Decompress);
157+
let decompress_colored = color_csv(decompress);
158+
self = self.hint(format!(
159+
"Supported extensions for {}decompression{} are: {}",
160+
*YELLOW, *RESET, decompress_colored
161+
));
162+
}
163+
164+
let aliases = formats::compact_shorthands();
165+
if !aliases.is_empty() {
166+
let aliases_colored = aliases
167+
.split(", ")
168+
.map(|t| format!("{}{}{}", *GREEN, t, *RESET))
169+
.collect::<Vec<_>>()
170+
.join(", ");
171+
self = self.hint(format!(
172+
"Supported {}aliases{} are: {}",
173+
*YELLOW, *RESET, aliases_colored
174+
));
175+
}
176+
177+
self.hint(format!(
178+
"For detailed capabilities and notes, run: {}ouch list-formats{}",
179+
*BLUE, *RESET
180+
))
181+
}
182+
183+
/// Adds a single, multi-line hint with the compact lists (compress + decompress).
141184
pub fn hint_all_supported_formats(self) -> Self {
142-
self.hint(format!("Supported extensions are: {PRETTY_SUPPORTED_EXTENSIONS}"))
143-
.hint(format!("Supported aliases are: {PRETTY_SUPPORTED_ALIASES}"))
185+
self.hint_supported_lists(true, true)
186+
}
187+
188+
/// Adds a compact list filtered to compression only.
189+
pub fn hint_supported_formats_for_compress(self) -> Self {
190+
self.hint_supported_lists(true, false)
191+
}
192+
193+
/// Adds a compact list filtered to decompression only.
194+
pub fn hint_supported_formats_for_decompress(self) -> Self {
195+
self.hint_supported_lists(false, true)
144196
}
145197
}
146198

0 commit comments

Comments
 (0)