use crate::ported::params::{getsparam, TERMFLAGS};
use crate::ported::utils::{zsetupterm, zwarnnam};
use std::sync::atomic::{AtomicI32, Ordering};
use std::sync::{Mutex, OnceLock};
use crate::ported::options::optlookup;
use crate::ported::zsh_h::{features, isset, module};
use crate::zsh_h::TERM_UNKNOWN;
pub fn ztgetflag(s: &str) -> i32 {
if !ensure_termcap_loaded() {
return -1; }
let s_c = match std::ffi::CString::new(s) {
Ok(c) => c,
Err(_) => return -1,
};
let flag = {
let _g = TERMCAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { tgetflag(s_c.as_ptr()) }
};
match flag {
1 => 1, _ => {
for b in BOOLCODES {
if *b == s {
return 0; }
}
-1 }
}
}
pub fn bin_echotc(
name: &str,
argv: &[String],
_ops: &crate::ported::zsh_h::options,
_func: i32,
) -> i32 {
const TERM_BAD: i32 = 1 << 1;
if argv.is_empty() {
zwarnnam(name, "missing argument");
return 1;
}
let s: &str = argv[0].as_str();
let argv_rest: Vec<&str> = argv[1..].iter().map(String::as_str).collect();
if (TERMFLAGS.load(Ordering::Relaxed) & TERM_BAD) != 0 {
return 1; }
if (TERMFLAGS.load(Ordering::Relaxed) & TERM_UNKNOWN) != 0 {
let interactive =
isset(optlookup("interactive"));
if interactive || !ensure_termcap_loaded() {
return 1; }
}
if !ensure_termcap_loaded() {
return 1;
}
let s_c = match std::ffi::CString::new(s) {
Ok(c) => c,
Err(_) => return 1,
};
let num = {
let _g = TERMCAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { tgetnum(s_c.as_ptr()) }
}; if num != -1 {
println!("{}", num); return 0; }
match ztgetflag(s) {
-1 => {} 0 => {
println!("no"); return 0; }
_ => {
println!("yes"); return 0; }
}
let mut buf: [libc::c_char; 2048] = [0; 2048]; let mut area = buf.as_mut_ptr();
let value = {
let _g = TERMCAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let t = unsafe { tgetstr(s_c.as_ptr(), &mut area) }; if t.is_null() || (t as isize) == -1 || unsafe { *t } == 0 {
drop(_g);
zwarnnam(name, &format!("no such capability: {}", s)); return 1; }
unsafe { std::ffi::CStr::from_ptr(t) }
.to_string_lossy()
.into_owned()
};
let mut argct = 0usize; let bytes = value.as_bytes();
let mut i = 0;
while i < bytes.len() {
if bytes[i] == b'%' {
i += 1;
if i < bytes.len() {
match bytes[i] {
b'd' | b'2' | b'3' | b'.' | b'+' => argct += 1, _ => {}
}
}
}
i += 1;
}
if argv_rest.len() != argct {
let msg = if argv_rest.len() < argct {
"not enough arguments"
}
else {
"too many arguments"
}; zwarnnam(name, msg); return 1; }
if argct == 0 {
print!("{}", value); } else {
let mut out = value;
for arg in &argv_rest {
out = out.replacen("%d", arg, 1);
out = out.replacen("%2", arg, 1);
out = out.replacen("%3", arg, 1);
}
print!("{}", out); }
0 }
pub fn gettermcap(_ht: *mut crate::ported::zsh_h::HashTable, name: &str) -> Option<crate::ported::zsh_h::Param> {
use crate::ported::zsh_h::{hashnode, param, Param, PM_READONLY, PM_SCALAR, PM_UNSET};
let mk = |s: String, extra: i32| -> Param {
Box::new(param {
node: hashnode {
next: None,
nam: name.to_string(),
flags: PM_READONLY as i32 | extra,
},
u_data: 0,
u_arr: None,
u_str: Some(s),
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
})
};
if !ensure_termcap_loaded() {
return None;
}
let n_c = std::ffi::CString::new(name).ok()?;
let mut buf: [libc::c_char; 1024] = [0; 1024];
let mut area = buf.as_mut_ptr();
let _g = TERMCAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
let raw = unsafe { tgetstr(n_c.as_ptr(), &mut area) }; if !raw.is_null() {
let s = unsafe { std::ffi::CStr::from_ptr(raw) }
.to_string_lossy()
.into_owned();
return Some(mk(s, PM_SCALAR as i32));
}
let n = unsafe { tgetnum(n_c.as_ptr()) }; if n != -1 {
return Some(mk(n.to_string(), PM_SCALAR as i32));
}
match unsafe { tgetflag(n_c.as_ptr()) } {
1 => Some(mk("yes".to_string(), PM_SCALAR as i32)),
0 => {
if BOOLCODES.iter().any(|b| *b == name) {
Some(mk(String::new(), PM_SCALAR as i32))
} else {
Some(mk(String::new(), PM_SCALAR as i32 | PM_UNSET as i32))
}
}
_ => Some(mk(String::new(), PM_SCALAR as i32 | PM_UNSET as i32)),
}
}
pub fn scantermcap(
_ht: *mut crate::ported::zsh_h::HashTable,
func: Option<crate::ported::zsh_h::ScanFunc>,
flags: i32,
) {
use crate::ported::zsh_h::{hashnode, param, PM_SCALAR};
let f = match func {
Some(f) => f,
None => return,
};
if !ensure_termcap_loaded() {
return;
}
for &name in BOOLCODES
.iter()
.chain(NUMCODES.iter())
.chain(STRCODES.iter())
{
if let Some(pm) = gettermcap(std::ptr::null_mut(), name) {
use crate::ported::zsh_h::PM_UNSET;
if (pm.node.flags & PM_UNSET as i32) != 0 {
continue;
}
let node = param {
node: hashnode {
next: None,
nam: name.to_string(),
flags: PM_SCALAR as i32,
},
u_data: 0,
u_arr: None,
u_str: pm.u_str.clone(),
u_val: 0,
u_dval: 0.0,
u_hash: None,
gsu_s: None,
gsu_i: None,
gsu_f: None,
gsu_a: None,
gsu_h: None,
base: 0,
width: 0,
env: None,
ename: None,
old: None,
level: 0,
};
let node_box = Box::new(node.node.clone());
f(&node_box, flags);
}
}
}
unsafe extern "C" {
fn tgetent(bp: *mut libc::c_char, name: *const libc::c_char) -> libc::c_int;
fn tgetflag(id: *const libc::c_char) -> libc::c_int;
fn tgetnum(id: *const libc::c_char) -> libc::c_int;
fn tgetstr(id: *const libc::c_char, area: *mut *mut libc::c_char) -> *mut libc::c_char;
}
#[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 {
let _ = zsetupterm(); 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 TERMCAP_LOCK: Mutex<()> = Mutex::new(());
static BOOLCODES: &[&str] = &[
"am", "bs", "bw", "da", "db", "eo", "es", "gn", "hc", "hs", "in", "km", "mi", "ms", "nc", "ns",
"os", "ul", "ut", "xb", "xn", "xo", "xs", "xt",
];
static NUMCODES: &[&str] = &[
"co", "it", "lh", "lm", "lw", "li", "ma", "MW", "Nl", "pa", "Nco", "sg", "tw", "ug", "vt", "ws",
];
static STRCODES: &[&str] = &[
"ae", "al", "AL", "ac", "as", "bc", "bl", "bt", "cb", "cd", "ce", "cm", "cr", "cs", "ct", "cl",
"cv", "DC", "DL", "DO", "do", "ds", "ec", "ed", "ei", "fs", "ho", "hd", "hu", "i1", "i3", "i2",
"ic", "IC", "if", "im", "ip", "is", "kA", "kb", "kB", "kC", "kd", "kD", "kE", "kF", "ke", "kh",
"kH", "kI",
"kL", "kl", "kM", "kN", "kP", "kr", "kR", "kS", "ks", "kT", "kt", "ku", "l0", "l1", "l2", "l3",
"l4", "l5", "l6", "l7", "l8", "l9", "le", "ll", "ma", "mb", "MC", "md", "me", "mh", "mk", "mm",
"mo", "mp", "mr", "nd", "nl", "nw", "pc", "pf", "pk", "pl", "pn", "po", "pO", "ps", "px", "rc",
"rf", "RI", "rp", "rs", "sa", "sc", "se", "SF", "sf", "so", "SR", "sr", "st", "ta", "te", "ti",
"ts", "uc", "ue", "up", "UP", "us", "vb", "ve", "vi", "vs", "wi",
];
fn ensure_termcap_loaded() -> bool {
static STATE: AtomicI32 = AtomicI32::new(0);
match STATE.load(Ordering::Relaxed) {
1 => true,
-1 => false,
_ => {
let term = getsparam("TERM").unwrap_or_else(|| "dumb".into());
let term_c = match std::ffi::CString::new(term) {
Ok(c) => c,
Err(_) => return false,
};
let r = {
let _g = TERMCAP_LOCK.lock().unwrap_or_else(|e| e.into_inner());
unsafe { tgetent(std::ptr::null_mut(), term_c.as_ptr()) }
};
let ok = r > 0;
STATE.store(if ok { 1 } else { -1 }, Ordering::Relaxed);
ok
}
}
}
static MODULE_FEATURES: OnceLock<Mutex<features>> = OnceLock::new();
fn featuresarray(_m: *const module, _f: &Mutex<features>) -> Vec<String> {
vec!["b:echotc".to_string(), "p:termcap".to_string()]
}
fn handlefeatures(
_m: *const module,
_f: &Mutex<features>,
enables: &mut Option<Vec<i32>>,
) -> i32 {
if enables.is_none() {
*enables = Some(vec![1; 2]);
}
0
}
fn setfeatureenables(_m: *const module, _f: &Mutex<features>, _e: Option<&[i32]>) -> i32 {
0
}
fn module_features() -> &'static Mutex<features> {
MODULE_FEATURES.get_or_init(|| {
Mutex::new(features {
bn_list: None,
bn_size: 1,
cd_list: None,
cd_size: 0,
mf_list: None,
mf_size: 0,
pd_list: None,
pd_size: 1,
n_abstract: 0,
})
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ztgetflag_known_on_returns_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztgetflag("am"), 1);
}
#[test]
fn ztgetflag_unknown_returns_minus_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztgetflag("zz"), -1);
}
#[test]
fn gettermcap_co_returns_columns() {
let _g = crate::test_util::global_state_lock();
let pm = gettermcap(std::ptr::null_mut(), "co").expect("co must resolve to Param");
let v = pm.u_str.as_deref().unwrap_or("");
let n: i32 = v.parse().unwrap_or(0);
assert!(n > 0);
}
#[test]
fn gettermcap_unknown_returns_unset_param() {
let _g = crate::test_util::global_state_lock();
use crate::ported::zsh_h::PM_UNSET;
if let Some(pm) = gettermcap(std::ptr::null_mut(), "zz_nonexistent") {
assert!(pm.node.flags & PM_UNSET as i32 != 0, "PM_UNSET set");
}
}
#[test]
fn scantermcap_emits_bool_caps() {
let _g = crate::test_util::global_state_lock();
use std::sync::Mutex;
static SEEN: Mutex<Vec<String>> = Mutex::new(Vec::new());
SEEN.lock().unwrap().clear();
fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
SEEN.lock().unwrap().push(node.nam.clone());
}
scantermcap(std::ptr::null_mut(), Some(cb), 0);
let seen = SEEN.lock().unwrap().clone();
assert!(seen.iter().any(|k| k == "am"));
}
#[test]
fn echotc_with_no_args_returns_one() {
let _g = crate::test_util::global_state_lock();
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_echotc("echotc", &[], &ops, 0);
assert_eq!(r, 1, "echotc must report missing-arg error");
}
#[test]
fn echotc_unknown_cap_returns_nonzero() {
let _g = crate::test_util::global_state_lock();
let ops = crate::ported::zsh_h::options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_echotc(
"echotc",
&["zz_definitely_not_a_cap".to_string()],
&ops,
0,
);
assert_ne!(r, 0, "unknown cap must error");
}
#[test]
fn ztgetflag_rejects_embedded_nul() {
let _g = crate::test_util::global_state_lock();
assert_eq!(
ztgetflag("a\0m"),
-1,
"embedded NUL must surface as -1, not panic or false-match"
);
}
#[test]
fn ztgetflag_empty_string_returns_neg_one() {
let _g = crate::test_util::global_state_lock();
assert_eq!(ztgetflag(""), -1);
}
#[test]
fn scantermcap_keys_are_unique() {
let _g = crate::test_util::global_state_lock();
use std::sync::Mutex;
static KEYS: Mutex<Vec<String>> = Mutex::new(Vec::new());
KEYS.lock().unwrap().clear();
fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
KEYS.lock().unwrap().push(node.nam.clone());
}
scantermcap(std::ptr::null_mut(), Some(cb), 0);
let collected = KEYS.lock().unwrap().clone();
let mut seen = std::collections::HashSet::new();
for k in &collected {
assert!(
seen.insert(k.clone()),
"duplicate termcap key emitted: {}",
k
);
}
}
#[test]
fn scantermcap_keys_are_nonempty() {
let _g = crate::test_util::global_state_lock();
use std::sync::Mutex;
static KEYS: Mutex<Vec<String>> = Mutex::new(Vec::new());
KEYS.lock().unwrap().clear();
fn cb(node: &crate::ported::zsh_h::HashNode, _flags: i32) {
KEYS.lock().unwrap().push(node.nam.clone());
}
scantermcap(std::ptr::null_mut(), Some(cb), 0);
for k in KEYS.lock().unwrap().iter() {
assert!(
!k.is_empty(),
"scantermcap emitted empty key — null entry leak?"
);
}
}
#[test]
fn gettermcap_is_case_sensitive() {
let _g = crate::test_util::global_state_lock();
use crate::ported::zsh_h::PM_UNSET;
let r1 = gettermcap(std::ptr::null_mut(), "co").expect("co Param");
assert!(r1.node.flags & PM_UNSET as i32 == 0, "co must be set");
if let Some(r2) = gettermcap(std::ptr::null_mut(), "CO") {
assert!(
r2.node.flags & PM_UNSET as i32 != 0,
"termcap names case-sensitive; CO must be PM_UNSET"
);
}
}
#[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);
assert_eq!(boot_(m), 0);
assert_eq!(cleanup_(m), 0);
assert_eq!(finish_(m), 0);
}
}