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 index(&self) -> usize {
78 self.index
79 }
80
81 pub fn set_index(&mut self, index: usize) -> Option<&T> {
85 if index < self.themes.len() {
86 self.index = index;
87 self.themes.get(index)
88 } else {
89 None
90 }
91 }
92}
93
94impl ThemeCursor<Theme> {
96 pub fn with_builtins() -> Self {
98 let mut themes: Vec<Theme> = BuiltinTheme::iter().map(|b| b.theme()).collect();
99 themes.sort();
100 ThemeCursor::new(themes)
101 }
102
103 #[cfg(feature = "fs")]
105 pub fn with_user_themes() -> Self {
106 use crate::all_user_themes;
107 let mut themes = all_user_themes();
108 themes.sort();
109 ThemeCursor::new(themes)
110 }
111
112 #[cfg(feature = "fs")]
114 pub fn with_all_themes() -> Self {
115 use crate::all_themes;
116 let mut themes = all_themes();
117 themes.sort();
118 ThemeCursor::new(themes)
119 }
120
121 pub fn set_current(&mut self, name: &str) -> Option<&Theme> {
126 let slug = heck::AsKebabCase(name).to_string();
127 let idx = self.themes.iter().position(|t| t.name_slug() == slug)?;
128 self.set_index(idx)
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135 use crate::Theme;
136
137 fn make_theme(name: &str) -> Theme {
138 let yaml = format!(
140 r#"scheme: "{name}"
141author: "test"
142base00: "1c1c1c"
143base01: "2c2c2c"
144base02: "3c3c3c"
145base03: "555753"
146base04: "888a85"
147base05: "eeeeec"
148base06: "d3d7cf"
149base07: "eeeeec"
150base08: "cc0000"
151base09: "c4a000"
152base0A: "c4a000"
153base0B: "4e9a06"
154base0C: "06989a"
155base0D: "3465a4"
156base0E: "75507b"
157base0F: "cc0000"
158base10: "1c1c1c"
159base11: "000000"
160base12: "ef2929"
161base13: "fce94f"
162base14: "8ae234"
163base15: "34e2e2"
164base16: "729fcf"
165base17: "ad7fa8"
166"#
167 );
168 Theme::from_base24_str(&yaml).expect("test theme YAML is invalid")
169 }
170
171 fn cursor_with_names(names: &[&str]) -> ThemeCursor<Theme> {
172 ThemeCursor::new(names.iter().map(|n| make_theme(n)))
173 }
174
175 fn name(t: Option<&Theme>) -> &str {
176 t.map(|t| t.meta.name.as_str()).unwrap_or("<none>")
177 }
178
179 #[test]
182 fn empty_cursor() {
183 let mut c = ThemeCursor::<Theme>::new(vec![]);
184 assert_eq!(c.len(), 0);
185 assert!(c.is_empty());
186 assert!(c.peek().is_none());
187 assert!(c.next().is_none());
188 assert!(c.prev().is_none());
189 assert!(c.themes().is_empty());
190 }
191
192 #[test]
195 fn single_peek_returns_only_theme() {
196 let c = cursor_with_names(&["Alpha"]);
197 assert_eq!(name(c.peek()), "Alpha");
198 }
199
200 #[test]
201 fn single_next_wraps_to_itself() {
202 let mut c = cursor_with_names(&["Alpha"]);
203 assert_eq!(name(c.next()), "Alpha");
204 assert_eq!(name(c.next()), "Alpha");
205 }
206
207 #[test]
208 fn single_prev_wraps_to_itself() {
209 let mut c = cursor_with_names(&["Alpha"]);
210 assert_eq!(name(c.prev()), "Alpha");
211 assert_eq!(name(c.prev()), "Alpha");
212 }
213
214 #[test]
217 fn peek_returns_first_on_creation() {
218 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
219 assert_eq!(name(c.peek()), "Alpha");
220 }
221
222 #[test]
223 fn next_advances_through_all_themes() {
224 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
225 assert_eq!(name(c.next()), "Beta");
226 assert_eq!(name(c.next()), "Gamma");
227 }
228
229 #[test]
230 fn next_wraps_from_last_to_first() {
231 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
232 c.next(); c.next(); assert_eq!(name(c.next()), "Alpha"); }
236
237 #[test]
238 fn prev_wraps_from_first_to_last() {
239 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
240 assert_eq!(name(c.prev()), "Gamma"); }
242
243 #[test]
244 fn prev_retreats_in_order() {
245 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
246 c.next(); c.next(); assert_eq!(name(c.prev()), "Beta");
249 assert_eq!(name(c.prev()), "Alpha");
250 }
251
252 #[test]
253 fn next_then_prev_returns_to_start() {
254 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
255 c.next(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
258 }
259
260 #[test]
261 fn full_cycle_forward_returns_to_start() {
262 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
263 c.next();
264 c.next();
265 c.next(); assert_eq!(name(c.peek()), "Alpha");
267 }
268
269 #[test]
270 fn full_cycle_backward_returns_to_start() {
271 let mut c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
272 c.prev(); c.prev(); c.prev(); assert_eq!(name(c.peek()), "Alpha");
276 }
277
278 #[test]
281 fn themes_returns_all_in_order() {
282 let c = cursor_with_names(&["Alpha", "Beta", "Gamma"]);
283 let names: Vec<&str> = c.themes().iter().map(|t| t.meta.name.as_str()).collect();
284 assert_eq!(names, vec!["Alpha", "Beta", "Gamma"]);
285 }
286
287 #[test]
290 fn with_builtins_is_non_empty() {
291 let c = ThemeCursor::with_builtins();
292 assert!(!c.is_empty());
293 assert!(c.peek().is_some());
294 }
295
296 #[test]
297 fn with_builtins_is_sorted() {
298 let c = ThemeCursor::with_builtins();
299 let names: Vec<&str> = c.themes().iter().map(|t| t.meta.name.as_str()).collect();
300 let mut sorted = names.clone();
301 sorted.sort();
302 assert_eq!(names, sorted);
303 }
304
305 #[test]
306 fn with_builtins_can_cycle() {
307 let mut c = ThemeCursor::with_builtins();
308 let first = c.peek().unwrap().meta.name.clone();
309 for _ in 0..c.len() {
311 c.next();
312 }
313 assert_eq!(c.peek().unwrap().meta.name, first);
314 }
315}