Compare commits
3 Commits
63ff28dd5d
...
539fa954a4
Author | SHA1 | Date |
---|---|---|
emilis | 539fa954a4 | |
emilis | 80425f9aff | |
emilis | 7890424ea9 |
|
@ -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"
|
||||
|
|
|
@ -8,3 +8,7 @@ edition = "2021"
|
|||
[dependencies]
|
||||
anyhow = "1.0.68"
|
||||
termion = "2.0.1"
|
||||
|
||||
[dependencies.serde]
|
||||
version = "1"
|
||||
features = ["std", "derive"]
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -3,6 +3,7 @@ use termion::raw::{IntoRawMode, RawTerminal};
|
|||
|
||||
extern crate termion;
|
||||
mod component;
|
||||
mod theme;
|
||||
mod token;
|
||||
|
||||
pub struct Display {
|
||||
|
|
|
@ -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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue