Skip to content

Commit

Permalink
Use compressed theme to make URL < 512 bytes
Browse files Browse the repository at this point in the history
  • Loading branch information
tarkah committed Sep 6, 2024
1 parent 51cd89b commit f9e0e24
Show file tree
Hide file tree
Showing 3 changed files with 295 additions and 11 deletions.
4 changes: 2 additions & 2 deletions data/src/message.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,14 @@ const URL_PATH_EXC_PUNC: &str = concatcp!(

static URL_REGEX: Lazy<Regex> = 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()
});
Expand Down
298 changes: 291 additions & 7 deletions data/src/theme.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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");

Expand Down Expand Up @@ -57,33 +59,33 @@ 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<Self, Error> {
let bytes = base64::engine::general_purpose::URL_SAFE_NO_PAD.decode(content)?;

Ok(toml::from_str(&String::from_utf8(bytes)?)?)
compression::decompress::<compact::Colors>(&bytes)
.map_err(Error::Decompress)
.map(Self::from)
}
}

#[derive(Debug, Error)]
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)]
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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<Colors> 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<super::Colors> 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<Color>,
#[serde(default, with = "color_serde_maybe")]
pub p: Option<Color>,
#[serde(default, with = "color_serde_maybe")]
pub q: Option<Color>,
#[serde(default, with = "color_serde_maybe")]
pub rt: Option<Color>,
#[serde(default, with = "color_serde_maybe")]
pub ch: Option<Color>,
#[serde(default, with = "color_serde_maybe")]
pub mon: Option<Color>,
#[serde(default, with = "color_serde_maybe")]
pub mof: Option<Color>,
#[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<Color, D::Error>
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<S>(color: &Color, serializer: S) -> Result<S::Ok, S::Error>
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<Option<Color>, D::Error>
where
D: Deserializer<'de>,
{
Ok(Option::<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<S>(color: &Option<Color>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
color
.map(|color| u32::from_be_bytes(color.into_rgba8()))
.serialize(serializer)
}
}
}
4 changes: 2 additions & 2 deletions data/src/url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)?;
Expand Down

0 comments on commit f9e0e24

Please sign in to comment.