use chrono::
{
DateTime,
Local,
};
use ratatui::
{
text::{ Line, Span },
backend::FromCrossterm,
style::
{
Color,
Modifier,
Style,
},
};
use crate::{ colors, config };
use super::
{
consts,
markup,
state::{ self, Entry, Picture, Transfer },
};
pub struct Theme {
pub disable_colors: bool,
pub disable_logo: bool,
pub show_id: bool,
pub show_message_ids: bool,
pub show_timestamps: bool,
pub message_stripes: bool,
pub render_math: bool,
}
impl Theme
{
pub fn load() -> Self
{
Self
{
disable_colors: config::read_config::<bool>("disable_colors"),
disable_logo: config::read_config::<bool>("disable_logo"),
show_id: config::read_config::<bool>("show_id"),
show_message_ids: config::read_config::<bool>("show_message_ids"),
show_timestamps: config::read_config::<bool>("show_timestamps"),
message_stripes: config::read_config::<bool>("message_stripes"),
render_math: config::read_config::<bool>("render_math"),
}
}
pub fn reload(&mut self) {
*self = Self::load();
}
pub fn render(&self, entry: &Entry, width: u16) -> Vec<Line<'static>>
{
match entry
{
Entry::Line(line) => state::wrap_line(line, width),
Entry::Message { username, id, message_id, timestamp, text, colors } =>
{
let id = if self.show_id { format!(" ({id})") } else { String::new() };
let prefix = vec!
[
self.timestamp(*timestamp),
self.colorize(username.clone(), colors.username_color),
Span::styled(id, DIM),
Span::raw(": "),
];
self.message_id(markup::render(prefix, text, self.style(colors.message_color), width, self.render_math), *message_id, width)
},
Entry::History { username, message_id, timestamp, text, colors } => self.message_id(markup::render(vec!
[
self.timestamp(*timestamp),
self.colorize(username.clone(), colors.username_color),
Span::raw(": "),
], text, self.style(colors.message_color), width, self.render_math), *message_id, width),
Entry::Private { sent, username, id, text, colors } =>
{
let prefix = vec!
[
Span::styled(if *sent { "[PM TO] " } else { "[PM FROM] " }, ACCENT),
self.colorize(username.clone(), colors.username_color),
Span::raw(format!(" ({id}): ")),
];
markup::render(prefix, text, self.style(colors.message_color), width, self.render_math)
},
Entry::Transfer(transfer) => state::wrap_line(&Line::from(progress(transfer, width)), width),
Entry::Image { username, filename, message_id, timestamp, username_color, picture, .. } =>
{
let mut spans = vec!
[
self.timestamp(*timestamp),
match username_color.filter(|_| !self.disable_colors).and_then(colors::u8_to_color)
{
Some(color) => Span::styled(username.clone(), Style::new().fg(Color::from_crossterm(color))),
None => Span::styled(username.clone(), ACCENT),
},
Span::styled(format!(" sent an image ({filename})"), DIM),
];
match picture
{
Picture::Absent => spans.push(Span::styled(" [ show ]", ACCENT)),
Picture::Waiting | Picture::Deferred => spans.push(Span::styled(" [ loading... ]", DIM)),
Picture::Gone => spans.push(Span::styled(" [ unavailable ]", ERROR)),
Picture::Ready(..) => {},
}
self.message_id(state::wrap_line(&Line::from(spans), width), *message_id, width)
},
}
}
fn timestamp(&self, timestamp: Option<u64>) -> Span<'static> {
let Some(time) = timestamp.filter(|_| self.show_timestamps)
.and_then(|timestamp| DateTime::from_timestamp(timestamp as i64, 0))
.map(|time| time.with_timezone(&Local))
else { return Span::raw("") };
let format = match time.date_naive() == Local::now().date_naive()
{
true => "%H:%M ",
false => "%Y-%m-%d %H:%M ",
};
Span::styled(time.format(format).to_string(), DIM)
}
fn message_id(&self, mut lines: Vec<Line<'static>>, message_id: u64, width: u16) -> Vec<Line<'static>>
{
if !self.show_message_ids { return lines; }
let tag = format!("#{message_id}");
let width = width as usize;
match lines.last_mut()
{
Some(last) if last.width() + 1 + tag.len() <= width =>
{
last.spans.push(Span::raw(" ".repeat(width - last.width() - tag.len())));
last.spans.push(Span::styled(tag, DIM));
},
_ => lines.push(Line::from(vec![Span::raw(" ".repeat(width.saturating_sub(tag.len()))), Span::styled(tag, DIM)])),
}
lines
}
pub fn colorize(&self, text: String, color: Option<u8>) -> Span<'static> {
Span::styled(text, self.style(color))
}
pub fn style(&self, color: Option<u8>) -> Style {
match color.and_then(colors::u8_to_color)
{
Some(c) if !self.disable_colors => Style::new().fg(Color::from_crossterm(c)),
_ => Style::new(),
}
}
}
pub fn stripe(background: Option<(u8, u8, u8)>) -> Color
{
let Some((r, g, b)) = background else { return STRIPE_FALLBACK };
let light = (r as u32 * 299 + g as u32 * 587 + b as u32 * 114) / 1000 > 128;
let target = if light { 0.0 } else { 255.0 };
let nudge = |c: u8| (c as f32 + (target - c as f32) * STRIPE_LIFT).round() as u8;
Color::Rgb(nudge(r), nudge(g), nudge(b))
}
fn progress(transfer: &Transfer, width: u16) -> Vec<Span<'static>>
{
let Transfer { upload, image, filename, done, total, outcome, .. } = transfer;
let kind = if *image { "image" } else { "file" };
let (label, style) = match (outcome, upload)
{
(None, true) => (format!("Uploading {kind} \"{filename}\""), DIM),
(None, false) => (format!("Downloading {kind} \"{filename}\""), DIM),
(Some(true), true) => (format!("Uploaded {kind} \"{filename}\""), OK),
(Some(true), false) => (format!("Downloaded {kind} \"{filename}\""), OK),
(Some(false), _) => (format!("Transferring {kind} \"{filename}\" failed"), ERROR),
};
let percent = state::percent(*done, *total);
let cells = consts::PROGRESS_CELLS.min(width as usize / 2);
let filled = (percent as usize * cells) / 100;
let mut spans = vec![Span::styled(label, style)];
if cells >= consts::MIN_PROGRESS_CELLS
{
spans.extend(
[
Span::raw(" "),
Span::styled("▕", BORDER),
Span::styled("█".repeat(filled), if outcome.is_some() { style } else { ACCENT }),
Span::styled("░".repeat(cells - filled), BORDER),
Span::styled("▏", BORDER),
]);
}
spans.push(Span::styled(format!(" {percent:>3}% {}/{}", size(*done), size(*total)), DIM));
spans
}
fn size(bytes: u64) -> String
{
const UNITS: [&str; 4] = ["B", "KB", "MB", "GB"];
let mut value = bytes as f64;
let mut unit = 0;
while value >= 1000.0 && unit < UNITS.len() - 1
{
value /= 1000.0;
unit += 1;
}
match unit
{
0 => format!("{bytes}B"),
_ => format!("{value:.1}{}", UNITS[unit]),
}
}
pub const TEXT: Style = Style::new().fg(Color::Rgb(0xEE, 0xD1, 0xD6)); pub const BORDER: Style = Style::new().fg(Color::Rgb(0xCA, 0xB4, 0xB7)); pub const BORDER_ACTIVE: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)); pub const TITLE: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)).add_modifier(Modifier::BOLD);
pub const DIM: Style = Style::new().fg(Color::Rgb(0xCA, 0xB4, 0xB7));
pub const ACCENT: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF));
pub const NOTICE: Style = Style::new().fg(Color::Rgb(0xFF, 0xDD, 0xE2)); pub const ERROR: Style = Style::new().fg(Color::Rgb(0xF6, 0x46, 0xC6)); pub const OK: Style = Style::new().fg(Color::Rgb(0xFF, 0xBB, 0xBA)); pub const SPEAKING: Style = Style::new().fg(Color::Rgb(0xFF, 0xBB, 0xBA)).add_modifier(Modifier::BOLD);
pub const LOGO_COLOR: Color = Color::Rgb(0x5C, 0x46, 0x4B); pub const LOGO: Style = Style::new().fg(LOGO_COLOR); pub const LOGO_UNDER: Style = Style::new().bg(LOGO_COLOR);
pub const CODE_BG: Color = Color::Rgb(0x2E, 0x24, 0x28); pub const CODE: Style = Style::new().fg(Color::Rgb(0xFF, 0xBB, 0xBA)).bg(CODE_BG); pub const CODE_BLOCK: Style = Style::new().fg(Color::Rgb(0xEE, 0xD1, 0xD6)).bg(CODE_BG);
pub const CODE_BAR: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)).bg(CODE_BG); pub const CODE_LANG: Style = Style::new().fg(Color::Rgb(0xCA, 0xB4, 0xB7)).bg(CODE_BG)
.add_modifier(Modifier::ITALIC);
pub const HEADING: Style = Style::new().fg(Color::Rgb(0xFF, 0xDD, 0xE2)); pub const QUOTE: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)); pub const BULLET: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)); pub const RULE: Style = Style::new().fg(Color::Rgb(0xCA, 0xB4, 0xB7));
pub const LINK: Style = Style::new().fg(Color::Rgb(0x9D, 0xCE, 0xFF)).add_modifier(Modifier::UNDERLINED);
pub const MATH: Style = Style::new().fg(Color::Rgb(0xFF, 0xDD, 0xE2));
pub const SELECTED: Style = Style::new().bg(Color::Rgb(0x00, 0x5F, 0x5F));
pub const STRIPE_FALLBACK: Color = Color::Rgb(0x1B, 0x1F, 0x24); const STRIPE_LIFT: f32 = 0.07;
pub const MENTION: Style = Style::new().bg(Color::Rgb(0x4B, 0x3A, 0x1F));
pub const SELECTION: Style = Style::new().bg(Color::Rgb(0x30, 0x45, 0x63));
pub const ARG_REQUIRED: Style = Style::new().fg(Color::Rgb(0xD7, 0xAF, 0x87)); pub const ARG_OPTIONAL: Style = Style::new().fg(Color::Rgb(0xFF, 0xB4, 0xAB)); pub const ARG_ACTIVE: Style = Style::new().fg(Color::Rgb(0xFF, 0xAF, 0x5F)) .add_modifier(Modifier::BOLD)
.add_modifier(Modifier::UNDERLINED);