Skip to main content

neumann_shell/style/
icons.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2//! Unicode and ASCII icons for terminal output.
3
4/// Icon set for terminal display.
5///
6/// Provides both Unicode and ASCII variants for terminal compatibility.
7#[derive(Debug, Clone, Copy)]
8pub struct Icons {
9    pub success: &'static str,
10    pub error: &'static str,
11    pub warning: &'static str,
12    pub info: &'static str,
13    pub table: &'static str,
14    pub node: &'static str,
15    pub edge: &'static str,
16    pub vector: &'static str,
17    pub blob: &'static str,
18    pub key: &'static str,
19    pub chain: &'static str,
20    pub bullet: &'static str,
21    pub arrow: &'static str,
22    pub check: &'static str,
23    pub cross: &'static str,
24    pub spinner: &'static str,
25}
26
27impl Icons {
28    /// Unicode icons for terminals with good Unicode support.
29    pub const UNICODE: Self = Self {
30        success: "\u{2714}", // checkmark
31        error: "\u{2718}",   // X mark
32        warning: "\u{26A0}", // warning triangle
33        info: "\u{2139}",    // info circle
34        table: "\u{2630}",   // trigram
35        node: "\u{25CF}",    // filled circle
36        edge: "\u{2192}",    // right arrow
37        vector: "\u{2248}",  // approximately equal
38        blob: "\u{25A0}",    // filled square
39        key: "\u{1F511}",    // key emoji (fallback to ASCII in most contexts)
40        chain: "\u{26D3}",   // chains
41        bullet: "\u{2022}",  // bullet
42        arrow: "\u{2192}",   // right arrow
43        check: "\u{2714}",   // checkmark
44        cross: "\u{2718}",   // X mark
45        spinner: "\u{25CF}", // filled circle
46    };
47
48    /// ASCII-only icons for maximum compatibility.
49    pub const ASCII: Self = Self {
50        success: "[ok]",
51        error: "[!!]",
52        warning: "[!]",
53        info: "[i]",
54        table: "[#]",
55        node: "(o)",
56        edge: "->",
57        vector: "[~]",
58        blob: "[@]",
59        key: "[*]",
60        chain: "[=]",
61        bullet: "*",
62        arrow: "->",
63        check: "[ok]",
64        cross: "[x]",
65        spinner: "*",
66    };
67
68    /// Auto-detect which icon set to use based on terminal capabilities.
69    #[must_use]
70    pub const fn auto() -> &'static Self {
71        // For now, prefer ASCII for maximum compatibility
72        // In the future, could detect Unicode support via console crate
73        &Self::ASCII
74    }
75
76    /// Returns ASCII icons for plain/no-color mode.
77    #[must_use]
78    pub const fn plain() -> &'static Self {
79        &Self::ASCII
80    }
81}
82
83impl Default for &Icons {
84    fn default() -> Self {
85        Icons::auto()
86    }
87}
88
89#[cfg(test)]
90mod tests {
91    use super::*;
92
93    #[test]
94    fn test_unicode_icons_not_empty() {
95        assert!(!Icons::UNICODE.success.is_empty());
96        assert!(!Icons::UNICODE.error.is_empty());
97        assert!(!Icons::UNICODE.node.is_empty());
98    }
99
100    #[test]
101    fn test_ascii_icons_not_empty() {
102        assert!(!Icons::ASCII.success.is_empty());
103        assert!(!Icons::ASCII.error.is_empty());
104        assert!(!Icons::ASCII.node.is_empty());
105    }
106
107    #[test]
108    fn test_auto_returns_valid_icons() {
109        let icons = Icons::auto();
110        assert!(!icons.success.is_empty());
111    }
112
113    #[test]
114    fn test_plain_returns_ascii_icons() {
115        let icons = Icons::plain();
116        assert!(!icons.success.is_empty());
117        // Plain should return ASCII icons
118        assert_eq!(icons.success, Icons::ASCII.success);
119    }
120
121    #[test]
122    fn test_default_impl() {
123        let icons: &Icons = Default::default();
124        assert!(!icons.success.is_empty());
125    }
126
127    #[test]
128    fn test_all_unicode_icons_fields() {
129        let icons = &Icons::UNICODE;
130        assert!(!icons.warning.is_empty());
131        assert!(!icons.info.is_empty());
132        assert!(!icons.table.is_empty());
133        assert!(!icons.edge.is_empty());
134        assert!(!icons.vector.is_empty());
135        assert!(!icons.blob.is_empty());
136        assert!(!icons.key.is_empty());
137        assert!(!icons.chain.is_empty());
138        assert!(!icons.bullet.is_empty());
139        assert!(!icons.arrow.is_empty());
140        assert!(!icons.check.is_empty());
141        assert!(!icons.cross.is_empty());
142        assert!(!icons.spinner.is_empty());
143    }
144
145    #[test]
146    fn test_all_ascii_icons_fields() {
147        let icons = &Icons::ASCII;
148        assert!(!icons.warning.is_empty());
149        assert!(!icons.info.is_empty());
150        assert!(!icons.table.is_empty());
151        assert!(!icons.edge.is_empty());
152        assert!(!icons.vector.is_empty());
153        assert!(!icons.blob.is_empty());
154        assert!(!icons.key.is_empty());
155        assert!(!icons.chain.is_empty());
156        assert!(!icons.bullet.is_empty());
157        assert!(!icons.arrow.is_empty());
158        assert!(!icons.check.is_empty());
159        assert!(!icons.cross.is_empty());
160        assert!(!icons.spinner.is_empty());
161    }
162
163    #[test]
164    fn test_icons_debug() {
165        let icons = Icons::ASCII;
166        let debug_str = format!("{icons:?}");
167        assert!(debug_str.contains("Icons"));
168    }
169
170    #[test]
171    fn test_icons_clone() {
172        let icons = Icons::ASCII;
173        let cloned = icons;
174        assert_eq!(icons.success, cloned.success);
175    }
176}