Files
fd/src/hyperlink.rs
Joseph Adams cd8fedc183 Fix Windows hyperlinks for paths with spaces
Fixes #1843

Changed the host() function for Windows to return '/' instead of ''
to generate RFC 3986 compliant file URLs with three slashes.

Before: file://C:/test/hi%20there.txt (broken)
After:  file:///C:/test/hi%20there.txt (works)

This fixes hyperlinks failing on Windows when file paths contain
spaces or other characters that require percent-encoding.
2026-01-14 23:53:43 +01:00

88 lines
2.3 KiB
Rust

use crate::filesystem::absolute_path;
use std::fmt::{self, Formatter, Write};
use std::path::{Path, PathBuf};
pub(crate) struct PathUrl(PathBuf);
impl PathUrl {
pub(crate) fn new(path: &Path) -> Option<PathUrl> {
Some(PathUrl(absolute_path(path).ok()?))
}
}
impl fmt::Display for PathUrl {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "file://{}", host())?;
let bytes = self.0.as_os_str().as_encoded_bytes();
for &byte in bytes.iter() {
encode(f, byte)?;
}
Ok(())
}
}
fn encode(f: &mut Formatter, byte: u8) -> fmt::Result {
// NOTE:
// Most terminals can handle non-ascii unicode characters in a file url fine. But on some OSes (notably
// windows), the encoded bytes of the path may not be valid UTF-8. Since we don't know if a
// byte >= 128 is part of a valid UTF-8 encoding or not, we just percent encode any non-ascii
// byte.
// Percent encoding these bytes is probably safer anyway.
match byte {
b'0'..=b'9' | b'A'..=b'Z' | b'a'..=b'z' | b'/' | b':' | b'-' | b'.' | b'_' | b'~' => {
f.write_char(byte.into())
}
#[cfg(windows)]
b'\\' => f.write_char('/'),
_ => {
write!(f, "%{byte:02X}")
}
}
}
#[cfg(unix)]
fn host() -> &'static str {
use std::sync::OnceLock;
static HOSTNAME: OnceLock<String> = OnceLock::new();
HOSTNAME
.get_or_init(|| {
nix::unistd::gethostname()
.ok()
.and_then(|h| h.into_string().ok())
.unwrap_or_default()
})
.as_ref()
}
#[cfg(not(unix))]
const fn host() -> &'static str {
"/"
}
#[cfg(test)]
mod test {
use super::*;
// This allows us to test the encoding without having to worry about the host, or absolute path
struct Encoded(&'static str);
impl fmt::Display for Encoded {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
for byte in self.0.bytes() {
encode(f, byte)?;
}
Ok(())
}
}
#[test]
fn test_unicode_encoding() {
assert_eq!(
Encoded("$*\x1bßé/∫😃\x07").to_string(),
"%24%2A%1B%C3%9F%C3%A9/%E2%88%AB%F0%9F%98%83%07",
);
}
}