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}