Skip to main content

void/ui/
icons.rs

1//! UI icons with Nerd Font glyphs or ASCII fallbacks.
2//!
3//! Set `VOID_ICONS=nerd|ascii|auto` to override detection. On Windows, `auto`
4//! defaults to ASCII unless `VOID_USE_NERD_FONTS=1` (terminal font configured).
5
6use nerd_font_symbols::md;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum IconMode {
10    Nerd,
11    Ascii,
12}
13
14#[derive(Debug, Clone, Copy)]
15pub struct IconSet {
16    pub logo: &'static str,
17    pub dashboard: &'static str,
18    pub tasks: &'static str,
19    pub stats: &'static str,
20    pub settings: &'static str,
21    pub help: &'static str,
22    pub play: &'static str,
23    pub pause: &'static str,
24    pub check: &'static str,
25    pub idle: &'static str,
26    pub timer: &'static str,
27    pub fire: &'static str,
28    pub target: &'static str,
29    pub calendar: &'static str,
30    pub chart: &'static str,
31    pub cycle: &'static str,
32    pub task_active: &'static str,
33    pub task_todo: &'static str,
34    pub task_progress: &'static str,
35    pub task_done: &'static str,
36    pub star: &'static str,
37    pub alert: &'static str,
38    pub plus: &'static str,
39    pub delete: &'static str,
40    pub edit: &'static str,
41    pub export: &'static str,
42    pub zen: &'static str,
43    pub skip: &'static str,
44    pub reset: &'static str,
45    pub end: &'static str,
46    pub chevron: &'static str,
47    pub focus: &'static str,
48    pub heart: &'static str,
49    pub dot: &'static str,
50}
51
52const NERD: IconSet = IconSet {
53    logo: md::MD_WEATHER_NIGHT,
54    dashboard: md::MD_VIEW_DASHBOARD,
55    tasks: md::MD_FORMAT_LIST_BULLETED,
56    stats: md::MD_CHART_LINE,
57    settings: md::MD_COG,
58    help: md::MD_HELP_CIRCLE,
59    play: md::MD_PLAY,
60    pause: md::MD_PAUSE,
61    check: md::MD_CHECK_CIRCLE,
62    idle: md::MD_TIMER_OUTLINE,
63    timer: md::MD_TIMER,
64    fire: md::MD_FIRE,
65    target: md::MD_TARGET,
66    calendar: md::MD_CALENDAR,
67    chart: md::MD_CHART_BAR,
68    cycle: md::MD_DOTS_HORIZONTAL,
69    task_active: md::MD_PLAY_CIRCLE,
70    task_todo: md::MD_CHECKBOX_BLANK_CIRCLE_OUTLINE,
71    task_progress: md::MD_PROGRESS_CLOCK,
72    task_done: md::MD_CHECK_CIRCLE,
73    star: md::MD_STAR,
74    alert: md::MD_CALENDAR_ALERT,
75    plus: md::MD_PLUS,
76    delete: md::MD_DELETE,
77    edit: md::MD_PENCIL,
78    export: md::MD_EXPORT,
79    zen: md::MD_WEATHER_NIGHT,
80    skip: md::MD_SKIP_NEXT,
81    reset: md::MD_REFRESH,
82    end: md::MD_STOP,
83    chevron: md::MD_CHEVRON_RIGHT,
84    focus: md::MD_CROSSHAIRS,
85    heart: md::MD_HEART,
86    dot: "ยท",
87};
88
89const ASCII: IconSet = IconSet {
90    logo: "*",
91    dashboard: "#",
92    tasks: "T",
93    stats: "S",
94    settings: "G",
95    help: "?",
96    play: ">",
97    pause: "||",
98    check: "+",
99    idle: "-",
100    timer: "t",
101    fire: "^",
102    target: "@",
103    calendar: "C",
104    chart: "=",
105    cycle: "...",
106    task_active: ">",
107    task_todo: "o",
108    task_progress: "~",
109    task_done: "x",
110    star: "*",
111    alert: "!",
112    plus: "+",
113    delete: "X",
114    edit: "E",
115    export: "S",
116    zen: "z",
117    skip: ">>",
118    reset: "R",
119    end: "#",
120    chevron: ">",
121    focus: "*",
122    heart: "<3",
123    dot: ".",
124};
125
126impl IconSet {
127    pub fn detect() -> Self {
128        match std::env::var("VOID_ICONS")
129            .ok()
130            .map(|v| v.to_ascii_lowercase())
131            .as_deref()
132        {
133            Some("nerd") | Some("nerd-font") => NERD,
134            Some("ascii") | Some("text") => ASCII,
135            Some("auto") | None => Self::detect_auto(),
136            Some(other) => {
137                eprintln!("void: unknown VOID_ICONS={other:?}, using auto");
138                Self::detect_auto()
139            }
140        }
141    }
142
143    pub fn mode(self) -> IconMode {
144        if self.logo == NERD.logo {
145            IconMode::Nerd
146        } else {
147            IconMode::Ascii
148        }
149    }
150
151    fn detect_auto() -> Self {
152        #[cfg(windows)]
153        {
154            if std::env::var("VOID_USE_NERD_FONTS")
155                .is_ok_and(|v| matches!(v.to_ascii_lowercase().as_str(), "1" | "true" | "yes"))
156            {
157                NERD
158            } else {
159                ASCII
160            }
161        }
162        #[cfg(not(windows))]
163        {
164            NERD
165        }
166    }
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn ascii_set_uses_plain_characters() {
175        assert_eq!(ASCII.play, ">");
176        assert_eq!(ASCII.check, "+");
177        assert_eq!(ASCII.task_done, "x");
178    }
179
180    #[test]
181    fn nerd_set_uses_private_use_glyphs() {
182        assert!(NERD.play.chars().any(|c| c as u32 >= 0xe000));
183    }
184
185    #[test]
186    fn explicit_env_overrides_auto() {
187        std::env::set_var("VOID_ICONS", "ascii");
188        assert_eq!(IconSet::detect().play, ASCII.play);
189        std::env::set_var("VOID_ICONS", "nerd");
190        assert_eq!(IconSet::detect().play, NERD.play);
191        std::env::remove_var("VOID_ICONS");
192    }
193}