todo_tree/printer/
utils.rs1use super::options::PrintOptions;
4use crate::core::TodoPriority;
5use crate::display::priority_to_color;
6use colored::Colorize;
7use std::path::Path;
8
9pub fn format_path(path: &Path, options: &PrintOptions) -> String {
12 if options.full_paths {
13 path.display().to_string()
14 } else if let Some(base) = &options.base_path {
15 path.strip_prefix(base)
16 .map(|p| p.display().to_string())
17 .unwrap_or_else(|_| path.display().to_string())
18 } else {
19 path.display().to_string()
20 }
21}
22
23pub fn make_clickable_link(path: &Path, line: usize, options: &PrintOptions) -> Option<String> {
27 if !options.clickable_links || !hyperlinks_supported() {
28 return None;
29 }
30
31 let display_path = format_path(path, options);
32 let abs_path = path.canonicalize().ok()?;
33 let file_url = format!("file://{}:{}", abs_path.display(), line);
34
35 let link = format!(
36 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
37 file_url,
38 if options.colored {
39 display_path.bold().to_string()
40 } else {
41 display_path
42 }
43 );
44
45 Some(link)
46}
47
48pub fn make_line_link(path: &Path, line: usize, options: &PrintOptions) -> Option<String> {
52 if !options.clickable_links || !hyperlinks_supported() {
53 return None;
54 }
55
56 let abs_path = path.canonicalize().ok()?;
57 let file_url = format!("file://{}:{}", abs_path.display(), line);
58 let display = format!("L{}", line);
59
60 let link = format!(
61 "\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
62 file_url,
63 if options.colored {
64 display.cyan().to_string()
65 } else {
66 display
67 }
68 );
69
70 Some(link)
71}
72
73pub fn colorize_tag(tag: &str, options: &PrintOptions) -> String {
76 if !options.colored {
77 return tag.to_string();
78 }
79
80 let color = priority_to_color(TodoPriority::from_tag(tag));
81 tag.color(color).bold().to_string()
82}
83
84fn hyperlinks_supported() -> bool {
87 supports_hyperlinks::on(supports_hyperlinks::Stream::Stdout)
88}
89
90#[cfg(test)]
91static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
92
93#[cfg(test)]
97struct ForceHyperlinkGuard(Option<std::ffi::OsString>);
98
99#[cfg(test)]
100impl ForceHyperlinkGuard {
101 fn set(value: &str) -> Self {
102 let saved = std::env::var_os("FORCE_HYPERLINK");
103 unsafe {
104 std::env::set_var("FORCE_HYPERLINK", value);
105 }
106 Self(saved)
107 }
108}
109
110#[cfg(test)]
111impl Drop for ForceHyperlinkGuard {
112 fn drop(&mut self) {
113 unsafe {
114 match &self.0 {
115 Some(v) => std::env::set_var("FORCE_HYPERLINK", v),
116 None => std::env::remove_var("FORCE_HYPERLINK"),
117 }
118 }
119 }
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use std::path::PathBuf;
126
127 fn options() -> PrintOptions {
128 PrintOptions::default()
129 }
130
131 #[test]
132 fn format_path_uses_full_path_when_requested() {
133 let opts = PrintOptions {
134 full_paths: true,
135 ..options()
136 };
137 let path = PathBuf::from("src/main.rs");
138 assert_eq!(format_path(&path, &opts), path.display().to_string());
139 }
140
141 #[test]
142 fn format_path_strips_base_path_when_set() {
143 let opts = PrintOptions {
144 base_path: Some(PathBuf::from("/repo")),
145 ..options()
146 };
147 let path = PathBuf::from("/repo/src/main.rs");
148 assert_eq!(format_path(&path, &opts), "src/main.rs");
149 }
150
151 #[test]
152 fn format_path_falls_back_when_strip_prefix_fails() {
153 let opts = PrintOptions {
154 base_path: Some(PathBuf::from("/other")),
155 ..options()
156 };
157 let path = PathBuf::from("/repo/src/main.rs");
158 assert_eq!(format_path(&path, &opts), path.display().to_string());
159 }
160
161 #[test]
162 fn format_path_uses_display_when_no_base_path() {
163 let path = PathBuf::from("src/main.rs");
164 assert_eq!(format_path(&path, &options()), path.display().to_string());
165 }
166
167 #[test]
168 fn colorize_tag_returns_plain_text_when_uncolored() {
169 let opts = PrintOptions {
170 colored: false,
171 ..options()
172 };
173 assert_eq!(colorize_tag("TODO", &opts), "TODO");
174 }
175
176 #[test]
177 fn colorize_tag_includes_tag_text_when_colored() {
178 let opts = PrintOptions {
179 colored: true,
180 ..options()
181 };
182 assert!(colorize_tag("TODO", &opts).contains("TODO"));
183 }
184
185 #[test]
186 fn make_clickable_link_none_when_disabled() {
187 let opts = PrintOptions {
188 clickable_links: false,
189 ..options()
190 };
191 assert!(make_clickable_link(Path::new("src/main.rs"), 1, &opts).is_none());
192 }
193
194 #[test]
195 fn make_line_link_none_when_disabled() {
196 let opts = PrintOptions {
197 clickable_links: false,
198 ..options()
199 };
200 assert!(make_line_link(Path::new("src/main.rs"), 1, &opts).is_none());
201 }
202
203 #[test]
204 fn hyperlinks_supported_false_when_forced_off() {
205 let _lock = ENV_LOCK.lock().unwrap();
206 let _guard = ForceHyperlinkGuard::set("0");
207
208 assert!(!hyperlinks_supported());
209 }
210
211 #[test]
212 fn hyperlinks_supported_true_when_forced_on() {
213 let _lock = ENV_LOCK.lock().unwrap();
214 let _guard = ForceHyperlinkGuard::set("1");
215
216 assert!(hyperlinks_supported());
217 }
218
219 #[test]
220 fn make_clickable_link_none_when_terminal_unsupported() {
221 let _lock = ENV_LOCK.lock().unwrap();
222 let _guard = ForceHyperlinkGuard::set("0");
223
224 let opts = options();
225 assert!(make_clickable_link(Path::new("src/main.rs"), 1, &opts).is_none());
226 }
227
228 #[test]
229 fn make_clickable_link_none_when_path_does_not_exist() {
230 let _lock = ENV_LOCK.lock().unwrap();
231 let _guard = ForceHyperlinkGuard::set("1");
232
233 let opts = options();
234 let missing = Path::new("/definitely/not/a/real/path/hopefully.rs");
235 assert!(make_clickable_link(missing, 1, &opts).is_none());
236 }
237
238 #[test]
239 fn make_clickable_link_some_for_existing_path_on_supported_terminal() {
240 let _lock = ENV_LOCK.lock().unwrap();
241 let _guard = ForceHyperlinkGuard::set("1");
242
243 let opts = PrintOptions {
244 colored: true,
245 ..options()
246 };
247 let link = make_clickable_link(Path::new("Cargo.toml"), 1, &opts);
248 assert!(link.is_some());
249 assert!(link.unwrap().contains("\x1b]8;;file://"));
250 }
251
252 #[test]
253 fn make_clickable_link_uncolored_variant() {
254 let _lock = ENV_LOCK.lock().unwrap();
255 let _guard = ForceHyperlinkGuard::set("1");
256
257 let opts = PrintOptions {
258 colored: false,
259 ..options()
260 };
261 let link = make_clickable_link(Path::new("Cargo.toml"), 1, &opts);
262 assert!(link.is_some());
263 }
264
265 #[test]
266 fn make_line_link_some_for_existing_path_on_supported_terminal() {
267 let _lock = ENV_LOCK.lock().unwrap();
268 let _guard = ForceHyperlinkGuard::set("1");
269
270 let opts = PrintOptions {
271 colored: true,
272 ..options()
273 };
274 let link = make_line_link(Path::new("Cargo.toml"), 42, &opts);
275 assert!(link.is_some());
276 assert!(link.unwrap().contains("L42"));
277 }
278
279 #[test]
280 fn make_line_link_uncolored_variant() {
281 let _lock = ENV_LOCK.lock().unwrap();
282 let _guard = ForceHyperlinkGuard::set("1");
283
284 let opts = PrintOptions {
285 colored: false,
286 ..options()
287 };
288 let link = make_line_link(Path::new("Cargo.toml"), 42, &opts);
289 assert!(link.is_some());
290 }
291
292 #[test]
293 fn make_line_link_none_when_path_does_not_exist() {
294 let _lock = ENV_LOCK.lock().unwrap();
295 let _guard = ForceHyperlinkGuard::set("1");
296
297 let opts = options();
298 let missing = Path::new("/definitely/not/a/real/path/hopefully.rs");
299 assert!(make_line_link(missing, 1, &opts).is_none());
300 }
301}