From d45e7b894c1496ab0be9afcae01c0d6569cf84c4 Mon Sep 17 00:00:00 2001 From: Matias Fontanini Date: Sun, 15 Oct 2023 07:57:49 -0700 Subject: [PATCH] Use hex encoding for colors --- Cargo.lock | 151 ++++++++++++++++++++++++++++++++++- Cargo.toml | 4 +- scripts/migrate-theme.py | 29 +++++++ src/builder.rs | 12 +-- src/diff.rs | 12 +-- src/presentation.rs | 3 +- src/render/draw.rs | 10 ++- src/render/operator.rs | 3 +- src/render/terminal.rs | 5 +- src/render/text.rs | 6 +- src/style.rs | 79 +++++++++++++++++- src/theme.rs | 12 +-- themes/dark.yaml | 34 ++++---- themes/tokyonight-storm.yaml | 34 ++++---- 14 files changed, 313 insertions(+), 81 deletions(-) create mode 100755 scripts/migrate-theme.py diff --git a/Cargo.lock b/Cargo.lock index 125d0ce..d3beab1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "ansi_colours" version = "1.2.2" @@ -149,6 +164,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.48.5", +] + [[package]] name = "clap" version = "4.4.0" @@ -230,6 +258,12 @@ dependencies = [ "windows-sys 0.45.0", ] +[[package]] +name = "core-foundation-sys" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa" + [[package]] name = "crc32fast" version = "1.3.2" @@ -320,8 +354,18 @@ version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7b750cb3417fd1b327431a470f388520309479ab0bf5e323505daf0290cd3850" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.14.4", + "darling_macro 0.14.4", +] + +[[package]] +name = "darling" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0209d94da627ab5605dcccf08bb18afa5009cfbef48d8a8b7d7bdbc79be25c5e" +dependencies = [ + "darling_core 0.20.3", + "darling_macro 0.20.3", ] [[package]] @@ -338,22 +382,50 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "darling_core" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "177e3443818124b357d8e76f53be906d60937f0d3a90773a664fa63fa253e621" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.29", +] + [[package]] name = "darling_macro" version = "0.14.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a4aab4dbc9f7611d8b55048a3a16d2d010c2c8334e46304b40ac1cc14bf3b48e" dependencies = [ - "darling_core", + "darling_core 0.14.4", "quote", "syn 1.0.109", ] +[[package]] +name = "darling_macro" +version = "0.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836a9bbc7ad63342d6d6e7b815ccab164bc77a2d95d84bc3117a8c0d5c98e2d5" +dependencies = [ + "darling_core 0.20.3", + "quote", + "syn 2.0.29", +] + [[package]] name = "deranged" version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2696e8a945f658fd14dc3b87242e6b80cd0f36ff04ea560fa39082368847946" +dependencies = [ + "serde", +] [[package]] name = "derive_builder" @@ -370,7 +442,7 @@ version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c11bdc11a0c47bc7d37d582b5285da6849c96681023680b906673c5707af7b0f" dependencies = [ - "darling", + "darling 0.14.4", "proc-macro2", "quote", "syn 1.0.109", @@ -571,6 +643,35 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "443144c8cdadd93ebf52ddb4056d257f5b52c04d3c804e657d19eb73fc33668b" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "iana-time-zone" +version = "0.1.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fad5b825842d2b38bd206f3e81d6957625fd7f0a361e345c30e01a0ae2dd613" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "ident_case" version = "1.0.1" @@ -604,6 +705,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -614,6 +716,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", + "serde", ] [[package]] @@ -908,11 +1011,13 @@ dependencies = [ "clap", "comrak", "crossterm", + "hex", "image", "merge-struct", "once_cell", "rstest", "serde", + "serde_with", "serde_yaml", "strum", "syntect", @@ -1151,6 +1256,35 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ca3b16a3d82c4088f343b7480a93550b3eabe1a358569c2dfe38bbcead07237" +dependencies = [ + "base64", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.0.0", + "serde", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e6be15c453eb305019bfa438b1593c731f36a289a7853f7707ee29e870b3b3c" +dependencies = [ + "darling 0.20.3", + "proc-macro2", + "quote", + "syn 2.0.29", +] + [[package]] name = "serde_yaml" version = "0.9.25" @@ -1554,6 +1688,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.45.0" diff --git a/Cargo.toml b/Cargo.toml index e6e7670..3334de5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,11 +11,13 @@ edition = "2021" clap = { version = "4.4", features = ["derive"] } comrak = { version = "0.19", default-features = false } crossterm = { version = "0.27", features = ["serde"] } -merge-struct = "0.1.0" +hex = "0.4" image = "0.24" +merge-struct = "0.1.0" once_cell = "1.18" serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.9" +serde_with = "3.3" syntect = "5.1" strum = { version = "0.25", features = ["derive"] } thiserror = "1" diff --git a/scripts/migrate-theme.py b/scripts/migrate-theme.py new file mode 100755 index 0000000..b4049de --- /dev/null +++ b/scripts/migrate-theme.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 + +import re +import sys + + +def migrate_color(line: str) -> str: + def as_hex(number_str: str) -> int: + number = int(number_str) + return f"{number:02x}" + + matches = re.match(r'.*"rgb_\(([\d]+),([\d]+),([\d]+)\)"', line) + if not matches: + return line + hex_color = "".join(map(as_hex, matches.group(1, 2, 3))) + original_color = f"rgb_({matches[1]},{matches[2]},{matches[3]})" + return line.replace(original_color, hex_color) + + +def migrate(line: str) -> str: + migrators = [migrate_color] + for migrator in migrators: + line = migrator(line) + return line + + +for line in sys.stdin: + new_line = migrate(line) + sys.stdout.write(new_line) diff --git a/src/builder.rs b/src/builder.rs index 417375e..d57963e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -14,10 +14,8 @@ use crate::{ properties::WindowSize, }, resource::{LoadImageError, Resources}, - style::TextStyle, - theme::{ - Alignment, AuthorPositioning, Colors, ElementType, FooterStyle, LoadThemeError, Margin, PresentationTheme, - }, + style::{Colors, TextStyle}, + theme::{Alignment, AuthorPositioning, ElementType, FooterStyle, LoadThemeError, Margin, PresentationTheme}, }; use serde::Deserialize; use std::{borrow::Cow, cell::RefCell, iter, mem, path::PathBuf, rc::Rc, str::FromStr}; @@ -100,11 +98,7 @@ impl<'a> PresentationBuilder<'a> { } self.needs_enter_column = false; let last_valid = matches!(last, RenderOperation::EnterColumn { .. } | RenderOperation::ExitLayout); - if last_valid { - Ok(()) - } else { - Err(BuildError::NotInsideColumn) - } + if last_valid { Ok(()) } else { Err(BuildError::NotInsideColumn) } } fn push_slide_prelude(&mut self) { diff --git a/src/diff.rs b/src/diff.rs index 5351daf..65d462a 100644 --- a/src/diff.rs +++ b/src/diff.rs @@ -91,9 +91,9 @@ mod test { use crate::{ presentation::{AsRenderOperations, PreformattedLine}, render::properties::WindowSize, - theme::{Alignment, Colors, Margin}, + style::{Color, Colors}, + theme::{Alignment, Margin}, }; - use crossterm::style::Color; use rstest::rstest; use std::rc::Rc; @@ -151,8 +151,8 @@ mod test { #[test] fn different_colors() { - let lhs = RenderOperation::SetColors(Colors { background: None, foreground: Some(Color::Red) }); - let rhs = RenderOperation::SetColors(Colors { background: None, foreground: Some(Color::Black) }); + let lhs = RenderOperation::SetColors(Colors { background: None, foreground: Some(Color::new(1, 2, 3)) }); + let rhs = RenderOperation::SetColors(Colors { background: None, foreground: Some(Color::new(3, 2, 1)) }); assert!(!lhs.is_content_different(&rhs)); } @@ -209,13 +209,13 @@ mod test { let lhs = Presentation::new(vec![Slide { render_operations: vec![RenderOperation::SetColors(Colors { background: None, - foreground: Some(Color::Red), + foreground: Some(Color::new(255, 0, 0)), })], }]); let rhs = Presentation::new(vec![Slide { render_operations: vec![RenderOperation::SetColors(Colors { background: None, - foreground: Some(Color::Black), + foreground: Some(Color::new(0, 0, 0)), })], }]); diff --git a/src/presentation.rs b/src/presentation.rs index a3c74b7..a0be1ad 100644 --- a/src/presentation.rs +++ b/src/presentation.rs @@ -1,7 +1,8 @@ use crate::{ markdown::text::WeightedLine, render::{media::Image, properties::WindowSize}, - theme::{Alignment, Colors, PresentationTheme}, + style::Colors, + theme::{Alignment, PresentationTheme}, }; use serde::Deserialize; use std::rc::Rc; diff --git a/src/render/draw.rs b/src/render/draw.rs index 1c02ee4..bc6ae71 100644 --- a/src/render/draw.rs +++ b/src/render/draw.rs @@ -6,10 +6,9 @@ use crate::{ }, presentation::{Presentation, RenderOperation}, render::properties::WindowSize, - style::TextStyle, - theme::{Alignment, Colors, Margin}, + style::{Color, Colors, TextStyle}, + theme::{Alignment, Margin}, }; -use crossterm::style::Color; use std::io; /// The result of a render operation. @@ -52,7 +51,10 @@ where let alignment = Alignment::Center { minimum_size: 0, minimum_margin: Margin::Percent(8) }; let operations = [ RenderOperation::ClearScreen, - RenderOperation::SetColors(Colors { foreground: Some(Color::Red), background: Some(Color::Black) }), + RenderOperation::SetColors(Colors { + foreground: Some(Color::new(255, 0, 0)), + background: Some(Color::new(0, 0, 0)), + }), RenderOperation::JumpToVerticalCenter, RenderOperation::RenderTextLine { line: WeightedLine::from(heading), alignment: alignment.clone() }, RenderOperation::RenderLineBreak, diff --git a/src/render/operator.rs b/src/render/operator.rs index 11a83b1..08ae4a0 100644 --- a/src/render/operator.rs +++ b/src/render/operator.rs @@ -10,7 +10,8 @@ use crate::{ markdown::text::WeightedLine, presentation::{AsRenderOperations, PreformattedLine, RenderOperation}, render::{layout::Positioning, properties::WindowSize}, - theme::{Alignment, Colors}, + style::Colors, + theme::Alignment, }; use std::{io, mem}; diff --git a/src/render/terminal.rs b/src/render/terminal.rs index aa2c861..e8ff78d 100644 --- a/src/render/terminal.rs +++ b/src/render/terminal.rs @@ -1,5 +1,5 @@ use super::properties::CursorPosition; -use crate::theme::Colors; +use crate::style::Colors; use crossterm::{ cursor, style::{self, StyledContent}, @@ -72,8 +72,7 @@ impl Terminal { } pub(crate) fn set_colors(&mut self, colors: Colors) -> io::Result<()> { - let colors = style::Colors { background: colors.background, foreground: colors.foreground }; - self.writer.queue(style::SetColors(colors))?; + self.writer.queue(style::SetColors(colors.into()))?; Ok(()) } diff --git a/src/render/text.rs b/src/render/text.rs index 6872d7e..a5fedf7 100644 --- a/src/render/text.rs +++ b/src/render/text.rs @@ -1,3 +1,4 @@ +use super::terminal::Terminal; use crate::{ markdown::text::WeightedLine, render::{ @@ -5,13 +6,10 @@ use crate::{ layout::{Layout, Positioning}, properties::WindowSize, }, - style::TextStyle, - theme::Colors, + style::{Colors, TextStyle}, }; use std::io; -use super::terminal::Terminal; - const MINIMUM_LINE_LENGTH: u16 = 10; /// Draws text on the screen. diff --git a/src/style.rs b/src/style.rs index 6ade13e..7a98f2e 100644 --- a/src/style.rs +++ b/src/style.rs @@ -1,5 +1,11 @@ -use crate::theme::Colors; use crossterm::style::Stylize; +use hex::{FromHex, FromHexError}; +use serde::{Deserialize, Serialize}; +use serde_with::{DeserializeFromStr, SerializeDisplay}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; /// The style of a piece of text. #[derive(Clone, Debug, Default, PartialEq, Eq)] @@ -94,10 +100,10 @@ impl TextStyle { styled = styled.italic().underlined(); } if let Some(color) = self.colors.background { - styled = styled.on(color); + styled = styled.on(color.into()); } if let Some(color) = self.colors.foreground { - styled = styled.with(color); + styled = styled.with(color.into()); } styled } @@ -111,3 +117,70 @@ enum TextFormatFlags { Strikethrough = 8, Link = 16, } + +#[derive(Debug, Copy, Clone, PartialEq, Eq, SerializeDisplay, DeserializeFromStr)] +pub struct Color(crossterm::style::Color); + +impl Color { + pub fn new(r: u8, g: u8, b: u8) -> Self { + Self(crossterm::style::Color::Rgb { r, g, b }) + } +} + +impl FromStr for Color { + type Err = ParseColorError; + + fn from_str(input: &str) -> Result { + let values = <[u8; 3]>::from_hex(input)?; + Ok(Self(crossterm::style::Color::Rgb { r: values[0], g: values[1], b: values[2] })) + } +} + +impl Display for Color { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let rgb = match self.0 { + crossterm::style::Color::Rgb { r, g, b } => [r, g, b], + _ => panic!("not rgb"), + }; + write!(f, "{}", hex::encode(rgb)) + } +} + +impl From for crossterm::style::Color { + fn from(value: Color) -> Self { + value.0 + } +} + +/// Text colors. +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] +pub struct Colors { + /// The background color. + pub background: Option, + + /// The foreground color. + pub foreground: Option, +} + +impl From for crossterm::style::Colors { + fn from(value: Colors) -> Self { + let foreground = value.foreground.map(Color::into); + let background = value.background.map(Color::into); + Self { foreground, background } + } +} + +#[derive(thiserror::Error, Debug)] +#[error("invalid color: {0}")] +pub struct ParseColorError(#[from] FromHexError); + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn color_serde() { + let color: Color = "beef42".parse().unwrap(); + assert_eq!(color.to_string(), "beef42"); + } +} diff --git a/src/theme.rs b/src/theme.rs index ee86946..429c7e7 100644 --- a/src/theme.rs +++ b/src/theme.rs @@ -1,4 +1,4 @@ -use crossterm::style::Color; +use crate::style::Colors; use serde::{Deserialize, Serialize}; use std::{fs, io, path::Path}; @@ -400,16 +400,6 @@ pub enum ElementType { BlockQuote, } -/// Text colors. -#[derive(Clone, Debug, Default, Deserialize, PartialEq, Eq, Serialize)] -pub struct Colors { - /// The background color. - pub background: Option, - - /// The foreground color. - pub foreground: Option, -} - /// Where to position the author's name in the intro slide. #[derive(Clone, Debug, Default, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] diff --git a/themes/dark.yaml b/themes/dark.yaml index 06e3643..e2c7f68 100644 --- a/themes/dark.yaml +++ b/themes/dark.yaml @@ -3,15 +3,15 @@ default: margin: percent: 8 colors: - foreground: "rgb_(230,230,230)" - background: "rgb_(4,3,18)" + foreground: "e6e6e6" + background: "040312" slide_title: alignment: center padding_bottom: 1 padding_top: 1 colors: - foreground: "rgb_(238,147,34)" + foreground: "ee9322" code: alignment: center @@ -23,8 +23,8 @@ code: inline_code: colors: - foreground: "rgb_(4,222,32)" - background: "rgb_(69,80,69)" + foreground: "04de20" + background: "455045" table: alignment: center @@ -33,51 +33,51 @@ intro_slide: title: alignment: center colors: - foreground: "rgb_(180,204,255)" + foreground: "b4ccff" subtitle: alignment: center colors: - foreground: "rgb_(165,215,232)" + foreground: "a5d7e8" author: alignment: center colors: - foreground: "rgb_(182,234,218)" + foreground: "b6eada" positioning: page_bottom headings: h1: prefix: "██" colors: - foreground: "rgb_(48,133,195)" + foreground: "3085c3" h2: prefix: "▓▓▓" colors: - foreground: "rgb_(168,223,142)" + foreground: "a8df8e" h3: prefix: "▒▒▒▒" colors: - foreground: "rgb_(247,140,162)" + foreground: "f78ca2" h4: prefix: "░░░░░" colors: - foreground: "rgb_(210,210,210)" + foreground: "d2d2d2" h5: prefix: "░░░░░░" colors: - foreground: "rgb_(210,210,210)" + foreground: "d2d2d2" h6: prefix: "░░░░░░░" colors: - foreground: "rgb_(210,210,210)" + foreground: "d2d2d2" block_quote: prefix: "▍ " colors: - foreground: "rgb_(240,240,240)" - background: "rgb_(41,46,66)" + foreground: "f0f0f0" + background: "292e42" footer: style: progress_bar colors: - foreground: "rgb_(122,162,247)" + foreground: "7aa2f7" diff --git a/themes/tokyonight-storm.yaml b/themes/tokyonight-storm.yaml index b15f94a..f3beac8 100644 --- a/themes/tokyonight-storm.yaml +++ b/themes/tokyonight-storm.yaml @@ -3,15 +3,15 @@ default: margin: percent: 8 colors: - foreground: "rgb_(192,202,245)" - background: "rgb_(36,40,59)" + foreground: "c0caf5" + background: "24283b" slide_title: alignment: center padding_bottom: 1 padding_top: 1 colors: - foreground: "rgb_(224,175,104)" + foreground: "e0af68" code: alignment: center @@ -23,8 +23,8 @@ code: inline_code: colors: - foreground: "rgb_(158,206,106)" - background: "rgb_(54,74,130)" + foreground: "9ece6a" + background: "364a82" table: alignment: center @@ -33,51 +33,51 @@ intro_slide: title: alignment: center colors: - foreground: "rgb_(122,162,247)" + foreground: "7aa2f7" subtitle: alignment: center colors: - foreground: "rgb_(169,177,214)" + foreground: "a9b1d6" author: alignment: center colors: - foreground: "rgb_(158,206,106)" + foreground: "9ece6a" positioning: page_bottom headings: h1: prefix: "██" colors: - foreground: "rgb_(158,206,106)" + foreground: "9ece6a" h2: prefix: "▓▓▓" colors: - foreground: "rgb_(247,118,142)" + foreground: "f7768e" h3: prefix: "▒▒▒▒" colors: - foreground: "rgb_(122,162,247)" + foreground: "7aa2f7" h4: prefix: "░░░░░" colors: - foreground: "rgb_(187,154,247)" + foreground: "bb9af7" h5: prefix: "░░░░░░" colors: - foreground: "rgb_(187,154,247)" + foreground: "bb9af7" h6: prefix: "░░░░░░░" colors: - foreground: "rgb_(187,154,247)" + foreground: "bb9af7" block_quote: prefix: "▍ " colors: - foreground: "rgb_(240,240,240)" - background: "rgb_(84,92,126)" + foreground: "f0f0f0" + background: "545c7e" footer: style: progress_bar colors: - foreground: "rgb_(122,162,247)" + foreground: "7aa2f7"