use crate::ported::params::{paramtab, setaparam};
use crate::ported::utils::{imeta_byte, strpfx};
use crate::ported::zsh_h::{
isset, zattr, Inpar, Nularg, Outpar, COL_SEQ_BG, COL_SEQ_FG, PROMPTBANG, PROMPTPERCENT,
TERM_BAD, TERM_NOUP, TERM_UNKNOWN, TSC_PROMPT, TSC_RAW, TXTBGCOLOUR, TXTBOLDFACE, TXTFGCOLOUR,
TXTSTANDOUT, TXTUNDERLINE, TXT_ATTR_ALL, TXT_ATTR_BG_24BIT, TXT_ATTR_BG_COL_MASK,
TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_BG_MASK, TXT_ATTR_FG_24BIT, TXT_ATTR_FG_COL_MASK,
TXT_ATTR_FG_COL_SHIFT, TXT_ATTR_FG_MASK, TXT_ERROR,
};
use crate::zsh_h::Meta;
use crate::DPUTS;
use std::cell::RefCell;
use std::env;
use std::sync::atomic::Ordering;
pub(crate) mod prompt_tls {
use crate::ported::hist::curhist;
use crate::ported::jobs::JOBTAB;
use crate::ported::modules::parameter::FUNCSTACK;
use crate::ported::params::{getsparam, paramtab};
use crate::ported::utils::adjustcolumns;
use std::cell::RefCell;
use std::env;
thread_local! {
pub(super) static PWD: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static HOME: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static USER: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static HOST: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static HOST_SHORT: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static TTY: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static LASTVAL: RefCell<i32> = const { RefCell::new(0) };
pub(super) static HISTNUM: RefCell<i64> = const { RefCell::new(1) };
pub(super) static SHLVL: RefCell<i32> = const { RefCell::new(1) };
pub(super) static NUM_JOBS: RefCell<i32> = const { RefCell::new(0) };
pub(super) static IS_ROOT: RefCell<bool> = const { RefCell::new(false) };
pub(super) static CMDSTACK: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
pub(super) static PSVAR: RefCell<Vec<String>> = const { RefCell::new(Vec::new()) };
pub(super) static TERM_WIDTH: RefCell<usize> = const { RefCell::new(80) };
pub(super) static LINENO: RefCell<i64> = const { RefCell::new(1) };
pub(super) static SCRIPTNAME: RefCell<Option<String>> = const { RefCell::new(None) };
pub(super) static SCRIPTFILENAME: RefCell<Option<String>> =
const { RefCell::new(None) };
pub(super) static ARGEXTRA: RefCell<String> = const { RefCell::new(String::new()) };
pub(super) static FUNC_LINE_BASE: RefCell<Option<i64>> = const { RefCell::new(None) };
pub(super) static FUNCSTACK_FILENAME: RefCell<Option<String>> =
const { RefCell::new(None) };
}
pub(crate) fn sync_from_globals() {
let pwd = getsparam("PWD")
.filter(|p| !p.is_empty())
.or_else(|| env::var("PWD").ok().filter(|p| !p.is_empty()))
.or_else(|| {
env::current_dir()
.ok()
.map(|p| p.to_string_lossy().to_string())
})
.unwrap_or_else(|| "/".to_string());
let home = getsparam("HOME").unwrap_or_default();
let user = getsparam("USER")
.or_else(|| getsparam("LOGNAME"))
.or_else(|| env::var("USER").ok())
.or_else(|| env::var("LOGNAME").ok())
.unwrap_or_else(|| "user".to_string());
let host = getsparam("HOST")
.or_else(|| {
hostname::get()
.ok()
.map(|h| h.to_string_lossy().to_string())
})
.unwrap_or_else(|| "localhost".to_string());
let host_short = host.split('.').next().unwrap_or(&host).to_string();
let shlvl = getsparam("SHLVL")
.and_then(|s| s.parse().ok())
.or_else(|| env::var("SHLVL").ok().and_then(|s| s.parse().ok()))
.unwrap_or(1);
PWD.with(|c| *c.borrow_mut() = pwd);
HOME.with(|c| *c.borrow_mut() = home);
USER.with(|c| *c.borrow_mut() = user);
HOST.with(|c| *c.borrow_mut() = host);
HOST_SHORT.with(|c| *c.borrow_mut() = host_short);
TTY.with(|c| c.borrow_mut().clear());
LASTVAL.with(|c| {
*c.borrow_mut() =
crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
});
HISTNUM.with(|c| {
*c.borrow_mut() = curhist.load(std::sync::atomic::Ordering::Relaxed);
});
SHLVL.with(|c| *c.borrow_mut() = shlvl);
NUM_JOBS.with(|c| {
*c.borrow_mut() = JOBTAB
.get_or_init(|| std::sync::Mutex::new(Vec::new()))
.lock()
.map(|t| t.iter().filter(|j| j.is_inuse()).count() as i32)
.unwrap_or(0);
});
IS_ROOT.with(|c| *c.borrow_mut() = unsafe { libc::geteuid() } == 0);
CMDSTACK.with(|c| {
*c.borrow_mut() = super::CMDSTACK.with(|stack| stack.borrow().clone());
});
PSVAR.with(|c| {
*c.borrow_mut() = paramtab()
.read()
.ok()
.and_then(|t| t.get("psvar").and_then(|p| p.u_arr.clone()))
.unwrap_or_default();
});
TERM_WIDTH.with(|c| {
*c.borrow_mut() = adjustcolumns();
});
LINENO.with(|c| {
*c.borrow_mut() = getsparam("LINENO")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(1);
});
let scriptname = crate::ported::utils::scriptname_get();
SCRIPTNAME.with(|c| *c.borrow_mut() = scriptname.clone().or_else(|| getsparam("0")));
SCRIPTFILENAME.with(|c| {
*c.borrow_mut() = crate::ported::utils::scriptfilename_get()
.or_else(|| scriptname.clone())
.or_else(|| getsparam("0"));
});
FUNC_LINE_BASE.with(|c| *c.borrow_mut() = None);
FUNCSTACK_FILENAME.with(|c| {
*c.borrow_mut() = FUNCSTACK
.lock()
.ok()
.and_then(|s| s.last().and_then(|fs| fs.filename.clone()));
});
ARGEXTRA.with(|c| {
*c.borrow_mut() = getsparam("ZSH_ARGZERO")
.or_else(|| env::args().next())
.unwrap_or_else(|| "zsh".to_string());
});
}
}
#[allow(non_camel_case_types)]
pub struct buf_vars {
pub buf: Vec<u8>,
pub bufspc: usize,
pub bp: usize,
pub bufline: usize,
pub bp1: Option<usize>,
pub fm: String,
pub fm_pos: usize,
pub truncwidth: i32,
pub dontcount: i32,
pub trunccount: i32,
pub rstring: Option<String>,
pub Rstring: Option<String>,
attrs: zattr,
in_escape: bool,
}
pub fn promptpath(path: &str, npath: usize, tilde: bool, home: &str) -> String {
let display = if tilde {
if !home.is_empty() && path.starts_with(home) {
let rest = &path[home.len()..];
if rest.is_empty() || rest.starts_with('/') {
format!("~{}", rest)
} else {
crate::ported::utils::finddir(path).unwrap_or_else(|| path.to_string())
}
} else {
crate::ported::utils::finddir(path).unwrap_or_else(|| path.to_string())
}
} else {
path.to_string()
};
if npath == 0 {
return display;
}
let signed = npath as i64;
let neg_n = if signed < 0 || (signed as u64) >= (i32::MIN as u32 as u64) {
let as_i32 = (signed as i32).wrapping_neg();
if as_i32 > 0 {
Some(as_i32 as usize)
} else {
None
}
} else {
None
};
let components: Vec<&str> = display.split('/').filter(|s| !s.is_empty()).collect();
if let Some(first_n) = neg_n {
if components.len() <= first_n {
return display;
}
components[..first_n].join("/")
} else {
if components.len() <= npath {
return display;
}
components[components.len() - npath..].join("/")
}
}
pub fn promptexpand(
s: &str,
_ns: i32,
_marker: Option<&str>,
) -> (String, Option<usize>, Option<usize>) {
let expanded = expand_prompt(s);
let rs_offset = s.find("%E").or_else(|| s.find("%E)")); let cap_rs_offset = s.find("%>>"); (expanded, rs_offset, cap_rs_offset)
}
pub fn zattrescape(attrs: zattr) -> String {
let mut result = String::new();
if attrs & TXTBOLDFACE != 0 {
result.push_str("%B");
} if attrs & TXTUNDERLINE != 0 {
result.push_str("%U");
} if attrs & TXTSTANDOUT != 0 {
result.push_str("%S");
} if attrs & TXTFGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT;
let c = if attrs & TXT_ATTR_FG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
result.push_str(&format!("%F{{{}}}", color_name(c)));
}
if attrs & TXTBGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_BG_COL_MASK) >> TXT_ATTR_BG_COL_SHIFT;
let c = if attrs & TXT_ATTR_BG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
result.push_str(&format!("%K{{{}}}", color_name(c)));
}
result
}
pub fn parsehighlight(spec: &str) -> zattr {
let mut attrs: zattr = 0;
for part in spec.split(',') {
let part = part.trim();
match part {
"bold" => attrs |= TXTBOLDFACE, "underline" => attrs |= TXTUNDERLINE, "standout" => attrs |= TXTSTANDOUT, "none" => {
attrs = 0; }
s if s.starts_with("fg=") => {
if let Some(code) = match_named_colour(&s[3..]) {
attrs = zattr_set_fg_palette(attrs, code); }
}
s if s.starts_with("bg=") => {
if let Some(code) = match_named_colour(&s[3..]) {
attrs = zattr_set_bg_palette(attrs, code); }
}
_ => {}
}
}
attrs
}
pub fn parsecolorchar(bv: &mut buf_vars, arg: zattr, is_fg: bool) -> zattr {
use crate::ported::zsh_h::{
TXTBGCOLOUR, TXTFGCOLOUR, TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_FG_COL_SHIFT,
};
let on_bit = if is_fg { TXTFGCOLOUR } else { TXTBGCOLOUR };
let shift = if is_fg {
TXT_ATTR_FG_COL_SHIFT
} else {
TXT_ATTR_BG_COL_SHIFT
};
if bv.fm.as_bytes().get(bv.fm_pos + 1).copied() == Some(b'{') {
bv.fm_pos += 2;
let bytes = bv.fm.as_bytes();
let mut ep = bv.fm_pos;
while ep < bytes.len() && bytes[ep] != b'}' {
ep += 1;
}
if ep < bytes.len() {
let name: String = bv.fm[bv.fm_pos..ep].to_string();
bv.fm_pos = ep;
if let Some(color) = color_from_name(&name) {
return on_bit | ((color as zattr) << shift);
}
on_bit | (arg << shift)
} else {
if bv.fm_pos > 0 {
bv.fm_pos -= 1;
}
arg
}
} else {
on_bit | (arg << shift)
}
}
pub fn putpromptchar(bv: &mut buf_vars, doprint: i32, endchar: i32) -> i32 {
use crate::ported::zsh_h::{isset, PROMPTPERCENT};
use crate::ported::ztype_h::idigit;
loop {
let c = match bv.fm.as_bytes().get(bv.fm_pos).copied() {
Some(0) | None => return 0, Some(c) if c == endchar as u8 => return c as i32, Some(c) => c,
};
let mut arg: i32 = 0;
if c == b'%' && isset(PROMPTPERCENT) {
let mut minus = 0;
bv.fm_pos += 1;
if bv.fm.as_bytes().get(bv.fm_pos).copied() == Some(b'-') {
minus = 1;
bv.fm_pos += 1;
}
let nb = bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0);
if idigit(nb) {
let start = bv.fm_pos;
while bv.fm_pos < bv.fm.len() && idigit(bv.fm.as_bytes()[bv.fm_pos]) {
bv.fm_pos += 1;
}
arg = bv.fm[start..bv.fm_pos].parse::<i32>().unwrap_or(0);
if minus != 0 {
arg = -arg;
}
} else if minus != 0 {
arg = -1;
}
if bv.fm.as_bytes().get(bv.fm_pos).copied() == Some(b'(') {
bv.fm_pos += 1; if bv.fm_pos < bv.fm.len() && idigit(bv.fm.as_bytes()[bv.fm_pos]) {
let start = bv.fm_pos;
while bv.fm_pos < bv.fm.len() && idigit(bv.fm.as_bytes()[bv.fm_pos]) {
bv.fm_pos += 1;
}
arg = bv.fm[start..bv.fm_pos].parse::<i32>().unwrap_or(0);
} else if arg < 0 {
arg = -arg; }
let tc = bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0);
let mut test: i32 = 0;
match tc {
b'c' | b'.' | b'~' | b'/' | b'C' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let home = prompt_tls::HOME.with(|c| c.borrow().clone());
let strip_home = matches!(tc, b'c' | b'.' | b'~');
let ss: &str = if strip_home && !home.is_empty() && pwd == home {
arg -= 1; ""
} else if strip_home
&& !home.is_empty()
&& pwd.starts_with(&format!("{}/", home))
{
arg -= 1; &pwd[home.len()..]
} else {
&pwd
};
let bytes = ss.as_bytes();
if bytes.len() >= 2 && bytes[0] == b'/' {
arg -= 1; }
let skip_first = if !bytes.is_empty() && bytes[0] == b'/' {
1
} else {
0
};
for &b in &bytes[skip_first..] {
if b == b'/' {
arg -= 1; }
}
if arg <= 0 {
test = 1; }
}
b'?' => {
let lv = prompt_tls::LASTVAL.with(|c| *c.borrow());
if lv == arg {
test = 1;
}
}
b'#' => {
let euid = unsafe { libc::geteuid() } as i32;
if euid == arg {
test = 1;
}
}
b'g' => {
let egid = unsafe { libc::getegid() } as i32;
if egid == arg {
test = 1;
}
}
b'l' => {
if 0 >= arg {
test = 1;
}
}
b'L' => {
let shlvl = crate::ported::params::getsparam("SHLVL")
.and_then(|s| s.parse::<i32>().ok())
.unwrap_or(1);
if shlvl >= arg {
test = 1;
}
}
b'_' => {
let cmdsp = prompt_tls::CMDSTACK
.with(|c| c.borrow().len() as i32);
if cmdsp >= arg {
test = 1;
}
}
b'!' => {
let euid = unsafe { libc::geteuid() };
if euid == 0 {
test = 1;
}
}
b'j' => {
let mut numjobs = 0i32;
if let Some(tab_lock) = crate::ported::jobs::JOBTAB.get() {
if let Ok(tab) = tab_lock.lock() {
let max = crate::ported::jobs::MAXJOB
.get()
.and_then(|m| m.lock().ok().map(|g| *g))
.unwrap_or(0);
let mut j = 1usize;
while j <= max && j < tab.len() {
let jb = &tab[j];
if jb.stat != 0
&& !jb.procs.is_empty()
&& (jb.stat & crate::ported::zsh_h::STAT_NOPRINT) == 0
{
numjobs += 1;
}
j += 1;
}
}
}
if numjobs >= arg {
test = 1;
}
}
b'e' => {
let depth = crate::ported::modules::parameter::FUNCSTACK
.lock()
.ok()
.map(|stk| stk.len() as i32)
.unwrap_or(0);
let mut t = arg;
let mut remaining = depth;
while remaining > 0 && t > 0 {
t -= 1;
remaining -= 1;
}
if t == 0 {
test = 1;
}
}
b'v' => {
let psvar_len = crate::ported::params::getaparam("psvar")
.map(|v| v.len() as i32)
.unwrap_or(0);
if psvar_len >= arg {
test = 1;
}
}
b'V' => {
if let Some(psvar) = crate::ported::params::getaparam("psvar") {
if !psvar.is_empty() && (psvar.len() as i32) >= arg {
let idx = if arg > 0 { arg - 1 } else { 0 };
if let Some(elem) = psvar.get(idx as usize) {
if !elem.is_empty() {
test = 1;
}
}
}
}
}
b'S' => {
if arg <= 0 {
test = 1;
}
}
_ => {
}
}
bv.fm_pos += 1; let sep = match bv.fm.as_bytes().get(bv.fm_pos).copied() {
Some(0) | None => return 0,
Some(c) => c,
};
bv.fm_pos += 1; let otruncwidth = bv.truncwidth;
bv.truncwidth = 0;
let r1 = putpromptchar(bv, if test == 1 { doprint } else { 0 }, sep as i32);
if r1 == 0 {
bv.truncwidth = otruncwidth;
return 0;
}
bv.fm_pos += 1;
if bv.fm_pos >= bv.fm.len() {
bv.truncwidth = otruncwidth;
return 0;
}
let r2 = putpromptchar(bv, if test == 0 { doprint } else { 0 }, b')' as i32);
if r2 == 0 {
bv.truncwidth = otruncwidth;
return 0;
}
bv.truncwidth = otruncwidth;
bv.fm_pos += 1; continue;
}
if doprint == 0 {
let xc = bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0);
match xc {
b'[' => {
while bv.fm_pos + 1 < bv.fm.len() && idigit(bv.fm.as_bytes()[bv.fm_pos + 1])
{
bv.fm_pos += 1;
}
while bv.fm_pos + 1 < bv.fm.len() && bv.fm.as_bytes()[bv.fm_pos + 1] != b']'
{
bv.fm_pos += 1;
}
bv.fm_pos += 1; }
b'<' => {
while bv.fm_pos + 1 < bv.fm.len() && bv.fm.as_bytes()[bv.fm_pos + 1] != b'<'
{
bv.fm_pos += 1;
}
bv.fm_pos += 1;
}
b'>' => {
while bv.fm_pos + 1 < bv.fm.len() && bv.fm.as_bytes()[bv.fm_pos + 1] != b'>'
{
bv.fm_pos += 1;
}
bv.fm_pos += 1;
}
b'D' => {
if bv.fm.as_bytes().get(bv.fm_pos + 1).copied() == Some(b'{') {
while bv.fm_pos + 1 < bv.fm.len()
&& bv.fm.as_bytes()[bv.fm_pos + 1] != b'}'
{
bv.fm_pos += 1;
}
bv.fm_pos += 1;
}
}
_ => {} }
bv.fm_pos += 1;
continue;
}
let xc = bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0);
let trunc_to_last = |path: &str, n: usize| -> String {
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if parts.len() <= n {
path.to_string()
} else {
parts[parts.len() - n..].join("/")
}
};
match xc {
b'~' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let home = prompt_tls::HOME.with(|c| c.borrow().clone());
let mut s = if !home.is_empty() && pwd.starts_with(&home) {
format!("~{}", &pwd[home.len()..])
} else {
pwd
};
if arg > 0 {
s = trunc_to_last(&s, arg as usize);
}
stradd(bv, &s);
}
b'd' | b'/' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let s = if arg > 0 {
trunc_to_last(&pwd, arg as usize)
} else if arg < 0 {
let mut npath = arg;
let bytes = pwd.as_bytes();
let mut end = bytes.len();
let mut i = 1usize; while i < bytes.len() {
if bytes[i] == b'/' {
npath += 1;
if npath == 0 {
end = i;
break;
}
}
i += 1;
}
pwd[..end].to_string()
} else {
pwd
};
stradd(bv, &s);
}
b'c' | b'.' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let home = prompt_tls::HOME.with(|c| c.borrow().clone());
let path = if !home.is_empty() && pwd.starts_with(&home) {
format!("~{}", &pwd[home.len()..])
} else {
pwd
};
let n = if arg > 0 { arg as usize } else { 1 };
let parts: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
let tail = if parts.len() <= n {
path
} else {
parts[parts.len() - n..].join("/")
};
stradd(bv, &tail);
}
b'C' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let n = if arg > 0 { arg as usize } else { 1 };
let parts: Vec<&str> = pwd.split('/').filter(|s| !s.is_empty()).collect();
let tail = if parts.len() <= n {
pwd
} else {
parts[parts.len() - n..].join("/")
};
stradd(bv, &tail);
}
b'n' => {
let u = prompt_tls::USER.with(|c| c.borrow().clone());
stradd(bv, &u);
}
b'M' => {
let h = prompt_tls::HOST.with(|c| c.borrow().clone());
stradd(bv, &h);
}
b'm' => {
let h = prompt_tls::HOST.with(|c| c.borrow().clone());
let n = if arg == 0 { 1 } else { arg };
let parts: Vec<&str> = h.split('.').collect();
let out: String = if n > 0 {
let take = (n as usize).min(parts.len());
parts[..take].join(".")
} else {
let take = ((-n) as usize).min(parts.len());
parts[parts.len() - take..].join(".")
};
stradd(bv, &out);
}
b'S' => {
let _ = tsetattrs(TXTSTANDOUT); let sgr = applytextattributes(TSC_PROMPT);
if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b's' => {
let _ = tunsetattrs(TXTSTANDOUT); let sgr = applytextattributes(TSC_PROMPT); if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'B' => {
let _ = tsetattrs(TXTBOLDFACE); let sgr = applytextattributes(TSC_PROMPT); if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'b' => {
let _ = tunsetattrs(TXTBOLDFACE); let sgr = applytextattributes(TSC_PROMPT); if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'U' => {
let _ = tsetattrs(TXTUNDERLINE); let sgr = applytextattributes(TSC_PROMPT); if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'u' => {
let _ = tunsetattrs(TXTUNDERLINE); let sgr = applytextattributes(TSC_PROMPT); if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'F' | b'K' => {
let is_fg = xc == b'F';
let mut color: Option<Color> = None;
if bv.fm.as_bytes().get(bv.fm_pos + 1).copied() == Some(b'{') {
let start = bv.fm_pos + 2;
let mut end = start;
while end < bv.fm.len() && bv.fm.as_bytes()[end] != b'}' {
end += 1;
}
if end < bv.fm.len() {
let name = &bv.fm[start..end];
color = color_from_name(name);
bv.fm_pos = end; }
} else if arg >= 0 {
color = Some(arg as Color);
}
if let Some(c) = color {
let attr = if is_fg {
zattr_set_fg_palette(0, c as u8)
} else {
zattr_set_bg_palette(0, c as u8)
};
let _ = tsetattrs(attr);
let sgr = applytextattributes(TSC_PROMPT);
if !sgr.is_empty() {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
} else {
let mask = if is_fg { TXTFGCOLOUR } else { TXTBGCOLOUR };
let _ = tunsetattrs(mask);
let mut sgr = applytextattributes(TSC_PROMPT);
if sgr.is_empty() {
sgr = if is_fg {
"\x1b[39m".to_string()
} else {
"\x1b[49m".to_string()
};
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
b'f' | b'k' => {
let is_fg = xc == b'f';
let mask = if is_fg { TXTFGCOLOUR } else { TXTBGCOLOUR };
let _ = tunsetattrs(mask);
let mut sgr = applytextattributes(TSC_PROMPT);
if sgr.is_empty() {
sgr = if is_fg {
"\x1b[39m".to_string()
} else {
"\x1b[49m".to_string()
};
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in sgr.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
b'{' => {
if bv.dontcount == 0 {
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
}
bv.dontcount += 1;
}
b'}' => {
if bv.dontcount > 0 {
bv.dontcount -= 1;
if bv.dontcount == 0 {
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
}
}
b'#' => {
let euid = unsafe { libc::geteuid() };
pputc(bv, if euid == 0 { b'#' } else { b'%' });
}
b'?' => {
let lv = prompt_tls::LASTVAL.with(|c| *c.borrow());
stradd(bv, &lv.to_string());
}
b'j' => {
let mut numjobs = 0i32;
if let Some(tab_lock) = crate::ported::jobs::JOBTAB.get() {
if let Ok(tab) = tab_lock.lock() {
let max = crate::ported::jobs::MAXJOB
.get()
.and_then(|m| m.lock().ok().map(|g| *g))
.unwrap_or(0);
let mut j = 1usize;
while j <= max && j < tab.len() {
let jb = &tab[j];
if jb.stat != 0
&& !jb.procs.is_empty()
&& (jb.stat & crate::ported::zsh_h::STAT_NOPRINT) == 0
{
numjobs += 1; }
j += 1;
}
}
}
stradd(bv, &numjobs.to_string()); }
b'!' | b'h' => {
let n = crate::ported::hist::curhist.load(std::sync::atomic::Ordering::SeqCst);
stradd(bv, &n.to_string());
}
b't' | b'T' | b'@' | b'*' | b'w' | b'W' | b'D' => {
let tmfmt: String;
match xc {
b'T' => tmfmt = "%K:%M".to_string(), b'*' => tmfmt = "%K:%M:%S".to_string(), b'w' => tmfmt = "%a %f".to_string(), b'W' => tmfmt = "%m/%d/%y".to_string(), b'D' => {
if bv.fm.as_bytes().get(bv.fm_pos + 1).copied() == Some(b'{') {
let bytes = bv.fm.as_bytes();
let mut ss = bv.fm_pos + 2; let mut collected = String::new();
while ss < bytes.len() && bytes[ss] != b'}' {
if bytes[ss] == b'\\' && ss + 1 < bytes.len() {
ss += 1;
collected.push(bytes[ss] as char);
} else {
collected.push(bytes[ss] as char);
}
ss += 1;
}
bv.fm_pos = ss;
if collected.is_empty() {
bv.fm_pos += 1;
continue;
}
tmfmt = collected;
} else {
tmfmt = "%y-%m-%d".to_string(); }
}
_ => tmfmt = "%l:%M%p".to_string(),
}
let now = std::time::SystemTime::now();
let rendered = crate::ported::utils::ztrftime(&tmfmt, now);
stradd(bv, &rendered);
}
b'i' => {
let ln = crate::ported::params::getsparam("LINENO")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or_else(|| {
crate::ported::input::lineno.with(|l| l.get()) as i64
});
stradd(bv, &ln.to_string());
}
b'L' => {
let shlvl = crate::ported::params::getsparam("SHLVL")
.and_then(|s| s.parse::<i32>().ok())
.or_else(|| std::env::var("SHLVL").ok().and_then(|s| s.parse().ok()))
.unwrap_or(1);
stradd(bv, &shlvl.to_string());
}
b'N' => {
let nam = crate::ported::utils::scriptname_get()
.filter(|s: &String| !s.is_empty())
.or_else(|| {
crate::ported::params::getsparam("ZSH_SCRIPT")
.filter(|s| !s.is_empty())
})
.or_else(|| {
crate::ported::params::getsparam("ZSH_NAME")
.filter(|s| !s.is_empty())
})
.unwrap_or_else(|| "zsh".to_string());
stradd(bv, &nam);
}
b'x' => {
let in_fn_filename = prompt_tls::FUNCSTACK_FILENAME
.with(|c| c.borrow().clone());
let nam = if let Some(fname) = in_fn_filename {
fname
} else {
prompt_tls::SCRIPTFILENAME
.with(|c| c.borrow().clone())
.or_else(|| prompt_tls::ARGEXTRA.with(|c| {
let s = c.borrow().clone();
if s.is_empty() { None } else { Some(s) }
}))
.unwrap_or_else(|| "zsh".to_string())
};
stradd(bv, &nam);
}
b'e' => {
let depth = crate::ported::modules::parameter::FUNCSTACK
.lock()
.ok()
.map(|stk| stk.len())
.unwrap_or(0);
stradd(bv, &depth.to_string());
}
b'I' => {
let cur_lineno: i64 = crate::ported::params::getsparam("LINENO")
.and_then(|s| s.parse::<i64>().ok())
.unwrap_or(1);
let in_fn_offset: Option<i64> = crate::ported::modules::parameter::FUNCSTACK
.lock()
.ok()
.and_then(|stk| stk.last().map(|fs| fs.flineno));
let abs = match in_fn_offset {
Some(off) => cur_lineno + off,
None => cur_lineno,
};
stradd(bv, &abs.to_string());
}
b'_' => {
let stack = prompt_tls::CMDSTACK.with(|c| c.borrow().clone());
let cmdsp = stack.len() as i32;
if cmdsp > 0 {
let (start, mut count) = if arg >= 0 {
let n = if arg == 0 || arg > cmdsp { cmdsp } else { arg };
((cmdsp - n) as usize, n) } else {
let n = if -arg > cmdsp { cmdsp } else { -arg };
(0usize, n) };
let mut t0 = start;
while count > 0 {
count -= 1;
let idx = stack[t0] as usize;
if let Some(name) = CMDNAMES.get(idx) {
stradd(bv, name); }
if count > 0 {
stradd(bv, " "); }
t0 += 1;
}
}
}
b'^' => {
let stack = prompt_tls::CMDSTACK.with(|c| c.borrow().clone());
let cmdsp = stack.len() as i32;
if cmdsp > 0 {
let (start, mut count) = if arg >= 0 {
let n = if arg == 0 || arg > cmdsp { cmdsp } else { arg };
((cmdsp - 1) as usize, n) } else {
let n = if -arg > cmdsp { cmdsp } else { -arg };
((n - 1) as usize, n) };
let mut t0 = start as i32;
while count > 0 {
count -= 1;
if t0 < 0 || (t0 as usize) >= stack.len() {
break;
}
let idx = stack[t0 as usize] as usize;
if let Some(name) = CMDNAMES.get(idx) {
stradd(bv, name); }
if count > 0 {
stradd(bv, " "); }
t0 -= 1;
}
}
}
b'l' => {
let tty = unsafe {
let p = libc::ttyname(0);
if p.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(p)
.to_str()
.unwrap_or("")
.to_string()
}
};
if tty.is_empty() {
stradd(bv, "()");
} else if let Some(rest) = tty.strip_prefix("/dev/tty") {
stradd(bv, rest);
} else if let Some(rest) = tty.strip_prefix("/dev/") {
stradd(bv, rest);
} else {
stradd(bv, &tty);
}
}
b'y' => {
let tty = unsafe {
let p = libc::ttyname(0);
if p.is_null() {
String::new()
} else {
std::ffi::CStr::from_ptr(p)
.to_str()
.unwrap_or("")
.to_string()
}
};
if tty.is_empty() {
stradd(bv, "()");
} else if let Some(rest) = tty.strip_prefix("/dev/") {
stradd(bv, rest);
} else {
stradd(bv, &tty);
}
}
b'v' => {
let n = if arg == 0 { 1 } else { arg };
let psvar = crate::ported::params::getsparam("psvar");
let arr: Vec<String> = if let Some(s) = psvar {
s.split(' ').map(String::from).collect()
} else {
crate::ported::exec_hooks::array("psvar").unwrap_or_default()
};
let idx: i32 = if n < 0 {
arr.len() as i32 + n
} else {
n - 1
};
if idx >= 0 && (idx as usize) < arr.len() {
stradd(bv, &arr[idx as usize]);
}
}
b'E' => {
let esc = "\x1b[K";
addbufspc(bv, 1);
bv.buf[bv.bp] = Inpar as u8;
bv.bp += 1;
for &b in esc.as_bytes() {
pputc(bv, b);
}
addbufspc(bv, 1);
bv.buf[bv.bp] = Outpar as u8;
bv.bp += 1;
}
b'%' => pputc(bv, b'%'),
b')' => pputc(bv, b')'),
b'<' | b'>' => {
let truncchar = xc as i32;
let _ = prompttrunc(bv, arg, truncchar, doprint, endchar);
}
b'[' => {
let mut local_arg = arg;
bv.fm_pos += 1;
let bytes = bv.fm.as_bytes();
if bv.fm_pos < bytes.len() && idigit(bytes[bv.fm_pos]) {
let mut end = bv.fm_pos;
while end < bytes.len() && idigit(bytes[end]) {
end += 1;
}
let num: i32 = std::str::from_utf8(&bytes[bv.fm_pos..end])
.ok()
.and_then(|s| s.parse().ok())
.unwrap_or(0);
local_arg = num;
bv.fm_pos = end;
}
let _ = prompttrunc(bv, local_arg, b']' as i32, doprint, endchar);
if bv.fm_pos > 0 {
bv.fm_pos -= 1;
}
}
0 => return 0,
_ => {}
}
bv.fm_pos += 1;
} else {
if doprint != 0 {
pputc(bv, c);
}
bv.fm_pos += 1;
}
}
}
pub fn addbufspc(bv: &mut buf_vars, mut need: i32) {
need = need.saturating_mul(2); if bv.bp as i32 + need > bv.bufspc as i32 {
if need & 255 != 0 {
need = (need | 255) + 1;
}
let new_size = bv.bufspc as i32 + need;
bv.buf.resize(new_size as usize, 0); bv.bufspc = new_size as usize; }
}
pub fn pputc(bv: &mut buf_vars, mut c: u8) {
use crate::ported::ztype_h::imeta;
if imeta(c) {
addbufspc(bv, 2); bv.buf[bv.bp] = Meta as u8; bv.bp += 1;
c ^= 32; } else {
addbufspc(bv, 1); }
bv.buf[bv.bp] = c; bv.bp += 1;
if c == b'\n' && bv.dontcount == 0 {
bv.bufline = bv.bp;
}
}
pub fn stradd(bv: &mut buf_vars, d: &str) {
let mut raw: Vec<u8> = d.as_bytes().to_vec();
crate::ported::utils::unmetafy(&mut raw);
match std::str::from_utf8(&raw) {
Ok(decoded) => {
for ch in decoded.chars() {
let pc = crate::ported::utils::wcs_nicechar(ch, None, None);
addbufspc(bv, pc.len() as i32); for &b in pc.as_bytes() {
pputc(bv, b); }
}
}
Err(_) => {
for &b in &raw {
let pc = crate::ported::utils::nicechar(b as char);
addbufspc(bv, pc.len() as i32); for &out_b in pc.as_bytes() {
pputc(bv, out_b); }
}
}
}
}
pub fn tsetcap(cap: i32, flags: i32) -> String {
let mut out = String::new();
let tclen_guard = crate::ported::init::tclen.lock().unwrap();
let cap_ok = cap >= 0 && (cap as usize) < tclen_guard.len() && tclen_guard[cap as usize] != 0;
drop(tclen_guard);
let termflags = crate::ported::params::TERMFLAGS.load(Ordering::SeqCst);
if !(cap_ok && (termflags & (TERM_NOUP | TERM_BAD | TERM_UNKNOWN)) == 0) {
return out;
}
let cap_str = crate::ported::init::tcstr
.lock()
.unwrap()
.get(cap as usize)
.cloned()
.unwrap_or_default();
match flags {
x if x == TSC_RAW => {
let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
let out_fd = if fd >= 0 { fd } else { 2 };
let _ = crate::ported::utils::write_loop(out_fd, cap_str.as_bytes());
}
x if x == TSC_PROMPT => {
out.push(Inpar); out.push_str(&cap_str); out.push(Outpar); }
_ => {
let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
let out_fd = if fd >= 0 { fd } else { 1 };
let _ = crate::ported::utils::write_loop(out_fd, cap_str.as_bytes());
}
}
out
}
thread_local! {
pub static PUTSTR_BUF: RefCell<Vec<u8>> = const { RefCell::new(Vec::new()) };
}
pub fn putstr(d: i32) -> i32 {
PUTSTR_BUF.with(|b| {
let mut buf = b.borrow_mut();
let byte = (d & 0xff) as u8;
if byte >= 0x83 {
buf.push(Meta);
buf.push(byte ^ 0x20);
} else {
buf.push(byte);
}
});
0 }
pub fn prompttrunc(
bv: &mut buf_vars,
arg: i32,
truncchar: i32, doprint: i32,
endchar: i32,
) -> i32 {
if arg > 0 {
let ch = bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0);
let truncatleft = ch == b'<'; let w = bv.bp;
if bv.truncwidth != 0 {
while bv.fm_pos > 0 {
bv.fm_pos -= 1; if bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0) == b'%' {
break;
}
}
if bv.fm_pos > 0 {
bv.fm_pos -= 1;
} return 0; }
bv.truncwidth = arg; if bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0) != b']' {
bv.fm_pos += 1; }
let tchar = truncchar as u8;
while let Some(&c) = bv.fm.as_bytes().get(bv.fm_pos) {
if c == 0 || c == tchar {
break;
}
let mut cur = c;
if cur == b'\\' && bv.fm.as_bytes().get(bv.fm_pos + 1).is_some() {
bv.fm_pos += 1;
cur = bv.fm.as_bytes()[bv.fm_pos];
}
if bv.bp >= bv.buf.len() {
bv.buf.resize(bv.bp + 1, 0);
}
bv.buf[bv.bp] = cur; bv.bp += 1;
bv.fm_pos += 1;
}
if bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0) == 0 {
return 0; }
if bv.bp == w && truncchar == b']' as i32 {
if bv.bp >= bv.buf.len() {
bv.buf.resize(bv.bp + 1, 0);
}
bv.buf[bv.bp] = b'<'; bv.bp += 1;
}
let ptr = w;
let trunc_bytes = bv.buf[ptr..bv.bp].to_vec();
let truncstr = String::from_utf8_lossy(&trunc_bytes).into_owned();
bv.bp = ptr; let w_save = bv.bp; bv.fm_pos += 1; bv.trunccount = bv.dontcount; putpromptchar(bv, doprint, endchar); bv.trunccount = 0; let ptr = w_save; if bv.bp < bv.buf.len() {
bv.buf[bv.bp] = 0;
}
let region_bytes = &bv.buf[ptr..bv.bp];
let region_str = std::str::from_utf8(region_bytes).unwrap_or("");
let mut visible_w: i32 = 0;
for _ in region_str.chars() {
visible_w += 1;
}
if visible_w > bv.truncwidth {
let maxwidth = bv.truncwidth - truncstr.chars().count() as i32;
if maxwidth < 0 {
bv.bp = ptr;
let mb = truncstr.as_bytes();
for &b in mb {
if bv.bp >= bv.buf.len() {
bv.buf.resize(bv.bp + 1, 0);
}
bv.buf[bv.bp] = b;
bv.bp += 1;
}
} else {
let region_chars: Vec<char> = region_str.chars().collect();
let len = region_chars.len() as i32;
let keep = maxwidth.max(0) as usize;
let kept: String = if truncatleft {
let drop_n = (len - keep as i32).max(0) as usize;
let suffix: String = region_chars[drop_n..].iter().collect();
format!("{}{}", truncstr, suffix)
} else {
let prefix: String = region_chars[..keep.min(region_chars.len())]
.iter()
.collect();
format!("{}{}", prefix, truncstr)
};
bv.bp = ptr;
for &b in kept.as_bytes() {
if bv.bp >= bv.buf.len() {
bv.buf.resize(bv.bp + 1, 0);
}
bv.buf[bv.bp] = b;
bv.bp += 1;
}
}
}
if bv.bp < bv.buf.len() {
bv.buf[bv.bp] = 0; }
bv.truncwidth = 0; } else {
let tchar = truncchar as u8;
let endchar_u8 = endchar as u8;
if bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0) != endchar_u8 {
bv.fm_pos += 1; }
while let Some(&c) = bv.fm.as_bytes().get(bv.fm_pos) {
if c == 0 || c == tchar {
break;
}
if c == b'\\' && bv.fm.as_bytes().get(bv.fm_pos + 1).is_some() {
bv.fm_pos += 1; }
bv.fm_pos += 1; }
if bv.truncwidth != 0
|| bv.fm.as_bytes().get(bv.fm_pos).copied().unwrap_or(0) == 0
{
return 0;
}
}
1 }
pub fn cmdpush(cmdtok: u8) {
CMDSTACK.with(|s| {
let mut st = s.borrow_mut();
if st.len() < CMDSTACKSZ {
st.push(cmdtok);
}
});
}
pub fn cmdpop() {
CMDSTACK.with(|s| {
let mut st = s.borrow_mut();
DPUTS!(st.is_empty(), "BUG: cmdstack empty"); st.pop();
});
}
#[allow(unused_variables)]
pub fn applytextattributes(flags: i32) -> String {
let mut current = current_attrs_lock().lock().expect("current_attrs poisoned");
let pending = pending_attrs_lock()
.lock()
.expect("pending_attrs poisoned")
.clone();
let mut result = String::new();
let old = *current;
let new = pending;
let old_b = old & TXTBOLDFACE != 0;
let new_b = new & TXTBOLDFACE != 0;
let old_u = old & TXTUNDERLINE != 0;
let new_u = new & TXTUNDERLINE != 0;
let old_s = old & TXTSTANDOUT != 0;
let new_s = new & TXTSTANDOUT != 0;
let bold_off = old_b && !new_b;
let underline_off = old_u && !new_u;
let standout_off = old_s && !new_s;
let attr_on = (!old_b && new_b) || (!old_u && new_u) || (!old_s && new_s);
let fg_emit_color = |attrs, out: &mut String| {
if attrs & TXTFGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT;
let c = if attrs & TXT_ATTR_FG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
out.push_str(&color_to_ansi(c, true));
}
};
let bg_emit_color = |attrs, out: &mut String| {
if attrs & TXTBGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_BG_COL_MASK) >> TXT_ATTR_BG_COL_SHIFT;
let c = if attrs & TXT_ATTR_BG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
out.push_str(&color_to_ansi(c, false));
}
};
if bold_off {
result.push_str("\x1b[0m");
if new_b {
result.push_str("\x1b[1m");
}
if new_u {
result.push_str("\x1b[4m");
}
if new_s {
result.push_str("\x1b[7m");
}
fg_emit_color(new, &mut result);
bg_emit_color(new, &mut result);
*current = pending;
return result;
}
if underline_off {
result.push_str("\x1b[24m");
}
if standout_off {
result.push_str("\x1b[27m");
}
if attr_on {
if !old_b && new_b {
result.push_str("\x1b[1m");
}
if !old_u && new_u {
result.push_str("\x1b[4m");
}
if !old_s && new_s {
result.push_str("\x1b[7m");
}
fg_emit_color(new, &mut result);
bg_emit_color(new, &mut result);
}
if (old & TXT_ATTR_FG_MASK) != (new & TXT_ATTR_FG_MASK) && !attr_on {
if new & TXTFGCOLOUR != 0 {
fg_emit_color(new, &mut result);
} else {
result.push_str("\x1b[39m");
}
}
if (old & TXT_ATTR_BG_MASK) != (new & TXT_ATTR_BG_MASK) && !attr_on {
if new & TXTBGCOLOUR != 0 {
bg_emit_color(new, &mut result);
} else {
result.push_str("\x1b[49m");
}
}
let diff = result;
*current = pending;
diff
}
pub fn treplaceattrs(newattrs: zattr) {
if newattrs == TXT_ERROR {
return; }
let unknown = txtunknownattrs.load(Ordering::SeqCst); if unknown != 0 {
let mut cur = current_attrs_lock().lock().expect("current_attrs poisoned");
*cur &= !unknown; *cur |= unknown & !newattrs; }
*pending_attrs_lock().lock().expect("pending_attrs poisoned") = newattrs; }
pub static txtunknownattrs: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub static memo_term_color: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
pub fn tsetattrs(newattrs: zattr) -> String {
let unknown = txtunknownattrs.load(Ordering::Relaxed);
{
let mut cur = current_attrs_lock().lock().expect("current_attrs poisoned");
*cur &= !(newattrs & unknown as zattr);
}
{
let mut pend = pending_attrs_lock().lock().expect("pending_attrs poisoned");
*pend |= newattrs & TXT_ATTR_ALL; if (newattrs & TXTFGCOLOUR) != 0 {
*pend &= !TXT_ATTR_FG_MASK; *pend |= newattrs & TXT_ATTR_FG_MASK; }
if (newattrs & TXTBGCOLOUR) != 0 {
*pend &= !TXT_ATTR_BG_MASK; *pend |= newattrs & TXT_ATTR_BG_MASK; }
}
apply_text_attributes(newattrs)
}
pub fn tunsetattrs(newattrs: zattr) -> String {
let unknown = txtunknownattrs.load(Ordering::Relaxed);
{
let mut cur = current_attrs_lock().lock().expect("current_attrs poisoned");
*cur |= newattrs & unknown as zattr;
}
{
let mut pend = pending_attrs_lock().lock().expect("pending_attrs poisoned");
*pend &= !(newattrs & TXT_ATTR_ALL);
if newattrs & TXTFGCOLOUR != 0 {
*pend &= !TXT_ATTR_FG_MASK;
}
if newattrs & TXTBGCOLOUR != 0 {
*pend &= !TXT_ATTR_BG_MASK;
}
}
String::new()
}
#[allow(non_snake_case)]
pub fn map256toRGB(atr: &mut u64, shift: u32, set24: u64) {
if (*atr & set24) != 0 {
return;
}
let colour: u32 = ((*atr >> shift) & 0xff) as u32;
if colour < 16 {
return;
}
let (red, green, blue) = if (16..232).contains(&colour) {
let mut c = colour - 16;
let blue = (if c != 0 { 0x37 } else { 0 }) + 40 * (c % 6);
c /= 6;
let green = (if c != 0 { 0x37 } else { 0 }) + 40 * (c % 6);
c /= 6;
let red = (if c != 0 { 0x37 } else { 0 }) + 40 * c;
(red, green, blue)
} else {
let v = 8 + 10 * (colour - 232);
(v, v, v)
};
*atr &= !((0xffffff_u64) << shift);
*atr |= set24 | ((((red as u64) << 8 | green as u64) << 8 | blue as u64) << shift);
}
pub fn mixattrs(primary: zattr, mask: zattr, secondary: zattr) -> zattr {
let mut out: zattr = 0;
for bit in [TXTBOLDFACE, TXTUNDERLINE, TXTSTANDOUT] {
if mask & bit != 0 {
out |= primary & bit;
} else {
out |= secondary & bit;
}
}
if mask & TXTFGCOLOUR != 0 {
out |= primary & TXT_ATTR_FG_MASK;
} else {
out |= secondary & TXT_ATTR_FG_MASK;
}
if mask & TXTBGCOLOUR != 0 {
out |= primary & TXT_ATTR_BG_MASK;
} else {
out |= secondary & TXT_ATTR_BG_MASK;
}
out
}
pub fn countprompt(s: &str, wp: &mut i32, hp: &mut i32, overf: i32) {
let mut zterm_columns = crate::ported::utils::adjustcolumns() as i32;
if zterm_columns <= 0 {
zterm_columns = 80;
}
let mut w: i32 = 0; let mut h: i32 = 1;
let multi = 0i32; let mut wcw: i32 = 0;
let mut visible = true;
for c in s.chars() {
while w > zterm_columns && overf >= 0 && multi == 0 {
h += 1; if wcw != 0 {
w = wcw; break; } else {
w -= zterm_columns; }
}
wcw = 0;
if c == Inpar {
visible = false; } else if c == Outpar {
visible = true; } else if c == Nularg {
w += 1; } else if visible {
if c == '\t' {
w = (w | 7) + 1; continue;
} else if c == '\n' {
w = 0; h += 1; continue; }
let cw = unicode_width::UnicodeWidthChar::width(c).unwrap_or(1) as i32;
wcw = cw; w += cw; }
}
if w == zterm_columns && overf == 0 && zterm_columns > 0 {
w = 0; h += 1; }
*wp = w; *hp = h; }
pub fn match_named_colour(teststrp: &str) -> Option<u8> {
for (i, &n) in COLOUR_NAMES.iter().enumerate() {
if n == teststrp {
return Some(i as u8); }
}
teststrp.parse::<u8>().ok()
}
pub fn truecolor_terminal() -> bool {
if let Some(flist) = crate::ported::params::getaparam(".term.extensions") {
for f in &flist {
if f.is_empty() {
continue;
}
let (result, name) = match f.strip_prefix('-') {
Some(rest) => (false, rest),
None => (true, f.as_str()),
};
if name == "truecolor" {
return result; }
}
}
false }
pub fn match_colour(cursor: Option<&mut usize>, spec: &str, is_fg: bool, colour: i32) -> zattr {
let (shft, on) = if is_fg {
(TXT_ATTR_FG_COL_SHIFT, TXTFGCOLOUR) } else {
(TXT_ATTR_BG_COL_SHIFT, TXTBGCOLOUR) };
let mut colour = colour;
if let Some(cursor) = cursor {
let pos = *cursor;
let rest = &spec[pos..];
if rest.starts_with('#')
&& rest
.as_bytes()
.get(1)
.map(|b| b.is_ascii_hexdigit())
.unwrap_or(false)
{
let mut end = 1usize;
while end < rest.len() && rest.as_bytes()[end].is_ascii_hexdigit() {
end += 1;
}
let hex_str = &rest[1..end];
let col = i64::from_str_radix(hex_str, 16).unwrap_or(-1);
if col < 0 {
return TXT_ERROR;
}
let (r, g, b) = match end {
4 => {
let r = ((col >> 8) | ((col >> 8) << 4)) as u8; let mut g = ((col & 0xf0) >> 4) as u8; g |= g << 4; let mut b = (col & 0xf) as u8; b |= b << 4; (r, g, b)
}
7 => {
let r = (col >> 16) as u8; let g = ((col & 0xff00) >> 8) as u8; let b = (col & 0xff) as u8; (r, g, b)
}
_ => return TXT_ERROR, };
*cursor += end;
let pixel = (((r as zattr) << 8) + g as zattr) << 8;
let pixel = pixel + b as zattr;
let bit24 = if is_fg {
TXT_ATTR_FG_24BIT
} else {
TXT_ATTR_BG_24BIT
};
return on | bit24 | (pixel << shft);
} else if rest
.as_bytes()
.first()
.map(|b| b.is_ascii_alphabetic())
.unwrap_or(false)
{
let end = rest
.find(|c: char| !c.is_ascii_alphabetic())
.unwrap_or(rest.len());
let name = &rest[..end];
match match_named_colour(name) {
Some(8) => {
*cursor += end;
return 0; }
Some(c) => {
*cursor += end;
colour = c as i32;
}
None => return TXT_ERROR, }
} else {
let end = rest
.find(|c: char| !c.is_ascii_digit())
.unwrap_or(rest.len());
let digits = &rest[..end];
match digits.parse::<i32>() {
Ok(n) if (0..256).contains(&n) => {
*cursor += end;
colour = n;
}
_ => return TXT_ERROR, }
}
}
on | ((colour as zattr) << shft) }
pub fn match_highlight(spec: &str) -> (zattr, zattr) {
let attrs = parsehighlight(spec);
let mut mask: zattr = 0;
mask |= attrs & (TXTBOLDFACE | TXTUNDERLINE | TXTSTANDOUT); if attrs & TXTFGCOLOUR != 0 {
mask |= TXTFGCOLOUR;
} if attrs & TXTBGCOLOUR != 0 {
mask |= TXTBGCOLOUR;
} (attrs, mask)
}
pub fn output_colour(colour: u8, is_fg: bool) -> String {
let base = if is_fg { 30 } else { 40 };
if colour < 8 {
format!("\x1b[{}m", base + colour)
} else if colour < 16 {
let bright_base = if is_fg { 90 } else { 100 };
format!("\x1b[{}m", bright_base + colour - 8)
} else {
let mode = if is_fg { 38 } else { 48 };
format!("\x1b[{};5;{}m", mode, colour)
}
}
pub fn output_highlight(attrs: zattr) -> String {
apply_text_attributes(attrs)
}
pub fn set_default_colour_sequences() -> (String, String) {
("\x1b[0m".to_string(), "\x1b[0m".to_string())
}
pub fn set_colour_code(spec: &str) -> Option<String> {
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), spec, true, 0);
if attr == TXT_ERROR {
return None;
}
let colour = ((attr & !TXTFGCOLOUR) >> TXT_ATTR_FG_COL_SHIFT) as u8;
Some(output_colour(colour, true))
}
#[derive(Default, Clone)]
pub struct colour_sequences {
pub start: String, pub end: String, pub def: String, }
pub static fg_bg_sequences: std::sync::Mutex<[colour_sequences; 2]> = std::sync::Mutex::new([
colour_sequences {
start: String::new(),
end: String::new(),
def: String::new(),
},
colour_sequences {
start: String::new(),
end: String::new(),
def: String::new(),
},
]);
pub static colseq_buf: std::sync::Mutex<Vec<u8>> = std::sync::Mutex::new(Vec::new());
pub static colseq_buf_allocs: std::sync::atomic::AtomicI32 = std::sync::atomic::AtomicI32::new(0);
pub fn allocate_colour_buffer() {
if colseq_buf_allocs.fetch_add(1, Ordering::SeqCst) != 0 {
return; }
let atrs: Option<Vec<String>> = {
let tab = paramtab().read().ok();
tab.and_then(|t| {
t.get("zle_highlight")
.map(|p| crate::ported::params::arrgetfn(p))
})
};
if let Some(atrs) = atrs {
let mut seqs = fg_bg_sequences.lock().unwrap();
for atr in &atrs {
if strpfx("fg_start_code:", atr) {
if let Some(c) = set_colour_code(&atr[14..]) {
seqs[COL_SEQ_FG as usize].start = c;
}
} else if strpfx("fg_default_code:", atr) {
if let Some(c) = set_colour_code(&atr[16..]) {
seqs[COL_SEQ_FG as usize].def = c;
}
} else if strpfx("fg_end_code:", atr) {
if let Some(c) = set_colour_code(&atr[12..]) {
seqs[COL_SEQ_FG as usize].end = c;
}
} else if strpfx("bg_start_code:", atr) {
if let Some(c) = set_colour_code(&atr[14..]) {
seqs[COL_SEQ_BG as usize].start = c;
}
} else if strpfx("bg_default_code:", atr) {
if let Some(c) = set_colour_code(&atr[16..]) {
seqs[COL_SEQ_BG as usize].def = c;
}
} else if strpfx("bg_end_code:", atr) {
if let Some(c) = set_colour_code(&atr[12..]) {
seqs[COL_SEQ_BG as usize].end = c;
}
}
}
}
let seqs = fg_bg_sequences.lock().unwrap();
let mut lenfg: usize = seqs[COL_SEQ_FG as usize].def.len(); if lenfg < 1 {
lenfg = 1;
} lenfg += seqs[COL_SEQ_FG as usize].start.len() + seqs[COL_SEQ_FG as usize].end.len();
let mut lenbg: usize = seqs[COL_SEQ_BG as usize].def.len(); if lenbg < 1 {
lenbg = 1;
} lenbg += seqs[COL_SEQ_BG as usize].start.len() + seqs[COL_SEQ_BG as usize].end.len(); drop(seqs);
let len = if lenfg > lenbg { lenfg } else { lenbg }; *colseq_buf.lock().unwrap() = vec![0u8; len + 15]; }
pub fn free_colour_buffer() {
if colseq_buf_allocs.fetch_sub(1, Ordering::SeqCst) - 1 != 0 {
return; }
colseq_buf.lock().unwrap().clear(); }
pub fn set_colour_attribute(color: Color, is_fg: bool) -> String {
color_to_ansi(color, is_fg)
}
pub static CMDNAMES: [&str; crate::ported::zsh_h::CS_COUNT as usize] = [
"for",
"while",
"repeat",
"select", "until",
"if",
"then",
"else", "elif",
"math",
"cond",
"cmdor", "cmdand",
"pipe",
"errpipe",
"foreach", "case",
"function",
"subsh",
"cursh", "array",
"quote",
"dquote",
"bquote", "cmdsubst",
"mathsubst",
"elif-then",
"heredoc", "heredocd",
"brace",
"braceparam",
"always", ];
pub type Color = u32; pub const COLOR_24BIT: Color = 0x0100_0000;
pub const COLOUR_DEFAULT: u8 = 8;
pub const COLOR_BLACK: Color = 0; pub const COLOR_RED: Color = 1; pub const COLOR_GREEN: Color = 2; pub const COLOR_YELLOW: Color = 3; pub const COLOR_BLUE: Color = 4; pub const COLOR_MAGENTA: Color = 5; pub const COLOR_CYAN: Color = 6; pub const COLOR_WHITE: Color = 7; pub const COLOR_DEFAULT: Color = COLOUR_DEFAULT as Color;
pub static COLOUR_NAMES: [&str; 9] = [
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "default", ];
fn color_rgb(r: u8, g: u8, b: u8) -> Color {
COLOR_24BIT | ((r as Color) << 16) | ((g as Color) << 8) | (b as Color)
}
fn color_get_rgb(c: Color) -> Option<(u8, u8, u8)> {
if c & COLOR_24BIT == 0 {
None
} else {
Some((
((c >> 16) & 0xff) as u8,
((c >> 8) & 0xff) as u8,
(c & 0xff) as u8,
))
}
}
fn color_to_ansi(c: Color, is_fg: bool) -> String {
if let Some((r, g, b)) = color_get_rgb(c) {
let lead = if is_fg { 38 } else { 48 };
format!("\x1b[{};2;{};{};{}m", lead, r, g, b)
} else {
output_colour(c as u8, is_fg)
}
}
fn color_from_name(name: &str) -> Option<Color> {
if let Some(rest) = name.strip_prefix('#') {
if rest.len() == 6 {
let r = u8::from_str_radix(&rest[0..2], 16).ok();
let g = u8::from_str_radix(&rest[2..4], 16).ok();
let b = u8::from_str_radix(&rest[4..6], 16).ok();
match (r, g, b) {
(Some(r), Some(g), Some(b)) => Some(color_rgb(r, g, b) as Color),
_ => None,
}
} else {
match_named_colour(name).map(|idx| idx as Color)
}
} else {
match_named_colour(name).map(|idx| idx as Color)
}
}
fn zattr_set_fg_palette(attrs: zattr, idx: u8) -> zattr {
let cleared = attrs & !TXT_ATTR_FG_MASK;
cleared | TXTFGCOLOUR | ((idx as zattr) << TXT_ATTR_FG_COL_SHIFT)
}
fn zattr_fg_palette(attrs: zattr) -> Option<u8> {
if (attrs & TXTFGCOLOUR) == 0 || (attrs & TXT_ATTR_FG_24BIT) != 0 {
return None;
}
Some(((attrs >> TXT_ATTR_FG_COL_SHIFT) & 0xff) as u8)
}
fn zattr_set_fg_rgb(attrs: zattr, r: u8, g: u8, b: u8) -> zattr {
let cleared = attrs & !TXT_ATTR_FG_MASK;
let rgb = ((r as zattr) << 16) | ((g as zattr) << 8) | (b as zattr);
cleared | TXTFGCOLOUR | TXT_ATTR_FG_24BIT | (rgb << TXT_ATTR_FG_COL_SHIFT)
}
fn zattr_set_bg_palette(attrs: zattr, idx: u8) -> zattr {
let cleared = attrs & !TXT_ATTR_BG_MASK;
cleared | TXTBGCOLOUR | ((idx as zattr) << TXT_ATTR_BG_COL_SHIFT)
}
fn zattr_set_bg_rgb(attrs: zattr, r: u8, g: u8, b: u8) -> zattr {
let cleared = attrs & !TXT_ATTR_BG_MASK;
let rgb = ((r as zattr) << 16) | ((g as zattr) << 8) | (b as zattr);
cleared | TXTBGCOLOUR | TXT_ATTR_BG_24BIT | (rgb << TXT_ATTR_BG_COL_SHIFT)
}
pub fn expand_prompt(s: &str) -> String {
let s_owned: String;
let s = if crate::ported::zsh_h::isset(crate::ported::zsh_h::PROMPTSUBST) {
let saved_errflag = crate::ported::utils::errflag
.load(std::sync::atomic::Ordering::Relaxed);
let saved_lastval = crate::ported::builtin::LASTVAL
.load(std::sync::atomic::Ordering::Relaxed);
s_owned = crate::ported::subst::singsub(s);
let cur = crate::ported::utils::errflag
.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::utils::errflag.store(
saved_errflag | (cur & crate::ported::zsh_h::ERRFLAG_INT),
std::sync::atomic::Ordering::Relaxed,
);
crate::ported::builtin::LASTVAL
.store(saved_lastval, std::sync::atomic::Ordering::Relaxed);
s_owned.as_str()
} else {
s
};
prompt_tls::sync_from_globals();
crate::ported::utils::inittyptab();
*current_attrs_lock().lock().expect("current_attrs poisoned") = 0;
*pending_attrs_lock().lock().expect("pending_attrs poisoned") = 0;
let mut bv = buf_vars {
buf: vec![0u8; 256],
bufspc: 256,
bp: 0,
bufline: 0,
bp1: None,
fm: s.to_string(),
fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: 0,
in_escape: false,
};
putpromptchar(&mut bv, 1, 0); let end = bv.bp.min(bv.buf.len());
let mut raw = bv.buf[..end].to_vec();
crate::ported::utils::unmetafy(&mut raw);
let translated: Vec<u8> = raw
.into_iter()
.filter_map(|b| match b {
x if x == Inpar as u8 => Some(0x01),
x if x == Outpar as u8 => Some(0x02),
x if x == Nularg as u8 => None,
other => Some(other),
})
.collect();
String::from_utf8_lossy(&translated).into_owned()
}
pub fn expand_prompt_default(s: &str) -> String {
expand_prompt(s)
}
pub fn prompt_width(s: &str) -> usize {
let mut width = 0;
let mut in_escape = false;
let mut chars = s.chars().peekable();
while let Some(c) = chars.next() {
match c {
'\x01' => in_escape = true, '\x02' => in_escape = false, '\x1b' => {
while let Some(&next) = chars.peek() {
chars.next();
if next == 'm' {
break;
}
}
}
_ if !in_escape => {
width += unicode_width::UnicodeWidthChar::width(c).unwrap_or(1);
}
_ => {}
}
}
width
}
pub fn output_truecolor(r: u8, g: u8, b: u8, is_fg: bool) -> String {
let mode = if is_fg { 38 } else { 48 };
format!("\x1b[{};2;{};{};{}m", mode, r, g, b)
}
const CMDSTACKSZ: usize = 256;
thread_local! {
pub static CMDSTACK: std::cell::RefCell<Vec<u8>> = const { std::cell::RefCell::new(Vec::new())
};
}
pub fn apply_text_attributes(attrs: zattr) -> String {
let mut codes: Vec<String> = Vec::new();
if attrs & TXTBOLDFACE != 0 {
codes.push("1".to_string());
} if attrs & TXTUNDERLINE != 0 {
codes.push("4".to_string());
} if attrs & TXTSTANDOUT != 0 {
codes.push("7".to_string());
} if attrs & TXTFGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT;
let c = if attrs & TXT_ATTR_FG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
codes.push(
color_to_ansi(c, true)
.trim_start_matches("\x1b[")
.trim_end_matches('m')
.to_string(),
);
}
if attrs & TXTBGCOLOUR != 0 {
let raw = (attrs & TXT_ATTR_BG_COL_MASK) >> TXT_ATTR_BG_COL_SHIFT;
let c = if attrs & TXT_ATTR_BG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
codes.push(
color_to_ansi(c, false)
.trim_start_matches("\x1b[")
.trim_end_matches('m')
.to_string(),
);
}
if codes.is_empty() {
String::new()
} else {
format!("\x1b[{}m", codes.join(";"))
}
}
pub fn reset_text_attributes() -> &'static str {
"\x1b[0m"
}
pub fn right_prompt_padding(
left_width: usize,
right_prompt: &str,
term_width: usize,
indent: usize,
) -> Option<String> {
let right_width = prompt_width(right_prompt);
let total = left_width + right_width + indent;
if total >= term_width {
return None; }
let padding = term_width - total;
Some(" ".repeat(padding))
}
pub fn transient_prompt(_original: &str) -> String {
String::new()
}
fn color_name(c: Color) -> String {
if let Some((r, g, b)) = color_get_rgb(c) {
return format!("#{:02x}{:02x}{:02x}", r, g, b);
}
let idx = (c & 0xff) as usize;
if idx < COLOUR_NAMES.len() {
return COLOUR_NAMES[idx].to_string();
}
idx.to_string()
}
fn current_attrs_lock() -> &'static std::sync::Mutex<zattr> {
static CUR: std::sync::OnceLock<std::sync::Mutex<zattr>> = std::sync::OnceLock::new();
CUR.get_or_init(|| std::sync::Mutex::new(0 as zattr))
}
fn pending_attrs_lock() -> &'static std::sync::Mutex<zattr> {
static PND: std::sync::OnceLock<std::sync::Mutex<zattr>> = std::sync::OnceLock::new();
PND.get_or_init(|| std::sync::Mutex::new(0 as zattr))
}
pub fn set_pending_text_attrs(attrs: zattr) {
*pending_attrs_lock().lock().expect("pending_attrs poisoned") = attrs;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn truecolor_terminal_routes_through_term_extensions_array() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::params::getaparam(".term.extensions");
let _ = setaparam(".term.extensions", vec![]);
assert!(
!truecolor_terminal(),
"empty .term.extensions must report off"
);
let _ = setaparam(".term.extensions", vec!["truecolor".to_string()]);
assert!(
truecolor_terminal(),
".term.extensions=(truecolor) must report on"
);
let _ = setaparam(".term.extensions", vec!["-truecolor".to_string()]);
assert!(
!truecolor_terminal(),
".term.extensions=(-truecolor) must report off"
);
let _ = setaparam(".term.extensions", saved.unwrap_or_default());
}
#[test]
fn promptpath_substitutes_home_prefix_with_tilde() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/home/user/project", 0, true, "/home/user");
assert!(
r.starts_with('~'),
"home-prefix must collapse to ~ (got {r:?})"
);
}
#[test]
fn promptpath_npath_one_keeps_only_last_component() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/a/b/c/d", 1, false, "");
assert!(!r.contains("a/b") && r.ends_with("d"), "got {r:?}");
}
#[test]
fn parsehighlight_bold_sets_bold_bit() {
let _g = crate::test_util::global_state_lock();
assert_ne!(parsehighlight("bold") & TXTBOLDFACE, 0);
}
#[test]
fn match_named_colour_covers_full_ansi_palette_and_default() {
let _g = crate::test_util::global_state_lock();
for &name in &[
"black", "red", "green", "yellow", "blue", "magenta", "cyan", "white", "default",
] {
assert!(match_named_colour(name).is_some(), "{name:?} must resolve");
}
}
#[test]
fn match_colour_named_red_fg() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "red", true, 0);
assert_ne!(attr, TXT_ERROR);
assert_eq!(attr & TXTFGCOLOUR, TXTFGCOLOUR);
let idx = (attr >> TXT_ATTR_FG_COL_SHIFT) & 0xff;
assert_eq!(idx, 1, "red index 1");
assert_eq!(cur, 3, "consumed exactly 'red'");
}
#[test]
fn match_colour_named_default_is_zero() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "default", true, 0);
assert_eq!(attr, 0);
}
#[test]
fn match_colour_named_default_advances_cursor() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "default", true, 0);
assert_eq!(attr, 0);
assert_eq!(
cur, 7,
"c:2001 — match_named_colour advances teststrp past 'default'; \
return-zero branch must NOT skip the advance"
);
}
#[test]
fn match_colour_numeric() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "12", false, 0);
assert_ne!(attr, TXT_ERROR);
assert_eq!(attr & TXTBGCOLOUR, TXTBGCOLOUR);
let idx = (attr >> TXT_ATTR_BG_COL_SHIFT) & 0xff;
assert_eq!(idx, 12);
}
#[test]
fn match_colour_numeric_out_of_range_errors() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
assert_eq!(match_colour(Some(&mut cur), "500", true, 0), TXT_ERROR);
}
#[test]
fn match_colour_truecolor_six_digit() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "#ff8040", true, 0);
assert_ne!(attr, TXT_ERROR);
assert_eq!(attr & TXTFGCOLOUR, TXTFGCOLOUR);
assert_eq!(attr & TXT_ATTR_FG_24BIT, TXT_ATTR_FG_24BIT);
let pixel = (attr >> TXT_ATTR_FG_COL_SHIFT) & 0xffffff;
assert_eq!(pixel, 0xff8040);
assert_eq!(cur, 7);
}
#[test]
fn match_colour_truecolor_three_digit_expands() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "#f8a", true, 0);
assert_ne!(attr, TXT_ERROR);
let pixel = (attr >> TXT_ATTR_FG_COL_SHIFT) & 0xffffff;
assert_eq!(pixel, 0xff_88_aa, "got pixel 0x{:06x}", pixel);
}
#[test]
fn match_colour_hash_without_hex_errors() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "#x", true, 0);
assert_eq!(attr, TXT_ERROR);
}
#[test]
fn match_colour_does_not_advance_cursor_on_error() {
let _g = crate::test_util::global_state_lock();
let mut cur = 0usize;
let attr = match_colour(Some(&mut cur), "500extra", true, 0);
assert_eq!(attr, TXT_ERROR);
assert_eq!(cur, 0, "cursor must stay at 0 on TXT_ERROR; got {}", cur);
}
#[test]
fn match_named_colour_returns_none_for_unknown() {
let _g = crate::test_util::global_state_lock();
assert!(match_named_colour("definitely_not_a_color_zshrs").is_none());
}
#[test]
fn countprompt_recognises_canonical_inpar_outpar_nularg_bytes() {
let _g = crate::test_util::global_state_lock();
let mut w = 0i32;
let mut h = 0i32;
let probe = format!("abc{}ESC{}def", Inpar, Outpar);
countprompt(&probe, &mut w, &mut h, 0);
assert_eq!(
w, 6,
"c:1179-1182 — Inpar..Outpar region must be zero-width; \
got w={w} for 3+0+3-col prompt"
);
let mut w = 0i32;
let mut h = 0i32;
let probe = format!("{}", Nularg);
countprompt(&probe, &mut w, &mut h, 0);
assert_eq!(
w, 1,
"c:1183-1184 — Nularg counts as 1 visible column; got w={w}"
);
}
#[test]
fn promptpath_without_tilde_keeps_absolute_path() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/home/user/project", 0, false, "/home/user");
assert!(
r.starts_with("/home/user"),
"tilde=false must NOT collapse to ~; got {r:?}"
);
assert!(
!r.starts_with('~'),
"tilde=false output must not start with ~"
);
}
#[test]
fn promptpath_path_exactly_home_renders_as_tilde_only() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/home/user", 0, true, "/home/user");
assert_eq!(r, "~", "path == home must render as plain '~'; got {r:?}");
}
#[test]
fn promptpath_unrelated_path_unchanged() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/etc/zshrc", 0, true, "/home/user");
assert_eq!(r, "/etc/zshrc", "non-home path must pass through unchanged");
}
#[test]
fn promptpath_npath_zero_means_no_truncation() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/a/b/c/d/e", 0, false, "");
assert_eq!(r, "/a/b/c/d/e", "npath=0 must keep full path");
}
#[test]
fn promptpath_npath_two_keeps_last_two_components() {
let _g = crate::test_util::global_state_lock();
let r = promptpath("/a/b/c/d", 2, false, "");
assert!(
r.contains("c") && r.contains("d"),
"npath=2 must include last 2 components; got {r:?}"
);
assert!(
!r.contains("/a/"),
"npath=2 must NOT include first 2 components"
);
}
#[test]
fn parsehighlight_none_returns_zero() {
let _g = crate::test_util::global_state_lock();
let r = parsehighlight("none");
assert_eq!(r, 0, "'none' must yield zero attrs; got {:#x}", r);
}
#[test]
fn parsehighlight_underline_sets_underline_bit() {
let _g = crate::test_util::global_state_lock();
let r = parsehighlight("underline");
assert_ne!(r, 0, "underline must set at least one bit");
}
#[test]
fn parsehighlight_unknown_keyword_returns_zero() {
let _g = crate::test_util::global_state_lock();
let r = parsehighlight("definitely_not_a_real_attr");
assert_eq!(r, 0, "unknown attr must be silently ignored");
}
#[test]
fn match_named_colour_is_case_sensitive() {
let _g = crate::test_util::global_state_lock();
assert!(match_named_colour("red").is_some());
assert!(
match_named_colour("RED").is_none(),
"uppercase color must NOT resolve per C source's strcmp"
);
assert!(match_named_colour("Red").is_none());
}
#[test]
fn match_named_colour_empty_returns_none() {
let _g = crate::test_util::global_state_lock();
assert!(match_named_colour("").is_none());
}
#[test]
fn cmdpush_cmdpop_round_trip_does_not_panic() {
let _g = crate::test_util::global_state_lock();
cmdpush(0);
cmdpush(1);
cmdpush(2);
cmdpop();
cmdpop();
cmdpop();
cmdpop();
}
#[test]
fn pputc_writes_one_byte_advancing_bp() {
let _g = crate::test_util::global_state_lock();
let mut bv = buf_vars {
buf: vec![0u8; 16],
bufspc: 16,
bp: 0,
bufline: 0,
bp1: None,
fm: String::new(),
fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: 0,
in_escape: false,
};
pputc(&mut bv, b'X');
assert_eq!(&bv.buf[..bv.bp], b"X");
pputc(&mut bv, b'Y');
assert_eq!(&bv.buf[..bv.bp], b"XY");
}
#[test]
fn stradd_ascii_passes_through_to_bp() {
let _g = crate::test_util::global_state_lock();
let mut bv = buf_vars {
buf: vec![0u8; 16],
bufspc: 16,
bp: 0,
bufline: 0,
bp1: None,
fm: String::new(),
fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: 0,
in_escape: false,
};
stradd(&mut bv, "hello");
assert_eq!(&bv.buf[..bv.bp], b"hello");
}
#[test]
fn tsetattrs_updates_pending_attrs_non_color() {
let _g = crate::test_util::global_state_lock();
set_pending_text_attrs(0);
let _ = tsetattrs(TXTBOLDFACE);
let p = *pending_attrs_lock().lock().unwrap();
assert_ne!(p & TXTBOLDFACE, 0, "TXTBOLDFACE bit ORed into pending");
}
#[test]
fn tsetattrs_fg_color_replaces_fg_mask() {
let _g = crate::test_util::global_state_lock();
let palette_idx_5: zattr = (5u64 << TXT_ATTR_FG_COL_SHIFT) & TXT_ATTR_FG_COL_MASK;
let palette_idx_2: zattr = (2u64 << TXT_ATTR_FG_COL_SHIFT) & TXT_ATTR_FG_COL_MASK;
set_pending_text_attrs(TXTFGCOLOUR | palette_idx_5);
let _ = tsetattrs(TXTFGCOLOUR | palette_idx_2);
let p = *pending_attrs_lock().lock().unwrap();
assert_eq!(
p & TXT_ATTR_FG_COL_MASK,
palette_idx_2,
"FG palette index replaced (idx=2), not ORed with prior idx=5"
);
}
fn expand(s: &str) -> String {
let _g = crate::test_util::global_state_lock();
expand_prompt(s)
}
#[test]
fn promptexpand_plain_text_passes_through() {
assert_eq!(expand("literal"), "literal");
}
#[test]
fn promptexpand_empty_input_returns_empty() {
assert_eq!(expand(""), "");
}
#[test]
fn promptexpand_double_percent_yields_single_percent() {
assert_eq!(expand("%%"), "%");
}
#[test]
fn promptexpand_percent_in_middle() {
assert_eq!(expand("pre%%post"), "pre%post");
}
#[test]
fn promptexpand_repeated_percent_escapes() {
assert_eq!(expand("a%%b%%c"), "a%b%c");
}
#[test]
fn promptexpand_capital_B_emits_sgr_bold_with_ignore_markers() {
assert_eq!(expand("%B"), "\x01\x1b[1m\x02");
}
#[test]
fn promptexpand_lowercase_b_alone_no_reset_emitted() {
assert_eq!(expand("%b"), "");
}
#[test]
fn promptexpand_capital_U_emits_sgr_underline_with_ignore_markers() {
assert_eq!(expand("%U"), "\x01\x1b[4m\x02");
}
#[test]
fn promptexpand_capital_S_emits_some_sgr_with_ignore_markers() {
let out = expand("%S");
assert!(
out.starts_with('\x01') && out.ends_with('\x02'),
"%S must be wrapped in ignore markers; got {out:?}"
);
assert!(out.contains("\x1b["), "%S must contain an SGR escape");
assert!(out.ends_with("m\x02"), "%S must end with SGR `m`+marker");
}
#[test]
fn promptexpand_F_red_emits_sgr_fg_red_with_ignore_markers() {
assert_eq!(expand("%F{red}"), "\x01\x1b[31m\x02");
}
#[test]
fn promptexpand_lowercase_f_alone_no_reset_emitted() {
assert_eq!(expand("%f"), "\x01\x1b[39m\x02");
}
#[test]
fn promptexpand_K_blue_emits_sgr_bg_blue_with_ignore_markers() {
assert_eq!(expand("%K{blue}"), "\x01\x1b[44m\x02");
}
#[test]
fn promptexpand_lowercase_k_alone_no_reset_emitted() {
assert_eq!(expand("%k"), "\x01\x1b[49m\x02");
}
#[test]
fn promptexpand_literal_braces_wrap_content_in_ignore_markers() {
assert_eq!(expand("%{ABCD%}"), "\x01ABCD\x02");
}
#[test]
fn promptexpand_literal_braces_followed_by_plain_text() {
assert_eq!(expand("%{ABCD%}xyz"), "\x01ABCD\x02xyz");
}
#[test]
fn promptexpand_hash_yields_percent_for_non_root() {
let out = expand("%#");
assert!(
out == "%" || out == "#",
"%# must produce '%' (non-root) or '#' (root); got {out:?}"
);
}
#[test]
fn promptexpand_color_frames_text() {
assert_eq!(
expand("%F{green}HI%f"),
"\x01\x1b[32m\x02HI\x01\x1b[39m\x02"
);
}
#[test]
fn promptexpand_F_numeric_color_index() {
assert_eq!(expand("%F{1}"), "\x01\x1b[31m\x02");
}
#[test]
fn promptexpand_text_around_escape_unchanged() {
let out = expand("before%Bmid%bafter");
assert!(
out.starts_with("before") && out.ends_with("after"),
"text framing must be preserved; got {out:?}"
);
}
#[test]
fn promptexpand_unknown_escape_does_not_panic() {
let _ = expand("%Q");
}
#[test]
fn promptexpand_corpus_percent_percent_yields_literal() {
let out = expand("%%");
assert_eq!(out, "%", "%% → literal %");
}
#[test]
fn promptexpand_corpus_percent_n_yields_nonempty_username() {
let out = expand("%n");
assert!(!out.is_empty(), "%n must produce a username");
assert!(!out.contains('%'), "%n must not leave a literal %");
}
#[test]
fn promptexpand_corpus_ternary_question_zero_branch() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(0, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%(?.OK.FAIL)");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "OK", "default $?=0 chooses OK branch");
}
#[test]
fn promptexpand_corpus_underline_emits_sgr() {
let out = expand("%Utext%u");
assert!(
out.contains("\x1b[4m") || out.contains("\x1b[04m"),
"%U should emit SGR underline-on, got {out:?}",
);
assert!(
out.contains("\x1b[24m") || out.contains("\x1b[0m"),
"%u should emit SGR underline-off / reset, got {out:?}",
);
}
#[test]
fn promptexpand_corpus_standout_emits_sgr() {
let out = expand("%Stext%s");
assert!(
out.contains("\x1b[3m")
|| out.contains("\x1b[03m")
|| out.contains("\x1b[7m")
|| out.contains("\x1b[07m"),
"%S should emit terminfo `smso` SGR (italic or reverse), got {out:?}",
);
}
#[test]
fn promptexpand_corpus_zero_width_brackets_preserve_text() {
let out = expand("%{ESC%}abc");
assert!(out.contains("ESC"), "literal content survives %{{...%}}");
assert!(out.ends_with("abc"), "trailing text after %{{...%}}");
}
#[test]
fn promptexpand_corpus_plain_text_with_escapes_preserves_letters() {
let out = expand("abc%Bdef%bxyz");
let plain: String = out
.chars()
.filter(|c| !c.is_ascii_control() && *c != '[')
.collect();
assert!(plain.contains("abc"), "starts with abc, got {out:?}");
assert!(plain.contains("def"), "middle has def, got {out:?}");
assert!(plain.contains("xyz"), "ends with xyz, got {out:?}");
}
#[test]
fn promptexpand_corpus_pwd_escape_yields_path() {
let out = expand("%d");
assert!(!out.is_empty(), "%d must produce a path");
assert!(
out.starts_with('/') || out.starts_with('~') || out.contains(std::path::MAIN_SEPARATOR),
"%d should look path-like, got {out:?}",
);
}
#[test]
fn putstr_low_byte_appends_and_returns_zero() {
PUTSTR_BUF.with(|b| b.borrow_mut().clear());
let r = putstr(b'A' as i32);
assert_eq!(r, 0, "tputs callback contract: always returns 0");
let buf = PUTSTR_BUF.with(|b| b.borrow().clone());
assert_eq!(buf, vec![b'A']);
}
#[test]
fn putstr_high_byte_gets_metafied() {
PUTSTR_BUF.with(|b| b.borrow_mut().clear());
let _ = putstr(0x84); let buf = PUTSTR_BUF.with(|b| b.borrow().clone());
assert_eq!(buf, vec![Meta, 0x84 ^ 0x20], "high byte metafied");
}
#[test]
fn putstr_successive_calls_accumulate_in_order() {
PUTSTR_BUF.with(|b| b.borrow_mut().clear());
for c in b"hi!" {
let _ = putstr(*c as i32);
}
let buf = PUTSTR_BUF.with(|b| b.borrow().clone());
assert_eq!(buf, b"hi!".to_vec());
}
#[test]
fn putpromptchar_pwd_tilde_substitutes_home() {
let _g = crate::test_util::global_state_lock();
let saved_home = std::env::var("HOME").ok();
let saved_pwd = std::env::var("PWD").ok();
unsafe {
std::env::set_var("HOME", "/home/user");
std::env::set_var("PWD", "/home/user/work");
}
crate::ported::params::setsparam("HOME", "/home/user");
crate::ported::params::setsparam("PWD", "/home/user/work");
let out = expand_prompt("%~");
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", &h); }
crate::ported::params::setsparam("HOME", &h);
}
if let Some(p) = saved_pwd {
unsafe { std::env::set_var("PWD", &p); }
crate::ported::params::setsparam("PWD", &p);
}
assert_eq!(out, "~/work");
}
#[test]
fn putpromptchar_d_emits_raw_pwd() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("PWD").ok();
unsafe { std::env::set_var("PWD", "/tmp/x"); }
crate::ported::params::setsparam("PWD", "/tmp/x");
let out = expand_prompt("%d");
if let Some(p) = saved {
unsafe { std::env::set_var("PWD", &p); }
crate::ported::params::setsparam("PWD", &p);
}
assert_eq!(out, "/tmp/x");
}
#[test]
fn putpromptchar_slash_equals_d() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("PWD").ok();
unsafe {
std::env::set_var("PWD", "/a/b/c");
}
let a = expand_prompt("%/");
let b = expand_prompt("%d");
if let Some(p) = saved {
unsafe {
std::env::set_var("PWD", p);
}
}
assert_eq!(a, b);
}
#[test]
fn putpromptchar_c_emits_trailing_component_with_tilde() {
let _g = crate::test_util::global_state_lock();
let saved_home = std::env::var("HOME").ok();
let saved_pwd = std::env::var("PWD").ok();
unsafe {
std::env::set_var("HOME", "/home/u");
std::env::set_var("PWD", "/home/u/proj/src");
}
crate::ported::params::setsparam("HOME", "/home/u");
crate::ported::params::setsparam("PWD", "/home/u/proj/src");
let out = expand_prompt("%c");
if let Some(h) = saved_home {
unsafe { std::env::set_var("HOME", &h); }
crate::ported::params::setsparam("HOME", &h);
}
if let Some(p) = saved_pwd {
unsafe { std::env::set_var("PWD", &p); }
crate::ported::params::setsparam("PWD", &p);
}
assert_eq!(out, "src");
}
#[test]
fn putpromptchar_2c_emits_two_trailing_components() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("PWD").ok();
unsafe { std::env::set_var("PWD", "/a/b/c/d"); }
crate::ported::params::setsparam("PWD", "/a/b/c/d");
let out = expand_prompt("%2c");
if let Some(p) = saved {
unsafe { std::env::set_var("PWD", &p); }
crate::ported::params::setsparam("PWD", &p);
}
assert_eq!(out, "c/d");
}
#[test]
fn putpromptchar_n_emits_username() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("USER").ok();
unsafe {
std::env::set_var("USER", "alice");
}
crate::ported::params::setsparam("USER", "alice");
let out = expand_prompt("%n");
if let Some(u) = saved {
unsafe {
std::env::set_var("USER", &u);
}
crate::ported::params::setsparam("USER", &u);
} else {
crate::ported::params::unsetparam("USER");
}
assert_eq!(out, "alice");
}
#[test]
fn putpromptchar_M_emits_non_empty_hostname() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%M");
assert!(!out.is_empty(), "%M should emit a hostname; got empty");
}
#[test]
fn putpromptchar_question_emits_lastval() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(42, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%?");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "42");
}
#[test]
fn putpromptchar_hash_emits_percent_for_non_root() {
let _g = crate::test_util::global_state_lock();
let euid = unsafe { libc::geteuid() };
if euid == 0 {
return; }
assert_eq!(expand_prompt("%#"), "%");
}
#[test]
fn putpromptchar_standout_on_off_round_trip() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%S%s");
assert!(
out.starts_with('\x01'),
"expected start marker, got {out:?}"
);
assert!(out.contains("\x1b["), "expected SGR escape");
}
#[test]
fn putpromptchar_bold_on_off_emits_both_diffs() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%BX%b");
assert_eq!(out, "\x01\x1b[1m\x02X\x01\x1b[0m\x02");
}
#[test]
fn putpromptchar_color_brackets_text() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%F{green}HI%f");
assert_eq!(out, "\x01\x1b[32m\x02HI\x01\x1b[39m\x02");
}
#[test]
fn putpromptchar_fg_then_bg_emits_both_colors() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%F{red}%K{blue}");
assert!(out.contains("\x1b[31m"), "expected red fg in {out:?}");
assert!(out.contains("\x1b[44m"), "expected blue bg in {out:?}");
}
#[test]
fn putpromptchar_braces_wrap_content_in_ignore_markers() {
let _g = crate::test_util::global_state_lock();
assert_eq!(expand_prompt("%{xyz%}"), "\x01xyz\x02");
}
#[test]
fn putpromptchar_ternary_question_zero_chooses_true_branch() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(0, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%(?.OK.FAIL)");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "OK");
}
#[test]
fn putpromptchar_ternary_question_nonzero_chooses_false_branch() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%(?.OK.FAIL)");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "FAIL");
}
#[test]
fn putpromptchar_ternary_question_with_arg_matches_lastval() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(1, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%(1?.OK.FAIL)");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "OK");
}
#[test]
fn putpromptchar_ternary_hash_non_root_chooses_false_branch() {
let _g = crate::test_util::global_state_lock();
let euid = unsafe { libc::geteuid() };
if euid == 0 {
return;
}
assert_eq!(expand_prompt("%(#.root.user)"), "user");
}
#[test]
fn putpromptchar_plain_text_between_escapes_preserved() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("USER").ok();
unsafe { std::env::set_var("USER", "bob"); }
crate::ported::params::setsparam("USER", "bob");
let out = expand_prompt("user=%n done");
if let Some(u) = saved {
unsafe { std::env::set_var("USER", &u); }
crate::ported::params::setsparam("USER", &u);
}
assert_eq!(out, "user=bob done");
}
#[test]
fn putpromptchar_double_percent_yields_one_percent() {
let _g = crate::test_util::global_state_lock();
assert_eq!(expand_prompt("a%%b"), "a%b");
}
#[test]
fn putpromptchar_unknown_escape_emits_literal_pair() {
let _g = crate::test_util::global_state_lock();
assert_eq!(expand_prompt("%Z"), "");
}
#[test]
fn putpromptchar_ternary_false_branch_doprint_zero_suppresses_chars() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::builtin::LASTVAL.load(std::sync::atomic::Ordering::Relaxed);
crate::ported::builtin::LASTVAL.store(0, std::sync::atomic::Ordering::Relaxed);
let out = expand_prompt("%(?.a.bbb)");
crate::ported::builtin::LASTVAL.store(saved, std::sync::atomic::Ordering::Relaxed);
assert_eq!(out, "a", "false-branch 'bbb' must NOT leak when doprint=0");
}
#[test]
fn putpromptchar_h_emits_curhist() {
let _g = crate::test_util::global_state_lock();
let saved = crate::ported::hist::curhist.load(std::sync::atomic::Ordering::SeqCst);
crate::ported::hist::curhist.store(42, std::sync::atomic::Ordering::SeqCst);
let out = expand_prompt("%h");
crate::ported::hist::curhist.store(saved, std::sync::atomic::Ordering::SeqCst);
assert!(
out.chars().all(|c| c.is_ascii_digit()),
"%h should emit digits from curhist; got {out:?}"
);
assert!(
!out.is_empty(),
"%h should not be empty when history exists"
);
}
#[test]
fn putpromptchar_j_emits_job_count() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%j");
assert!(
out.parse::<i32>().is_ok(),
"%j should be a decimal integer; got {out:?}"
);
}
#[test]
fn putpromptchar_ternary_c_test_dir_depth_match() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("PWD").ok();
unsafe {
std::env::set_var("PWD", "/a/b/c");
}
let out = expand_prompt("%(2c.deep.shallow)");
if let Some(p) = saved {
unsafe {
std::env::set_var("PWD", p);
}
}
assert_eq!(out, "deep", "depth 3 >= arg 2 → true branch");
}
#[test]
fn putpromptchar_ternary_L_shlvl_match() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("SHLVL").ok();
unsafe {
std::env::set_var("SHLVL", "3");
}
let out = expand_prompt("%(2L.nested.top)");
if let Some(s) = saved {
unsafe {
std::env::set_var("SHLVL", s);
}
}
assert_eq!(out, "nested");
}
#[test]
fn putpromptchar_truncation_bracket_truncates_to_width() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%5[a]bcdef");
assert!(out.len() <= 5, "%5[a] should truncate to width 5, got {out:?}");
}
#[test]
fn putpromptchar_T_emits_HH_MM_time() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%T");
let bytes = out.as_bytes();
assert!(
bytes.len() >= 4 && bytes.contains(&b':'),
"%T should be HH:MM format; got {out:?}"
);
}
#[test]
fn putpromptchar_D_braced_format_yields_strftime() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%D{%Y}");
let y: i32 = out.parse().unwrap_or(0);
assert!(
y >= 2024,
"%D{{%Y}} should emit a 4-digit year; got {out:?}"
);
}
#[test]
fn parsecolorchar_signature_matches_c() {
use crate::ported::zsh_h::{TXTFGCOLOUR, TXT_ATTR_FG_COL_MASK, TXT_ATTR_FG_COL_SHIFT};
let mut bv = buf_vars {
buf: vec![0u8; 16],
bufspc: 16,
bp: 0,
bufline: 0,
bp1: None,
fm: "F".to_string(), fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: 0,
in_escape: false,
};
let zattr_out = super::parsecolorchar(&mut bv, 1, true);
assert_eq!(zattr_out & TXTFGCOLOUR, TXTFGCOLOUR);
assert_eq!(
(zattr_out & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT,
1
);
}
#[test]
fn match_colour_signature_documented_split() {
let z = match_colour(None, "", true, 5);
assert!(z & TXTFGCOLOUR != 0, "fg color bit should be set");
}
#[test]
fn putpromptchar_uppercase_C_trailing_no_tilde() {
let _g = crate::test_util::global_state_lock();
let saved = std::env::var("PWD").ok();
unsafe { std::env::set_var("PWD", "/a/b/c"); }
crate::ported::params::setsparam("PWD", "/a/b/c");
let out = expand_prompt("%C");
if let Some(p) = saved {
unsafe { std::env::set_var("PWD", &p); }
crate::ported::params::setsparam("PWD", &p);
}
assert_eq!(out, "c", "%C with default arg=1 → last component");
}
#[test]
fn putpromptchar_N_emits_script_name() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%N");
assert!(!out.is_empty(), "%N should emit script name or argv[0]");
}
#[test]
fn putpromptchar_lowercase_m_emits_host_short() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%m");
assert!(!out.is_empty(), "%m should emit hostname (short form)");
assert!(
!out.contains('.'),
"%m with arg=1 should not contain dots; got {out:?}"
);
}
#[test]
fn putpromptchar_l_emits_tty_short_name() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%l");
assert_ne!(out, "%l", "%l must be expanded, not literal");
}
#[test]
fn putpromptchar_y_emits_tty_name() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%y");
assert_ne!(out, "%y", "%y must be expanded, not literal");
}
#[test]
fn putpromptchar_w_emits_day_date() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%w");
assert!(
out.chars().any(|c| c.is_ascii_digit()),
"%w should contain a day-number digit; got {out:?}"
);
}
#[test]
fn putpromptchar_E_emits_clear_eol_escape() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%E");
assert!(out.contains('\x1b'), "%E should emit an ANSI escape");
}
#[test]
fn putpromptchar_G_emits_glitch_space() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%G");
assert_ne!(out, "%G", "%G must NOT pass through as literal");
}
#[test]
fn putpromptchar_v_emits_psvar_element() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%v");
assert_ne!(out, "%v", "%v must be expanded, not literal");
}
#[test]
fn putpromptchar_underscore_emits_cmdstack() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%_");
assert_ne!(out, "%_", "%_ must be expanded, not literal");
}
#[test]
fn putpromptchar_L_emits_shlvl() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%L");
assert!(
out.parse::<i32>().is_ok(),
"%L should emit a decimal shell-level; got {out:?}"
);
}
#[test]
fn putpromptchar_i_emits_lineno() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%i");
assert!(
out.parse::<i32>().is_ok(),
"%i should emit a decimal line number; got {out:?}"
);
}
#[test]
fn putpromptchar_I_emits_funcstack_lineno() {
let _g = crate::test_util::global_state_lock();
let out = expand_prompt("%I");
assert!(
out.parse::<i32>().is_ok(),
"%I should be decimal; got {out:?}"
);
}
#[test]
fn promptpath_returns_string_type() {
let _g = crate::test_util::global_state_lock();
let _: String = promptpath("", 0, false, "");
}
#[test]
fn promptpath_empty_path_returns_empty() {
let _g = crate::test_util::global_state_lock();
assert_eq!(promptpath("", 0, false, ""), "");
}
#[test]
fn zattrescape_returns_string_type() {
let _: String = zattrescape(0);
}
#[test]
fn zattrescape_zero_is_pure() {
let first = zattrescape(0);
for _ in 0..3 {
assert_eq!(zattrescape(0), first, "zattrescape(0) must be pure");
}
}
#[test]
fn parsehighlight_returns_zattr_type() {
let _: zattr = parsehighlight("");
}
#[test]
fn cmdpush_cmdpop_round_trip_safe() {
let _g = crate::test_util::global_state_lock();
cmdpush(0);
cmdpop();
}
#[test]
fn countprompt_empty_no_panic() {
let _g = crate::test_util::global_state_lock();
let mut w = 0i32;
let mut h = 0i32;
countprompt("", &mut w, &mut h, 0);
}
#[test]
fn match_named_colour_empty_returns_none_pin() {
assert!(match_named_colour("").is_none(), "empty color name → None");
}
#[test]
fn match_named_colour_red_returns_some() {
let r = match_named_colour("red");
assert!(r.is_some(), "'red' must be a known color");
}
#[test]
fn truecolor_terminal_returns_bool_type() {
let _g = crate::test_util::global_state_lock();
let _: bool = truecolor_terminal();
}
#[test]
fn match_highlight_returns_tuple_type() {
let _g = crate::test_util::global_state_lock();
let _: (zattr, zattr) = match_highlight("");
}
#[test]
fn promptexpand_percent_j_returns_job_count() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%j", 0, None);
assert!(
got.chars().all(|c| c.is_ascii_digit()),
"%j must expand to a decimal count, got {:?}",
got
);
assert_ne!(got, "%j", "%j must NOT emit literally");
}
#[test]
fn promptexpand_percent_bang_returns_history_number() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%!", 0, None);
assert!(
got.chars().all(|c| c.is_ascii_digit()) || got.starts_with('-'),
"%! must expand to a (signed) decimal, got {:?}",
got
);
assert_ne!(got, "%!", "%! must NOT emit literally");
}
#[test]
fn promptexpand_percent_T_returns_hhmm_clock() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%T", 0, None);
assert_ne!(got, "%T", "%T must NOT emit literally");
let n = got.len();
assert!(
n == 4 || n == 5,
"%T → 'H:MM' or 'HH:MM' (4 or 5 chars), got {:?} (len {})",
got, n
);
let colon_at = n - 3;
assert_eq!(&got[colon_at..colon_at + 1], ":", "colon at H/HH boundary in {:?}", got);
}
#[test]
fn promptexpand_percent_star_returns_hhmmss_clock() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%*", 0, None);
assert_ne!(got, "%*", "%* must NOT emit literally");
let n = got.len();
assert!(
n == 7 || n == 8,
"%* → 'H:MM:SS' or 'HH:MM:SS' (7 or 8 chars), got {:?} (len {})",
got, n
);
assert_eq!(&got[n - 3..n - 2], ":", "second colon at offset {} in {:?}", n - 3, got);
assert_eq!(&got[n - 6..n - 5], ":", "first colon at offset {} in {:?}", n - 6, got);
}
#[test]
fn promptexpand_percent_D_braces_year_returns_four_digit_year() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%D{%Y}", 0, None);
assert_ne!(got, "%D{%Y}", "must NOT emit literally");
let year: u32 = got
.parse()
.unwrap_or_else(|_| panic!("%D{{%Y}} must be 4-digit int, got {:?}", got));
assert!(year >= 2025, "year >= 2025, got {}", year);
}
#[test]
fn promptexpand_percent_D_bare_returns_dash_separated_date() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%D", 0, None);
assert_ne!(got, "%D", "%D must NOT emit literally");
assert_eq!(got.len(), 8, "%D → 'YY-MM-DD' (8 chars), got {:?}", got);
assert_eq!(&got[2..3], "-", "first dash at offset 2");
assert_eq!(&got[5..6], "-", "second dash at offset 5");
}
#[test]
fn promptexpand_percent_i_returns_digit() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%i", 0, None);
assert_ne!(got, "%i", "%i must NOT emit literally");
assert!(
got.chars().all(|c| c.is_ascii_digit()),
"%i must be a decimal, got {:?}",
got
);
}
#[test]
fn promptexpand_percent_percent_still_literal() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("%%", 0, None);
assert_eq!(got, "%", "%% → literal %");
}
#[test]
fn promptpath_returns_string_pin_alt() {
let _: String = promptpath("/tmp", 0, false, "/home/u");
}
#[test]
fn promptpath_empty_path_safe() {
let _ = promptpath("", 0, false, "/home/u");
let _ = promptpath("", 0, true, "/home/u");
}
#[test]
fn promptpath_deterministic() {
for (p, npath, tilde, home) in [
("/tmp", 0usize, false, "/home/u"),
("/usr/local/bin", 2, true, "/home/u"),
("/home/u/proj", 0, true, "/home/u"),
] {
let a = promptpath(p, npath, tilde, home);
let b = promptpath(p, npath, tilde, home);
assert_eq!(
a, b,
"promptpath({:?}, {}, {}, {:?}) must be pure",
p, npath, tilde, home
);
}
}
#[test]
fn promptexpand_returns_tuple_type() {
let _g = crate::test_util::global_state_lock();
let _: (String, Option<usize>, Option<usize>) = promptexpand("", 0, None);
}
#[test]
fn promptexpand_empty_input_returns_empty_string() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("", 0, None);
assert_eq!(got, "", "empty prompt → empty output");
}
#[test]
fn promptexpand_plain_text_returned_verbatim() {
let _g = crate::test_util::global_state_lock();
let (got, _, _) = promptexpand("hello world", 0, None);
assert_eq!(got, "hello world", "plain text (no %) returned verbatim");
}
#[test]
fn zattrescape_returns_string_pin_alt() {
let _: String = zattrescape(0);
}
#[test]
fn zattrescape_zero_deterministic() {
let a = zattrescape(0);
let b = zattrescape(0);
assert_eq!(a, b, "zattrescape(0) must be pure");
}
#[test]
fn parsehighlight_empty_returns_some_value() {
let _: zattr = parsehighlight("");
}
#[test]
fn countprompt_empty_input_zero_width_one_height() {
let mut w = -1i32;
let mut h = -1i32;
countprompt("", &mut w, &mut h, 0);
assert_eq!(w, 0, "empty width = 0");
assert_eq!(h, 1, "empty height = 1 (first-line default)");
}
#[test]
fn countprompt_plain_ascii_no_panic() {
let mut w = 0i32;
let mut h = 0i32;
countprompt("hello", &mut w, &mut h, 0);
assert!(w > 0, "width should be > 0 for 'hello'; got {}", w);
}
#[test]
fn truecolor_terminal_returns_bool_and_deterministic() {
let _g = crate::test_util::global_state_lock();
let _: bool = truecolor_terminal();
let a = truecolor_terminal();
let b = truecolor_terminal();
assert_eq!(a, b, "truecolor_terminal must be pure");
}
#[test]
fn match_named_colour_returns_option_u8_deterministic() {
let _: Option<u8> = match_named_colour("red");
for name in &["red", "blue", "__unknown__", ""] {
let a = match_named_colour(name);
let b = match_named_colour(name);
assert_eq!(a, b, "match_named_colour({:?}) must be pure", name);
}
}
#[test]
fn match_named_colour_empty_returns_none_alt() {
assert!(match_named_colour("").is_none(), "empty colour name → None");
}
}