mprocs 0.9.4

TUI for running multiple processes
Documentation
use std::fmt::Write;

use unicode_width::UnicodeWidthStr;

use super::{
  attrs::Attrs,
  common::{CursorStyle, Size},
  grid::{Grid, Pos},
  Cell,
};

pub struct ScreenDiffer {
  cells: Vec<Cell>,
  brush: Attrs,
  pos: Pos,
  cursor_pos: Option<Pos>,
  cursor_style: CursorStyle,
}

pub trait BufferView {
  fn size(&self) -> Size;
  fn get_cell(&self, pos: Pos) -> Option<&Cell>;
  fn get_cursor_pos(&self) -> Option<Pos>;
  fn get_cursor_style(&self) -> CursorStyle;
}

impl ScreenDiffer {
  pub fn new() -> Self {
    Self {
      cells: Vec::new(),
      brush: Attrs::default(),
      pos: Pos {
        row: u16::MAX,
        col: u16::MAX,
      },
      cursor_pos: Some(Pos { col: 0, row: 0 }),
      cursor_style: CursorStyle::default(),
    }
  }

  pub fn diff<V: BufferView, W: Write>(
    &mut self,
    w: &mut W,
    view: &V,
  ) -> std::fmt::Result {
    let prev = &mut self.cells;
    let brush = &mut self.brush;

    let size = view.size();
    let mut full_rerender = false;
    let target_len = (size.height * size.width) as usize;
    if target_len != prev.len() {
      full_rerender = true;
      prev.resize(target_len, Cell::default());
      if prev.capacity() > target_len * 2 {
        prev.shrink_to(target_len);
      }
    }
    for y in 0..size.height {
      for x in 0..size.width {
        let offset = (size.width * y + x) as usize;
        let cell = view
          .get_cell(Pos { col: x, row: y })
          .cloned()
          .unwrap_or_default();

        let mut sep = {
          let mut first = true;
          move |w: &mut W| {
            if first {
              first = false;
              Ok(())
            } else {
              write!(w, ";")
            }
          }
        };

        if full_rerender || cell != prev[offset] {
          let attrs = *cell.attrs();

          if *brush != attrs {
            write!(w, "\x1b[")?;
            if brush.fgcolor != attrs.fgcolor {
              sep(w)?;
              match attrs.fgcolor {
                super::Color::Default => write!(w, "39")?,
                super::Color::Idx(idx) => write!(w, "38;5;{}", idx)?,
                super::Color::Rgb(r, g, b) => write!(w, "38;2;{r};{g};{b}")?,
              }
            }
            if brush.bgcolor != attrs.bgcolor {
              sep(w)?;
              match attrs.bgcolor {
                super::Color::Default => write!(w, "49")?,
                super::Color::Idx(idx) => write!(w, "48;5;{}", idx)?,
                super::Color::Rgb(r, g, b) => write!(w, "48;2;{r};{g};{b}")?,
              }
            }
            if brush.bold() != attrs.bold() {
              sep(w)?;
              let value = if attrs.bold() { 1 } else { 22 };
              write!(w, "{value}")?;
            }
            if brush.italic() != attrs.italic() {
              sep(w)?;
              let value = if attrs.italic() { 3 } else { 23 };
              write!(w, "{value}")?;
            }
            if brush.underline() != attrs.underline() {
              sep(w)?;
              let value = if attrs.underline() { 4 } else { 24 };
              write!(w, "{value}")?;
            }
            if brush.inverse() != attrs.inverse() {
              sep(w)?;
              let value = if attrs.inverse() { 7 } else { 27 };
              write!(w, "{value}")?;
            }
            write!(w, "m")?;

            *brush = attrs;
          }

          let pos = Pos { row: y, col: x };
          if self.pos != pos {
            write!(w, "\x1b[{};{}H", pos.row + 1, pos.col + 1)?;
            self.pos = pos;
          }

          let c = if cell.width() > 0 {
            cell.contents()
          } else {
            " "
          };
          write!(w, "{}", c)?;
          self.pos.col = (size.width - 1).min(self.pos.col + c.width() as u16);
          prev[offset] = cell;
        }
      }
    }

    if self.cursor_pos.is_some() != view.get_cursor_pos().is_some() {
      if view.get_cursor_pos().is_some() {
        write!(w, "\x1b[?25h")?;
      } else {
        write!(w, "\x1b[?25l")?;
      }
    }
    if let Some(pos) = view.get_cursor_pos() {
      if self.pos != pos {
        write!(w, "\x1b[{};{}H", pos.row + 1, pos.col + 1)?;
      }
      self.pos = pos;
    }
    self.cursor_pos = view.get_cursor_pos();

    if self.cursor_style != view.get_cursor_style() {
      let cmd = match view.get_cursor_style() {
        CursorStyle::Default => 0,
        CursorStyle::BlinkingBlock => 1,
        CursorStyle::SteadyBlock => 2,
        CursorStyle::BlinkingUnderline => 3,
        CursorStyle::SteadyUnderline => 4,
        CursorStyle::BlinkingBar => 5,
        CursorStyle::SteadyBar => 6,
      };
      write!(w, "\x1b[{} q", cmd)?;
      self.cursor_style = view.get_cursor_style();
    }

    Ok(())
  }
}

impl BufferView for Vec<Vec<Cell>> {
  fn size(&self) -> Size {
    Size {
      height: self.len() as u16,
      width: self.get(0).map_or(0, |row| row.len() as u16),
    }
  }

  fn get_cell(&self, pos: Pos) -> Option<&Cell> {
    self
      .get(pos.row as usize)
      .map(|row| row.get(pos.col as usize))
      .flatten()
  }

  fn get_cursor_pos(&self) -> Option<Pos> {
    None
  }

  fn get_cursor_style(&self) -> CursorStyle {
    CursorStyle::Default
  }
}

impl BufferView for Grid {
  fn size(&self) -> Size {
    self.size()
  }

  fn get_cell(&self, pos: Pos) -> Option<&Cell> {
    self.visible_cell(pos)
  }

  fn get_cursor_pos(&self) -> Option<Pos> {
    self.cursor_pos
  }

  fn get_cursor_style(&self) -> CursorStyle {
    self.cursor_style
  }
}

#[cfg(test)]
mod tests {
  use crate::term::Color;

  use super::*;

  #[test]
  fn basic() {
    let attrs = Attrs {
      fgcolor: Color::Idx(4),
      ..Default::default()
    };

    let mut differ = ScreenDiffer::new();
    let mut out = String::new();

    differ.diff(&mut out, &vec![vec![]]).unwrap();
    assert_eq!("\x1b[?25l", out); // Hide cursor

    let screen = vec![vec![
      Cell::new("1"),
      Cell::new("2"),
      Cell::new("3").with_attrs(attrs),
      Cell::new("4").with_attrs(attrs),
      Cell::new("5"),
    ]];
    out.clear();
    differ.diff(&mut out, &screen).unwrap();
    assert_eq!("\x1b[1;1H12\x1b[38;5;4m34\x1b[39m5", out);

    let screen = vec![vec![
      Cell::new("1"),
      Cell::new("_"),
      Cell::new("3"),
      Cell::new("4").with_attrs(attrs),
      Cell::new("5"),
    ]];
    out.clear();
    differ.diff(&mut out, &screen).unwrap();
    assert_eq!("\x1b[1;2H_3", out);
  }
}