docterm 0.2.0

A TUI-first documentation browser for Dash/Zeal docsets, optimized for the terminal.
use crate::config;

/// Resolve the library database path: config value, or XDG / HOME fallback.
pub fn resolve_db_path(config: &config::Config) -> std::path::PathBuf {
    if let Some(p) = &config.db_path {
        return p.clone();
    }
    let home = std::env::var("HOME")
        .map(std::path::PathBuf::from)
        .unwrap_or_else(|_| std::path::PathBuf::from("."));
    home.join(".local/share/docterm/library.db")
}

/// Replace ASCII control characters (except `\t` and `\n`) and the ANSI escape
/// character with U+FFFD REPLACEMENT CHARACTER.
///
/// Use before writing feed-derived strings (docset names, versions) to stdout
/// via `println!` — a hostile feed entry containing a bare `\x1b[` could
/// otherwise inject terminal escape sequences.  Ratatui already sanitises text
/// during its own render, so this is only needed for CLI paths that write
/// directly to the terminal.
pub fn sanitize_for_terminal(s: &str) -> String {
    s.chars()
        .map(|c| match c {
            '\t' | '\n' => c,
            c if c.is_control() => '\u{FFFD}',
            c => c,
        })
        .collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sanitize_passes_printable_ascii() {
        assert_eq!(sanitize_for_terminal("Rust 1.94"), "Rust 1.94");
    }

    #[test]
    fn sanitize_passes_unicode_letters_and_punctuation() {
        assert_eq!(sanitize_for_terminal("café — β"), "café — β");
    }

    #[test]
    fn sanitize_keeps_tab_and_newline() {
        assert_eq!(sanitize_for_terminal("a\tb\nc"), "a\tb\nc");
    }

    #[test]
    fn sanitize_replaces_ansi_csi_intro() {
        // \x1b[31m is the ANSI red foreground sequence introducer.
        let dirty = "\x1b[31mred\x1b[0m";
        let clean = sanitize_for_terminal(dirty);
        assert!(!clean.contains('\x1b'));
        assert!(clean.contains('\u{FFFD}'));
    }

    #[test]
    fn sanitize_replaces_bel_and_backspace() {
        assert!(!sanitize_for_terminal("a\x07b").contains('\x07'));
        assert!(!sanitize_for_terminal("a\x08b").contains('\x08'));
    }

    #[test]
    fn sanitize_replaces_delete_char() {
        assert!(!sanitize_for_terminal("a\x7fb").contains('\x7f'));
    }
}