use crate::base::Size;
#[derive(Debug, Default)]
pub(crate) struct Utf16Decoder {
pending_high: Option<u16>,
}
impl Utf16Decoder {
pub(crate) fn push(&mut self, unit: u16, out: &mut Vec<u8>) {
if let Some(high) = self.pending_high.take() {
if (0xdc00..=0xdfff).contains(&unit) {
let c =
0x10000u32 + ((u32::from(high) - 0xd800) << 10) + (u32::from(unit) - 0xdc00);
push_char(
char::from_u32(c).unwrap_or(char::REPLACEMENT_CHARACTER),
out,
);
return;
}
push_char(char::REPLACEMENT_CHARACTER, out);
}
match unit {
0xd800..=0xdbff => self.pending_high = Some(unit),
0xdc00..=0xdfff => push_char(char::REPLACEMENT_CHARACTER, out),
_ => push_char(
char::from_u32(u32::from(unit)).unwrap_or(char::REPLACEMENT_CHARACTER),
out,
),
}
}
}
fn push_char(c: char, out: &mut Vec<u8>) {
let mut buf = [0u8; 4];
out.extend_from_slice(c.encode_utf8(&mut buf).as_bytes());
}
#[derive(Debug, Default)]
pub(crate) struct WakeLatch {
pending: bool,
}
impl WakeLatch {
pub(crate) fn arm(&mut self) {
self.pending = true;
}
pub(crate) fn take(&mut self) -> bool {
std::mem::take(&mut self.pending)
}
}
#[derive(Debug, Default)]
pub(crate) struct ResizeTracker {
seen: Size,
}
impl ResizeTracker {
pub(crate) fn reset(&mut self, size: Size) {
self.seen = size;
}
pub(crate) fn observe(&mut self, fresh: Size) -> Option<Size> {
if fresh != self.seen && !fresh.is_empty() {
self.seen = fresh;
Some(fresh)
} else {
None
}
}
}
pub(crate) fn clamp_repeat(count: u16, cap: u16) -> u16 {
count.clamp(1, cap)
}
#[cfg(test)]
mod tests {
use super::*;
fn decode_units(units: &[u16]) -> String {
let mut d = Utf16Decoder::default();
let mut out = Vec::new();
for &u in units {
d.push(u, &mut out);
}
String::from_utf8_lossy(&out).into_owned()
}
#[test]
fn utf16_bmp_and_astral_pairs() {
assert_eq!(decode_units(&[0x68, 0x69]), "hi");
assert_eq!(decode_units(&[0x6f22, 0x5b57]), "漢字");
assert_eq!(decode_units(&[0xd83c, 0xdf89]), "🎉");
assert_eq!(decode_units(&[0x61, 0xd83c, 0xdf89, 0x62]), "a🎉b");
}
#[test]
fn utf16_pair_split_across_batches_survives() {
let mut d = Utf16Decoder::default();
let mut out = Vec::new();
d.push(0xd83c, &mut out); assert!(out.is_empty(), "no premature emission");
d.push(0xdf89, &mut out); assert_eq!(String::from_utf8_lossy(&out), "🎉");
}
#[test]
fn utf16_lone_halves_become_replacement_never_lost() {
assert_eq!(decode_units(&[0xd83c, 0x41]), "\u{fffd}A");
assert_eq!(decode_units(&[0xdf89]), "\u{fffd}");
assert_eq!(decode_units(&[0xd83c, 0xd83c, 0xdf89]), "\u{fffd}🎉");
assert_eq!(decode_units(&[0xdc00, 0xdc01]), "\u{fffd}\u{fffd}");
}
#[test]
fn wake_latch_delivers_exactly_once_and_survives_input_wins() {
let mut w = WakeLatch::default();
assert!(!w.take(), "unarmed latch delivers nothing");
w.arm();
assert!(w.take(), "armed latch delivers");
assert!(!w.take(), "…exactly once");
w.arm();
w.arm();
assert!(w.take());
assert!(!w.take());
w.arm();
for _turn_with_input in 0..3 {
}
assert!(w.take(), "wake survived the input turns");
}
#[test]
fn resize_tracker_dedupes_and_guards() {
let mut r = ResizeTracker::default();
assert_eq!(r.observe(Size::new(80, 24)), Some(Size::new(80, 24)));
assert_eq!(r.observe(Size::new(80, 24)), None);
assert_eq!(r.observe(Size::new(120, 40)), Some(Size::new(120, 40)));
assert_eq!(r.observe(Size::ZERO), None);
assert_eq!(r.observe(Size::new(0, 40)), None);
assert_eq!(r.observe(Size::new(120, 40)), None, "baseline kept");
r.reset(Size::new(90, 30));
assert_eq!(r.observe(Size::new(90, 30)), None);
}
#[test]
fn repeat_clamp_bounds_both_ends() {
assert_eq!(clamp_repeat(0, 1024), 1, "conhost zero-repeat quirk");
assert_eq!(clamp_repeat(1, 1024), 1);
assert_eq!(clamp_repeat(7, 1024), 7);
assert_eq!(clamp_repeat(u16::MAX, 1024), 1024, "hostile count capped");
}
}