use super::options::PrintOptions;
use crate::core::TodoPriority;
use crate::display::priority_to_color;
use colored::Colorize;
use std::path::Path;
pub fn format_path(path: &Path, options: &PrintOptions) -> String {
if options.full_paths {
path.display().to_string()
} else if let Some(base) = &options.base_path {
path.strip_prefix(base)
.map(|p| p.display().to_string())
.unwrap_or_else(|_| path.display().to_string())
} else {
path.display().to_string()
}
}
pub fn make_clickable_link(path: &Path, line: usize, options: &PrintOptions) -> Option<String> {
if !options.clickable_links || !hyperlinks_supported() {
return None;
}
let display_path = format_path(path, options);
let abs_path = path.canonicalize().ok()?;
let file_url = format!("file://{}:{}", abs_path.display(), line);
let link = format!(
"\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
file_url,
if options.colored {
display_path.bold().to_string()
} else {
display_path
}
);
Some(link)
}
pub fn make_line_link(path: &Path, line: usize, options: &PrintOptions) -> Option<String> {
if !options.clickable_links || !hyperlinks_supported() {
return None;
}
let abs_path = path.canonicalize().ok()?;
let file_url = format!("file://{}:{}", abs_path.display(), line);
let display = format!("L{}", line);
let link = format!(
"\x1b]8;;{}\x1b\\{}\x1b]8;;\x1b\\",
file_url,
if options.colored {
display.cyan().to_string()
} else {
display
}
);
Some(link)
}
pub fn colorize_tag(tag: &str, options: &PrintOptions) -> String {
if !options.colored {
return tag.to_string();
}
let color = priority_to_color(TodoPriority::from_tag(tag));
tag.color(color).bold().to_string()
}
fn hyperlinks_supported() -> bool {
supports_hyperlinks::on(supports_hyperlinks::Stream::Stdout)
}
#[cfg(test)]
static ENV_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[cfg(test)]
struct ForceHyperlinkGuard(Option<std::ffi::OsString>);
#[cfg(test)]
impl ForceHyperlinkGuard {
fn set(value: &str) -> Self {
let saved = std::env::var_os("FORCE_HYPERLINK");
unsafe {
std::env::set_var("FORCE_HYPERLINK", value);
}
Self(saved)
}
}
#[cfg(test)]
impl Drop for ForceHyperlinkGuard {
fn drop(&mut self) {
unsafe {
match &self.0 {
Some(v) => std::env::set_var("FORCE_HYPERLINK", v),
None => std::env::remove_var("FORCE_HYPERLINK"),
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn options() -> PrintOptions {
PrintOptions::default()
}
#[test]
fn format_path_uses_full_path_when_requested() {
let opts = PrintOptions {
full_paths: true,
..options()
};
let path = PathBuf::from("src/main.rs");
assert_eq!(format_path(&path, &opts), path.display().to_string());
}
#[test]
fn format_path_strips_base_path_when_set() {
let opts = PrintOptions {
base_path: Some(PathBuf::from("/repo")),
..options()
};
let path = PathBuf::from("/repo/src/main.rs");
assert_eq!(format_path(&path, &opts), "src/main.rs");
}
#[test]
fn format_path_falls_back_when_strip_prefix_fails() {
let opts = PrintOptions {
base_path: Some(PathBuf::from("/other")),
..options()
};
let path = PathBuf::from("/repo/src/main.rs");
assert_eq!(format_path(&path, &opts), path.display().to_string());
}
#[test]
fn format_path_uses_display_when_no_base_path() {
let path = PathBuf::from("src/main.rs");
assert_eq!(format_path(&path, &options()), path.display().to_string());
}
#[test]
fn colorize_tag_returns_plain_text_when_uncolored() {
let opts = PrintOptions {
colored: false,
..options()
};
assert_eq!(colorize_tag("TODO", &opts), "TODO");
}
#[test]
fn colorize_tag_includes_tag_text_when_colored() {
let opts = PrintOptions {
colored: true,
..options()
};
assert!(colorize_tag("TODO", &opts).contains("TODO"));
}
#[test]
fn make_clickable_link_none_when_disabled() {
let opts = PrintOptions {
clickable_links: false,
..options()
};
assert!(make_clickable_link(Path::new("src/main.rs"), 1, &opts).is_none());
}
#[test]
fn make_line_link_none_when_disabled() {
let opts = PrintOptions {
clickable_links: false,
..options()
};
assert!(make_line_link(Path::new("src/main.rs"), 1, &opts).is_none());
}
#[test]
fn hyperlinks_supported_false_when_forced_off() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("0");
assert!(!hyperlinks_supported());
}
#[test]
fn hyperlinks_supported_true_when_forced_on() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
assert!(hyperlinks_supported());
}
#[test]
fn make_clickable_link_none_when_terminal_unsupported() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("0");
let opts = options();
assert!(make_clickable_link(Path::new("src/main.rs"), 1, &opts).is_none());
}
#[test]
fn make_clickable_link_none_when_path_does_not_exist() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = options();
let missing = Path::new("/definitely/not/a/real/path/hopefully.rs");
assert!(make_clickable_link(missing, 1, &opts).is_none());
}
#[test]
fn make_clickable_link_some_for_existing_path_on_supported_terminal() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = PrintOptions {
colored: true,
..options()
};
let link = make_clickable_link(Path::new("Cargo.toml"), 1, &opts);
assert!(link.is_some());
assert!(link.unwrap().contains("\x1b]8;;file://"));
}
#[test]
fn make_clickable_link_uncolored_variant() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = PrintOptions {
colored: false,
..options()
};
let link = make_clickable_link(Path::new("Cargo.toml"), 1, &opts);
assert!(link.is_some());
}
#[test]
fn make_line_link_some_for_existing_path_on_supported_terminal() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = PrintOptions {
colored: true,
..options()
};
let link = make_line_link(Path::new("Cargo.toml"), 42, &opts);
assert!(link.is_some());
assert!(link.unwrap().contains("L42"));
}
#[test]
fn make_line_link_uncolored_variant() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = PrintOptions {
colored: false,
..options()
};
let link = make_line_link(Path::new("Cargo.toml"), 42, &opts);
assert!(link.is_some());
}
#[test]
fn make_line_link_none_when_path_does_not_exist() {
let _lock = ENV_LOCK.lock().unwrap();
let _guard = ForceHyperlinkGuard::set("1");
let opts = options();
let missing = Path::new("/definitely/not/a/real/path/hopefully.rs");
assert!(make_line_link(missing, 1, &opts).is_none());
}
}