use std::collections::HashMap;
use std::sync::atomic::Ordering;
use std::sync::{Arc, Mutex, OnceLock};
use super::zle_h::{
TH_IMMORTAL, WIDGET_INT, WIDGET_INUSE, WIDGET_NCOMP, WidgetImpl, ZLE_ISCOMP, ZLE_KEEPSUFFIX,
ZLE_MENUCMP, widget,
};
use crate::ported::utils::zwarnnam;
use crate::ported::zsh_h::{options, OPT_ISSET, DISABLED};
#[allow(unused_imports)]
use crate::ported::zle::{
deltochar::*, textobjects::*, zle_h::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
};
#[allow(unused_imports)]
pub fn createthingytab() {
let _ = thingytab(); }
impl Thingy {
pub fn new(name: &str) -> Self {
Thingy {
nam: name.to_string(),
flags: 0,
rc: 1,
widget: None,
}
}
pub fn builtin(name: &str) -> Self {
let widget = widget::builtin(name);
Thingy {
nam: name.to_string(),
flags: TH_IMMORTAL,
rc: 1,
widget: Some(Arc::new(widget)),
}
}
pub fn user_defined(name: &str, func_name: &str) -> Self {
let widget = widget::user_defined(name, func_name);
Thingy {
nam: name.to_string(),
flags: 0,
rc: 1,
widget: Some(Arc::new(widget)),
}
}
pub fn is(&self, name: &str) -> bool {
self.nam == name
}
pub fn is_thingy(&self, name: &str) -> bool {
self.nam == name || self.nam == format!(".{}", name)
}
}
pub fn emptythingytab() {
let names: Vec<String> = thingytab()
.lock()
.unwrap()
.iter()
.filter(|(_, t)| (t.flags & DISABLED) == 0)
.map(|(k, _)| k.clone())
.collect();
names.iter().for_each(|n| scanemptythingies(n)); }
pub fn scanemptythingies(name: &str) {
let internal = {
let tab = thingytab().lock().unwrap();
tab.get(name)
.and_then(|t| t.widget.as_ref().map(|w| (w.flags & WIDGET_INT) != 0))
.unwrap_or(true)
};
if !internal {
unbindwidget(name, 1); }
}
pub fn makethingynode() -> Thingy {
let mut t = Thingy::new(""); t.flags |= DISABLED; t.rc = 0; t }
pub fn freethingynode(name: &str) {
let _ = thingytab().lock().unwrap().remove(name);
}
pub fn refthingy(name: &str) {
let mut tab = thingytab().lock().unwrap();
if let Some(t) = tab.get_mut(name) {
t.rc += 1; }
}
pub fn unrefthingy(th: &str) {
let drop = thingytab()
.lock()
.unwrap()
.get_mut(th) .map(|t| {
t.rc -= 1;
t.rc == 0
})
.unwrap_or(false);
if drop {
freethingynode(th);
} }
pub fn rthingy(nam: &str) {
{
let mut tab = thingytab().lock().unwrap();
if !tab.contains_key(nam) {
let mut t = makethingynode(); t.nam = nam.to_string(); tab.insert(nam.to_string(), t); }
}
refthingy(nam); }
pub fn rthingy_nocreate(name: &str) -> bool {
let exists = thingytab().lock().unwrap().contains_key(name); if !exists {
return false; }
refthingy(name); true
}
pub fn bindwidget(w: Arc<widget>, t: &str) -> i32 {
let (immortal, disabled, same) = {
let tab = thingytab().lock().unwrap();
match tab.get(t) {
Some(t) => (
(t.flags & TH_IMMORTAL) != 0,
(t.flags & DISABLED) != 0,
t.widget
.as_ref()
.map(|w2| Arc::ptr_eq(w2, &w))
.unwrap_or(false),
),
None => (false, true, false),
}
};
if immortal {
unrefthingy(t); return -1; }
if !disabled {
if same {
return 0; }
unbindwidget(t, 1); }
let mut tab = thingytab().lock().unwrap();
if let Some(t) = tab.get_mut(t) {
t.widget = Some(w); t.flags &= !DISABLED; }
0 }
pub fn unbindwidget(t: &str, override_: i32) -> i32 {
let (disabled, immortal, w_opt) = {
let tab = thingytab().lock().unwrap();
match tab.get(t) {
Some(t) => (
(t.flags & DISABLED) != 0,
(t.flags & TH_IMMORTAL) != 0,
t.widget.clone(),
),
None => return 0,
}
};
if disabled {
return 0;
}
if override_ == 0 && immortal {
return -1;
}
if let Some(w) = w_opt {
let peer_count = {
let tab = thingytab().lock().unwrap();
tab.values()
.filter(|other| other.nam != t)
.filter(|other| {
other
.widget
.as_ref()
.map(|w2| Arc::ptr_eq(w2, &w))
.unwrap_or(false)
})
.count()
};
if peer_count == 0 {
freewidget(w);
}
}
let mut tab = thingytab().lock().unwrap();
if let Some(t) = tab.get_mut(t) {
t.flags &= !TH_IMMORTAL; t.flags |= DISABLED; t.widget = None;
}
drop(tab);
unrefthingy(t); 0 }
pub fn freewidget(w: Arc<widget>) {
if (w.flags & WIDGET_INUSE) != 0 {
return; }
drop(w); }
pub fn addzlefunction(
name: &str,
ifunc: ZleIntFunc,
flags: i32,
) -> Option<Arc<widget>> {
if name.starts_with('.') {
return None; }
let dotn = format!(".{}", name);
let blocked = {
let tab = thingytab().lock().unwrap();
tab.get(&dotn)
.map(|t| (t.flags & TH_IMMORTAL) != 0)
.unwrap_or(false)
};
if blocked {
return None; }
let w = Arc::new(widget {
flags: flags | WIDGET_INT, first: None,
u: WidgetImpl::Internal(ifunc), });
rthingy(&dotn); bindwidget(w.clone(), &dotn); if let Some(t) = thingytab().lock().unwrap().get_mut(&dotn) {
t.flags |= TH_IMMORTAL; }
rthingy(name); bindwidget(w.clone(), name); Some(w) }
pub fn deletezlefunction(w: &Arc<widget>) {
let names: Vec<String> = {
let tab = thingytab().lock().unwrap();
tab.iter()
.filter(|(_, t)| {
t.widget
.as_ref()
.map(|w2| Arc::ptr_eq(w2, w))
.unwrap_or(false)
})
.map(|(k, _)| k.clone())
.collect()
};
for n in names {
unbindwidget(&n, 1); }
}
pub fn bin_zle(
name: &str,
args: &[String], ops: &options,
_func: i32,
) -> i32 {
type OpHandler = fn(&str, &[String], &options, i32) -> i32;
let opns: [(u8, OpHandler, i32, i32); 14] = [
(b'l', bin_zle_list, 0, -1), (b'D', bin_zle_del, 1, -1), (b'A', bin_zle_link, 2, 2), (b'N', bin_zle_new, 1, 2), (b'C', bin_zle_complete, 3, 3), (b'R', bin_zle_refresh, 0, -1), (b'M', bin_zle_mesg, 1, 1), (b'U', bin_zle_unget, 1, 1), (b'K', bin_zle_keymap, 1, 1), (b'I', bin_zle_invalidate, 0, 0), (b'f', bin_zle_flags, 1, -1), (b'F', bin_zle_fd, 0, 2), (b'T', bin_zle_transform, 0, 2), (0u8, bin_zle_call, 0, -1), ];
let op_idx = opns
.iter()
.position(|(o, _, _, _)| *o != 0 && OPT_ISSET(ops, *o))
.unwrap_or(opns.len() - 1);
if opns[op_idx].0 != 0 {
for (o, _, _, _) in opns.iter().skip(op_idx + 1) {
if *o != 0 && OPT_ISSET(ops, *o) {
zwarnnam(name, "incompatible operation selection options"); return 1; }
}
}
let n = args.len() as i32; let (op_o, op_func, op_min, op_max) = &opns[op_idx];
if n < *op_min {
zwarnnam(name, &format!("not enough arguments for -{}", *op_o as char)); return 1; } else if *op_max != -1 && n > *op_max {
zwarnnam(name, &format!("too many arguments for -{}", *op_o as char)); return 1; }
op_func(name, args, ops, *op_o as i32)
}
pub fn bin_zle_list(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if args.is_empty() {
let _ = scanlistwidgets(1);
return 0;
}
let mut ret = 0;
for arg in args {
let exists = thingytab().lock().unwrap().contains_key(arg);
if !exists {
ret = 1;
break;
}
}
ret }
pub fn bin_zle_refresh(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
let s_save: Option<String> = STATUSLINE.lock().unwrap().clone(); let ocl: i32 = CLEARLIST
.load(Ordering::Relaxed);
if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
return 1; }
*STATUSLINE.lock().unwrap() = None;
if !args.is_empty() {
if !args[0].is_empty() {
*STATUSLINE.lock().unwrap() = Some(args[0].clone()); }
if args.len() > 1 {
let zmultsav: i32 =
crate::ported::zle::compcore::ZMULT.load(Ordering::Relaxed); let list: Vec<String> = args[1..].to_vec(); crate::ported::zle::compcore::ZMULT.store(1, Ordering::Relaxed); listlist(&list, 0); if STATUSLINE.lock().unwrap().is_some() {
LASTLISTLEN
.fetch_add(1, Ordering::Relaxed); }
SHOWINGLIST
.store(0, Ordering::Relaxed);
CLEARLIST
.store(0, Ordering::Relaxed);
crate::ported::zle::compcore::ZMULT
.store(zmultsav, Ordering::Relaxed);
} else if OPT_ISSET(ops, b'c') {
CLEARLIST
.store(1, Ordering::Relaxed); LASTLISTLEN
.store(0, Ordering::Relaxed); }
} else if OPT_ISSET(ops, b'c') {
CLEARLIST
.store(1, Ordering::Relaxed); LISTSHOWN
.store(1, Ordering::Relaxed); LASTLISTLEN
.store(0, Ordering::Relaxed); }
zrefresh(); *STATUSLINE.lock().unwrap() = s_save; CLEARLIST
.store(ocl, Ordering::Relaxed); 0 }
pub fn bin_zle_mesg(name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
crate::ported::utils::zwarnnam(
name,
"can only be called from widget function",
);
return 1; }
if let Some(arg) = args.first() {
crate::ported::zle::zle_utils::showmsg(arg); }
use crate::ported::zsh_h::SFC_WIDGET;
if crate::ported::builtin::SFCONTEXT.load(std::sync::atomic::Ordering::Relaxed)
!= SFC_WIDGET
{
crate::ported::zle::zle_refresh::zrefresh(); }
0 }
pub fn bin_zle_unget(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
return 1; }
if let Some(arg) = args.first() {
for byte in arg.bytes().rev() {
ungetbyte(byte);
}
}
0 }
pub fn bin_zle_keymap(name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) == 0 {
crate::ported::utils::zwarnnam(
name,
"can only be called from widget function",
);
return 1; }
if args.is_empty() {
return 1;
}
crate::ported::zle::zle_keymap::selectkeymap(&args[0], 0) }
pub fn scanlistwidgets(list: i32) -> i32 {
use std::io::Write;
let tab = thingytab().lock().unwrap();
let mut entries: Vec<(String, String)> = Vec::new();
for (name, t) in tab.iter() {
let w = match t.widget.as_ref() {
Some(w) => w,
None => continue,
};
if (w.flags & WIDGET_INT) != 0 {
continue;
}
let fn_name = match &w.u {
WidgetImpl::UserFunc(s) => s.clone(),
_ => name.clone(),
};
entries.push((name.clone(), fn_name));
}
drop(tab);
entries.sort_by(|a, b| a.0.cmp(&b.0));
let stdout = std::io::stdout();
let mut handle = stdout.lock();
for (name, fn_name) in &entries {
if list != 0 {
if &fn_name != &name {
let _ = writeln!(handle, "{} ({})", name, fn_name);
} else {
let _ = writeln!(handle, "{}", name);
}
} else {
if &fn_name != &name {
let _ = writeln!(handle, "zle -N {} {}", name, fn_name);
} else {
let _ = writeln!(handle, "zle -N {}", name);
}
}
}
0
}
pub fn bin_zle_del(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
let mut ret = 0;
for arg in args {
let exists = thingytab().lock().unwrap().contains_key(arg);
if !exists {
ret = 1; } else if unbindwidget(arg, 0) != 0 {
ret = 1; }
}
ret }
pub fn bin_zle_link(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if args.len() < 2 {
return 1;
}
let src = &args[0];
let dst = &args[1];
let widget = {
let tab = thingytab().lock().unwrap();
tab.get(src).and_then(|t| t.widget.clone())
};
let Some(w) = widget else {
return 1; };
rthingy(dst); if bindwidget(w, dst) != 0 {
return 1; }
0 }
pub fn bin_zle_new(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if args.is_empty() {
return 1;
}
let fname = if args.len() >= 2 {
args[1].clone()
} else {
args[0].clone()
};
let w = Arc::new(widget {
flags: 0i32, first: None,
u: WidgetImpl::UserFunc(fname), });
rthingy(&args[0]); if bindwidget(w.clone(), &args[0]) == 0 {
return 0; }
freewidget(w);
1 }
pub fn bin_zle_complete(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
if args.len() < 3 {
return 1;
}
let lookup = if args[1].starts_with('.') {
args[1].clone()
} else {
format!(".{}", args[1])
};
let comp_widget = {
let tab = thingytab().lock().unwrap();
tab.get(&lookup).and_then(|t| t.widget.clone())
};
let Some(cw) = comp_widget else {
return 1; };
if (cw.flags & ZLE_ISCOMP) == 0 {
return 1;
}
let w = Arc::new(widget {
flags: WIDGET_NCOMP | ZLE_MENUCMP | ZLE_KEEPSUFFIX,
first: None,
u: WidgetImpl::UserFunc(args[2].clone()),
});
rthingy(&args[0]);
if bindwidget(w.clone(), &args[0]) != 0 {
freewidget(w);
return 1; }
0 }
pub fn zle_usable() -> i32 {
let active = crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0;
let incompctlfunc = crate::ported::zle::compctl::INCOMPCTLFUNC .with(|c| c.get());
let incompfunc = crate::ported::zle::complete::INCOMPFUNC.load(Ordering::Relaxed) != 0;
if active && !incompctlfunc && !incompfunc {
1
} else {
0
}
}
pub fn bin_zle_flags(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
let mut ret: i32 = 0; if zle_usable() == 0 {
zwarnnam("zle", "can only set flags from a widget"); return 1; }
let bindk_present = BINDK
.lock()
.map(|b| b.is_some())
.unwrap_or(false);
if bindk_present {
for flag in args {
match flag.as_str() {
"yank" => {
}
"yankbefore" => {
}
"kill" => {
}
"menucmp" | "linemove" | "keepsuffix" => {
}
"vichange" => {
if invicmdmode(
&crate::ported::zle::zle_keymap::curkeymapname(),
) {
startvichange(-1); let zm_flags = ZMOD.lock().unwrap().flags;
if (zm_flags & (MOD_MULT | MOD_TMULT)) != 0 {
if let Ok(mut tab) = crate::ported::params::paramtab().write() {
if let Some(pm) = tab.get_mut("NUMERIC") {
if (pm.node.flags as u32 & crate::ported::zsh_h::PM_SPECIAL) != 0 {
pm.node.flags &=
!(crate::ported::zsh_h::PM_UNSET as i32);
}
}
}
}
}
}
_ => {
zwarnnam(
"zle",
&format!("invalid flag `{}' given to zle -f", flag),
); ret = 1; }
}
}
}
ret }
pub fn bin_zle_call(_name: &str, args: &[String], _ops: &options, _func: i32) -> i32 {
let modsave: modifier = (*ZMOD.lock().unwrap()).clone(); let mut saveflag = 0i32; let mut setbindk = 0i32; let mut setlbindk = 0i32; let mut keymap_restore: Option<String> = None; let mut argv: Vec<String> = args.to_vec();
let wname = if argv.is_empty() {
None
} else {
Some(argv.remove(0))
};
if wname.is_none() {
return if zle_usable() != 0 { 0 } else { 1 }; }
let wname = wname.unwrap();
if zle_usable() == 0 {
zwarnnam("zle", "widgets can only be called when ZLE is active"); return 1; }
while !argv.is_empty() && argv[0].starts_with('-') {
let cur = argv[0].clone();
if cur.len() == 1 || (cur.len() == 2 && cur.as_bytes()[1] == b'-') {
argv.remove(0); break; }
let mut byte_idx = 1usize; let mut consumed_next = false;
let cur_bytes = cur.as_bytes();
while byte_idx < cur_bytes.len() {
let c = cur_bytes[byte_idx];
byte_idx += 1;
match c {
b'f' => {
let flag: Option<String> = if byte_idx < cur_bytes.len() {
Some(
std::str::from_utf8(&cur_bytes[byte_idx..])
.unwrap_or("")
.to_string(),
)
} else if argv.len() > 1 {
Some(argv[1].clone())
} else {
None
};
if flag.as_deref() != Some("nolast") {
zwarnnam("zle", "'nolast' expected after -f"); return 1; }
if byte_idx >= cur_bytes.len() {
argv.remove(1); consumed_next = true;
}
setlbindk = 1; byte_idx = cur_bytes.len(); }
b'n' => {
let num: Option<String> = if byte_idx < cur_bytes.len() {
Some(
std::str::from_utf8(&cur_bytes[byte_idx..])
.unwrap_or("")
.to_string(),
)
} else if argv.len() > 1 {
Some(argv[1].clone())
} else {
None
};
if num.is_none() {
zwarnnam("zle", &format!("number expected after -{}", c as char)); return 1; }
if byte_idx >= cur_bytes.len() {
argv.remove(1);
consumed_next = true;
}
saveflag = 1; let n: i32 = num.unwrap().parse().unwrap_or(0); let mut zm = ZMOD.lock().unwrap();
zm.mult = n; zm.flags |= MOD_MULT; byte_idx = cur_bytes.len();
}
b'N' => {
saveflag = 1; let mut zm = ZMOD.lock().unwrap();
zm.mult = 1; zm.flags &= !MOD_MULT; }
b'K' => {
let keymap_tmp: Option<String> = if byte_idx < cur_bytes.len() {
Some(
std::str::from_utf8(&cur_bytes[byte_idx..])
.unwrap_or("")
.to_string(),
)
} else if argv.len() > 1 {
Some(argv[1].clone())
} else {
None
};
if keymap_tmp.is_none() {
zwarnnam("zle", &format!("keymap expected after -{}", c as char)); return 1; }
if byte_idx >= cur_bytes.len() {
argv.remove(1);
consumed_next = true;
}
keymap_restore =
Some(crate::ported::zle::zle_keymap::curkeymapname().clone()); if crate::ported::zle::zle_keymap::selectkeymap(
&keymap_tmp.unwrap(),
0,
) != 0
{
return 1; }
byte_idx = cur_bytes.len();
}
b'w' => {
setbindk = 1; }
_ => {
zwarnnam("zle", &format!("unknown option: {}", cur)); return 1; }
}
}
argv.remove(0); let _ = consumed_next; }
rthingy(&wname); let ret = execzlefunc(
&wname, &argv, setbindk, setlbindk,
); unrefthingy(&wname);
if saveflag != 0 {
*ZMOD.lock().unwrap() = modsave; }
if let Some(k) = keymap_restore {
crate::ported::zle::zle_keymap::selectkeymap(&k, 0); }
ret }
pub fn bin_zle_invalidate(_name: &str, _args: &[String], _ops: &options, _func: i32) -> i32 {
if crate::ported::builtins::sched::zleactive.load(Ordering::Relaxed) != 0 {
ZLE_RESET_NEEDED.store(1, Ordering::SeqCst);
0 } else {
1 }
}
pub fn bin_zle_fd(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
let mut fd: i32 = 0; let mut found: bool = false;
if !args.is_empty() {
match args[0].parse::<i32>() {
Ok(n) if n >= 0 => fd = n,
_ => {
zwarnnam(
"zle",
&format!("Bad file descriptor number for -F: {}", args[0]),
); return 1; }
}
}
if OPT_ISSET(ops, b'L') || args.is_empty() {
if !args.is_empty() && args.len() > 1 {
zwarnnam("zle", "too many arguments for -FL"); return 1; }
if let Ok(tab) = WATCH_FDS.lock() {
for w in tab.iter() {
if !args.is_empty() && w.fd != fd {
continue; }
found = true; let w_flag = if w.widget != 0 { "-w " } else { "" };
println!("zle -F {}{} {}", w_flag, w.fd, w.func);
}
}
return if !args.is_empty() && !found { 1 } else { 0 }; }
if args.len() > 1 {
let funcnam = args[1].clone(); if let Ok(mut tab) = WATCH_FDS.lock() {
for w in tab.iter_mut() {
if w.fd == fd {
w.func = funcnam.clone(); w.widget = if OPT_ISSET(ops, b'w') { 1 } else { 0 }; found = true; break; }
}
if !found {
tab.push(watch_fd {
fd, func: funcnam, widget: if OPT_ISSET(ops, b'w') { 1 } else { 0 }, });
}
}
} else {
if let Ok(mut tab) = WATCH_FDS.lock() {
let len_before = tab.len();
tab.retain(|w| w.fd != fd); found = tab.len() < len_before; }
if !found {
zwarnnam(
"zle",
&format!("No handler installed for fd {}", fd),
); return 1; }
}
0 }
pub fn bin_zle_transform(_name: &str, args: &[String], ops: &options, _func: i32) -> i32 {
let mut badargs: i32 = 0;
if OPT_ISSET(ops, b'L') {
if !args.is_empty() {
if args.len() > 1 {
badargs = 1; } else if args[0] != "tc" {
badargs = 2; }
}
if badargs == 0 {
let cur = TCOUT_FUNC_NAME.lock().ok().and_then(|n| n.clone());
if let Some(fname) = cur {
print!("zle -T tc "); print!("{}", crate::ported::utils::quotedzputs(&fname)); println!(); }
}
} else if OPT_ISSET(ops, b'r') {
if args.is_empty() {
badargs = -1; } else if args.len() > 1 {
badargs = 1; } else if args[0] == "tc" {
if let Ok(mut name) = TCOUT_FUNC_NAME.lock() {
if name.is_some() {
*name = None; }
}
} else {
badargs = 2;
}
} else {
if args.is_empty() || args.len() < 2 {
badargs = -1; } else {
if args[0] == "tc" {
if let Ok(mut name) = TCOUT_FUNC_NAME.lock() {
*name = Some(args[1].clone()); }
} else {
badargs = 2; }
}
}
if badargs != 0 {
if badargs == 2 {
zwarnnam(
"zle",
&format!("-T: no such transformation '{}'", args[0]), );
} else {
let way = if badargs > 0 { "many" } else { "few" }; zwarnnam("zle", &format!("too {} arguments for option -T", way));
}
return 1; }
0 }
pub fn init_thingies() -> i32 {
createthingytab(); let names = crate::ported::zle::zle_bindings::IWIDGET_NAMES;
let mut tab = thingytab().lock().unwrap();
for nam in names {
if !tab.contains_key(*nam) {
let mut t = makethingynode();
t.nam = nam.to_string(); t.flags |= TH_IMMORTAL; tab.insert(nam.to_string(), t);
}
}
0
}
#[derive(Debug, Clone)]
pub struct Thingy {
pub nam: String, pub flags: i32, pub rc: i32, pub widget: Option<Arc<widget>>, }
static THINGYTAB: OnceLock<Mutex<HashMap<String, Thingy>>> = OnceLock::new();
pub fn gethashnode2(name: &str) -> Option<Thingy> {
thingytab().lock().ok()?.get(name).cloned()
}
pub fn listwidgets() -> Vec<String> {
thingytab()
.lock()
.map(|t| t.keys().cloned().collect())
.unwrap_or_default()
}
pub fn getwidgettarget(name: &str) -> Option<String> {
let tab = thingytab().lock().ok()?;
let t = tab.get(name)?;
let w = t.widget.as_ref()?;
match &w.u {
WidgetImpl::Internal(_) => Some(name.to_string()),
WidgetImpl::UserFunc(s) => Some(s.clone()),
WidgetImpl::Comp { func, .. } => Some(func.clone()),
}
}
pub fn thingytab() -> &'static Mutex<HashMap<String, Thingy>> {
THINGYTAB.get_or_init(|| Mutex::new(HashMap::new()))
}
#[cfg(test)]
mod tests {
use super::*;
static LOCK: Mutex<()> = Mutex::new(());
fn reset_tab() {
thingytab().lock().unwrap().clear();
}
#[test]
fn bin_zle_rejects_incompatible_op_flags() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
let mut ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'l' as usize] = 1;
ops.ind[b'D' as usize] = 1;
let r = bin_zle("zle", &[], &ops, 0);
assert_eq!(
r, 1,
"c:373-374 — incompatible op flags (-l + -D) → return 1"
);
}
#[test]
fn bin_zle_call_rejects_unknown_option() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
let ops_empty = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_call("zle", &[
"widget_name".to_string(),
"-q".to_string(),
], &ops_empty, 0);
crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
assert_eq!(r, 1, "c:791-794 — unknown option char → return 1");
}
#[test]
fn init_thingies_populates_known_widget_names() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
thingytab().lock().unwrap().clear();
init_thingies();
let tab = thingytab().lock().unwrap();
assert!(
tab.contains_key("accept-line"),
"c:1028 — accept-line must be in THINGYTAB"
);
assert!(
tab.contains_key("self-insert"),
"c:1028 — self-insert must be in THINGYTAB"
);
assert!(
tab.contains_key("undefined-key"),
"c:1028 — undefined-key must be in THINGYTAB"
);
let al = tab.get("accept-line").unwrap();
assert_ne!(
al.flags & TH_IMMORTAL,
0,
"c:1027 — TH_IMMORTAL bit must be set"
);
}
#[test]
fn bin_zle_fd_rejects_bad_fd_string() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
let ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_fd("zle", &["notanumber".to_string()], &ops, 0);
assert_eq!(r, 1, "c:865-867 — non-numeric fd → 1");
let r2 = bin_zle_fd("zle", &["-1".to_string()], &ops, 0);
assert_eq!(r2, 1, "c:865-867 — negative fd → 1");
}
#[test]
fn bin_zle_fd_adds_new_handler() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
WATCH_FDS.lock().unwrap().clear();
let ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_fd(
"zle",
&["7".to_string(), "my_handler".to_string()],
&ops,
0,
);
assert_eq!(r, 0, "c:889-914 — install → 0");
let tab = WATCH_FDS.lock().unwrap();
assert_eq!(tab.len(), 1);
assert_eq!(tab[0].fd, 7);
assert_eq!(tab[0].func, "my_handler");
assert_eq!(tab[0].widget, 0);
}
#[test]
fn bin_zle_fd_delete_nonexistent_returns_1() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
WATCH_FDS.lock().unwrap().clear();
let ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_fd("zle", &["99".to_string()], &ops, 0);
assert_eq!(r, 1, "c:944-946 — delete unknown fd → 1");
}
#[test]
fn bin_zle_transform_rejects_too_few_args() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
let ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_transform("zle", &[], &ops, 0);
assert_eq!(r, 1, "c:989-1010 — too few args → 1");
}
#[test]
fn bin_zle_transform_default_form_sets_tc_handler() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
*TCOUT_FUNC_NAME.lock().unwrap() = None;
let ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_transform(
"zle",
&["tc".to_string(), "my_handler".to_string()],
&ops,
0,
);
assert_eq!(r, 0, "c:992-996 — valid `tc fname` → 0");
assert_eq!(
TCOUT_FUNC_NAME.lock().unwrap().as_deref(),
Some("my_handler"),
"c:996 — name should be stored"
);
}
#[test]
fn bin_zle_transform_r_clears_tc_handler() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
*TCOUT_FUNC_NAME.lock().unwrap() = Some("preset".to_string());
let mut ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'r' as usize] = 1;
let r = bin_zle_transform("zle", &["tc".to_string()], &ops, 0);
assert_eq!(r, 0, "c:983-985 — `-r tc` → 0");
assert!(
TCOUT_FUNC_NAME.lock().unwrap().is_none(),
"c:985 — name should be cleared"
);
}
#[test]
fn bin_zle_transform_rejects_unknown_transform() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
let mut ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'L' as usize] = 1;
let r = bin_zle_transform("zle", &["bogus".to_string()], &ops, 0);
assert_eq!(r, 1, "c:969-970/1005 — unknown transform → 1");
}
#[test]
fn bin_zle_flags_rejects_unknown_flag() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
*BINDK.lock().unwrap() = Some(Thingy {
nam: "dummy".to_string(),
flags: 0,
rc: 1,
widget: None,
});
let ops_empty = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_flags("zle", &["bogus_flag".to_string()], &ops_empty, 0);
*BINDK.lock().unwrap() = None;
crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
assert_eq!(r, 1, "c:692-693 — unknown flag → zwarnnam + ret=1");
}
#[test]
fn bin_zle_flags_accepts_yank_kill() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
*BINDK.lock().unwrap() = Some(Thingy {
nam: "dummy".to_string(),
flags: 0,
rc: 1,
widget: None,
});
let ops_empty = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_flags("zle", &[
"yank".to_string(),
"yankbefore".to_string(),
"kill".to_string(),
], &ops_empty, 0);
*BINDK.lock().unwrap() = None;
crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
assert_eq!(r, 0, "c:665-669 — all recognized → ret=0");
}
#[test]
fn bin_zle_call_rejects_bad_f_arg() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
crate::ported::builtins::sched::zleactive.store(1, Ordering::Relaxed);
let ops_empty = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
let r = bin_zle_call("zle", &[
"widget".to_string(),
"-f".to_string(),
"bogus".to_string(),
], &ops_empty, 0);
crate::ported::builtins::sched::zleactive.store(0, Ordering::Relaxed);
assert_eq!(r, 1, "c:741 — -f with non-'nolast' → return 1");
}
#[test]
fn bin_zle_rejects_too_few_args() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
let mut ops = options {
ind: [0u8; crate::ported::zsh_h::MAX_OPS],
args: Vec::new(),
argscount: 0,
argsalloc: 0,
};
ops.ind[b'D' as usize] = 1; let r = bin_zle("zle", &[], &ops, 0);
assert_eq!(
r, 1,
"c:379-381 — zle -D with zero args → 'not enough' → return 1"
);
}
#[test]
fn rthingy_creates_then_refs() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("foo");
let tab = thingytab().lock().unwrap();
let t = tab.get("foo").expect("rthingy must create");
assert_eq!(t.rc, 1);
assert_ne!((t.flags & DISABLED), 0);
}
#[test]
fn refthingy_unrefthingy_roundtrip() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("bar");
refthingy("bar");
assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 2);
unrefthingy("bar");
assert_eq!(thingytab().lock().unwrap().get("bar").unwrap().rc, 1);
unrefthingy("bar");
assert!(!thingytab().lock().unwrap().contains_key("bar"));
}
#[test]
fn rthingy_nocreate_returns_false_for_missing() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
assert!(!rthingy_nocreate("absent"));
assert!(!thingytab().lock().unwrap().contains_key("absent"));
}
#[test]
fn rthingy_nocreate_refs_existing() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("present");
assert!(rthingy_nocreate("present"));
assert_eq!(thingytab().lock().unwrap().get("present").unwrap().rc, 2);
}
#[test]
fn createthingytab_is_idempotent() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
createthingytab();
let after_first = thingytab().lock().unwrap().len();
createthingytab();
let after_second = thingytab().lock().unwrap().len();
assert_eq!(
after_first, after_second,
"createthingytab must not re-populate; was {} now {}",
after_first, after_second
);
}
#[test]
fn emptythingytab_skips_disabled_rthingy_entries() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("a");
rthingy("b");
rthingy("c");
let before = thingytab().lock().unwrap().len();
assert!(before >= 3);
emptythingytab();
let after = thingytab().lock().unwrap().len();
assert_eq!(
after, before,
"emptythingytab must NOT purge DISABLED rthingy entries"
);
}
#[test]
fn freethingynode_on_missing_name_is_safe() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
freethingynode("never-existed");
}
#[test]
fn unrefthingy_at_rc_one_frees_entry() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("solo");
assert_eq!(thingytab().lock().unwrap().get("solo").unwrap().rc, 1);
unrefthingy("solo");
assert!(
!thingytab().lock().unwrap().contains_key("solo"),
"rc=0 entry must be removed from thingytab"
);
}
#[test]
fn unrefthingy_on_missing_is_safe() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
unrefthingy("never-bound");
}
#[test]
fn makethingynode_starts_at_rc_zero_with_disabled_flag() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let n = makethingynode();
assert_eq!(n.rc, 0, "fresh node must have rc=0");
assert_ne!((n.flags & DISABLED), 0, "fresh node must have DISABLED flag set");
}
#[test]
fn rthingy_same_name_twice_only_increments_refcount() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
rthingy("dup");
rthingy("dup");
let tab = thingytab().lock().unwrap();
assert_eq!(tab.len(), 1);
assert!(
tab.get("dup").unwrap().rc >= 2,
"second rthingy must bump rc, not create a sibling"
);
}
#[test]
fn many_rthingy_unref_cycles_leave_no_residue() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _g = LOCK.lock().unwrap();
reset_tab();
for i in 0..20 {
rthingy(&format!("entry-{}", i));
}
assert!(thingytab().lock().unwrap().len() >= 20);
for i in 0..20 {
unrefthingy(&format!("entry-{}", i));
}
for i in 0..20 {
assert!(
!thingytab()
.lock()
.unwrap()
.contains_key(&format!("entry-{}", i)),
"entry-{} should be gone after final unref",
i
);
}
}
#[test]
fn zle_thingy_corpus_rthingy_nocreate_missing_returns_false() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
reset_tab();
assert!(!rthingy_nocreate("zshrs_never_thingy_xyz"));
}
#[test]
fn zle_thingy_corpus_rthingy_then_nocreate_finds_it() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
reset_tab();
rthingy("zshrs_test_thingy_a");
assert!(rthingy_nocreate("zshrs_test_thingy_a"));
}
#[test]
fn zle_thingy_corpus_unrefthingy_missing_no_panic() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
unrefthingy("zshrs_never_thingy_xyz_abc");
}
#[test]
#[ignore = "ZSHRS BUG: emptythingytab does not clear user-rthingy entries"]
fn zle_thingy_corpus_emptythingytab_clears_user_entries() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
reset_tab();
for i in 0..5 {
rthingy(&format!("e-{i}"));
}
assert!(thingytab().lock().unwrap().len() >= 5);
emptythingytab();
let t = thingytab().lock().unwrap();
for i in 0..5 {
assert!(!t.contains_key(&format!("e-{i}")), "e-{i} cleared");
}
}
#[test]
fn zle_thingy_corpus_freethingynode_missing_no_panic() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let _l = LOCK.lock().unwrap_or_else(|e| e.into_inner());
freethingynode("zshrs_never_thingy_xyz_abc");
}
}