1use std::{
2 env,
3 io::{self, IsTerminal},
4};
5
6use owo_colors::Style;
7
8#[derive(Debug, Clone)]
12pub struct GraphicalTheme {
13 pub(crate) characters: ThemeCharacters,
14 pub(crate) styles: ThemeStyles,
15}
16
17fn force_color() -> bool {
18 env::var("CI").is_ok() || env::var("FORCE_COLOR").is_ok_and(|env| env != "0")
20}
21
22impl Default for GraphicalTheme {
23 fn default() -> Self {
24 Self::new(io::stdout().is_terminal() && io::stderr().is_terminal())
25 }
26}
27
28impl GraphicalTheme {
29 #[must_use]
31 pub(crate) fn new(is_terminal: bool) -> Self {
32 if force_color() {
33 return Self::unicode();
34 }
35 match env::var("NO_COLOR") {
36 _ if !is_terminal => Self::none(),
37 Ok(string) if string != "0" => Self::unicode_nocolor(),
38 _ => Self::unicode(),
39 }
40 }
41
42 #[must_use]
51 pub fn unicode() -> Self {
52 Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::rgb() }
53 }
54
55 #[must_use]
58 pub fn unicode_nocolor() -> Self {
59 Self { characters: ThemeCharacters::unicode(), styles: ThemeStyles::none() }
60 }
61
62 #[must_use]
65 pub fn none() -> Self {
66 Self { characters: ThemeCharacters::ascii(), styles: ThemeStyles::none() }
67 }
68
69 #[must_use]
71 pub const fn warning_style(&self) -> Style {
72 self.styles.warning
73 }
74}
75
76#[derive(Debug, Clone)]
77#[expect(clippy::redundant_pub_crate, reason = "prevents public glob re-export")]
78pub(crate) struct ThemeStyles {
79 pub(crate) error: Style,
80 pub(crate) warning: Style,
81 pub(crate) advice: Style,
82 pub(crate) help: Style,
83 pub(crate) note: Style,
84 pub(crate) link: Style,
85 pub(crate) linum: Style,
86 pub(crate) highlights: [Style; 3],
87}
88
89fn style() -> Style {
90 Style::new()
91}
92
93impl ThemeStyles {
94 fn rgb() -> Self {
95 Self {
96 error: style().fg_rgb::<225, 80, 80>().bold(), warning: style().fg_rgb::<244, 191, 117>().bold(),
98 advice: style().fg_rgb::<106, 159, 181>(),
99 help: style().fg_rgb::<106, 159, 181>(),
100 note: style().fg_rgb::<106, 159, 181>(),
101 link: style().fg_rgb::<92, 157, 255>().bold(),
102 linum: style().dimmed(),
103 highlights: [
104 style().fg_rgb::<246, 87, 248>(),
105 style().fg_rgb::<30, 201, 212>(),
106 style().fg_rgb::<145, 246, 111>(),
107 ],
108 }
109 }
110
111 fn none() -> Self {
112 Self {
113 error: style(),
114 warning: style(),
115 advice: style(),
116 help: style(),
117 note: style(),
118 link: style(),
119 linum: style(),
120 highlights: [style(); 3],
121 }
122 }
123}
124
125#[derive(Debug, Clone, Copy, Eq, PartialEq)]
130#[expect(clippy::redundant_pub_crate, reason = "prevents public glob re-export")]
131pub(crate) struct ThemeCharacters {
132 pub(crate) hbar: char,
133 pub(crate) vbar: char,
134 pub(crate) vbar_break: char,
135
136 pub(crate) uarrow: char,
137 pub(crate) rarrow: char,
138
139 pub(crate) ltop: char,
140 pub(crate) lbot: char,
141
142 pub(crate) lcross: char,
143 pub(crate) rcross: char,
144
145 pub(crate) underbar: char,
146 pub(crate) underline: char,
147
148 pub(crate) error: &'static str,
149 pub(crate) warning: &'static str,
150 pub(crate) advice: &'static str,
151}
152
153impl ThemeCharacters {
154 const fn unicode() -> Self {
155 Self {
156 hbar: '─',
157 vbar: '│',
158 vbar_break: '·',
159 uarrow: '▲',
160 rarrow: '▶',
161 ltop: '╭',
162 lbot: '╰',
163 lcross: '├',
164 rcross: '┤',
165 underbar: '┬',
166 underline: '─',
167 error: "×",
168 warning: "⚠",
169 advice: "☞",
170 }
171 }
172
173 const fn ascii() -> Self {
174 Self {
175 hbar: '-',
176 vbar: '|',
177 vbar_break: ':',
178 uarrow: '^',
179 rarrow: '>',
180 ltop: ',',
181 lbot: '`',
182 lcross: '|',
183 rcross: '|',
184 underbar: '|',
185 underline: '^',
186 error: "x",
187 warning: "!",
188 advice: ">",
189 }
190 }
191}