use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
mod attrs;
mod cell;
mod color;
mod cursor;
mod glyph;
pub use attrs::PaneAttributes;
pub use cell::PaneCell;
pub use color::PaneColor;
pub use cursor::PaneCursor;
pub use glyph::PaneGlyph;
#[derive(Debug, Default, Clone, PartialEq, Eq, Hash)]
pub struct PaneSnapshot {
pub cols: u16,
pub rows: u16,
pub cells: Vec<PaneCell>,
pub cursor: PaneCursor,
pub revision: u64,
}
impl PaneSnapshot {
pub fn new(
cols: u16,
rows: u16,
cells: Vec<PaneCell>,
cursor: PaneCursor,
) -> Result<Self, PaneSnapshotShapeError> {
let snapshot = Self {
cols,
rows,
cells,
cursor,
revision: 0,
};
snapshot.validate_shape()?;
Ok(snapshot)
}
#[must_use]
pub fn with_revision(mut self, revision: u64) -> Self {
self.revision = revision;
self
}
#[must_use]
pub fn expected_cell_count(&self) -> usize {
expected_cell_count(self.cols, self.rows)
}
#[must_use]
pub fn is_row_major_shape(&self) -> bool {
self.cells.len() == self.expected_cell_count()
}
pub fn validate_shape(&self) -> Result<(), PaneSnapshotShapeError> {
let expected = self.expected_cell_count();
if self.cells.len() == expected {
Ok(())
} else {
Err(PaneSnapshotShapeError {
cols: self.cols,
rows: self.rows,
actual_cells: self.cells.len(),
expected_cells: expected,
})
}
}
#[must_use]
pub fn cell(&self, row: u16, col: u16) -> Option<&PaneCell> {
if row >= self.rows || col >= self.cols {
return None;
}
let index = usize::from(row)
.saturating_mul(usize::from(self.cols))
.saturating_add(usize::from(col));
self.cells.get(index)
}
#[must_use]
pub fn row_cells(&self, row: u16) -> Option<&[PaneCell]> {
if row >= self.rows {
return None;
}
let cols = usize::from(self.cols);
let start = usize::from(row).checked_mul(cols)?;
let end = start.checked_add(cols)?;
self.cells.get(start..end)
}
#[must_use]
pub fn owning_cell_col(&self, row: u16, col: u16) -> Option<u16> {
let cell = self.cell(row, col)?;
if !cell.is_padding() {
return Some(col);
}
let mut owner = col;
while owner > 0 {
owner -= 1;
let candidate = self.cell(row, owner)?;
if !candidate.is_padding() {
let width = u16::from(candidate.glyph.width.max(1));
if owner.saturating_add(width) > col {
return Some(owner);
}
return None;
}
}
None
}
pub fn visible_cells(&self) -> impl Iterator<Item = (u16, u16, &PaneCell)> + '_ {
let cols = usize::from(self.cols);
let rows = usize::from(self.rows);
self.cells
.iter()
.enumerate()
.filter_map(move |(index, cell)| {
if cols == 0 || cell.is_padding() {
return None;
}
let row = index / cols;
if row >= rows {
return None;
}
let col = index % cols;
Some((row as u16, col as u16, cell))
})
}
#[must_use]
pub fn visible_row_text(&self, row: u16) -> Option<String> {
self.lossy_row_cells(row).map(render_cells_lossy)
}
#[must_use]
pub fn row_text(&self, row: u16) -> String {
self.visible_row_text(row).unwrap_or_default()
}
#[must_use]
pub fn visible_lines(&self) -> Vec<String> {
(0..self.rows)
.map(|row| self.visible_row_text(row).unwrap_or_default())
.collect()
}
#[must_use]
pub fn visible_text(&self) -> String {
self.visible_lines().join("\n")
}
fn lossy_row_cells(&self, row: u16) -> Option<&[PaneCell]> {
if row >= self.rows {
return None;
}
let cols = usize::from(self.cols);
if cols == 0 {
return Some(&[]);
}
let start = usize::from(row).checked_mul(cols)?;
if start >= self.cells.len() {
return Some(&[]);
}
let end = start.saturating_add(cols).min(self.cells.len());
Some(&self.cells[start..end])
}
}
impl Serialize for PaneSnapshot {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.validate_shape().map_err(serde::ser::Error::custom)?;
PaneSnapshotFieldsRef {
cols: self.cols,
rows: self.rows,
cells: &self.cells,
cursor: &self.cursor,
revision: self.revision,
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for PaneSnapshot {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let fields = PaneSnapshotFields::deserialize(deserializer)?;
Self::new(fields.cols, fields.rows, fields.cells, fields.cursor)
.map(|snapshot| snapshot.with_revision(fields.revision))
.map_err(serde::de::Error::custom)
}
}
#[derive(Serialize)]
struct PaneSnapshotFieldsRef<'a> {
cols: u16,
rows: u16,
cells: &'a [PaneCell],
cursor: &'a PaneCursor,
revision: u64,
}
#[derive(Debug, Default, Deserialize)]
#[serde(default)]
struct PaneSnapshotFields {
cols: u16,
rows: u16,
cells: Vec<PaneCell>,
cursor: PaneCursor,
revision: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct PaneSnapshotShapeError {
cols: u16,
rows: u16,
actual_cells: usize,
expected_cells: usize,
}
impl PaneSnapshotShapeError {
#[must_use]
pub const fn cols(&self) -> u16 {
self.cols
}
#[must_use]
pub const fn rows(&self) -> u16 {
self.rows
}
#[must_use]
pub const fn actual_cells(&self) -> usize {
self.actual_cells
}
#[must_use]
pub const fn expected_cells(&self) -> usize {
self.expected_cells
}
}
impl fmt::Display for PaneSnapshotShapeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"pane snapshot shape mismatch: {}x{} expects {} cells, got {}",
self.cols, self.rows, self.expected_cells, self.actual_cells
)
}
}
impl std::error::Error for PaneSnapshotShapeError {}
fn expected_cell_count(cols: u16, rows: u16) -> usize {
usize::from(cols) * usize::from(rows)
}
fn render_cells_lossy(cells: &[PaneCell]) -> String {
let mut rendered = String::new();
for cell in cells {
if cell.is_padding() {
continue;
}
rendered.push_str(cell.text());
}
while rendered.ends_with(' ') {
rendered.pop();
}
rendered
}