use std::io::IsTerminal;
const OSC8_OPEN: &str = "\x1b]8;;";
const OSC8_CLOSE: &str = "\x1b]8;;\x07"; const ST: char = '\x07';
pub fn render_link(text: &str, url: &str) -> String {
if supports_osc8() {
render_link_osc8(text, url)
} else {
render_link_plain(text, url)
}
}
pub fn render_url(url: &str) -> String {
render_link(url, url)
}
pub fn render_link_osc8(text: &str, url: &str) -> String {
let safe_url = strip_controls(url);
let mut safe_text = strip_controls(text);
if safe_text.is_empty() {
safe_text = safe_url.clone();
}
format!("{OSC8_OPEN}{safe_url}{ST}{safe_text}{OSC8_CLOSE}")
}
pub fn render_link_plain(text: &str, url: &str) -> String {
let safe_url = strip_controls(url);
let safe_text = strip_controls(text);
if safe_text.is_empty() || safe_text == safe_url {
format!("(link: {safe_url})")
} else {
format!("{safe_text} ({safe_url})")
}
}
pub fn supports_osc8() -> bool {
if env_flag("NEWT_FORCE_OSC8") {
return true;
}
if env_flag("NEWT_NO_OSC8") {
return false;
}
is_tty_stdout()
}
fn is_tty_stdout() -> bool {
std::io::stdout().is_terminal()
}
fn env_flag(name: &str) -> bool {
std::env::var(name)
.map(|v| {
let v = v.trim();
!(v.is_empty()
|| v == "0"
|| v.eq_ignore_ascii_case("false")
|| v.eq_ignore_ascii_case("no")
|| v.eq_ignore_ascii_case("off"))
})
.unwrap_or(false)
}
fn strip_controls(s: &str) -> String {
s.chars().filter(|ch| !ch.is_control()).collect()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_render_link_plain_fallback() {
let result = render_link_plain("View issue", "https://github.com/foo/bar/issues/771");
assert_eq!(result, "View issue (https://github.com/foo/bar/issues/771)");
}
#[test]
fn test_render_link_empty_text() {
let result = render_link_plain("", "https://example.com");
assert_eq!(result, "(link: https://example.com)");
}
#[test]
fn test_render_url() {
let url = "https://github.com/foo/bar/issues/771";
let result = render_link_plain(url, url);
assert_eq!(result, "(link: https://github.com/foo/bar/issues/771)");
}
#[test]
fn test_render_link_osc8_format() {
let text = "click me";
let url = "https://example.com/path?q=1&r=2&x=y";
let expected_open = format!("\x1b]8;;{url}\x07");
let expected_close = "\x1b]8;;\x07";
assert_eq!(
render_link_osc8(text, url),
format!("{expected_open}{text}{expected_close}")
);
}
#[test]
fn test_special_chars_in_url() {
let url = "https://example.com?a=1&b=2#frag";
let result = render_link_plain("link", url);
assert!(result.contains(url));
}
#[test]
fn test_unicode_in_text() {
let result = render_link_plain("🔗 click here", "https://example.com");
assert!(result.contains("🔗 click here"));
}
#[test]
fn test_long_url_truncation_not_needed() {
let long_url = format!("https://example.com/{}", "a".repeat(1000));
let result = render_link_osc8("long", &long_url);
assert!(result.contains(&long_url));
}
#[test]
fn test_reset_sequence_is_correct() {
let result = render_link_osc8("x", "https://example.com");
assert!(result.ends_with("\x1b]8;;\x07"));
}
#[test]
fn test_multiple_links_in_output() {
let a = render_link_osc8("first", "https://a.com");
let b = render_link_osc8("second", "https://b.com");
let combined = format!("{a}\n{b}");
assert_eq!(combined.matches("\x1b]8;;").count(), 4);
}
#[test]
fn test_render_link_with_id_param() {
let text = "issue";
let url = "https://github.com/foo/bar/issues/771";
let result = render_link_osc8(text, url);
assert!(result.starts_with("\x1b]8;;"));
assert!(result.contains(url));
}
#[test]
fn test_fallback_format_when_no_tty() {
let result = render_link_plain("https://example.com", "https://example.com");
assert_eq!(result, "(link: https://example.com)");
}
#[test]
fn test_fallback_format_with_different_text_and_url() {
let result = render_link_plain("Click here", "https://example.com");
assert_eq!(result, "Click here (https://example.com)");
}
#[test]
fn test_osc8_escape_sequence_structure() {
let result = render_link_osc8("hello", "http://x.com");
assert_eq!(&result[0..1], "\x1b"); assert!(result.contains("\x1b]8;;")); assert!(result.ends_with("\x07")); }
#[test]
fn test_url_with_fragment() {
let url = "https://example.com/page#section";
let result = render_link_osc8("page", url);
assert!(result.contains(url));
}
#[test]
fn test_url_with_query_params() {
let url = "https://example.com/search?q=rust&lang=en";
let result = render_link_osc8("search", url);
assert!(result.contains(url));
}
#[test]
fn test_control_chars_are_stripped_from_osc8_fields() {
let result = render_link_osc8("hi\x1b[31m", "https://example.com/\x07bad");
assert!(!result.contains("\x1b[31m"), "{result:?}");
assert!(!result.contains("\x07bad"), "{result:?}");
assert!(result.contains("https://example.com/bad"), "{result:?}");
assert!(result.contains("hi[31m"), "{result:?}");
}
}