Skip to main content

smux/
ui.rs

1use std::env;
2
3use crate::config::{Config, IconColors, IconMode};
4
5const SESSION_ICON: &str = "";
6const DIRECTORY_ICON: &str = "󰉋";
7const TEMPLATE_ICON: &str = "󰙅";
8const PROJECT_ICON: &str = "󰏖";
9const ANSI_RESET: &str = "\x1b[0m";
10const ANSI_BOLD: &str = "\x1b[1m";
11
12#[derive(Clone, Copy, Debug, Eq, PartialEq)]
13pub struct DisplayStyle {
14    icons_enabled: bool,
15    icon_mode: IconMode,
16    icon_colors: IconColors,
17}
18
19impl DisplayStyle {
20    pub fn from_config(config: Option<&Config>) -> Self {
21        let (icon_mode, icon_colors) = config.map_or_else(
22            || (IconMode::Auto, IconColors::default()),
23            |config| (config.settings.icons, config.settings.icon_colors),
24        );
25        Self::new(icon_mode, icon_colors)
26    }
27
28    pub fn from_icon_mode(icon_mode: IconMode) -> Self {
29        Self::new(icon_mode, IconColors::default())
30    }
31
32    pub fn new(icon_mode: IconMode, icon_colors: IconColors) -> Self {
33        let icons_enabled = match icon_mode {
34            IconMode::Always => true,
35            IconMode::Never => false,
36            IconMode::Auto => terminal_supports_icons(),
37        };
38
39        Self {
40            icons_enabled,
41            icon_mode,
42            icon_colors,
43        }
44    }
45
46    pub fn icons_enabled(self) -> bool {
47        self.icons_enabled
48    }
49
50    pub fn icon_mode(self) -> IconMode {
51        self.icon_mode
52    }
53
54    pub fn icon_colors(self) -> IconColors {
55        self.icon_colors
56    }
57
58    pub fn session_label(self, value: &str) -> String {
59        self.label(SESSION_ICON, self.icon_colors.session, "session", value)
60    }
61
62    pub fn current_session_label(self, value: &str) -> String {
63        if self.icons_enabled {
64            format!(
65                "{ANSI_BOLD}\x1b[38;5;{color}m{icon}{ANSI_RESET}  {ANSI_BOLD}\x1b[38;5;{color}m{value}{ANSI_RESET}",
66                color = self.icon_colors.session,
67                icon = SESSION_ICON,
68            )
69        } else {
70            format!("current  {value}")
71        }
72    }
73
74    pub fn directory_label(self, value: &str) -> String {
75        self.label(DIRECTORY_ICON, self.icon_colors.directory, "dir", value)
76    }
77
78    pub fn template_label(self, value: &str) -> String {
79        self.label(TEMPLATE_ICON, self.icon_colors.template, "template", value)
80    }
81
82    pub fn project_label(self, value: &str) -> String {
83        self.label(PROJECT_ICON, self.icon_colors.project, "project", value)
84    }
85
86    fn label(self, icon: &str, color: u8, text: &str, value: &str) -> String {
87        if self.icons_enabled {
88            format!("\x1b[38;5;{color}m{icon}{ANSI_RESET}  {value}")
89        } else {
90            format!("{text:<8} {value}")
91        }
92    }
93}
94
95pub fn terminal_supports_icons() -> bool {
96    if matches!(env::var("TERM"), Ok(term) if term == "dumb") {
97        return false;
98    }
99
100    match locale_value() {
101        Some(locale) => {
102            let locale = locale.to_string_lossy().to_ascii_lowercase();
103            locale.contains("utf-8") || locale.contains("utf8")
104        }
105        None => true,
106    }
107}
108
109fn locale_value() -> Option<std::ffi::OsString> {
110    ["LC_ALL", "LC_CTYPE", "LANG"]
111        .into_iter()
112        .find_map(env::var_os)
113}
114
115#[cfg(test)]
116mod tests {
117    use super::DisplayStyle;
118    use crate::config::{IconColors, IconMode};
119
120    #[test]
121    fn always_mode_enables_icons() {
122        let style = DisplayStyle::from_icon_mode(IconMode::Always);
123        assert!(style.icons_enabled());
124        assert!(
125            style
126                .session_label("demo")
127                .starts_with("\u{1b}[38;5;75m\u{1b}[0m")
128        );
129    }
130
131    #[test]
132    fn never_mode_uses_text_labels() {
133        let style = DisplayStyle::from_icon_mode(IconMode::Never);
134        assert!(!style.icons_enabled());
135        assert_eq!(style.directory_label("/tmp/demo"), "dir      /tmp/demo");
136        assert_eq!(style.template_label("rust"), "template rust");
137        assert_eq!(style.project_label("demo"), "project  demo");
138        assert_eq!(style.current_session_label("demo"), "current  demo");
139    }
140
141    #[test]
142    fn custom_palette_changes_icon_colors() {
143        let style = DisplayStyle::new(
144            IconMode::Always,
145            IconColors {
146                session: 33,
147                directory: 44,
148                template: 55,
149                project: 66,
150            },
151        );
152
153        assert!(style.session_label("demo").starts_with("\u{1b}[38;5;33m"));
154        assert!(
155            style
156                .directory_label("/tmp/demo")
157                .starts_with("\u{1b}[38;5;44m")
158        );
159        assert!(style.template_label("rust").starts_with("\u{1b}[38;5;55m"));
160        assert!(style.project_label("demo").starts_with("\u{1b}[38;5;66m"));
161    }
162
163    #[test]
164    fn current_session_label_uses_bold_style() {
165        let style = DisplayStyle::from_icon_mode(IconMode::Always);
166        let label = style.current_session_label("demo");
167        assert!(label.starts_with("\u{1b}[1m\u{1b}[38;5;75m\u{1b}[0m"));
168        assert!(label.ends_with("  \u{1b}[1m\u{1b}[38;5;75mdemo\u{1b}[0m"));
169    }
170}