use crate::ported::zsh_h::features;
use crate::zsh_h::module;
use crate::ported::zsh_h::TSC_PROMPT;
use std::sync::atomic::Ordering;
use std::sync::{Mutex, OnceLock};
pub const GROUPVAR: &str = ".zle.hlgroups";
pub fn convertattr(attrstr: &str, sgr: bool) -> String {
let (atr, _setmask, _rest) = crate::ported::prompt::match_highlight(attrstr, None);
let savecurrent = *crate::ported::prompt::current_attrs_lock().lock().unwrap(); let saveunknown = crate::ported::prompt::txtunknownattrs.load(Ordering::Relaxed); crate::ported::prompt::txtunknownattrs.store(0, Ordering::Relaxed); crate::ported::prompt::treplaceattrs(atr); let esc_stream = crate::ported::prompt::applytextattributes(TSC_PROMPT); *crate::ported::prompt::current_attrs_lock().lock().unwrap() = savecurrent; crate::ported::prompt::set_pending_text_attrs(savecurrent); crate::ported::prompt::txtunknownattrs.store(saveunknown, Ordering::Relaxed);
if sgr {
let bytes = esc_stream.as_bytes();
let mut out = String::new();
let mut i = 0;
let mut ended_after_m = false;
while i + 1 < bytes.len() && bytes[i] == 0x1b && bytes[i + 1] == b'[' {
i += 2; while i < bytes.len() {
let b = bytes[i];
if b.is_ascii_digit() {
out.push(b as char); i += 1;
} else if b == b';' || b == b':' {
out.push(';'); i += 1;
} else {
break; }
}
if i >= bytes.len() || bytes[i] != b'm' {
ended_after_m = false; break;
}
out.push(';'); ended_after_m = true;
i += 1; }
if ended_after_m {
out.pop();
}
if out.is_empty() {
out.push('0');
}
out
} else {
esc_stream }
}
pub fn getgroup(name: &str, sgr: bool) -> Option<String> {
let _ = name;
let var = GROUPVAR;
let tab = crate::ported::params::paramtab();
let table = match tab.read() {
Ok(t) => t,
Err(_) => return None,
};
let pm = match table.get(var) {
Some(p) => p,
None => return None, };
if crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32) != crate::ported::zsh_h::PM_HASHED {
return None; }
drop(table);
let store = match crate::ported::params::paramtab_hashed_storage().lock() {
Ok(s) => s,
Err(_) => return None,
};
let raw_attr = match store.get(var).and_then(|m| m.get(name)) {
Some(v) => v.clone(), None => return None, };
drop(store);
Some(convertattr(&raw_attr, sgr)) }
pub fn scangroup(sgr: bool) -> Vec<(String, String)> {
let var = GROUPVAR;
let tab = crate::ported::params::paramtab();
let table = match tab.read() {
Ok(t) => t,
Err(_) => return Vec::new(),
};
let pm = match table.get(var) {
Some(p) => p,
None => return Vec::new(),
};
if crate::ported::zsh_h::PM_TYPE(pm.node.flags as u32) != crate::ported::zsh_h::PM_HASHED {
return Vec::new();
}
drop(table);
let store = match crate::ported::params::paramtab_hashed_storage().lock() {
Ok(s) => s,
Err(_) => return Vec::new(),
};
let hlg = match store.get(var) {
Some(m) => m.clone(),
None => return Vec::new(),
};
drop(store);
let mut out: Vec<(String, String)> = Vec::with_capacity(hlg.len());
for (k, v) in hlg.iter() {
let converted = convertattr(v, sgr);
out.push((k.clone(), converted));
}
out
}
pub fn getpmesc(_ht: *mut crate::ported::zsh_h::HashTable, name: &str) -> Option<crate::ported::zsh_h::Param> {
use crate::ported::zsh_h::{hashnode, param, PM_SCALAR, PM_SPECIAL, PM_UNSET};
let val = getgroup(name, false); let unset = if val.is_none() { PM_UNSET } else { 0 }; Some(Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(), flags: (PM_SCALAR | PM_SPECIAL | unset) as i32, },
u_str: Some(val.unwrap_or_default()), ..Default::default()
}))
}
pub fn scanpmesc(
_ht: *mut crate::ported::zsh_h::HashTable,
func: Option<crate::ported::zsh_h::ParamScanFunc>,
flags: i32,
) {
use crate::ported::zsh_h::{hashnode, param, PM_SCALAR};
let Some(func) = func else { return };
for (nam, val) in scangroup(false) {
let pm = param {
node: hashnode { next: None, nam, flags: PM_SCALAR as i32 }, u_str: Some(val), ..Default::default()
};
func(&pm, flags); }
}
pub fn getpmsgr(_ht: *mut crate::ported::zsh_h::HashTable, name: &str) -> Option<crate::ported::zsh_h::Param> {
use crate::ported::zsh_h::{hashnode, param, PM_SCALAR, PM_SPECIAL, PM_UNSET};
let val = getgroup(name, true); let unset = if val.is_none() { PM_UNSET } else { 0 }; Some(Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(), flags: (PM_SCALAR | PM_SPECIAL | unset) as i32, },
u_str: Some(val.unwrap_or_default()), ..Default::default()
}))
}
pub fn scanpmsgr(
_ht: *mut crate::ported::zsh_h::HashTable,
func: Option<crate::ported::zsh_h::ParamScanFunc>,
flags: i32,
) {
use crate::ported::zsh_h::{hashnode, param, PM_SCALAR};
let Some(func) = func else { return };
for (nam, val) in scangroup(true) {
let pm = param {
node: hashnode { next: None, nam, flags: PM_SCALAR as i32 }, u_str: Some(val), ..Default::default()
};
func(&pm, flags); }
}
#[allow(unused_variables)]
pub fn setup_(m: *const module) -> i32 {
0 }
pub fn features_(m: *const module, features: &mut Vec<String>) -> i32 {
*features = featuresarray(m, module_features());
0 }
pub fn enables_(m: *const module, enables: &mut Option<Vec<i32>>) -> i32 {
handlefeatures(m, module_features(), enables) }
#[allow(unused_variables)]
pub fn boot_(m: *const module) -> i32 {
0 }
pub fn cleanup_(m: *const module) -> i32 {
setfeatureenables(m, module_features(), None) }
#[allow(unused_variables)]
pub fn finish_(m: *const module) -> i32 {
0 }
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["p:.zle.esc".to_string(), "p:.zle.sgr".to_string()]
}
fn handlefeatures(m: *const module, f: &Mutex<features>, enables: &mut Option<Vec<i32>>) -> i32 {
crate::ported::module::handlefeatures("zsh/hlgroup", &featuresarray(m, f), enables)
}
fn setfeatureenables(m: *const module, f: &Mutex<features>, e: Option<&[i32]>) -> i32 {
crate::ported::module::setfeatureenables("zsh/hlgroup", &featuresarray(m, f), e)
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 0,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 2,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
fn collect_scan(
scan: fn(*mut crate::ported::zsh_h::HashTable, Option<crate::ported::zsh_h::ParamScanFunc>, i32),
) -> Vec<(String, String)> {
thread_local! {
static OUT: std::cell::RefCell<Vec<(String, String)>> = const { std::cell::RefCell::new(Vec::new()) };
}
fn cb(pm: &crate::ported::zsh_h::param, _flags: i32) {
OUT.with(|o| o.borrow_mut().push((pm.node.nam.clone(), pm.u_str.clone().unwrap_or_default())));
}
OUT.with(|o| o.borrow_mut().clear());
scan(std::ptr::null_mut(), Some(cb), 0);
OUT.with(|o| o.borrow().clone())
}
fn with_attr_caps<R>(f: impl FnOnce() -> R) -> R {
use crate::ported::zsh_h::{TCBOLDFACEBEG, TCUNDERLINEBEG};
let saved_len = *crate::ported::init::tclen.lock().unwrap();
let saved_str = crate::ported::init::tcstr.lock().unwrap().clone();
let saved_flags = crate::ported::params::TERMFLAGS.load(Ordering::SeqCst);
let saved_colours = crate::ported::init::tccolours.swap(0, Ordering::SeqCst);
{
let mut len = crate::ported::init::tclen.lock().unwrap();
let mut caps = crate::ported::init::tcstr.lock().unwrap();
caps[TCBOLDFACEBEG as usize] = "\x1b[1m".to_string();
len[TCBOLDFACEBEG as usize] = 4;
caps[TCUNDERLINEBEG as usize] = "\x1b[4m".to_string();
len[TCUNDERLINEBEG as usize] = 4;
}
crate::ported::params::TERMFLAGS.store(0, Ordering::SeqCst);
let r = f();
*crate::ported::init::tclen.lock().unwrap() = saved_len;
*crate::ported::init::tcstr.lock().unwrap() = saved_str;
crate::ported::params::TERMFLAGS.store(saved_flags, Ordering::SeqCst);
crate::ported::init::tccolours.store(saved_colours, Ordering::SeqCst);
r
}
#[test]
fn convertattr_unsupported_words_render_nothing() {
let _g = crate::test_util::global_state_lock();
for word in [
"dim", "blink", "strikethrough", "hidden", "invisible",
"reverse", "inverse",
] {
assert_eq!(
with_attr_caps(|| convertattr(word, false)),
"",
"`{word}` must render nothing without a matching capability"
);
}
}
#[test]
fn convertattr_faint_renders_sgr_2() {
let _g = crate::test_util::global_state_lock();
assert_eq!(with_attr_caps(|| convertattr("faint", false)), "\x1b[2m");
}
#[test]
fn convertattr_truecolor_renders_sgr_38_2() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::params::getaparam(".term.extensions");
let _ = crate::ported::params::setaparam(".term.extensions", vec!["truecolor".to_string()]);
let got = with_attr_caps(|| convertattr("fg=#ff0000", false));
let _ = crate::ported::params::setaparam(".term.extensions", saved.unwrap_or_default());
assert_eq!(got, "\x1b[38;2;255;0;0m");
}
#[test]
fn convertattr_empty_escape_is_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(with_attr_caps(|| convertattr("", false)), "");
}
#[test]
fn convertattr_bold_escape() {
let _g = crate::test_util::global_state_lock();
assert_eq!(with_attr_caps(|| convertattr("bold", false)), "\x1b[1m");
}
#[test]
fn convertattr_chained_escape() {
let _g = crate::test_util::global_state_lock();
let s = with_attr_caps(|| convertattr("bold,underline", false));
assert!(s.contains("\x1b[1m"));
assert!(s.contains("\x1b[4m"));
}
#[test]
fn convertattr_sgr_bold() {
let _g = crate::test_util::global_state_lock();
assert_eq!(with_attr_caps(|| convertattr("bold", true)), "1");
}
#[test]
fn convertattr_sgr_chain() {
let _g = crate::test_util::global_state_lock();
assert_eq!(with_attr_caps(|| convertattr("bold,underline", true)), "1;4");
}
#[test]
fn convertattr_sgr_empty_returns_zero() {
let _g = crate::test_util::global_state_lock();
assert_eq!(convertattr("", true), "0");
}
#[test]
fn getgroup_returns_none_until_paramtable_wired() {
let _g = crate::test_util::global_state_lock();
assert_eq!(getgroup("any", false), None);
assert_eq!(getgroup("any", true), None);
}
#[test]
fn scangroup_returns_empty_until_paramtable_wired() {
let _g = crate::test_util::global_state_lock();
assert!(scangroup(false).is_empty());
assert!(scangroup(true).is_empty());
}
#[test]
fn convertattr_empty_input_is_safe() {
let _g = crate::test_util::global_state_lock();
let _ = convertattr("", false);
let _ = convertattr("", true);
}
#[test]
fn convertattr_bold_emits_sgr_bold() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("bold", false);
assert!(
s.contains("\x1b[1m") || s.contains("\x1b[1;"),
"bold attr must emit SGR 1, got {:?}",
s
);
}
#[test]
fn convertattr_unknown_attr_is_safe() {
let _g = crate::test_util::global_state_lock();
let _ = convertattr("definitely_not_a_real_attr", false);
}
#[test]
fn convertattr_256_color_upper_boundary() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=255", false);
assert!(
s.contains("38;5;255"),
"256-color upper boundary 255 must encode correctly, got {:?}",
s
);
}
#[test]
fn getpmesc_empty_or_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getpmesc(std::ptr::null_mut(), "").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
assert!(getpmesc(std::ptr::null_mut(), "definitely_not_in_table_xyzzy").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn getpmsgr_empty_or_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getpmsgr(std::ptr::null_mut(), "").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
assert!(getpmsgr(std::ptr::null_mut(), "definitely_not_in_table_xyzzy").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn scanpmesc_and_scanpmsgr_are_empty_until_wired() {
let _g = crate::test_util::global_state_lock();
assert!(collect_scan(scanpmesc).is_empty());
assert!(collect_scan(scanpmsgr).is_empty());
}
#[test]
fn module_lifecycle_shims_all_return_zero() {
let _g = crate::test_util::global_state_lock();
let m: *const module = std::ptr::null();
assert_eq!(setup_(m), 0);
let mut features = Vec::new();
assert_eq!(features_(m, &mut features), 0);
let mut enables: Option<Vec<i32>> = None;
assert_eq!(enables_(m, &mut enables), 0);
}
#[test]
fn convertattr_light_prefix_is_alias_for_bright() {
let _g = crate::test_util::global_state_lock();
let bright = convertattr("fg=bright-green", false);
let light = convertattr("fg=light-green", false);
assert_eq!(
bright, light,
"c:40 — light- and bright- prefixes must produce identical SGR codes"
);
}
#[test]
fn convertattr_sgr_bg_color() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
convertattr("bg=blue", true),
"44",
"SGR mode strips ESC[/m wrapper → bare digit string"
);
assert_eq!(convertattr("bg=red", true), "41");
}
#[test]
fn convertattr_unknown_color_drops_silently() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=not_a_real_color", false);
assert_eq!(s, "", "unknown color → no escape emitted");
let s = convertattr("fg=not_a_real_color", true);
assert_eq!(s, "0");
}
#[test]
fn hlgroup_corpus_bold_is_sgr_1() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("bold", false);
assert!(s.contains("\x1b[1m"), "bold = SGR 1, got {s:?}");
}
#[test]
fn hlgroup_corpus_underline_is_sgr_4() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("underline", false);
assert!(s.contains("\x1b[4m"));
}
#[test]
fn hlgroup_corpus_italic_is_sgr_3() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("italic", false);
assert!(s.contains("\x1b[3m"));
}
#[test]
fn hlgroup_corpus_fg_red_is_sgr_31() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=red", false);
assert!(s.contains("\x1b[31m"), "fg=red = SGR 31, got {s:?}");
}
#[test]
fn hlgroup_corpus_bg_blue_is_sgr_44() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("bg=blue", false);
assert!(s.contains("\x1b[44m"), "bg=blue = SGR 44, got {s:?}");
}
#[test]
fn hlgroup_corpus_combined_attrs() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("bold,fg=red", false);
assert!(s.contains("\x1b[1m"), "has bold");
assert!(s.contains("\x1b[31m"), "has fg=red");
}
#[test]
fn convertattr_bold_emits_sgr_1() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("bold", false);
assert!(s.contains("\x1b[1m"), "got {:?}", s);
}
#[test]
fn convertattr_underline_emits_sgr_4() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("underline", false);
assert!(s.contains("\x1b[4m"), "got {:?}", s);
}
#[test]
fn convertattr_fg_red_emits_sgr_31() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=red", false);
assert!(s.contains("\x1b[31m"));
}
#[test]
fn convertattr_fg_green_emits_sgr_32() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=green", false);
assert!(s.contains("\x1b[32m"));
}
#[test]
fn convertattr_fg_blue_emits_sgr_34() {
let _g = crate::test_util::global_state_lock();
let s = convertattr("fg=blue", false);
assert!(s.contains("\x1b[34m"));
}
#[test]
fn convertattr_is_deterministic() {
let _g = crate::test_util::global_state_lock();
for input in ["bold", "fg=red", "underline,fg=blue", ""] {
let first = convertattr(input, false);
for _ in 0..5 {
assert_eq!(convertattr(input, false), first);
}
}
}
#[test]
fn getgroup_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getgroup("zshrs_never_real_group_xyz", false).is_none());
}
#[test]
fn getgroup_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getgroup("", false).is_none());
}
#[test]
fn scangroup_returns_vec_no_panic() {
let _g = crate::test_util::global_state_lock();
let _ = scangroup(false);
let _ = scangroup(true);
}
#[test]
fn getpmesc_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getpmesc(std::ptr::null_mut(), "zshrs_never_real_esc_xyz").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn getpmsgr_unknown_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(getpmsgr(std::ptr::null_mut(), "zshrs_never_real_sgr_xyz").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn hlgroup_setup_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(setup_(std::ptr::null()), 0);
}
#[test]
fn hlgroup_cleanup_returns_zero_pin() {
let _g = crate::test_util::global_state_lock();
assert_eq!(cleanup_(std::ptr::null()), 0);
}
#[test]
fn convertattr_returns_string_type() {
let _: String = convertattr("", false);
let _: String = convertattr("fg=red", true);
}
#[test]
fn convertattr_empty_input_no_panic() {
let _ = convertattr("", false);
let _ = convertattr("", true);
}
#[test]
fn convertattr_is_pure_full_sweep() {
let _g = crate::test_util::global_state_lock();
with_attr_caps(|| {
for input in ["", "fg=red", "bold", "bg=blue,underline"] {
let first_no_sgr = convertattr(input, false);
let first_sgr = convertattr(input, true);
for _ in 0..3 {
assert_eq!(
convertattr(input, false),
first_no_sgr,
"convertattr({:?}, false) must be pure",
input
);
assert_eq!(
convertattr(input, true),
first_sgr,
"convertattr({:?}, true) must be pure",
input
);
}
}
});
}
#[test]
fn getgroup_returns_option_string_type() {
let _: Option<String> = getgroup("any", false);
}
#[test]
fn scangroup_returns_vec_type() {
let _: Vec<(String, String)> = scangroup(false);
}
#[test]
fn scangroup_is_deterministic() {
let first = scangroup(false);
for _ in 0..3 {
assert_eq!(scangroup(false), first, "scangroup must be deterministic");
}
}
#[test]
fn getpmesc_empty_returns_none() {
assert!(getpmesc(std::ptr::null_mut(), "").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn getpmsgr_empty_returns_none() {
assert!(getpmsgr(std::ptr::null_mut(), "").is_some_and(|p| p.node.flags as u32 & crate::ported::zsh_h::PM_UNSET != 0));
}
#[test]
fn scanpm_variants_return_vec_type() {
let _: Vec<(String, String)> = collect_scan(scanpmesc);
let _: Vec<(String, String)> = collect_scan(scanpmsgr);
}
#[test]
fn hlgroup_full_lifecycle_returns_zero_for_all() {
let _g = crate::test_util::global_state_lock();
let null = std::ptr::null();
assert_eq!(setup_(null), 0);
let mut feats = Vec::new();
let _ = features_(null, &mut feats);
let mut enables: Option<Vec<i32>> = None;
let _ = enables_(null, &mut enables);
assert_eq!(boot_(null), 0);
assert_eq!(cleanup_(null), 0);
assert_eq!(finish_(null), 0);
}
#[test]
fn convertattr_returns_string_pin_alt() {
let _: String = convertattr("fg=red", false);
}
#[test]
fn convertattr_empty_input_both_modes_deterministic() {
let a1 = convertattr("", false);
let a2 = convertattr("", false);
assert_eq!(a1, a2, "empty(false) must be pure");
let b1 = convertattr("", true);
let b2 = convertattr("", true);
assert_eq!(b1, b2, "empty(true) must be pure");
}
#[test]
fn getgroup_empty_name_deterministic() {
let a = getgroup("", false);
let b = getgroup("", false);
assert_eq!(
a.is_some(),
b.is_some(),
"getgroup('') must be deterministic"
);
}
#[test]
fn getgroup_sgr_mode_returns_option_string_type() {
let _: Option<String> = getgroup("name", true);
}
#[test]
fn scangroup_sgr_mode_returns_vec_type() {
let _: Vec<(String, String)> = scangroup(true);
}
#[test]
fn scangroup_both_modes_deterministic() {
let f1 = scangroup(false);
let f2 = scangroup(false);
assert_eq!(f1, f2, "scangroup(false) must be deterministic");
let t1 = scangroup(true);
let t2 = scangroup(true);
assert_eq!(t1, t2, "scangroup(true) must be deterministic");
}
#[test]
fn getpmesc_returns_option_string_type() {
let _: Option<crate::ported::zsh_h::Param> = getpmesc(std::ptr::null_mut(), "anykey");
}
#[test]
fn getpmsgr_returns_option_string_type() {
let _: Option<crate::ported::zsh_h::Param> = getpmsgr(std::ptr::null_mut(), "anykey");
}
#[test]
fn scanpmesc_is_deterministic() {
let a = collect_scan(scanpmesc);
let b = collect_scan(scanpmesc);
assert_eq!(a, b, "scanpmesc must be deterministic");
}
#[test]
fn scanpmsgr_is_deterministic() {
let a = collect_scan(scanpmsgr);
let b = collect_scan(scanpmsgr);
assert_eq!(a, b, "scanpmsgr must be deterministic");
}
#[test]
fn scangroup_keys_all_non_empty() {
for (k, _) in scangroup(false) {
assert!(
!k.is_empty(),
"scangroup must not yield entries with empty key"
);
}
}
}