use crate::base::{Point, Size};
use super::grid::{Grid, Paint, VtCell};
const MAX_CSI_LEN: usize = 256;
const MAX_STRING_LEN: usize = 4096;
const MAX_UNKNOWN_SAMPLES: usize = 32;
#[derive(Debug, Default)]
enum State {
#[default]
Ground,
Esc,
Charset,
Csi {
buf: Vec<u8>,
overflow: bool,
},
Osc {
buf: Vec<u8>,
esc: bool,
},
StringFrame {
kind: u8,
len: usize,
esc: bool,
},
}
pub use super::vt_state::{Modes, VtCounters};
pub struct VtScreen {
grid: Grid,
cursor: Point,
wrap_pending: bool,
paint: Paint,
modes: Modes,
state: State,
utf8: Vec<u8>,
last_write: Option<Point>,
pending_zwj: bool,
pending_regional: bool,
saved_cursor: Option<(Point, Paint)>,
margins: Option<(i32, i32)>,
cursor_style: u32,
clipboard: Option<(String, String)>,
notifications: Vec<String>,
links: Vec<(String, String)>,
counters: VtCounters,
unknown_samples: Vec<String>,
title: Option<String>,
}
impl VtScreen {
pub fn new(size: Size) -> VtScreen {
let mut modes = Modes::default();
modes.insert(25);
modes.insert(7);
VtScreen {
grid: Grid::new(size.w, size.h),
cursor: Point::ZERO,
wrap_pending: false,
paint: Paint::default(),
modes,
state: State::Ground,
utf8: Vec::new(),
last_write: None,
pending_zwj: false,
pending_regional: false,
saved_cursor: None,
margins: None,
cursor_style: 0,
clipboard: None,
notifications: Vec::new(),
links: Vec::new(),
counters: VtCounters::default(),
unknown_samples: Vec::new(),
title: None,
}
}
pub fn size(&self) -> Size {
Size::new(self.grid.w, self.grid.h)
}
pub fn cursor(&self) -> Point {
self.cursor
}
pub fn modes(&self) -> &Modes {
&self.modes
}
pub fn counters(&self) -> &VtCounters {
&self.counters
}
pub fn unknown_seq_count(&self) -> u64 {
self.counters.unknown
}
pub fn unknown_samples(&self) -> &[String] {
&self.unknown_samples
}
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
pub fn cell(&self, x: i32, y: i32) -> Option<&VtCell> {
self.grid.cell(x, y)
}
pub fn link_target(&self, id: u32) -> Option<(&str, &str)> {
self.links
.get(id as usize)
.map(|(p, u)| (p.as_str(), u.as_str()))
}
pub fn current_paint(&self) -> Paint {
self.paint
}
pub fn feed(&mut self, bytes: &[u8]) {
for &b in bytes {
self.feed_byte(b);
}
}
fn feed_byte(&mut self, b: u8) {
if (b == 0x18 || b == 0x1a) && !matches!(self.state, State::Ground) {
self.note_unknown("sequence aborted by CAN/SUB");
self.state = State::Ground;
return;
}
match std::mem::take(&mut self.state) {
State::Ground => self.ground_byte(b),
State::Esc => self.esc_byte(b),
State::Charset => { }
State::Csi { buf, overflow } => self.csi_byte(b, buf, overflow),
State::Osc { buf, esc } => self.osc_byte(b, buf, esc),
State::StringFrame { kind, len, esc } => self.string_byte(b, kind, len, esc),
}
}
fn ground_byte(&mut self, b: u8) {
match b {
0x1b => {
self.flush_partial_utf8();
self.state = State::Esc;
}
0x0d => {
self.flush_partial_utf8();
self.cursor.x = 0;
self.wrap_pending = false;
}
0x0a..=0x0c => {
self.flush_partial_utf8();
self.line_feed();
}
0x08 => {
self.flush_partial_utf8();
self.cursor.x = (self.cursor.x - 1).max(0);
self.wrap_pending = false;
}
0x09 => {
self.flush_partial_utf8();
let next = ((self.cursor.x / 8) + 1) * 8;
self.cursor.x = next.min(self.grid.w - 1);
self.wrap_pending = false;
}
0x00..=0x1f | 0x7f => {
self.flush_partial_utf8();
self.note_unknown(&format!("C0 control 0x{b:02x}"));
}
_ => self.utf8_byte(b),
}
}
fn utf8_byte(&mut self, b: u8) {
if self.utf8.is_empty() && b < 0x80 {
self.print_char(b as char);
return;
}
self.utf8.push(b);
let need = match self.utf8[0] {
0xc2..=0xdf => 2,
0xe0..=0xef => 3,
0xf0..=0xf4 => 4,
_ => {
self.utf8.clear();
self.counters.utf8_errors += 1;
self.print_char('\u{fffd}');
return;
}
};
if self.utf8.len() > 1 && (b & 0xc0) != 0x80 {
self.utf8.clear();
self.counters.utf8_errors += 1;
self.print_char('\u{fffd}');
self.feed_byte(b);
return;
}
if self.utf8.len() == need {
let taken = std::mem::take(&mut self.utf8);
match std::str::from_utf8(&taken) {
Ok(s) => {
if let Some(c) = s.chars().next() {
self.print_char(c);
}
}
Err(_) => {
self.counters.utf8_errors += 1;
self.print_char('\u{fffd}');
}
}
}
}
fn flush_partial_utf8(&mut self) {
self.pending_zwj = false;
self.pending_regional = false;
if !self.utf8.is_empty() {
self.utf8.clear();
self.counters.utf8_errors += 1;
self.print_char('\u{fffd}');
}
}
}
#[path = "vt_print.rs"]
mod print;
impl VtScreen {
fn esc_byte(&mut self, b: u8) {
match b {
b'[' => {
self.state = State::Csi {
buf: Vec::new(),
overflow: false,
}
}
b']' => {
self.state = State::Osc {
buf: Vec::new(),
esc: false,
}
}
b'P' | b'_' | b'^' | b'X' => {
self.state = State::StringFrame {
kind: b,
len: 0,
esc: false,
}
}
b'(' | b')' | b'*' | b'+' => self.state = State::Charset,
b'7' => self.saved_cursor = Some((self.cursor, self.paint)),
b'8' => {
if let Some((p, paint)) = self.saved_cursor {
self.cursor =
Point::new(p.x.clamp(0, self.grid.w - 1), p.y.clamp(0, self.grid.h - 1));
self.paint = paint;
self.wrap_pending = false;
}
}
b'M' => {
let (top, bottom) = self.scroll_span();
if self.cursor.y == top {
self.grid
.scroll_down_region(top, bottom, 1, self.paint.erase_paint());
self.last_write = None;
} else if self.cursor.y > 0 {
self.cursor.y -= 1;
}
self.wrap_pending = false;
}
b'D' => self.line_feed(), b'E' => {
self.cursor.x = 0;
self.line_feed();
}
b'c' => self.full_reset(),
b'\\' => { }
0x1b => {
self.note_unknown("ESC ESC");
self.state = State::Esc;
}
_ => self.note_unknown(&format!("ESC 0x{b:02x} ({})", printable(b))),
}
}
fn full_reset(&mut self) {
let size = self.size();
*self = VtScreen::new(size);
}
fn csi_byte(&mut self, b: u8, mut buf: Vec<u8>, overflow: bool) {
match b {
0x1b => {
self.note_unknown(&format!("CSI aborted by ESC after {:?}", sample(&buf)));
self.state = State::Esc;
}
0x40..=0x7e => {
if overflow {
self.note_unknown("CSI overflow (>256 bytes)");
} else {
self.dispatch_csi(&buf, b);
}
}
0x20..=0x3f => {
if buf.len() >= MAX_CSI_LEN {
self.state = State::Csi {
buf,
overflow: true,
};
} else {
if !overflow {
buf.push(b);
}
self.state = State::Csi { buf, overflow };
}
}
_ => {
self.note_unknown(&format!("byte 0x{b:02x} inside CSI"));
}
}
}
fn osc_byte(&mut self, b: u8, mut buf: Vec<u8>, esc: bool) {
if esc {
if b == b'\\' {
self.dispatch_osc(&buf);
} else {
self.note_unknown("OSC torn by ESC (no ST)");
self.state = State::Esc;
self.feed_byte(b);
}
return;
}
match b {
0x07 => self.dispatch_osc(&buf),
0x1b => self.state = State::Osc { buf, esc: true },
_ => {
if buf.len() < MAX_STRING_LEN {
buf.push(b);
}
self.state = State::Osc { buf, esc: false };
}
}
}
fn dispatch_osc(&mut self, buf: &[u8]) {
let s = String::from_utf8_lossy(buf);
let (code, rest) = match s.split_once(';') {
Some((c, r)) => (c, r),
None => (s.as_ref(), ""),
};
match code {
"8" => {
let (params, uri) = rest.split_once(';').unwrap_or(("", rest));
if uri.is_empty() {
self.paint.link = None;
} else {
let key = (params.to_string(), uri.to_string());
let id = match self.links.iter().position(|k| *k == key) {
Some(i) => i as u32,
None => {
self.links.push(key);
(self.links.len() - 1) as u32
}
};
self.paint.link = Some(id);
}
}
"0" | "2" => self.title = Some(rest.to_string()),
"52" => {
let (sel, payload) = rest.split_once(';').unwrap_or((rest, ""));
self.clipboard = Some((sel.to_string(), payload.to_string()));
}
"9" => {
self.notifications.push(rest.to_string());
}
"99" => {
let body = rest.split_once(';').map(|(_, b)| b).unwrap_or(rest);
self.notifications.push(body.to_string());
}
_ => self.note_unknown(&format!("OSC {code}")),
}
}
fn string_byte(&mut self, b: u8, kind: u8, len: usize, esc: bool) {
if esc {
if b == b'\\' {
self.counters.string_frames += 1;
} else {
self.note_unknown("string frame torn by ESC");
self.state = State::Esc;
self.feed_byte(b);
}
return;
}
match b {
0x1b => {
self.state = State::StringFrame {
kind,
len,
esc: true,
}
}
0x07 if kind == b'P' || kind == b'_' => {
self.counters.string_frames += 1;
}
_ => {
self.state = State::StringFrame {
kind,
len: (len + 1).min(MAX_STRING_LEN),
esc: false,
}
}
}
}
pub(super) fn grid_mut(&mut self) -> &mut Grid {
&mut self.grid
}
pub(super) fn grid_ref(&self) -> &Grid {
&self.grid
}
pub(super) fn set_cursor_clamped(&mut self, x: i32, y: i32) {
self.cursor = Point::new(x.clamp(0, self.grid.w - 1), y.clamp(0, self.grid.h - 1));
self.wrap_pending = false;
}
pub(super) fn paint_mut(&mut self) -> &mut Paint {
&mut self.paint
}
pub(super) fn modes_mut(&mut self) -> &mut Modes {
&mut self.modes
}
pub(super) fn counters_mut(&mut self) -> &mut VtCounters {
&mut self.counters
}
pub(super) fn clear_wrap_pending(&mut self) {
self.wrap_pending = false;
}
pub(super) fn home_cursor(&mut self) {
self.cursor = Point::ZERO;
self.wrap_pending = false;
}
pub(super) fn note_unknown(&mut self, what: &str) {
self.counters.unknown += 1;
if self.unknown_samples.len() < MAX_UNKNOWN_SAMPLES {
self.unknown_samples.push(what.to_string());
}
}
pub fn wrap_is_pending(&self) -> bool {
self.wrap_pending
}
pub fn margins(&self) -> Option<(i32, i32)> {
self.margins
}
pub fn cursor_style(&self) -> u32 {
self.cursor_style
}
pub fn clipboard(&self) -> Option<(&str, &str)> {
self.clipboard
.as_ref()
.map(|(s, p)| (s.as_str(), p.as_str()))
}
pub fn notifications(&self) -> &[String] {
&self.notifications
}
pub(super) fn scroll_span(&self) -> (i32, i32) {
self.margins.unwrap_or((0, self.grid.h - 1))
}
pub(super) fn set_margins(&mut self, m: Option<(i32, i32)>) {
self.margins = m;
}
pub(super) fn set_cursor_style(&mut self, style: u32) {
self.cursor_style = style;
}
}
fn printable(b: u8) -> char {
if (0x20..0x7f).contains(&b) {
b as char
} else {
'?'
}
}
fn sample(buf: &[u8]) -> String {
String::from_utf8_lossy(&buf[..buf.len().min(24)]).into_owned()
}