1use owo_colors::{OwoColorize, Stream};
2
3use crate::path_display;
4
5fn symbol_for_stream(s: &str, stream: Stream) -> String {
6 match s {
7 "✓" => s.if_supports_color(stream, |t| t.green()).to_string(),
8 "↑" => s.if_supports_color(stream, |t| t.cyan()).to_string(),
9 "~" => s.if_supports_color(stream, |t| t.yellow()).to_string(),
10 "!" => s.if_supports_color(stream, |t| t.magenta()).to_string(),
11 "✗" => s.if_supports_color(stream, |t| t.red()).to_string(),
12 other => other.to_string(),
13 }
14}
15
16pub fn symbol(s: &str) -> String {
17 symbol_for_stream(s, Stream::Stdout)
18}
19
20pub fn symbol_stderr(s: &str) -> String {
26 symbol_for_stream(s, Stream::Stderr)
27}
28
29pub fn green(s: &str) -> String {
30 s.if_supports_color(Stream::Stdout, |t| t.green())
31 .to_string()
32}
33
34pub fn yellow(s: &str) -> String {
35 s.if_supports_color(Stream::Stdout, |t| t.yellow())
36 .to_string()
37}
38
39pub fn yellow_stderr(s: &str) -> String {
42 s.if_supports_color(Stream::Stderr, |t| t.yellow())
43 .to_string()
44}
45
46pub fn bold_yellow_stderr(s: &str) -> String {
48 use owo_colors::Style;
49 s.if_supports_color(Stream::Stderr, |t| t.style(Style::new().bold().yellow()))
50 .to_string()
51}
52
53pub fn cyan_stderr(s: &str) -> String {
54 s.if_supports_color(Stream::Stderr, |t| t.cyan())
55 .to_string()
56}
57
58pub fn dim_stderr(s: &str) -> String {
59 s.if_supports_color(Stream::Stderr, |t| t.dimmed())
60 .to_string()
61}
62
63pub fn red(s: &str) -> String {
64 s.if_supports_color(Stream::Stdout, |t| t.red()).to_string()
65}
66
67pub fn bold(s: &str) -> String {
68 s.if_supports_color(Stream::Stdout, |t| t.bold())
69 .to_string()
70}
71
72pub fn dim(s: &str) -> String {
73 s.if_supports_color(Stream::Stdout, |t| t.dimmed())
74 .to_string()
75}
76
77pub fn cyan(s: &str) -> String {
78 s.if_supports_color(Stream::Stdout, |t| t.cyan())
79 .to_string()
80}
81
82pub fn status_label(s: &str, sym: &str) -> String {
83 match sym {
84 "✓" => s
85 .if_supports_color(Stream::Stdout, |t| t.green())
86 .to_string(),
87 "↑" => s
88 .if_supports_color(Stream::Stdout, |t| t.cyan())
89 .to_string(),
90 "~" | "!" => s
91 .if_supports_color(Stream::Stdout, |t| t.yellow())
92 .to_string(),
93 "✗" => s.if_supports_color(Stream::Stdout, |t| t.red()).to_string(),
94 _ => s.to_string(),
95 }
96}
97
98const PRESETS_NOTE_LABEL_WIDTH: usize = 18; fn presets_note_value(plain_label: &str, styled_label: &str, value: &str) -> String {
102 let pad = " ".repeat(PRESETS_NOTE_LABEL_WIDTH.saturating_sub(plain_label.chars().count()) + 2);
103 format!("{styled_label}{pad}{value}")
104}
105
106fn presets_note(plain_label: &str, styled_label: &str, dir: &std::path::Path) -> String {
107 presets_note_value(plain_label, styled_label, &path_display::format(dir))
108}
109
110pub fn presets_source_note(value: &str) -> String {
112 use owo_colors::Style;
113 let label = "◈ Preset Source";
114 let styled = label
115 .if_supports_color(Stream::Stdout, |t| t.style(Style::new().bold().cyan()))
116 .to_string();
117 presets_note_value(label, &styled, value)
118}
119
120pub fn external_presets_note(dir: &std::path::Path) -> String {
122 presets_source_note(&format!("external · {}", path_display::format(dir)))
123}
124
125pub fn presets_overlay_note(dir: &std::path::Path) -> String {
127 use owo_colors::Style;
128 let label = "◈ Presets Overlay";
129 let styled = label
130 .if_supports_color(Stream::Stdout, |t| t.style(Style::new().bold().yellow()))
131 .to_string();
132 presets_note(label, &styled, dir)
133}
134
135pub fn shell_deployment_note(value: &str) -> String {
137 let label = "◈ Shell Deployment";
138 presets_note_value(label, label, value)
139}
140
141#[cfg(test)]
142mod tests {
143 use super::*;
144 use std::path::Path;
145
146 #[test]
147 fn external_presets_note_contains_path() {
148 let path = Path::new("/home/user/.custom/presets");
149 let note = external_presets_note(path);
150 assert!(
151 note.contains("/home/user/.custom/presets"),
152 "note should include the path: {note:?}"
153 );
154 }
155
156 #[test]
157 fn external_presets_note_uses_shared_source_label() {
158 let path = Path::new("/some/dir");
159 let note = external_presets_note(path);
160 assert!(
161 note.contains("Preset Source") && note.contains("external"),
162 "note should use the shared preset-source vocabulary: {note:?}"
163 );
164 }
165
166 #[test]
167 fn presets_note_values_use_the_same_column() {
168 let source = presets_source_note("built-in");
169 let external = external_presets_note(Path::new("/external"));
170 let overlay = presets_overlay_note(Path::new("/overlay"));
171 let deployment = shell_deployment_note("snapshot");
172
173 assert_eq!(source.find("built-in"), external.find("external"));
174 assert_eq!(source.find("built-in"), overlay.find("/overlay"));
175 assert_eq!(source.find("built-in"), deployment.find("snapshot"));
176 }
177}