1use git::Config;
2
3use crate::{
4 errors::ConfigError,
5 utils::{_get_string, get_string},
6 Color,
7 ConfigErrorCause,
8};
9
10fn get_color(config: Option<&Config>, name: &str, default: Color) -> Result<Color, ConfigError> {
11 if let Some(value) = _get_string(config, name)? {
12 Color::try_from(value.to_lowercase().as_str()).map_err(|invalid_color_error| {
13 ConfigError::new(
14 name,
15 value.as_str(),
16 ConfigErrorCause::InvalidColor(invalid_color_error),
17 )
18 })
19 }
20 else {
21 Ok(default)
22 }
23}
24
25#[derive(Clone, Debug)]
27#[non_exhaustive]
28pub struct Theme {
29 pub character_vertical_spacing: String,
31 pub color_action_break: Color,
33 pub color_action_drop: Color,
35 pub color_action_edit: Color,
37 pub color_action_exec: Color,
39 pub color_action_fixup: Color,
41 pub color_action_pick: Color,
43 pub color_action_reword: Color,
45 pub color_action_squash: Color,
47 pub color_action_label: Color,
49 pub color_action_reset: Color,
51 pub color_action_merge: Color,
53 pub color_action_update_ref: Color,
55 pub color_background: Color,
57 pub color_diff_add: Color,
59 pub color_diff_change: Color,
61 pub color_diff_context: Color,
63 pub color_diff_remove: Color,
65 pub color_diff_whitespace: Color,
67 pub color_foreground: Color,
69 pub color_indicator: Color,
71 pub color_selected_background: Color,
73}
74
75impl Theme {
76 #[must_use]
78 #[inline]
79 #[allow(clippy::missing_panics_doc)]
80 pub fn new() -> Self {
81 Self::new_with_config(None).unwrap() }
83
84 pub(super) fn new_with_config(git_config: Option<&Config>) -> Result<Self, ConfigError> {
86 Ok(Self {
87 character_vertical_spacing: get_string(
88 git_config,
89 "interactive-rebase-tool.verticalSpacingCharacter",
90 "~",
91 )?,
92 color_action_break: get_color(git_config, "interactive-rebase-tool.breakColor", Color::LightWhite)?,
93 color_action_drop: get_color(git_config, "interactive-rebase-tool.dropColor", Color::LightRed)?,
94 color_action_edit: get_color(git_config, "interactive-rebase-tool.editColor", Color::LightBlue)?,
95 color_action_exec: get_color(git_config, "interactive-rebase-tool.execColor", Color::LightWhite)?,
96 color_action_fixup: get_color(git_config, "interactive-rebase-tool.fixupColor", Color::LightMagenta)?,
97 color_action_pick: get_color(git_config, "interactive-rebase-tool.pickColor", Color::LightGreen)?,
98 color_action_reword: get_color(git_config, "interactive-rebase-tool.rewordColor", Color::LightYellow)?,
99 color_action_squash: get_color(git_config, "interactive-rebase-tool.squashColor", Color::LightCyan)?,
100 color_action_label: get_color(git_config, "interactive-rebase-tool.labelColor", Color::DarkYellow)?,
101 color_action_reset: get_color(git_config, "interactive-rebase-tool.resetColor", Color::DarkYellow)?,
102 color_action_merge: get_color(git_config, "interactive-rebase-tool.mergeColor", Color::DarkYellow)?,
103 color_action_update_ref: get_color(
104 git_config,
105 "interactive-rebase-tool.updateRefColor",
106 Color::DarkMagenta,
107 )?,
108 color_background: get_color(git_config, "interactive-rebase-tool.backgroundColor", Color::Default)?,
109 color_diff_add: get_color(git_config, "interactive-rebase-tool.diffAddColor", Color::LightGreen)?,
110 color_diff_change: get_color(
111 git_config,
112 "interactive-rebase-tool.diffChangeColor",
113 Color::LightYellow,
114 )?,
115 color_diff_context: get_color(
116 git_config,
117 "interactive-rebase-tool.diffContextColor",
118 Color::LightWhite,
119 )?,
120 color_diff_remove: get_color(git_config, "interactive-rebase-tool.diffRemoveColor", Color::LightRed)?,
121 color_diff_whitespace: get_color(git_config, "interactive-rebase-tool.diffWhitespace", Color::LightBlack)?,
122 color_foreground: get_color(git_config, "interactive-rebase-tool.foregroundColor", Color::Default)?,
123 color_indicator: get_color(git_config, "interactive-rebase-tool.indicatorColor", Color::LightCyan)?,
124 color_selected_background: get_color(
125 git_config,
126 "interactive-rebase-tool.selectedBackgroundColor",
127 Color::Index(237),
128 )?,
129 })
130 }
131}
132
133impl TryFrom<&Config> for Theme {
134 type Error = ConfigError;
135
136 #[inline]
137 fn try_from(config: &Config) -> Result<Self, Self::Error> {
138 Self::new_with_config(Some(config))
139 }
140}
141
142#[cfg(test)]
143mod tests {
144 use claim::{assert_err, assert_ok};
145 use rstest::rstest;
146 use testutils::assert_err_eq;
147
148 use super::*;
149 use crate::{
150 errors::InvalidColorError,
151 testutils::{invalid_utf, with_git_config},
152 ConfigErrorCause,
153 };
154
155 macro_rules! config_test {
156 ($key:ident, $config_name:literal, $default:expr) => {
157 let config = Theme::new();
158 let value = config.$key;
159 assert_eq!(
160 value,
161 $default,
162 "Default for theme configuration '{}' was expected to be '{:?}' but '{:?}' was found",
163 stringify!($key),
164 $default,
165 value
166 );
167
168 let config_value = format!("{} = \"42\"", $config_name);
169 with_git_config(
170 &["[interactive-rebase-tool]", config_value.as_str()],
171 |git_config| {
172 let config = Theme::new_with_config(Some(&git_config)).unwrap();
173 assert_eq!(
174 config.$key,
175 Color::Index(42),
176 "Value for theme configuration '{}' was expected to be changed but was not",
177 stringify!($key)
178 );
179 },
180 );
181 };
182 }
183
184 #[test]
185 fn new() {
186 let _config = Theme::new();
187 }
188
189 #[test]
190 fn try_from_git_config() {
191 with_git_config(&[], |git_config| {
192 assert_ok!(Theme::try_from(&git_config));
193 });
194 }
195
196 #[test]
197 fn try_from_git_config_error() {
198 with_git_config(&["[interactive-rebase-tool]", "breakColor = invalid"], |git_config| {
199 assert_err!(Theme::try_from(&git_config));
200 });
201 }
202
203 #[test]
204 fn character_vertical_spacing() {
205 assert_eq!(Theme::new().character_vertical_spacing, "~");
206 with_git_config(
207 &["[interactive-rebase-tool]", "verticalSpacingCharacter = \"X\""],
208 |config| {
209 let theme = Theme::new_with_config(Some(&config)).unwrap();
210 assert_eq!(theme.character_vertical_spacing, "X");
211 },
212 );
213 }
214
215 #[test]
216 fn theme_color() {
217 config_test!(color_action_break, "breakColor", Color::LightWhite);
218 config_test!(color_action_drop, "dropColor", Color::LightRed);
219 config_test!(color_action_edit, "editColor", Color::LightBlue);
220 config_test!(color_action_exec, "execColor", Color::LightWhite);
221 config_test!(color_action_fixup, "fixupColor", Color::LightMagenta);
222 config_test!(color_action_pick, "pickColor", Color::LightGreen);
223 config_test!(color_action_reword, "rewordColor", Color::LightYellow);
224 config_test!(color_action_squash, "squashColor", Color::LightCyan);
225 config_test!(color_action_label, "labelColor", Color::DarkYellow);
226 config_test!(color_action_reset, "resetColor", Color::DarkYellow);
227 config_test!(color_action_merge, "mergeColor", Color::DarkYellow);
228 config_test!(color_action_update_ref, "updateRefColor", Color::DarkMagenta);
229 config_test!(color_background, "backgroundColor", Color::Default);
230 config_test!(color_diff_add, "diffAddColor", Color::LightGreen);
231 config_test!(color_diff_change, "diffChangeColor", Color::LightYellow);
232 config_test!(color_diff_context, "diffContextColor", Color::LightWhite);
233 config_test!(color_diff_remove, "diffRemoveColor", Color::LightRed);
234 config_test!(color_diff_whitespace, "diffWhitespace", Color::LightBlack);
235 config_test!(color_foreground, "foregroundColor", Color::Default);
236 config_test!(color_indicator, "indicatorColor", Color::LightCyan);
237 config_test!(color_selected_background, "selectedBackgroundColor", Color::Index(237));
238 }
239
240 #[test]
241 fn value_parsing_invalid_color() {
242 with_git_config(&["[interactive-rebase-tool]", "breakColor = -2"], |git_config| {
243 assert_err_eq!(
244 Theme::new_with_config(Some(&git_config)),
245 ConfigError::new(
246 "interactive-rebase-tool.breakColor",
247 "-2",
248 ConfigErrorCause::InvalidColor(InvalidColorError::Indexed)
249 )
250 );
251 });
252 }
253
254 #[rstest]
255 #[case::color_invalid_utf("breakColor")]
256 #[case::color_invalid_utf("verticalSpacingCharacter")]
257 fn value_parsing_invalid_utf(#[case] key: &str) {
258 with_git_config(
259 &[
260 "[interactive-rebase-tool]",
261 format!("{key} = {}", invalid_utf()).as_str(),
262 ],
263 |git_config| {
264 assert_err_eq!(
265 Theme::new_with_config(Some(&git_config)),
266 ConfigError::new_read_error(
267 format!("interactive-rebase-tool.{key}").as_str(),
268 ConfigErrorCause::InvalidUtf
269 )
270 );
271 },
272 );
273 }
274}