diff --git a/Cargo.lock b/Cargo.lock index 6e66672..f7495b8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,12 +11,54 @@ dependencies = [ "memchr", ] +[[package]] +name = "anstream" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d664a92ecae85fd0a7392615844904654d1d5f5514837f471ddef4a057aba1b6" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + [[package]] name = "anstyle" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" version = "1.0.75" @@ -133,6 +175,12 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "colorchoice" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" + [[package]] name = "crossbeam-deque" version = "0.8.3" @@ -184,15 +232,6 @@ version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07" -[[package]] -name = "enable-ansi-support" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4ff3ae2a9aa54bf7ee0983e59303224de742818c1822d89f07da9856d9bc60" -dependencies = [ - "windows-sys 0.42.0", -] - [[package]] name = "encoding_rs" version = "0.8.33" @@ -326,12 +365,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "is_ci" -version = "1.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb" - [[package]] name = "itertools" version = "0.10.5" @@ -541,16 +574,6 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" -[[package]] -name = "supports-color" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6398cde53adc3c4557306a96ce67b302968513830a77a95b2b17305d9719a89" -dependencies = [ - "is-terminal", - "is_ci", -] - [[package]] name = "syn" version = "2.0.39" @@ -620,6 +643,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "wait-timeout" version = "0.2.0" @@ -670,21 +699,6 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" -[[package]] -name = "windows-sys" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" -dependencies = [ - "windows_aarch64_gnullvm 0.42.2", - "windows_aarch64_msvc 0.42.2", - "windows_i686_gnu 0.42.2", - "windows_i686_msvc 0.42.2", - "windows_x86_64_gnu 0.42.2", - "windows_x86_64_gnullvm 0.42.2", - "windows_x86_64_msvc 0.42.2", -] - [[package]] name = "windows-sys" version = "0.48.0" @@ -733,12 +747,6 @@ dependencies = [ "windows_x86_64_msvc 0.52.0", ] -[[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" - [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -751,12 +759,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" -[[package]] -name = "windows_aarch64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" - [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -769,12 +771,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" -[[package]] -name = "windows_i686_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" - [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -787,12 +783,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" -[[package]] -name = "windows_i686_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" - [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -805,12 +795,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" - [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -823,12 +807,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" - [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -841,12 +819,6 @@ version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" - [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -863,12 +835,13 @@ checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" name = "zet" version = "2.0.0" dependencies = [ + "anstream", + "anstyle", "anyhow", "assert_cmd", "assert_fs", "bstr", "clap", - "enable-ansi-support", "encoding_rs", "encoding_rs_io", "fxhash", @@ -877,7 +850,6 @@ dependencies = [ "itertools 0.10.5", "memchr", "once_cell", - "supports-color", "terminal_size", "textwrap", ] diff --git a/Cargo.toml b/Cargo.toml index ebbfd2a..ba131a7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,8 @@ edition = '2021' [dependencies] anyhow = "1.0.42" +anstyle = "1.0.4" +anstream = "0.6.5" bstr = { version = "1.1.0", default-features = false, features = ["std", "alloc"] } encoding_rs = "0.8.28" encoding_rs_io = "0.1.7" @@ -22,8 +24,6 @@ clap = { version = "4.1.4", default-features = false, features = ["std","error-c memchr = "2.4.0" indexmap = "1.7.0" is-terminal = "0.4.2" -enable-ansi-support = "0.2.1" -supports-color = "2.0.0" textwrap = "0.16.0" once_cell = "1.17.1" terminal_size = "0.2.5" diff --git a/src/args.rs b/src/args.rs index bd19570..3442751 100644 --- a/src/args.rs +++ b/src/args.rs @@ -2,7 +2,7 @@ use crate::help; use crate::operations::LogType; -use crate::styles::{set_color_choice, ColorChoice}; +use crate::styles::ColorChoice; use clap::{Parser, ValueEnum}; use std::path::PathBuf; @@ -12,34 +12,16 @@ use std::path::PathBuf; pub fn parsed() -> Args { let parsed = CliArgs::parse(); let cc = parsed.color.unwrap_or(ColorChoice::Auto); - set_color_choice(cc); if parsed.help { - help_and_exit(); + help_and_exit(&cc); } if parsed.version { println!("{}", help::version()); exit_success(); } - let Some(op) = parsed.command else { help_and_exit() }; - if op == CliName::Help { - help_and_exit() - } - let log_type = if parsed.count_files { - LogType::Files - } else if parsed.count_lines { - LogType::Lines - } else if parsed.count { - if parsed.files { - LogType::Files - } else { - LogType::Lines - } - } else { - LogType::None - }; - + let Some(op) = parsed.command else { help_and_exit(&cc) }; let op = match op { - CliName::Help => help_and_exit(), // This can't happen, but... + CliName::Help => help_and_exit(&cc), CliName::Intersect => OpName::Intersect, CliName::Union => OpName::Union, CliName::Diff => OpName::Diff, @@ -58,12 +40,33 @@ pub fn parsed() -> Args { } } }; + + let log_type = if parsed.count_files { + LogType::Files + } else if parsed.count_lines { + LogType::Lines + } else if parsed.count { + if parsed.files { + LogType::Files + } else { + LogType::Lines + } + } else { + LogType::None + }; + Args { op, log_type, paths: parsed.paths } } -fn help_and_exit() -> ! { - help::print(); - exit_success(); +fn help_and_exit(cc: &ColorChoice) -> ! { + let code = match help::print(cc) { + Err(e) => { + eprintln!("{e}"); + 1 + } + Ok(()) => SUCCESS_CODE, + }; + safe_exit(code); } const SUCCESS_CODE: i32 = 0; diff --git a/src/help.rs b/src/help.rs index d22858c..23c79aa 100644 --- a/src/help.rs +++ b/src/help.rs @@ -1,4 +1,6 @@ -use crate::styles::{global_style, StyleSheet, StyledStr}; +use crate::styles::{app_name, as_item, as_title, ColorChoice, StyledStr}; +use anstream; +use anyhow::{bail, Result}; use once_cell::sync::Lazy; use std::borrow::Cow; use terminal_size::{terminal_size, Height, Width}; @@ -18,38 +20,50 @@ struct Entry<'a> { caption: &'a str, } -fn name(style: &StyleSheet) -> StyledStr { - style.app_name("zet") +fn name() -> StyledStr<'static> { + app_name("zet") } pub(crate) fn version() -> String { let version = std::env!("CARGO_PKG_VERSION"); - let name = name(global_style()); + let name = name(); format!("{name} {version}") } -pub(crate) fn print() { +pub(crate) fn print(color_choice: &ColorChoice) -> Result<()> { + let color_choice = match color_choice { + ColorChoice::Always => anstream::ColorChoice::Always, + ColorChoice::Auto => anstream::ColorChoice::Auto, + ColorChoice::Never => anstream::ColorChoice::Never, + }; + let mut stdout = anstream::AutoStream::new(std::io::stdout().lock(), color_choice); + match fallable_print(&mut stdout) { + Err(e) => bail!("failed printing to stdout: {e}"), + Ok(_) => Ok(()), + } +} +fn fallable_print(stdout: &mut dyn std::io::Write) -> std::io::Result { let input = include_str!("help.txt"); - let style = global_style(); - let help = parse(style, input); - println!("{}", version()); + let help = parse(input); + writeln!(stdout, "{}", version())?; for help_item in help { match help_item { HelpItem::Paragraph(text) => { - wrap(text, &C.wrap_options).iter().for_each(|line| println!("{line}")) - } - HelpItem::Usage(args) => { - println!("{}{}{}", style.title("Usage: "), name(style), args) + for line in wrap(text, &C.wrap_options) { + writeln!(stdout, "{line}")?; + } } + HelpItem::Usage(args) => writeln!(stdout, "{}{}{}", as_title("Usage: "), name(), args)?, HelpItem::Section(s) => { - println!("{}", style.title(s.title)); - s.print_entries(); + writeln!(stdout, "{}", as_title(s.title))?; + s.print_entries(stdout)?; } }; } + Ok(0) } -fn parse<'a>(style: &StyleSheet, text: &'a str) -> Vec> { +fn parse(text: &str) -> Vec { const USAGE: &str = "Usage: "; let mut help = Vec::new(); let mut lines = text.lines().fuse(); @@ -68,7 +82,7 @@ fn parse<'a>(style: &StyleSheet, text: &'a str) -> Vec> { } let Some(sp_sp) = entry.rfind(" ") else { panic!("No double space in {entry}") }; let (item, caption) = entry.split_at(sp_sp + 2); - entries.push(Entry { item: style.item(item), caption }); + entries.push(Entry { item: as_item(item), caption }); }; help.push(HelpItem::Section(Section { title, entries })); if let Some(part) = result { @@ -82,11 +96,11 @@ fn parse<'a>(style: &StyleSheet, text: &'a str) -> Vec> { } impl<'a> Section<'a> { - fn print_entries(&self) { + fn print_entries(self, stdout: &mut dyn std::io::Write) -> std::io::Result { let fits_in_line = self.entries.iter().all(Entry::fits_in_line); if fits_in_line { for entry in &self.entries { - println!("{}{}", entry.item, entry.caption); + writeln!(stdout, "{}{}", entry.item, entry.caption)?; } } else { let same_line_help = self.same_line_help_lines(); @@ -97,7 +111,7 @@ impl<'a> Section<'a> { &next_line_help }; for line in help.iter().flatten() { - println!("{line}"); + writeln!(stdout, "{line}")?; } } fn badness(vv: &[Vec]) -> usize { @@ -106,6 +120,7 @@ impl<'a> Section<'a> { total + v.len() + m * 2 }) } + Ok(0) } fn next_line_help_indent(&self) -> &'a str { let max_indent = diff --git a/src/styles.rs b/src/styles.rs index 3ee9009..5329a1d 100644 --- a/src/styles.rs +++ b/src/styles.rs @@ -1,47 +1,32 @@ +use anstyle::{AnsiColor, Color, Style}; use clap::ValueEnum; -use enable_ansi_support::enable_ansi_support; -use once_cell::sync::OnceCell; use std::fmt; #[derive(Debug, Clone, ValueEnum)] -pub enum ColorChoice { +pub(crate) enum ColorChoice { Auto, Always, Never, } -#[derive(Debug, Clone, Copy)] -pub struct StyleSheet { - app_prefix: Option<&'static str>, - item_prefix: Option<&'static str>, - title_prefix: Option<&'static str>, - error_prefix: Option<&'static str>, - literal_prefix: Option<&'static str>, +const GREEN: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Green))); +const BOLD_GREEN: Style = GREEN.bold(); +const YELLOW: Style = Style::new().fg_color(Some(Color::Ansi(AnsiColor::Yellow))); + +#[must_use] +pub(crate) fn app_name(content: &str) -> StyledStr { + StyledStr { prefix: BOLD_GREEN, content } } -impl StyleSheet { - #[must_use] - pub fn app_name<'a>(&self, content: &'a str) -> StyledStr<'a> { - StyledStr { prefix: self.app_prefix, content } - } - #[must_use] - pub fn item<'a>(&self, content: &'a str) -> StyledStr<'a> { - StyledStr { prefix: self.item_prefix, content } - } - #[must_use] - pub fn title<'a>(&self, content: &'a str) -> StyledStr<'a> { - StyledStr { prefix: self.title_prefix, content } - } - #[must_use] - pub fn error<'a>(&self, content: &'a str) -> StyledStr<'a> { - StyledStr { prefix: self.error_prefix, content } - } - #[must_use] - pub fn literal<'a>(&self, content: &'a str) -> StyledStr<'a> { - StyledStr { prefix: self.literal_prefix, content } - } +#[must_use] +pub(crate) fn as_item(content: &str) -> StyledStr { + StyledStr { prefix: GREEN, content } +} +#[must_use] +pub(crate) fn as_title(content: &str) -> StyledStr { + StyledStr { prefix: YELLOW, content } } -pub struct StyledStr<'a> { - prefix: Option<&'static str>, +pub(crate) struct StyledStr<'a> { + prefix: Style, content: &'a str, } impl<'a> StyledStr<'a> { @@ -50,10 +35,6 @@ impl<'a> StyledStr<'a> { self.content.len() } #[must_use] - pub fn is_empty(&self) -> bool { - self.content.is_empty() - } - #[must_use] pub fn indented_by(&self) -> usize { use bstr::ByteSlice; self.content.as_bytes().find_not_byteset(b" ").unwrap_or(self.len()) @@ -61,64 +42,7 @@ impl<'a> StyledStr<'a> { } impl<'a> fmt::Display for StyledStr<'a> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - if let Some(prefix) = self.prefix { - write!(f, "{}{}{}", prefix, self.content, RESET) - } else { - write!(f, "{}", self.content) - } - } -} - -const BOLD_RED: &str = "\x1B[31;1m"; -const GREEN: &str = "\x1B[32m"; -const BOLD_GREEN: &str = "\x1B[32;1m"; -const YELLOW: &str = "\x1B[33m"; -const RESET: &str = "\x1B[m"; - -const NEVER: StyleSheet = StyleSheet { - app_prefix: None, - item_prefix: None, - title_prefix: None, - error_prefix: None, - literal_prefix: None, -}; -const ALWAYS: StyleSheet = StyleSheet { - app_prefix: Some(BOLD_GREEN), - item_prefix: Some(GREEN), - title_prefix: Some(YELLOW), - error_prefix: Some(BOLD_RED), - literal_prefix: Some(YELLOW), -}; -fn auto() -> StyleSheet { - use supports_color::Stream; - let use_color = enable_ansi_support().is_ok() && supports_color::on(Stream::Stdout).is_some(); - if use_color { - ALWAYS - } else { - NEVER - } -} - -static COLOR_CHOICE: OnceCell = OnceCell::new(); -static STYLE_SHEET: OnceCell = OnceCell::new(); -pub(crate) fn set_color_choice(cc: ColorChoice) { - COLOR_CHOICE.set(cc).expect("set_color_choice may only be called once"); -} -pub(crate) fn global_style() -> &'static StyleSheet { - if let Some(style) = STYLE_SHEET.get() { - style - } else { - let cc = COLOR_CHOICE.get().expect("Please call set_color_choice before calling style"); - let style = match cc { - ColorChoice::Always => { - let _ = enable_ansi_support(); - ALWAYS - } - ColorChoice::Never => NEVER, - ColorChoice::Auto => auto(), - }; - let _ = STYLE_SHEET.set(style); // .set only errors if the value was already set - STYLE_SHEET.get().expect("This can't happen: STYLE_SHEET was unset after being set") + write!(f, "{}{}{}", self.prefix.render(), self.content, self.prefix.render_reset()) } } @@ -129,10 +53,8 @@ mod test { #[test] fn test_len() { let contents = "abc"; - for choice in [ALWAYS, NEVER] { - assert_eq!(choice.app_name(contents).len(), contents.len()); - assert_eq!(choice.item(contents).len(), contents.len()); - assert_eq!(choice.title(contents).len(), contents.len()); - } + assert_eq!(app_name(contents).len(), contents.len()); + assert_eq!(as_item(contents).len(), contents.len()); + assert_eq!(as_title(contents).len(), contents.len()); } }