use std::cell::RefCell;
use std::collections::VecDeque;
use std::io::{self, BufRead, BufReader, Read};
use std::io::Write;
#[derive(Clone, Default)]
#[allow(non_camel_case_types)]
struct instacks { buf: String, bufpos: usize, flags: i32, alias: Option<String>, }
#[allow(dead_code)]
const INSTACK_INITIAL: usize = 4;
use crate::ported::zsh_h::{
INP_ALCONT, INP_ALIAS, INP_APPEND, INP_CONT, INP_FREE, INP_HIST,
INP_HISTCONT, INP_LINENO, INP_RAW_KEEP,
};
pub fn shinbufreset() { shinbuffer.with(|b| b.borrow_mut().clear());
shinbufpos.with(|p| p.set(0));
}
thread_local! {
#[allow(non_upper_case_globals)]
pub static SHIN: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
#[allow(non_upper_case_globals)]
pub static strin: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
#[allow(non_upper_case_globals)]
pub static inbufct: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
#[allow(non_upper_case_globals)]
pub static inbufflags: std::cell::Cell<i32> = const { std::cell::Cell::new(0) };
#[allow(non_upper_case_globals)]
static inbuf: RefCell<String> = const { RefCell::new(String::new()) };
#[allow(non_upper_case_globals)]
static inbufpos: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
static instack: RefCell<Vec<instacks>> = const { RefCell::new(Vec::new()) };
#[allow(non_upper_case_globals)]
pub static lexstop: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
#[allow(non_upper_case_globals)]
pub static lineno: std::cell::Cell<usize> = const { std::cell::Cell::new(1) };
#[allow(non_upper_case_globals)]
static shinbuffer: RefCell<String> = const { RefCell::new(String::new()) };
#[allow(non_upper_case_globals)]
static shinbufpos: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
static shinsavestack: RefCell<Vec<(String, usize)>> = const { RefCell::new(Vec::new()) };
static pushback: RefCell<VecDeque<char>> = const { RefCell::new(VecDeque::new()) };
static raw_input: RefCell<String> = const { RefCell::new(String::new()) };
}
pub fn shinbufalloc() { shinbuffer.with(|b| {
*b.borrow_mut() = String::with_capacity(SHIN_BUF_SIZE);
});
shinbufreset();
}
pub fn shinbufsave() { let (snap_buf, snap_pos) = (
shinbuffer.with(|b| std::mem::take(&mut *b.borrow_mut())),
shinbufpos.with(|p| p.replace(0)),
);
shinsavestack.with(|s| s.borrow_mut().push((snap_buf, snap_pos)));
shinbufalloc();
}
pub fn shinbufrestore() { if let Some((buf, pos)) = shinsavestack.with(|s| s.borrow_mut().pop()) {
shinbuffer.with(|b| *b.borrow_mut() = buf);
shinbufpos.with(|p| p.set(pos));
}
}
pub fn shingetchar() -> i32 { let bufd = shinbuffer.with(|b| b.borrow().clone());
let pos = shinbufpos.with(|p| p.get());
if pos < bufd.len() {
if let Some(ch) = bufd.chars().nth(pos) {
shinbufpos.with(|p| p.set(pos + 1));
return ch as i32;
}
}
shinbufreset();
let stdin = std::io::stdin();
let mut reader = BufReader::new(stdin.lock());
let mut line = String::new();
match reader.read_line(&mut line) {
Ok(0) => -1,
Ok(_) => {
let first = line.chars().next().map(|c| c as i32).unwrap_or(-1);
shinbuffer.with(|b| *b.borrow_mut() = line);
shinbufpos.with(|p| p.set(1));
first
}
Err(_) => -1,
}
}
pub fn shingetline() -> String { let mut result = String::new();
loop {
match shingetchar() {
-1 => return result,
ch_i32 => {
let c = char::from_u32(ch_i32 as u32).unwrap_or('\0');
if c == '\n' {
result.push('\n');
return result;
}
if imeta(c) {
result.push(META);
result.push(char::from_u32((c as u32) ^ 32).unwrap_or(c));
} else {
result.push(c);
}
}
}
}
}
pub fn ingetc() -> Option<char> { if lexstop.with(|c| c.get()) {
return Some(' ');
}
if let Some(c) = pushback.with(|p| p.borrow_mut().pop_front()) {
raw_input.with(|r| r.borrow_mut().push(c));
return Some(c);
}
loop {
let pos = inbufpos.with(|p| p.get());
let buf = inbuf.with(|b| b.borrow().clone());
if pos < buf.len() {
let c = buf.chars().nth(pos)?;
inbufpos.with(|p| p.set(pos + 1));
inbufct.with(|c| c.set(c.get().saturating_sub(1)));
let cu32 = c as u32;
if cu32 < 256
&& crate::ported::ztype_h::itok(cu32 as u8)
{
continue;
}
let inp_lineno =
(inbufflags.with(|f| f.get()) & INP_LINENO) != 0;
let is_strin = strin.with(|s| s.get()) != 0;
if (inp_lineno || !is_strin) && c == '\n' {
lineno.with(|l| l.set(l.get() + 1));
}
raw_input.with(|r| r.borrow_mut().push(c));
return Some(c);
}
let ct = inbufct.with(|c| c.get());
let is_strin = strin.with(|s| s.get()) != 0;
let is_stop = lexstop.with(|c| c.get());
if ct == 0 && (is_strin || is_stop) {
lexstop.with(|c| c.set(true));
return None;
}
if (inbufflags.with(|f| f.get()) & INP_CONT) != 0 {
inpoptop();
continue;
}
lexstop.with(|c| c.set(true));
return None;
}
}
pub fn inputline() -> String { let line = shingetline();
if line.is_empty() {
lexstop.with(|c| c.set(true));
}
line
}
pub fn inputsetline(str: &str, flags: i32) { inbuf.with(|b| *b.borrow_mut() = str.to_string());
inbufpos.with(|p| p.set(0));
let len = str.len() as i32;
if (flags & INP_CONT) != 0 {
inbufct.with(|c| c.set(c.get() + len));
} else {
inbufct.with(|c| c.set(len));
}
inbufflags.with(|f| f.set(flags));
}
pub fn inungetc(c: char) { if lexstop.with(|c| c.get()) {
return;
}
let pos = inbufpos.with(|p| p.get());
if pos > 0 {
inbufpos.with(|p| p.set(pos - 1));
inbufct.with(|cell| cell.set(cell.get() + 1));
let inp_lineno =
(inbufflags.with(|f| f.get()) & INP_LINENO) != 0;
let is_strin = strin.with(|s| s.get()) != 0;
if (inp_lineno || !is_strin) && c == '\n' {
lineno.with(|l| l.set(l.get().saturating_sub(1)));
}
raw_input.with(|r| { r.borrow_mut().pop(); });
} else {
pushback.with(|p| p.borrow_mut().push_front(c));
}
}
pub fn zstuff(path: &str) -> io::Result<String> { std::fs::read_to_string(path)
}
pub fn stuff(filename: &str) -> i32 { let buf = match std::fs::read_to_string(filename) {
Ok(b) => b,
Err(_) => return 1,
};
let _ = std::io::stderr().write_all(buf.as_bytes());
let _ = std::io::stderr().flush();
inpush(&buf, INP_FREE, None);
0
}
pub fn inerrflush() { while !lexstop.with(|c| c.get()) && inbufct.with(|c| c.get()) > 0 {
let _ = ingetc();
}
}
pub fn inpush(str: &str, flags: i32, inalias: Option<String>) { let saved = instacks {
buf: inbuf.with(|b| std::mem::take(&mut *b.borrow_mut())),
bufpos: inbufpos.with(|p| p.replace(0)),
flags: inbufflags.with(|f| f.get()),
alias: None,
};
instack.with(|st| st.borrow_mut().push(saved));
inbuf.with(|b| *b.borrow_mut() = str.to_string());
inbufpos.with(|p| p.set(0));
let mut combined = flags;
if (flags & (INP_ALIAS | INP_HIST)) != 0 {
combined |= INP_CONT | INP_ALIAS;
if let Some(a) = inalias {
instack.with(|st| {
if let Some(last) = st.borrow_mut().last_mut() {
last.alias = Some(a);
if (flags & INP_HIST) != 0 {
last.flags |= INP_HISTCONT;
} else {
last.flags |= INP_ALCONT;
}
}
});
}
}
let new_len = inbuf.with(|b| b.borrow().len()) as i32;
if (combined & INP_CONT) != 0 {
inbufct.with(|c| c.set(c.get() + new_len));
} else {
inbufct.with(|c| c.set(new_len));
}
inbufflags.with(|f| f.set(combined));
}
pub fn inpoptop() { if let Some(entry) = instack.with(|st| st.borrow_mut().pop()) {
inbuf.with(|b| *b.borrow_mut() = entry.buf);
inbufpos.with(|p| p.set(entry.bufpos));
inbufflags.with(|f| f.set(entry.flags));
let remaining = inbuf.with(|b| b.borrow().len())
.saturating_sub(entry.bufpos) as i32;
inbufct.with(|c| c.set(remaining));
}
}
pub fn inpop() { loop {
let was_cont = (inbufflags.with(|f| f.get()) & INP_CONT) != 0;
inpoptop();
if !was_cont {
break;
}
}
}
pub fn inpopalias() { while (inbufflags.with(|f| f.get()) & INP_ALIAS) != 0 {
inpoptop();
}
}
pub fn ingetptr() -> String { let pos = inbufpos.with(|p| p.get());
inbuf.with(|b| b.borrow().get(pos..).map(str::to_string).unwrap_or_default())
}
const SHIN_BUF_SIZE: usize = 8192;
pub use crate::ported::zsh_h::META;
fn imeta(c: char) -> bool { let b = c as u32;
if b > 0xff { return false; }
crate::ported::ztype_h::imeta(b as u8)
}
#[cfg(test)]
mod tests {
use super::*;
fn reset_input() {
super::inbuf.with(|b| b.borrow_mut().clear());
super::inbufpos.with(|p| p.set(0));
super::inbufct.with(|c| c.set(0));
super::inbufflags.with(|f| f.set(0));
super::lexstop.with(|c| c.set(false));
super::lineno.with(|l| l.set(1));
super::instack.with(|st| st.borrow_mut().clear());
super::pushback.with(|p| p.borrow_mut().clear());
super::raw_input.with(|r| r.borrow_mut().clear());
}
#[test]
fn test_input_buffer_basic() {
reset_input();
inputsetline("hello", 0);
assert_eq!(ingetc(), Some('h'));
assert_eq!(ingetc(), Some('e'));
assert_eq!(ingetc(), Some('l'));
assert_eq!(ingetc(), Some('l'));
assert_eq!(ingetc(), Some('o'));
assert_eq!(ingetc(), None);
}
#[test]
fn test_input_ungetc() {
reset_input();
inputsetline("abc", 0);
assert_eq!(ingetc(), Some('a'));
assert_eq!(ingetc(), Some('b'));
inungetc('b');
assert_eq!(ingetc(), Some('b'));
assert_eq!(ingetc(), Some('c'));
}
#[test]
fn test_input_stack() {
reset_input();
inputsetline("outer", 0);
assert_eq!(ingetc(), Some('o'));
inpush("inner", INP_CONT, None);
assert_eq!(ingetc(), Some('i'));
assert_eq!(ingetc(), Some('n'));
assert_eq!(ingetc(), Some('n'));
assert_eq!(ingetc(), Some('e'));
assert_eq!(ingetc(), Some('r'));
assert_eq!(ingetc(), Some('u'));
assert_eq!(ingetc(), Some('t'));
}
#[test]
fn test_line_number_tracking() {
reset_input();
inputsetline("a\nb\nc", INP_LINENO);
assert_eq!(lineno.with(|l| l.get()), 1);
ingetc(); ingetc(); assert_eq!(lineno.with(|l| l.get()), 2);
ingetc(); ingetc(); assert_eq!(lineno.with(|l| l.get()), 3);
}
#[test]
fn test_meta_encoding() {
let _g = crate::ported::ztype_h::TYPTAB_TEST_LOCK
.lock().unwrap_or_else(|e| e.into_inner());
crate::ported::utils::inittyptab();
assert!(imeta('\x00'));
assert!(imeta('\u{83}'));
assert!(imeta('\u{a2}'));
assert!(imeta('\u{84}'));
assert!(imeta('\u{9c}'));
assert!(imeta('\u{9d}'));
assert!(imeta('\u{a1}'));
assert!(!imeta('a'));
assert!(!imeta('Z'));
assert!(!imeta('0'));
assert!(!imeta('\x01'));
assert!(!imeta('\x1f'));
assert!(!imeta('\u{a3}'));
let encoded = char::from_u32(('\x00' as u32) ^ 32).unwrap_or('\x00');
let decoded = char::from_u32((encoded as u32) ^ 32).unwrap_or(encoded);
assert_eq!(decoded, '\x00');
}
#[test]
fn test_ingetptr() {
reset_input();
inputsetline("hello world", 0);
ingetc(); ingetc(); ingetc(); ingetc(); ingetc(); assert_eq!(ingetptr(), " world");
}
#[test]
fn test_inerrflush() {
reset_input();
inputsetline("remaining input", 0);
ingetc();
inerrflush();
assert!(lexstop.with(|c| c.get()) || inbufct.with(|c| c.get()) == 0);
}
#[test]
fn shinbufreset_clears_buffer_and_zeros_pos() {
super::shinbuffer.with(|b| {
*b.borrow_mut() = "leftover".to_string();
});
super::shinbufpos.with(|p| p.set(7));
super::shinbufreset();
super::shinbuffer.with(|b| {
assert!(b.borrow().is_empty(),
"c:161 — shinbuffer must be empty after reset");
});
assert_eq!(super::shinbufpos.with(|p| p.get()), 0,
"c:161 — shinbufpos must be 0 after reset");
}
#[test]
fn shinbufalloc_resets_and_capacity_hints() {
super::shinbuffer.with(|b| {
*b.borrow_mut() = "stale".to_string();
});
super::shinbufpos.with(|p| p.set(3));
super::shinbufalloc();
super::shinbuffer.with(|b| {
assert!(b.borrow().is_empty(),
"c:173 — fresh shinbuffer must be empty");
assert!(b.borrow().capacity() >= 1);
});
assert_eq!(super::shinbufpos.with(|p| p.get()), 0);
}
#[test]
fn shinbufsave_restore_round_trip() {
super::shinsavestack.with(|s| s.borrow_mut().clear());
super::shinbuffer.with(|b| *b.borrow_mut() = "abc".to_string());
super::shinbufpos.with(|p| p.set(2));
super::shinbufsave();
super::shinbuffer.with(|b| {
assert!(b.borrow().is_empty(),
"c:193 — shinbufsave invokes shinbufalloc (resets to empty)");
});
assert_eq!(super::shinbufpos.with(|p| p.get()), 0,
"c:193 — pos must be 0 after save");
super::shinbufrestore();
super::shinbuffer.with(|b| {
assert_eq!(*b.borrow(), "abc",
"c:200-209 — shinbufrestore restores saved buffer");
});
assert_eq!(super::shinbufpos.with(|p| p.get()), 2,
"c:200-209 — shinbufrestore restores saved pos");
}
#[test]
fn shinbufrestore_on_empty_stack_is_noop() {
super::shinsavestack.with(|s| s.borrow_mut().clear());
super::shinbuffer.with(|b| *b.borrow_mut() = "persist".to_string());
super::shinbufrestore();
super::shinbuffer.with(|b| {
assert_eq!(*b.borrow(), "persist",
"empty-stack restore must leave buffer untouched");
});
}
#[test]
fn ingetc_skips_token_bytes_via_itok_predicate() {
reset_input();
crate::ported::utils::inittyptab();
let bang: char = '\u{009c}'; let nularg: char = '\u{00a1}'; let mut s = String::new();
s.push('a');
s.push(bang);
s.push('b');
s.push(nularg);
s.push('c');
inputsetline(&s, 0);
assert_eq!(ingetc(), Some('a'));
assert_eq!(ingetc(), Some('b'),
"c:328 — Bang (0x9c) must be skipped (ITOK bit set per inittyptab)");
assert_eq!(ingetc(), Some('c'),
"c:328 — Nularg (0xa1) must be skipped (ITOK bit set per inittyptab)");
}
#[test]
fn ingetc_does_not_skip_imeta_only_bytes() {
reset_input();
crate::ported::utils::inittyptab();
let meta: char = '\u{0083}'; let marker: char = '\u{00a2}'; let mut s = String::new();
s.push('x');
s.push(meta);
s.push('y');
s.push(marker);
s.push('z');
inputsetline(&s, 0);
assert_eq!(ingetc(), Some('x'));
assert_eq!(ingetc(), Some(meta),
"c:328 — Meta (0x83) is IMETA-only, NOT ITOK; must pass through");
assert_eq!(ingetc(), Some('y'));
assert_eq!(ingetc(), Some(marker),
"c:328 / c:4197 — Marker (0xa2) is IMETA-only, NOT ITOK; must pass through");
assert_eq!(ingetc(), Some('z'));
}
}