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