config/
theme.rs

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/// Represents the theme configuration options.
26#[derive(Clone, Debug)]
27#[non_exhaustive]
28pub struct Theme {
29	/// The character for filling vertical spacing.
30	pub character_vertical_spacing: String,
31	/// The color for the break action.
32	pub color_action_break: Color,
33	/// The color for the drop action.
34	pub color_action_drop: Color,
35	/// The color for the edit action.
36	pub color_action_edit: Color,
37	/// The color for the exec action.
38	pub color_action_exec: Color,
39	/// The color for the fixup action.
40	pub color_action_fixup: Color,
41	/// The color for the pick action.
42	pub color_action_pick: Color,
43	/// The color for the reword action.
44	pub color_action_reword: Color,
45	/// The color for the squash action.
46	pub color_action_squash: Color,
47	/// The color for the label action.
48	pub color_action_label: Color,
49	/// The color for the reset action.
50	pub color_action_reset: Color,
51	/// The color for the merge action.
52	pub color_action_merge: Color,
53	/// The color for the update-ref action.
54	pub color_action_update_ref: Color,
55	/// The color for the background.
56	pub color_background: Color,
57	/// The color for added lines in a diff.
58	pub color_diff_add: Color,
59	/// The color for changed lines in a diff.
60	pub color_diff_change: Color,
61	/// The color for context lines in a diff.
62	pub color_diff_context: Color,
63	/// The color for removed lines in a diff.
64	pub color_diff_remove: Color,
65	/// The color for whitespace characters in a diff.
66	pub color_diff_whitespace: Color,
67	/// The color for the standard text.
68	pub color_foreground: Color,
69	/// The color for indicator text.
70	pub color_indicator: Color,
71	/// The background color for selected lines.
72	pub color_selected_background: Color,
73}
74
75impl Theme {
76	/// Create a new configuration with default values.
77	#[must_use]
78	#[inline]
79	#[allow(clippy::missing_panics_doc)]
80	pub fn new() -> Self {
81		Self::new_with_config(None).unwrap() // should never error with None config
82	}
83
84	/// Create a new theme from a Git Config reference.
85	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}