mod region;
use crate::{
frame::{plan_graphics, Change, Frame, Placement, PlacementSource},
layout::{fit_segments, OverflowPolicy},
target::RenderTarget,
};
use region::Region;
pub use region::RegionId;
use rich::{
control::{Control, ControlType},
protocol::RenderEnvironment,
Console, Segment,
};
use std::{io::Write, sync::Arc};
#[derive(Debug)]
pub enum LiveError {
InvalidRegion,
ExhaustedIds,
UnsupportedControl,
Closed,
Io(std::io::Error),
}
impl std::fmt::Display for LiveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "live output: {e}"),
other => write!(f, "live region error: {other:?}"),
}
}
}
impl std::error::Error for LiveError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
if let Self::Io(e) = self {
Some(e)
} else {
None
}
}
}
impl From<std::io::Error> for LiveError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[derive(Debug)]
struct Row {
text: String,
frame: Frame,
uncertain: bool,
}
impl Row {
fn placements(&self) -> &[Placement] {
self.frame.placements()
}
}
fn uncertain(text: &str) -> bool {
use unicode_width::UnicodeWidthChar;
text.chars().any(|c| {
matches!(
c,
'\u{fe0e}' | '\u{fe0f}'
| '\u{200d}'
| '\u{20e3}'
| '\u{1f1e6}'..='\u{1f1ff}'
| '\u{1f3fb}'..='\u{1f3ff}'
| '\u{e0020}'..='\u{e007f}'
) || (c.width() != c.width_cjk() && !matches!(c, '\u{2500}'..='\u{259f}'))
})
}
impl PartialEq for Row {
fn eq(&self, other: &Row) -> bool {
self.text == other.text
&& self.placements().len() == other.placements().len()
&& self
.placements()
.iter()
.zip(other.placements())
.all(|(a, b)| a.same(b))
}
}
pub struct LiveCoordinator<W: Write> {
writer: W,
target: RenderTarget,
owner: Arc<()>,
next: u64,
regions: Vec<Region>,
width: usize,
height: usize,
painted: Vec<Row>,
hidden: bool,
closed: bool,
graphics: Option<Arc<dyn PlacementSource>>,
retained: Vec<Placement>,
}
impl<W: Write> LiveCoordinator<W> {
pub fn new(writer: W, target: RenderTarget) -> Self {
let c = target.capabilities();
Self {
writer,
target,
owner: Arc::new(()),
next: 0,
regions: Vec::new(),
width: c.width,
height: c.height,
painted: Vec::new(),
hidden: false,
closed: false,
graphics: None,
retained: Vec::new(),
}
}
pub fn with_graphics(mut self, source: Arc<dyn PlacementSource>) -> Self {
self.graphics = Some(source);
self
}
fn source(&self) -> Option<&Arc<dyn PlacementSource>> {
self.graphics
.as_ref()
.filter(|_| self.target.capabilities().interactive)
}
fn check(&self) -> Result<(), LiveError> {
if self.closed {
Err(LiveError::Closed)
} else {
Ok(())
}
}
pub fn add(&mut self, content: Vec<Segment>) -> Result<RegionId, LiveError> {
self.check()?;
validate(&content)?;
let serial = self.next;
self.next = self.next.checked_add(1).ok_or(LiveError::ExhaustedIds)?;
let id = RegionId {
owner: self.owner.clone(),
serial,
};
self.regions.push(Region {
id: id.clone(),
content,
});
Ok(id)
}
fn index(&self, id: &RegionId) -> Result<usize, LiveError> {
if !Arc::ptr_eq(&self.owner, &id.owner) {
return Err(LiveError::InvalidRegion);
}
self.regions
.iter()
.position(|r| r.id.serial == id.serial)
.ok_or(LiveError::InvalidRegion)
}
pub fn update(&mut self, id: RegionId, content: Vec<Segment>) -> Result<(), LiveError> {
self.check()?;
validate(&content)?;
let index = self.index(&id)?;
self.regions[index].content = content;
Ok(())
}
pub fn remove(&mut self, id: RegionId) -> Result<(), LiveError> {
self.check()?;
let index = self.index(&id)?;
self.regions.remove(index);
Ok(())
}
pub fn handle(&mut self) -> LiveHandle<'_, W> {
LiveHandle { live: self }
}
fn dynamic(&self) -> bool {
self.target.capabilities().interactive && self.width > 1 && self.height > 1
}
fn control(&mut self, codes: &[ControlType]) -> std::io::Result<()> {
self.writer
.write_all(Control::new(codes).as_str().as_bytes())
}
fn row(&self, console: &Console, mut row: Vec<Segment>) -> Row {
if let Some(source) = self.source() {
row = source.prepare(row);
if validate(&row).is_err() {
row.retain(|segment| !segment.control);
}
}
if !self.target.capabilities().hyperlinks {
for segment in &mut row {
segment.style = segment.style.as_ref().map(|style| style.update_link(None));
}
}
let text = console.segments_to_string(&row);
let mut frame = Frame::from_segments(&row);
if let Some(source) = self.source() {
let placements = source
.placements(&frame)
.into_iter()
.filter(|p| p.row == 0 && p.rows == 1)
.collect();
frame.set_placements(placements);
}
Row {
uncertain: row.iter().any(|segment| uncertain(&segment.text)),
text,
frame,
}
}
fn graphics(&self, new: &Row, old: Option<&Row>, whole: bool) -> Vec<u8> {
if self.source().is_none() {
return Vec::new();
}
let width = new
.frame
.row_width(0)
.max(old.map_or(0, |o| o.frame.row_width(0)));
let changes = match old {
Some(old) if !whole => new.frame.diff(&old.frame),
_ => vec![Change {
row: 0,
columns: 0..width.max(1),
}],
};
let previous = old.map_or(&[][..], Row::placements);
let plan = plan_graphics(previous, new.placements(), &changes);
let console = self.target.console();
let (system, no_color) = (console.color_system(), console.no_color());
let mut out = String::new();
for (_, columns) in &plan.repaint {
let end = columns.end.min(new.frame.row_width(0));
out.push_str(
Control::new(&[ControlType::CursorMoveToColumn(columns.start as u32)]).as_str(),
);
if columns.start < end && new.frame.height() > 0 {
out.push_str(
&new.frame
.encode_span(0, columns.start..end, system, no_color),
);
}
}
for placement in &plan.draw {
out.push_str(
Control::new(&[ControlType::CursorMoveToColumn(placement.column as u32)]).as_str(),
);
out.push_str(&placement.graphic.draw(placement.frame));
}
out.into_bytes()
}
fn rows(&self, width: usize, height: usize) -> Vec<Row> {
let console = self.target.console();
let mut result = Vec::new();
for region in &self.regions {
for row in fit_segments(®ion.content, width, OverflowPolicy::Crop) {
if result.len() == height {
return result;
}
result.push(self.row(&console, row));
}
}
result
}
fn repaint(&self, new: &Row, old: &Row) -> (Vec<u8>, bool) {
let console = self.target.console();
let (system, no_color) = (console.color_system(), console.no_color());
let width = if new.frame.height() == 0 {
0
} else {
new.frame.row_width(0)
};
let mut cells = String::new();
if new.frame.height() > 0 && old.frame.height() > 0 && !new.uncertain && !old.uncertain {
for change in new.frame.diff(&old.frame) {
let column = change.columns.start.min(u32::MAX as usize) as u32;
cells.push_str(Control::new(&[ControlType::CursorMoveToColumn(column)]).as_str());
if change.columns.start < width {
cells.push_str(&new.frame.encode_span(
0,
change.columns.start..change.columns.end.min(width),
system,
no_color,
));
}
if change.columns.end > width {
cells.push_str(Control::new(&[ControlType::EraseInLine(0)]).as_str());
}
}
}
let mut whole = Control::new(&[ControlType::EraseInLine(2)])
.as_str()
.to_string();
whole.push_str(&new.text);
if new.text == old.text {
(Vec::new(), false)
} else if !cells.is_empty() && cells.len() < whole.len() {
(cells.into_bytes(), false)
} else {
(whole.into_bytes(), true)
}
}
fn clear(&mut self) -> std::io::Result<()> {
let count = self
.painted
.len()
.min(self.height.saturating_sub(1))
.min(u32::MAX as usize);
if count == 0 {
self.painted.clear();
return Ok(());
}
self.control(&[
ControlType::CarriageReturn,
ControlType::CursorUp(count as u32),
])?;
for _ in 0..count {
self.control(&[ControlType::EraseInLine(2)])?;
self.writer.write_all(b"\n\r")?;
}
self.control(&[ControlType::CursorUp(count as u32)])?;
self.painted.clear();
Ok(())
}
fn paint(&mut self) -> std::io::Result<()> {
if !self.dynamic() {
return Ok(());
}
let rows = self.rows(self.width - 1, (self.height - 1).min(u32::MAX as usize));
if rows == self.painted {
return Ok(());
}
if !self.hidden && !rows.is_empty() {
self.hidden = true;
self.control(&[ControlType::HideCursor])?;
}
if rows.len() == self.painted.len() {
for (i, row) in rows.iter().enumerate() {
if row == &self.painted[i] {
continue;
}
let distance = (rows.len() - i) as u32;
let (bytes, whole) = self.repaint(row, &self.painted[i]);
let graphics = self.graphics(row, Some(&self.painted[i]), whole);
self.control(&[ControlType::CarriageReturn, ControlType::CursorUp(distance)])?;
self.writer.write_all(&bytes)?;
self.writer.write_all(&graphics)?;
self.control(&[
ControlType::CarriageReturn,
ControlType::CursorDown(distance),
])?;
}
} else {
self.clear()?;
for row in &rows {
self.control(&[ControlType::CarriageReturn, ControlType::EraseInLine(2)])?;
self.writer.write_all(row.text.as_bytes())?;
let graphics = self.graphics(row, None, true);
self.writer.write_all(&graphics)?;
self.writer.write_all(b"\n\r")?;
}
}
if rows.is_empty() && self.hidden {
self.hidden = false;
self.control(&[ControlType::ShowCursor])?;
}
self.painted = rows;
self.writer.flush()
}
fn fail(&mut self, error: std::io::Error) -> LiveError {
let _ = self.finish();
LiveError::Io(error)
}
pub fn refresh(&mut self) -> Result<(), LiveError> {
self.check()?;
self.paint().map_err(|e| self.fail(e))
}
pub fn resize(&mut self, width: usize, height: usize) -> Result<(), LiveError> {
self.check()?;
self.width = width;
self.height = height;
let result = (|| {
if !self.dynamic() {
self.painted.clear();
if self.hidden {
self.hidden = false;
self.control(&[ControlType::ShowCursor])?;
}
return self.writer.flush();
}
self.clear()?;
self.paint()
})();
result.map_err(|e| self.fail(e))
}
pub fn print(&mut self, content: &[Segment]) -> Result<(), LiveError> {
self.check()?;
validate(content)?;
let result = (|| {
self.clear()?;
let interactive = self.target.capabilities().interactive;
let width = if interactive {
self.width.saturating_sub(1)
} else {
self.width
}
.max(1);
let console = self.target.console();
for row in fit_segments(content, width, OverflowPolicy::Fold) {
let row = self.row(&console, row);
self.writer.write_all(row.text.as_bytes())?;
let graphics = self.graphics(&row, None, true);
self.writer.write_all(&graphics)?;
self.retained.extend(row.placements().iter().cloned());
self.writer
.write_all(if interactive { b"\n\r" } else { b"\n" })?;
}
self.paint()
})();
result.map_err(|e| self.fail(e))
}
pub fn finish(&mut self) -> Result<(), LiveError> {
if self.closed {
return Ok(());
}
self.closed = true;
let mut error = None;
if !self.target.capabilities().interactive {
for row in self.rows(self.width, usize::MAX) {
if let Err(e) = writeln!(self.writer, "{}", row.text) {
error = Some(e);
break;
}
}
} else if let Err(e) = self.clear() {
error = Some(e);
} else if let Some(source) = self.source() {
let release = source.release(&self.retained);
if let Err(e) = self.writer.write_all(release.as_bytes()) {
error = Some(e);
}
}
if self.hidden {
self.hidden = false;
if let Err(e) = self.control(&[ControlType::ShowCursor]) {
if error.is_none() {
error = Some(e);
}
}
}
if let Err(e) = self.writer.flush() {
if error.is_none() {
error = Some(e);
}
}
error.map_or(Ok(()), |e| Err(LiveError::Io(e)))
}
}
impl<W: Write> Drop for LiveCoordinator<W> {
fn drop(&mut self) {
let _ = self.finish();
}
}
pub struct LiveHandle<'a, W: Write> {
live: &'a mut LiveCoordinator<W>,
}
impl<W: Write> LiveHandle<'_, W> {
pub fn print(&mut self, content: &[Segment]) -> Result<(), LiveError> {
self.live.print(content)
}
pub fn update(&mut self, id: RegionId, content: Vec<Segment>) -> Result<(), LiveError> {
self.live.update(id, content)
}
pub fn refresh(&mut self) -> Result<(), LiveError> {
self.live.refresh()
}
}
fn validate(content: &[Segment]) -> Result<(), LiveError> {
if content.iter().any(|s| {
s.control
|| s.text
.chars()
.any(|c| c.is_control() && c != '\n' && c != '\t')
|| s.style
.as_ref()
.and_then(|style| style.link())
.is_some_and(|link| link.chars().any(char::is_control))
}) {
Err(LiveError::UnsupportedControl)
} else {
Ok(())
}
}