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 bold_cyan(s: &str) -> String {
73 use owo_colors::Style;
74 s.if_supports_color(Stream::Stdout, |t| t.style(Style::new().bold().cyan()))
75 .to_string()
76}
77
78pub fn dim(s: &str) -> String {
79 s.if_supports_color(Stream::Stdout, |t| t.dimmed())
80 .to_string()
81}
82
83pub fn cyan(s: &str) -> String {
84 s.if_supports_color(Stream::Stdout, |t| t.cyan())
85 .to_string()
86}
87
88pub fn status_label(s: &str, sym: &str) -> String {
89 match sym {
90 "✓" => s
91 .if_supports_color(Stream::Stdout, |t| t.green())
92 .to_string(),
93 "↑" => s
94 .if_supports_color(Stream::Stdout, |t| t.cyan())
95 .to_string(),
96 "~" | "!" => s
97 .if_supports_color(Stream::Stdout, |t| t.yellow())
98 .to_string(),
99 "✗" => s.if_supports_color(Stream::Stdout, |t| t.red()).to_string(),
100 _ => s.to_string(),
101 }
102}
103
104const PRESETS_NOTE_LABEL_WIDTH: usize = 18; fn presets_note_value(plain_label: &str, styled_label: &str, value: &str) -> String {
108 let pad = " ".repeat(PRESETS_NOTE_LABEL_WIDTH.saturating_sub(plain_label.chars().count()) + 2);
109 format!("{styled_label}{pad}{value}")
110}
111
112fn presets_note(plain_label: &str, styled_label: &str, dir: &std::path::Path) -> String {
113 presets_note_value(plain_label, styled_label, &path_display::format(dir))
114}
115
116pub fn presets_source_note(value: &str) -> String {
118 use owo_colors::Style;
119 let label = "◈ Preset Source";
120 let styled = label
121 .if_supports_color(Stream::Stdout, |t| t.style(Style::new().bold().cyan()))
122 .to_string();
123 presets_note_value(label, &styled, value)
124}
125
126pub fn external_presets_note(dir: &std::path::Path) -> String {
128 presets_source_note(&format!("external · {}", path_display::format(dir)))
129}
130
131pub fn presets_overlay_note(dir: &std::path::Path) -> String {
133 use owo_colors::Style;
134 let label = "◈ Presets Overlay";
135 let styled = label
136 .if_supports_color(Stream::Stdout, |t| t.style(Style::new().bold().yellow()))
137 .to_string();
138 presets_note(label, &styled, dir)
139}
140
141pub fn shell_deployment_note(value: &str) -> String {
143 let label = "◈ Shell Deployment";
144 presets_note_value(label, label, value)
145}
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150 use std::path::Path;
151
152 #[test]
153 fn external_presets_note_contains_path() {
154 let path = Path::new("/home/user/.custom/presets");
155 let note = external_presets_note(path);
156 assert!(
157 note.contains("/home/user/.custom/presets"),
158 "note should include the path: {note:?}"
159 );
160 }
161
162 #[test]
163 fn external_presets_note_uses_shared_source_label() {
164 let path = Path::new("/some/dir");
165 let note = external_presets_note(path);
166 assert!(
167 note.contains("Preset Source") && note.contains("external"),
168 "note should use the shared preset-source vocabulary: {note:?}"
169 );
170 }
171
172 #[test]
173 fn presets_note_values_use_the_same_column() {
174 let source = presets_source_note("built-in");
175 let external = external_presets_note(Path::new("/external"));
176 let overlay = presets_overlay_note(Path::new("/overlay"));
177 let deployment = shell_deployment_note("snapshot");
178
179 assert_eq!(source.find("built-in"), external.find("external"));
180 assert_eq!(source.find("built-in"), overlay.find("/overlay"));
181 assert_eq!(source.find("built-in"), deployment.find("snapshot"));
182 }
183}