#![allow(non_snake_case)]
#![allow(dead_code)]
use std::io::Write;
use unicode_width::UnicodeWidthChar;
use crate::ported::functionbar::Ncurses;
pub const RICHSTRING_MAXLEN: usize = 350;
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug)]
pub struct RichCell {
pub chars: char,
pub attr: i32,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct RichString {
pub chlen: i32,
pub chptr: Vec<RichCell>,
pub highlightAttr: i32,
}
impl RichString {
pub fn new() -> Self {
RichString {
chlen: 0,
chptr: vec![RichCell::default()],
highlightAttr: 0,
}
}
fn iswprint(c: char) -> bool {
!c.is_control()
}
fn isprint(b: u8) -> bool {
(0x20..=0x7e).contains(&b)
}
}
impl Default for RichString {
fn default() -> Self {
Self::new()
}
}
pub fn RichString_setChar(this: &mut RichString, at: usize, ch: char) {
if at >= this.chptr.len() {
this.chptr.resize(at + 1, RichCell::default());
}
this.chptr[at] = RichCell { chars: ch, attr: 0 };
}
pub fn RichString_getCharVal(this: &RichString, i: usize) -> char {
this.chptr[i].chars
}
pub fn RichString_size(this: &RichString) -> i32 {
this.chlen
}
pub fn RichString_sizeVal(this: &RichString) -> i32 {
this.chlen
}
pub fn wcwidth(wc: char) -> i32 {
match UnicodeWidthChar::width(wc) {
Some(w) => w as i32,
None => -1,
}
}
pub fn RichString_extendLen(this: &mut RichString, len: usize) {
RichString_setChar(this, len, '\0');
this.chlen = len as i32;
}
pub fn RichString_setLen(this: &mut RichString, len: usize) {
if len < RICHSTRING_MAXLEN && (this.chlen as usize) < RICHSTRING_MAXLEN {
RichString_setChar(this, len, '\0');
this.chlen = len as i32;
} else {
RichString_extendLen(this, len);
}
}
pub fn RichString_rewind(this: &mut RichString, count: i32) {
RichString_setLen(this, (this.chlen - count) as usize);
}
pub fn mbstowcs_nonfatal(src: &[u8]) -> Vec<char> {
fn utf8_decode_one(b: &[u8]) -> Option<(char, usize)> {
let b0 = b[0];
if b0 < 0x80 {
return Some((b0 as char, 1));
}
let (len, min, init) = if (0xc2..=0xdf).contains(&b0) {
(2usize, 0x80u32, (b0 as u32) & 0x1f)
} else if (0xe0..=0xef).contains(&b0) {
(3, 0x800, (b0 as u32) & 0x0f)
} else if (0xf0..=0xf4).contains(&b0) {
(4, 0x10000, (b0 as u32) & 0x07)
} else {
return None; };
if b.len() < len {
return None;
}
let mut cp = init;
for &bk in &b[1..len] {
if !(0x80..=0xbf).contains(&bk) {
return None;
}
cp = (cp << 6) | ((bk as u32) & 0x3f);
}
if cp < min || cp > 0x10_ffff || (0xd800..=0xdfff).contains(&cp) {
return None;
}
char::from_u32(cp).map(|c| (c, len))
}
let mut out: Vec<char> = Vec::new();
let mut broken = false;
let mut i = 0;
while i < src.len() {
match utf8_decode_one(&src[i..]) {
Some((c, adv)) => {
if c == '\0' {
break; }
broken = false;
out.push(c);
i += adv;
}
None => {
if !broken {
broken = true;
out.push('\u{FFFD}');
}
i += 1;
}
}
}
out
}
pub fn RichString_writeFromWide(
this: &mut RichString,
attrs: i32,
data_c: &[u8],
from: i32,
len: usize,
) -> i32 {
if len < 1 {
return 0;
}
let data = mbstowcs_nonfatal(&data_c[..len.min(data_c.len())]);
let wlen = data.len();
if wlen == 0 {
return 0;
}
let new_len = from as usize + wlen;
RichString_setLen(this, new_len);
let mut j = 0usize;
for i in (from as usize)..new_len {
let c = if RichString::iswprint(data[j]) {
data[j]
} else {
'\u{FFFD}'
};
this.chptr[i] = RichCell {
chars: c,
attr: attrs & 0xffffff,
};
j += 1;
}
wlen as i32
}
pub fn RichString_appendnWideColumns(
this: &mut RichString,
attrs: i32,
data_c: &[u8],
len: usize,
columns: &mut i32,
) -> i32 {
let data = mbstowcs_nonfatal(&data_c[..len.min(data_c.len())]);
let wlen = data.len();
if wlen == 0 {
return 0;
}
let from = this.chlen;
let new_len = from as usize + wlen;
RichString_setLen(this, new_len);
let mut columns_written = 0;
let mut pos = from as usize;
for j in 0..wlen {
let c = if RichString::iswprint(data[j]) {
data[j]
} else {
'\u{FFFD}'
};
let cwidth = wcwidth(c);
if cwidth > *columns {
break;
}
*columns -= cwidth;
columns_written += cwidth;
this.chptr[pos] = RichCell {
chars: c,
attr: attrs & 0xffffff,
};
pos += 1;
}
RichString_setLen(this, pos);
*columns = columns_written;
(pos - from as usize) as i32
}
pub fn RichString_writeFromAscii(
this: &mut RichString,
attrs: i32,
data: &[u8],
from: i32,
len: usize,
) -> i32 {
let new_len = from as usize + len;
RichString_setLen(this, new_len);
let mut j = 0usize;
for i in (from as usize)..new_len {
let b = data[j];
debug_assert!(b <= 0x7f, "RichString ASCII input byte > SCHAR_MAX");
let c = if RichString::isprint(b) {
b as char
} else {
'\u{FFFD}'
};
this.chptr[i] = RichCell {
chars: c,
attr: attrs & 0xffffff,
};
j += 1;
}
len as i32
}
pub fn RichString_setAttrn(this: &mut RichString, attrs: i32, start: usize, charcount: usize) {
let end = (start + charcount).min(this.chlen as usize);
for i in start..end {
this.chptr[i].attr = attrs;
}
}
pub fn RichString_appendChr(this: &mut RichString, attrs: i32, c: char, count: i32) {
let from = this.chlen;
let new_len = from + count;
RichString_setLen(this, new_len as usize);
for i in (from as usize)..(new_len as usize) {
this.chptr[i] = RichCell {
chars: c,
attr: attrs,
};
}
}
pub fn RichString_findChar(this: &RichString, c: char, start: i32) -> i32 {
let wc = c;
let mut i = start;
while i < this.chlen {
if this.chptr[i as usize].chars == wc {
return i;
}
i += 1;
}
-1
}
pub fn RichString_delete(this: &mut RichString) {
if this.chlen > RICHSTRING_MAXLEN as i32 {
this.chptr = vec![RichCell::default()];
this.chlen = 0;
}
}
pub fn RichString_setAttr(this: &mut RichString, attrs: i32) {
RichString_setAttrn(this, attrs, 0, this.chlen as usize);
}
pub fn RichString_appendWide(this: &mut RichString, attrs: i32, data: &[u8]) -> i32 {
let from = this.chlen;
RichString_writeFromWide(this, attrs, data, from, data.len())
}
pub fn RichString_appendnWide(this: &mut RichString, attrs: i32, data: &[u8], len: usize) -> i32 {
let from = this.chlen;
RichString_writeFromWide(this, attrs, data, from, len)
}
pub fn RichString_writeWide(this: &mut RichString, attrs: i32, data: &[u8]) -> i32 {
RichString_writeFromWide(this, attrs, data, 0, data.len())
}
pub fn RichString_appendAscii(this: &mut RichString, attrs: i32, data: &[u8]) -> i32 {
let from = this.chlen;
RichString_writeFromAscii(this, attrs, data, from, data.len())
}
pub fn RichString_appendnAscii(this: &mut RichString, attrs: i32, data: &[u8], len: usize) -> i32 {
let from = this.chlen;
RichString_writeFromAscii(this, attrs, data, from, len)
}
pub fn RichString_writeAscii(this: &mut RichString, attrs: i32, data: &[u8]) -> i32 {
RichString_writeFromAscii(this, attrs, data, 0, data.len())
}
pub fn RichString_printoffnVal<W: Write>(
out: &mut W,
this: &RichString,
y: i32,
x: i32,
off: i32,
n: i32,
) {
for k in 0..n {
let idx = (off + k) as usize;
if idx >= this.chptr.len() {
break;
}
let cell = this.chptr[idx];
Ncurses::attrset(out, cell.attr);
Ncurses::mvaddch(out, y, x + k, cell.chars);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn ascii(s: &str) -> RichString {
let mut r = RichString::new();
RichString_appendAscii(&mut r, 0, s.as_bytes());
r
}
fn text(r: &RichString) -> String {
(0..r.chlen as usize).map(|i| r.chptr[i].chars).collect()
}
#[test]
fn new_is_empty_with_terminator() {
let r = RichString::new();
assert_eq!(r.chlen, 0);
assert_eq!(r.highlightAttr, 0);
assert_eq!(r.chptr[0], RichCell::default());
assert_eq!(r.chptr[0].chars, '\0');
}
#[test]
fn terminator_follows_valid_range() {
let mut r = RichString::new();
RichString_appendAscii(&mut r, 0, b"abc");
assert_eq!(r.chlen, 3);
assert_eq!(r.chptr[r.chlen as usize].chars, '\0');
}
#[test]
fn append_ascii_stores_each_byte_verbatim() {
let mut r = RichString::new();
let n = RichString_appendAscii(&mut r, 7, b"Hi!");
assert_eq!(n, 3);
assert_eq!(text(&r), "Hi!");
for i in 0..3 {
assert_eq!(r.chptr[i].chars, "Hi!".as_bytes()[i] as char);
assert_eq!(r.chptr[i].attr, 7);
}
}
#[test]
fn append_ascii_replaces_nonprintable_with_replacement_char() {
let mut r = RichString::new();
RichString_appendAscii(&mut r, 0, &[b'a', 0x1f, b'b', 0x7f]);
assert_eq!(text(&r), "a\u{FFFD}b\u{FFFD}");
}
#[test]
fn append_ascii_appends_after_existing() {
let mut r = ascii("foo");
RichString_appendAscii(&mut r, 0, b"bar");
assert_eq!(text(&r), "foobar");
assert_eq!(r.chlen, 6);
}
#[test]
fn write_ascii_overwrites_from_zero() {
let mut r = ascii("longtext");
let n = RichString_writeAscii(&mut r, 0, b"hi");
assert_eq!(n, 2);
assert_eq!(text(&r), "hi");
assert_eq!(r.chlen, 2);
}
#[test]
fn appendn_ascii_honors_len() {
let mut r = RichString::new();
let n = RichString_appendnAscii(&mut r, 0, b"abcdef", 3);
assert_eq!(n, 3);
assert_eq!(text(&r), "abc");
}
#[test]
fn write_from_ascii_masks_attr_to_24_bits() {
let mut r = RichString::new();
RichString_appendAscii(&mut r, 0x1234_5678, b"x");
assert_eq!(r.chptr[0].attr, 0x0034_5678); }
#[test]
fn append_chr_stores_attr_verbatim_no_mask() {
let mut r = RichString::new();
RichString_appendChr(&mut r, 0x1234_5678, '=', 3);
assert_eq!(text(&r), "===");
for i in 0..3 {
assert_eq!(r.chptr[i].attr, 0x1234_5678); assert_eq!(r.chptr[i].chars, '=');
}
}
#[test]
fn append_chr_does_not_filter_nonprintable() {
let mut r = RichString::new();
RichString_appendChr(&mut r, 0, '\u{1}', 2);
assert_eq!(r.chptr[0].chars, '\u{1}');
assert_eq!(r.chptr[1].chars, '\u{1}');
}
#[test]
fn set_attrn_changes_only_the_range_and_keeps_chars() {
let mut r = ascii("abcdef");
RichString_setAttrn(&mut r, 0x1234_5678, 2, 3); for i in 0..6 {
let expect = if (2..5).contains(&i) { 0x1234_5678 } else { 0 };
assert_eq!(r.chptr[i].attr, expect, "attr at {i}");
}
assert_eq!(text(&r), "abcdef"); }
#[test]
fn set_attrn_clamps_range_to_chlen() {
let mut r = ascii("abc");
RichString_setAttrn(&mut r, 9, 2, 100);
assert_eq!(r.chptr[2].attr, 9);
RichString_setAttrn(&mut r, 5, 10, 4);
}
#[test]
fn set_attr_applies_to_whole_string() {
let mut r = ascii("abcd");
RichString_setAttr(&mut r, 42);
for i in 0..4 {
assert_eq!(r.chptr[i].attr, 42);
}
}
#[test]
fn append_wide_decodes_multibyte_codepoints() {
let mut r = RichString::new();
let n = RichString_appendWide(&mut r, 0, "héllo日本".as_bytes());
assert_eq!(text(&r), "héllo日本");
assert_eq!(n, 7); assert_eq!(r.chlen, 7);
}
#[test]
fn write_wide_empty_is_noop_does_not_clear() {
let mut r = ascii("keep");
let n = RichString_writeWide(&mut r, 0, b"");
assert_eq!(n, 0);
assert_eq!(text(&r), "keep");
}
#[test]
fn wide_replaces_control_chars_with_replacement() {
let mut r = RichString::new();
RichString_appendWide(&mut r, 0, &[b'a', 0x0a, b'b']); assert_eq!(text(&r), "a\u{FFFD}b");
}
#[test]
fn mbstowcs_decodes_valid_utf8() {
assert_eq!(mbstowcs_nonfatal("aé日".as_bytes()), vec!['a', 'é', '日']);
}
#[test]
fn mbstowcs_one_replacement_per_broken_run() {
assert_eq!(
mbstowcs_nonfatal(&[b'a', 0xff, 0xfe, b'b']),
vec!['a', '\u{FFFD}', 'b']
);
assert_eq!(
mbstowcs_nonfatal(&[0xff, b'x', 0xfe]),
vec!['\u{FFFD}', 'x', '\u{FFFD}']
);
}
#[test]
fn mbstowcs_stops_at_nul() {
assert_eq!(mbstowcs_nonfatal(&[b'a', b'b', 0x00, b'c']), vec!['a', 'b']);
}
#[test]
fn mbstowcs_rejects_overlong_and_surrogate() {
assert_eq!(mbstowcs_nonfatal(&[0xc0, 0xaf]), vec!['\u{FFFD}']);
assert_eq!(mbstowcs_nonfatal(&[0xed, 0xa0, 0x80]), vec!['\u{FFFD}']);
}
#[test]
fn wcwidth_narrow_wide_combining_control() {
assert_eq!(wcwidth('A'), 1); assert_eq!(wcwidth(' '), 1);
assert_eq!(wcwidth('é'), 1); assert_eq!(wcwidth('世'), 2); assert_eq!(wcwidth('本'), 2);
assert_eq!(wcwidth('\u{1F600}'), 2); assert_eq!(wcwidth('\u{0301}'), 0); assert_eq!(wcwidth('\u{200B}'), 0); assert_eq!(wcwidth('\n'), -1); assert_eq!(wcwidth('\0'), -1);
}
#[test]
fn append_wide_columns_respects_budget_narrow_text() {
let mut r = RichString::new();
let mut cols = 3;
let n = RichString_appendnWideColumns(&mut r, 5, b"abcde", 5, &mut cols);
assert_eq!(n, 3); assert_eq!(cols, 3); assert_eq!(text(&r), "abc");
assert_eq!(r.chlen, 3);
assert_eq!(r.chptr[0].attr, 5);
}
#[test]
fn append_wide_columns_zero_budget_writes_nothing() {
let mut r = RichString::new();
let mut cols = 0;
let n = RichString_appendnWideColumns(&mut r, 0, b"abc", 3, &mut cols);
assert_eq!(n, 0);
assert_eq!(cols, 0);
assert_eq!(r.chlen, 0);
}
#[test]
fn append_wide_columns_all_fit() {
let mut r = RichString::new();
let mut cols = 100;
let n = RichString_appendnWideColumns(&mut r, 0, b"hello", 5, &mut cols);
assert_eq!(n, 5);
assert_eq!(cols, 5);
assert_eq!(text(&r), "hello");
}
#[test]
fn append_wide_columns_wide_char_costs_two_columns() {
let mut r = RichString::new();
let mut cols = 4;
let n = RichString_appendnWideColumns(&mut r, 0, "世界".as_bytes(), 6, &mut cols);
assert_eq!(n, 2); assert_eq!(cols, 4); assert_eq!(text(&r), "世界");
}
#[test]
fn append_wide_columns_wide_char_breaks_on_odd_budget() {
let mut r = RichString::new();
let mut cols = 3;
let n = RichString_appendnWideColumns(&mut r, 0, "世界".as_bytes(), 6, &mut cols);
assert_eq!(n, 1); assert_eq!(cols, 2); assert_eq!(text(&r), "世");
assert_eq!(r.chlen, 1);
}
#[test]
fn append_wide_columns_wide_char_needs_two_columns_min() {
let mut r = RichString::new();
let mut cols = 1;
let n = RichString_appendnWideColumns(&mut r, 0, "世".as_bytes(), 3, &mut cols);
assert_eq!(n, 0);
assert_eq!(cols, 0);
assert_eq!(r.chlen, 0);
}
#[test]
fn append_wide_columns_combining_mark_costs_zero() {
let mut r = RichString::new();
let mut cols = 1;
let n = RichString_appendnWideColumns(&mut r, 0, "e\u{0301}".as_bytes(), 3, &mut cols);
assert_eq!(n, 2); assert_eq!(cols, 1); assert_eq!(text(&r), "e\u{0301}");
}
#[test]
fn set_char_writes_cell_and_zeroes_attr() {
let mut r = ascii("abc");
r.chptr[1].attr = 99; RichString_setChar(&mut r, 1, 'Z');
assert_eq!(r.chptr[1].chars, 'Z');
assert_eq!(r.chptr[1].attr, 0); }
#[test]
fn set_char_grows_buffer_on_demand() {
let mut r = RichString::new();
RichString_setChar(&mut r, 500, 'q'); assert!(r.chptr.len() >= 501);
assert_eq!(r.chptr[500].chars, 'q');
}
#[test]
fn get_char_val_returns_primary_codepoint() {
let mut r = RichString::new();
RichString_appendWide(&mut r, 0, "a世b".as_bytes());
assert_eq!(RichString_getCharVal(&r, 0), 'a');
assert_eq!(RichString_getCharVal(&r, 1), '世');
assert_eq!(RichString_getCharVal(&r, 2), 'b');
}
#[test]
fn size_and_size_val_return_chlen() {
let r = ascii("hello");
assert_eq!(RichString_size(&r), 5);
assert_eq!(RichString_sizeVal(&r), 5);
assert_eq!(RichString_size(&RichString::new()), 0);
}
#[test]
fn rewind_shortens_string() {
let mut r = ascii("abcdef");
RichString_rewind(&mut r, 2);
assert_eq!(r.chlen, 4);
assert_eq!(text(&r), "abcd");
assert_eq!(r.chptr[r.chlen as usize].chars, '\0'); }
#[test]
fn extend_len_beyond_maxlen_grows_buffer() {
let mut r = RichString::new();
let big = RICHSTRING_MAXLEN + 10;
RichString_extendLen(&mut r, big);
assert_eq!(r.chlen, big as i32);
assert!(r.chptr.len() >= big + 1);
assert_eq!(r.chptr[big].chars, '\0'); }
#[test]
fn set_len_switches_to_extend_at_maxlen() {
let mut r = RichString::new();
let data = vec![b'z'; RICHSTRING_MAXLEN + 5];
let n = RichString_appendAscii(&mut r, 0, &data);
assert_eq!(n, (RICHSTRING_MAXLEN + 5) as i32);
assert_eq!(r.chlen, (RICHSTRING_MAXLEN + 5) as i32);
assert_eq!(text(&r).len(), RICHSTRING_MAXLEN + 5);
}
#[test]
fn delete_releases_overflow_buffer() {
let mut r = RichString::new();
RichString_appendAscii(&mut r, 0, &vec![b'x'; RICHSTRING_MAXLEN + 20]);
assert!(r.chlen > RICHSTRING_MAXLEN as i32);
RichString_delete(&mut r);
assert_eq!(r.chlen, 0);
assert_eq!(r.chptr, vec![RichCell::default()]);
}
#[test]
fn delete_noop_when_within_internal_buffer() {
let mut r = ascii("small");
RichString_delete(&mut r);
assert_eq!(r.chlen, 5);
assert_eq!(text(&r), "small");
}
#[test]
fn find_char_first_match_from_start_zero() {
let s = ascii("hello");
assert_eq!(RichString_findChar(&s, 'l', 0), 2);
assert_eq!(RichString_findChar(&s, 'h', 0), 0);
assert_eq!(RichString_findChar(&s, 'o', 0), 4);
}
#[test]
fn find_char_start_offset_skips_earlier() {
let s = ascii("hello");
assert_eq!(RichString_findChar(&s, 'l', 3), 3);
assert_eq!(RichString_findChar(&s, 'l', 4), -1);
}
#[test]
fn find_char_absent_and_empty_return_minus_one() {
assert_eq!(RichString_findChar(&ascii("hello"), 'z', 0), -1);
assert_eq!(RichString_findChar(&RichString::new(), 'a', 0), -1);
}
#[test]
fn find_char_start_at_or_past_end() {
let s = ascii("abc");
assert_eq!(RichString_findChar(&s, 'c', 3), -1);
assert_eq!(RichString_findChar(&s, 'a', 10), -1);
assert_eq!(RichString_findChar(&s, 'c', 2), 2);
}
#[test]
fn find_char_matches_wide_codepoint() {
let mut r = RichString::new();
RichString_appendWide(&mut r, 0, "a日b".as_bytes());
assert_eq!(RichString_findChar(&r, '日', 0), 1);
assert_eq!(RichString_findChar(&r, 'b', 0), 2);
}
fn printed_chars(buf: &[u8]) -> String {
let s = String::from_utf8(buf.to_vec()).unwrap();
let mut out = String::new();
let mut chars = s.chars();
while let Some(c) = chars.next() {
if c == '\u{1b}' {
let intro = chars.next();
if intro != Some('[') {
continue;
}
for e in chars.by_ref() {
if ('\u{40}'..='\u{7e}').contains(&e) {
break;
}
}
} else {
out.push(c);
}
}
out
}
#[test]
fn printoffn_blits_full_range() {
let r = ascii("hello");
let mut buf: Vec<u8> = Vec::new();
RichString_printoffnVal(&mut buf, &r, 0, 0, 0, 5);
assert_eq!(printed_chars(&buf), "hello");
}
#[test]
fn printoffn_honors_offset_and_count() {
let r = ascii("abcdef");
let mut buf: Vec<u8> = Vec::new();
RichString_printoffnVal(&mut buf, &r, 0, 0, 2, 3);
assert_eq!(printed_chars(&buf), "cde");
}
#[test]
fn printoffn_stops_at_buffer_end() {
let r = ascii("abc");
let mut buf: Vec<u8> = Vec::new();
RichString_printoffnVal(&mut buf, &r, 0, 0, 0, 100);
assert!(printed_chars(&buf).starts_with("abc"));
}
#[test]
fn printoffn_zero_count_emits_nothing() {
let r = ascii("abc");
let mut buf: Vec<u8> = Vec::new();
RichString_printoffnVal(&mut buf, &r, 0, 0, 0, 0);
assert!(buf.is_empty());
}
#[test]
fn printoffn_blits_wide_codepoints() {
let mut r = RichString::new();
RichString_appendWide(&mut r, 0, "a世b".as_bytes());
let mut buf: Vec<u8> = Vec::new();
RichString_printoffnVal(&mut buf, &r, 0, 0, 0, 3);
assert_eq!(printed_chars(&buf), "a世b");
}
}