use std::cell::RefCell;
use std::env;
use std::sync::atomic::Ordering;
use crate::DPUTS;
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;
pub(crate) mod prompt_tls {
use std::cell::RefCell;
use std::env;
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;
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(arg: &str, is_fg: bool) -> Option<(Color, String)> {
let color = color_from_name(arg)?; let ansi = color_to_ansi(color, is_fg); Some((color, ansi))
}
pub fn putpromptchar(bv: &mut buf_vars, doprint: i32, endchar: i32) -> i32 {
bv.run_putpromptchar(doprint, endchar)
}
pub fn pputc(buf: &mut String, c: char) {
buf.push(c);
}
pub fn addbufspc(need: i32) {
let _need_doubled = need.saturating_mul(2);
}
pub fn stradd(buf: &mut String, s: &str) {
buf.push_str(s);
}
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
}
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; }
0 }
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 need_reset = (old_b && !new_b) || (old_u && !new_u) || (old_s && !new_s);
if need_reset {
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");
}
} else {
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");
}
}
if (old & TXT_ATTR_FG_MASK) != (new & TXT_ATTR_FG_MASK) {
if new & TXTFGCOLOUR != 0 {
let raw = (new & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT;
let c = if new & TXT_ATTR_FG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
result.push_str(&color_to_ansi(c, true));
} else {
result.push_str("\x1b[39m");
}
}
if (old & TXT_ATTR_BG_MASK) != (new & TXT_ATTR_BG_MASK) {
if new & TXTBGCOLOUR != 0 {
let raw = (new & TXT_ATTR_BG_COL_MASK) >> TXT_ATTR_BG_COL_SHIFT;
let c = if new & TXT_ATTR_BG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
result.push_str(&color_to_ansi(c, false));
} 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 mut result = String::new();
if newattrs & TXTBOLDFACE != 0 {
result.push_str("\x1b[22m");
}
if newattrs & TXTUNDERLINE != 0 {
result.push_str("\x1b[24m");
}
if newattrs & TXTSTANDOUT != 0 {
result.push_str("\x1b[27m");
}
if newattrs & TXTFGCOLOUR != 0 {
result.push_str("\x1b[39m");
}
if newattrs & TXTBGCOLOUR != 0 {
result.push_str("\x1b[49m");
}
result
}
#[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 zterm_columns = crate::ported::utils::adjustcolumns() as i32;
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 {
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 }
impl buf_vars {
pub fn new(input: &str) -> Self {
Self {
buf: vec![0u8; 256],
bufspc: 256,
bp: 0,
bufline: 0,
bp1: None,
fm: input.to_string(),
fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: 0 as zattr, in_escape: false,
}
}
fn fork_snapshot(&self, input: String) -> buf_vars {
buf_vars {
buf: Vec::new(),
bufspc: 0,
bp: 0,
bufline: 0,
bp1: None,
fm: input,
fm_pos: 0,
truncwidth: 0,
dontcount: 0,
trunccount: 0,
rstring: None,
Rstring: None,
attrs: self.attrs,
in_escape: false,
}
}
fn addbufspc(&mut self, need: usize) {
let need = need.saturating_mul(2).max(need.max(1));
self.buf.reserve(need);
self.bufspc = self.buf.capacity();
}
fn pputc(&mut self, c: u8) {
self.addbufspc(2);
let bp = self.bp;
if imeta_byte(c) {
self.buf.resize(bp + 2, 0);
self.buf[bp] = Meta as u8;
self.buf[bp + 1] = c ^ 32;
self.bp = bp + 2;
} else {
self.buf.resize(bp + 1, 0);
self.buf[bp] = c;
self.bp = bp + 1;
}
if c == b'\n' && self.dontcount == 0 {
self.bufline = self.bp;
}
}
fn out_raw_byte(&mut self, b: u8) {
self.addbufspc(1);
let bp = self.bp;
self.buf.resize(bp + 1, 0);
self.buf[bp] = b;
self.bp = bp + 1;
}
fn out_char(&mut self, c: char) {
let mut tmp = [0u8; 4];
let enc = c.encode_utf8(&mut tmp);
for &b in enc.as_bytes() {
self.pputc(b);
}
}
fn out_str(&mut self, s: &str) {
for &b in s.as_bytes() {
self.pputc(b);
}
}
fn append_buf_from(&mut self, other: &buf_vars) {
let end = other.bp.min(other.buf.len());
if end == 0 {
return;
}
self.addbufspc(end);
let bp0 = self.bp;
self.buf.resize(bp0 + end, 0);
self.buf[bp0..bp0 + end].copy_from_slice(&other.buf[..end]);
self.bp = bp0 + end;
if self.dontcount == 0 {
for i in 0..end {
if other.buf[i] == b'\n' {
self.bufline = bp0 + i + 1;
}
}
}
}
pub fn expanded_utf8(&self) -> String {
let end = self.bp.min(self.buf.len());
let mut v = self.buf[..end].to_vec();
crate::ported::utils::unmetafy(&mut v);
String::from_utf8_lossy(&v).into_owned()
}
fn strip_prompt_tokens_ns0(&mut self) {
let end = self.bp.min(self.buf.len());
let mut v = Vec::with_capacity(end);
let mut i = 0usize;
while i < end {
let b = self.buf[i];
if b == (Meta as u8) {
if i + 1 < end {
v.push(b);
v.push(self.buf[i + 1]);
i += 2;
} else {
i += 1;
}
continue;
}
if b == Inpar as u8 || b == Outpar as u8 || b == Nularg as u8 {
i += 1;
continue;
}
v.push(b);
i += 1;
}
self.buf = v;
self.bp = self.buf.len();
}
pub fn finish_expanded_string(&mut self, keep_spacing_tokens: bool) -> String {
if !keep_spacing_tokens {
self.strip_prompt_tokens_ns0();
}
self.expanded_utf8()
}
pub(crate) fn run_putpromptchar(&mut self, doprint: i32, endchar: i32) -> i32 {
loop {
if self.fm_pos >= self.fm.len() {
return 0;
}
let ec = endchar as u8;
if ec != 0 {
let b = self.fm.as_bytes()[self.fm_pos];
if b == ec {
return endchar;
}
}
let c = match self.peek() {
Some(c) => c,
None => return 0,
};
if c == '%' && isset(PROMPTPERCENT) {
self.advance();
self.process_percent(doprint);
} else if c == '!' && isset(PROMPTBANG) {
if doprint != 0 {
self.advance();
if self.peek() == Some('!') {
self.advance();
self.out_char('!');
} else {
self.out_str(&prompt_tls::HISTNUM.with(|c| *c.borrow()).to_string());
}
} else {
self.advance();
if self.peek() == Some('!') {
self.advance();
}
}
} else {
self.advance();
if doprint != 0 {
self.out_char(c);
}
}
}
}
fn peek(&self) -> Option<char> {
self.fm[self.fm_pos..].chars().next()
}
fn advance(&mut self) -> Option<char> {
let c = self.peek()?;
self.fm_pos += c.len_utf8();
Some(c)
}
fn parse_number(&mut self) -> Option<i32> {
let start = self.fm_pos;
let mut negative = false;
if self.peek() == Some('-') {
negative = true;
self.advance();
}
while let Some(c) = self.peek() {
if c.is_ascii_digit() {
self.advance();
} else {
break;
}
}
if self.fm_pos == start || (negative && self.fm_pos == start + 1) {
if negative {
self.fm_pos = start;
}
return None;
}
let num_str = &self.fm[if negative { start + 1 } else { start }..self.fm_pos];
let num: i32 = num_str.parse().ok()?;
Some(if negative { -num } else { num })
}
fn parse_braced_arg(&mut self) -> Option<String> {
if self.peek() != Some('{') {
return None;
}
self.advance();
let start = self.fm_pos;
let mut depth = 1;
while let Some(c) = self.advance() {
match c {
'{' => depth += 1,
'}' => {
depth -= 1;
if depth == 0 {
return Some(self.fm[start..self.fm_pos - 1].to_string());
}
}
'\\' => {
self.advance(); }
_ => {}
}
}
None
}
fn path_with_tilde(&self, path: &str) -> String {
let home = prompt_tls::HOME.with(|c| c.borrow().clone());
if !home.is_empty() && path.starts_with(&home) {
format!("~{}", &path[home.len()..])
} else {
path.to_string()
}
}
fn trailing_path(&self, path: &str, n: usize, with_tilde: bool) -> String {
let path = if with_tilde {
self.path_with_tilde(path)
} else {
path.to_string()
};
if n == 0 {
return path;
}
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.len() <= n {
return path;
}
components[components.len() - n..].join("/")
}
fn leading_path(&self, path: &str, n: usize) -> String {
if n == 0 {
return path.to_string();
}
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.len() <= n {
return path.to_string();
}
let result = components[..n].join("/");
if path.starts_with('/') {
format!("/{}", result)
} else {
result
}
}
fn start_escape(&mut self) {
if !self.in_escape {
self.out_char('\x01'); self.in_escape = true;
}
}
fn end_escape(&mut self) {
if self.in_escape {
self.out_char('\x02'); self.in_escape = false;
}
}
fn apply_attrs(&mut self) {
self.start_escape();
if self.attrs & TXTBOLDFACE != 0 {
self.out_str("\x1b[1m");
}
if self.attrs & TXTUNDERLINE != 0 {
self.out_str("\x1b[4m");
}
if self.attrs & TXTSTANDOUT != 0 {
self.out_str("\x1b[3m");
}
if self.attrs & TXTFGCOLOUR != 0 {
let raw = (self.attrs & TXT_ATTR_FG_COL_MASK) >> TXT_ATTR_FG_COL_SHIFT;
let c = if self.attrs & TXT_ATTR_FG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
self.out_str(&color_to_ansi(c, true));
}
if self.attrs & TXTBGCOLOUR != 0 {
let raw = (self.attrs & TXT_ATTR_BG_COL_MASK) >> TXT_ATTR_BG_COL_SHIFT;
let c = if self.attrs & TXT_ATTR_BG_24BIT != 0 {
COLOR_24BIT | (raw as Color & 0x00ff_ffff)
} else {
raw as Color
};
self.out_str(&color_to_ansi(c, false));
}
self.end_escape();
}
fn parse_conditional(&mut self, arg: i32, doprint: i32) -> bool {
if self.peek() != Some('(') {
return false;
}
self.advance();
let cond_char = match self.advance() {
Some(c) => c,
None => return false,
};
let test = match cond_char {
'/' | 'c' | '.' | '~' | 'C' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let path = self.path_with_tilde(&pwd);
let depth = path.matches('/').count() as i32;
if arg == 0 {
depth > 0
} else {
depth >= arg
}
}
'?' => prompt_tls::LASTVAL.with(|c| *c.borrow()) == arg,
'#' => {
let euid = unsafe { libc::geteuid() };
euid == arg as u32
}
'L' => prompt_tls::SHLVL.with(|c| *c.borrow()) >= arg,
'j' => prompt_tls::NUM_JOBS.with(|c| *c.borrow()) >= arg,
'v' => (arg as usize) <= prompt_tls::PSVAR.with(|c| c.borrow().len()),
'V' => {
if arg <= 0 {
false
} else {
prompt_tls::PSVAR.with(|c| {
let v = c.borrow();
(arg as usize) <= v.len() && !v[arg as usize - 1].is_empty()
})
}
}
'_' => prompt_tls::CMDSTACK.with(|c| c.borrow().len()) >= arg as usize,
't' | 'T' | 'd' | 'D' | 'w' => {
let now = chrono::Local::now();
match cond_char {
't' => now.format("%M").to_string().parse::<i32>().unwrap_or(0) == arg,
'T' => now.format("%H").to_string().parse::<i32>().unwrap_or(0) == arg,
'd' => now.format("%d").to_string().parse::<i32>().unwrap_or(0) == arg,
'D' => now.format("%m").to_string().parse::<i32>().unwrap_or(0) == arg - 1,
'w' => now.format("%w").to_string().parse::<i32>().unwrap_or(0) == arg,
_ => false,
}
}
'!' => prompt_tls::IS_ROOT.with(|c| *c.borrow()),
_ => false,
};
let sep = match self.advance() {
Some(c) => c,
None => return false,
};
let true_start = self.fm_pos;
let mut depth = 1;
while let Some(c) = self.peek() {
if c == '(' {
depth += 1;
} else if c == ')' {
depth -= 1;
if depth == 0 {
break;
}
} else if c == sep && depth == 1 {
break;
}
self.advance();
}
let true_branch = self.fm[true_start..self.fm_pos].to_string();
if self.peek() != Some(sep) {
return false;
}
self.advance();
let false_start = self.fm_pos;
depth = 1;
while let Some(c) = self.peek() {
if c == '(' {
depth += 1;
} else if c == ')' {
depth -= 1;
if depth == 0 {
break;
}
}
self.advance();
}
let false_branch = self.fm[false_start..self.fm_pos].to_string();
if self.peek() != Some(')') {
return false;
}
self.advance();
let mut tsub = self.fork_snapshot(true_branch);
tsub.run_putpromptchar(if test { doprint } else { 0 }, 0);
self.append_buf_from(&tsub);
let mut fsub = self.fork_snapshot(false_branch);
fsub.run_putpromptchar(if test { 0 } else { doprint }, 0);
self.append_buf_from(&fsub);
true
}
fn process_percent(&mut self, doprint: i32) {
let arg = self.parse_number().unwrap_or(0);
if self.peek() == Some('(') {
self.parse_conditional(arg, doprint);
return;
}
if doprint == 0 {
match self.peek() {
Some('[') => {
self.advance();
let _ = self.parse_number();
while self.peek() != Some(']') {
if self.advance().is_none() {
break;
}
}
let _ = self.advance();
return;
}
Some('<') | Some('>') => {
let end = self.peek().unwrap();
self.advance();
while self.peek() != Some(end) {
if self.advance().is_none() {
break;
}
}
let _ = self.advance();
return;
}
Some('D') => {
self.advance();
if self.peek() == Some('{') {
while self.peek() != Some('}') {
if self.advance().is_none() {
break;
}
}
let _ = self.advance();
}
return;
}
_ => {
let _ = self.advance();
return;
}
}
}
let c = match self.advance() {
Some(c) => c,
None => return,
};
match c {
'~' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let path = if arg == 0 {
self.path_with_tilde(&pwd)
} else if arg > 0 {
self.trailing_path(&pwd, arg as usize, true)
} else {
self.leading_path(&self.path_with_tilde(&pwd), (-arg) as usize)
};
self.out_str(&path);
}
'd' | '/' => {
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let path = if arg == 0 {
pwd
} else if arg > 0 {
self.trailing_path(&pwd, arg as usize, false)
} else {
self.leading_path(&pwd, (-arg) as usize)
};
self.out_str(&path);
}
'c' | '.' => {
let n = if arg == 0 {
1
} else {
arg.unsigned_abs() as usize
};
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let path = self.trailing_path(&pwd, n, true);
self.out_str(&path);
}
'C' => {
let n = if arg == 0 {
1
} else {
arg.unsigned_abs() as usize
};
let pwd = prompt_tls::PWD.with(|c| c.borrow().clone());
let path = self.trailing_path(&pwd, n, false);
self.out_str(&path);
}
'N' => {
let name = prompt_tls::SCRIPTNAME
.with(|c| c.borrow().clone())
.unwrap_or_else(|| prompt_tls::ARGEXTRA.with(|c| c.borrow().clone()));
let n = if arg <= 0 {
0
} else {
arg.unsigned_abs() as usize
};
if n == 0 {
self.out_str(&name);
} else {
let tail = self.trailing_path(&name, n, false);
self.out_str(&tail);
}
}
'n' => {
let u = prompt_tls::USER.with(|c| c.borrow().clone());
self.out_str(&u);
}
'M' => {
let h = prompt_tls::HOST.with(|c| c.borrow().clone());
self.out_str(&h);
}
'm' => {
let n = if arg == 0 { 1 } else { arg };
let host = prompt_tls::HOST.with(|c| c.borrow().clone());
if n > 0 {
let parts: Vec<&str> = host.split('.').collect();
let take = (n as usize).min(parts.len());
self.out_str(&parts[..take].join("."));
} else {
let parts: Vec<&str> = host.split('.').collect();
let skip = ((-n) as usize).min(parts.len());
self.out_str(&parts[skip..].join("."));
}
}
'l' => {
let t = prompt_tls::TTY.with(|c| c.borrow().clone());
let tty = if t.starts_with("/dev/tty") {
t[8..].to_string()
} else if t.starts_with("/dev/") {
t[5..].to_string()
} else {
"()".to_string()
};
self.out_str(&tty);
}
'y' => {
let t = prompt_tls::TTY.with(|c| c.borrow().clone());
let tty = if t.is_empty() {
"()".to_string()
} else if t.starts_with("/dev/") {
t[5..].to_string()
} else {
t
};
self.out_str(&tty);
}
'?' => self.out_str(&prompt_tls::LASTVAL.with(|c| *c.borrow()).to_string()),
'#' => self.out_char(
if prompt_tls::IS_ROOT.with(|c| *c.borrow()) { '#' } else { '%' }
),
'h' | '!' => self.out_str(&prompt_tls::HISTNUM.with(|c| *c.borrow()).to_string()),
'j' => self.out_str(&prompt_tls::NUM_JOBS.with(|c| *c.borrow()).to_string()),
'L' => self.out_str(&prompt_tls::SHLVL.with(|c| *c.borrow()).to_string()),
'i' => self.out_str(&prompt_tls::LINENO.with(|c| *c.borrow()).to_string()),
'I' => {
let lineno = prompt_tls::LINENO.with(|c| *c.borrow());
let n = if let Some(base) = prompt_tls::FUNC_LINE_BASE.with(|c| *c.borrow()) {
lineno.saturating_add(base)
} else {
lineno
};
self.out_str(&n.to_string());
}
'x' => {
let n = if arg <= 0 {
0
} else {
arg.unsigned_abs() as usize
};
if prompt_tls::FUNC_LINE_BASE.with(|c| c.borrow().is_some()) {
let path = prompt_tls::FUNCSTACK_FILENAME
.with(|c| c.borrow().clone())
.unwrap_or_default();
if n == 0 {
self.out_str(&path);
} else {
let tail = self.trailing_path(&path, n, false);
self.out_str(&tail);
}
} else {
let name = prompt_tls::SCRIPTFILENAME
.with(|c| c.borrow().clone())
.or_else(|| prompt_tls::SCRIPTNAME.with(|c| c.borrow().clone()))
.unwrap_or_else(|| prompt_tls::ARGEXTRA.with(|c| c.borrow().clone()));
if n == 0 {
self.out_str(&name);
} else {
let tail = self.trailing_path(&name, n, false);
self.out_str(&tail);
}
}
}
'D' => {
let now = chrono::Local::now();
if let Some(fmt) = self.parse_braced_arg() {
let mut chrono_fmt = String::new();
let mut chars = fmt.chars().peekable();
while let Some(c) = chars.next() {
if c == '%' {
match chars.next() {
Some('a') => chrono_fmt.push_str("%a"),
Some('A') => chrono_fmt.push_str("%A"),
Some('b') | Some('h') => chrono_fmt.push_str("%b"),
Some('B') => chrono_fmt.push_str("%B"),
Some('c') => chrono_fmt.push_str("%c"),
Some('C') => chrono_fmt.push_str("%y"),
Some('d') => chrono_fmt.push_str("%d"),
Some('D') => chrono_fmt.push_str("%m/%d/%y"),
Some('e') => chrono_fmt.push_str("%e"),
Some('f') => chrono_fmt.push_str("%e"),
Some('F') => chrono_fmt.push_str("%Y-%m-%d"),
Some('H') => chrono_fmt.push_str("%H"),
Some('I') => chrono_fmt.push_str("%I"),
Some('j') => chrono_fmt.push_str("%j"),
Some('k') => chrono_fmt.push_str("%k"),
Some('K') => chrono_fmt.push_str("%H"),
Some('l') => chrono_fmt.push_str("%l"),
Some('L') => chrono_fmt.push_str("%3f"),
Some('m') => chrono_fmt.push_str("%m"),
Some('M') => chrono_fmt.push_str("%M"),
Some('n') => chrono_fmt.push('\n'),
Some('N') => chrono_fmt.push_str("%9f"),
Some('p') => chrono_fmt.push_str("%p"),
Some('P') => chrono_fmt.push_str("%P"),
Some('r') => chrono_fmt.push_str("%r"),
Some('R') => chrono_fmt.push_str("%R"),
Some('s') => chrono_fmt.push_str("%s"),
Some('S') => chrono_fmt.push_str("%S"),
Some('t') => chrono_fmt.push('\t'),
Some('T') => chrono_fmt.push_str("%T"),
Some('u') => chrono_fmt.push_str("%u"),
Some('U') => chrono_fmt.push_str("%U"),
Some('V') => chrono_fmt.push_str("%V"),
Some('w') => chrono_fmt.push_str("%w"),
Some('W') => chrono_fmt.push_str("%W"),
Some('x') => chrono_fmt.push_str("%x"),
Some('X') => chrono_fmt.push_str("%X"),
Some('y') => chrono_fmt.push_str("%y"),
Some('Y') => chrono_fmt.push_str("%Y"),
Some('z') => chrono_fmt.push_str("%z"),
Some('Z') => chrono_fmt.push_str("%Z"),
Some('%') => chrono_fmt.push('%'),
Some(other) => {
chrono_fmt.push('%');
chrono_fmt.push(other);
}
None => chrono_fmt.push('%'),
}
} else {
chrono_fmt.push(c);
}
}
self.out_str(&now.format(&chrono_fmt).to_string());
} else {
self.out_str(&now.format("%y-%m-%d").to_string());
}
}
'T' => {
let now = chrono::Local::now();
let formatted = now.format("%k:%M").to_string();
self.out_str(formatted.trim_start());
}
'*' => {
let now = chrono::Local::now();
let formatted = now.format("%k:%M:%S").to_string();
self.out_str(formatted.trim_start());
}
't' | '@' => {
let now = chrono::Local::now();
self.out_str(&now.format("%l:%M%p").to_string());
}
'w' => {
let now = chrono::Local::now();
self.out_str(&now.format("%a %e").to_string());
}
'W' => {
let now = chrono::Local::now();
self.out_str(&now.format("%m/%d/%y").to_string());
}
'B' => {
let fg_palette = zattr_fg_palette(self.attrs);
self.attrs |= TXTBOLDFACE; self.start_escape();
self.out_str("\x1b[1m");
self.end_escape();
if let Some(c) = fg_palette {
self.start_escape();
self.out_str(&color_to_ansi(c as Color, true));
self.end_escape();
}
}
'b' => {
let fg_palette = zattr_fg_palette(self.attrs);
self.attrs &= !TXTBOLDFACE; self.start_escape();
self.out_str("\x1b[0m");
self.end_escape();
if let Some(c) = fg_palette {
self.start_escape();
self.out_str(&color_to_ansi(c as Color, true));
self.end_escape();
}
}
'U' => {
self.attrs |= TXTUNDERLINE; self.start_escape();
self.out_str("\x1b[4m");
self.end_escape();
}
'u' => {
self.attrs &= !TXTUNDERLINE; self.start_escape();
self.out_str("\x1b[24m");
self.end_escape();
}
'S' => {
self.attrs |= TXTSTANDOUT; self.start_escape();
self.out_str("\x1b[3m");
self.end_escape();
}
's' => {
self.attrs &= !TXTSTANDOUT; self.start_escape();
self.out_str("\x1b[23m");
self.end_escape();
}
'F' => {
let color: Option<Color> = if let Some(name) = self.parse_braced_arg() {
color_from_name(&name) } else if arg > 0 {
Some(arg as Color) } else {
None
};
if let Some(c) = color {
if let Some((r, g, b)) = color_get_rgb(c) {
self.attrs = zattr_set_fg_rgb(self.attrs, r, g, b); } else {
self.attrs = zattr_set_fg_palette(self.attrs, c as u8); }
self.start_escape();
self.out_str(&color_to_ansi(c, true));
self.end_escape();
}
}
'f' => {
self.attrs &= !TXT_ATTR_FG_MASK; self.start_escape();
self.out_str("\x1b[39m");
self.end_escape();
}
'K' => {
let color: Option<Color> = if let Some(name) = self.parse_braced_arg() {
color_from_name(&name) } else if arg > 0 {
Some(arg as Color) } else {
None
};
if let Some(c) = color {
if let Some((r, g, b)) = color_get_rgb(c) {
self.attrs = zattr_set_bg_rgb(self.attrs, r, g, b); } else {
self.attrs = zattr_set_bg_palette(self.attrs, c as u8); }
self.start_escape();
self.out_str(&color_to_ansi(c, false));
self.end_escape();
}
}
'k' => {
self.attrs &= !TXT_ATTR_BG_MASK; self.start_escape();
self.out_str("\x1b[49m");
self.end_escape();
}
'{' => self.start_escape(),
'}' => self.end_escape(),
'G' => {
let n = if arg > 0 { arg as usize } else { 1 };
for _ in 0..n {
self.out_char(' ');
}
}
'v' => {
let idx = if arg == 0 { 1 } else { arg };
let s_opt = prompt_tls::PSVAR.with(|c| {
let v = c.borrow();
if idx > 0 && (idx as usize) <= v.len() {
Some(v[idx as usize - 1].clone())
} else {
None
}
});
if let Some(s) = s_opt {
self.out_str(&s);
}
}
'_' => {
let cmd_stack = prompt_tls::CMDSTACK.with(|c| c.borrow().clone());
let cmdsp = cmd_stack.len();
if cmdsp > 0 {
let names: Vec<&str> = if arg >= 0 {
let mut n = if arg == 0 { cmdsp } else { arg as usize };
if n > cmdsp {
n = cmdsp;
}
cmd_stack
.iter()
.skip(cmdsp - n)
.filter_map(|b| CMDNAMES.get(*b as usize).copied())
.collect()
} else {
let mut n = (-arg) as usize;
if n > cmdsp {
n = cmdsp;
}
cmd_stack
.iter()
.take(n)
.filter_map(|b| CMDNAMES.get(*b as usize).copied())
.collect()
};
self.out_str(&names.join(" "));
}
}
'E' => {
self.start_escape();
self.out_str("\x1b[K");
self.end_escape();
}
'%' => self.out_char('%'),
')' => self.out_char(')'),
'\0' => {}
_ => {
self.out_char('%');
self.out_char(c);
}
}
}
pub fn expand(mut self) -> String {
self.run_putpromptchar(1, 0);
self.finish_expanded_string(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 {
format!("\x1b[{};1m", 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 {
prompt_tls::sync_from_globals();
buf_vars::new(s).expand() }
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_appends_char_to_buffer() {
let _g = crate::test_util::global_state_lock();
let mut buf = String::new();
pputc(&mut buf, 'X');
assert_eq!(buf, "X");
pputc(&mut buf, 'Y');
assert_eq!(buf, "XY");
}
#[test]
fn stradd_appends_string_to_buffer() {
let _g = crate::test_util::global_state_lock();
let mut buf = String::from("pre/");
stradd(&mut buf, "post");
assert_eq!(buf, "pre/post");
stradd(&mut buf, "");
assert_eq!(buf, "pre/post", "empty append leaves buffer unchanged");
}
#[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_emits_attr_reset_with_ignore_markers() {
assert_eq!(expand("%b"), "\x01\x1b[0m\x02");
}
#[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_emits_default_fg_with_ignore_markers() {
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_emits_default_bg_with_ignore_markers() {
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]
#[ignore = "diagnostic dump — run with --ignored"]
fn dump_prompt_escapes() {
for s in &["%B", "%b", "%F{red}", "%f", "%{ABCD%}", "%{ABCD%}xyz"] {
let out = expand(s);
eprintln!("expand({s:?}) = {out:?}");
}
}
#[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]
#[ignore = "ZSHRS BUG: %(?..) ternary requires $? state plumbing"]
fn promptexpand_corpus_ternary_question_zero_branch() {
let out = expand("%(?.OK.FAIL)");
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]
#[ignore = "ZSHRS BUG: %S emits SGR italic (\\e[3m) instead of standout/reverse (\\e[7m)"]
fn promptexpand_corpus_standout_emits_sgr() {
let out = expand("%Stext%s");
assert!(
out.contains("\x1b[7m") || out.contains("\x1b[07m"),
"%S should emit SGR standout-on, 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:?}",
);
}
}