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}