Compare commits

...

3 Commits

Author SHA1 Message Date
emilis 539fa954a4 fixed tests + refactored color + added string concatenation 2023-01-17 12:40:36 +00:00
emilis 80425f9aff Support multiple strings in a token 2023-01-17 12:12:37 +00:00
emilis 7890424ea9 added color 2023-01-17 11:26:45 +00:00
6 changed files with 387 additions and 57 deletions

56
Cargo.lock generated
View File

@ -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"

View File

@ -8,3 +8,7 @@ edition = "2021"
[dependencies]
anyhow = "1.0.68"
termion = "2.0.1"
[dependencies.serde]
version = "1"
features = ["std", "derive"]

View File

@ -1,11 +1,42 @@
#[derive(PartialEq, Eq, Debug)]
use crate::theme::{Color, ColorSet};
#[derive(Eq, Debug, Clone)]
pub enum Component {
X(usize),
String(String),
Clear(Clear),
Fg(Color),
Bg(Color),
}
#[derive(PartialEq, Eq, Debug)]
impl PartialEq for Component {
fn eq(&self, other: &Self) -> bool {
match self {
Self::X(x) => match other {
Self::X(other_x) => x == other_x,
_ => false,
},
Self::String(s) => match other {
Self::String(other_s) => s == other_s,
_ => false,
},
Self::Clear(clr) => match other {
Self::Clear(other_clr) => clr == other_clr,
_ => false,
},
Self::Fg(c) => match other {
Self::Fg(other_c) => c == other_c,
_ => false,
},
Self::Bg(c) => match other {
Self::Bg(other_c) => c == other_c,
_ => false,
},
}
}
}
#[derive(PartialEq, Debug, Clone, Copy, Eq)]
pub enum Clear {
Line,
Screen,

View File

@ -3,6 +3,7 @@ use termion::raw::{IntoRawMode, RawTerminal};
extern crate termion;
mod component;
mod theme;
mod token;
pub struct Display {

144
kkdisp/src/theme.rs Normal file
View File

@ -0,0 +1,144 @@
use serde::{de::Visitor, Deserialize, Serialize};
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 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() + &self.fg.fg()
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Color(u8, u8, u8);
impl Color {
pub const BLACK: Color = Color(0, 0, 0);
pub const RED: Color = Color(255, 0, 0);
pub const GREEN: Color = Color(0, 255, 0);
pub const BLUE: Color = Color(0, 0, 255);
}
impl Serialize for Color {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.serialize_str(&format!(
"#{:02X}{:02X}{:02X}",
self.0, self.1, self.2,
))
}
}
impl<'de> Deserialize<'de> for Color {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match TryInto::<Color>::try_into(v) {
Ok(c) => Ok(c),
Err(e) => Err(E::custom(e.to_string())),
}
}
fn visit_string<E>(
self,
v: String,
) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
match TryInto::<Color>::try_into(v) {
Ok(c) => Ok(c),
Err(e) => Err(E::custom(e.to_string())),
}
}
}
deserializer.deserialize_any(ColorCodeVisitor {})
}
}
impl Color {
pub fn bg(&self) -> String {
color::Rgb(self.0, self.1, self.2).bg_string()
}
pub fn fg(&self) -> String {
color::Rgb(self.0, self.1, self.2).fg_string()
}
}
impl TryFrom<String> for Color {
type Error = anyhow::Error;
fn try_from(value: String) -> Result<Self, Self::Error> {
Ok(value.as_str().try_into()?)
}
}
impl TryFrom<&str> for Color {
type Error = anyhow::Error;
fn try_from(value: &str) -> Result<Self, Self::Error> {
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(
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(),
},
}
}
}

View File

@ -1,4 +1,4 @@
use crate::component::Component;
use crate::{component::Component, theme::Color};
// (line (centered (limited (padded "hello world"))))
#[derive(Debug, Clone)]
@ -6,16 +6,39 @@ pub enum Token {
Centered(Box<Token>),
Limited(Box<Token>, usize),
CharPad(Box<Token>, char, usize),
String(String),
Fg(Box<Token>, Color),
Bg(Box<Token>, Color),
String(Box<Token>, String),
End,
}
impl From<String> for Token {
fn from(value: String) -> Self {
Token::String(value)
Self::text(value)
}
}
impl From<&str> for Token {
fn from(value: &str) -> Self {
Self::text(value)
}
}
impl Token {
pub fn text<T>(t: T) -> Self
where
T: Into<String>,
{
Self::End.string(t)
}
pub fn string<S>(self, s: S) -> Self
where
S: Into<String>,
{
Token::String(Box::new(self), s.into())
}
pub fn centered(self) -> Self {
Self::Centered(Box::new(self))
}
@ -29,43 +52,50 @@ impl Token {
}
pub fn str_len(&self) -> usize {
self.walk_to_end().len()
match self {
Token::String(t, s) => s.len() + t.str_len(),
Token::Fg(t, _) => t.str_len(),
Token::Bg(t, _) => t.str_len(),
Token::Centered(t) => t.str_len(),
Token::Limited(t, lim) => (*lim).min(t.str_len()),
Token::CharPad(t, _, pad_to) => {
(*pad_to).max(t.str_len())
}
Token::End => 0,
}
}
pub fn pad_char(self, c: char, pad_to: usize) -> Self {
Self::CharPad(Box::new(self), c, pad_to)
}
fn walk_to_end(&self) -> &String {
match self {
Token::String(s) => s,
Token::Centered(t) => t.walk_to_end(),
Token::Limited(t, _) => t.walk_to_end(),
Token::CharPad(t, _, _) => t.walk_to_end(),
}
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 with_width(self, width: usize) -> Vec<Component> {
match self {
Token::String(s) => vec![Component::String(s)],
Token::Centered(cnt) => cnt
.with_width(width)
Token::String(t, s) => vec![Component::String(s)]
.into_iter()
.map(|comp| {
if let Component::String(s) = comp {
let str_len = s.len();
let x = if str_len > width {
0
} else {
(width - str_len) / 2
};
vec![Component::X(x), Component::String(s)]
} else {
vec![comp]
}
})
.flatten()
.chain(t.with_width(width))
.collect(),
Token::Centered(cnt) => {
let mut str_len = cnt.str_len();
let components = if str_len > width {
str_len = width;
cnt.limited(width).with_width(width)
} else {
cnt.with_width(width)
};
vec![Component::X((width - str_len) / 2)]
.into_iter()
.chain(components)
.collect()
}
Token::Limited(s, lim) => s
.with_width(width)
.into_iter()
@ -96,6 +126,15 @@ 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(),
Token::End => vec![],
}
}
}
@ -110,29 +149,25 @@ mod tests {
vec![
(
"string -> string",
Token::String("hello".into()),
Token::text("hello"),
vec![Component::String("hello".into())],
),
(
"string -> limited",
Token::String(
"This string is too long for this!".into(),
)
.limited(4),
Token::text("This string is too long for this!")
.limited(4),
vec![Component::String("This".into())],
),
(
"string -> limit -> pad_to",
Token::String(
"This string is too long, but some".into(),
)
.limited(10)
.padded(15),
Token::text("This string is too long, but some")
.limited(10)
.padded(15),
vec![Component::String("This strin ".into())],
),
(
"center limited string",
Token::String("Ahhh this won't go far".into())
Token::text("Ahhh this won't go far")
.limited(10)
.centered(),
vec![
@ -142,12 +177,62 @@ mod tests {
),
(
"padded string with underscores is centered",
Token::String("It was...".into())
.pad_char('_', 15)
Token::text("It was...").pad_char('_', 15).centered(),
vec![
Component::X((WIDTH - 15) / 2),
Component::String("It was...______".into()),
],
),
(
"prefixes color",
Token::text("this is red").fg(Color::RED),
vec![
Component::Fg(Color::RED),
Component::String("this is red".into()),
],
),
(
"color at beginning of line",
Token::text("colored").centered().bg(Color::RED),
vec![
Component::Bg(Color::RED),
Component::X((WIDTH - 7) / 2),
Component::String("colored".into()),
],
),
(
"color isnt part of limit",
Token::text("ten chars!")
.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()),
],
),
(
"multicolor string centered",
Token::text("end")
.fg(Color::RED)
.string("mid")
.fg(Color::BLUE)
.string("start")
.fg(Color::GREEN)
.centered(),
vec![
Component::X(WIDTH - 3),
Component::String("It was...______".into()),
Component::X((WIDTH - 11) / 2),
Component::Fg(Color::GREEN),
Component::String("start".into()),
Component::Fg(Color::BLUE),
Component::String("mid".into()),
Component::Fg(Color::RED),
Component::String("end".into()),
],
),
]
@ -156,19 +241,28 @@ mod tests {
let (name, token, expected) = test;
eprintln!("token: {:#?}", &token);
let actual = token.with_width(WIDTH);
let actual_fmt = format!("{:#?}", &actual);
// let actual_fmt = format!("{:#?}", &actual);
if name == "multicolor string centered" {
// panic!("{}", actual_fmt);
}
assert!(
expected
.iter()
.zip(actual.iter())
.filter(|&(l, r)| l != r)
.count()
== 0,
"{} expected: {:#?} actual: {}",
name,
expected,
actual_fmt,
)
expected.len() == actual.len(),
"{}: length mismatch",
&name
);
for (i, exp) in expected.into_iter().enumerate() {
let act = &actual[i];
assert!(
exp == *act,
"{}: component at index {} mismatch.
expected:\n{:#?}
actual:\n{:#?}",
&name,
i,
exp,
act,
);
}
})
}
}