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}