1use crate::style::{Rgb, StyleSpec};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub struct TextStyle {
11 pub fg: Option<Rgb>,
12 pub bold: bool,
13 pub italic: bool,
14 pub strike: bool,
15 pub underline: bool,
16}
17
18impl TextStyle {
19 pub const DEFAULT: Self = Self {
20 fg: None,
21 bold: false,
22 italic: false,
23 strike: false,
24 underline: false,
25 };
26
27 pub const fn fg(fg: Rgb) -> Self {
28 Self {
29 fg: Some(fg),
30 ..Self::DEFAULT
31 }
32 }
33
34 pub const fn bold(mut self) -> Self {
35 self.bold = true;
36 self
37 }
38
39 pub const fn italic(mut self) -> Self {
40 self.italic = true;
41 self
42 }
43
44 pub const fn underline(mut self) -> Self {
45 self.underline = true;
46 self
47 }
48
49 fn to_spec(self) -> StyleSpec {
50 StyleSpec {
51 fg: self.fg,
52 bold: self.bold,
53 italic: self.italic,
54 strike: self.strike,
55 underline: self.underline,
56 }
57 }
58}
59
60#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62pub struct AlertTheme {
63 pub note: TextStyle,
64 pub tip: TextStyle,
65 pub important: TextStyle,
66 pub warning: TextStyle,
67 pub caution: TextStyle,
68}
69
70#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct Theme {
73 pub body: TextStyle,
74 pub muted: TextStyle,
75
76 pub h1: TextStyle,
77 pub h2: TextStyle,
78 pub h3: TextStyle,
79
80 pub link: TextStyle,
81
82 pub inline_code: TextStyle,
83
84 pub quote: TextStyle,
85
86 pub border: TextStyle,
87
88 pub alerts: AlertTheme,
89
90 pub syntax_theme: String,
91}
92
93impl Theme {
94 pub fn dark() -> Self {
96 Self {
97 body: TextStyle::DEFAULT,
98 muted: TextStyle::fg(Rgb(139, 148, 158)),
99 h1: TextStyle::fg(Rgb(63, 185, 80)).bold(),
100 h2: TextStyle::fg(Rgb(88, 166, 255)).bold(),
101 h3: TextStyle::fg(Rgb(233, 208, 115)).bold(),
102 link: TextStyle::fg(Rgb(88, 166, 255)).underline(),
103 inline_code: TextStyle::fg(Rgb(79, 193, 233)),
104 quote: TextStyle::fg(Rgb(139, 148, 158)),
105 border: TextStyle::fg(Rgb(48, 54, 61)),
106 alerts: AlertTheme {
107 note: TextStyle::fg(Rgb(88, 166, 255)),
108 tip: TextStyle::fg(Rgb(63, 185, 80)),
109 important: TextStyle::fg(Rgb(163, 113, 247)),
110 warning: TextStyle::fg(Rgb(219, 171, 121)),
111 caution: TextStyle::fg(Rgb(248, 81, 73)),
112 },
113 syntax_theme: "base16-ocean.dark".to_string(),
114 }
115 }
116
117 pub fn light() -> Self {
119 Self {
120 body: TextStyle::DEFAULT,
121 muted: TextStyle::fg(Rgb(101, 109, 118)),
122 h1: TextStyle::fg(Rgb(31, 122, 46)).bold(),
123 h2: TextStyle::fg(Rgb(9, 76, 178)).bold(),
124 h3: TextStyle::fg(Rgb(148, 108, 8)).bold(),
125 link: TextStyle::fg(Rgb(9, 76, 178)).underline(),
126 inline_code: TextStyle::fg(Rgb(6, 122, 174)),
127 quote: TextStyle::fg(Rgb(101, 109, 118)),
128 border: TextStyle::fg(Rgb(208, 211, 215)),
129 alerts: AlertTheme {
130 note: TextStyle::fg(Rgb(9, 76, 178)),
131 tip: TextStyle::fg(Rgb(31, 122, 46)),
132 important: TextStyle::fg(Rgb(111, 66, 193)),
133 warning: TextStyle::fg(Rgb(154, 103, 29)),
134 caution: TextStyle::fg(Rgb(191, 38, 34)),
135 },
136 syntax_theme: "InspiredGitHub".to_string(),
137 }
138 }
139
140 pub fn builtin(name: &str) -> Option<Self> {
142 match name {
143 "dark" => Some(Self::dark()),
144 "light" => Some(Self::light()),
145 _ => None,
146 }
147 }
148
149 pub(crate) fn spec(&self, style: mdsee_layout::SemanticStyle) -> StyleSpec {
151 use mdsee_layout::SemanticStyle;
152 let text_style = match style {
153 SemanticStyle::Body => self.body,
154 SemanticStyle::Muted => self.muted,
155 SemanticStyle::Heading1 => self.h1,
156 SemanticStyle::Heading2 => self.h2,
157 SemanticStyle::Heading3 => self.h3,
158 SemanticStyle::Heading4 => self.h1,
159 SemanticStyle::Heading5 => self.h2,
160 SemanticStyle::Heading6 => self.muted,
161 SemanticStyle::Strong => TextStyle::DEFAULT.bold(),
162 SemanticStyle::Emphasis => TextStyle::DEFAULT.italic(),
163 SemanticStyle::Strike => TextStyle::DEFAULT,
164 SemanticStyle::InlineCode => self.inline_code,
165 SemanticStyle::Link => self.link,
166 SemanticStyle::Quote => self.quote,
167 SemanticStyle::Code => self.quote,
168 SemanticStyle::Border => self.border,
169 SemanticStyle::AlertNote => self.alerts.note,
170 SemanticStyle::AlertTip => self.alerts.tip,
171 SemanticStyle::AlertImportant => self.alerts.important,
172 SemanticStyle::AlertWarning => self.alerts.warning,
173 SemanticStyle::AlertCaution => self.alerts.caution,
174 };
175 text_style.to_spec()
176 }
177}
178
179impl Default for Theme {
180 fn default() -> Self {
181 Self::dark()
182 }
183}
184
185pub fn select_auto_theme(colorfgbg: Option<&str>) -> Theme {
191 let background_is_light = colorfgbg.and_then(|value| {
192 value
193 .rsplit(';')
194 .next()
195 .and_then(|last| last.parse::<u8>().ok())
196 .map(|bg| bg == 7 || (9..=15).contains(&bg))
197 });
198 match background_is_light {
199 Some(true) => Theme::light(),
200 _ => Theme::dark(),
201 }
202}
203
204#[cfg(test)]
205mod tests {
206 use super::*;
207
208 #[test]
209 fn builtin_themes_are_resolvable() {
210 assert!(Theme::builtin("dark").is_some());
211 assert!(Theme::builtin("light").is_some());
212 assert!(Theme::builtin("solarized").is_none());
213 }
214
215 #[test]
216 fn dark_and_light_differ() {
217 assert_ne!(Theme::dark(), Theme::light());
218 }
219
220 #[test]
221 fn auto_theme_uses_colorfgbg_background() {
222 assert_eq!(select_auto_theme(Some("15;0")), Theme::dark());
223 assert_eq!(select_auto_theme(Some("0;15")), Theme::light());
224 assert_eq!(select_auto_theme(None), Theme::dark());
225 assert_eq!(select_auto_theme(Some("garbage")), Theme::dark());
226 }
227
228 #[test]
229 fn spec_maps_heading_and_link() {
230 let theme = Theme::dark();
231 let h1 = theme.spec(mdsee_layout::SemanticStyle::Heading1);
232 assert!(h1.bold);
233 assert_eq!(h1.fg, Some(Rgb(63, 185, 80)));
234
235 let link = theme.spec(mdsee_layout::SemanticStyle::Link);
236 assert!(link.underline);
237 }
238}