use crate::ids::{PaneId, TabId};
use crate::layout::Rect;
use serde::{Deserialize, Serialize};
use std::collections::BTreeMap;
use unicode_segmentation::UnicodeSegmentation as _;
use unicode_width::UnicodeWidthStr as _;
pub const MAX_DIM: u16 = crate::terminal::MAX_DIM;
pub const MAX_CELL_TEXT_BYTES: usize = 22;
pub const MAX_TITLE_BYTES: usize = 1024;
#[must_use]
pub fn printable(text: &str, max_chars: usize) -> String {
text.chars()
.filter(|character| !character.is_control())
.take(max_chars)
.collect()
}
pub const MAX_LABEL_BYTES: usize = 128;
pub const MAX_PANES: usize = 128;
pub const MAX_TABS: usize = 32;
pub const MAX_TOTAL_CELLS: usize = 262_144;
pub const MAX_MESSAGE_BYTES: usize = 512;
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum CellKind {
#[default]
Blank,
Text,
WideLeading,
WideContinuation,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum Color {
#[default]
Default,
Indexed(u8),
Rgb(u8, u8, u8),
}
impl From<vt100::Color> for Color {
fn from(value: vt100::Color) -> Self {
match value {
vt100::Color::Default => Self::Default,
vt100::Color::Idx(index) => Self::Indexed(index),
vt100::Color::Rgb(red, green, blue) => Self::Rgb(red, green, blue),
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct CellStyle {
pub foreground: Color,
pub background: Color,
pub bold: bool,
pub dim: bool,
pub italic: bool,
pub underline: bool,
pub inverse: bool,
}
impl CellStyle {
#[must_use]
pub fn is_default(&self) -> bool {
*self == Self::default()
}
#[must_use]
pub fn from_vt100(cell: &vt100::Cell) -> Self {
Self {
foreground: cell.fgcolor().into(),
background: cell.bgcolor().into(),
bold: cell.bold(),
dim: cell.dim(),
italic: cell.italic(),
underline: cell.underline(),
inverse: cell.inverse(),
}
}
}
#[must_use]
pub fn kind_of(cell: &vt100::Cell) -> CellKind {
if cell.is_wide_continuation() {
CellKind::WideContinuation
} else if cell.is_wide() {
CellKind::WideLeading
} else if cell.has_contents() {
CellKind::Text
} else {
CellKind::Blank
}
}
#[must_use]
pub fn classify(cell: &vt100::Cell) -> (&str, CellKind) {
if !cell.has_contents() {
return match kind_of(cell) {
CellKind::WideContinuation => ("", CellKind::WideContinuation),
CellKind::Blank | CellKind::Text | CellKind::WideLeading => ("", CellKind::Blank),
};
}
let text = cell.contents();
let kind = kind_of(cell);
let carried = match kind {
CellKind::Text => one_grapheme_of_width(text, 1),
CellKind::WideLeading => one_grapheme_of_width(text, 2),
CellKind::Blank | CellKind::WideContinuation => text.is_empty(),
};
match (carried, kind) {
(true, kind) => (text, kind),
(false, CellKind::WideContinuation) => ("", CellKind::WideContinuation),
(false, _) => ("", CellKind::Blank),
}
}
fn one_grapheme_of_width(text: &str, width: usize) -> bool {
if let [byte] = text.as_bytes() {
return width == 1 && (0x20..0x7f).contains(byte);
}
text.len() <= MAX_CELL_TEXT_BYTES
&& !text.chars().any(char::is_control)
&& text.graphemes(true).count() == 1
&& text.width() == width
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct Cell {
pub text: String,
pub kind: CellKind,
pub style: CellStyle,
}
impl Default for Cell {
fn default() -> Self {
Self {
text: String::new(),
kind: CellKind::Blank,
style: CellStyle::default(),
}
}
}
impl Cell {
#[must_use]
pub fn from_vt100(cell: &vt100::Cell) -> Self {
let (text, kind) = classify(cell);
Self {
text: text.to_owned(),
kind,
style: CellStyle::from_vt100(cell),
}
}
#[must_use]
pub fn valid(&self) -> bool {
self.text.len() <= MAX_CELL_TEXT_BYTES
&& !self.text.chars().any(char::is_control)
&& match self.kind {
CellKind::Blank | CellKind::WideContinuation => self.text.is_empty(),
CellKind::Text => self.text.graphemes(true).count() == 1 && self.text.width() == 1,
CellKind::WideLeading => {
self.text.graphemes(true).count() == 1 && self.text.width() == 2
}
}
}
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct Cursor {
pub row: u16,
pub column: u16,
pub hidden: bool,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MouseMode {
#[default]
None,
Press,
PressRelease,
ButtonMotion,
AnyMotion,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum MouseEncoding {
#[default]
Default,
Utf8,
Sgr,
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaneModes {
pub alternate_screen: bool,
pub application_keypad: bool,
pub application_cursor: bool,
pub bracketed_paste: bool,
pub mouse_mode: MouseMode,
pub mouse_encoding: MouseEncoding,
}
impl PaneModes {
#[must_use]
pub fn from_vt100(screen: &vt100::Screen) -> Self {
let mouse_mode = match screen.mouse_protocol_mode() {
vt100::MouseProtocolMode::None => MouseMode::None,
vt100::MouseProtocolMode::Press => MouseMode::Press,
vt100::MouseProtocolMode::PressRelease => MouseMode::PressRelease,
vt100::MouseProtocolMode::ButtonMotion => MouseMode::ButtonMotion,
vt100::MouseProtocolMode::AnyMotion => MouseMode::AnyMotion,
};
let mouse_encoding = match screen.mouse_protocol_encoding() {
vt100::MouseProtocolEncoding::Default => MouseEncoding::Default,
vt100::MouseProtocolEncoding::Utf8 => MouseEncoding::Utf8,
vt100::MouseProtocolEncoding::Sgr => MouseEncoding::Sgr,
};
Self {
alternate_screen: screen.alternate_screen(),
application_keypad: screen.application_keypad(),
application_cursor: screen.application_cursor(),
bracketed_paste: screen.bracketed_paste(),
mouse_mode,
mouse_encoding,
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub struct PaneView {
pub rows: u16,
pub columns: u16,
pub cells: Vec<Cell>,
pub cursor: Cursor,
pub modes: PaneModes,
pub title: String,
pub wrapped_rows: Vec<bool>,
pub offset: u32,
pub exit: Option<u32>,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
#[error("pane view exceeds frame bounds")]
pub struct PaneViewError;
impl PaneView {
pub fn from_screen(
screen: &vt100::Screen,
title: &str,
offset: u32,
exit: Option<u32>,
) -> Result<Self, PaneViewError> {
let (rows, columns) = screen.size();
if rows > MAX_DIM || columns > MAX_DIM || title.len() > MAX_TITLE_BYTES {
return Err(PaneViewError);
}
let capacity = usize::from(rows)
.checked_mul(usize::from(columns))
.ok_or(PaneViewError)?;
let mut cells = Vec::with_capacity(capacity);
for row in 0..rows {
for column in 0..columns {
cells.push(
screen
.cell(row, column)
.map_or_else(Cell::default, Cell::from_vt100),
);
}
}
let (cursor_row, cursor_column) = screen.cursor_position();
let view = Self {
rows,
columns,
cells,
cursor: Cursor {
row: cursor_row,
column: cursor_column,
hidden: screen.hide_cursor(),
},
modes: PaneModes::from_vt100(screen),
title: title.to_owned(),
wrapped_rows: (0..rows).map(|row| screen.row_wrapped(row)).collect(),
offset,
exit,
};
view.valid().then_some(view).ok_or(PaneViewError)
}
#[must_use]
pub fn valid(&self) -> bool {
self.shape_valid() && self.cells.iter().all(Cell::valid)
}
#[must_use]
pub fn shape_valid(&self) -> bool {
self.rows <= MAX_DIM
&& self.columns <= MAX_DIM
&& self.cells.len() == usize::from(self.rows) * usize::from(self.columns)
&& self.wrapped_rows.len() == usize::from(self.rows)
&& self.title.len() <= MAX_TITLE_BYTES
}
pub fn from_update(update: &PaneUpdate) -> Result<Self, PaneViewError> {
if !update.full || !update.within_bounds() {
return Err(PaneViewError);
}
let mut view = Self {
rows: update.rows,
columns: update.columns,
cells: vec![Cell::default(); usize::from(update.rows) * usize::from(update.columns)],
wrapped_rows: vec![false; usize::from(update.rows)],
..Self::default()
};
view.apply_rows(update)?;
view.apply_meta(update);
if update.lines.len() != usize::from(update.rows) {
return Err(PaneViewError);
}
Ok(view)
}
pub fn apply(&mut self, update: &PaneUpdate) -> Result<(), PaneViewError> {
if update.full {
*self = Self::from_update(update)?;
return Ok(());
}
if (update.rows, update.columns) != (self.rows, self.columns) || !update.within_bounds() {
return Err(PaneViewError);
}
self.apply_rows(update)?;
self.apply_meta(update);
Ok(())
}
fn apply_meta(&mut self, update: &PaneUpdate) {
self.cursor = update.cursor;
self.modes = update.modes;
self.title.clone_from(&update.title);
self.offset = update.offset;
self.exit = update.exit;
}
fn apply_rows(&mut self, update: &PaneUpdate) -> Result<(), PaneViewError> {
if update.title.len() > MAX_TITLE_BYTES {
return Err(PaneViewError);
}
let columns = usize::from(self.columns);
let mut cells = update.cells.as_slice();
let mut seen = vec![false; usize::from(self.rows)];
for line in &update.lines {
let row = usize::from(line.row);
let flag = seen.get_mut(row).ok_or(PaneViewError)?;
if *flag {
return Err(PaneViewError);
}
*flag = true;
let (carried, rest) = cells
.split_at_checked(usize::from(line.len))
.ok_or(PaneViewError)?;
cells = rest;
let target = self
.cells
.get_mut(row * columns..(row + 1) * columns)
.ok_or(PaneViewError)?;
expand(carried, target)?;
if let Some(wrapped) = self.wrapped_rows.get_mut(row) {
*wrapped = line.wrapped;
}
}
if !cells.is_empty() {
return Err(PaneViewError);
}
Ok(())
}
#[must_use]
pub fn cell(&self, row: u16, column: u16) -> Option<&Cell> {
if row >= self.rows || column >= self.columns {
return None;
}
let index = usize::from(row)
.checked_mul(usize::from(self.columns))?
.checked_add(usize::from(column))?;
self.cells.get(index)
}
#[must_use]
pub fn text_between(&self, start: (u16, u16), end: (u16, u16)) -> String {
let (start, end) = if start <= end {
(start, end)
} else {
(end, start)
};
let mut output = String::new();
let last_row = end.0.min(self.rows.saturating_sub(1));
for row in start.0..=last_row {
let first = if row == start.0 { start.1 } else { 0 };
let last = if row == end.0 {
end.1
} else {
self.columns.saturating_sub(1)
};
let mut line = String::new();
for column in first..=last.min(self.columns.saturating_sub(1)) {
if let Some(cell) = self.cell(row, column)
&& cell.kind != CellKind::WideContinuation
{
if cell.kind == CellKind::Blank {
line.push(' ');
} else {
line.push_str(&cell.text);
}
}
}
output.push_str(line.trim_end_matches(' '));
let wrapped = self
.wrapped_rows
.get(usize::from(row))
.copied()
.unwrap_or(false);
if row != last_row && !wrapped {
output.push('\n');
}
}
output
}
}
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TabEntry {
pub id: TabId,
pub label: String,
}
#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PaneRect {
pub pane: PaneId,
pub rect: Rect,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Frame {
pub workspace: String,
pub generation: u64,
pub tabs: Vec<TabEntry>,
pub active_tab: Option<TabId>,
pub focused: Option<PaneId>,
pub layout: Vec<PaneRect>,
pub panes: BTreeMap<PaneId, PaneView>,
pub exit_code: Option<u32>,
pub message: Option<String>,
}
impl Frame {
#[must_use]
pub fn valid(&self) -> bool {
self.tabs.len() <= MAX_TABS
&& self.panes.len() <= MAX_PANES
&& self.layout.len() <= MAX_PANES
&& self.workspace.len() <= 64
&& self
.tabs
.iter()
.all(|tab| tab.label.len() <= MAX_LABEL_BYTES)
&& self.panes.values().all(PaneView::shape_valid)
&& self
.panes
.values()
.map(|pane| pane.cells.len())
.sum::<usize>()
<= MAX_TOTAL_CELLS
&& self
.layout
.iter()
.all(|entry| self.panes.contains_key(&entry.pane))
&& self
.active_tab
.is_none_or(|active| self.tabs.iter().any(|tab| tab.id == active))
&& self
.focused
.is_none_or(|focused| self.layout.iter().any(|entry| entry.pane == focused))
&& self
.message
.as_ref()
.is_none_or(|message| message.len() <= MAX_MESSAGE_BYTES)
}
#[must_use]
pub fn pane(&self, id: PaneId) -> Option<&PaneView> {
self.panes.get(&id)
}
pub fn apply(&mut self, update: FrameUpdate) -> Result<(), FrameError> {
if !update.within_bounds(self) {
return Err(FrameError::Invalid);
}
if update.full {
self.panes.clear();
}
let shown: Vec<PaneId> = update.layout.iter().map(|entry| entry.pane).collect();
for (id, pane) in update.panes {
if !shown.contains(&id) {
continue;
}
match self.panes.get_mut(&id) {
Some(view) if !pane.full => view.apply(&pane)?,
_ => {
self.panes.insert(id, PaneView::from_update(&pane)?);
}
}
}
self.workspace = update.workspace;
self.generation = update.generation;
self.tabs = update.tabs;
self.active_tab = update.active_tab;
self.focused = update.focused;
self.layout = update.layout;
self.exit_code = update.exit_code;
self.message = update.message;
self.panes.retain(|id, _| shown.contains(id));
if self.valid() {
Ok(())
} else {
Err(FrameError::Invalid)
}
}
#[must_use]
pub fn focused_pane(&self) -> Option<&PaneView> {
self.panes.get(&self.focused?)
}
#[must_use]
pub fn rect(&self, id: PaneId) -> Option<Rect> {
self.layout
.iter()
.find(|entry| entry.pane == id)
.map(|entry| entry.rect)
}
#[must_use]
pub fn pane_at(&self, x: u16, y: u16) -> Option<PaneRect> {
self.layout
.iter()
.copied()
.find(|entry| entry.rect.contains(x, y))
}
}
#[derive(Debug, thiserror::Error)]
pub enum FrameError {
#[error(transparent)]
Pane(#[from] PaneViewError),
#[error("frame violates its bounds")]
Invalid,
}
fn is_false(value: &bool) -> bool {
!*value
}
fn is_zero_u16(value: &u16) -> bool {
*value == 0
}
fn is_zero_u32(value: &u32) -> bool {
*value == 0
}
#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Line {
pub row: u16,
#[serde(default, skip_serializing_if = "is_false")]
pub wrapped: bool,
pub len: u16,
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct WireCell {
#[serde(default, skip_serializing_if = "Option::is_none")]
pub text: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub kind: Option<CellKind>,
#[serde(default, skip_serializing_if = "CellStyle::is_default")]
pub style: CellStyle,
#[serde(default, skip_serializing_if = "is_zero_u16")]
pub run: u16,
}
impl WireCell {
fn matches_blank(&self, kind: CellKind, style: CellStyle) -> bool {
self.text.is_none() && self.kind.unwrap_or_default() == kind && self.style == style
}
}
pub fn push_wire(
cells: &mut Vec<WireCell>,
row_start: usize,
text: &str,
kind: CellKind,
style: CellStyle,
) {
if text.is_empty() {
if let Some(last) = cells.get_mut(row_start..).and_then(<[WireCell]>::last_mut)
&& last.matches_blank(kind, style)
&& last.run < u16::MAX
{
last.run = last.run.max(1).saturating_add(1);
return;
}
cells.push(WireCell {
text: None,
kind: (kind != CellKind::Blank).then_some(kind),
style,
run: 0,
});
return;
}
cells.push(WireCell {
text: Some(text.to_owned()),
kind: (kind != CellKind::Text).then_some(kind),
style,
run: 0,
});
}
fn expand(carried: &[WireCell], target: &mut [Cell]) -> Result<(), PaneViewError> {
let mut column = 0_usize;
for wire in carried {
let cell = match &wire.text {
Some(text) => Cell {
text: text.clone(),
kind: wire.kind.unwrap_or(CellKind::Text),
style: wire.style,
},
None => Cell {
text: String::new(),
kind: wire.kind.unwrap_or(CellKind::Blank),
style: wire.style,
},
};
if !cell.valid() {
return Err(PaneViewError);
}
let count = if cell.text.is_empty() {
usize::from(wire.run.max(1))
} else if wire.run > 1 {
return Err(PaneViewError);
} else {
1
};
let slots = target
.get_mut(column..column.saturating_add(count))
.ok_or(PaneViewError)?;
if slots.len() != count {
return Err(PaneViewError);
}
for slot in slots {
slot.clone_from(&cell);
}
column = column.saturating_add(count);
}
if column != target.len() {
return Err(PaneViewError);
}
Ok(())
}
fn bounded_seq<'de, D, T>(deserializer: D, limit: usize) -> Result<Vec<T>, D::Error>
where
D: serde::Deserializer<'de>,
T: serde::Deserialize<'de>,
{
struct Bounded<T>(usize, std::marker::PhantomData<T>);
impl<'de, T: serde::Deserialize<'de>> serde::de::Visitor<'de> for Bounded<T> {
type Value = Vec<T>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(formatter, "a sequence of at most {} elements", self.0)
}
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<T>, A::Error> {
let mut items = Vec::new();
while let Some(item) = seq.next_element()? {
if items.len() >= self.0 {
return Err(serde::de::Error::custom("sequence exceeds its bound"));
}
items.push(item);
}
Ok(items)
}
}
deserializer.deserialize_seq(Bounded(limit, std::marker::PhantomData))
}
fn bounded_lines<'de, D: serde::Deserializer<'de>>(deserializer: D) -> Result<Vec<Line>, D::Error> {
bounded_seq(deserializer, usize::from(MAX_DIM))
}
fn bounded_cells<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Vec<WireCell>, D::Error> {
bounded_seq(deserializer, MAX_TOTAL_CELLS)
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct PaneUpdate {
pub rows: u16,
pub columns: u16,
pub cursor: Cursor,
pub modes: PaneModes,
pub title: String,
#[serde(default, skip_serializing_if = "is_false")]
pub full: bool,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "bounded_lines"
)]
pub lines: Vec<Line>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
deserialize_with = "bounded_cells"
)]
pub cells: Vec<WireCell>,
#[serde(default, skip_serializing_if = "is_zero_u32")]
pub offset: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub exit: Option<u32>,
}
impl PaneUpdate {
#[must_use]
pub fn within_bounds(&self) -> bool {
self.rows <= MAX_DIM
&& self.columns <= MAX_DIM
&& self.lines.len() <= usize::from(self.rows)
&& self.cells.len() <= self.lines.len().saturating_mul(usize::from(self.columns))
&& self.title.len() <= MAX_TITLE_BYTES
}
pub fn full_from_screen(
screen: &vt100::Screen,
title: &str,
offset: u32,
exit: Option<u32>,
) -> Result<Self, PaneViewError> {
let (rows, columns) = screen.size();
if rows > MAX_DIM || columns > MAX_DIM || title.len() > MAX_TITLE_BYTES {
return Err(PaneViewError);
}
let (cursor_row, cursor_column) = screen.cursor_position();
let mut update = Self {
rows,
columns,
cursor: Cursor {
row: cursor_row,
column: cursor_column,
hidden: screen.hide_cursor(),
},
modes: PaneModes::from_vt100(screen),
title: title.to_owned(),
full: true,
lines: Vec::with_capacity(usize::from(rows)),
cells: Vec::new(),
offset,
exit,
};
for row in 0..rows {
let start = update.cells.len();
for column in 0..columns {
match screen.cell(row, column) {
Some(cell) => {
let (text, kind) = classify(cell);
push_wire(
&mut update.cells,
start,
text,
kind,
CellStyle::from_vt100(cell),
);
}
None => push_wire(
&mut update.cells,
start,
"",
CellKind::Blank,
CellStyle::default(),
),
}
}
update.lines.push(Line {
row,
wrapped: screen.row_wrapped(row),
len: u16::try_from(update.cells.len() - start).unwrap_or(u16::MAX),
});
}
Ok(update)
}
pub fn merge(&mut self, newer: Self) {
if newer.full || (self.rows, self.columns) != (newer.rows, newer.columns) {
*self = newer;
return;
}
let mut rows: BTreeMap<u16, (bool, Vec<WireCell>)> = BTreeMap::new();
let mut cells = self.cells.iter();
for line in &self.lines {
let carried: Vec<WireCell> = cells
.by_ref()
.take(usize::from(line.len))
.cloned()
.collect();
rows.insert(line.row, (line.wrapped, carried));
}
let mut cells = newer.cells.into_iter();
for line in newer.lines {
let carried: Vec<WireCell> = cells.by_ref().take(usize::from(line.len)).collect();
rows.insert(line.row, (line.wrapped, carried));
}
self.lines.clear();
self.cells.clear();
for (row, (wrapped, carried)) in rows {
self.lines.push(Line {
row,
wrapped,
len: u16::try_from(carried.len()).unwrap_or(u16::MAX),
});
self.cells.extend(carried);
}
self.cursor = newer.cursor;
self.modes = newer.modes;
self.title = newer.title;
self.offset = newer.offset;
self.exit = newer.exit;
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct FrameUpdate {
pub workspace: String,
pub generation: u64,
pub tabs: Vec<TabEntry>,
pub active_tab: Option<TabId>,
pub focused: Option<PaneId>,
pub layout: Vec<PaneRect>,
#[serde(deserialize_with = "bounded_panes")]
pub panes: BTreeMap<PaneId, PaneUpdate>,
pub exit_code: Option<u32>,
pub message: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub full: bool,
}
fn bounded_panes<'de, D>(deserializer: D) -> Result<BTreeMap<PaneId, PaneUpdate>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct Panes;
impl<'de> serde::de::Visitor<'de> for Panes {
type Value = BTreeMap<PaneId, PaneUpdate>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
formatter,
"at most {MAX_PANES} panes within {MAX_TOTAL_CELLS} wire cells"
)
}
fn visit_map<A: serde::de::MapAccess<'de>>(
self,
mut map: A,
) -> Result<Self::Value, A::Error> {
let mut panes = BTreeMap::new();
let mut cells = 0_usize;
while let Some((id, pane)) = map.next_entry::<PaneId, PaneUpdate>()? {
cells = cells.saturating_add(pane.cells.len());
if panes.len() >= MAX_PANES || cells > MAX_TOTAL_CELLS {
return Err(serde::de::Error::custom("frame update exceeds its bounds"));
}
panes.insert(id, pane);
}
Ok(panes)
}
}
deserializer.deserialize_map(Panes)
}
impl FrameUpdate {
#[must_use]
pub fn within_bounds(&self, held: &Frame) -> bool {
if self.panes.len() > MAX_PANES
|| self.layout.len() > MAX_PANES
|| self.tabs.len() > MAX_TABS
{
return false;
}
let mut cells = 0_usize;
for (id, pane) in &self.panes {
if !pane.within_bounds() {
return false;
}
if !self.shows(*id) {
continue;
}
let size = match held.panes.get(id) {
Some(view) if !pane.full && !self.full => view.cells.len(),
_ => usize::from(pane.rows).saturating_mul(usize::from(pane.columns)),
};
cells = cells.saturating_add(size);
}
if !self.full {
cells = held
.panes
.iter()
.filter(|(id, _)| !self.panes.contains_key(id) && self.shows(**id))
.fold(cells, |sum, (_, view)| sum.saturating_add(view.cells.len()));
}
cells <= MAX_TOTAL_CELLS
}
fn shows(&self, pane: PaneId) -> bool {
self.layout.iter().any(|entry| entry.pane == pane)
}
pub fn merge(&mut self, newer: Self) {
if newer.full {
*self = newer;
return;
}
self.panes
.retain(|id, _| newer.layout.iter().any(|entry| entry.pane == *id));
for (id, pane) in newer.panes {
match self.panes.get_mut(&id) {
Some(existing) => existing.merge(pane),
None => {
self.panes.insert(id, pane);
}
}
}
self.workspace = newer.workspace;
self.generation = newer.generation;
self.tabs = newer.tabs;
self.active_tab = newer.active_tab;
self.focused = newer.focused;
self.layout = newer.layout;
self.exit_code = newer.exit_code;
if newer.message.is_some() {
self.message = newer.message;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn full_updates_round_trip_to_the_screen_view() {
let mut parser = vt100::Parser::new(3, 8, 0);
parser.process("e\u{301}界x\x1b[1;31mred\r\n wrap".as_bytes());
let direct = PaneView::from_screen(parser.screen(), "t", 2, Some(1)).unwrap_or_default();
let update =
PaneUpdate::full_from_screen(parser.screen(), "t", 2, Some(1)).unwrap_or_default();
let rebuilt = PaneView::from_update(&update);
assert_eq!(rebuilt.as_ref().ok(), Some(&direct), "{update:?}");
let json = serde_json::to_string(&update).unwrap_or_default();
let parsed: PaneUpdate = serde_json::from_str(&json).unwrap_or_default();
assert_eq!(parsed, update);
assert!(json.contains("\"run\":"), "blank runs are compact: {json}");
}
#[test]
fn hostile_updates_are_rejected_before_anything_is_allocated() {
let huge = PaneUpdate {
rows: u16::MAX,
columns: u16::MAX,
full: true,
..PaneUpdate::default()
};
assert!(PaneView::from_update(&huge).is_err());
let shown = |ids: std::ops::RangeInclusive<u32>| -> Vec<PaneRect> {
ids.map(|id| PaneRect {
pane: PaneId(id),
rect: Rect::default(),
})
.collect()
};
let mut frame = Frame::default();
let mut update = FrameUpdate {
layout: shown(1..=1),
..FrameUpdate::default()
};
update.panes.insert(PaneId(1), huge);
assert!(frame.apply(update).is_err());
let mut update = FrameUpdate {
layout: shown(1..=8),
..FrameUpdate::default()
};
for id in 1..=8 {
update.panes.insert(
PaneId(id),
PaneUpdate {
rows: MAX_DIM,
columns: MAX_DIM,
full: true,
..PaneUpdate::default()
},
);
}
assert!(!update.within_bounds(&frame));
let oversized = PaneUpdate {
rows: 1,
columns: 1,
full: true,
lines: vec![Line::default(); usize::from(MAX_DIM) + 1],
..PaneUpdate::default()
};
let json = serde_json::to_string(&oversized).unwrap_or_default();
assert!(json.contains("\"lines\""));
assert!(serde_json::from_str::<PaneUpdate>(&json).is_err());
let fitting = PaneUpdate {
lines: vec![Line::default(); usize::from(MAX_DIM)],
..oversized
};
let json = serde_json::to_string(&fitting).unwrap_or_default();
assert!(
serde_json::from_str::<PaneUpdate>(&json).is_ok(),
"decodes; rejected later"
);
}
#[test]
fn a_large_viewer_switches_tabs_within_the_cell_budget() {
let big = || PaneUpdate {
rows: MAX_DIM,
columns: MAX_DIM,
full: true,
lines: (0..MAX_DIM)
.map(|row| Line {
row,
wrapped: false,
len: 1,
})
.collect(),
cells: vec![
WireCell {
run: MAX_DIM,
..WireCell::default()
};
usize::from(MAX_DIM)
],
..PaneUpdate::default()
};
let switch = |id: u32| {
let mut update = FrameUpdate {
layout: vec![PaneRect {
pane: PaneId(id),
rect: Rect {
x: 0,
y: 0,
width: MAX_DIM,
height: MAX_DIM,
},
}],
..FrameUpdate::default()
};
update.panes.insert(PaneId(id), big());
update
};
let mut frame = Frame::default();
assert!(frame.apply(switch(1)).is_ok());
assert!(
frame.apply(switch(2)).is_ok(),
"the left pane does not count"
);
assert_eq!(frame.panes.len(), 1);
let mut merged = switch(3);
merged.merge(switch(4));
merged.merge(switch(5));
assert_eq!(
merged.panes.len(),
1,
"panes that left the layout are pruned"
);
assert!(frame.apply(merged).is_ok());
assert!(frame.panes.contains_key(&PaneId(5)));
let mut update = FrameUpdate::default();
for id in 1..=129 {
update.panes.insert(PaneId(id), PaneUpdate::default());
}
let json = serde_json::to_string(&update).unwrap_or_default();
assert!(serde_json::from_str::<FrameUpdate>(&json).is_err());
}
#[test]
fn vt100_conversion_keeps_combining_text_and_wide_cells() {
let mut parser = vt100::Parser::new(2, 6, 0);
parser.process("e\u{301}界x".as_bytes());
let view = PaneView::from_screen(parser.screen(), "", 0, None).unwrap_or_default();
assert_eq!(
view.cell(0, 0).map(|cell| cell.text.as_str()),
Some("e\u{301}")
);
assert_eq!(
view.cell(0, 1).map(|cell| cell.kind),
Some(CellKind::WideLeading)
);
assert_eq!(
view.cell(0, 2).map(|cell| cell.kind),
Some(CellKind::WideContinuation)
);
assert_eq!(view.text_between((0, 0), (0, 5)), "e\u{301}界x");
}
#[test]
fn cells_reject_terminal_controls_and_multiple_clusters() {
for text in ["\u{1b}[2J", "\n", "ab", "a\u{85}", "界"] {
let cell = Cell {
text: text.to_owned(),
kind: CellKind::Text,
style: CellStyle::default(),
};
assert!(!cell.valid(), "accepted unsafe cell {text:?}");
}
for text in ["🇰🇷", "한"] {
assert!(
Cell {
text: text.to_owned(),
kind: CellKind::WideLeading,
style: CellStyle::default(),
}
.valid()
);
}
}
#[test]
fn wrapped_rows_join_and_blank_tails_are_trimmed() {
let mut parser = vt100::Parser::new(3, 4, 0);
parser.process(b"abcdef\r\nxy");
let view = PaneView::from_screen(parser.screen(), "", 0, None).unwrap_or_default();
assert_eq!(view.text_between((0, 0), (2, 3)), "abcdef\nxy");
assert_eq!(view.text_between((2, 3), (1, 0)), "ef\nxy");
}
#[test]
fn frame_validation_rejects_dangling_references() {
let mut frame = Frame::default();
frame.layout.push(PaneRect {
pane: PaneId(7),
rect: Rect::default(),
});
assert!(!frame.valid());
frame.layout.clear();
frame.focused = Some(PaneId(1));
assert!(!frame.valid());
frame.focused = None;
frame.active_tab = Some(TabId(3));
assert!(!frame.valid());
frame.active_tab = None;
assert!(frame.valid());
let oversized = PaneView {
rows: u16::MAX,
columns: u16::MAX,
..PaneView::default()
};
frame.panes.insert(PaneId(1), oversized);
assert!(!frame.valid());
}
}