├── data ├── no-gradient.svg ├── invalid.ggr ├── test1.svg ├── Neon_Green.ggr ├── invalid.svg └── gradients.svg ├── docs └── images │ ├── gradient-cli-1.png │ └── gradient-cli-2.png ├── .github ├── FUNDING.yml └── workflows │ ├── release.yml │ └── ci.yml ├── .gitignore ├── Makefile ├── Cargo.toml ├── LICENSE-MIT ├── src ├── util.rs ├── cli.rs ├── svg_gradient.rs └── main.rs ├── README.md ├── tests └── app.rs ├── LICENSE-APACHE └── Cargo.lock /data/no-gradient.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /docs/images/gradient-cli-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mazznoer/gradient-rs/HEAD/docs/images/gradient-cli-1.png -------------------------------------------------------------------------------- /docs/images/gradient-cli-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/mazznoer/gradient-rs/HEAD/docs/images/gradient-cli-2.png -------------------------------------------------------------------------------- /data/invalid.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: My Gradient 3 | 3 4 | 0.000000 0.672788 0.699499 0.000000 1.000000 0.000000 0.000000 0.129412 1.000000 0.000000 0.901961 1 0 5 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: mazznoer 4 | ko_fi: mazznoer 5 | liberapay: mazznoer 6 | custom: "https://paypal.me/mazznoer" 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | debug/ 4 | target/ 5 | 6 | # These are backup files generated by rustfmt 7 | **/*.rs.bk 8 | 9 | completions/ 10 | /*.svg 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | .PHONY: all check test 4 | 5 | all: check test 6 | 7 | check: 8 | cargo build --all-features && \ 9 | cargo clippy --all-features -- -D warnings && \ 10 | cargo fmt --all -- --check 11 | 12 | test: 13 | cargo test --all-features 14 | -------------------------------------------------------------------------------- /data/test1.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /data/Neon_Green.ggr: -------------------------------------------------------------------------------- 1 | GIMP Gradient 2 | Name: Neon Green 3 | 4 4 | 0.000000 0.672788 0.699499 0.000000 1.000000 0.000000 0.000000 0.129412 1.000000 0.000000 0.901961 1 0 5 | 0.699499 0.737062 0.774624 0.129412 1.000000 0.000000 0.901961 0.823529 1.000000 0.807843 1.000000 1 0 6 | 0.774624 0.812187 0.849750 0.823529 1.000000 0.807843 1.000000 0.196078 1.000000 0.000000 0.901961 1 0 7 | 0.849750 0.874791 1.000000 0.196078 1.000000 0.000000 0.901961 0.031373 1.000000 0.000000 0.000000 1 0 8 | -------------------------------------------------------------------------------- /data/invalid.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gradient" 3 | version = "0.4.1" 4 | authors = ["Nor Khasyatillah "] 5 | edition = "2018" 6 | description = "A command line tool for playing with color gradients" 7 | keywords = ["color", "gradient", "colormap", "color-scheme", "gimp"] 8 | categories = ["command-line-utilities", "graphics"] 9 | readme = "README.md" 10 | repository = "https://github.com/mazznoer/gradient-rs" 11 | license = "MIT OR Apache-2.0" 12 | exclude = [ 13 | ".github/*", 14 | "docs/*", 15 | "test_data/*", 16 | "tests/*", 17 | ] 18 | 19 | [profile.release] 20 | lto = true 21 | strip = true 22 | 23 | [dependencies] 24 | colorgrad = { version = "0.8", features = ["preset", "named-colors", "lab", "ggr"] } 25 | csscolorparser = { version = "0.8" } 26 | lexopt = { version = "0.3" } 27 | svg = { version = "0.18" } 28 | terminal_size = { version = "0.4" } 29 | 30 | [dev-dependencies] 31 | assert_cmd = { version = "2.0" } 32 | -------------------------------------------------------------------------------- /LICENSE-MIT: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Nor Khasyatillah 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | push: 8 | tags: 9 | - v[0-9]+.* 10 | 11 | jobs: 12 | upload-assets: 13 | strategy: 14 | matrix: 15 | os: 16 | - ubuntu-latest 17 | - macos-latest 18 | - windows-latest 19 | runs-on: ${{ matrix.os }} 20 | steps: 21 | - uses: actions/checkout@v3 22 | - uses: taiki-e/upload-rust-binary-action@v1 23 | with: 24 | # (required) 25 | bin: gradient 26 | # (optional) On which platform to distribute the `.tar.gz` file. 27 | # [default value: unix] 28 | # [possible values: all, unix, windows, none] 29 | tar: unix 30 | # (optional) On which platform to distribute the `.zip` file. 31 | # [default value: windows] 32 | # [possible values: all, unix, windows, none] 33 | zip: windows 34 | # create dir 35 | leading-dir: true 36 | # additional files 37 | #include: target/completions 38 | env: 39 | OUT_DIR: target 40 | # (required) 41 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 42 | -------------------------------------------------------------------------------- /data/gradients.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | paths-ignore: 6 | - "**.md" 7 | pull_request: 8 | paths-ignore: 9 | - "**.md" 10 | 11 | env: 12 | CARGO_TERM_COLOR: always 13 | 14 | jobs: 15 | build: 16 | strategy: 17 | matrix: 18 | os: [macos-latest, windows-latest, ubuntu-latest] 19 | runs-on: ${{ matrix.os }} 20 | 21 | steps: 22 | - uses: actions/checkout@v4 23 | 24 | - name: Build 25 | run: | 26 | cargo build --all-features 27 | env: 28 | OUT_DIR: target 29 | 30 | - name: Run tests 31 | run: | 32 | cargo test --all-features 33 | 34 | - name: Run cargo clippy 35 | run: | 36 | cargo clippy --all-features -- -D warnings 37 | 38 | - name: Run cargo fmt 39 | run: | 40 | cargo fmt --all -- --check 41 | 42 | #- name: Check shell completions 43 | # if: matrix.os != 'windows-latest' 44 | # run: | 45 | # ls -A target/completions 46 | 47 | - name: Run gradient 48 | if: matrix.os != 'windows-latest' 49 | run: | 50 | echo "Preset" 51 | ./target/debug/gradient -p magma -W 45 52 | echo "Take colors" 53 | ./target/debug/gradient -p sinebow -t 10 54 | echo "CSS gradient" 55 | ./target/debug/gradient --css "deeppink, gold, seagreen" -W 45 56 | 57 | - name: Run gradient (Windows) 58 | if: matrix.os == 'windows-latest' 59 | run: | 60 | echo "Preset" 61 | target/debug/gradient -p magma -W 45 62 | echo "Take colors" 63 | target/debug/gradient -p sinebow -t 10 64 | echo "CSS gradient" 65 | target/debug/gradient --css "deeppink, gold, seagreen" -W 45 66 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use crate::{Color, OutputColor}; 2 | 3 | pub fn blend_color(fg: &Color, bg: &Color) -> Color { 4 | Color::new( 5 | ((1.0 - fg.a) * bg.r) + (fg.a * fg.r), 6 | ((1.0 - fg.a) * bg.g) + (fg.a * fg.g), 7 | ((1.0 - fg.a) * bg.b) + (fg.a * fg.b), 8 | 1.0, 9 | ) 10 | } 11 | 12 | pub fn blend_on(fg: &mut Color, bg: &Color) { 13 | fg.r = ((1.0 - fg.a) * bg.r) + (fg.a * fg.r); 14 | fg.g = ((1.0 - fg.a) * bg.g) + (fg.a * fg.g); 15 | fg.b = ((1.0 - fg.a) * bg.b) + (fg.a * fg.b); 16 | fg.a = 1.0; 17 | } 18 | 19 | pub fn color_to_ansi(col: &Color, cb: &[Color; 2], width: usize) -> String { 20 | let mut ss = "".to_string(); 21 | for i in 0..width { 22 | let chr = if (i & 1) == 0 { "\u{2580}" } else { "\u{2584}" }; 23 | let [a, b, c, _] = blend_color(col, &cb[0]).to_rgba8(); 24 | let [d, e, f, _] = blend_color(col, &cb[1]).to_rgba8(); 25 | ss.push_str(&format!("\x1B[38;2;{a};{b};{c};48;2;{d};{e};{f}m{chr}")); 26 | } 27 | ss.push_str("\x1B[39;49m"); 28 | ss 29 | } 30 | 31 | pub fn bold(s: &str) -> String { 32 | format!("\x1B[1m{s}\x1B[0m") 33 | } 34 | 35 | fn fmt_float(t: f32, precision: usize) -> String { 36 | let s = format!("{t:.precision$}"); 37 | s.trim_end_matches('0').trim_end_matches('.').to_string() 38 | } 39 | 40 | fn fmt_alpha(alpha: f32) -> String { 41 | if alpha < 1.0 { 42 | format!(" / {}%", (alpha.max(0.0) * 100.0 + 0.5).floor()) 43 | } else { 44 | "".into() 45 | } 46 | } 47 | 48 | fn to_hsv_str(col: &Color) -> String { 49 | let [h, s, v, alpha] = col.to_hsva(); 50 | let h = if h.is_nan() { 51 | "none".into() 52 | } else { 53 | fmt_float(h, 2) 54 | }; 55 | let s = (s * 100.0 + 0.5).floor(); 56 | let v = (v * 100.0 + 0.5).floor(); 57 | format!("hsv({h} {s}% {v}%{})", fmt_alpha(alpha)) 58 | } 59 | 60 | pub fn format_color(col: &Color, format: OutputColor) -> String { 61 | match format { 62 | OutputColor::Hex => col.to_css_hex(), 63 | OutputColor::Rgb => col.to_css_rgb(), 64 | OutputColor::Hsl => col.to_css_hsl(), 65 | OutputColor::Hwb => col.to_css_hwb(), 66 | OutputColor::Hsv => to_hsv_str(col), 67 | OutputColor::Lab => col.to_css_lab(), 68 | OutputColor::Lch => col.to_css_lch(), 69 | OutputColor::Oklab => col.to_css_oklab(), 70 | OutputColor::Oklch => col.to_css_oklch(), 71 | } 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use super::*; 77 | 78 | #[test] 79 | fn test() { 80 | assert_eq!(fmt_alpha(0.0), " / 0%"); 81 | assert_eq!(fmt_alpha(0.5), " / 50%"); 82 | assert_eq!(fmt_alpha(1.0), ""); 83 | assert_eq!(fmt_alpha(1.2), ""); 84 | 85 | let red = Color::new(1.0, 0.0, 0.0, 1.0); 86 | assert_eq!(format_color(&red, OutputColor::Hex), "#ff0000"); 87 | assert_eq!(format_color(&red, OutputColor::Rgb), "rgb(255 0 0)"); 88 | assert_eq!(format_color(&red, OutputColor::Hsl), "hsl(0 100% 50%)"); 89 | assert_eq!(format_color(&red, OutputColor::Hsv), "hsv(0 100% 100%)"); 90 | assert_eq!(format_color(&red, OutputColor::Hwb), "hwb(0 0% 0%)"); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `gradient` 2 | 3 | [![Build Status](https://github.com/mazznoer/gradient-rs/actions/workflows/ci.yml/badge.svg)](https://github.com/mazznoer/gradient-rs/actions) 4 | [![crates.io](https://img.shields.io/crates/v/gradient.svg)](https://crates.io/crates/gradient) 5 | 6 | A command-line tool for playing with color gradients. 7 | 8 | ![gradient-cli-tool](docs/images/gradient-cli-2.png) 9 | 10 | ## Features 11 | 12 | * Lots of preset gradients. 13 | * Custom gradient. 14 | * Read gradients from SVG & GIMP gradient (ggr) file. 15 | * Display gradient in the terminal. 16 | * Get colors from gradient. 17 | 18 | ## Installation 19 | 20 | Pre-compiled binaries for Linux, macOS and Windows is avaliable on [release page](https://github.com/mazznoer/gradient-rs/releases). 21 | 22 | ### Cargo 23 | 24 | `gradient` can be installed using [cargo](https://www.rust-lang.org/tools/install). 25 | 26 | ```shell 27 | cargo install gradient 28 | ``` 29 | 30 | ### NetBSD 31 | 32 | On NetBSD, a pre-compiled binary is available from the official repositories. 33 | To install it, simply run: 34 | 35 | ```shell 36 | pkgin install gradient 37 | ``` 38 | 39 | ## Usage 40 | 41 | ``` 42 | gradient [OPTIONS] 43 | ``` 44 | 45 | ### Options: 46 | 47 | * `-W`, `--width` `` : Gradient display width (default: terminal width) 48 | * `-H`, `--height` `` : Gradient display height (default: 2) 49 | * `-b`, `--background` `` : Background color (default: checkerboard) 50 | * `--cb-color` `` `` : Checkerboard color 51 | * `-s`, `--sample` ``... : Get color(s) at specific position 52 | * `-t`, `--take` `` : Get N colors evenly spaced across gradient 53 | * `-o`, `--format` `` : Output color format (default: hex) [hex, rgb, rgb255, hsl, hsv, hwb] 54 | * `-a`, `--array` : Print colors as array 55 | 56 | ### Preset gradient 57 | 58 | * `-p`, `--preset` `` : Using the preset gradient 59 | * `-l`, `--list-presets` : Lists all available preset gradient names 60 | 61 | ### Custom gradient 62 | 63 | * `-c`, `--custom` ``... : Create custom gradient 64 | * `-m`, `--blend-mode` `` : Custom gradient blending mode (default: oklab) [rgb, linear-rgb, hsv, oklab] 65 | * `-i`, `--interpolation` `` : Custom gradient interpolation mode (default: catmull-rom) [linear, basis, catmull-rom] 66 | * `-P`, `--position` ``... : Custom gradient color position 67 | 68 | ### Gradient file 69 | 70 | * `-f`, `--file` ``... : Read gradient from SVG or GIMP gradient (ggr) file(s) 71 | * `--ggr-fg` `` : GGR foreground color (default: black) 72 | * `--ggr-bg` `` : GGR background color (default: white) 73 | * `--svg-id` `` : Pick one SVG gradient by ID 74 | 75 | `COLOR` can be specified using [CSS color format](https://www.w3.org/TR/css-color-4/). 76 | 77 | ## Usage Examples 78 | 79 | Get 100 colors (evenly spaced accross gradient domain) from rainbow preset gradient. 80 | 81 | ```shell 82 | gradient -p rainbow -t 100 83 | ``` 84 | 85 | Display all gradients from svg file. 86 | 87 | ```shell 88 | gradient -f file.svg 89 | ``` 90 | 91 | Create custom gradient. 92 | 93 | ```shell 94 | gradient -c gold ff4700 'rgb(90,230,170)' 'hsl(340,50%,50%)' 'hsv(270,60%,70%)' 'hwb(230,50%,0%)' 95 | ``` 96 | 97 | **TODO** add more examples 98 | 99 | -------------------------------------------------------------------------------- /tests/app.rs: -------------------------------------------------------------------------------- 1 | use assert_cmd::Command; 2 | 3 | fn gradient() -> Command { 4 | let mut cmd = Command::cargo_bin("gradient").unwrap(); 5 | cmd.current_dir(env!("CARGO_MANIFEST_DIR")); 6 | cmd 7 | } 8 | 9 | #[test] 10 | fn basic() { 11 | gradient().assert().failure(); 12 | 13 | gradient().arg("--list-presets").assert().success(); 14 | 15 | gradient().arg("--named-colors").assert().success(); 16 | 17 | gradient().arg("--preset").arg("magma").assert().success(); 18 | 19 | gradient() 20 | .arg("--preset") 21 | .arg("rainbow") 22 | .arg("--sample") 23 | .args(&["0", "0.35", "0.77"]) 24 | .assert() 25 | .success() 26 | .stdout("#6e40aa\n#fb9633\n#1cbccc\n"); 27 | 28 | gradient() 29 | .arg("--css") 30 | .arg("#f05, rgb(0, 255, 90)") 31 | .arg("--take") 32 | .arg("5") 33 | .arg("--array") 34 | .assert() 35 | .success() 36 | .stdout(concat!( 37 | r##"["#ff0055", "#ed7458", "#d0a95a", "#a0d55b", "#00ff5a"]"##, 38 | "\n" 39 | )); 40 | 41 | gradient() 42 | .arg("--custom") 43 | .arg("hwb(75, 25%, 10%)") 44 | .arg("#bad455") 45 | .arg("goldenrod") 46 | .arg("--cb-color") 47 | .arg("#000") 48 | .arg("red") 49 | .assert() 50 | .success(); 51 | 52 | gradient() 53 | .args(&[ 54 | "--custom", 55 | "gold,purple,red", 56 | "--position", 57 | "0,70,100", 58 | "--blend-mode", 59 | "lab", 60 | "--interpolation", 61 | "basis", 62 | ]) 63 | .assert() 64 | .success(); 65 | 66 | gradient() 67 | .arg("-c") 68 | .arg("#46f, #ab7, #abc456") 69 | .arg("-P") 70 | .arg("0, 73,100 ") 71 | .arg("-s") 72 | .arg(" 0,73.0, 100 , 120") 73 | .assert() 74 | .success() 75 | .stdout("#4466ff\n#aabb77\n#abc456\n#abc456\n"); 76 | 77 | gradient() 78 | .arg("--custom") 79 | .arg("red, rgb(0,255,0), #00f") 80 | .arg("--position=-5,5,10") 81 | .arg("--sample=-5,10,5") 82 | .assert() 83 | .success() 84 | .stdout("#ff0000\n#0000ff\n#00ff00\n"); 85 | 86 | gradient() 87 | .arg("--file") 88 | .arg("data/gradients.svg") 89 | .arg("data/Neon_Green.ggr") 90 | .assert() 91 | .success(); 92 | } 93 | 94 | #[test] 95 | fn others() { 96 | // contains invalid gradient 97 | gradient() 98 | .arg("-f") 99 | .arg("data/test1.svg") 100 | .assert() 101 | .failure(); 102 | 103 | // #grad-1 is a valid gradient 104 | gradient() 105 | .arg("-f") 106 | .arg("data/test1.svg") 107 | .arg("--svg-id") 108 | .arg("grad-1") 109 | .assert() 110 | .success(); 111 | 112 | // #grad-0 is an invalid gradient 113 | gradient() 114 | .arg("-f") 115 | .arg("data/test1.svg") 116 | .arg("--svg-id") 117 | .arg("grad-0") 118 | .assert() 119 | .failure(); 120 | } 121 | 122 | #[test] 123 | fn invalid() { 124 | // invalid preset name 125 | gradient().arg("--preset").arg("sunset").assert().failure(); 126 | 127 | // conflicting arguments [--preset, --custom, --css] 128 | 129 | gradient() 130 | .arg("--preset") 131 | .arg("plasma") 132 | .arg("--custom") 133 | .arg("red") 134 | .arg("blue") 135 | .assert() 136 | .failure(); 137 | 138 | gradient() 139 | .arg("--preset") 140 | .arg("plasma") 141 | .arg("--css") 142 | .arg("red,blue") 143 | .assert() 144 | .failure(); 145 | 146 | gradient() 147 | .arg("--custom") 148 | .arg("red") 149 | .arg("blue") 150 | .arg("--css") 151 | .arg("red,blue") 152 | .assert() 153 | .failure(); 154 | 155 | // conflicting arguments [--take, --sample] 156 | 157 | gradient() 158 | .arg("--preset") 159 | .arg("plasma") 160 | .arg("--take") 161 | .arg("5") 162 | .arg("--sample") 163 | .arg("0.1") 164 | .arg("0.73") 165 | .assert() 166 | .failure(); 167 | 168 | // invalid CSS gradient 169 | gradient() 170 | .arg("--css") 171 | .arg("red, 25%, 70%, blue") 172 | .assert() 173 | .failure(); 174 | 175 | // invalid position 176 | 177 | gradient() 178 | .arg("--custom") 179 | .arg("red, lime") 180 | .arg("--position") 181 | .arg("0, 0.5, 1") 182 | .assert() 183 | .failure(); 184 | 185 | // invalid SVG gradient 186 | gradient() 187 | .arg("--file") 188 | .arg("data/invalid.svg") 189 | .assert() 190 | .failure(); 191 | 192 | // invalid GIMP gradient 193 | gradient() 194 | .arg("--file") 195 | .arg("data/invalid.ggr") 196 | .assert() 197 | .failure() 198 | .stderr("data/invalid.ggr (invalid GIMP gradient)\n"); 199 | 200 | // SVG without gradient 201 | gradient() 202 | .arg("--file") 203 | .arg("data/no-gradient.svg") 204 | .assert() 205 | .failure(); 206 | 207 | // non-existent file 208 | gradient() 209 | .arg("--file") 210 | .arg("gradients.svg") 211 | .assert() 212 | .failure() 213 | .stderr("gradients.svg: file not found.\n"); 214 | 215 | // unsupported file formats 216 | 217 | gradient() 218 | .arg("--file") 219 | .arg("Cargo.toml") 220 | .assert() 221 | .failure() 222 | .stderr("Cargo.toml: file format not supported.\n"); 223 | 224 | gradient() 225 | .arg("--file") 226 | .arg("Makefile") 227 | .assert() 228 | .failure() 229 | .stderr("Makefile: file format not supported.\n"); 230 | 231 | // --cb-color need exactly 2 values 232 | 233 | gradient() 234 | .arg("--css") 235 | .arg("f00, f000") 236 | .arg("--cb-color") 237 | .assert() 238 | .failure(); 239 | 240 | gradient() 241 | .arg("--css") 242 | .arg("f00, f000") 243 | .arg("--cb-color") 244 | .arg("black") 245 | .assert() 246 | .failure(); 247 | 248 | gradient() 249 | .arg("--css") 250 | .arg("f00, f000") 251 | .arg("--cb-color") 252 | .arg("black") 253 | .arg("gold") 254 | .arg("lime") 255 | .assert() 256 | .failure(); 257 | } 258 | -------------------------------------------------------------------------------- /LICENSE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright 2021 Nor Khasyatillah 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 4 4 | 5 | [[package]] 6 | name = "anstyle" 7 | version = "1.0.13" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" 10 | 11 | [[package]] 12 | name = "assert_cmd" 13 | version = "2.1.1" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "bcbb6924530aa9e0432442af08bbcafdad182db80d2e560da42a6d442535bf85" 16 | dependencies = [ 17 | "anstyle", 18 | "bstr", 19 | "libc", 20 | "predicates", 21 | "predicates-core", 22 | "predicates-tree", 23 | "wait-timeout", 24 | ] 25 | 26 | [[package]] 27 | name = "autocfg" 28 | version = "1.5.0" 29 | source = "registry+https://github.com/rust-lang/crates.io-index" 30 | checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" 31 | 32 | [[package]] 33 | name = "bitflags" 34 | version = "2.10.0" 35 | source = "registry+https://github.com/rust-lang/crates.io-index" 36 | checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" 37 | 38 | [[package]] 39 | name = "bstr" 40 | version = "1.12.1" 41 | source = "registry+https://github.com/rust-lang/crates.io-index" 42 | checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab" 43 | dependencies = [ 44 | "memchr", 45 | "regex-automata", 46 | "serde", 47 | ] 48 | 49 | [[package]] 50 | name = "colorgrad" 51 | version = "0.8.0" 52 | source = "registry+https://github.com/rust-lang/crates.io-index" 53 | checksum = "aee94de557db6ddae3ca7b37b5fbe77ed6f159219f1815a8c2c1a9854c73087e" 54 | dependencies = [ 55 | "csscolorparser", 56 | "libm", 57 | ] 58 | 59 | [[package]] 60 | name = "csscolorparser" 61 | version = "0.8.0" 62 | source = "registry+https://github.com/rust-lang/crates.io-index" 63 | checksum = "3b9e6b904cb9ee4cb0bb93ba2b1bf3dcd2d3fc58c25557934a4b56cbd2ad9f88" 64 | dependencies = [ 65 | "num-traits", 66 | "phf", 67 | "uncased", 68 | ] 69 | 70 | [[package]] 71 | name = "difflib" 72 | version = "0.4.0" 73 | source = "registry+https://github.com/rust-lang/crates.io-index" 74 | checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" 75 | 76 | [[package]] 77 | name = "errno" 78 | version = "0.3.14" 79 | source = "registry+https://github.com/rust-lang/crates.io-index" 80 | checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" 81 | dependencies = [ 82 | "libc", 83 | "windows-sys 0.61.2", 84 | ] 85 | 86 | [[package]] 87 | name = "fastrand" 88 | version = "2.3.0" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" 91 | 92 | [[package]] 93 | name = "gradient" 94 | version = "0.4.1" 95 | dependencies = [ 96 | "assert_cmd", 97 | "colorgrad", 98 | "csscolorparser", 99 | "lexopt", 100 | "svg", 101 | "terminal_size", 102 | ] 103 | 104 | [[package]] 105 | name = "lexopt" 106 | version = "0.3.1" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "9fa0e2a1fcbe2f6be6c42e342259976206b383122fc152e872795338b5a3f3a7" 109 | 110 | [[package]] 111 | name = "libc" 112 | version = "0.2.177" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "2874a2af47a2325c2001a6e6fad9b16a53b802102b528163885171cf92b15976" 115 | 116 | [[package]] 117 | name = "libm" 118 | version = "0.2.15" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" 121 | 122 | [[package]] 123 | name = "linux-raw-sys" 124 | version = "0.11.0" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" 127 | 128 | [[package]] 129 | name = "memchr" 130 | version = "2.7.6" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" 133 | 134 | [[package]] 135 | name = "num-traits" 136 | version = "0.2.19" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 139 | dependencies = [ 140 | "autocfg", 141 | "libm", 142 | ] 143 | 144 | [[package]] 145 | name = "phf" 146 | version = "0.13.1" 147 | source = "registry+https://github.com/rust-lang/crates.io-index" 148 | checksum = "c1562dc717473dbaa4c1f85a36410e03c047b2e7df7f45ee938fbef64ae7fadf" 149 | dependencies = [ 150 | "phf_macros", 151 | "phf_shared", 152 | "serde", 153 | ] 154 | 155 | [[package]] 156 | name = "phf_generator" 157 | version = "0.13.1" 158 | source = "registry+https://github.com/rust-lang/crates.io-index" 159 | checksum = "135ace3a761e564ec88c03a77317a7c6b80bb7f7135ef2544dbe054243b89737" 160 | dependencies = [ 161 | "fastrand", 162 | "phf_shared", 163 | ] 164 | 165 | [[package]] 166 | name = "phf_macros" 167 | version = "0.13.1" 168 | source = "registry+https://github.com/rust-lang/crates.io-index" 169 | checksum = "812f032b54b1e759ccd5f8b6677695d5268c588701effba24601f6932f8269ef" 170 | dependencies = [ 171 | "phf_generator", 172 | "phf_shared", 173 | "proc-macro2", 174 | "quote", 175 | "syn", 176 | "uncased", 177 | ] 178 | 179 | [[package]] 180 | name = "phf_shared" 181 | version = "0.13.1" 182 | source = "registry+https://github.com/rust-lang/crates.io-index" 183 | checksum = "e57fef6bc5981e38c2ce2d63bfa546861309f875b8a75f092d1d54ae2d64f266" 184 | dependencies = [ 185 | "siphasher", 186 | "uncased", 187 | ] 188 | 189 | [[package]] 190 | name = "predicates" 191 | version = "3.1.3" 192 | source = "registry+https://github.com/rust-lang/crates.io-index" 193 | checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" 194 | dependencies = [ 195 | "anstyle", 196 | "difflib", 197 | "predicates-core", 198 | ] 199 | 200 | [[package]] 201 | name = "predicates-core" 202 | version = "1.0.9" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" 205 | 206 | [[package]] 207 | name = "predicates-tree" 208 | version = "1.0.12" 209 | source = "registry+https://github.com/rust-lang/crates.io-index" 210 | checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" 211 | dependencies = [ 212 | "predicates-core", 213 | "termtree", 214 | ] 215 | 216 | [[package]] 217 | name = "proc-macro2" 218 | version = "1.0.103" 219 | source = "registry+https://github.com/rust-lang/crates.io-index" 220 | checksum = "5ee95bc4ef87b8d5ba32e8b7714ccc834865276eab0aed5c9958d00ec45f49e8" 221 | dependencies = [ 222 | "unicode-ident", 223 | ] 224 | 225 | [[package]] 226 | name = "quote" 227 | version = "1.0.42" 228 | source = "registry+https://github.com/rust-lang/crates.io-index" 229 | checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" 230 | dependencies = [ 231 | "proc-macro2", 232 | ] 233 | 234 | [[package]] 235 | name = "regex-automata" 236 | version = "0.4.13" 237 | source = "registry+https://github.com/rust-lang/crates.io-index" 238 | checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" 239 | 240 | [[package]] 241 | name = "rustix" 242 | version = "1.1.2" 243 | source = "registry+https://github.com/rust-lang/crates.io-index" 244 | checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e" 245 | dependencies = [ 246 | "bitflags", 247 | "errno", 248 | "libc", 249 | "linux-raw-sys", 250 | "windows-sys 0.61.2", 251 | ] 252 | 253 | [[package]] 254 | name = "serde" 255 | version = "1.0.228" 256 | source = "registry+https://github.com/rust-lang/crates.io-index" 257 | checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" 258 | dependencies = [ 259 | "serde_core", 260 | ] 261 | 262 | [[package]] 263 | name = "serde_core" 264 | version = "1.0.228" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" 267 | dependencies = [ 268 | "serde_derive", 269 | ] 270 | 271 | [[package]] 272 | name = "serde_derive" 273 | version = "1.0.228" 274 | source = "registry+https://github.com/rust-lang/crates.io-index" 275 | checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" 276 | dependencies = [ 277 | "proc-macro2", 278 | "quote", 279 | "syn", 280 | ] 281 | 282 | [[package]] 283 | name = "siphasher" 284 | version = "1.0.1" 285 | source = "registry+https://github.com/rust-lang/crates.io-index" 286 | checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" 287 | 288 | [[package]] 289 | name = "svg" 290 | version = "0.18.0" 291 | source = "registry+https://github.com/rust-lang/crates.io-index" 292 | checksum = "94afda9cd163c04f6bee8b4bf2501c91548deae308373c436f36aeff3cf3c4a3" 293 | 294 | [[package]] 295 | name = "syn" 296 | version = "2.0.110" 297 | source = "registry+https://github.com/rust-lang/crates.io-index" 298 | checksum = "a99801b5bd34ede4cf3fc688c5919368fea4e4814a4664359503e6015b280aea" 299 | dependencies = [ 300 | "proc-macro2", 301 | "quote", 302 | "unicode-ident", 303 | ] 304 | 305 | [[package]] 306 | name = "terminal_size" 307 | version = "0.4.3" 308 | source = "registry+https://github.com/rust-lang/crates.io-index" 309 | checksum = "60b8cb979cb11c32ce1603f8137b22262a9d131aaa5c37b5678025f22b8becd0" 310 | dependencies = [ 311 | "rustix", 312 | "windows-sys 0.60.2", 313 | ] 314 | 315 | [[package]] 316 | name = "termtree" 317 | version = "0.5.1" 318 | source = "registry+https://github.com/rust-lang/crates.io-index" 319 | checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" 320 | 321 | [[package]] 322 | name = "uncased" 323 | version = "0.9.10" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "e1b88fcfe09e89d3866a5c11019378088af2d24c3fbd4f0543f96b479ec90697" 326 | dependencies = [ 327 | "version_check", 328 | ] 329 | 330 | [[package]] 331 | name = "unicode-ident" 332 | version = "1.0.22" 333 | source = "registry+https://github.com/rust-lang/crates.io-index" 334 | checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" 335 | 336 | [[package]] 337 | name = "version_check" 338 | version = "0.9.5" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" 341 | 342 | [[package]] 343 | name = "wait-timeout" 344 | version = "0.2.1" 345 | source = "registry+https://github.com/rust-lang/crates.io-index" 346 | checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" 347 | dependencies = [ 348 | "libc", 349 | ] 350 | 351 | [[package]] 352 | name = "windows-link" 353 | version = "0.2.1" 354 | source = "registry+https://github.com/rust-lang/crates.io-index" 355 | checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" 356 | 357 | [[package]] 358 | name = "windows-sys" 359 | version = "0.60.2" 360 | source = "registry+https://github.com/rust-lang/crates.io-index" 361 | checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" 362 | dependencies = [ 363 | "windows-targets", 364 | ] 365 | 366 | [[package]] 367 | name = "windows-sys" 368 | version = "0.61.2" 369 | source = "registry+https://github.com/rust-lang/crates.io-index" 370 | checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" 371 | dependencies = [ 372 | "windows-link", 373 | ] 374 | 375 | [[package]] 376 | name = "windows-targets" 377 | version = "0.53.5" 378 | source = "registry+https://github.com/rust-lang/crates.io-index" 379 | checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" 380 | dependencies = [ 381 | "windows-link", 382 | "windows_aarch64_gnullvm", 383 | "windows_aarch64_msvc", 384 | "windows_i686_gnu", 385 | "windows_i686_gnullvm", 386 | "windows_i686_msvc", 387 | "windows_x86_64_gnu", 388 | "windows_x86_64_gnullvm", 389 | "windows_x86_64_msvc", 390 | ] 391 | 392 | [[package]] 393 | name = "windows_aarch64_gnullvm" 394 | version = "0.53.1" 395 | source = "registry+https://github.com/rust-lang/crates.io-index" 396 | checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" 397 | 398 | [[package]] 399 | name = "windows_aarch64_msvc" 400 | version = "0.53.1" 401 | source = "registry+https://github.com/rust-lang/crates.io-index" 402 | checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" 403 | 404 | [[package]] 405 | name = "windows_i686_gnu" 406 | version = "0.53.1" 407 | source = "registry+https://github.com/rust-lang/crates.io-index" 408 | checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" 409 | 410 | [[package]] 411 | name = "windows_i686_gnullvm" 412 | version = "0.53.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" 415 | 416 | [[package]] 417 | name = "windows_i686_msvc" 418 | version = "0.53.1" 419 | source = "registry+https://github.com/rust-lang/crates.io-index" 420 | checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" 421 | 422 | [[package]] 423 | name = "windows_x86_64_gnu" 424 | version = "0.53.1" 425 | source = "registry+https://github.com/rust-lang/crates.io-index" 426 | checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" 427 | 428 | [[package]] 429 | name = "windows_x86_64_gnullvm" 430 | version = "0.53.1" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" 433 | 434 | [[package]] 435 | name = "windows_x86_64_msvc" 436 | version = "0.53.1" 437 | source = "registry+https://github.com/rust-lang/crates.io-index" 438 | checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" 439 | -------------------------------------------------------------------------------- /src/cli.rs: -------------------------------------------------------------------------------- 1 | use std::num::ParseFloatError; 2 | use std::path::PathBuf; 3 | use std::str::FromStr; 4 | 5 | use colorgrad::{Color, ParseColorError}; 6 | 7 | #[derive(Clone)] 8 | pub enum BlendMode { 9 | Rgb, 10 | LinearRgb, 11 | Oklab, 12 | Lab, 13 | } 14 | 15 | impl FromStr for BlendMode { 16 | type Err = String; 17 | 18 | fn from_str(s: &str) -> Result { 19 | match s.to_lowercase().as_str() { 20 | "rgb" => Ok(Self::Rgb), 21 | "linear-rgb" => Ok(Self::LinearRgb), 22 | "oklab" => Ok(Self::Oklab), 23 | "lab" => Ok(Self::Lab), 24 | _ => Err(format!( 25 | "Invalid --blend-mode '{s}' [pick from: rgb, linear-rgb, oklab, lab]" 26 | )), 27 | } 28 | } 29 | } 30 | 31 | #[derive(Copy, Clone)] 32 | pub enum Interpolation { 33 | Linear, 34 | Basis, 35 | CatmullRom, 36 | } 37 | 38 | impl FromStr for Interpolation { 39 | type Err = String; 40 | 41 | fn from_str(s: &str) -> Result { 42 | match s.to_lowercase().as_str() { 43 | "linear" => Ok(Self::Linear), 44 | "basis" => Ok(Self::Basis), 45 | "catmull-rom" => Ok(Self::CatmullRom), 46 | _ => Err(format!( 47 | "Invalid --interpolation '{s}' [pick from: linear, basis, catmull-rom]" 48 | )), 49 | } 50 | } 51 | } 52 | 53 | #[derive(Copy, Clone, PartialEq)] 54 | pub enum OutputColor { 55 | Hex, 56 | Rgb, 57 | Hsl, 58 | Hsv, 59 | Hwb, 60 | Lab, 61 | Lch, 62 | Oklab, 63 | Oklch, 64 | } 65 | 66 | impl FromStr for OutputColor { 67 | type Err = String; 68 | 69 | fn from_str(s: &str) -> Result { 70 | match s.to_lowercase().as_str() { 71 | "hex" => Ok(Self::Hex), 72 | "rgb" => Ok(Self::Rgb), 73 | "hsl" => Ok(Self::Hsl), 74 | "hsv" => Ok(Self::Hsv), 75 | "hwb" => Ok(Self::Hwb), 76 | "lab" => Ok(Self::Lab), 77 | "lch" => Ok(Self::Lch), 78 | "oklab" => Ok(Self::Oklab), 79 | "oklch" => Ok(Self::Oklch), 80 | _ => Err(format!( 81 | "Invalid --format '{s}' [pick from: hex, rgb, hsl, hsv, hwb, lab, lch, oklab, oklch]" 82 | )), 83 | } 84 | } 85 | } 86 | 87 | pub const PRESET_NAMES: [&str; 38] = [ 88 | "blues", 89 | "br-bg", 90 | "bu-gn", 91 | "bu-pu", 92 | "cividis", 93 | "cool", 94 | "cubehelix", 95 | "gn-bu", 96 | "greens", 97 | "greys", 98 | "inferno", 99 | "magma", 100 | "or-rd", 101 | "oranges", 102 | "pi-yg", 103 | "plasma", 104 | "pr-gn", 105 | "pu-bu", 106 | "pu-bu-gn", 107 | "pu-or", 108 | "pu-rd", 109 | "purples", 110 | "rainbow", 111 | "rd-bu", 112 | "rd-gy", 113 | "rd-pu", 114 | "rd-yl-bu", 115 | "rd-yl-gn", 116 | "reds", 117 | "sinebow", 118 | "spectral", 119 | "turbo", 120 | "viridis", 121 | "warm", 122 | "yl-gn", 123 | "yl-gn-bu", 124 | "yl-or-br", 125 | "yl-or-rd", 126 | ]; 127 | 128 | const VERSION: &str = "\ 129 | gradient v0.5.0 130 | "; 131 | 132 | const HELP: &str = "\ 133 | A command line tool for playing with color gradients 134 | 135 | Usage: gradient [OPTIONS] 136 | 137 | Options: 138 | -W, --width Gradient display width 139 | -H, --height Gradient display height 140 | -b, --background Background color 141 | --cb-color Checkerboard color 142 | -t, --take Get N colors evenly spaced across gradient 143 | -s, --sample ... Get color(s) at specific position 144 | -o, --format Output color format. [default: hex] 145 | [hex, rgb, hsl, hsv, hwb, lab, lch, oklab, oklch] 146 | -a, --array Print colors from --take or --sample, as array 147 | --named-colors Lists all CSS named colors 148 | -h, --help Print help (see more with '--help') 149 | --version Print version 150 | 151 | PRESET GRADIENT: 152 | -l, --list-presets Lists all available preset gradient names 153 | -p, --preset Use the preset gradient 154 | 155 | CUSTOM GRADIENT: 156 | -c, --custom ... Create custom gradient with the specified colors 157 | -P, --position ... Custom gradient color position 158 | -C, --css Custom gradient using CSS gradient format 159 | -m, --blend-mode Custom gradient blending mode 160 | [values: rgb, linear-rgb, oklab, lab] 161 | -i, --interpolation Custom gradient interpolation mode 162 | [values: linear, basis, catmull-rom] 163 | 164 | GRADIENT FILE: 165 | --ggr-bg GGR background color [default: white] 166 | --ggr-fg GGR foreground color [default: black] 167 | --svg-id Pick SVG gradient by ID 168 | -f, --file ... Read gradient from SVG or GIMP gradient (ggr) file(s) 169 | 170 | \x1B[1mCOLOR\x1B[0m can be specified using CSS color format . 171 | "; 172 | 173 | const EXTRA_HELP: &str = " 174 | \x1B[1;4mUsage Examples:\x1B[0m 175 | Display preset gradient 176 | 177 | \x1B[1m$\x1B[0m gradient --preset rainbow 178 | 179 | Get 15 colors from preset gradient 180 | 181 | \x1B[1m$\x1B[0m gradient --preset spectral --take 15 182 | 183 | Create & display custom gradient 184 | 185 | \x1B[1m$\x1B[0m gradient --custom deeppink gold seagreen 186 | 187 | Create custom gradient & get 20 colors 188 | 189 | \x1B[1m$\x1B[0m gradient --custom ff00ff 'rgb(50,200,70)' 'hwb(195,0,0.5)' --take 20 190 | 191 | \x1B[1;4mRepository:\x1B[0m 192 | URL: https://github.com/mazznoer/gradient-rs 193 | "; 194 | 195 | #[derive(Default)] 196 | pub struct Opt { 197 | pub list_presets: bool, 198 | pub preset: Option, 199 | pub custom: Option>, 200 | pub position: Option>, 201 | pub css: Option, 202 | pub blend_mode: Option, 203 | pub interpolation: Option, 204 | pub ggr_bg: Option, 205 | pub ggr_fg: Option, 206 | pub svg_id: Option, 207 | pub file: Option>, 208 | pub width: Option, 209 | pub height: Option, 210 | pub background: Option, 211 | pub cb_color: Option<[Color; 2]>, 212 | pub take: Option, 213 | pub sample: Option>, 214 | pub format: Option, 215 | pub array: bool, 216 | pub named_colors: bool, 217 | } 218 | 219 | #[rustfmt::skip] 220 | pub fn parse_args() -> Result { 221 | use std::process::exit; 222 | use lexopt::prelude::*; 223 | 224 | let mut opt: Opt = Default::default(); 225 | 226 | let mut parser = lexopt::Parser::from_env(); 227 | 228 | while let Some(arg) = parser.next()? { 229 | match arg { 230 | Short('h') => { 231 | print!("{HELP}"); 232 | exit(0); 233 | } 234 | Long("help") => { 235 | print!("{HELP}"); 236 | print!("{EXTRA_HELP}"); 237 | exit(0); 238 | } 239 | Long("version") => { 240 | print!("{VERSION}"); 241 | exit(0); 242 | } 243 | Short('l') | Long("list-presets") => { 244 | opt.list_presets = true; 245 | } 246 | Short('p') | Long("preset") => { 247 | if opt.custom.is_some() || opt.css.is_some() { 248 | return Err("choose one: --preset, --custom, --css".into()); 249 | } 250 | opt.preset = Some(parser.value()?.parse_with(parse_preset_name)?); 251 | } 252 | Short('c') | Long("custom") => { 253 | if opt.preset.is_some() || opt.css.is_some() { 254 | return Err("choose one: --preset, --custom, --css".into()); 255 | } 256 | for s in parser.values()? { 257 | let v = s.parse_with(parse_colors)?; 258 | if let Some(ref mut colors) = opt.custom { 259 | colors.extend(v); 260 | } else { 261 | opt.custom = Some(v); 262 | } 263 | } 264 | } 265 | Short('P') | Long("position") => { 266 | for s in parser.values()? { 267 | let v = s.parse_with(parse_floats)?; 268 | if let Some(ref mut position) = opt.position { 269 | position.extend(v); 270 | } else { 271 | opt.position = Some(v); 272 | } 273 | } 274 | } 275 | Short('C') | Long("css") => { 276 | if opt.custom.is_some() || opt.preset.is_some() { 277 | return Err("choose one: --preset, --custom, --css".into()); 278 | } 279 | opt.css = Some(parser.value()?.parse()?); 280 | } 281 | Short('m') | Long("blend-mode") => { 282 | opt.blend_mode = Some(parser.value()?.parse()?); 283 | } 284 | Short('i') | Long("interpolation") => { 285 | opt.interpolation = Some(parser.value()?.parse()?); 286 | } 287 | Long("ggr-bg") => { 288 | opt.ggr_bg = Some(parser.value()?.parse()?); 289 | } 290 | Long("ggr-fg") => { 291 | opt.ggr_fg = Some(parser.value()?.parse()?); 292 | } 293 | Long("svg-id") => { 294 | opt.svg_id = Some(parser.value()?.parse()?); 295 | } 296 | Short('f') | Long("file") => { 297 | let res: Result,_> = parser.values()?.map(|s|s.parse()).collect(); 298 | opt.file = Some(res?); 299 | } 300 | Short('W') | Long("width") => { 301 | opt.width = Some(parser.value()?.parse()?); 302 | } 303 | Short('H') | Long("height") => { 304 | opt.height = Some(parser.value()?.parse()?); 305 | } 306 | Short('b') | Long("background") => { 307 | opt.background = Some(parser.value()?.parse()?); 308 | } 309 | Long("cb-color") => { 310 | opt.cb_color = Some([ 311 | parser.value()?.parse()?, 312 | parser.value()?.parse()?, 313 | ]); 314 | } 315 | Short('t') | Long("take") => { 316 | if opt.sample.is_some() { 317 | return Err("--take cannot be used with --sample".into()); 318 | } 319 | opt.take = Some(parser.value()?.parse()?); 320 | } 321 | Short('s') | Long("sample") => { 322 | if opt.take.is_some() { 323 | return Err("--take cannot be used with --sample".into()); 324 | } 325 | for s in parser.values()? { 326 | let v = s.parse_with(parse_floats)?; 327 | if let Some(ref mut sample) = opt.sample { 328 | sample.extend(v); 329 | } else { 330 | opt.sample = Some(v); 331 | } 332 | } 333 | } 334 | Short('o') | Long("format") => { 335 | opt.format = Some(parser.value()?.parse()?); 336 | } 337 | Short('a') | Long("array") => { 338 | opt.array = true; 339 | } 340 | Long("named-colors") => { 341 | opt.named_colors = true; 342 | } 343 | _ => return Err(arg.unexpected()), 344 | } 345 | } 346 | 347 | Ok(opt) 348 | } 349 | 350 | fn parse_preset_name(s: &str) -> Result { 351 | let s = s.to_lowercase(); 352 | if PRESET_NAMES.contains(&s.as_ref()) { 353 | return Ok(s); 354 | } 355 | Err("invalid preset name, try --list-presets".into()) 356 | } 357 | 358 | fn parse_floats(s: &str) -> Result, ParseFloatError> { 359 | s.split(',').map(|s| s.trim().parse()).collect() 360 | } 361 | 362 | fn parse_colors(s: &str) -> Result, ParseColorError> { 363 | let mut colors = Vec::new(); 364 | let mut start = 0; 365 | let mut inside = false; 366 | 367 | for (i, c) in s.char_indices() { 368 | if c == ',' && !inside { 369 | colors.push(s[start..i].parse()?); 370 | start = i + 1; 371 | } else if c == '(' { 372 | inside = true; 373 | } else if c == ')' { 374 | inside = false; 375 | } 376 | } 377 | colors.push(s[start..].parse()?); 378 | Ok(colors) 379 | } 380 | 381 | #[cfg(test)] 382 | mod tests { 383 | use super::*; 384 | 385 | #[test] 386 | fn preset_name_test() { 387 | assert_eq!(parse_preset_name("rainbow"), Ok("rainbow".into())); 388 | assert_eq!(parse_preset_name("Rainbow"), Ok("rainbow".into())); 389 | assert_eq!(parse_preset_name("PLASMA"), Ok("plasma".into())); 390 | 391 | assert!(parse_preset_name("sky").is_err()); 392 | } 393 | 394 | #[test] 395 | fn parse_floats_test() { 396 | assert_eq!(parse_floats("90"), Ok(vec![90.0])); 397 | assert_eq!(parse_floats("0,0.5,1"), Ok(vec![0.0, 0.5, 1.0])); 398 | assert_eq!( 399 | parse_floats(" 12.7 , 0.56 ,-0.9"), 400 | Ok(vec![12.7, 0.56, -0.9]) 401 | ); 402 | assert_eq!(parse_floats("-2.3, 0.73 "), Ok(vec![-2.3, 0.73])); 403 | 404 | assert!(parse_floats("1,0.5,6p,8").is_err()); 405 | assert!(parse_floats("").is_err()); 406 | } 407 | 408 | #[test] 409 | fn parse_colors_test() { 410 | let res = parse_colors("rgb(0,255, 0), #ff0, rgba(100%, 0%, 0%, 100%), blue").unwrap(); 411 | 412 | assert_eq!(res.len(), 4); 413 | assert_eq!(res[0].to_css_hex(), "#00ff00"); 414 | assert_eq!(res[1].to_css_hex(), "#ffff00"); 415 | assert_eq!(res[2].to_css_hex(), "#ff0000"); 416 | assert_eq!(res[3].to_css_hex(), "#0000ff"); 417 | 418 | assert!(parse_colors("red, rgb(90,20,)").is_err()); 419 | } 420 | } 421 | -------------------------------------------------------------------------------- /src/svg_gradient.rs: -------------------------------------------------------------------------------- 1 | use colorgrad::Color; 2 | use colorgrad::GradientBuilder; 3 | use svg::node::element::tag as svg_tag; 4 | use svg::parser::Event; 5 | 6 | fn parse_percent_or_float(s: &str) -> Option { 7 | if let Some(s) = s.strip_suffix('%') { 8 | if let Ok(t) = s.parse::() { 9 | return Some(t / 100.0); 10 | } 11 | return None; 12 | } 13 | s.parse::().ok() 14 | } 15 | 16 | // returns (color, opacity) 17 | fn parse_styles(s: &str) -> (Option<&str>, Option<&str>) { 18 | let mut val = (None, None); 19 | 20 | for x in s.split(';') { 21 | let d = x.split(':').collect::>(); 22 | 23 | if d.len() == 2 { 24 | match d[0].trim().to_lowercase().as_ref() { 25 | "stop-color" => val.0 = Some(d[1]), 26 | "stop-opacity" => val.1 = Some(d[1]), 27 | _ => {} 28 | } 29 | } 30 | } 31 | 32 | val 33 | } 34 | 35 | #[derive(Debug)] 36 | pub struct SvgGradient { 37 | pub id: Option, 38 | pub colors: Vec, 39 | pub pos: Vec, 40 | pub valid: bool, 41 | } 42 | 43 | impl SvgGradient { 44 | pub fn gradient_builder(&mut self) -> Option { 45 | if !self.valid || self.colors.is_empty() { 46 | return None; 47 | } 48 | if self.pos[0] > 0.0 { 49 | self.pos.insert(0, 0.0); 50 | self.colors.insert(0, self.colors[0].clone()); 51 | } 52 | let last = self.colors.len() - 1; 53 | if self.pos[last] < 1.0 { 54 | self.pos.push(1.0); 55 | self.colors.push(self.colors[last].clone()); 56 | } 57 | let mut gb = GradientBuilder::new(); 58 | gb.colors(&self.colors); 59 | gb.domain(&self.pos); 60 | Some(gb) 61 | } 62 | } 63 | 64 | pub fn parse_svg(s: &str, target_id: Option<&str>) -> Vec { 65 | let mut res = Vec::new(); 66 | let mut index = 0; 67 | let mut prev_pos = f32::NEG_INFINITY; 68 | let mut inside = false; 69 | let mut skip = false; 70 | 71 | for event in svg::read(s).unwrap() { 72 | match event { 73 | Event::Tag(svg_tag::LinearGradient, t, attributes) 74 | | Event::Tag(svg_tag::RadialGradient, t, attributes) => match t { 75 | svg_tag::Type::Start => { 76 | let id = attributes.get("id").map(|v| v.to_string()); 77 | skip = match (id.as_ref(), target_id) { 78 | (Some(a), Some(b)) => a != b, 79 | (None, Some(_)) => true, 80 | _ => false, 81 | }; 82 | if skip { 83 | continue; 84 | } 85 | inside = true; 86 | res.push(SvgGradient { 87 | id, 88 | colors: Vec::new(), 89 | pos: Vec::new(), 90 | valid: true, 91 | }); 92 | } 93 | 94 | svg_tag::Type::End => { 95 | if inside && !skip { 96 | index += 1; 97 | } 98 | inside = false; 99 | prev_pos = f32::NEG_INFINITY; 100 | } 101 | 102 | svg_tag::Type::Empty => {} 103 | }, 104 | Event::Tag(svg_tag::Stop, _, attributes) => { 105 | if !inside || skip || res.is_empty() { 106 | continue; 107 | } 108 | 109 | let mut color: Option = None; 110 | let mut opacity: Option = None; 111 | 112 | if let Some(s) = attributes.get("stop-color") { 113 | let Ok(c) = s.parse::() else { 114 | res[index].valid = false; 115 | continue; 116 | }; 117 | color = Some(c); 118 | } 119 | 120 | if let Some(s) = attributes.get("stop-opacity") { 121 | let Some(opc) = parse_percent_or_float(s) else { 122 | res[index].valid = false; 123 | continue; 124 | }; 125 | opacity = Some(opc); 126 | } 127 | 128 | if let Some(styles) = attributes.get("style") { 129 | let (col, opac) = parse_styles(styles); 130 | 131 | if let Some(s) = col { 132 | let Ok(c) = s.parse::() else { 133 | res[index].valid = false; 134 | continue; 135 | }; 136 | color = Some(c); 137 | } 138 | 139 | if let Some(s) = opac { 140 | let Some(opc) = parse_percent_or_float(s) else { 141 | res[index].valid = false; 142 | continue; 143 | }; 144 | opacity = Some(opc); 145 | } 146 | } 147 | 148 | let offset = if let Some(pos) = attributes.get("offset") { 149 | let Some(of) = parse_percent_or_float(pos) else { 150 | res[index].valid = false; 151 | continue; 152 | }; 153 | Some(of) 154 | } else { 155 | None 156 | }; 157 | 158 | let color = color.unwrap_or(Color::new(0.0, 0.0, 0.0, 1.0)); 159 | 160 | let offset = offset.unwrap_or(prev_pos); 161 | 162 | let color = if let Some(opacity) = opacity { 163 | Color::new(color.r, color.g, color.b, opacity.clamp(0.0, 1.0)) 164 | } else { 165 | color 166 | }; 167 | 168 | prev_pos = if offset.is_finite() { 169 | offset.max(prev_pos) 170 | } else { 171 | 0.0 172 | }; 173 | 174 | res[index].colors.push(color); 175 | res[index].pos.push(prev_pos); 176 | } 177 | _ => {} 178 | } 179 | } 180 | 181 | res 182 | } 183 | 184 | #[cfg(test)] 185 | mod tests { 186 | use super::*; 187 | 188 | fn colors2hex(colors: &[Color]) -> Vec { 189 | colors.iter().map(|c| c.to_css_hex()).collect() 190 | } 191 | 192 | fn str_colors2hex(colors: &[&str]) -> Vec { 193 | colors 194 | .iter() 195 | .map(|s| s.parse::().unwrap().to_css_hex()) 196 | .collect() 197 | } 198 | 199 | macro_rules! assert_gradient { 200 | ($sg:expr, $id:expr, $colors:expr, $pos:expr) => { 201 | assert_eq!($sg.id, Some($id.into())); 202 | assert_eq!(colors2hex(&$sg.colors), str_colors2hex($colors)); 203 | assert_eq!(&$sg.pos, $pos); 204 | assert!($sg.valid); 205 | }; 206 | } 207 | 208 | #[test] 209 | fn utils() { 210 | assert_eq!(parse_percent_or_float("50%"), Some(0.5)); 211 | assert_eq!(parse_percent_or_float("100%"), Some(1.0)); 212 | assert_eq!(parse_percent_or_float("1"), Some(1.0)); 213 | assert_eq!(parse_percent_or_float("0.73"), Some(0.73)); 214 | 215 | assert_eq!(parse_percent_or_float(""), None); 216 | assert_eq!(parse_percent_or_float("16g7"), None); 217 | 218 | assert_eq!( 219 | parse_styles("stop-color:#ff0; stop-opacity:0.5"), 220 | (Some("#ff0"), Some("0.5")) 221 | ); 222 | assert_eq!(parse_styles("stop-color:skyblue"), (Some("skyblue"), None)); 223 | assert_eq!(parse_styles("stop-opacity:50%"), (None, Some("50%"))); 224 | assert_eq!(parse_styles(""), (None, None)); 225 | } 226 | 227 | #[test] 228 | fn svg_parsing() { 229 | let result = parse_svg( 230 | r##" 231 | 232 | 233 | 234 | 235 | 236 | "##, 237 | None, 238 | ); 239 | assert_eq!(result.len(), 1); 240 | assert_gradient!( 241 | result[0], 242 | "banana", 243 | &["#c41189", "#00bfff", "#ffd700"], 244 | &[0.0, 0.5, 1.0] 245 | ); 246 | 247 | // Using percentage 248 | let result = parse_svg( 249 | r##" 250 | 251 | 252 | 253 | 254 | 255 | "##, 256 | None, 257 | ); 258 | assert_eq!(result.len(), 1); 259 | assert_gradient!( 260 | result[0], 261 | "apple", 262 | &["deeppink", "gold", "seagreen"], 263 | &[0.0, 0.5, 1.0] 264 | ); 265 | 266 | // radialGradient tag 267 | let result = parse_svg( 268 | r##" 269 | 270 | 271 | 272 | 273 | 274 | "##, 275 | None, 276 | ); 277 | assert_eq!(result.len(), 1); 278 | assert_gradient!( 279 | result[0], 280 | "mango", 281 | &["deeppink", "gold", "seagreen"], 282 | &[0.0, 0.5, 1.0] 283 | ); 284 | 285 | fn set_alpha(col: &str, alpha: f32) -> String { 286 | let c = col.parse::().unwrap(); 287 | Color::new(c.r, c.g, c.b, alpha).to_css_hex() 288 | } 289 | 290 | // Using style attribute 291 | let result = parse_svg( 292 | r##" 293 | 294 | 295 | 296 | 297 | 298 | "##, 299 | None, 300 | ); 301 | assert_eq!(result.len(), 1); 302 | assert_gradient!( 303 | result[0], 304 | "papaya", 305 | &["tomato", &set_alpha("gold", 0.9), "steelblue"], 306 | &[0.0, 0.5, 1.0] 307 | ); 308 | 309 | // Multiple gradients 310 | let result = parse_svg( 311 | r##" 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | "##, 326 | None, 327 | ); 328 | assert_eq!(result.len(), 2); 329 | assert_gradient!( 330 | result[0], 331 | "gradient-1", 332 | &["#c4114d", "#6268a6", "#57cf4f", "#ffe04d"], 333 | &[0.0, 0.5, 0.5, 1.0] 334 | ); 335 | assert_gradient!( 336 | result[1], 337 | "gradient-2", 338 | &["#c4114d", "#6268a6", "#57cf4f", "#ffe04d"], 339 | &[0.0, 0.5, 0.5, 1.0] 340 | ); 341 | 342 | // Incomplete stop attributes 343 | let result = parse_svg( 344 | r##" 345 | 346 | 347 | 348 | 349 | 350 | "##, 351 | None, 352 | ); 353 | assert_eq!(result.len(), 1); 354 | assert_gradient!( 355 | result[0], 356 | "g4657", 357 | &["black", "gold", "steelblue"], 358 | &[0.0, 0.7, 0.7] 359 | ); 360 | } 361 | 362 | #[test] 363 | fn filter_by_id() { 364 | let s = r##" 365 | 366 | 367 | 368 | 369 | 370 | 371 | 372 | 373 | 374 | 375 | 376 | 377 | 378 | 379 | 380 | 381 | 382 | 383 | 384 | 385 | 386 | 387 | 388 | "##; 389 | 390 | let result = parse_svg(s, None); 391 | assert_eq!(result.len(), 5); 392 | 393 | let result = parse_svg(s, Some("guava")); 394 | assert_eq!(result.len(), 2); 395 | assert_gradient!( 396 | result[0], 397 | "guava", 398 | &["#ff0000", "#00ff00", "#0000ff"], 399 | &[0.1, 0.5, 0.9] 400 | ); 401 | assert_gradient!(result[1], "guava", &["#abc123"], &[0.35]); 402 | 403 | let result = parse_svg(s, Some("avocado")); 404 | assert_eq!(result.len(), 1); 405 | assert_gradient!( 406 | result[0], 407 | "avocado", 408 | &["#ffff00", "#ff00ff", "#00ffff"], 409 | &[0.2, 0.4, 0.8] 410 | ); 411 | 412 | let result = parse_svg(s, Some("")); 413 | assert_eq!(result.len(), 1); 414 | assert_gradient!(result[0], "", &["#123456"], &[1.0]); 415 | 416 | let result = parse_svg(s, Some("pineapple")); 417 | assert!(result.is_empty()); 418 | } 419 | 420 | #[test] 421 | fn malformed_gradients() { 422 | let result = parse_svg( 423 | r##" 424 | 425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | 434 | 435 | 436 | 437 | 438 | 439 | 440 | 441 | 442 | 443 | 444 | 445 | 446 | 447 | 448 | 449 | 450 | 451 | 452 | "##, 453 | None, 454 | ); 455 | assert_eq!(result.len(), 4); 456 | assert_gradient!(result[0], "empty", &[], &[]); 457 | assert_gradient!( 458 | result[1], 459 | "empty-stops", 460 | &["black", "black", "black"], 461 | &[0.0, 0.0, 0.0] 462 | ); 463 | assert_gradient!( 464 | result[2], 465 | "without-color", 466 | &["black", "black", "black"], 467 | &[0.0, 0.75, 1.0] 468 | ); 469 | assert_gradient!( 470 | result[3], 471 | "without-offset", 472 | &["red", "lime", "blue"], 473 | &[0.0, 0.0, 0.0] 474 | ); 475 | } 476 | 477 | #[test] 478 | fn invalid_gradients() { 479 | let result = parse_svg( 480 | r##" 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 509 | 510 | 511 | 512 | "##, 513 | None, 514 | ); 515 | assert_eq!(result.len(), 6); 516 | for g in &result { 517 | assert_eq!(g.valid, false); 518 | assert_eq!(g.id, None); 519 | } 520 | } 521 | } 522 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | use std::ffi::OsStr; 2 | use std::fs::File; 3 | use std::io::{self, BufReader, IsTerminal, Read, Write}; 4 | use std::process::exit; 5 | 6 | use colorgrad::{Color, GimpGradient, Gradient}; 7 | use csscolorparser::NAMED_COLORS; 8 | 9 | mod cli; 10 | use cli::{BlendMode, Interpolation, Opt, OutputColor, PRESET_NAMES}; 11 | 12 | mod svg_gradient; 13 | 14 | mod util; 15 | use util::bold; 16 | 17 | #[derive(PartialEq)] 18 | enum OutputMode { 19 | Gradient, 20 | ColorsN, 21 | ColorsSample, 22 | } 23 | 24 | struct GradientApp<'a> { 25 | opt: Opt, 26 | stdout: io::BufWriter>, 27 | is_terminal: bool, 28 | output_mode: OutputMode, 29 | output_format: OutputColor, 30 | use_solid_bg: bool, 31 | background: Color, 32 | cb_color: [Color; 2], 33 | term_width: usize, 34 | width: usize, 35 | height: usize, 36 | } 37 | 38 | impl GradientApp<'_> { 39 | fn new(opt: Opt, stdout: io::Stdout) -> Self { 40 | let term_width = if let Some((terminal_size::Width(w), _)) = terminal_size::terminal_size() 41 | { 42 | Some(w as usize) 43 | } else { 44 | None 45 | }; 46 | 47 | let background = if let Some(ref c) = opt.background { 48 | c.clone() 49 | } else { 50 | Color::new(0.0, 0.0, 0.0, 1.0) 51 | }; 52 | 53 | let cb_color = if let Some(ref c) = opt.cb_color { 54 | c.clone() 55 | } else { 56 | [ 57 | Color::new(0.05, 0.05, 0.05, 1.0), 58 | Color::new(0.20, 0.20, 0.20, 1.0), 59 | ] 60 | }; 61 | 62 | let width = opt 63 | .width 64 | .unwrap_or_else(|| term_width.unwrap_or(80)) 65 | .max(10) 66 | .min(term_width.unwrap_or(1000)); 67 | 68 | let output_mode = if opt.take.is_some() { 69 | OutputMode::ColorsN 70 | } else if opt.sample.is_some() { 71 | OutputMode::ColorsSample 72 | } else { 73 | OutputMode::Gradient 74 | }; 75 | 76 | let is_terminal = stdout.is_terminal(); 77 | 78 | Self { 79 | output_mode, 80 | stdout: io::BufWriter::new(stdout.lock()), 81 | is_terminal, 82 | use_solid_bg: opt.background.is_some(), 83 | background, 84 | cb_color, 85 | term_width: term_width.unwrap_or(80), 86 | width, 87 | height: opt.height.unwrap_or(2).clamp(1, 50), 88 | output_format: opt.format.unwrap_or(OutputColor::Hex), 89 | opt, 90 | } 91 | } 92 | 93 | fn run(&mut self) -> io::Result { 94 | let result = if self.opt.list_presets { 95 | // List all preset gradients 96 | self.width = self.term_width.min(80); 97 | self.height = 2; 98 | for name in PRESET_NAMES { 99 | writeln!(self.stdout, "{name}")?; 100 | self.opt.preset = Some(name.into()); 101 | self.preset_gradient()?; 102 | } 103 | Ok(0) 104 | } else if self.opt.named_colors { 105 | // List all CSS named colors 106 | for (&name, &[r, g, b]) in NAMED_COLORS.entries() { 107 | writeln!( 108 | self.stdout, 109 | "\x1B[48;2;{r};{g};{b}m \x1B[49;38;2;{r};{g};{b}m #{r:02x}{g:02x}{b:02x}\x1B[39m {name}" 110 | )?; 111 | } 112 | Ok(0) 113 | } else if self.opt.preset.is_some() { 114 | self.preset_gradient() 115 | } else if self.opt.custom.is_some() || self.opt.css.is_some() { 116 | self.custom_gradient() 117 | } else if self.opt.file.is_some() { 118 | self.file_gradient() 119 | } else { 120 | self.example_help()?; 121 | Ok(1) 122 | }; 123 | 124 | self.stdout.flush()?; 125 | result 126 | } 127 | 128 | fn preset_gradient(&mut self) -> io::Result { 129 | use colorgrad::preset::*; 130 | 131 | fn cubehelix() -> CubehelixGradient { 132 | cubehelix_default() 133 | } 134 | 135 | macro_rules! presets { 136 | ($($name:ident),*) => { 137 | match self 138 | .opt 139 | .preset 140 | .as_ref() 141 | .unwrap() 142 | .to_lowercase() 143 | .replace('-', "_") 144 | .as_ref() 145 | { 146 | $(stringify!($name) => self.handle_output(&$name())?,)* 147 | _ => { 148 | writeln!(io::stderr(), "Error: Invalid preset gradient name. Use -l flag to list all preset gradient names.")?; 149 | return Ok(1); 150 | } 151 | } 152 | } 153 | } 154 | 155 | presets!( 156 | blues, br_bg, bu_gn, bu_pu, cividis, cool, cubehelix, gn_bu, greens, greys, inferno, 157 | magma, or_rd, oranges, pi_yg, plasma, pr_gn, pu_bu, pu_bu_gn, pu_or, pu_rd, purples, 158 | rainbow, rd_bu, rd_gy, rd_pu, rd_yl_bu, rd_yl_gn, reds, sinebow, spectral, turbo, 159 | viridis, warm, yl_gn, yl_gn_bu, yl_or_br, yl_or_rd 160 | ); 161 | 162 | Ok(0) 163 | } 164 | 165 | fn custom_gradient(&mut self) -> io::Result { 166 | let mut gb = colorgrad::GradientBuilder::new(); 167 | 168 | if let Some(ref css_gradient) = self.opt.css { 169 | gb.css(css_gradient); 170 | } else { 171 | gb.colors(self.opt.custom.as_ref().unwrap()); 172 | 173 | if let Some(ref pos) = self.opt.position { 174 | gb.domain(pos); 175 | } 176 | } 177 | 178 | gb.mode(match self.opt.blend_mode { 179 | Some(BlendMode::Rgb) => colorgrad::BlendMode::Rgb, 180 | Some(BlendMode::LinearRgb) => colorgrad::BlendMode::LinearRgb, 181 | Some(BlendMode::Lab) => colorgrad::BlendMode::Lab, 182 | _ => colorgrad::BlendMode::Oklab, 183 | }); 184 | 185 | match self.opt.interpolation { 186 | Some(Interpolation::Linear) => match gb.build::() { 187 | Ok(g) => self.handle_output(&g)?, 188 | Err(e) => { 189 | writeln!(io::stderr(), "Custom gradient error: {e}")?; 190 | return Ok(1); 191 | } 192 | }, 193 | Some(Interpolation::Basis) => match gb.build::() { 194 | Ok(g) => self.handle_output(&g)?, 195 | Err(e) => { 196 | writeln!(io::stderr(), "Custom gradient error: {e}")?; 197 | return Ok(1); 198 | } 199 | }, 200 | _ => match gb.build::() { 201 | Ok(g) => self.handle_output(&g)?, 202 | Err(e) => { 203 | writeln!(io::stderr(), "Custom gradient error: {e}")?; 204 | return Ok(1); 205 | } 206 | }, 207 | }; 208 | 209 | Ok(0) 210 | } 211 | 212 | fn file_gradient(&mut self) -> io::Result { 213 | use colorgrad::{BasisGradient, CatmullRomGradient, LinearGradient}; 214 | 215 | let ggr_bg_color = if let Some(ref c) = self.opt.ggr_bg { 216 | c.clone() 217 | } else { 218 | Color::new(1.0, 1.0, 1.0, 1.0) 219 | }; 220 | 221 | let ggr_fg_color = if let Some(ref c) = self.opt.ggr_fg { 222 | c.clone() 223 | } else { 224 | Color::new(0.0, 0.0, 0.0, 1.0) 225 | }; 226 | 227 | let show_info = self.is_terminal || self.output_mode == OutputMode::Gradient; 228 | let mut status = 0; 229 | 230 | for path in self.opt.file.as_ref().unwrap().clone() { 231 | if !path.exists() { 232 | eprintln!("{}: file not found.", &path.display()); 233 | status = 1; 234 | continue; 235 | } 236 | 237 | let Some(ext) = path.extension().and_then(OsStr::to_str) else { 238 | eprintln!("{}: file format not supported.", &path.display()); 239 | status = 1; 240 | continue; 241 | }; 242 | 243 | let ext = ext.to_lowercase(); 244 | 245 | if &ext == "ggr" { 246 | match GimpGradient::new( 247 | BufReader::new(File::open(&path)?), 248 | &ggr_fg_color, 249 | &ggr_bg_color, 250 | ) { 251 | Ok(grad) => { 252 | if show_info { 253 | writeln!(self.stdout, "{} {}", &path.display(), bold(grad.name()))?; 254 | } 255 | self.handle_output(&grad)?; 256 | } 257 | Err(_) => { 258 | eprintln!("{} (invalid GIMP gradient)", &path.display()); 259 | status = 1; 260 | continue; 261 | } 262 | } 263 | } else if &ext == "svg" { 264 | let mut file = File::open(&path)?; 265 | let mut content = String::new(); 266 | file.read_to_string(&mut content)?; 267 | let svg_grads = svg_gradient::parse_svg(&content, self.opt.svg_id.as_deref()); 268 | 269 | let cmode = match self.opt.blend_mode { 270 | Some(BlendMode::Rgb) => colorgrad::BlendMode::Rgb, 271 | Some(BlendMode::LinearRgb) => colorgrad::BlendMode::LinearRgb, 272 | Some(BlendMode::Lab) => colorgrad::BlendMode::Lab, 273 | _ => colorgrad::BlendMode::Oklab, 274 | }; 275 | let mut valid = 0; 276 | let mut invalid = 0; 277 | 278 | for mut sg in svg_grads { 279 | assert_eq!(sg.colors.len(), sg.pos.len()); 280 | 281 | let id = sg 282 | .id 283 | .as_ref() 284 | .map(|s| format!("#{s}")) 285 | .unwrap_or("[without id]".into()); 286 | 287 | let Some(mut gb) = sg.gradient_builder() else { 288 | eprintln!("{} {} (invalid gradient)", &path.display(), bold(&id)); 289 | status = 1; 290 | invalid += 1; 291 | continue; 292 | }; 293 | 294 | if show_info { 295 | writeln!(self.stdout, "{} {}", &path.display(), bold(&id))?; 296 | } 297 | 298 | gb.mode(cmode); 299 | 300 | match self.opt.interpolation { 301 | Some(Interpolation::Linear) => { 302 | let g: LinearGradient = gb.build().unwrap(); 303 | self.handle_output(&g)?; 304 | } 305 | Some(Interpolation::Basis) => { 306 | let g: BasisGradient = gb.build().unwrap(); 307 | self.handle_output(&g)?; 308 | } 309 | _ => { 310 | let g: CatmullRomGradient = gb.build().unwrap(); 311 | self.handle_output(&g)?; 312 | } 313 | } 314 | valid += 1; 315 | } 316 | 317 | if valid == 0 && invalid == 0 { 318 | if self.opt.svg_id.is_some() { 319 | eprintln!("{} -- (nothing matched)", &path.display(),); 320 | } else { 321 | eprintln!("{} -- (no gradients found)", &path.display()); 322 | } 323 | status = 1; 324 | } 325 | } else { 326 | eprintln!("{}: file format not supported.", &path.display()); 327 | status = 1; 328 | } 329 | } 330 | 331 | Ok(status) 332 | } 333 | 334 | fn handle_output(&mut self, grad: &dyn Gradient) -> io::Result { 335 | match self.output_mode { 336 | OutputMode::Gradient => self.display_gradient(grad), 337 | 338 | OutputMode::ColorsN => { 339 | let mut colors = grad.colors(self.opt.take.unwrap()); 340 | if self.use_solid_bg { 341 | for col in &mut colors { 342 | util::blend_on(col, &self.background); 343 | } 344 | } 345 | self.display_colors(&colors) 346 | } 347 | 348 | OutputMode::ColorsSample => { 349 | let colors: Vec<_> = self 350 | .opt 351 | .sample 352 | .as_ref() 353 | .unwrap() 354 | .iter() 355 | .map(|t| { 356 | let mut c = grad.at(*t).clamp(); 357 | if self.use_solid_bg { 358 | util::blend_on(&mut c, &self.background); 359 | } 360 | c 361 | }) 362 | .collect(); 363 | self.display_colors(&colors) 364 | } 365 | } 366 | } 367 | 368 | fn display_gradient(&mut self, grad: &dyn Gradient) -> io::Result { 369 | let colors = grad.colors(self.width * 2); 370 | 371 | for y in 0..self.height { 372 | for (x, cols) in colors.chunks_exact(2).enumerate() { 373 | let bg_color = if self.use_solid_bg { 374 | &self.background 375 | } else if ((x / 2) & 1) ^ (y & 1) == 0 { 376 | &self.cb_color[0] 377 | } else { 378 | &self.cb_color[1] 379 | }; 380 | 381 | let [a, b, c, _] = util::blend_color(&cols[0], bg_color).to_rgba8(); 382 | let [d, e, f, _] = util::blend_color(&cols[1], bg_color).to_rgba8(); 383 | 384 | write!( 385 | self.stdout, 386 | "\x1B[38;2;{a};{b};{c};48;2;{d};{e};{f}m\u{258C}", 387 | )?; 388 | } 389 | 390 | writeln!(self.stdout, "\x1B[39;49m")?; 391 | } 392 | 393 | Ok(0) 394 | } 395 | 396 | fn display_colors(&mut self, colors: &[Color]) -> io::Result { 397 | if self.opt.array { 398 | let f = self.output_format; 399 | let cols: Vec<_> = colors.iter().map(|c| util::format_color(c, f)).collect(); 400 | writeln!(self.stdout, "{cols:?}")?; 401 | return Ok(0); 402 | } 403 | 404 | if self.is_terminal { 405 | if self.output_format != OutputColor::Hex { 406 | for col in colors { 407 | writeln!( 408 | self.stdout, 409 | "{} {}", 410 | util::color_to_ansi(col, &self.cb_color, 7), 411 | util::format_color(col, self.output_format) 412 | )?; 413 | } 414 | return Ok(0); 415 | } 416 | 417 | let mut buff0 = "".to_string(); 418 | let mut buff1 = "".to_string(); 419 | let last = colors.len() - 1; 420 | let mut w = 0; 421 | 422 | for (i, col) in colors.iter().enumerate() { 423 | let hex = util::format_color(col, self.output_format); 424 | let wc = hex.len(); 425 | buff0.push_str(&util::color_to_ansi(col, &self.cb_color, wc)); 426 | buff1.push_str(&hex); 427 | w += wc; 428 | if w < self.term_width { 429 | buff0.push(' '); 430 | buff1.push(' '); 431 | w += 1; 432 | } 433 | let nwc = if i == last { 434 | 0 435 | } else { 436 | util::format_color(&colors[i + 1], self.output_format).len() 437 | }; 438 | 439 | if w + nwc > self.term_width || i == last { 440 | writeln!(self.stdout, "{buff0}\n{buff1}")?; 441 | buff0.clear(); 442 | buff1.clear(); 443 | w = 0; 444 | } 445 | } 446 | return Ok(0); 447 | } 448 | 449 | for col in colors { 450 | writeln!( 451 | self.stdout, 452 | "{}", 453 | util::format_color(col, self.output_format) 454 | )?; 455 | } 456 | 457 | Ok(0) 458 | } 459 | 460 | fn example_help(&mut self) -> io::Result { 461 | fn parse_colors(colors: &[&str]) -> Vec { 462 | colors 463 | .iter() 464 | .map(|s| Color::from_html(s).unwrap()) 465 | .collect() 466 | } 467 | 468 | let prompt = "\u{21AA} "; 469 | self.width = self.term_width.min(80); 470 | self.height = 2; 471 | 472 | writeln!( 473 | self.stdout, 474 | "{} Specify gradient using --preset, --custom, --css or --file\n", 475 | bold("INFO:") 476 | )?; 477 | writeln!(self.stdout, "{}", bold("EXAMPLES:"))?; 478 | writeln!(self.stdout, "{prompt} gradient --preset rainbow")?; 479 | self.opt.preset = Some("rainbow".into()); 480 | self.preset_gradient()?; 481 | 482 | writeln!( 483 | self.stdout, 484 | "{prompt} gradient --custom C41189 'rgb(0,191,255)' gold 'hsv(91,88%,50%)'" 485 | )?; 486 | self.opt.preset = None; 487 | self.opt.custom = Some(parse_colors(&[ 488 | "C41189", 489 | "rgb(0,191,255)", 490 | "gold", 491 | "hsv(91,88%,50%)", 492 | ])); 493 | self.custom_gradient()?; 494 | 495 | writeln!(self.stdout, "{prompt} gradient --css 'white, 25%, blue'")?; 496 | self.opt.custom = None; 497 | self.opt.css = Some("white, 25%, blue".into()); 498 | self.custom_gradient()?; 499 | 500 | writeln!( 501 | self.stdout, 502 | "{prompt} gradient --file Test.svg Neon_Green.ggr" 503 | )?; 504 | writeln!(self.stdout, "Test.svg {}", bold("#purple-gradient"))?; 505 | self.opt.css = None; 506 | self.opt.custom = Some(parse_colors(&["4a1578", "c5a8de"])); 507 | self.custom_gradient()?; 508 | 509 | writeln!(self.stdout, "Neon_Green.ggr {}", bold("Neon Green"))?; 510 | const GGR_SAMPLE: &str = include_str!("../data/Neon_Green.ggr"); 511 | let color = Color::default(); 512 | let br = BufReader::new(GGR_SAMPLE.as_bytes()); 513 | let grad = GimpGradient::new(br, &color, &color).unwrap(); 514 | self.display_gradient(&grad)?; 515 | 516 | writeln!(self.stdout, "{prompt} gradient --preset viridis --take 10")?; 517 | self.opt.custom = None; 518 | self.opt.preset = Some("viridis".into()); 519 | self.opt.take = Some(10); 520 | self.output_mode = OutputMode::ColorsN; 521 | self.preset_gradient()?; 522 | 523 | Ok(0) 524 | } 525 | } 526 | 527 | fn main() { 528 | let opt = match cli::parse_args() { 529 | Ok(opt) => opt, 530 | Err(err) => { 531 | eprintln!("Error: {err}."); 532 | exit(1); 533 | } 534 | }; 535 | 536 | let mut ga = GradientApp::new(opt, io::stdout()); 537 | 538 | match ga.run() { 539 | Ok(exit_code) => { 540 | exit(exit_code); 541 | } 542 | 543 | Err(err) if err.kind() == io::ErrorKind::BrokenPipe => { 544 | exit(0); 545 | } 546 | 547 | Err(err) => { 548 | eprintln!("{err}"); 549 | exit(1); 550 | } 551 | } 552 | } 553 | --------------------------------------------------------------------------------