Skip to main content

f00_format/
hyperlink.rs

1//! OSC 8 hyperlinks for file names (`ls --hyperlink`).
2
3use std::path::Path;
4
5/// Wrap `text` in an OSC 8 hyperlink to `path` when enabled.
6pub fn hyperlink_name(path: &Path, text: &str, enabled: bool) -> String {
7    if !enabled {
8        return text.to_string();
9    }
10    let uri = file_uri(path);
11    // OSC 8: ESC ] 8 ; params ; URI ST  text  ESC ] 8 ;; ST
12    format!("\x1b]8;;{uri}\x1b\\{text}\x1b]8;;\x1b\\")
13}
14
15fn file_uri(path: &Path) -> String {
16    let abs = if path.is_absolute() {
17        path.to_path_buf()
18    } else {
19        std::env::current_dir()
20            .map(|cwd| cwd.join(path))
21            .unwrap_or_else(|_| path.to_path_buf())
22    };
23    let host = hostname();
24    let mut uri = String::from("file://");
25    if !host.is_empty() {
26        uri.push_str(&host);
27    }
28    // Percent-encode path bytes.
29    for b in abs.to_string_lossy().bytes() {
30        match b {
31            b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'/' | b'.' | b'-' | b'_' | b'~' => {
32                uri.push(b as char);
33            }
34            _ => uri.push_str(&format!("%{b:02X}")),
35        }
36    }
37    uri
38}
39
40fn hostname() -> String {
41    std::env::var("HOSTNAME")
42        .or_else(|_| std::env::var("COMPUTERNAME"))
43        .unwrap_or_default()
44}
45
46#[cfg(test)]
47mod tests {
48    use super::*;
49    use std::path::PathBuf;
50
51    #[test]
52    fn disabled_is_passthrough() {
53        assert_eq!(hyperlink_name(Path::new("/tmp/x"), "x", false), "x");
54    }
55
56    #[test]
57    fn enabled_wraps_osc8() {
58        let s = hyperlink_name(&PathBuf::from("/tmp/x"), "x", true);
59        assert!(s.contains("\x1b]8;;file://"), "{s:?}");
60        assert!(s.contains("/tmp/x") || s.contains("%"), "{s:?}");
61        assert!(s.ends_with("\x1b]8;;\x1b\\"), "{s:?}");
62    }
63}