1use crate::{BuiltinTheme, Theme};
2
3pub struct ThemeCursor<T> {
17 themes: Vec<T>,
18 index: usize,
19}
20
21impl<T> ThemeCursor<T> {
22 pub fn new(themes: Vec<T>) -> Self {
24 Self { index: 0, themes }
25 }
26
27 pub fn peek(&self) -> Option<&T> {
29 self.themes.get(self.index)
30 }
31
32 #[allow(clippy::should_implement_trait)]
36 pub fn next(&mut self) -> Option<&T> {
37 if self.is_empty() {
38 return None;
39 }
40 self.index = (self.index + 1) % self.themes.len();
41 self.themes.get(self.index)
42 }
43
44 pub fn prev(&mut self) -> Option<&T> {
48 if self.is_empty() {
49 return None;
50 }
51 if self.index == 0 {
52 self.index = self.themes.len() - 1;
53 } else {
54 self.index -= 1;
55 }
56 self.themes.get(self.index)
57 }
58
59 pub fn themes(&self) -> &[T] {
61 &self.themes
62 }
63
64 pub fn len(&self) -> usize {
66 self.themes.len()
67 }
68
69 pub fn is_empty(&self) -> bool {
71 self.themes.is_empty()
72 }
73}
74
75impl ThemeCursor<Theme> {
77 pub fn with_builtins() -> Self {
79 let themes = BuiltinTheme::iter().map(|b| b.theme()).collect();
80 ThemeCursor::new(themes)
81 }
82
83 #[cfg(feature = "fs")]
85 pub fn with_user_themes() -> Self {
86 use crate::all_user_themes;
87 ThemeCursor::new(all_user_themes())
88 }
89
90 #[cfg(feature = "fs")]
92 pub fn with_all_themes() -> Self {
93 use crate::all_themes;
94 ThemeCursor::new(all_themes())
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use crate::{Ansi, Meta, Semantic, Theme, Ui, UiBg, UiBorder, UiCursor, UiFg, UiSelection};
102
103 fn make_theme(name: &str) -> Theme {
104 Theme {
105 meta: Meta {
106 name: name.to_string(),
107 slug: None,
108 author: None,
109 version: None,
110 description: None,
111 dark: None,
112 },
113 ansi: Ansi {
114 black: "#000000".into(),
115 red: "#cc0000".into(),
116 green: "#4e9a06".into(),
117 yellow: "#c4a000".into(),
118 blue: "#3465a4".into(),
119 magenta: "#75507b".into(),
120 cyan: "#06989a".into(),
121 white: "#d3d7cf".into(),
122 bright_black: "#555753".into(),
123 bright_red: "#ef2929".into(),
124 bright_green: "#8ae234".into(),
125 bright_yellow: "#fce94f".into(),
126 bright_blue: "#729fcf".into(),
127 bright_magenta: "#ad7fa8".into(),
128 bright_cyan: "#34e2e2".into(),
129 bright_white: "#eeeeec".into(),
130 },
131 palette: None,
132 base16: None,
133 semantic: Semantic {
134 error: "#cc0000".into(),
135 warning: "#c4a000".into(),
136 info: "#3465a4".into(),
137 success: "#4e9a06".into(),
138 highlight: "#c4a000".into(),
139 link: "#06989a".into(),
140 },
141 ui: Ui {
142 bg: UiBg {
143 primary: "#1c1c1c".into(),
144 secondary: "#2c2c2c".into(),
145 },
146 fg: UiFg {
147 primary: "#eeeeec".into(),
148 secondary: "#d3d7cf".into(),
149 muted: "#888a85".into(),
150 },
151 border: UiBorder {
152 primary: "#555753".into(),
153 muted: "#2c2c2c".into(),
154 },
155 cursor: UiCursor {
156 primary: "#eeeeec".into(),
157 muted: "#888a85".into(),
158 },
159 selection: UiSelection {
160 bg: "#3465a4".into(),
161 fg: "#eeeeec".into(),
162 },
163 },
164 }
165 }
166
167 fn cursor_with_names(names: &[&str]) -> ThemeCursor<Theme> {
168 ThemeCursor::new(names.iter().map(|n| make_theme(n)).collect())
169 }
170
171 fn name(t: Option<&Theme>) -> &str {
172 t.map(|t| t.meta.name.as_str()).unwrap_or("<none>")
173 }
174
175 #[test]
178 fn empty_cursor_peek_is_none() {
179 let c = ThemeCursor::<Theme>::new(vec![]);
180 assert!(c.peek().is_none());
181 }
182
183 #[test]
184 fn empty_cursor_next_is_none() {
185 let mut c = ThemeCursor::<Theme>::new(vec![]);
186 assert!(c.next().is_none());
187 }
188
189 #[test]
190 fn empty_cursor_prev_is_none() {
191 let mut c = ThemeCursor::<Theme>::new(vec![]);
192 assert!(c.prev().is_none());
193 }
194
195 #[test]
196 fn empty_cursor_len_and_is_empty() {
197 let c = ThemeCursor::<Theme>::new(vec![]);
198 assert_eq!(c.len(), 0);
199 assert!(c.is_empty());
200 }
201
202 #[test]
205 fn single_peek_returns_only_theme() {
206 let c = cursor_with_names(&["Alpha"]);
207 assert_eq!(name(c.peek()), "Alpha");
208 }
209
210 #[test]
211 fn single_next_wraps_to_itself() {
212 let mut c = cursor_with_names(&["Alpha"]);
213 assert_eq!(name(c.next()), "Alpha");
214 assert_eq!(name(c.next()), "Alpha");
215 }
216
217 #[test]
218 fn single_prev_wraps_to_itself() {
219 let mut c = cursor_with_names(&["Alpha"]);
220 assert_eq!(name(c.prev()), "Alpha");
221 assert_eq!(name(c.prev()), "Alpha");
222 }
223
224 #[test]
227 fn peek_returns_first_on_creation() {
228 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
229 assert_eq!(name(c.peek()), "Alpha");
230 }
231
232 #[test]
233 fn next_advances_through_all_themes() {
234 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
235 assert_eq!(name(c.next()), "Beta");
236 assert_eq!(name(c.next()), "Gamma");
237 }
238
239 #[test]
240 fn next_wraps_from_last_to_first() {
241 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
242 c.next(); c.next(); assert_eq!(name(c.next()), "Alpha"); }
246
247 #[test]
248 fn prev_wraps_from_first_to_last() {
249 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
250 assert_eq!(name(c.prev()), "Gamma"); }
252
253 #[test]
254 fn prev_retreats_in_order() {
255 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
256 c.next(); c.next(); assert_eq!(name(c.prev()), "Beta");
259 assert_eq!(name(c.prev()), "Alpha");
260 }
261
262 #[test]
263 fn next_then_prev_returns_to_start() {
264 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
265 c.next(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
268 }
269
270 #[test]
271 fn full_cycle_forward_returns_to_start() {
272 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
273 c.next();
274 c.next();
275 c.next(); assert_eq!(name(c.peek()), "Alpha");
277 }
278
279 #[test]
280 fn full_cycle_backward_returns_to_start() {
281 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
282 c.prev(); c.prev(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
286 }
287
288 #[test]
291 fn themes_returns_all_in_order() {
292 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
293 let names: Vec<&str> = c.themes().iter().map(|t| t.meta.name.as_str()).collect();
294 assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
295 }
296
297 #[test]
298 fn themes_is_empty_for_empty_cursor() {
299 let c = ThemeCursor::<Theme>::new(vec![]);
300 assert!(c.themes().is_empty());
301 }
302
303 #[test]
306 fn with_builtins_is_non_empty() {
307 let c = ThemeCursor::with_builtins();
308 assert!(!c.is_empty());
309 assert!(c.peek().is_some());
310 }
311
312 #[test]
313 fn with_builtins_can_cycle() {
314 let mut c = ThemeCursor::with_builtins();
315 let first = c.peek().unwrap().meta.name.clone();
316 for _ in 0..c.len() {
318 c.next();
319 }
320 assert_eq!(c.peek().unwrap().meta.name, first);
321 }
322}