1use crate::{BuiltinTheme, Theme};
2
3#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ThemeCursor<T> {
18 themes: Vec<T>,
19 index: usize,
20}
21
22impl<T> ThemeCursor<T> {
23 pub fn new(themes: impl IntoIterator<Item = T>) -> Self {
25 let themes = themes.into_iter().collect();
26 Self { index: 0, themes }
27 }
28
29 pub fn peek(&self) -> Option<&T> {
31 self.themes.get(self.index)
32 }
33
34 #[allow(clippy::should_implement_trait)]
38 pub fn next(&mut self) -> Option<&T> {
39 if self.is_empty() {
40 return None;
41 }
42 self.index = (self.index + 1) % self.themes.len();
43 self.themes.get(self.index)
44 }
45
46 pub fn prev(&mut self) -> Option<&T> {
50 if self.is_empty() {
51 return None;
52 }
53 if self.index == 0 {
54 self.index = self.themes.len() - 1;
55 } else {
56 self.index -= 1;
57 }
58 self.themes.get(self.index)
59 }
60
61 pub fn themes(&self) -> &[T] {
63 &self.themes
64 }
65
66 pub fn len(&self) -> usize {
68 self.themes.len()
69 }
70
71 pub fn is_empty(&self) -> bool {
73 self.themes.is_empty()
74 }
75
76 pub fn set_index(&mut self, index: usize) -> Option<&T> {
80 if index < self.themes.len() {
81 self.index = index;
82 self.themes.get(index)
83 } else {
84 None
85 }
86 }
87}
88
89impl ThemeCursor<Theme> {
91 pub fn with_builtins() -> Self {
93 let mut themes: Vec<Theme> = BuiltinTheme::iter().map(|b| b.theme()).collect();
94 themes.sort();
95 ThemeCursor::new(themes)
96 }
97
98 #[cfg(feature = "fs")]
100 pub fn with_user_themes() -> Self {
101 use crate::all_user_themes;
102 let mut themes = all_user_themes();
103 themes.sort();
104 ThemeCursor::new(themes)
105 }
106
107 #[cfg(feature = "fs")]
109 pub fn with_all_themes() -> Self {
110 use crate::all_themes;
111 let mut themes = all_themes();
112 themes.sort();
113 ThemeCursor::new(themes)
114 }
115
116 pub fn set_current(&mut self, name: &str) -> Option<&Theme> {
121 let slug = heck::AsKebabCase(name).to_string();
122 let idx = self.themes.iter().position(|t| t.name_slug() == slug)?;
123 self.set_index(idx)
124 }
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::Theme;
131
132 fn make_theme(name: &str) -> Theme {
133 let yaml = format!(
135 r#"scheme: "{name}"
136author: "test"
137base00: "1c1c1c"
138base01: "2c2c2c"
139base02: "3c3c3c"
140base03: "555753"
141base04: "888a85"
142base05: "eeeeec"
143base06: "d3d7cf"
144base07: "eeeeec"
145base08: "cc0000"
146base09: "c4a000"
147base0A: "c4a000"
148base0B: "4e9a06"
149base0C: "06989a"
150base0D: "3465a4"
151base0E: "75507b"
152base0F: "cc0000"
153base10: "1c1c1c"
154base11: "000000"
155base12: "ef2929"
156base13: "fce94f"
157base14: "8ae234"
158base15: "34e2e2"
159base16: "729fcf"
160base17: "ad7fa8"
161"#
162 );
163 Theme::from_base24_str(&yaml).expect("test theme YAML is invalid")
164 }
165
166 fn cursor_with_names(names: &[&str]) -> ThemeCursor<Theme> {
167 ThemeCursor::new(names.iter().map(|n| make_theme(n)))
168 }
169
170 fn name(t: Option<&Theme>) -> &str {
171 t.map(|t| t.meta.name.as_str()).unwrap_or("<none>")
172 }
173
174 #[test]
177 fn empty_cursor() {
178 let mut c = ThemeCursor::<Theme>::new(vec![]);
179 assert_eq!(c.len(), 0);
180 assert!(c.is_empty());
181 assert!(c.peek().is_none());
182 assert!(c.next().is_none());
183 assert!(c.prev().is_none());
184 assert!(c.themes().is_empty());
185 }
186
187 #[test]
190 fn single_peek_returns_only_theme() {
191 let c = cursor_with_names(&["Alpha"]);
192 assert_eq!(name(c.peek()), "Alpha");
193 }
194
195 #[test]
196 fn single_next_wraps_to_itself() {
197 let mut c = cursor_with_names(&["Alpha"]);
198 assert_eq!(name(c.next()), "Alpha");
199 assert_eq!(name(c.next()), "Alpha");
200 }
201
202 #[test]
203 fn single_prev_wraps_to_itself() {
204 let mut c = cursor_with_names(&["Alpha"]);
205 assert_eq!(name(c.prev()), "Alpha");
206 assert_eq!(name(c.prev()), "Alpha");
207 }
208
209 #[test]
212 fn peek_returns_first_on_creation() {
213 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
214 assert_eq!(name(c.peek()), "Alpha");
215 }
216
217 #[test]
218 fn next_advances_through_all_themes() {
219 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
220 assert_eq!(name(c.next()), "Beta");
221 assert_eq!(name(c.next()), "Gamma");
222 }
223
224 #[test]
225 fn next_wraps_from_last_to_first() {
226 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
227 c.next(); c.next(); assert_eq!(name(c.next()), "Alpha"); }
231
232 #[test]
233 fn prev_wraps_from_first_to_last() {
234 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
235 assert_eq!(name(c.prev()), "Gamma"); }
237
238 #[test]
239 fn prev_retreats_in_order() {
240 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
241 c.next(); c.next(); assert_eq!(name(c.prev()), "Beta");
244 assert_eq!(name(c.prev()), "Alpha");
245 }
246
247 #[test]
248 fn next_then_prev_returns_to_start() {
249 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
250 c.next(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
253 }
254
255 #[test]
256 fn full_cycle_forward_returns_to_start() {
257 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
258 c.next();
259 c.next();
260 c.next(); assert_eq!(name(c.peek()), "Alpha");
262 }
263
264 #[test]
265 fn full_cycle_backward_returns_to_start() {
266 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
267 c.prev(); c.prev(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
271 }
272
273 #[test]
276 fn themes_returns_all_in_order() {
277 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
278 let names: Vec<&str> = c.themes().iter().map(|t| t.meta.name.as_str()).collect();
279 assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
280 }
281
282 #[test]
285 fn with_builtins_is_non_empty() {
286 let c = ThemeCursor::with_builtins();
287 assert!(!c.is_empty());
288 assert!(c.peek().is_some());
289 }
290
291 #[test]
292 fn with_builtins_is_sorted() {
293 let c = ThemeCursor::with_builtins();
294 let names: Vec<&str> = c.themes().iter().map(|t| t.meta.name.as_str()).collect();
295 let mut sorted = names.clone();
296 sorted.sort();
297 assert_eq!(names, sorted);
298 }
299
300 #[test]
301 fn with_builtins_can_cycle() {
302 let mut c = ThemeCursor::with_builtins();
303 let first = c.peek().unwrap().meta.name.clone();
304 for _ in 0..c.len() {
306 c.next();
307 }
308 assert_eq!(c.peek().unwrap().meta.name, first);
309 }
310}