1#[derive(Debug, Clone, Default)]
11pub struct Style {
12 fg: Option<String>,
13 bg: Option<String>,
14 bold: bool,
15 dim: bool,
16 italic: bool,
17 underline: bool,
18 strikethrough: bool,
19 reverse: bool,
20}
21
22impl Style {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn fg(mut self, ansi: impl Into<String>) -> Self {
29 self.fg = Some(ansi.into());
30 self
31 }
32
33 pub fn bg(mut self, ansi: impl Into<String>) -> Self {
35 self.bg = Some(ansi.into());
36 self
37 }
38
39 pub fn bold(mut self) -> Self {
41 self.bold = true;
42 self
43 }
44
45 pub fn dim(mut self) -> Self {
47 self.dim = true;
48 self
49 }
50
51 pub fn italic(mut self) -> Self {
53 self.italic = true;
54 self
55 }
56
57 pub fn underline(mut self) -> Self {
59 self.underline = true;
60 self
61 }
62
63 pub fn strikethrough(mut self) -> Self {
65 self.strikethrough = true;
66 self
67 }
68
69 pub fn reverse(mut self) -> Self {
71 self.reverse = true;
72 self
73 }
74
75 pub fn apply(&self, text: &str) -> String {
79 let mut prefix = String::new();
80 let mut suffix = String::new();
81
82 if let Some(ref fg) = self.fg {
83 prefix.push_str(fg);
84 suffix.push_str("\x1b[39m");
85 }
86 if let Some(ref bg) = self.bg {
87 prefix.push_str(bg);
88 suffix.push_str("\x1b[49m");
89 }
90 if self.bold {
91 prefix.push_str("\x1b[1m");
92 suffix.push_str("\x1b[22m");
93 }
94 if self.italic {
95 prefix.push_str("\x1b[3m");
96 suffix.push_str("\x1b[23m");
97 }
98 if self.underline {
99 prefix.push_str("\x1b[4m");
100 suffix.push_str("\x1b[24m");
101 }
102 if self.dim {
103 prefix.push_str("\x1b[2m");
104 suffix.push_str("\x1b[22m");
105 }
106 if self.strikethrough {
107 prefix.push_str("\x1b[9m");
108 suffix.push_str("\x1b[29m");
109 }
110 if self.reverse {
111 prefix.push_str("\x1b[7m");
112 suffix.push_str("\x1b[27m");
113 }
114
115 format!("{}{}{}", prefix, text, suffix)
116 }
117
118 pub fn apply_padded(&self, text: &str, width: usize) -> String {
120 let styled = self.apply(text);
121 let vw = crate::tui::util::visible_width(&styled);
122 if vw < width {
123 format!("{}{}", styled, " ".repeat(width - vw))
124 } else {
125 styled
126 }
127 }
128
129 pub fn has_fg(&self) -> bool {
131 self.fg.is_some()
132 }
133
134 pub fn has_bg(&self) -> bool {
136 self.bg.is_some()
137 }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
143pub enum ThemeKey {
144 Accent,
145 BashMode,
146 Border,
147 BorderAccent,
148 BorderMuted,
149 CustomMessageBg,
150 CustomMessageLabel,
151 CustomMessageText,
152 Dim,
153 Error,
154 MdCode,
155 MdCodeBlock,
156 MdCodeBlockBorder,
157 MdHeading,
158 MdHr,
159 MdLink,
160 MdLinkUrl,
161 MdListBullet,
162 MdQuote,
163 MdQuoteBorder,
164 Muted,
165 SelectedBg,
166 Success,
167 SyntaxComment,
168 SyntaxFunction,
169 SyntaxKeyword,
170 SyntaxNumber,
171 SyntaxOperator,
172 SyntaxPunctuation,
173 SyntaxString,
174 SyntaxType,
175 SyntaxVariable,
176 Text,
177 ThinkingHigh,
178 ThinkingLow,
179 ThinkingMedium,
180 ThinkingMinimal,
181 ThinkingOff,
182 ThinkingText,
183 ThinkingXhigh,
184 ToolDiffAdded,
185 ToolDiffContext,
186 ToolDiffRemoved,
187 ToolErrorBg,
188 ToolOutput,
189 ToolPendingBg,
190 ToolSuccessBg,
191 ToolTitle,
192 UserMessageBg,
193 UserMessageText,
194 Warning,
195}
196
197impl ThemeKey {
198 pub fn as_str(&self) -> &'static str {
200 match self {
201 Self::Accent => "accent",
202 Self::BashMode => "bashMode",
203 Self::Border => "border",
204 Self::BorderAccent => "borderAccent",
205 Self::BorderMuted => "borderMuted",
206 Self::CustomMessageBg => "customMessageBg",
207 Self::CustomMessageLabel => "customMessageLabel",
208 Self::CustomMessageText => "customMessageText",
209 Self::Dim => "dim",
210 Self::Error => "error",
211 Self::MdCode => "mdCode",
212 Self::MdCodeBlock => "mdCodeBlock",
213 Self::MdCodeBlockBorder => "mdCodeBlockBorder",
214 Self::MdHeading => "mdHeading",
215 Self::MdHr => "mdHr",
216 Self::MdLink => "mdLink",
217 Self::MdLinkUrl => "mdLinkUrl",
218 Self::MdListBullet => "mdListBullet",
219 Self::MdQuote => "mdQuote",
220 Self::MdQuoteBorder => "mdQuoteBorder",
221 Self::Muted => "muted",
222 Self::SelectedBg => "selectedBg",
223 Self::Success => "success",
224 Self::SyntaxComment => "syntaxComment",
225 Self::SyntaxFunction => "syntaxFunction",
226 Self::SyntaxKeyword => "syntaxKeyword",
227 Self::SyntaxNumber => "syntaxNumber",
228 Self::SyntaxOperator => "syntaxOperator",
229 Self::SyntaxPunctuation => "syntaxPunctuation",
230 Self::SyntaxString => "syntaxString",
231 Self::SyntaxType => "syntaxType",
232 Self::SyntaxVariable => "syntaxVariable",
233 Self::Text => "text",
234 Self::ThinkingHigh => "thinkingHigh",
235 Self::ThinkingLow => "thinkingLow",
236 Self::ThinkingMedium => "thinkingMedium",
237 Self::ThinkingMinimal => "thinkingMinimal",
238 Self::ThinkingOff => "thinkingOff",
239 Self::ThinkingText => "thinkingText",
240 Self::ThinkingXhigh => "thinkingXhigh",
241 Self::ToolDiffAdded => "toolDiffAdded",
242 Self::ToolDiffContext => "toolDiffContext",
243 Self::ToolDiffRemoved => "toolDiffRemoved",
244 Self::ToolErrorBg => "toolErrorBg",
245 Self::ToolOutput => "toolOutput",
246 Self::ToolPendingBg => "toolPendingBg",
247 Self::ToolSuccessBg => "toolSuccessBg",
248 Self::ToolTitle => "toolTitle",
249 Self::UserMessageBg => "userMessageBg",
250 Self::UserMessageText => "userMessageText",
251 Self::Warning => "warning",
252 }
253 }
254
255 pub fn all() -> &'static [ThemeKey] {
257 use ThemeKey::*;
258 &[
259 Accent,
260 BashMode,
261 Border,
262 BorderAccent,
263 BorderMuted,
264 CustomMessageBg,
265 CustomMessageLabel,
266 CustomMessageText,
267 Dim,
268 Error,
269 MdCode,
270 MdCodeBlock,
271 MdCodeBlockBorder,
272 MdHeading,
273 MdHr,
274 MdLink,
275 MdLinkUrl,
276 MdListBullet,
277 MdQuote,
278 MdQuoteBorder,
279 Muted,
280 SelectedBg,
281 Success,
282 SyntaxComment,
283 SyntaxFunction,
284 SyntaxKeyword,
285 SyntaxNumber,
286 SyntaxOperator,
287 SyntaxPunctuation,
288 SyntaxString,
289 SyntaxType,
290 SyntaxVariable,
291 Text,
292 ThinkingHigh,
293 ThinkingLow,
294 ThinkingMedium,
295 ThinkingMinimal,
296 ThinkingOff,
297 ThinkingText,
298 ThinkingXhigh,
299 ToolDiffAdded,
300 ToolDiffContext,
301 ToolDiffRemoved,
302 ToolErrorBg,
303 ToolOutput,
304 ToolPendingBg,
305 ToolSuccessBg,
306 ToolTitle,
307 UserMessageBg,
308 UserMessageText,
309 Warning,
310 ]
311 }
312}
313
314pub trait Theme {
319 fn fg(&self, color: &str, text: &str) -> String;
322
323 fn bg(&self, color: &str, text: &str) -> String;
325
326 fn bold(&self, text: &str) -> String;
328
329 fn italic(&self, text: &str) -> String;
331
332 fn inverse(&self, text: &str) -> String;
334
335 fn fg_key(&self, key: ThemeKey, text: &str) -> String {
337 self.fg(key.as_str(), text)
338 }
339
340 fn bg_key(&self, key: ThemeKey, text: &str) -> String {
342 self.bg(key.as_str(), text)
343 }
344
345 fn fg_ansi(&self, _color: &str) -> &str {
348 ""
349 }
350
351 fn fg_ansi_key(&self, key: ThemeKey) -> &str {
353 self.fg_ansi(key.as_str())
354 }
355}
356
357#[derive(Debug, Clone, Copy, Default)]
360pub struct NoopTheme;
361
362impl Theme for NoopTheme {
363 fn fg(&self, _color: &str, text: &str) -> String {
364 text.to_string()
365 }
366
367 fn bg(&self, _color: &str, text: &str) -> String {
368 text.to_string()
369 }
370
371 fn bold(&self, text: &str) -> String {
372 text.to_string()
373 }
374
375 fn italic(&self, text: &str) -> String {
376 text.to_string()
377 }
378
379 fn inverse(&self, text: &str) -> String {
380 text.to_string()
381 }
382}