From e6116033482dbcc63f983a12d60d2faaff3e9525 Mon Sep 17 00:00:00 2001 From: Luna Razzaghipour Date: Mon, 16 Oct 2023 20:43:13 +1100 Subject: [PATCH 1/5] Add custom argument parsing --- Cargo.lock | 153 ---------------------------------- crates/pipes-rs/Cargo.toml | 1 - crates/pipes-rs/src/config.rs | 66 +-------------- crates/pipes-rs/src/main.rs | 151 ++++++++++++++++++++++++++++++--- crates/pipes-rs/src/usage | 19 +++++ 5 files changed, 163 insertions(+), 227 deletions(-) create mode 100644 crates/pipes-rs/src/usage diff --git a/Cargo.lock b/Cargo.lock index 6353d2c..2d1d990 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,54 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "anstream" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" -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.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" -dependencies = [ - "utf8parse", -] - -[[package]] -name = "anstyle-query" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ca11d4be1bab0c8bc8734a9aa7bf4ee8316d462a08c6ac5052f888fef5b494b" -dependencies = [ - "windows-sys", -] - -[[package]] -name = "anstyle-wincon" -version = "3.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" -dependencies = [ - "anstyle", - "windows-sys", -] - [[package]] name = "anyhow" version = "1.0.75" @@ -89,53 +41,6 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" -[[package]] -name = "clap" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" -dependencies = [ - "clap_builder", - "clap_derive", -] - -[[package]] -name = "clap_builder" -version = "4.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" -dependencies = [ - "anstream", - "anstyle", - "clap_lex", - "strsim", - "terminal_size", -] - -[[package]] -name = "clap_derive" -version = "4.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0862016ff20d69b84ef8247369fabf5c008a7417002411897d40ee1f4532b873" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "clap_lex" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd7cc57abe963c6d3b9d8be5b06ba7c8957a930305ca90304f24ef040aa6f961" - -[[package]] -name = "colorchoice" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" - [[package]] name = "crossterm" version = "0.27.0" @@ -167,16 +72,6 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" -[[package]] -name = "errno" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3e13f66a2f95e32a39eaa81f6b95d42878ca0e1db0c7543723dfe12557e860" -dependencies = [ - "libc", - "windows-sys", -] - [[package]] name = "etcetera" version = "0.8.0" @@ -194,12 +89,6 @@ version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" -[[package]] -name = "heck" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" - [[package]] name = "home" version = "0.5.5" @@ -235,12 +124,6 @@ dependencies = [ "libc", ] -[[package]] -name = "linux-raw-sys" -version = "0.4.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da2479e8c062e40bf0066ffa0bc823de0a9368974af99c9f6df941d2c231e03f" - [[package]] name = "lock_api" version = "0.4.10" @@ -335,7 +218,6 @@ name = "pipes-rs" version = "1.6.2" dependencies = [ "anyhow", - "clap", "etcetera", "mimalloc", "model", @@ -381,19 +263,6 @@ dependencies = [ "parking_lot", ] -[[package]] -name = "rustix" -version = "0.38.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "745ecfa778e66b2b63c88a61cb36e0eea109e803b0b86bf9879fbc77c70e86ed" -dependencies = [ - "bitflags 2.4.1", - "errno", - "libc", - "linux-raw-sys", - "windows-sys", -] - [[package]] name = "scopeguard" version = "1.2.0" @@ -465,12 +334,6 @@ version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "942b4a808e05215192e39f4ab80813e599068285906cc91aa64f923db842bd5a" -[[package]] -name = "strsim" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" - [[package]] name = "syn" version = "2.0.38" @@ -491,16 +354,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "terminal_size" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" -dependencies = [ - "rustix", - "windows-sys", -] - [[package]] name = "tincture" version = "1.0.0" @@ -553,12 +406,6 @@ 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 = "wasi" version = "0.11.0+wasi-snapshot-preview1" diff --git a/crates/pipes-rs/Cargo.toml b/crates/pipes-rs/Cargo.toml index d8cd201..b227089 100644 --- a/crates/pipes-rs/Cargo.toml +++ b/crates/pipes-rs/Cargo.toml @@ -7,7 +7,6 @@ version = "1.6.2" [dependencies] anyhow = "1.0.70" -clap = { version = "4.2.1", features = ["derive", "help", "wrap_help"] } etcetera = "0.8.0" mimalloc = { version = "0.1.36", default-features = false } model = { path = "../model" } diff --git a/crates/pipes-rs/src/config.rs b/crates/pipes-rs/src/config.rs index 23c1b4d..e07e0ac 100644 --- a/crates/pipes-rs/src/config.rs +++ b/crates/pipes-rs/src/config.rs @@ -1,5 +1,4 @@ use anyhow::Context; -use clap::Parser; use etcetera::app_strategy::{AppStrategy, AppStrategyArgs, Xdg}; use model::pipe::{ColorMode, Kind, KindSet, Palette}; use serde::{Deserialize, Serialize}; @@ -7,66 +6,24 @@ use std::fs; use std::path::PathBuf; use std::time::Duration; -#[derive(Serialize, Deserialize, Default, Parser)] -#[command( - name = "pipes-rs", - version, - about = "An over-engineered rewrite of pipes.sh in Rust." -)] +#[derive(Serialize, Deserialize, Default)] pub struct Config { - /// what kind of terminal coloring to use - #[arg(short, long)] pub color_mode: Option, - - /// the color palette used assign colors to pipes - #[arg(long)] pub palette: Option, - - /// cycle hue of pipes - #[arg(long, value_name = "DEGREES")] pub rainbow: Option, - - /// delay between frames in milliseconds - #[arg(short, long = "delay")] pub delay_ms: Option, - - /// number of frames of animation that are displayed in a second; use 0 for unlimited - #[arg(short, long)] pub fps: Option, - - /// portion of screen covered before resetting (0.0–1.0) - #[arg(short, long)] pub reset_threshold: Option, - - /// kinds of pipes separated by commas, e.g. heavy,curved - #[arg(short, long)] pub kinds: Option, - - /// whether to use bold - #[arg(short, long, value_name = "BOOL")] pub bold: Option, - - /// whether pipes should retain style after hitting the edge - #[arg(short, long, value_name = "BOOL")] pub inherit_style: Option, - - /// number of pipes - #[arg(name = "pipe-num", short, long, value_name = "NUM")] pub num_pipes: Option, - - /// chance of a pipe turning (0.0–1.0) - #[arg(short, long)] pub turn_chance: Option, - - /// Print license - #[arg(long)] - #[serde(default)] - pub license: bool, } impl Config { pub fn read() -> anyhow::Result { - let config = Self::read_from_disk_with_default()?.combine(Self::parse()); + let config = Self::read_from_disk_with_default()?; config.validate()?; Ok(config) @@ -98,7 +55,7 @@ impl Config { toml::from_str(&contents).context("failed to read config") } - fn validate(&self) -> anyhow::Result<()> { + pub fn validate(&self) -> anyhow::Result<()> { if let Some(reset_threshold) = self.reset_threshold() { if !(0.0..=1.0).contains(&reset_threshold) { anyhow::bail!("reset threshold should be within 0 and 1") @@ -172,21 +129,4 @@ impl Config { pub fn turn_chance(&self) -> f32 { self.turn_chance.unwrap_or(0.15) } - - fn combine(self, other: Self) -> Self { - Self { - color_mode: other.color_mode.or(self.color_mode), - palette: other.palette.or(self.palette), - rainbow: other.rainbow.or(self.rainbow), - delay_ms: other.delay_ms.or(self.delay_ms), - fps: other.fps.or(self.fps), - reset_threshold: other.reset_threshold.or(self.reset_threshold), - kinds: other.kinds.or(self.kinds), - bold: other.bold.or(self.bold), - inherit_style: other.inherit_style.or(self.inherit_style), - num_pipes: other.num_pipes.or(self.num_pipes), - turn_chance: other.turn_chance.or(self.turn_chance), - license: self.license || other.license, - } - } } diff --git a/crates/pipes-rs/src/main.rs b/crates/pipes-rs/src/main.rs index 565893b..593528d 100644 --- a/crates/pipes-rs/src/main.rs +++ b/crates/pipes-rs/src/main.rs @@ -1,23 +1,154 @@ use mimalloc::MiMalloc; +use model::pipe::{ColorMode, Palette}; use pipes_rs::{App, Config}; -use std::io::{self, Write}; +use std::{env, process}; #[global_allocator] static GLOBAL: MiMalloc = MiMalloc; fn main() -> anyhow::Result<()> { - let config = Config::read()?; - - if config.license { - let mut stdout = io::stdout(); - stdout.write_all(b"pipes-rs is licensed under the Blue Oak Model License 1.0.0,\nthe text of which you will find below.\n\n")?; - stdout.write_all(include_bytes!("../../../LICENSE.md"))?; - stdout.flush()?; - return Ok(()); - } + let mut config = Config::read()?; + parse_args(&mut config); + config.validate()?; let app = App::new(config)?; app.run()?; Ok(()) } + +fn parse_args(config: &mut Config) { + let args: Vec<_> = env::args().skip(1).collect(); + let mut args_i = args.iter(); + + while let Some(arg) = args_i.next() { + match arg.as_str() { + "--license" => { + if args.len() != 1 { + eprintln!("error: provided arguments other than --license"); + process::exit(1); + } + + println!("pipes-rs is licensed under the Blue Oak Model License 1.0.0,"); + println!("the text of which you will find below."); + println!("\n{}", include_str!("../../../LICENSE.md")); + process::exit(0); + } + + "--help" => { + println!("{}", include_str!("usage")); + process::exit(0); + } + + _ => {} + } + + let (option, value) = arg.split_once('=').unwrap_or_else(|| match args_i.next() { + Some(value) => (arg, value), + None => required_value(arg), + }); + + match option { + "--color-mode" | "-c" => { + config.color_mode = match value { + "ansi" => Some(ColorMode::Ansi), + "rgb" => Some(ColorMode::Rgb), + "none" => Some(ColorMode::None), + _ => invalid_value(option, value, "“ansi”, “rgb” or “none”"), + } + } + + "--palette" => { + config.palette = match value { + "default" => Some(Palette::Default), + "darker" => Some(Palette::Darker), + "pastel" => Some(Palette::Pastel), + "matrix" => Some(Palette::Matrix), + _ => invalid_value(option, value, "“default”, “darker”, “pastel” or “matrix”"), + } + } + + "--rainbow" => { + config.rainbow = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "an integer between 0 and 255"), + } + } + + "--delay" | "-d" => { + config.delay_ms = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "a positive integer"), + } + } + + "--fps" | "-f" => { + config.fps = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "a number"), + } + } + + "--reset-threshold" | "-r" => { + config.reset_threshold = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "a number"), + } + } + + "--kinds" | "-k" => { + config.kinds = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "kinds of pipes separated by commas"), + } + } + + "--bold" | "-b" => { + config.bold = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "“true” or “false”"), + } + } + + "--inherit-style" | "-i" => { + config.inherit_style = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "“true” or “false”"), + } + } + + "--pipe-num" | "-p" => { + config.num_pipes = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "a positive integer"), + } + } + + "--turn-chance" | "-t" => { + config.turn_chance = match value.parse() { + Ok(v) => Some(v), + Err(_) => invalid_value(option, value, "a number"), + } + } + + _ => { + eprintln!("error: unrecognized option {option}"); + eprintln!("see --help"); + process::exit(1); + } + } + } +} + +fn required_value(option: &str) -> ! { + eprintln!("error: a value is required for {option} but none was supplied"); + eprintln!("see --help"); + process::exit(1); +} + +fn invalid_value(option: &str, actual: &str, expected: &str) -> ! { + eprintln!("error: invalid value “{actual}” for {option}"); + eprint!(" expected {expected}"); + eprintln!("\nsee --help"); + process::exit(1); +} diff --git a/crates/pipes-rs/src/usage b/crates/pipes-rs/src/usage new file mode 100644 index 0000000..e7ffb31 --- /dev/null +++ b/crates/pipes-rs/src/usage @@ -0,0 +1,19 @@ +An over-engineered rewrite of pipes.sh in Rust. + +Usage: pipes-rs [OPTIONS] + +Options: + -c, --color-mode what kind of terminal coloring to use + --palette the color palette used assign colors to pipes + --rainbow cycle hue of pipes + -d, --delay delay between frames in milliseconds + -f, --fps number of frames of animation that are displayed in a second; use 0 for unlimited + -r, --reset-threshold portion of screen covered before resetting (0.0–1.0) + -k, --kinds kinds of pipes separated by commas, e.g. heavy,curved + -b, --bold whether to use bold [possible values: true, false] + -i, --inherit-style whether pipes should retain style after hitting the edge [possible values: true, false] + -p, --pipe-num number of pipes + -t, --turn-chance chance of a pipe turning (0.0–1.0) + --license Print license + -h, --help Print help + -V, --version Print version From 615ab3a026d595dc7efebb8107aac08f1324b558 Mon Sep 17 00:00:00 2001 From: Luna Razzaghipour Date: Mon, 16 Oct 2023 20:44:57 +1100 Subject: [PATCH 2/5] Remove unused `FromStr` impls --- crates/model/src/pipe/color.rs | 30 ---------------------------- crates/model/src/pipe/kind.rs | 36 +++++++++++++--------------------- 2 files changed, 14 insertions(+), 52 deletions(-) diff --git a/crates/model/src/pipe/color.rs b/crates/model/src/pipe/color.rs index be729c2..89867f2 100644 --- a/crates/model/src/pipe/color.rs +++ b/crates/model/src/pipe/color.rs @@ -1,5 +1,4 @@ use std::ops::Range; -use std::str::FromStr; #[derive(Clone, Copy)] pub struct Color { @@ -90,19 +89,6 @@ pub enum ColorMode { None, } -impl FromStr for ColorMode { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(match s { - "ansi" => Self::Ansi, - "rgb" => Self::Rgb, - "none" => Self::None, - _ => anyhow::bail!(r#"unknown color mode (expected “ansi”, “rgb”, or “none”)"#), - }) - } -} - #[derive(Clone, Copy, serde::Serialize, serde::Deserialize)] #[serde(rename_all = "snake_case")] pub enum Palette { @@ -112,22 +98,6 @@ pub enum Palette { Matrix, } -impl FromStr for Palette { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(match s { - "default" => Self::Default, - "darker" => Self::Darker, - "pastel" => Self::Pastel, - "matrix" => Self::Matrix, - _ => anyhow::bail!( - r#"unknown palette (expected “default”, “darker”, “pastel”, or “matrix”)"# - ), - }) - } -} - impl Palette { pub(super) fn get_hue_range(self) -> Range { match self { diff --git a/crates/model/src/pipe/kind.rs b/crates/model/src/pipe/kind.rs index 3db438c..b8a5be0 100644 --- a/crates/model/src/pipe/kind.rs +++ b/crates/model/src/pipe/kind.rs @@ -86,27 +86,6 @@ enum KindWidth { Custom(NonZeroUsize), } -impl FromStr for Kind { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - Ok(match s { - "heavy" => Self::Heavy, - "light" => Self::Light, - "curved" => Self::Curved, - "knobby" => Self::Knobby, - "emoji" => Self::Emoji, - "outline" => Self::Outline, - "dots" => Self::Dots, - "blocks" => Self::Blocks, - "sus" => Self::Sus, - _ => anyhow::bail!( - r#"unknown pipe kind (expected “heavy”, “light”, “curved”, “knobby”, “emoji”, “outline”, “dots”, “blocks”, or “sus”)"#, - ), - }) - } -} - #[derive(serde::Serialize, serde::Deserialize, Clone)] pub struct KindSet(Vec); @@ -117,7 +96,20 @@ impl FromStr for KindSet { let mut set = Vec::new(); for kind in s.split(',') { - let kind = Kind::from_str(kind)?; + let kind = match kind { + "heavy" => Kind::Heavy, + "light" => Kind::Light, + "curved" => Kind::Curved, + "knobby" => Kind::Knobby, + "emoji" => Kind::Emoji, + "outline" => Kind::Outline, + "dots" => Kind::Dots, + "blocks" => Kind::Blocks, + "sus" => Kind::Sus, + _ => anyhow::bail!( + r#"unknown pipe kind (expected “heavy”, “light”, “curved”, “knobby”, “emoji”, “outline”, “dots”, “blocks”, or “sus”)"#, + ), + }; if !set.contains(&kind) { set.push(kind); From fd76311d66ce16b3d50f8b177ed401c60e36bcdc Mon Sep 17 00:00:00 2001 From: Luna Razzaghipour Date: Mon, 16 Oct 2023 21:15:58 +1100 Subject: [PATCH 3/5] Handle `--version`/`-V` --- crates/pipes-rs/src/main.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/pipes-rs/src/main.rs b/crates/pipes-rs/src/main.rs index 593528d..dc54eef 100644 --- a/crates/pipes-rs/src/main.rs +++ b/crates/pipes-rs/src/main.rs @@ -35,6 +35,16 @@ fn parse_args(config: &mut Config) { process::exit(0); } + "--version" | "-V" => { + if args.len() != 1 { + eprintln!("error: provided arguments other than --version"); + process::exit(1); + } + + println!("pipes-rs {}", env!("CARGO_PKG_VERSION")); + process::exit(0); + } + "--help" => { println!("{}", include_str!("usage")); process::exit(0); From 1867e32c43dd3986fa49279573931b6592756cbe Mon Sep 17 00:00:00 2001 From: Luna Razzaghipour Date: Mon, 16 Oct 2023 21:18:14 +1100 Subject: [PATCH 4/5] `eprint!` -> `eprintln!` --- crates/pipes-rs/src/main.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/pipes-rs/src/main.rs b/crates/pipes-rs/src/main.rs index dc54eef..4346ead 100644 --- a/crates/pipes-rs/src/main.rs +++ b/crates/pipes-rs/src/main.rs @@ -158,7 +158,7 @@ fn required_value(option: &str) -> ! { fn invalid_value(option: &str, actual: &str, expected: &str) -> ! { eprintln!("error: invalid value “{actual}” for {option}"); - eprint!(" expected {expected}"); - eprintln!("\nsee --help"); + eprintln!(" expected {expected}"); + eprintln!("see --help"); process::exit(1); } From 8f2be7c722d60f2eb44b86be5993ceee22e54eb4 Mon Sep 17 00:00:00 2001 From: Luna Razzaghipour Date: Mon, 16 Oct 2023 21:27:23 +1100 Subject: [PATCH 5/5] Improve error message for bare arguments --- crates/pipes-rs/src/main.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/pipes-rs/src/main.rs b/crates/pipes-rs/src/main.rs index 4346ead..257733c 100644 --- a/crates/pipes-rs/src/main.rs +++ b/crates/pipes-rs/src/main.rs @@ -53,6 +53,12 @@ fn parse_args(config: &mut Config) { _ => {} } + if !arg.starts_with('-') { + eprintln!("error: unexpected argument “{arg}” found"); + eprintln!("see --help"); + process::exit(1); + } + let (option, value) = arg.split_once('=').unwrap_or_else(|| match args_i.next() { Some(value) => (arg, value), None => required_value(arg),