use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::Arc;
use rich::cells::cell_len;
use rich::measure::Measurement;
use rich::style::{Meta, MetaValue};
use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
use crate::model::MicroAsset;
use crate::registry::MicroRegistry;
pub const MICRO_META_KEY: &str = "rich.micro";
pub const PAD_CELL: char = '\u{2800}';
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
pub(crate) const SENTINEL_BASE: u32 = 0x10_0000;
pub(crate) const SENTINEL_LAST: u32 = 0x10_FFFD;
pub(crate) fn is_sentinel(c: char) -> bool {
(SENTINEL_BASE..=SENTINEL_LAST).contains(&(c as u32))
}
#[derive(Clone, Debug, PartialEq, Eq, Hash)]
pub struct MicroMeta {
pub name: String,
pub cols: usize,
pub rows: usize,
pub id: u64,
}
impl MicroMeta {
pub fn new(asset: &MicroAsset) -> Self {
MicroMeta {
name: asset.name().to_string(),
cols: asset.cols(),
rows: asset.size().rows(),
id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
}
}
pub fn to_value(&self) -> MetaValue {
MetaValue::List(vec![
MetaValue::Str(self.name.clone()),
MetaValue::Int(self.cols as i64),
MetaValue::Int(self.rows as i64),
MetaValue::Int(self.id as i64),
])
}
pub fn to_style(&self) -> Style {
let mut meta = Meta::new();
meta.insert(MICRO_META_KEY, self.to_value());
Style::from_meta(meta)
}
pub fn from_style(style: &Style) -> Option<Self> {
match style.meta_ref()?.get(MICRO_META_KEY)? {
MetaValue::List(items) => match items.as_slice() {
[MetaValue::Str(name), MetaValue::Int(cols), MetaValue::Int(rows), MetaValue::Int(id)]
if *cols > 0 && *rows > 0 =>
{
Some(MicroMeta {
name: name.clone(),
cols: *cols as usize,
rows: *rows as usize,
id: *id as u64,
})
}
_ => None,
},
_ => None,
}
}
pub fn from_segment(segment: &Segment) -> Option<Self> {
if segment.control {
return None;
}
segment.style.as_ref().and_then(Self::from_style)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum FallbackPreference {
#[default]
Emoji,
Text,
Alt,
}
fn pad(value: &str, cols: usize) -> String {
let width = cell_len(value);
let mut out = value.to_string();
out.extend(std::iter::repeat_n(PAD_CELL, cols.saturating_sub(width)));
out
}
fn alt_cells(alt: &str, cols: usize) -> String {
let mut out = String::new();
let mut width = 0;
for c in alt.chars() {
let c = if c.is_whitespace() || c.is_control() || is_sentinel(c) {
PAD_CELL
} else {
c
};
let w = cell_len(c.encode_utf8(&mut [0; 4]));
if width + w > cols {
break;
}
out.push(c);
width += w;
}
pad(&out, cols)
}
pub fn fallback_cells(asset: &MicroAsset, preference: FallbackPreference) -> String {
let cols = asset.cols();
let fallback = asset.fallback();
let choice = match preference {
FallbackPreference::Emoji => fallback.emoji.as_ref().or(fallback.text.as_ref()),
FallbackPreference::Text => fallback.text.as_ref(),
FallbackPreference::Alt => None,
};
match choice {
Some(value) if cell_len(value) <= cols && cell_len(value) > 0 => pad(value, cols),
_ => alt_cells(asset.alt(), cols),
}
}
pub fn placeholder(asset: &MicroAsset, preference: FallbackPreference) -> Text {
placeholder_with(asset, preference, MicroMeta::new(asset))
}
pub(crate) fn placeholder_with(
asset: &MicroAsset,
preference: FallbackPreference,
meta: MicroMeta,
) -> Text {
let cells = fallback_cells(asset, preference);
let mut text = Text::new("");
text.append(&cells, Some(meta.to_style().into()));
text
}
#[derive(Clone, Debug)]
pub struct Placement<'a> {
pub asset: &'a MicroAsset,
pub meta: MicroMeta,
pub style: Option<Style>,
pub fallback: String,
}
pub trait MicroRenderer: Send + Sync {
fn name(&self) -> &str;
fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>>;
}
impl<T: MicroRenderer + ?Sized> MicroRenderer for Arc<T> {
fn name(&self) -> &str {
(**self).name()
}
fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>> {
(**self).render(placement, console)
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct FallbackRenderer {
pub preference: FallbackPreference,
}
impl FallbackRenderer {
pub fn new(preference: FallbackPreference) -> Self {
FallbackRenderer { preference }
}
}
impl MicroRenderer for FallbackRenderer {
fn name(&self) -> &str {
"fallback"
}
fn render(&self, placement: &Placement<'_>, _console: &Console) -> Option<Vec<Segment>> {
Some(vec![Segment::new(
fallback_cells(placement.asset, self.preference),
placement.style.clone(),
)])
}
}
pub struct MicroView<R> {
inner: R,
registry: Arc<MicroRegistry>,
renderers: Vec<Arc<dyn MicroRenderer>>,
}
impl<R: Renderable> MicroView<R> {
pub fn new(inner: R, registry: Arc<MicroRegistry>) -> Self {
MicroView {
inner,
registry,
renderers: Vec::new(),
}
}
pub fn renderer(mut self, renderer: Arc<dyn MicroRenderer>) -> Self {
self.renderers.push(renderer);
self
}
}
impl<R: Renderable> Renderable for MicroView<R> {
fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
let segments = self.inner.rich_render(console, options);
if self.renderers.is_empty() {
return segments;
}
rewrite(segments, &self.registry, &self.renderers, console)
}
fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
self.inner.measure(console, options)
}
}
pub fn rewrite(
segments: Vec<Segment>,
registry: &MicroRegistry,
renderers: &[Arc<dyn MicroRenderer>],
console: &Console,
) -> Vec<Segment> {
let mut out = Vec::with_capacity(segments.len());
let mut index = 0;
while index < segments.len() {
let Some(meta) = MicroMeta::from_segment(&segments[index]) else {
out.push(segments[index].clone());
index += 1;
continue;
};
let mut end = index;
let mut width = 0;
while end < segments.len()
&& MicroMeta::from_segment(&segments[end]).is_some_and(|m| m.id == meta.id)
{
width += segments[end].cell_length();
end += 1;
}
let run = &segments[index..end];
let asset = registry
.resolve(&meta.name)
.filter(|asset| asset.cols() == meta.cols);
let drawn = match asset {
Some(asset) if width == meta.cols => {
let placement = Placement {
asset,
meta: meta.clone(),
style: run[0].style.clone(),
fallback: run.iter().map(|s| s.text.as_str()).collect(),
};
renderers.iter().find_map(|renderer| {
renderer.render(&placement, console).filter(|drawn| {
drawn.iter().map(Segment::cell_length).sum::<usize>() == meta.cols
})
})
}
_ => None,
};
match drawn {
Some(drawn) => out.extend(drawn),
None => out.extend_from_slice(run),
}
index = end;
}
out
}