Skip to main content

wisp/
theme.rs

1use crate::settings::{ThemeSettings, load_theme_file};
2use clankerdiff_ratatui::{RatatuiTheme, RatatuiUiTheme, composite_color, layered_style};
3use clankerdiff_ratatui::theme::{ReviewTheme, SelectionState, ThemeId};
4use ratatui::style::{Color, Style};
5use std::path::Path;
6use std::sync::Arc;
7
8#[derive(Clone, Debug)]
9pub struct Theme {
10    pub text_primary: Color,
11    pub text_secondary: Color,
12    pub background: Color,
13    pub surface: Color,
14    pub accent: Color,
15    pub heading: Color,
16    pub link: Color,
17    pub blockquote: Color,
18    pub code_fg: Color,
19    pub code_bg: Color,
20    pub success: Color,
21    pub warning: Color,
22    pub error: Color,
23    pub info: Color,
24    pub muted: Color,
25    pub diff_added_fg: Color,
26    pub diff_added_bg: Color,
27    pub diff_removed_fg: Color,
28    pub diff_removed_bg: Color,
29    review: Arc<ReviewTheme>,
30}
31
32#[derive(Debug, thiserror::Error)]
33pub enum ThemeLoadError {
34    #[error("Could not read theme: {0}")]
35    Io(#[from] std::io::Error),
36    #[error(transparent)]
37    Theme(#[from] clankerdiff_ratatui::theme::ThemeError),
38    #[error("Invalid theme file name: {0}")]
39    InvalidFile(String),
40}
41
42#[derive(Debug, thiserror::Error)]
43pub enum ThemeApplicationError {
44    #[error(transparent)]
45    Load(#[from] ThemeLoadError),
46    #[error("Could not save theme settings: {0}")]
47    Save(#[source] std::io::Error),
48    #[error("Theme task failed: {0}")]
49    Task(#[from] tokio::task::JoinError),
50}
51
52impl Theme {
53    pub fn load_selection(selection: &ThemeSettings) -> Result<Self, ThemeLoadError> {
54        match selection {
55            ThemeSettings::Builtin { id } => Ok(Self::from_review(ReviewTheme::builtin(id)?)),
56            ThemeSettings::File { file } => load_theme_file(file),
57        }
58    }
59
60    pub fn load_from_path(path: &Path) -> Result<Self, ThemeLoadError> {
61        if path.extension().is_none_or(|extension| extension != "json") {
62            return Err(ThemeLoadError::InvalidFile(path.display().to_string()));
63        }
64        let bytes = std::fs::read(path)?;
65        Ok(Self::from_review(ReviewTheme::from_bytes(
66            ThemeId::Custom(path.file_name().unwrap_or_default().to_string_lossy().into_owned()),
67            &bytes,
68        )?))
69    }
70
71    pub fn review(&self) -> &ReviewTheme {
72        &self.review
73    }
74
75    pub fn surface_style(&self) -> Style {
76        let ui = self.review.ui;
77        layered_style(ui.text, ui.surface, ui.canvas)
78    }
79
80    pub fn selection_style(&self, state: SelectionState) -> Style {
81        RatatuiUiTheme::from(&self.review.ui).selection_style(state)
82    }
83
84    pub fn from_review(review: ReviewTheme) -> Self {
85        let native = RatatuiTheme::from(&review);
86        let ui = native.ui;
87        let color = |value| composite_color(value, review.diff.background);
88        Self {
89            text_primary: ui.text,
90            text_secondary: ui.text_secondary,
91            background: ui.canvas,
92            surface: ui.surface,
93            accent: ui.accent,
94            heading: color(review.markdown.heading),
95            link: color(review.markdown.link),
96            blockquote: color(review.markdown.quote),
97            code_fg: color(review.markdown.code),
98            code_bg: color(review.markdown.code_background),
99            success: ui.positive,
100            warning: ui.warning,
101            error: ui.destructive,
102            info: ui.info,
103            muted: ui.text_muted,
104            diff_added_fg: native.addition,
105            diff_added_bg: native.addition_background,
106            diff_removed_fg: native.deletion,
107            diff_removed_bg: native.deletion_background,
108            review: Arc::new(review),
109        }
110    }
111}
112
113impl Default for Theme {
114    fn default() -> Self {
115        Self::from_review(ReviewTheme::default())
116    }
117}