1use std::collections::HashMap;
43use std::sync::Arc;
44use std::sync::atomic::{AtomicU64, Ordering};
45
46use rich::cells::cell_len;
47use rich::measure::Measurement;
48use rich::style::{Meta, MetaValue};
49use rich::{Console, ConsoleOptions, Renderable, Segment, Style, Text};
50
51use crate::model::MicroAsset;
52use crate::registry::MicroRegistry;
53use crate::rows::runs;
54
55pub const MICRO_META_KEY: &str = "rich.micro";
57
58pub const PAD_CELL: char = '\u{2800}';
62
63static NEXT_ID: AtomicU64 = AtomicU64::new(1);
64
65pub(crate) const SENTINEL_BASE: u32 = 0x10_0000;
68pub(crate) const SENTINEL_LAST: u32 = 0x10_FFFD;
69
70pub(crate) fn is_sentinel(c: char) -> bool {
71 (SENTINEL_BASE..=SENTINEL_LAST).contains(&(c as u32))
72}
73
74#[derive(Clone, Debug, PartialEq, Eq, Hash)]
77pub struct MicroMeta {
78 pub name: String,
79 pub cols: usize,
80 pub rows: usize,
81 pub id: u64,
83 pub row: usize,
86}
87
88impl MicroMeta {
89 pub fn new(asset: &MicroAsset) -> Self {
91 MicroMeta {
92 name: asset.name().to_string(),
93 cols: asset.cols(),
94 rows: asset.size().rows(),
95 id: NEXT_ID.fetch_add(1, Ordering::Relaxed),
96 row: 0,
97 }
98 }
99
100 #[must_use]
102 pub fn with_row(&self, row: usize) -> Self {
103 MicroMeta {
104 row,
105 ..self.clone()
106 }
107 }
108
109 pub fn to_value(&self) -> MetaValue {
112 let mut items = vec![
113 MetaValue::Str(self.name.clone()),
114 MetaValue::Int(self.cols as i64),
115 MetaValue::Int(self.rows as i64),
116 MetaValue::Int(self.id as i64),
117 ];
118 if self.row > 0 {
119 items.push(MetaValue::Int(self.row as i64));
120 }
121 MetaValue::List(items)
122 }
123
124 pub fn to_style(&self) -> Style {
126 let mut meta = Meta::new();
127 meta.insert(MICRO_META_KEY, self.to_value());
128 Style::from_meta(meta)
129 }
130
131 pub fn from_style(style: &Style) -> Option<Self> {
133 let MetaValue::List(items) = style.meta_ref()?.get(MICRO_META_KEY)? else {
134 return None;
135 };
136 let (head, row) = match items.as_slice() {
137 [head @ .., MetaValue::Int(row)] if items.len() == 5 => (head, *row),
138 head if items.len() == 4 => (head, 0),
139 _ => return None,
140 };
141 match head {
142 [
143 MetaValue::Str(name),
144 MetaValue::Int(cols),
145 MetaValue::Int(rows),
146 MetaValue::Int(id),
147 ] if *cols > 0 && *rows > 0 && (0..*rows).contains(&row) => Some(MicroMeta {
148 name: name.clone(),
149 cols: *cols as usize,
150 rows: *rows as usize,
151 id: *id as u64,
152 row: row as usize,
153 }),
154 _ => None,
155 }
156 }
157
158 pub fn from_segment(segment: &Segment) -> Option<Self> {
160 if segment.control {
161 return None;
162 }
163 segment.style.as_ref().and_then(Self::from_style)
164 }
165}
166
167#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
169pub enum FallbackPreference {
170 #[default]
172 Emoji,
173 Text,
175 Alt,
177}
178
179fn pad(value: &str, cols: usize) -> String {
181 let width = cell_len(value);
182 let mut out = value.to_string();
183 out.extend(std::iter::repeat_n(PAD_CELL, cols.saturating_sub(width)));
184 out
185}
186
187fn alt_cells(alt: &str, cols: usize) -> String {
190 let mut out = String::new();
191 let mut width = 0;
192 for c in alt.chars() {
193 let c = if c.is_whitespace() || c.is_control() || is_sentinel(c) {
194 PAD_CELL
195 } else {
196 c
197 };
198 let w = cell_len(c.encode_utf8(&mut [0; 4]));
199 if width + w > cols {
200 break;
201 }
202 out.push(c);
203 width += w;
204 }
205 pad(&out, cols)
206}
207
208pub fn fallback_cells(asset: &MicroAsset, preference: FallbackPreference) -> String {
210 let cols = asset.cols();
211 let fallback = asset.fallback();
212 let choice = match preference {
213 FallbackPreference::Emoji => fallback.emoji.as_ref().or(fallback.text.as_ref()),
214 FallbackPreference::Text => fallback.text.as_ref(),
215 FallbackPreference::Alt => None,
216 };
217 match choice {
218 Some(value) if cell_len(value) <= cols && cell_len(value) > 0 => pad(value, cols),
220 _ => alt_cells(asset.alt(), cols),
221 }
222}
223
224pub fn fallback_row(asset: &MicroAsset, preference: FallbackPreference, row: usize) -> String {
228 if row == 0 {
229 fallback_cells(asset, preference)
230 } else {
231 PAD_CELL.to_string().repeat(asset.cols())
232 }
233}
234
235pub fn placeholder(asset: &MicroAsset, preference: FallbackPreference) -> Text {
239 placeholder_with(asset, preference, MicroMeta::new(asset))
240}
241
242pub(crate) fn placeholder_with(
243 asset: &MicroAsset,
244 preference: FallbackPreference,
245 meta: MicroMeta,
246) -> Text {
247 let cells = fallback_cells(asset, preference);
248 let mut text = Text::new("");
249 text.append(&cells, Some(meta.to_style().into()));
250 text
251}
252
253#[derive(Clone, Debug)]
256pub struct Placement<'a> {
257 pub asset: &'a MicroAsset,
258 pub meta: MicroMeta,
259 pub style: Option<Style>,
262 pub fallback: String,
264}
265
266pub trait MicroRenderer: Send + Sync {
281 fn name(&self) -> &str;
283
284 fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>>;
286}
287
288impl<T: MicroRenderer + ?Sized> MicroRenderer for Arc<T> {
289 fn name(&self) -> &str {
290 (**self).name()
291 }
292 fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>> {
293 (**self).render(placement, console)
294 }
295}
296
297#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
301pub struct FallbackRenderer {
302 pub preference: FallbackPreference,
303}
304
305impl FallbackRenderer {
306 pub fn new(preference: FallbackPreference) -> Self {
307 FallbackRenderer { preference }
308 }
309}
310
311impl MicroRenderer for FallbackRenderer {
312 fn name(&self) -> &str {
313 "fallback"
314 }
315
316 fn render(&self, placement: &Placement<'_>, _console: &Console) -> Option<Vec<Segment>> {
317 Some(vec![Segment::new(
318 fallback_row(placement.asset, self.preference, placement.meta.row),
319 placement.style.clone(),
320 )])
321 }
322}
323
324#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
328pub struct MicroView<R> {
329 inner: R,
330 registry: Arc<MicroRegistry>,
331 renderers: Vec<Arc<dyn MicroRenderer>>,
332}
333
334impl<R: Renderable> MicroView<R> {
335 pub fn new(inner: R, registry: Arc<MicroRegistry>) -> Self {
336 MicroView {
337 inner,
338 registry,
339 renderers: Vec::new(),
340 }
341 }
342
343 pub fn renderer(mut self, renderer: Arc<dyn MicroRenderer>) -> Self {
345 self.renderers.push(renderer);
346 self
347 }
348}
349
350impl<R: Renderable> Renderable for MicroView<R> {
351 fn rich_render(&self, console: &Console, options: &ConsoleOptions) -> Vec<Segment> {
352 let segments = self.inner.rich_render(console, options);
353 if self.renderers.is_empty() {
354 return segments;
355 }
356 rewrite(segments, &self.registry, &self.renderers, console)
357 }
358
359 fn measure(&self, console: &Console, options: &ConsoleOptions) -> Measurement {
360 self.inner.measure(console, options)
361 }
362}
363
364pub fn rewrite(
368 segments: Vec<Segment>,
369 registry: &MicroRegistry,
370 renderers: &[Arc<dyn MicroRenderer>],
371 console: &Console,
372) -> Vec<Segment> {
373 rewrite_rows(segments, registry, renderers, console, false)
374}
375
376fn width_of(drawn: &[Segment]) -> usize {
378 drawn.iter().map(Segment::cell_length).sum()
379}
380
381pub(crate) fn rewrite_rows(
385 segments: Vec<Segment>,
386 registry: &MicroRegistry,
387 renderers: &[Arc<dyn MicroRenderer>],
388 console: &Console,
389 each_row: bool,
390) -> Vec<Segment> {
391 let found = runs(&segments);
392 if found.is_empty() {
393 return segments;
394 }
395 let mut by_row: HashMap<(u64, usize), Vec<usize>> = HashMap::new();
397 for (index, run) in found.iter().enumerate() {
398 by_row
399 .entry((run.meta.id, run.meta.row))
400 .or_default()
401 .push(index);
402 }
403 let placement = |index: usize, asset| {
404 let run = &found[index];
405 Placement {
406 asset,
407 meta: run.meta.clone(),
408 style: segments[run.start].style.clone(),
409 fallback: segments[run.start..run.end]
410 .iter()
411 .map(|s| s.text.as_str())
412 .collect(),
413 }
414 };
415 let mut drawn: Vec<Option<Vec<Segment>>> = vec![None; found.len()];
416 for (index, run) in found.iter().enumerate() {
417 let meta = &run.meta;
418 let asset = registry
419 .resolve(&meta.name)
420 .filter(|asset| asset.cols() == meta.cols && asset.rows() == meta.rows);
421 let Some(asset) = asset.filter(|_| run.width == meta.cols) else {
422 continue;
423 };
424 if meta.rows == 1 || each_row {
425 let placement = placement(index, asset);
426 drawn[index] = renderers.iter().find_map(|renderer| {
427 renderer
428 .render(&placement, console)
429 .filter(|cells| width_of(cells) == meta.cols)
430 });
431 continue;
432 }
433 if meta.row != 0 {
434 continue;
436 }
437 let mut group = vec![index];
439 for row in 1..meta.rows {
440 let below = by_row.get(&(meta.id, row)).and_then(|candidates| {
441 candidates.iter().copied().find(|&other| {
442 let other = &found[other];
443 other.line == run.line + row
444 && other.column == run.column
445 && other.width == meta.cols
446 })
447 });
448 match below {
449 Some(other) => group.push(other),
450 None => break,
451 }
452 }
453 if group.len() != meta.rows {
454 continue;
455 }
456 let placements: Vec<Placement<'_>> = group.iter().map(|&i| placement(i, asset)).collect();
457 let rows = renderers.iter().find_map(|renderer| {
458 placements
459 .iter()
460 .map(|placement| {
461 renderer
462 .render(placement, console)
463 .filter(|cells| width_of(cells) == meta.cols)
464 })
465 .collect::<Option<Vec<_>>>()
466 });
467 if let Some(rows) = rows {
468 for (&other, cells) in group.iter().zip(rows) {
469 drawn[other] = Some(cells);
470 }
471 }
472 }
473 let mut out = Vec::with_capacity(segments.len());
474 let mut rest = segments.into_iter();
475 let mut at = 0;
476 for (run, cells) in found.iter().zip(drawn) {
477 out.extend(rest.by_ref().take(run.start - at));
478 let original: Vec<Segment> = rest.by_ref().take(run.end - run.start).collect();
479 match cells {
480 Some(cells) => out.extend(cells),
481 None => out.extend(original),
482 }
483 at = run.end;
484 }
485 out.extend(rest);
486 out
487}