use std::cell::{Cell, RefCell};
use std::io::{self, Read, Write};
use std::sync::atomic::Ordering;
use std::time::Duration;
use crate::ported::zsh_h::ASSPM_AUGMENT;
use crate::ported::zle::zle_h::{
CURC_DEFAULT, CURC_INSERT, CURC_PENDING, CURC_REGION_END, CURC_REGION_START, CURF_BAR,
CURF_BLINK, CURF_BLOCK, CURF_BLUE_SHIFT, CURF_COLOR, CURF_COLOR_MASK, CURF_GREEN_SHIFT,
CURF_HIDDEN, CURF_RED_SHIFT, CURF_SHAPE_MASK, CURF_STEADY, CURF_UNDERLINE,
};
thread_local! {
static CURSOR_FORMS: RefCell<Vec<u32>> =
RefCell::new(vec![0u32; CURC_DEFAULT as usize]);
static CURSOR_ENABLED_MASK: Cell<u32> = const { Cell::new(0) };
static CURSORFORM_SETUP: Cell<bool> = const { Cell::new(false) };
static AID: Cell<u32> = const { Cell::new(0) };
static PRE_BUFFER: RefCell<Vec<u8>> = RefCell::new(
b"\x1b]133;A;cl=m;aid=zZZZZZZ\x1b\\".to_vec()
);
}
#[allow(unused_imports)]
use crate::ported::zle::{
deltochar::*, textobjects::*, zle_hist::*, zle_main::*, zle_misc::*, zle_move::*,
zle_params::*, zle_refresh::*, zle_tricky::*, zle_utils::*, zle_vi::*, zle_word::*,
};
#[allow(unused_imports)]
#[allow(unused_imports)]
pub const TIMEOUT: i64 = -51;
pub const TAG: u8 = 1 << 7;
pub const SEQ: u8 = TAG | (1 << 6);
pub const T_BEGIN: u8 = 0x80; pub const T_END: u8 = 0x81; pub const T_OR: u8 = 0x82; pub const T_REPEAT: u8 = 0x83; pub const T_NUM: u8 = 0x84; pub const T_HEX: u8 = 0x85; pub const T_HEXCH: u8 = 0x86; pub const T_WILDCARD: u8 = 0x87; pub const T_RECORD: u8 = 0x88; pub const T_CAPTURE: u8 = 0x89; pub const T_DROP: u8 = 0x91; pub const T_CONTINUE: u8 = 0x92; pub const T_NEXT: u8 = 0x94;
pub fn find_branch(s: &str, ch: u8) -> Option<usize> {
s.bytes().position(|b| b == ch)
}
pub fn find_matching(s: &str, open: u8, close: u8) -> Option<usize> {
let bytes = s.as_bytes();
let mut depth = 0i32;
for (i, &b) in bytes.iter().enumerate() {
if b == open {
depth += 1;
} else if b == close {
depth -= 1;
if depth == 0 {
return Some(i);
}
}
}
None
}
fn probe_terminal(query: &str, timeout_ms: u64) -> io::Result<String> {
#[cfg(unix)]
{
let mut old_termios: libc::termios = unsafe { std::mem::zeroed() };
let has_old = unsafe { libc::tcgetattr(0, &mut old_termios) } == 0;
if has_old {
let mut raw = old_termios;
raw.c_lflag &= !(libc::ICANON | libc::ECHO);
raw.c_cc[libc::VMIN] = 0;
raw.c_cc[libc::VTIME] = (timeout_ms / 100).min(255) as u8;
unsafe { libc::tcsetattr(0, libc::TCSANOW, &raw) };
}
let _ = {
let fd = crate::ported::init::SHTTY.load(Ordering::Relaxed);
let out = if fd >= 0 { fd } else { 1 };
crate::ported::utils::write_loop(out, query.as_bytes())
};
let mut response = Vec::new();
let mut buf = [0u8; 1];
let deadline = std::time::Instant::now() + Duration::from_millis(timeout_ms);
while std::time::Instant::now() < deadline {
match io::stdin().read(&mut buf) {
Ok(1) => {
response.push(buf[0]);
if buf[0] == b'c'
|| buf[0] == b'n'
|| buf[0] == b't'
|| buf[0] == b'\\'
|| buf[0] == 0x07
{
break;
}
}
Ok(0) => break,
_ => break,
}
}
if has_old {
unsafe { libc::tcsetattr(0, libc::TCSANOW, &old_termios) };
}
Ok(String::from_utf8_lossy(&response).to_string())
}
#[cfg(not(unix))]
{
let _ = (query, timeout_ms);
Ok(String::new())
}
}
pub static memo_cursor: std::sync::atomic::AtomicU32 =
std::sync::atomic::AtomicU32::new(0);
const COLORVAR: [&str; 3] = [".term.fg", ".term.bg", ".term.cursor"];
const MODEVAR: &str = ".term.mode";
pub fn handle_color(bg: i32, red: i32, green: i32, blue: i32) -> i32 {
use crate::ported::zsh_h::{
TXT_ATTR_BG_24BIT, TXT_ATTR_BG_COL_SHIFT, TXT_ATTR_BG_MASK,
TXT_ATTR_FG_24BIT, TXT_ATTR_FG_COL_SHIFT, TXT_ATTR_FG_MASK,
};
let packed = (((red as u64) << 8) + green as u64) << 8 | blue as u64;
let memo_tc = &crate::ported::prompt::memo_term_color;
match bg {
0 => {
let mut v = memo_tc.load(std::sync::atomic::Ordering::Relaxed);
v &= !TXT_ATTR_FG_MASK; v |= TXT_ATTR_FG_24BIT | (packed << TXT_ATTR_FG_COL_SHIFT); memo_tc.store(v, std::sync::atomic::Ordering::Relaxed);
}
1 => {
let mut v = memo_tc.load(std::sync::atomic::Ordering::Relaxed);
v &= !TXT_ATTR_BG_MASK; v |= TXT_ATTR_BG_24BIT | (packed << TXT_ATTR_BG_COL_SHIFT); memo_tc.store(v, std::sync::atomic::Ordering::Relaxed);
let lightness =
0.2126_f32 * red as f32 + 0.7152_f32 * green as f32 + 0.0722_f32 * blue as f32;
let mode = if lightness <= 127.0 { "dark" } else { "light" }; let _ = crate::ported::params::assignsparam(MODEVAR, mode, 0); }
2 => {
let v = ((red as u32) << 24) | ((green as u32) << 16) | ((blue as u32) << 8); memo_cursor.store(v, std::sync::atomic::Ordering::Relaxed);
}
_ => return 0,
}
let colour = format!("#{:02x}{:02x}{:02x}", red, green, blue); if let Some(name) = COLORVAR.get(bg as usize) {
let _ = crate::ported::params::assignsparam(name, &colour, 0); }
0
}
static FEATURES: &[&str] = &[
"bg",
"fg",
"cursorcolor",
"modkeys-kitty",
"truecolor",
"id",
]; static EXTVAR: &str = ".term.extensions"; static IDVAR: &str = ".term.id"; static VERVAR: &str = ".term.version";
pub fn handle_query(sequence: i32, numbers: &[i32], capture: &str) {
match sequence {
1 => {
if numbers.len() == 4 {
handle_color(numbers[0], numbers[1], numbers[2], numbers[3]); }
}
2 => {
crate::ported::params::assignaparam(
EXTVAR, vec![FEATURES[3].to_string()],
ASSPM_AUGMENT,
);
}
3 => {
crate::ported::params::assignaparam(
EXTVAR, vec![FEATURES[4].to_string()],
ASSPM_AUGMENT,
);
}
4 => {
crate::ported::params::assignsparam(IDVAR, capture, 0); }
5 => {
crate::ported::params::assignsparam(VERVAR, capture, 0); }
_ => {}
}
}
pub fn query_terminal() {
const TQ_BGCOLOR: &str = "\x1b]11;?\x1b\\";
const TQ_FGCOLOR: &str = "\x1b]10;?\x1b\\";
const TQ_CURSOR: &str = "\x1b]12;?\x1b\\";
const TQ_KITTYKB: &str = "\x1b[?u";
const TQ_RGB: &str = "\x1bP+q524742\x1b\\";
const TQ_XTVERSION: &str = "\x1b[>0q";
const TQ_DA: &str = "\x1b[c \r";
#[cfg(unix)]
{
if unsafe { libc::isatty(1) } != 1 {
return;
}
}
let mut tquery = String::with_capacity(64);
if extension_enabled("bg", "color", true) {
tquery.push_str(TQ_BGCOLOR);
tquery.push_str(TQ_FGCOLOR);
}
if extension_enabled("cursor", "color", true) {
tquery.push_str(TQ_CURSOR);
}
if extension_enabled("kbprotocol", "kitty", false) {
tquery.push_str(TQ_KITTYKB);
}
if extension_enabled("truecolor", "query", true) {
tquery.push_str(TQ_RGB);
}
if extension_enabled("xtversion", "query", true) {
tquery.push_str(TQ_XTVERSION);
}
tquery.push_str(TQ_DA);
let _ = probe_terminal(&tquery, PROBE_TIMEOUT_MS);
}
fn base64_encode(data: &[u8]) -> String {
const CHARS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
let mut result = String::with_capacity(data.len().div_ceil(3) * 4);
for chunk in data.chunks(3) {
let b0 = chunk[0] as u32;
let b1 = chunk.get(1).copied().unwrap_or(0) as u32;
let b2 = chunk.get(2).copied().unwrap_or(0) as u32;
let n = (b0 << 16) | (b1 << 8) | b2;
result.push(CHARS[((n >> 18) & 63) as usize] as char);
result.push(CHARS[((n >> 12) & 63) as usize] as char);
if chunk.len() > 1 {
result.push(CHARS[((n >> 6) & 63) as usize] as char);
} else {
result.push('=');
}
if chunk.len() > 2 {
result.push(CHARS[(n & 63) as usize] as char);
} else {
result.push('=');
}
}
result
}
pub fn base64_decode(src: &str) -> Vec<u8> {
let bytes = src.as_bytes();
let len = bytes.len();
let mut buf = vec![0u8; (3 * len) / 4 + 1];
let mut i: usize = 0;
let mut b: usize = 0;
while i < len && bytes[i] != b'=' {
let c = bytes[i];
let n: u32 = if c.is_ascii_digit() {
(c - b'0') as u32 + 52
} else if c.is_ascii_lowercase() {
(c - b'a') as u32 + 26
} else if c.is_ascii_uppercase() {
(c - b'A') as u32
} else if c == b'+' {
62
} else if c == b'/' {
63
} else {
0
};
if i % 4 != 0 {
buf[b] |= (n >> (2 * (3 - (i % 4)))) as u8;
b += 1;
}
i += 1; if i >= len {
break; }
if b < buf.len() {
buf[b] = (n << (2 * (i % 4))) as u8;
}
}
buf.truncate(b);
buf }
pub fn handle_paste(seq: &str, len: usize) -> String {
let capture = seq.get(..len).unwrap_or(seq); String::from_utf8_lossy(&base64_decode(capture)).into_owned() }
pub fn url_encode(s: &str) -> String {
let mut result = String::with_capacity(s.len() * 3);
for b in s.bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
result.push(b as char);
}
_ => {
result.push_str(&format!("%{:02X}", b));
}
}
}
result
}
pub fn system_clipget(clip: char) -> Option<String> {
let mut seq = String::from("\x1b]52;.;?\x1b\\"); unsafe {
seq.as_bytes_mut()[5] = clip as u8;
} let reply = probe_terminal(&seq, 200).ok()?; let prefix = format!("\x1b]52;{};", clip);
let rest = reply.strip_prefix(&prefix)?;
let payload_end = rest.find('\x1b').or_else(|| rest.find('\x07'))?;
let b64 = &rest[..payload_end];
let bytes = base64_decode(b64);
String::from_utf8(bytes).ok() }
pub fn system_clipput(data: &str) -> String {
let mut buf = Vec::new();
{
let encoder = base64_encode(data.as_bytes());
buf.extend_from_slice(b"\x1b]52;c;");
buf.extend_from_slice(encoder.as_bytes());
buf.extend_from_slice(b"\x1b\\");
}
String::from_utf8_lossy(&buf).to_string()
}
pub fn extension_enabled(class: &str, ext: &str, def: bool) -> bool {
let elist: Vec<String> = {
let tab = match crate::ported::params::paramtab().read() {
Ok(t) => t,
Err(_) => return def,
};
match tab.get(".term.extensions") {
Some(pm) => pm.u_arr.clone().unwrap_or_default(),
None => Vec::new(),
}
};
for e in elist.iter() {
let (negate, body) = match e.strip_prefix('-') {
Some(rest) => (true, rest),
None => (false, e.as_str()),
};
if !body.starts_with(class) {
continue;
}
let after = &body[class.len()..];
if after.is_empty() {
return !negate;
}
if let Some(rest) = after.strip_prefix(':') {
if rest == ext {
return !negate;
}
}
}
def
}
const EDITEXT: &[(&str, [&str; 2], usize, bool)] = &[
("bracketed-paste", ["", ""], 0, true),
("integration-prompt", ["\x1b]133;B\x1b\\", ""], 11, true),
("modkeys-xterm", ["\x1b[>4;1m", "\x1b[>4m"], 7, false),
];
pub fn collate_seq(sindex: usize, dir: i32) {
let mut seq = Vec::<u8>::with_capacity(256); let max = EDITEXT.len() as i32;
let elist: Vec<String> = {
let tab = match crate::ported::params::paramtab().read() {
Ok(t) => t,
Err(_) => return,
};
match tab.get(".term.extensions") {
Some(pm) => pm.u_arr.clone().unwrap_or_default(),
None => Vec::new(),
}
};
let mut i: i32 = if dir > 0 { 0 } else { max - 1 };
while i >= 0 && i < max {
let (key, seqs, class, default_enabled) = EDITEXT[i as usize];
let mut enabled = default_enabled; if i != 0 && seqs[sindex].is_empty() {
i += dir;
continue;
}
for e in elist.iter() {
let (negate, body) = match e.strip_prefix('-') {
Some(r) => (true, r),
None => (false, e.as_str()),
};
if negate != enabled {
continue;
}
let class_str = &key[..class.min(key.len())];
let after_class = if body.starts_with(class_str) {
&body[class_str.len()..]
} else if body == key {
""
} else {
continue;
};
if after_class.is_empty() || after_class == &key[class..] {
enabled = !negate; break;
}
}
if enabled {
if i != 0 {
seq.extend_from_slice(seqs[sindex].as_bytes()); } else {
let bracket = {
let tab = match crate::ported::params::paramtab().read() {
Ok(t) => t,
Err(_) => {
i += dir;
continue;
}
};
tab.get("zle_bracketed_paste")
.and_then(|pm| pm.u_arr.clone())
.unwrap_or_default()
};
if bracket.len() == 2 {
if let Some(s) = bracket.get(sindex) {
seq.extend_from_slice(s.as_bytes()); }
}
}
}
i += dir;
}
let fd = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::Relaxed);
if fd >= 0 && !seq.is_empty() {
let _ = crate::ported::utils::write_loop(fd, &seq); }
}
pub fn start_edit() -> i32 {
collate_seq(0, 1); 0
}
pub fn end_edit() -> i32 {
collate_seq(1, -1); 0
}
pub fn prompt_markers() -> [String; 3] {
if !extension_enabled("integration", "prompt", true) {
return [String::new(), String::new(), String::new()];
}
if AID.with(|a| a.get()) == 0 {
let host = crate::ported::params::getsparam("HOST").unwrap_or_default();
let h_hash = if host.is_empty() {
0
} else {
crate::ported::hashtable::hasher(&host)
};
let pid = unsafe { libc::getpid() } as u32;
let mut aid = h_hash ^ pid;
if aid == 0 {
aid = 1;
}
AID.with(|a| a.set(aid));
let aid_bytes = aid.to_ne_bytes();
let b64 = base64_encode(&aid_bytes);
PRE_BUFFER.with(|p| {
let mut buf = p.borrow_mut();
let payload = b64.as_bytes();
let n = payload.len().min(6).min(buf.len().saturating_sub(13));
for i in 0..n {
buf[13 + i] = payload[i];
}
});
}
[
"\x1b]133;P;k=i\x1b\\".to_string(), "\x1b]133;P;k=s\x1b\\".to_string(), "\x1b]133;P;k=r\x1b\\".to_string(), ]
}
pub fn mark_output(start: bool) {
const START: &[u8] = b"\x1b]133;C\x1b\\"; const END: &[u8] = b"\x1b]133;D\x1b\\"; if extension_enabled("integration", "output", true) {
let shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
if shtty < 0 {
return;
}
let _ = crate::ported::utils::write_loop(
shtty,
if start { START } else { END },
);
}
}
pub fn write_urlencoded(fd: i32, path_components: &str) {
let enc = url_encode(path_components);
let _ = crate::ported::utils::write_loop(fd, enc.as_bytes());
}
pub fn notify_pwd() {
if !extension_enabled("integration", "pwd", true) {
return;
}
let hostnam = match crate::ported::params::getsparam("HOST") {
Some(h) if !h.contains('/') => h,
_ => return,
};
let pwd = crate::ported::params::getsparam("PWD").unwrap_or_default();
let shtty = crate::ported::init::SHTTY.load(Ordering::Relaxed);
if shtty < 0 {
return;
}
let _ = crate::ported::utils::write_loop(shtty, b"\x1b]7;file://");
write_urlencoded(shtty, &hostnam);
write_urlencoded(shtty, &pwd);
let _ = crate::ported::utils::write_loop(shtty, b"\x1b\\");
}
pub fn match_cursorform(spec: &str) -> u32 {
const SHAPES: &[(&str, u32, u32)] = &[
("none", 0, 0xff),
("underline", CURF_UNDERLINE as u32, CURF_SHAPE_MASK as u32),
("bar", CURF_BAR as u32, CURF_SHAPE_MASK as u32),
("block", CURF_BLOCK as u32, CURF_SHAPE_MASK as u32),
("blink", CURF_BLINK as u32, CURF_STEADY as u32),
("steady", CURF_STEADY as u32, CURF_BLINK as u32),
("hidden", CURF_HIDDEN as u32, 0),
];
let mut cursor_form: u32 = 0; let mut s = spec;
while !s.is_empty() {
let mut found = false;
if let Some(rest) = s.strip_prefix("color=#") {
let mut end = 0;
for (i, ch) in rest.char_indices() {
if ch.is_ascii_hexdigit() {
end = i + ch.len_utf8();
} else {
break;
}
}
let hex_str = &rest[..end];
let col: u32 = u32::from_str_radix(hex_str, 16).unwrap_or(0);
if end == 4 {
let red: u32 = col >> 8;
let green: u32 = (col & 0xf0) >> 4;
let blue: u32 = col & 0xf;
cursor_form &= 0xff; cursor_form |= (CURF_COLOR as u32)
| ((red << 4 | red) << CURF_RED_SHIFT)
| ((green << 4 | green) << CURF_GREEN_SHIFT)
| ((blue << 4 | blue) << CURF_BLUE_SHIFT);
found = true;
} else if end == 6 {
cursor_form |= (col << 8) | (CURF_COLOR as u32); found = true;
}
s = &rest[end..];
}
if !found {
for (name, value, mask) in SHAPES {
if let Some(rest) = s.strip_prefix(name) {
cursor_form &= !*mask; cursor_form |= *value; s = rest;
found = true;
break;
}
}
}
if !found {
match s.find(',') {
Some(idx) => s = &s[idx..],
None => break,
}
}
let mut it = s.chars();
match it.next() {
Some(',') => s = it.as_str(),
_ => break,
}
}
cursor_form
}
pub fn zle_set_cursorform() {
let atrs: Vec<String> = {
let tab = match crate::ported::params::paramtab().read() {
Ok(t) => t,
Err(_) => return,
};
match tab.get("zle_cursorform") {
Some(pm) => pm.u_arr.clone().unwrap_or_default(),
None => Vec::new(),
}
};
const CONTEXTS: [&str; 8] = [
"edit:",
"command:",
"insert:",
"overwrite:",
"pending:",
"regionstart:",
"regionend:",
"visual:",
];
CURSOR_FORMS.with(|cf| {
let mut f = cf.borrow_mut();
f.resize(CURC_DEFAULT as usize, 0);
for slot in f.iter_mut() {
*slot = 0;
}
f[CURC_INSERT as usize] = CURF_BAR as u32;
f[CURC_PENDING as usize] = CURF_UNDERLINE as u32;
});
for spec in atrs.iter() {
if let Some(rest) = spec.strip_prefix("region:") {
let v = match_cursorform(rest);
CURSOR_FORMS.with(|cf| {
let mut f = cf.borrow_mut();
f[CURC_REGION_END as usize] = v;
f[CURC_REGION_START as usize] = v;
});
continue;
}
for (i, ctx) in CONTEXTS.iter().enumerate() {
if let Some(rest) = spec.strip_prefix(ctx) {
let v = match_cursorform(rest);
CURSOR_FORMS.with(|cf| {
cf.borrow_mut()[i] = v;
});
break;
}
}
}
let setup = CURSORFORM_SETUP.with(|s| s.get());
if !setup {
CURSORFORM_SETUP.with(|s| s.set(true));
let mut mask: u32 = 0;
if !extension_enabled("cursor", "shape", true) {
mask |= (CURF_SHAPE_MASK as u32) | (CURF_BLINK as u32) | (CURF_STEADY as u32);
}
if !extension_enabled("cursor", "color", true) {
mask |= CURF_COLOR_MASK;
}
CURSOR_ENABLED_MASK.with(|m| m.set(mask));
}
}
pub fn free_cursor_forms() {
CURSOR_FORMS.with(|cf| cf.borrow_mut().clear());
}
pub fn cursor_form() {
use crate::ported::zle::zle_h::{CURC_COMMAND, CURC_EDIT, CURC_OVERWRITE, CURC_VISUAL};
let forms_empty = CURSOR_FORMS.with(|cf| cf.borrow().is_empty());
if forms_empty {
return; }
let context: u32 = {
let insmode = crate::ported::zle::zle_main::INSMODE
.load(std::sync::atomic::Ordering::SeqCst);
let vichgflag = crate::ported::zle::zle_vi::VICHGFLAG
.load(std::sync::atomic::Ordering::SeqCst);
let region_active = crate::ported::zle::zle_main::REGION_ACTIVE
.load(std::sync::atomic::Ordering::SeqCst);
let in_vicmd = {
let name = crate::ported::zle::zle_keymap::curkeymapname();
*name == "vicmd"
};
if insmode == 0 {
CURC_OVERWRITE as u32
} else if vichgflag == 2 {
CURC_PENDING as u32
} else if region_active != 0 {
if in_vicmd {
CURC_VISUAL as u32
} else {
let mark =
crate::ported::zle::zle_main::MARK.load(std::sync::atomic::Ordering::SeqCst);
let zlecs =
crate::ported::zle::zle_main::ZLECS.load(std::sync::atomic::Ordering::SeqCst);
if mark as usize > zlecs {
CURC_REGION_START as u32
} else {
CURC_REGION_END as u32
}
}
} else if in_vicmd {
CURC_COMMAND as u32
} else if vichgflag != 0 {
CURC_INSERT as u32
} else {
CURC_EDIT as u32
}
};
let want = CURSOR_FORMS.with(|cf| cf.borrow().get(context as usize).copied().unwrap_or(0));
static STATE: std::sync::atomic::AtomicU32 = std::sync::atomic::AtomicU32::new(0);
let prev_state = STATE.load(std::sync::atomic::Ordering::Relaxed);
let disabled = CURSOR_ENABLED_MASK.with(|m| m.get());
let changed = (want ^ prev_state) & !disabled;
if changed == 0 {
return; }
let mut seq = String::new();
if (changed & CURF_HIDDEN as u32) != 0 {
if (want & CURF_HIDDEN as u32) != 0 {
seq.push_str("\x1b[?25l"); } else {
seq.push_str("\x1b[?25h"); }
}
let mut changed_mut = changed;
if (changed_mut & CURF_SHAPE_MASK as u32) != 0 {
let mut c: u8 = b'0';
let shape = want & CURF_SHAPE_MASK as u32;
if shape == CURF_BAR as u32 {
c += 2; c += 2;
c += 2 - (if (want & CURF_BLINK as u32) != 0 { 1 } else { 0 });
} else if shape == CURF_UNDERLINE as u32 {
c += 2; c += 2 - (if (want & CURF_BLINK as u32) != 0 { 1 } else { 0 });
} else if shape == CURF_BLOCK as u32 {
c += 2 - (if (want & CURF_BLINK as u32) != 0 { 1 } else { 0 }); }
changed_mut &= !((CURF_BLINK | CURF_STEADY) as u32);
seq.push_str("\x1b[");
seq.push(c as char);
seq.push_str(" q");
}
if (changed_mut & ((CURF_BLINK | CURF_STEADY) as u32)) != 0 {
seq.push_str(if (want & CURF_BLINK as u32) != 0 {
"\x1b[?12h"
} else {
"\x1b[?12l"
});
}
if (changed_mut & CURF_COLOR_MASK) != 0 {
let mut want_color = want;
if (want & CURF_COLOR_MASK) == 0 {
want_color = memo_cursor.load(std::sync::atomic::Ordering::Relaxed) | (want & 0xff);
}
let r = (want_color >> CURF_RED_SHIFT) & 0xff;
let g = (want_color >> CURF_GREEN_SHIFT) & 0xff;
let b = (want_color >> CURF_BLUE_SHIFT) & 0xff;
seq.push_str(&format!("\x1b]12;rgb:{:02x}00/{:02x}00/{:02x}00\x1b\\", r, g, b));
}
if !seq.is_empty() {
let fd = crate::ported::init::SHTTY.load(std::sync::atomic::Ordering::Relaxed);
if fd >= 0 {
let _ = crate::ported::utils::write_loop(fd, seq.as_bytes()); }
}
STATE.store(want, std::sync::atomic::Ordering::Relaxed); let _ = CURF_COLOR; }
const PROBE_TIMEOUT_MS: u64 = 500;
#[cfg(test)]
mod term_pat_tag_tests {
use super::*;
#[test]
fn tag_high_bit_set() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(TAG, 0x80);
assert_eq!(SEQ, 0xc0);
}
#[test]
fn t_constants_have_high_bit_set() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
for tag in [
T_BEGIN, T_END, T_OR, T_REPEAT, T_NUM, T_HEX, T_HEXCH, T_WILDCARD, T_RECORD, T_CAPTURE,
T_DROP, T_CONTINUE, T_NEXT,
] {
assert!(tag & TAG != 0, "tag 0x{:02x} should have high bit set", tag);
}
}
#[test]
fn timeout_sentinel_negative() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(TIMEOUT, -51);
}
#[test]
fn t_repeat_in_seq_range() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert!((T_BEGIN..=T_HEXCH).contains(&T_REPEAT));
}
}
#[cfg(test)]
mod tests {
use crate::zle::zle_h::CURF_DEFAULT;
use super::*;
#[test]
fn test_url_encode() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(url_encode("/home/user"), "/home/user");
assert_eq!(url_encode("/path with spaces"), "/path%20with%20spaces");
assert_eq!(url_encode("hello&world"), "hello%26world");
}
#[test]
fn curf_constants_match_c() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(CURF_DEFAULT, 0);
assert_eq!(CURF_UNDERLINE, 1);
assert_eq!(CURF_BAR, 2);
assert_eq!(CURF_BLOCK, 3);
assert_eq!(CURF_SHAPE_MASK, 3);
}
#[test]
fn match_cursorform_shapes_set_low_two_bits() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(match_cursorform("underline"), CURF_UNDERLINE as u32);
assert_eq!(match_cursorform("bar"), CURF_BAR as u32);
assert_eq!(match_cursorform("block"), CURF_BLOCK as u32);
assert_eq!(match_cursorform("none"), 0);
}
#[test]
fn match_cursorform_blink_steady_clear_each_other() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(
match_cursorform("blink,steady"),
CURF_STEADY as u32,
"steady should overwrite blink"
);
assert_eq!(
match_cursorform("steady,blink"),
CURF_BLINK as u32,
"blink should overwrite steady"
);
}
#[test]
fn match_cursorform_shape_plus_blink_compose() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let v = match_cursorform("bar,blink");
assert_eq!(v, CURF_BAR as u32 | CURF_BLINK as u32);
}
#[test]
fn match_cursorform_hidden_does_not_clobber_shape() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let v = match_cursorform("block,hidden");
assert_eq!(v, CURF_BLOCK as u32 | CURF_HIDDEN as u32);
}
#[test]
fn match_cursorform_color_4digit_nibble_form() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let v = match_cursorform("color=#0f80");
let expected = (CURF_COLOR as u32)
| (0xff_u32 << CURF_RED_SHIFT)
| (0x88_u32 << CURF_GREEN_SHIFT)
| (0x00_u32 << CURF_BLUE_SHIFT);
assert_eq!(v, expected);
}
#[test]
fn match_cursorform_color_6digit_left_shifts_by_8() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let v = match_cursorform("color=#abcdef");
assert_eq!(v, (0xabcdef_u32 << 8) | CURF_COLOR as u32);
}
#[test]
fn match_cursorform_unknown_component_skips_to_next_comma() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let v = match_cursorform("garbage,bar");
assert_eq!(v, CURF_BAR as u32);
}
#[test]
fn free_cursor_forms_nulls_storage_only() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
zle_set_cursorform();
let setup_before = CURSORFORM_SETUP.with(|s| s.get());
assert!(setup_before, "first zle_set_cursorform should flip setup");
let mask_before = CURSOR_ENABLED_MASK.with(|m| m.get());
free_cursor_forms();
CURSOR_FORMS.with(|cf| assert_eq!(cf.borrow().len(), 0));
assert!(
CURSORFORM_SETUP.with(|s| s.get()),
"free_cursor_forms must NOT reset setup (C doesn't)"
);
assert_eq!(
CURSOR_ENABLED_MASK.with(|m| m.get()),
mask_before,
"free_cursor_forms must NOT clear enabled_mask (C doesn't)"
);
zle_set_cursorform();
CURSOR_FORMS.with(|cf| {
assert_eq!(cf.borrow()[CURC_INSERT as usize], CURF_BAR as u32);
});
}
#[test]
fn prompt_markers_computes_aid_and_splices_into_pre_buffer() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
AID.with(|a| a.set(0));
PRE_BUFFER.with(|p| {
*p.borrow_mut() = b"\x1b]133;A;cl=m;aid=zZZZZZZ\x1b\\".to_vec();
});
crate::ported::params::setsparam("HOST", "testhost");
let _ = prompt_markers();
let aid_after = AID.with(|a| a.get());
assert_ne!(aid_after, 0, "AID should be computed on first call");
let buf = PRE_BUFFER.with(|p| p.borrow().clone());
let spliced = &buf[13..19];
assert_ne!(spliced, b"ZZZZZZ", "pre[13..19] should be overwritten");
let expected_b64 = base64_encode(&aid_after.to_ne_bytes());
assert_eq!(spliced, &expected_b64.as_bytes()[..6]);
let before = AID.with(|a| a.get());
let _ = prompt_markers();
assert_eq!(AID.with(|a| a.get()), before, "AID stable after first call");
}
#[test]
fn prompt_markers_aid_collision_guard() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
AID.with(|a| a.set(0));
crate::ported::params::setsparam("HOST", "");
let _ = prompt_markers();
assert_ne!(
AID.with(|a| a.get()),
0,
"AID must be nonzero after first call regardless of inputs"
);
}
#[test]
fn prompt_markers_shape_when_default_enabled() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let m = prompt_markers();
assert_eq!(m[0], "\x1b]133;P;k=i\x1b\\");
assert_eq!(m[1], "\x1b]133;P;k=s\x1b\\");
assert_eq!(m[2], "\x1b]133;P;k=r\x1b\\");
}
#[test]
fn extension_enabled_matches_whole_class() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
crate::ported::params::setaparam(".term.extensions", vec!["-integration".to_string()]);
assert!(!extension_enabled("integration", "prompt", true));
assert!(!extension_enabled("integration", "pwd", true));
crate::ported::params::setaparam(".term.extensions", vec![]);
}
#[test]
fn extension_enabled_matches_specific_ext() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
crate::ported::params::setaparam(".term.extensions", vec!["-integration:pwd".to_string()]);
assert!(extension_enabled("integration", "prompt", true));
assert!(!extension_enabled("integration", "pwd", true));
crate::ported::params::setaparam(".term.extensions", vec![]);
}
#[test]
fn extension_enabled_respects_default() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
crate::ported::params::setaparam(".term.extensions", vec![]);
assert!(extension_enabled("integration", "prompt", true));
assert!(!extension_enabled("integration", "prompt", false));
}
#[test]
fn zle_set_cursorform_seeds_default_slots() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
zle_set_cursorform();
CURSOR_FORMS.with(|cf| {
let f = cf.borrow();
assert_eq!(f[CURC_INSERT as usize], CURF_BAR as u32);
assert_eq!(f[CURC_PENDING as usize], CURF_UNDERLINE as u32);
assert_eq!(f[CURC_REGION_START as usize], 0);
assert_eq!(f[CURC_REGION_END as usize], 0);
});
}
#[test]
fn test_base64_encode() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(base64_encode(b"hello"), "aGVsbG8=");
assert_eq!(base64_encode(b""), "");
assert_eq!(base64_encode(b"a"), "YQ==");
}
#[test]
fn base64_decode_round_trip_with_encode() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
let encoded = base64_encode(b"hello");
assert_eq!(base64_decode(&encoded), b"hello");
}
#[test]
fn base64_decode_well_known() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(base64_decode("TWFu"), b"Man");
}
#[test]
fn base64_decode_with_padding() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(base64_decode("YQ=="), b"a");
assert_eq!(base64_decode("YWI="), b"ab");
assert_eq!(base64_decode("YWJj"), b"abc");
}
#[test]
fn base64_decode_handles_plus_slash() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(base64_decode("+z/+"), vec![0xfb, 0x3f, 0xfe]);
}
#[test]
fn base64_decode_empty_input() {
let _g = crate::test_util::global_state_lock();
let _g = zle_test_setup();
assert_eq!(base64_decode(""), Vec::<u8>::new());
}
}