Skip to main content

tui_nuances/
builtin.rs

1//! Built-in named themes and lookup / cycling helpers.
2//!
3//! Cycling order of [`Theme::all`]: `default`, the popular editor palettes
4//! (`catppuccin-mocha` … `monokai`, then the extra editor/palette themes ported from gitkraft, `porsche`), then the accessibility themes
5//! `colorblind`, `high-contrast` and `mono`.
6
7use crate::theme::{rgb, Spec, Theme};
8use ratatui::style::Color;
9
10named_themes! {
11    catppuccin_mocha, "catppuccin-mocha", "Catppuccin Mocha (dark)." =>
12    [0x1e1e2e, 0xcdd6f4, 0x9399b2, 0x313244, 0x89b4fa, 0xcba6f7, 0xa6e3a1, 0xf9e2af, 0xf38ba8, 0x89dceb],
13    [0x89b4fa, 0xf9e2af, 0xcba6f7, 0xa6e3a1, 0xf38ba8, 0x94e2d5, 0xfab387, 0xf5c2e7];
14    catppuccin_latte, "catppuccin-latte", "Catppuccin Latte (light)." =>
15    [0xeff1f5, 0x4c4f69, 0x7c7f93, 0xccd0da, 0x1e66f5, 0x8839ef, 0x40a02b, 0xdf8e1d, 0xd20f39, 0x04a5e5],
16    [0x1e66f5, 0xdf8e1d, 0x8839ef, 0x40a02b, 0xd20f39, 0x179299, 0xfe640b, 0xea76cb];
17    dracula, "dracula", "Dracula (dark)." =>
18    [0x282a36, 0xf8f8f2, 0x8e9bc4, 0x44475a, 0xbd93f9, 0xff79c6, 0x50fa7b, 0xf1fa8c, 0xff5555, 0x8be9fd],
19    [0xbd93f9, 0xff79c6, 0x50fa7b, 0xf1fa8c, 0xffb86c, 0x8be9fd, 0xff5555, 0x6272a4];
20    nord, "nord", "Nord (dark, arctic blues)." =>
21    [0x2e3440, 0xd8dee9, 0x8b96ab, 0x3b4252, 0x88c0d0, 0x81a1c1, 0xa3be8c, 0xebcb8b, 0xbf616a, 0x5e81ac],
22    [0x88c0d0, 0xa3be8c, 0xebcb8b, 0xb48ead, 0xd08770, 0x81a1c1, 0xbf616a, 0x8fbcbb];
23    gruvbox, "gruvbox", "Gruvbox (dark, warm retro)." =>
24    [0x282828, 0xebdbb2, 0xa89984, 0x3c3836, 0x83a598, 0xd3869b, 0xb8bb26, 0xfabd2f, 0xfb4934, 0x8ec07c],
25    [0x83a598, 0xfabd2f, 0xb8bb26, 0xd3869b, 0xfb4934, 0x8ec07c, 0xfe8019, 0xebdbb2];
26    tokyo_night, "tokyo-night", "Tokyo Night (dark)." =>
27    [0x1a1b26, 0xc0caf5, 0x737aa2, 0x292e42, 0x7aa2f7, 0xbb9af7, 0x9ece6a, 0xe0af68, 0xf7768e, 0x7dcfff],
28    [0x7aa2f7, 0xe0af68, 0xbb9af7, 0x9ece6a, 0xf7768e, 0x7dcfff, 0xff9e64, 0x73daca];
29    solarized_dark, "solarized-dark", "Solarized Dark." =>
30    [0x002b36, 0x93a1a1, 0x839496, 0x073642, 0x268bd2, 0x6c71c4, 0x859900, 0xb58900, 0xdc322f, 0x2aa198],
31    [0x268bd2, 0xb58900, 0x6c71c4, 0x859900, 0xdc322f, 0x2aa198, 0xcb4b16, 0xd33682];
32    solarized_light, "solarized-light", "Solarized Light." =>
33    [0xfdf6e3, 0x586e75, 0x657b83, 0xeee8d5, 0x268bd2, 0x6c71c4, 0x859900, 0xb58900, 0xdc322f, 0x2aa198],
34    [0x268bd2, 0xb58900, 0x6c71c4, 0x859900, 0xdc322f, 0x2aa198, 0xcb4b16, 0xd33682];
35    one_dark, "one-dark", "One Dark (Atom)." =>
36    [0x282c34, 0xabb2bf, 0x7f848e, 0x3e4451, 0x61afef, 0xc678dd, 0x98c379, 0xe5c07b, 0xe06c75, 0x56b6c2],
37    [0x61afef, 0xe5c07b, 0xc678dd, 0x98c379, 0xe06c75, 0x56b6c2, 0xd19a66, 0xabb2bf];
38    rose_pine, "rose-pine", "Rosé Pine (dark)." =>
39    [0x191724, 0xe0def4, 0x908caa, 0x1f1d2e, 0xc4a7e7, 0xebbcba, 0x9ccfd8, 0xf6c177, 0xeb6f92, 0x31748f],
40    [0xc4a7e7, 0xf6c177, 0xebbcba, 0x9ccfd8, 0xeb6f92, 0x31748f, 0xea9a97, 0xe0def4];
41    monokai, "monokai", "Monokai (dark)." =>
42    [0x272822, 0xf8f8f2, 0x90908a, 0x3e3d32, 0x66d9ef, 0xae81ff, 0xa6e22e, 0xe6db74, 0xf92672, 0x66d9ef],
43    [0x66d9ef, 0xa6e22e, 0xf92672, 0xe6db74, 0xae81ff, 0xfd971f, 0x75715e, 0xf8f8f2];
44    default_light, "default-light", "Default Light (light, from gitkraft)." =>
45    [0xfafaff, 0x1e2332, 0x828ca0, 0xebeef8, 0x008cdc, 0xff641e, 0x1ea050, 0xc88c00, 0xd23232, 0xff641e],
46    [0x008cdc, 0x1ea050, 0xd23232, 0xc88c00, 0xff641e, 0x8c50c8, 0x009696, 0xbe3c78];
47    grape, "grape", "Grape (dark, from gitkraft)." =>
48    [0x120c1e, 0xe6dcff, 0x6e6482, 0x1e1432, 0xc878ff, 0x82b4ff, 0xa06eff, 0xd2aaff, 0xff5096, 0x82b4ff],
49    [0xc878ff, 0xa06eff, 0xff5096, 0xd2aaff, 0x82b4ff, 0x50dcc8, 0xffb464, 0x78ffa0];
50    ocean, "ocean", "Ocean (dark, from gitkraft)." =>
51    [0x001423, 0xc8f0f5, 0x507882, 0x002337, 0x00c8b4, 0x00afd2, 0x50e6c8, 0xffdc50, 0xff6464, 0x00afd2],
52    [0x00c8b4, 0x50e6c8, 0xff6464, 0xffdc50, 0x00afd2, 0xb482ff, 0xffa064, 0xdc78c8];
53    sunset, "sunset", "Sunset (dark, from gitkraft)." =>
54    [0x160806, 0xffebd2, 0x8c6450, 0x280f0a, 0xff5050, 0xff9632, 0xffb450, 0xffe650, 0xff3232, 0xff9632],
55    [0xff5050, 0xffb450, 0xff3232, 0xffe650, 0xff9632, 0x64b4ff, 0xb478ff, 0x50dcc8];
56    forest, "forest", "Forest (dark, from gitkraft)." =>
57    [0x081208, 0xd2ebc8, 0x5a7850, 0x0f1e0f, 0x64c850, 0x50a050, 0x78d25a, 0xdcc850, 0xd25050, 0x50a050],
58    [0x64c850, 0x78d25a, 0xd25050, 0xdcc850, 0x50a050, 0x82a0ff, 0xc882b4, 0xc8b464];
59    rose, "rose", "Rose (dark, from gitkraft)." =>
60    [0x1c0610, 0xffe6eb, 0x8c5a6e, 0x320c1e, 0xff6496, 0xff8cb4, 0xffa0be, 0xffdcb4, 0xdc3c64, 0xff8cb4],
61    [0xff6496, 0xffa0be, 0xdc3c64, 0xffdcb4, 0xff8cb4, 0x64b4ff, 0x78dcc8, 0xc8b464];
62    neon, "neon", "Neon (dark, from gitkraft)." =>
63    [0x06000e, 0xe6e6ff, 0x645078, 0x0f001e, 0xff00c8, 0x00ffc8, 0x00ff82, 0xffdc00, 0xff1e50, 0x00ffc8],
64    [0xff00c8, 0x00ff82, 0xff1e50, 0xffdc00, 0x00ffc8, 0x6450ff, 0xff7800, 0x00b4ff];
65    gruvbox_light, "gruvbox-light", "Gruvbox Light (light, from gitkraft)." =>
66    [0xfbf1c7, 0x3c3836, 0x928374, 0xd5c4a1, 0xd65d0e, 0xd79921, 0x79740e, 0xd79921, 0xd65d0e, 0xd79921],
67    [0xd65d0e, 0x79740e, 0xcc241d, 0xd79921, 0x458588, 0xb16286, 0x689d6a, 0x076678];
68    catppuccin_frappe, "catppuccin-frappe", "Catppuccin Frappé (dark, from gitkraft)." =>
69    [0x303446, 0xc6d0f5, 0x737994, 0x414559, 0xca9ee6, 0x8caaee, 0xa6d189, 0xe5c890, 0xe78284, 0x8caaee],
70    [0xca9ee6, 0xa6d189, 0xe78284, 0xe5c890, 0x8caaee, 0x81c8be, 0xf4b8e4, 0xef9f76];
71    catppuccin_macchiato, "catppuccin-macchiato", "Catppuccin Macchiato (dark, from gitkraft)." =>
72    [0x24273a, 0xcad3f5, 0x6e738d, 0x363a4f, 0xc6a0f6, 0x8aadf4, 0xa6da95, 0xeed49f, 0xed8796, 0x8aadf4],
73    [0xc6a0f6, 0xa6da95, 0xed8796, 0xeed49f, 0x8aadf4, 0x8bd5ca, 0xf5bde6, 0xf0a880];
74    tokyo_night_storm, "tokyo-night-storm", "Tokyo Night Storm (dark, from gitkraft)." =>
75    [0x24283b, 0xc0caf5, 0x565f89, 0x2d314b, 0xbb9af7, 0x7aa2f7, 0x9ece6a, 0xe0af68, 0xf7768e, 0x7aa2f7],
76    [0xbb9af7, 0x9ece6a, 0xf7768e, 0xe0af68, 0x7aa2f7, 0x7dcfff, 0xff9e64, 0x73daca];
77    tokyo_night_light, "tokyo-night-light", "Tokyo Night Light (light, from gitkraft)." =>
78    [0xd5d6db, 0x343b58, 0x848cb0, 0xd0d5e3, 0x5a4a78, 0x2e7ee9, 0x485e30, 0x8c6c3e, 0xd20f39, 0x2e7ee9],
79    [0x5a4a78, 0x485e30, 0xd20f39, 0x8c6c3e, 0x2e7ee9, 0x388c96, 0xa6528c, 0xb46432];
80    kanagawa_wave, "kanagawa-wave", "Kanagawa Wave (dark, from gitkraft)." =>
81    [0x16161e, 0xdcd7ba, 0x727169, 0x2a2a37, 0xd27e99, 0x7e9cd8, 0x76946a, 0xdca561, 0xd27e99, 0x7e9cd8],
82    [0xd27e99, 0x76946a, 0xff5a64, 0xdca561, 0x7e9cd8, 0x6a9589, 0xe46876, 0x957fb8];
83    kanagawa_dragon, "kanagawa-dragon", "Kanagawa Dragon (dark, from gitkraft)." =>
84    [0x141414, 0xc5c9c5, 0xa6a69c, 0x282727, 0xd27e99, 0x8ba4b0, 0x87a987, 0xc8aa6d, 0xd27e99, 0x8ba4b0],
85    [0xd27e99, 0x87a987, 0xe36464, 0xc8aa6d, 0x8ba4b0, 0x6a9589, 0xc46c7c, 0xa591c4];
86    kanagawa_lotus, "kanagawa-lotus", "Kanagawa Lotus (light, from gitkraft)." =>
87    [0xf6f3e4, 0x545464, 0xc4b28a, 0xe7dba0, 0xa09abe, 0x4d699b, 0x6f894e, 0x77713f, 0xc04747, 0x4d699b],
88    [0xa09abe, 0x6f894e, 0xc04747, 0x77713f, 0x4d699b, 0x648c82, 0xb46478, 0x8c78aa];
89    moonfly, "moonfly", "Moonfly (dark, from gitkraft)." =>
90    [0x080808, 0xb2b2b2, 0x4e4e4e, 0x1c1c1c, 0xae81ff, 0x80a0ff, 0x8cc85f, 0xe2a478, 0xff7383, 0x80a0ff],
91    [0xae81ff, 0x8cc85f, 0xff7383, 0xe2a478, 0x80a0ff, 0x74b4bb, 0xffc078, 0xc88cff];
92    nightfly, "nightfly", "Nightfly (dark, from gitkraft)." =>
93    [0x011626, 0xacbbcb, 0x4b6479, 0x0b2942, 0xc792ea, 0x82aaff, 0xa1cd5e, 0xf3da0b, 0xfc5749, 0x82aaff],
94    [0xc792ea, 0xa1cd5e, 0xfc5749, 0xf3da0b, 0x82aaff, 0x21c8d7, 0xee8262, 0xaec8ff];
95    oxocarbon, "oxocarbon", "Oxocarbon (dark, from gitkraft)." =>
96    [0x161616, 0xf2f4f8, 0x525252, 0x262626, 0xff7eb6, 0x78a9ff, 0x42be65, 0xfacc37, 0xff6165, 0x78a9ff],
97    [0xff7eb6, 0x42be65, 0xff6165, 0xfacc37, 0x78a9ff, 0x08bdba, 0xbe95ff, 0xffaa78];
98    cyberpunk, "cyberpunk", "Cyberpunk (dark, from gitkraft)." =>
99    [0x0a0a10, 0xe6e6dc, 0x5a5a64, 0x14141e, 0xfcee0a, 0x00d2eb, 0x00dcb4, 0xff9600, 0xff3246, 0x00d2eb],
100    [0xfcee0a, 0x00dcb4, 0xff3246, 0xff9600, 0x00d2eb, 0xb400ff, 0xff0096, 0x00ff64];
101    rose_pine_moon, "rose-pine-moon", "Rose Pine Moon (dark, from gitkraft)." =>
102    [0x232136, 0xe0def4, 0x6e6a86, 0x302e46, 0xc4a7e7, 0x9ccfd8, 0x3e8fb0, 0xf6c177, 0xeb6f92, 0x9ccfd8],
103    [0xc4a7e7, 0x3e8fb0, 0xeb6f92, 0xf6c177, 0x9ccfd8, 0xea9a97, 0x9ccfd8, 0xe0def4];
104    rose_pine_dawn, "rose-pine-dawn", "Rose Pine Dawn (light, from gitkraft)." =>
105    [0xfaf4ed, 0x575279, 0x9893a5, 0xf2e9dd, 0x907aa9, 0x56949f, 0x286983, 0xea9d34, 0xb4637a, 0x56949f],
106    [0x907aa9, 0x286983, 0xb4637a, 0xea9d34, 0x56949f, 0xd7827e, 0x575279, 0x9893a5];
107    ayu_mirage, "ayu-mirage", "Ayu Mirage (dark, from gitkraft)." =>
108    [0x1f2430, 0xcccac2, 0x686868, 0x2a303e, 0x73d0ff, 0x73d0ff, 0x87d96c, 0xfacc6e, 0xed8274, 0x73d0ff],
109    [0x73d0ff, 0x87d96c, 0xed8274, 0xfacc6e, 0x409fff, 0xd9bfff, 0xffaa6c, 0xcccac2];
110    everforest_dark, "everforest-dark", "Everforest Dark (dark, from gitkraft)." =>
111    [0x1e2326, 0xd3c6aa, 0x7a8478, 0x2a3032, 0xa7c080, 0x7fbbb3, 0xa7c080, 0xdbbc7f, 0xe67e80, 0x7fbbb3],
112    [0xa7c080, 0x7fbbb3, 0xe67e80, 0xdbbc7f, 0xd699b6, 0xd3c6aa, 0xa6b0a0, 0x7a8478];
113    atom_one_light, "atom-one-light", "Atom One Light (light, from gitkraft)." =>
114    [0xf9f9f9, 0x2a2c33, 0x767676, 0xededed, 0x2f5af3, 0x2f5af3, 0x3f953a, 0xd2b67c, 0xde3e35, 0x2f5af3],
115    [0x2f5af3, 0x3f953a, 0xde3e35, 0xd2b67c, 0xa626a4, 0x0184bc, 0x2a2c33, 0xb4b4b4];
116    night_owl, "night-owl", "Night Owl (dark, from gitkraft)." =>
117    [0x011627, 0xd6deeb, 0x575656, 0x0c2337, 0x82aaff, 0x82aaff, 0x22da6e, 0xaddb67, 0xef5350, 0x82aaff],
118    [0x82aaff, 0x22da6e, 0xef5350, 0xaddb67, 0xc792ea, 0x7fdbca, 0xd6deeb, 0xffcb8b];
119    poimandres, "poimandres", "Poimandres (dark, from gitkraft)." =>
120    [0x1a1e28, 0xa6accd, 0x646a82, 0x262a37, 0x5de4c7, 0x89ddff, 0x5de4c7, 0xfffac2, 0xd0679d, 0x89ddff],
121    [0x5de4c7, 0x89ddff, 0xd0679d, 0xfffac2, 0xa6accd, 0xaddb67, 0x646a82, 0xffb478];
122    flexoki_dark, "flexoki-dark", "Flexoki Dark (dark, from gitkraft)." =>
123    [0x100f0f, 0xcecdc3, 0x575653, 0x1e1c1c, 0x4385be, 0x4385be, 0x879a39, 0xd0a215, 0xd14d41, 0x4385be],
124    [0x4385be, 0x879a39, 0xd14d41, 0xd0a215, 0xce5d97, 0x3aa99f, 0xcecdc3, 0xab8d48];
125    flexoki_light, "flexoki-light", "Flexoki Light (light, from gitkraft)." =>
126    [0xfffcf0, 0x100f0f, 0x6f6e69, 0xf2eede, 0x205ea6, 0x205ea6, 0x66800b, 0xad8301, 0xaf3029, 0x205ea6],
127    [0x205ea6, 0x66800b, 0xaf3029, 0xad8301, 0xa34077, 0x24837b, 0x100f0f, 0x85682e];
128    carbonfox, "carbonfox", "Carbonfox (dark, from gitkraft)." =>
129    [0x161616, 0xf2f4f8, 0x64646e, 0x232323, 0x78a9ff, 0x78a9ff, 0x25be6a, 0x08bdba, 0xee5396, 0x78a9ff],
130    [0x78a9ff, 0x25be6a, 0xee5396, 0x08bdba, 0xbe95ff, 0xff7eb6, 0xf2f4f8, 0xffaa78];
131    andromeda, "andromeda", "Andromeda (dark, from gitkraft)." =>
132    [0x262a33, 0xe5e5e5, 0x666666, 0x323742, 0x05bc79, 0x0fa8cd, 0x05bc79, 0xe5e512, 0xcd3131, 0x0fa8cd],
133    [0x05bc79, 0x0fa8cd, 0xcd3131, 0xe5e512, 0xbe95ff, 0xff7eb6, 0xe5e5e5, 0x666666];
134    synthwave, "synthwave", "Synthwave (dark, from gitkraft)." =>
135    [0x0a0810, 0xdad9c7, 0x7f7094, 0x14101e, 0xf6188f, 0x12c3e2, 0x1ebb2b, 0xfdf834, 0xf6188f, 0x12c3e2],
136    [0xf6188f, 0x1ebb2b, 0x12c3e2, 0xfdf834, 0xbe95ff, 0xff7eb6, 0xdad9c7, 0x7f7094];
137}
138
139impl Theme {
140    /// Porsche: black/white with the signature red accent (`#D5001C`).
141    ///
142    /// Bull is white and bear is red, as in the Porsche chart look.
143    #[must_use]
144    pub fn porsche() -> Self {
145        let mut t = Self::from_spec(
146            "porsche",
147            Spec {
148                bg: 0x000000,
149                fg: 0xffffff,
150                muted: 0x9b9ba0,
151                surface: 0x1a1a1c,
152                accent: 0xd5001c,
153                secondary: 0xe3e3e6,
154                success: 0x2e9e5b,
155                warning: 0xf5a623,
156                error: 0xff4d5e,
157                info: 0x4aa3ff,
158                palette: &[
159                    0xd5001c, 0xffffff, 0x9b9ba0, 0xff5a6e, 0x6b6b70, 0xe3e3e6, 0x8c0014, 0xc4c4c8,
160                ],
161            },
162        );
163        t.bull = rgb(0xffffff);
164        t.bear = rgb(0xd5001c);
165        t
166    }
167
168    /// Colorblind-safe theme (Okabe–Ito palette; blue/orange instead of
169    /// green/red). Keeps the terminal background.
170    #[must_use]
171    pub fn colorblind() -> Self {
172        let mut t = Self::from_spec(
173            "colorblind",
174            Spec {
175                bg: 0x000000,
176                fg: 0xffffff,
177                muted: 0xbbbbbb,
178                surface: 0x222222,
179                accent: 0x56b4e9,
180                secondary: 0xcc79a7,
181                success: 0x0072b2,
182                warning: 0xf0e442,
183                error: 0xe69f00,
184                info: 0x56b4e9,
185                palette: &[
186                    0x0072b2, 0xe69f00, 0x009e73, 0xf0e442, 0x56b4e9, 0xd55e00, 0xcc79a7, 0xbbbbbb,
187                ],
188            },
189        );
190        t.background = None;
191        t.surface = None;
192        t.tooltip_bg = Color::DarkGray;
193        t.neutral = rgb(0xbbbbbb);
194        t
195    }
196
197    /// Maximum-contrast theme: pure black/white with saturated status colors.
198    #[must_use]
199    pub fn high_contrast() -> Self {
200        let mut t = Self::from_spec(
201            "high-contrast",
202            Spec {
203                bg: 0x000000,
204                fg: 0xffffff,
205                muted: 0xd0d0d0,
206                surface: 0x1c1c1c,
207                accent: 0xffff00,
208                secondary: 0x00ffff,
209                success: 0x00ff00,
210                warning: 0xffa500,
211                error: 0xff5050,
212                info: 0x00ffff,
213                palette: &[
214                    0xffff00, 0x00ffff, 0xff00ff, 0x00ff00, 0xff5050, 0xffa500, 0x5f9fff, 0xffffff,
215                ],
216            },
217        );
218        t.border = rgb(0xffffff);
219        t.selection_bg = rgb(0xffffff);
220        t.selection_fg = rgb(0x000000);
221        t.disabled = rgb(0xa0a0a0);
222        t
223    }
224
225    /// Monochrome theme for 1-bit-ish terminals: only white, gray and dark
226    /// gray, terminal background. Selection is shown in reverse video colors.
227    #[must_use]
228    pub fn mono() -> Self {
229        Self {
230            name: "mono".into(),
231            background: None,
232            surface: None,
233            foreground: Color::White,
234            muted: Color::Gray,
235            disabled: Color::DarkGray,
236            border: Color::Gray,
237            border_focused: Color::White,
238            title: Color::White,
239            primary: Color::White,
240            secondary: Color::Gray,
241            accent: Color::White,
242            success: Color::White,
243            warning: Color::Gray,
244            error: Color::White,
245            info: Color::Gray,
246            selection_bg: Color::White,
247            selection_fg: Color::Black,
248            highlight: Color::White,
249            palette: vec![
250                Color::White,
251                Color::Gray,
252                Color::DarkGray,
253                Color::White,
254                Color::Gray,
255                Color::DarkGray,
256                Color::White,
257                Color::Gray,
258            ],
259            bull: Color::White,
260            bear: Color::DarkGray,
261            neutral: Color::Gray,
262            axis: Color::DarkGray,
263            grid: Color::DarkGray,
264            crosshair: Color::Gray,
265            tooltip_fg: Color::Black,
266            tooltip_bg: Color::White,
267        }
268    }
269
270    /// All built-in themes, in cycling order.
271    #[must_use]
272    pub fn all() -> Vec<Theme> {
273        let mut v = vec![Theme::default()];
274        v.extend(NAMED.iter().map(|(_, f)| f()));
275        v.push(Theme::porsche());
276        v.push(Theme::colorblind());
277        v.push(Theme::high_contrast());
278        v.push(Theme::mono());
279        v
280    }
281
282    /// Names of all built-in themes, in cycling order.
283    #[must_use]
284    pub fn names() -> Vec<&'static str> {
285        let mut v = vec!["default"];
286        v.extend(NAMED.iter().map(|(n, _)| *n));
287        v.extend(["porsche", "colorblind", "high-contrast", "mono"]);
288        v
289    }
290
291    /// Look a built-in theme up by name. Case-insensitive; `_` and spaces
292    /// are accepted for `-` (`"Tokyo Night"` → `tokyo-night`).
293    #[must_use]
294    pub fn by_name(name: &str) -> Option<Theme> {
295        let key: String = name
296            .trim()
297            .chars()
298            .map(|c| match c {
299                'é' | 'è' | 'É' => 'e',
300                c => c.to_ascii_lowercase(),
301            })
302            .collect::<String>()
303            .replace(['_', ' '], "-");
304        Self::all().into_iter().find(|t| t.name == key)
305    }
306
307    fn position(&self) -> Option<usize> {
308        Self::names().iter().position(|n| *n == self.name)
309    }
310
311    /// The next built-in theme (wraps). Custom-named themes start at `default`.
312    #[must_use]
313    pub fn next(&self) -> Theme {
314        let all = Self::all();
315        let i = self.position().map_or(0, |i| (i + 1) % all.len());
316        all[i].clone()
317    }
318
319    /// The previous built-in theme (wraps). Custom-named themes start at the last one.
320    #[must_use]
321    pub fn prev(&self) -> Theme {
322        let all = Self::all();
323        let i = self
324            .position()
325            .map_or(all.len() - 1, |i| (i + all.len() - 1) % all.len());
326        all[i].clone()
327    }
328}
329
330#[cfg(test)]
331mod tests {
332    use super::*;
333
334    #[test]
335    fn names_match_all() {
336        let all: Vec<String> = Theme::all().into_iter().map(|t| t.name).collect();
337        assert_eq!(all, Theme::names());
338    }
339
340    #[test]
341    fn all_names_unique() {
342        let names = Theme::names();
343        assert_eq!(names.len(), 47);
344        let mut sorted = names.clone();
345        sorted.sort_unstable();
346        sorted.dedup();
347        assert_eq!(sorted.len(), names.len());
348    }
349
350    #[test]
351    fn by_name_accepts_variants() {
352        assert_eq!(Theme::by_name("Tokyo Night").unwrap().name, "tokyo-night");
353        assert_eq!(Theme::by_name("ROSE_PINE").unwrap().name, "rose-pine");
354        assert_eq!(
355            Theme::by_name("Catppuccin Frappé").unwrap().name,
356            "catppuccin-frappe"
357        );
358        assert!(Theme::by_name("nope").is_none());
359        assert!(Theme::by_name("").is_none());
360    }
361
362    #[test]
363    fn next_prev_inverse_and_wrap() {
364        let all = Theme::all();
365        for t in &all {
366            assert_eq!(t.next().prev(), *t);
367        }
368        assert_eq!(all.last().unwrap().next(), all[0]);
369        assert_eq!(all[0].prev(), *all.last().unwrap());
370    }
371
372    #[test]
373    fn custom_theme_cycles_into_builtins() {
374        let c = Theme::default().name("mine");
375        assert_eq!(c.next().name, "default");
376        assert_eq!(c.prev().name, "mono");
377    }
378
379    #[test]
380    fn porsche_bull_is_white() {
381        assert_eq!(Theme::porsche().bull, Color::Rgb(255, 255, 255));
382        assert_eq!(Theme::porsche().accent, Color::Rgb(0xd5, 0x00, 0x1c));
383    }
384
385    #[test]
386    fn colorblind_has_no_green_red_pair() {
387        let t = Theme::colorblind();
388        assert_eq!(t.background, None);
389        assert_eq!(t.bull, Color::Rgb(0, 0x72, 0xb2));
390    }
391}