#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) struct ItemKey(String);
impl ItemKey {
pub(crate) fn new(raw: impl Into<String>) -> Self {
ItemKey(raw.into())
}
pub(crate) fn as_str(&self) -> &str {
&self.0
}
pub(crate) fn display(&self) -> String {
strip_ansi(&self.0)
}
}
impl std::borrow::Borrow<str> for ItemKey {
fn borrow(&self) -> &str {
&self.0
}
}
impl PartialEq<str> for ItemKey {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for ItemKey {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
impl PartialEq<ItemKey> for str {
fn eq(&self, other: &ItemKey) -> bool {
self == other.0
}
}
impl PartialEq<ItemKey> for &str {
fn eq(&self, other: &ItemKey) -> bool {
*self == other.0
}
}
impl From<ItemKey> for String {
fn from(k: ItemKey) -> String {
k.0
}
}
fn is_control(c: char) -> bool {
c < '\x20' || ('\x7f'..='\u{009f}').contains(&c)
}
fn is_blocked_unicode(c: char) -> bool {
matches!(
c,
'\u{202A}'..='\u{202E}' | '\u{2066}'..='\u{2069}'
| '\u{2028}' | '\u{2029}'
| '\u{200E}' | '\u{200F}' | '\u{061C}'
| '\u{200B}' | '\u{2060}' | '\u{FEFF}'
| '\u{00AD}'
| '\u{180E}'
| '\u{2061}'..='\u{2064}'
| '\u{206A}'..='\u{206F}'
| '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}'
| '\u{FFF9}'..='\u{FFFB}'
| '\u{115F}' | '\u{3164}'
| '\u{E0000}'..='\u{E007F}'
)
}
pub(crate) fn strip_ansi(s: &str) -> String {
let bytes = strip_ansi_escapes::strip(s);
let text = String::from_utf8_lossy(&bytes);
let mut out = String::with_capacity(text.len());
let mut in_control_run = false;
for c in text.chars() {
if is_blocked_unicode(c) {
continue;
}
if is_control(c) {
if !in_control_run {
out.push(' ');
in_control_run = true;
}
continue;
}
out.push(c);
in_control_run = false;
}
out
}
pub(crate) fn display_path(p: &std::path::Path) -> String {
strip_ansi(&p.display().to_string())
}
pub(crate) fn has_blocked_chars(s: &str) -> bool {
s.chars().any(|c| is_control(c) || is_blocked_unicode(c))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn strip_ansi_removes_escape_sequences() {
assert_eq!(strip_ansi("\x1b[31mhello\x1b[0m"), "hello");
assert_eq!(strip_ansi("\x1b[1;32mgreen bold\x1b[0m"), "green bold");
}
#[test]
fn strip_ansi_removes_bidi_overrides() {
assert_eq!(strip_ansi("pay \u{202E}oot"), "pay oot");
assert_eq!(strip_ansi("\u{202A}\u{202B}\u{202C}\u{202D}\u{202E}"), "");
assert_eq!(strip_ansi("\u{2066}\u{2067}\u{2068}\u{2069}"), "");
}
#[test]
fn strip_ansi_removes_line_and_para_separators() {
assert_eq!(strip_ansi("line\u{2028}break"), "linebreak");
assert_eq!(strip_ansi("para\u{2029}sep"), "parasep");
}
#[test]
fn strip_ansi_preserves_printable_ascii() {
let s = "hello world 123!";
assert_eq!(strip_ansi(s), s);
}
#[test]
fn strip_ansi_preserves_printable_unicode() {
assert_eq!(strip_ansi("hello\u{00e9}"), "hello\u{00e9}");
assert_eq!(strip_ansi("caf\u{00e9}"), "caf\u{00e9}");
}
#[test]
fn strip_ansi_removes_c0_controls() {
assert_eq!(strip_ansi("a\x00b"), "ab");
assert_eq!(strip_ansi("a\x01b"), "ab");
assert_eq!(strip_ansi("a\x1fb"), "ab");
assert_eq!(strip_ansi("a b"), "a b");
}
#[test]
fn strip_ansi_collapses_newline_runs_to_one_space_not_joining_words() {
assert_eq!(
strip_ansi("make build\nmake install"),
"make build make install"
);
assert_eq!(strip_ansi("line one\r\nline two"), "line one line two");
assert_eq!(strip_ansi("a\n\n\nb"), "a b");
assert_eq!(strip_ansi("\ntail"), " tail");
assert_eq!(strip_ansi("head\n"), "head ");
}
#[test]
fn strip_ansi_blocked_unicode_still_vanishes_without_a_space() {
assert_eq!(strip_ansi("pay\u{202E}oot"), "payoot");
assert_eq!(strip_ansi("wo\u{200B}rd"), "word");
assert_eq!(strip_ansi("line\u{2028}break"), "linebreak");
}
#[test]
fn strip_ansi_removes_directional_marks_and_zero_width() {
assert_eq!(strip_ansi("a\u{200E}b"), "ab", "LRM");
assert_eq!(strip_ansi("a\u{200F}b"), "ab", "RLM");
assert_eq!(strip_ansi("a\u{061C}b"), "ab", "ALM");
assert_eq!(strip_ansi("a\u{200B}b"), "ab", "zero-width space");
assert_eq!(strip_ansi("a\u{2060}b"), "ab", "word joiner");
assert_eq!(
strip_ansi("a\u{FEFF}b"),
"ab",
"BOM / zero-width no-break space"
);
}
#[test]
fn strip_ansi_removes_m5_broadened_set() {
assert_eq!(strip_ansi("a\u{00AD}b"), "ab", "soft hyphen");
assert_eq!(strip_ansi("a\u{180E}b"), "ab", "Mongolian vowel separator");
assert_eq!(
strip_ansi("a\u{2061}\u{2062}\u{2063}\u{2064}b"),
"ab",
"invisible math operators"
);
assert_eq!(
strip_ansi("a\u{206A}b"),
"ab",
"deprecated Cf format character"
);
assert_eq!(strip_ansi("a\u{FE0F}b"), "ab", "variation selector");
assert_eq!(
strip_ansi("a\u{E0100}b"),
"ab",
"supplementary variation selector"
);
assert_eq!(strip_ansi("a\u{FFF9}b"), "ab", "interlinear annotation");
assert_eq!(strip_ansi("a\u{115F}b"), "ab", "Hangul choseong filler");
assert_eq!(strip_ansi("a\u{3164}b"), "ab", "Hangul filler");
assert_eq!(
strip_ansi("review\u{E0041}"),
"review",
"Unicode tag block character"
);
assert_eq!(
strip_ansi("\u{E0001}\u{E007F}"),
"",
"tag-block language tag and cancel tag"
);
}
#[test]
fn strip_ansi_empty_string() {
assert_eq!(strip_ansi(""), "");
}
#[test]
fn has_blocked_chars_flags_exactly_what_strip_removes() {
let ok = ["", "hello", "my-skill", "caf\u{00e9}", "a.b_c-2"];
for s in ok {
assert!(!has_blocked_chars(s), "{s:?} should be clean");
assert_eq!(
has_blocked_chars(s),
strip_ansi(s) != s,
"has_blocked_chars/strip_ansi disagree on {s:?}"
);
}
let bad = [
"a\x00b",
"a\nb",
"a\x1b[0mb",
"pay\u{202E}oot",
"a\u{2066}b",
"a\u{200B}b",
"a\u{200E}b",
"a\u{FEFF}b",
"a\u{2028}b",
"a\u{0085}b",
"a\u{00AD}b",
"a\u{180E}b",
"a\u{2061}b",
"a\u{206A}b",
"a\u{FE0F}b",
"a\u{E0100}b",
"a\u{FFF9}b",
"a\u{115F}b",
"a\u{3164}b",
"review\u{E0041}",
];
for s in bad {
assert!(has_blocked_chars(s), "{s:?} should be flagged");
assert_eq!(
has_blocked_chars(s),
strip_ansi(s) != s,
"has_blocked_chars/strip_ansi disagree on {s:?}"
);
}
}
#[test]
fn strip_ansi_tab_and_cr_are_consumed_by_first_pass_leaving_no_space() {
assert_eq!(
strip_ansi("a\tb"),
"ab",
"TAB must not survive to become a space"
);
assert_eq!(
strip_ansi("a\rb"),
"ab",
"lone CR must not survive to become a space"
);
assert_eq!(strip_ansi("a\x0bb"), "ab", "VT must not survive");
assert_eq!(strip_ansi("a\x0cb"), "ab", "FF must not survive");
assert_eq!(
strip_ansi("a\x1b[0mb"),
"ab",
"an ANSI reset leaves no space"
);
assert_eq!(
strip_ansi("ab\x1b"),
"ab",
"a trailing bare ESC leaves no space"
);
assert_eq!(
strip_ansi("a\nb"),
"a b",
"newline is the sole survivor and becomes a space"
);
}
#[test]
fn item_key_display_matches_strip_ansi() {
for raw in [
"skill:review",
"skill:evil\x1b[31mred\x1b[0m",
"agent:pay\u{202E}oot",
"rule:wo\u{200B}rd",
"tool:a\nb",
"",
] {
let key = ItemKey::new(raw);
assert_eq!(
key.display(),
strip_ansi(raw),
"ItemKey::display() must equal strip_ansi() for {raw:?}"
);
}
}
#[test]
fn item_key_as_str_is_raw() {
let raw = "skill:evil\x1b[31m";
let key = ItemKey::new(raw);
assert_eq!(key.as_str(), raw);
}
#[test]
fn item_key_identity_equality_and_str_borrow() {
use std::collections::HashSet;
let a = ItemKey::new("skill:review");
let b = ItemKey::new("skill:review");
assert_eq!(a, b);
assert_eq!(a, *"skill:review");
assert_eq!("skill:review", a);
let mut set: HashSet<ItemKey> = HashSet::new();
set.insert(a.clone());
assert!(set.contains("skill:review"));
}
}