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