From f9e0e24dd99be7119a8e150484dcafdef9cfaa19 Mon Sep 17 00:00:00 2001 From: Cory Forsstrom Date: Fri, 6 Sep 2024 10:28:45 -0700 Subject: [PATCH] Use compressed theme to make URL < 512 bytes --- data/src/message.rs | 4 +- data/src/theme.rs | 298 ++++++++++++++++++++++++++++++++++++++++++-- data/src/url.rs | 4 +- 3 files changed, 295 insertions(+), 11 deletions(-) diff --git a/data/src/message.rs b/data/src/message.rs index 9c69c6c6b..e0a98704c 100644 --- a/data/src/message.rs +++ b/data/src/message.rs @@ -41,14 +41,14 @@ const URL_PATH_EXC_PUNC: &str = concatcp!( static URL_REGEX: Lazy = Lazy::new(|| { Regex::new(concatcp!( - r#"(?i)((https?|ircs?):\/\/|www\.)[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b"#, + r#"(?i)(((https?|ircs?):\/\/|www\.)[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,63}\b"#, r#"("#, URL_PATH, r#"*"#, URL_PATH_EXC_PUNC, r#"|"#, URL_PATH_EXC_PUNC, - r#"?)"# + r#"?)|halloy:\/\/.*)"# )) .unwrap() }); diff --git a/data/src/theme.rs b/data/src/theme.rs index 47c54036b..05a76276d 100644 --- a/data/src/theme.rs +++ b/data/src/theme.rs @@ -10,6 +10,8 @@ use serde::{Deserialize, Serialize}; use thiserror::Error; use tokio::fs; +use crate::compression; + const DEFAULT_THEME_NAME: &str = "Ferra"; const DEFAULT_THEME_CONTENT: &str = include_str!("../../assets/themes/ferra.toml"); @@ -57,18 +59,20 @@ impl Colors { } pub fn encode_base64(&self) -> String { - let Ok(content) = toml::to_string(&self) else { + let Ok(compressed) = compression::compress(&compact::Colors::from(*self)) else { // "Impossible" state return String::new(); }; - base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&content) + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(&compressed) } pub fn decode_base64(content: &str) -> Result { let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(content)?; - Ok(toml::from_str(&String::from_utf8(bytes)?)?) + compression::decompress::(&bytes) + .map_err(Error::Decompress) + .map(Self::from) } } @@ -76,14 +80,12 @@ impl Colors { pub enum Error { #[error("Failed to serialize theme to toml: {0}")] Encode(#[from] toml::ser::Error), - #[error("Failed to deserialize theme to toml: {0}")] - Decode(#[from] toml::de::Error), #[error("Failed to write theme file: {0}")] Write(#[from] std::io::Error), - #[error("Encoded theme string is not valid UTF8: {0}")] - InvalidUTF8(#[from] std::string::FromUtf8Error), #[error("Failed to decode base64 theme string: {0}")] Base64Decode(#[from] base64::DecodeError), + #[error("Failed to decompress theme: {0}")] + Decompress(#[source] compression::Error), } #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] @@ -140,6 +142,7 @@ pub struct Buffer { pub nickname: Color, #[serde(default = "default_transparent", with = "color_serde")] pub selection: Color, + #[serde(default)] pub server_messages: ServerMessages, #[serde(default = "default_transparent", with = "color_serde")] pub timestamp: Color, @@ -337,3 +340,284 @@ mod color_serde_maybe { color.map(super::color_to_hex).serialize(serializer) } } + +mod compact { + use iced_core::Color; + use serde::{Deserialize, Serialize}; + + use super::default_transparent; + + impl From for super::Colors { + fn from(colors: Colors) -> Self { + super::Colors { + general: super::General { + background: colors.g.ba, + border: colors.g.bo, + horizontal_rule: colors.g.hr, + unread_indicator: colors.g.ui, + }, + text: super::Text { + primary: colors.t.p, + secondary: colors.t.se, + tertiary: colors.t.t, + success: colors.t.su, + error: colors.t.e, + }, + buffer: super::Buffer { + action: colors.bf.a, + background: colors.bf.ba, + background_text_input: colors.bf.bati, + background_title_bar: colors.bf.batb, + border: colors.bf.bo, + border_selected: colors.bf.bos, + code: colors.bf.c, + highlight: colors.bf.h, + nickname: colors.bf.n, + selection: colors.bf.sl, + server_messages: super::ServerMessages { + join: colors.bf.sm.j, + part: colors.bf.sm.p, + quit: colors.bf.sm.q, + reply_topic: colors.bf.sm.rt, + change_host: colors.bf.sm.ch, + monitored_online: colors.bf.sm.mon, + monitored_offline: colors.bf.sm.mof, + default: colors.bf.sm.d, + }, + timestamp: colors.bf.ti, + topic: colors.bf.to, + url: colors.bf.u, + }, + buttons: super::Buttons { + primary: super::Button { + background: colors.bt.p.b, + background_hover: colors.bt.p.bh, + background_selected: colors.bt.p.bs, + background_selected_hover: colors.bt.p.bsh, + }, + secondary: super::Button { + background: colors.bt.s.b, + background_hover: colors.bt.s.bh, + background_selected: colors.bt.s.bs, + background_selected_hover: colors.bt.s.bsh, + }, + }, + } + } + } + + impl From for Colors { + fn from(colors: super::Colors) -> Self { + Colors { + g: General { + ba: colors.general.background, + bo: colors.general.border, + hr: colors.general.horizontal_rule, + ui: colors.general.unread_indicator, + }, + t: Text { + p: colors.text.primary, + se: colors.text.secondary, + t: colors.text.tertiary, + su: colors.text.success, + e: colors.text.error, + }, + bf: Buffer { + a: colors.buffer.action, + ba: colors.buffer.background, + bati: colors.buffer.background_text_input, + batb: colors.buffer.background_title_bar, + bo: colors.buffer.border, + bos: colors.buffer.border_selected, + c: colors.buffer.code, + h: colors.buffer.highlight, + n: colors.buffer.nickname, + sl: colors.buffer.selection, + sm: ServerMessages { + j: colors.buffer.server_messages.join, + p: colors.buffer.server_messages.part, + q: colors.buffer.server_messages.quit, + rt: colors.buffer.server_messages.reply_topic, + ch: colors.buffer.server_messages.change_host, + mon: colors.buffer.server_messages.monitored_online, + mof: colors.buffer.server_messages.monitored_offline, + d: colors.buffer.server_messages.default, + }, + ti: colors.buffer.timestamp, + to: colors.buffer.topic, + u: colors.buffer.url, + }, + bt: Buttons { + p: Button { + b: colors.buttons.primary.background, + bh: colors.buttons.primary.background_hover, + bs: colors.buttons.primary.background_selected, + bsh: colors.buttons.primary.background_selected_hover, + }, + s: Button { + b: colors.buttons.secondary.background, + bh: colors.buttons.secondary.background_hover, + bs: colors.buttons.secondary.background_selected, + bsh: colors.buttons.secondary.background_selected_hover, + }, + }, + } + } + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize)] + pub struct Colors { + #[serde(default)] + pub g: General, + #[serde(default)] + pub t: Text, + #[serde(default)] + pub bf: Buffer, + #[serde(default)] + pub bt: Buttons, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct Buttons { + #[serde(default)] + pub p: Button, + #[serde(default)] + pub s: Button, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct Button { + #[serde(default = "default_transparent", with = "color_serde")] + pub b: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bh: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bs: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bsh: Color, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct General { + #[serde(default = "default_transparent", with = "color_serde")] + pub ba: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bo: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub hr: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub ui: Color, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct Buffer { + #[serde(default = "default_transparent", with = "color_serde")] + pub a: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub ba: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bati: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub batb: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bo: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub bos: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub c: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub h: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub n: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub sl: Color, + #[serde(default)] + pub sm: ServerMessages, + #[serde(default = "default_transparent", with = "color_serde")] + pub ti: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub to: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub u: Color, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct ServerMessages { + #[serde(default, with = "color_serde_maybe")] + pub j: Option, + #[serde(default, with = "color_serde_maybe")] + pub p: Option, + #[serde(default, with = "color_serde_maybe")] + pub q: Option, + #[serde(default, with = "color_serde_maybe")] + pub rt: Option, + #[serde(default, with = "color_serde_maybe")] + pub ch: Option, + #[serde(default, with = "color_serde_maybe")] + pub mon: Option, + #[serde(default, with = "color_serde_maybe")] + pub mof: Option, + #[serde(default = "default_transparent", with = "color_serde")] + pub d: Color, + } + + #[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)] + pub struct Text { + #[serde(default = "default_transparent", with = "color_serde")] + pub p: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub se: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub t: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub su: Color, + #[serde(default = "default_transparent", with = "color_serde")] + pub e: Color, + } + + mod color_serde { + use iced_core::Color; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + u32::deserialize(deserializer).map(|int| { + let [r, g, b, a] = int.to_be_bytes(); + Color::from_rgba8(r, g, b, a as f32 / 255.0) + }) + } + + pub fn serialize(color: &Color, serializer: S) -> Result + where + S: Serializer, + { + u32::from_be_bytes(color.into_rgba8()).serialize(serializer) + } + } + + mod color_serde_maybe { + use iced_core::Color; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + Ok(Option::::deserialize(deserializer)?.map(|int| { + let [r, g, b, a] = int.to_be_bytes(); + Color::from_rgba8(r, g, b, a as f32 / 255.0) + })) + } + + pub fn serialize(color: &Option, serializer: S) -> Result + where + S: Serializer, + { + color + .map(|color| u32::from_be_bytes(color.into_rgba8())) + .serialize(serializer) + } + } +} diff --git a/data/src/url.rs b/data/src/url.rs index decace3bf..cdc19f388 100644 --- a/data/src/url.rs +++ b/data/src/url.rs @@ -30,7 +30,7 @@ impl std::fmt::Display for Url { } pub fn theme(colors: &theme::Colors) -> String { - format!("halloy:///theme?encoded={}", colors.encode_base64()) + format!("halloy:///theme?e={}", colors.encode_base64()) } impl Url { @@ -60,7 +60,7 @@ impl FromStr for Url { "halloy" if url.path() == "/theme" => { let (_, encoded) = url .query_pairs() - .find(|(key, _)| key == "encoded") + .find(|(key, _)| key == "e") .ok_or(Error::MissingQueryPair)?; let colors = theme::Colors::decode_base64(&encoded)?;