aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--Cargo.lock16
-rw-r--r--Cargo.toml8
-rw-r--r--src/color.rs101
-rw-r--r--src/main.rs225
5 files changed, 351 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/Cargo.lock b/Cargo.lock
new file mode 100644
index 0000000..204d819
--- /dev/null
+++ b/Cargo.lock
@@ -0,0 +1,16 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "anyhow"
+version = "1.0.57"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc"
+
+[[package]]
+name = "blubcat"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+]
diff --git a/Cargo.toml b/Cargo.toml
new file mode 100644
index 0000000..1ac9ea0
--- /dev/null
+++ b/Cargo.toml
@@ -0,0 +1,8 @@
+[package]
+name = "blubcat"
+version = "0.1.0"
+edition = "2021"
+
+
+[dependencies]
+anyhow = "1.0.57"
diff --git a/src/color.rs b/src/color.rs
new file mode 100644
index 0000000..6ade373
--- /dev/null
+++ b/src/color.rs
@@ -0,0 +1,101 @@
+#[derive(Debug, Clone, Copy, PartialEq)]
+pub struct Color {
+ pub r: f64,
+ pub g: f64,
+ pub b: f64,
+}
+
+impl Color {
+ pub fn rgb(r: f64, g: f64, b: f64) -> Self {
+ Self { r, g, b }
+ }
+
+ pub fn parse(c: &str) -> Option<Self> {
+ match c {
+ "red" => Some(Color::RED),
+ "green" => Some(Color::GREEN),
+ "blue" => Some(Color::BLUE),
+ "yellow" => Some(Color::YELLOW),
+ "cyan" => Some(Color::CYAN),
+ "magenta" => Some(Color::MAGENTA),
+ "black" => Some(Color::BLACK),
+ "white" => Some(Color::WHITE),
+ _ => {
+ if c.len() == 6 {
+ Some(Self::from_rgb888(
+ u8::from_str_radix(&c[0..2], 16).ok()?,
+ u8::from_str_radix(&c[2..4], 16).ok()?,
+ u8::from_str_radix(&c[4..6], 16).ok()?,
+ ))
+ } else {
+ None
+ }
+ }
+ }
+ }
+
+ pub fn as_rgb888(&self) -> (u8, u8, u8) {
+ (
+ (self.r * 255.0) as u8,
+ (self.g * 255.0) as u8,
+ (self.b * 255.0) as u8,
+ )
+ }
+ pub fn from_rgb888(r: u8, g: u8, b: u8) -> Self {
+ Color {
+ r: r as f64 / 255.0,
+ g: g as f64 / 255.0,
+ b: b as f64 / 255.0,
+ }
+ }
+ pub const RED: Color = Color {
+ r: 1.0,
+ g: 0.0,
+ b: 0.0,
+ };
+ pub const GREEN: Color = Color {
+ r: 0.0,
+ g: 1.0,
+ b: 0.0,
+ };
+ pub const BLUE: Color = Color {
+ r: 0.0,
+ g: 0.0,
+ b: 1.0,
+ };
+ pub const YELLOW: Color = Color {
+ r: 1.0,
+ g: 1.0,
+ b: 0.0,
+ };
+ pub const CYAN: Color = Color {
+ r: 0.0,
+ g: 1.0,
+ b: 1.0,
+ };
+ pub const MAGENTA: Color = Color {
+ r: 1.0,
+ g: 0.0,
+ b: 1.0,
+ };
+
+ pub const BLACK: Color = Color {
+ r: 0.0,
+ g: 0.0,
+ b: 0.0,
+ };
+ pub const WHITE: Color = Color {
+ r: 1.0,
+ g: 1.0,
+ b: 1.0,
+ };
+
+ pub fn mix(a: Color, b: Color, x: f64) -> Color {
+ let y = 1.0 - x;
+ Color {
+ r: a.r * y + b.r * x,
+ g: a.g * y + b.g * x,
+ b: a.b * y + b.b * x,
+ }
+ }
+}
diff --git a/src/main.rs b/src/main.rs
new file mode 100644
index 0000000..757b842
--- /dev/null
+++ b/src/main.rs
@@ -0,0 +1,225 @@
+#![feature(box_syntax)]
+pub mod color;
+
+use anyhow::Result;
+use color::Color;
+use std::{
+ env,
+ f64::consts::PI,
+ fs::File,
+ io::{stdin, BufRead, BufReader, Read},
+ process::exit,
+};
+
+static HELP: &str = "
+Usage: blubcat PATTERN [FILE]...
+Concatenate FILE(s) to standard output, colorizing the output according to OPTION(s).
+
+With no FILE, or when FILE is -, the standard input is read.
+If PATTERN is repeated only the last one will be used.
+
+PATTERN:
+ -R --rainbow Rainbow
+ -S --sequence <c1,c2,c3,...> Sequences colors on the X-axis
+ -G --gradient <c1,c2,c3,...> Interpolates the colors on the X-axis
+ <PATTERN> <TRANSFORM> Apply TRANSFORM to PATTERN
+ <PRESET>
+
+TRANSFORM:
+ -r --rotate <angle>
+ -s --scale <factor>
+ -m --matrix <x> <y> <z> <w>
+
+PRESET:
+ -P --pride Pride flag
+
+COLOR:
+ <rrggbb>
+ black white red green blue yellow cyan magenta
+
+";
+
+fn main() -> Result<()> {
+ let mut args = env::args().skip(1);
+
+ let mut pat: Box<dyn Sample2D> = box Solid(Color::WHITE);
+
+ loop {
+ let a = args.next();
+ let mut arg_next = || args.next();
+ let mut arg_num = || {
+ arg_next()
+ .expect("value expected")
+ .parse::<f64>()
+ .expect("not a number")
+ };
+
+ if let Some(a) = a {
+ if !a.starts_with("-") || a == "--" || a == "-" {
+ break;
+ }
+ match a.as_str() {
+ "-?" | "--help" => {
+ println!("{}", HELP);
+ exit(0)
+ }
+
+ /* PRESETS */
+ "-P" | "--pride" => {
+ pat = box Transform {
+ matrix: ((1.0 / 12.0, 1.0 / 12.0), (0.0, 0.0)),
+ inner: box Extend(box Sequence(vec![
+ Color::parse("e50000").unwrap(),
+ Color::parse("ff8d00").unwrap(),
+ Color::parse("ffee00").unwrap(),
+ Color::parse("028121").unwrap(),
+ Color::parse("004cff").unwrap(),
+ Color::parse("770088").unwrap(),
+ ])),
+ }
+ }
+
+ /* PATTERNS */
+ "-R" | "--rainbow" => pat = box Extend(box Rainbow),
+ "-S" | "--sequence" => {
+ pat = box Extend(box Sequence(
+ arg_next()
+ .unwrap()
+ .split(",")
+ .map(|e| Color::parse(e).expect("color invalid"))
+ .collect::<Vec<_>>(),
+ ))
+ }
+ "-G" | "--gradient" => {
+ pat = box Extend(box Gradient(
+ arg_next()
+ .unwrap()
+ .split(",")
+ .map(|e| Color::parse(e).expect("color invalid"))
+ .collect::<Vec<_>>(),
+ ))
+ }
+
+ /* TRANSFORMS */
+ "-s" | "--scale" => {
+ let fac = arg_num();
+ pat = box Transform {
+ inner: pat,
+ matrix: ((fac, 0.0), (0.0, fac)),
+ }
+ }
+ "-r" | "--rotate" => {
+ let angle = arg_num() * PI * 2.0;
+ pat = box Transform {
+ inner: pat,
+ matrix: ((angle.cos(), -angle.sin()), (angle.sin(), angle.cos())),
+ }
+ }
+ "-m" | "--matrix" => {
+ pat = box Transform {
+ inner: pat,
+ matrix: ((arg_num(), arg_num()), (arg_num(), arg_num())),
+ }
+ }
+ _ => panic!("unknown option {}", &a),
+ }
+ } else {
+ break;
+ }
+ }
+
+ let inputs = args.collect::<Vec<String>>();
+
+ if inputs.len() == 0 {
+ colorize(stdin(), &mut pat)?
+ } else {
+ for f in inputs {
+ let file = File::open(f)?;
+ colorize(file, &mut pat)?;
+ }
+ }
+ Ok(())
+}
+
+fn colorize(file: impl Read, sampler: &mut Box<dyn Sample2D>) -> Result<()> {
+ let mut file = BufReader::new(file);
+ let mut line = String::new();
+ for y in 0.. {
+ if file.read_line(&mut line)? == 0 {
+ break;
+ }
+ for (x, c) in line.chars().enumerate() {
+ let (r, g, b) = sampler.sample(x as f64, y as f64).as_rgb888();
+ print!("\x1b[38;2;{r};{g};{b}m{c}")
+ }
+ line.clear();
+ }
+ Ok(())
+}
+
+trait Sample2D {
+ fn sample(&mut self, x: f64, y: f64) -> Color;
+}
+trait Sample1D {
+ fn sample(&mut self, x: f64) -> Color;
+}
+
+struct Extend(Box<dyn Sample1D>);
+impl Sample2D for Extend {
+ fn sample(&mut self, x: f64, _y: f64) -> Color {
+ self.0.sample(x)
+ }
+}
+
+struct Transform {
+ pub inner: Box<dyn Sample2D>,
+ pub matrix: ((f64, f64), (f64, f64)),
+}
+impl Sample2D for Transform {
+ fn sample(&mut self, x: f64, y: f64) -> Color {
+ let (x, y) = (
+ x * self.matrix.0 .0 + y * self.matrix.0 .1,
+ x * self.matrix.1 .0 + y * self.matrix.1 .1,
+ );
+ self.inner.sample(x, y)
+ }
+}
+
+struct Rainbow;
+impl Sample1D for Rainbow {
+ fn sample(&mut self, x: f64) -> Color {
+ static PI: f64 = 3.14;
+ let x = x * PI * 2.0;
+ return Color {
+ r: (x + PI / 3.0 * 0.0).sin() * 0.5 + 0.5,
+ g: (x + PI / 3.0 * 2.0).sin() * 0.5 + 0.5,
+ b: (x + PI / 3.0 * 4.0).sin() * 0.5 + 0.5,
+ };
+ }
+}
+
+struct Sequence(pub Vec<Color>);
+impl Sample1D for Sequence {
+ fn sample(&mut self, x: f64) -> Color {
+ self.0[(x * self.0.len() as f64).floor() as usize % self.0.len()]
+ }
+}
+
+struct Gradient(pub Vec<Color>);
+impl Sample1D for Gradient {
+ fn sample(&mut self, x: f64) -> Color {
+ let index = x * self.0.len() as f64;
+ let index_int = index.floor() as usize;
+ let index_error = index % 1.0;
+ let a = self.0[index_int % self.0.len()];
+ let b = self.0[(index_int + 1) % self.0.len()];
+ Color::mix(a, b, index_error)
+ }
+}
+
+struct Solid(Color);
+impl Sample2D for Solid {
+ fn sample(&mut self, _x: f64, _y: f64) -> Color {
+ self.0
+ }
+}