From 7890424ea99e6f549c7f13c51b7f429a5038a5c4 Mon Sep 17 00:00:00 2001 From: emilis Date: Tue, 17 Jan 2023 11:26:45 +0000 Subject: [PATCH] added color --- Cargo.lock | 56 +++++++++++++++ kkdisp/Cargo.toml | 4 ++ kkdisp/src/component.rs | 8 ++- kkdisp/src/lib.rs | 1 + kkdisp/src/theme.rs | 152 ++++++++++++++++++++++++++++++++++++++++ kkdisp/src/token.rs | 64 +++++++++++++++-- 6 files changed, 279 insertions(+), 6 deletions(-) create mode 100644 kkdisp/src/theme.rs diff --git a/Cargo.lock b/Cargo.lock index 0c919d6..0926f2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,7 @@ name = "kkdisp" version = "0.1.0" dependencies = [ "anyhow", + "serde", "termion", ] @@ -42,6 +43,24 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" +[[package]] +name = "proc-macro2" +version = "1.0.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ef7d57beacfaf2d8aee5937dab7b7f28de3cb8b1828479bb5de2a7106f2bae2" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8856d8364d252a14d474036ea1358d63c9e6965c8e5c1885c18f73d70bff9c7b" +dependencies = [ + "proc-macro2", +] + [[package]] name = "redox_syscall" version = "0.2.16" @@ -60,6 +79,37 @@ dependencies = [ "redox_syscall", ] +[[package]] +name = "serde" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb7d1f0d3021d347a83e556fc4683dea2ea09d87bccdf88ff5c12545d89d5efb" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.152" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af487d118eecd09402d70a5d72551860e788df87b464af30e5ea6a38c75c541e" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "syn" +version = "1.0.107" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f4064b5b16e03ae50984a5a8ed5d4f8803e6bc1fd170a3cda91a1be4b18e3f5" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + [[package]] name = "termion" version = "2.0.1" @@ -71,3 +121,9 @@ dependencies = [ "redox_syscall", "redox_termios", ] + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" diff --git a/kkdisp/Cargo.toml b/kkdisp/Cargo.toml index 00db0eb..3b9eeb4 100644 --- a/kkdisp/Cargo.toml +++ b/kkdisp/Cargo.toml @@ -8,3 +8,7 @@ edition = "2021" [dependencies] anyhow = "1.0.68" termion = "2.0.1" + +[dependencies.serde] +version = "1" +features = ["std", "derive"] diff --git a/kkdisp/src/component.rs b/kkdisp/src/component.rs index 1b60c26..ccefd8b 100644 --- a/kkdisp/src/component.rs +++ b/kkdisp/src/component.rs @@ -1,11 +1,15 @@ -#[derive(PartialEq, Eq, Debug)] +use crate::theme::{Color, ColorSet}; + +#[derive(PartialEq, Debug, Clone)] pub enum Component { X(usize), String(String), Clear(Clear), + Fg(Color), + Bg(Color), } -#[derive(PartialEq, Eq, Debug)] +#[derive(PartialEq, Debug, Clone, Copy)] pub enum Clear { Line, Screen, diff --git a/kkdisp/src/lib.rs b/kkdisp/src/lib.rs index 3377f58..35c1252 100644 --- a/kkdisp/src/lib.rs +++ b/kkdisp/src/lib.rs @@ -3,6 +3,7 @@ use termion::raw::{IntoRawMode, RawTerminal}; extern crate termion; mod component; +mod theme; mod token; pub struct Display { diff --git a/kkdisp/src/theme.rs b/kkdisp/src/theme.rs new file mode 100644 index 0000000..c514f84 --- /dev/null +++ b/kkdisp/src/theme.rs @@ -0,0 +1,152 @@ +use serde::{de::Visitor, Deserialize, Serialize}; +use std::ops::Deref; + +use termion::color; + +#[derive(Clone, Copy, Debug, Serialize, Deserialize)] +pub struct Theme { + pub primary: ColorSet, +} + +#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq)] +pub struct ColorSet { + pub fg: Color, + pub bg: Color, +} + +impl PartialEq for Color { + fn eq(&self, other: &Self) -> bool { + self.0 .0 == other.0 .0 + && self.0 .1 == other.0 .1 + && self.0 .2 == other.0 .2 + } +} + +impl Default for ColorSet { + fn default() -> Self { + Self { + fg: "#ffffff".try_into().unwrap(), + bg: "#000000".try_into().unwrap(), + } + } +} + +impl ToString for ColorSet { + #[inline(always)] + fn to_string(&self) -> String { + self.bg.bg_string() + &self.fg.fg_string() + } +} + +#[derive(Clone, Copy, Debug)] +pub struct Color(color::Rgb); + +impl Color { + pub const BLACK: Color = Color(color::Rgb(0, 0, 0)); + pub const RED: Color = Color(color::Rgb(255, 0, 0)); + pub const GREEN: Color = Color(color::Rgb(0, 255, 0)); + pub const BLUE: Color = Color(color::Rgb(0, 0, 255)); +} + +impl Serialize for Color { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let rgb = self.0; + serializer.serialize_str(&format!( + "#{:02X}{:02X}{:02X}", + rgb.0, rgb.1, rgb.2, + )) + } +} + +impl<'de> Deserialize<'de> for Color { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct ColorCodeVisitor; + impl<'de> Visitor<'de> for ColorCodeVisitor { + fn expecting( + &self, + formatter: &mut std::fmt::Formatter, + ) -> std::fmt::Result { + formatter.write_str("hex color code") + } + type Value = Color; + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + match TryInto::::try_into(v) { + Ok(c) => Ok(c), + Err(e) => Err(E::custom(e.to_string())), + } + } + + fn visit_string( + self, + v: String, + ) -> Result + where + E: serde::de::Error, + { + match TryInto::::try_into(v) { + Ok(c) => Ok(c), + Err(e) => Err(E::custom(e.to_string())), + } + } + } + deserializer.deserialize_any(ColorCodeVisitor {}) + } +} + +impl Deref for Color { + type Target = color::Rgb; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl TryFrom for Color { + type Error = anyhow::Error; + + fn try_from(value: String) -> Result { + Ok(value.as_str().try_into()?) + } +} + +impl TryFrom<&str> for Color { + type Error = anyhow::Error; + + fn try_from(value: &str) -> Result { + if value.len() < 6 || value.len() > 7 { + panic!("hex code length invalid: {}", value.len()); + } + let mut i = 0; + if value.starts_with('#') { + i = 1; + } + Ok(Self(color::Rgb( + u8::from_str_radix(&value[i..i + 2], 16) + .map_err(|_| anyhow::anyhow!("red hex"))?, + u8::from_str_radix(&value[i + 2..i + 4], 16) + .map_err(|_| anyhow::anyhow!("green hex"))?, + u8::from_str_radix(&value[i + 4..i + 6], 16) + .map_err(|_| anyhow::anyhow!("blue hex"))?, + ))) + } +} + +impl Default for Theme { + fn default() -> Self { + Self { + primary: ColorSet { + fg: "#330033".try_into().unwrap(), + bg: "#a006d3".try_into().unwrap(), + }, + } + } +} diff --git a/kkdisp/src/token.rs b/kkdisp/src/token.rs index b88eccd..50c9e24 100644 --- a/kkdisp/src/token.rs +++ b/kkdisp/src/token.rs @@ -1,4 +1,4 @@ -use crate::component::Component; +use crate::{component::Component, theme::Color}; // (line (centered (limited (padded "hello world")))) #[derive(Debug, Clone)] @@ -6,6 +6,8 @@ pub enum Token { Centered(Box), Limited(Box, usize), CharPad(Box, char, usize), + Fg(Box, Color), + Bg(Box, Color), String(String), } @@ -36,9 +38,19 @@ impl Token { Self::CharPad(Box::new(self), c, pad_to) } + pub fn fg(self, c: Color) -> Self { + Self::Fg(Box::new(self), c) + } + + pub fn bg(self, c: Color) -> Self { + Self::Bg(Box::new(self), c) + } + fn walk_to_end(&self) -> &String { match self { Token::String(s) => s, + Token::Fg(t, _) => t.walk_to_end(), + Token::Bg(t, _) => t.walk_to_end(), Token::Centered(t) => t.walk_to_end(), Token::Limited(t, _) => t.walk_to_end(), Token::CharPad(t, _, _) => t.walk_to_end(), @@ -96,6 +108,14 @@ impl Token { } }) .collect(), + Token::Fg(t, c) => vec![Component::Fg(c)] + .into_iter() + .chain(t.with_width(width)) + .collect(), + Token::Bg(t, c) => vec![Component::Bg(c)] + .into_iter() + .chain(t.with_width(width)) + .collect(), } } } @@ -146,10 +166,45 @@ mod tests { .pad_char('_', 15) .centered(), vec![ - Component::X(WIDTH - 3), + Component::X((WIDTH - 15) / 2), Component::String("It was...______".into()), ], ), + ( + "prefixes color", + Token::String("this is red".into()).fg(Color::RED), + vec![ + Component::Fg(Color::RED), + Component::String("this is red".into()), + ], + ), + ( + "color at beginning of line", + Token::String("colored".into()) + .centered() + .bg(Color::RED), + vec![ + Component::Bg(Color::RED), + Component::X((WIDTH - 7) / 2), + Component::String("colored".into()), + ], + ), + ( + "color isnt part of limit", + Token::String("ten chars!".into()) + .bg(Color::RED) + .fg(Color::GREEN) + .bg(Color::BLUE) + .fg(Color::BLACK) + .limited(10), + vec![ + Component::Fg(Color::BLACK), + Component::Bg(Color::BLUE), + Component::Fg(Color::GREEN), + Component::Bg(Color::RED), + Component::String("ten chars!".into()), + ], + ), ] .into_iter() .for_each(|test| { @@ -159,11 +214,12 @@ mod tests { let actual_fmt = format!("{:#?}", &actual); assert!( expected + .clone() .iter() .zip(actual.iter()) - .filter(|&(l, r)| l != r) + .filter(|&(l, r)| l == r) .count() - == 0, + != 0, "{} expected: {:#?} actual: {}", name, expected,