Skip to main content

vtcode_core/utils/ansi/
links.rs

1//! File-opener configuration and link/table helpers.
2
3use crate::ui::tui::InlineMessageKind;
4use std::sync::{Mutex, OnceLock};
5use unicode_width::UnicodeWidthStr;
6use url::Url;
7use vtcode_commons::{parse_editor_target, resolve_editor_path};
8
9static FILE_OPENER: OnceLock<Mutex<vtcode_config::FileOpener>> = OnceLock::new();
10
11pub fn apply_file_opener_config(file_opener: vtcode_config::FileOpener) {
12    let cell = FILE_OPENER.get_or_init(|| Mutex::new(vtcode_config::FileOpener::None));
13    if let Ok(mut guard) = cell.lock() {
14        *guard = file_opener;
15    }
16}
17
18pub(super) fn current_file_opener() -> vtcode_config::FileOpener {
19    FILE_OPENER
20        .get()
21        .map(|cell| *cell.lock().unwrap_or_else(|e| e.into_inner()))
22        .unwrap_or(vtcode_config::FileOpener::None)
23}
24
25pub(super) fn make_clickable_target(target: &str) -> Option<String> {
26    let trimmed = target.trim();
27    if trimmed.is_empty() {
28        return None;
29    }
30    if is_remote_link_target(trimmed) {
31        return Some(trimmed.to_string());
32    }
33
34    let opener = current_file_opener();
35    let scheme = opener.scheme()?;
36    let target = parse_editor_target(trimmed)?;
37    let cwd = std::env::current_dir().ok()?;
38    let file_url = Url::from_file_path(resolve_editor_path(target.path(), &cwd)).ok()?;
39    let suffix = target.location_suffix().unwrap_or("");
40    Some(format!("{scheme}://file{}{}", file_url.as_str().trim_start_matches("file://"), suffix))
41}
42
43pub(super) fn should_strip_inline_local_link_underline(target: &str) -> bool {
44    let trimmed = target.trim();
45    if trimmed.is_empty() {
46        return false;
47    }
48    if is_remote_link_target(trimmed) {
49        return false;
50    }
51    match Url::parse(trimmed) {
52        Ok(url) => url.scheme() == "file",
53        Err(_) => true,
54    }
55}
56
57fn is_remote_link_target(target: &str) -> bool {
58    target.starts_with("http://") || target.starts_with("https://")
59}
60
61pub(super) fn terminal_table_content_width(indent: &str) -> Option<usize> {
62    crossterm::terminal::size()
63        .ok()
64        .map(|(width, _)| usize::from(width).saturating_sub(UnicodeWidthStr::width(indent)))
65}
66
67pub(super) fn transcript_table_frame_width(kind: InlineMessageKind, agent_label_frame_width: usize) -> usize {
68    match kind {
69        // Agent messages receive ` •` plus one content padding cell during
70        // transcript reflow. Other table-bearing paths use their block prefix;
71        // their rendered table lines are body/detail lines without a right edge.
72        InlineMessageKind::Agent => UnicodeWidthStr::width(" • ") + agent_label_frame_width,
73        InlineMessageKind::Tool => UnicodeWidthStr::width("    "),
74        InlineMessageKind::Pty => UnicodeWidthStr::width("  "),
75        InlineMessageKind::Policy
76        | InlineMessageKind::User
77        | InlineMessageKind::Info
78        | InlineMessageKind::Error
79        | InlineMessageKind::Warning => 0,
80    }
81}