Skip to main content

rich_micro/
graphics.rs

1//! Drawing micro assets on a terminal (#572–#577): choosing a protocol, and
2//! the renderers and graphics source for each.
3//!
4//! # Choosing (#573)
5//!
6//! [`select`] picks one [`MicroMode`] per terminal, in this order: Kitty,
7//! iTerm2, Sixel, half-blocks, then the emoji or text fallback. It reads
8//! `rich-ext`'s [`GraphicsEnvironment`] (the protocol found, and the cell
9//! size in pixels) and honours `RICH_MICRO=kitty|iterm|sixel|blocks|text`.
10//! Nothing is drawn that the terminal was not found (or said by the user)
11//! to support, and output that is not a terminal (a pipe, a log, an export)
12//! always gets the text fallback, whatever the override says.
13//!
14//! # Drawing
15//!
16//! [`MicroGraphics`] holds one terminal's drawing state: the selection, the
17//! image cache, the Kitty ids it transmitted, and the animation clock.
18//!
19//! - For printing, [`MicroGraphics::renderer`] is a [`MicroRenderer`] for
20//!   [`MicroView`]: Kitty placeholders (transmitting each image once per
21//!   session), an iTerm2 or Sixel image drawn over blank cells with the
22//!   cursor saved and restored, block cells, or the fallback.
23//! - For `LiveCoordinator` and the interactive painter,
24//!   [`MicroGraphics::source`] is a [`PlacementSource`]: it swaps fallback
25//!   cells for the mode's cells before a frame is built, and reports iTerm2
26//!   and Sixel images (and Kitty uploads) as placements for the graphics side
27//!   channel, so diffs stay exact.
28//! - Every renderer checks [`Console::is_terminal`] at render time: an
29//!   export or a capture keeps the fallback.
30//!
31//! # Two rows (#711)
32//!
33//! An asset two rows high is drawn only when both its rows are in the
34//! output, the second right below the first (see [`crate::rows`]), and
35//! always over both: Kitty prints row 0 of its Unicode placeholders on the
36//! first row and row 1 on the second, for one image placed over `cols × 2`
37//! cells; iTerm2 and Sixel draw one image of the whole cell box (Sixel's
38//! `2 × cell height` pixels high) from the second row, once both rows' blank
39//! cells are on the screen, moving the cursor up to the first row to draw
40//! and back after; half-blocks draw two pixel rows per cell row, four for
41//! the asset; and the fallback is the emoji or text on the first row with
42//! blanks below. Animated two-row assets play as one-row ones do.
43//!
44//! # Fallback (#577)
45//!
46//! Where no protocol draws, an asset shows its emoji or text; failing both,
47//! a half-block rendering of its image at exactly its cells (when the
48//! terminal has colour); failing that, its alt text. `RICH_MICRO=blocks`
49//! puts the block rendering first.
50//!
51//! # Animation (#572)
52//!
53//! Frames are resampled to the cell size, deduplicated and rate-limited
54//! under the package's memory limits. Kitty (its frame protocol) and iTerm2
55//! (an animated GIF) play them themselves; Sixel and blocks show the frame
56//! the clock is on, like a spinner, whenever the view is redrawn, and the
57//! source tells an event loop when the next frame is due.
58//! `RICH_A11Y=reduced-motion` and `RICH_ANIMATION=0` show the still frame.
59//!
60//! ```no_run
61//! use std::sync::Arc;
62//! use rich::Console;
63//! use rich_micro::{render_markup, FallbackPreference, Layer, MicroAsset, MicroGraphics, MicroRegistry};
64//!
65//! let mut registry = MicroRegistry::new();
66//! registry.add(Layer::Inline, MicroAsset::new("ship", "rocket")?.with_emoji("🚀")?)?;
67//! let registry = Arc::new(registry);
68//!
69//! // Detects the terminal once: Kitty, iTerm2, Sixel, blocks or text.
70//! let graphics = MicroGraphics::detect(Arc::clone(&registry));
71//! let console = Console::new();
72//! let (text, _) = render_markup(&console, "Deploying :micro:ship:", &registry, FallbackPreference::Emoji);
73//! console.print(&graphics.view(text));
74//! # Ok::<(), rich_micro::MicroError>(())
75//! ```
76
77use std::collections::{HashMap, HashSet};
78use std::fmt;
79use std::sync::{Arc, Mutex, MutexGuard};
80use std::time::{Duration, Instant};
81
82use rich::{ColorSystem, Console, Renderable, Segment, Style};
83use rich_art::graphics::{CellPixels, overlay};
84use rich_ext::capabilities::{ColorDepth, Environment, Graphics, SystemEnvironment};
85use rich_ext::frame::{Frame, Graphic, Placement as FramePlacement, PlacementSource};
86use rich_ext::graphics::GraphicsEnvironment;
87
88use crate::cache::{ImageCache, Prepared, user_cache_dir};
89use crate::model::MicroAsset;
90use crate::package::Limits;
91use crate::registry::MicroRegistry;
92use crate::render::{
93    FallbackPreference, MicroMeta, MicroRenderer, MicroView, PAD_CELL, Placement, fallback_row,
94};
95
96/// How micro assets are drawn on one terminal.
97#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum MicroMode {
99    /// Kitty's graphics protocol, with Unicode placeholders.
100    Kitty,
101    /// iTerm2's inline images (also WezTerm).
102    Iterm,
103    /// Sixel, inline.
104    Sixel,
105    /// Half-block cells in colour.
106    Blocks,
107    /// The emoji or text fallback.
108    Text,
109}
110
111impl MicroMode {
112    /// `kitty`, `iterm`, `sixel`, `blocks` or `text`.
113    pub fn name(self) -> &'static str {
114        match self {
115            MicroMode::Kitty => "kitty",
116            MicroMode::Iterm => "iterm",
117            MicroMode::Sixel => "sixel",
118            MicroMode::Blocks => "blocks",
119            MicroMode::Text => "text",
120        }
121    }
122
123    /// Parse a `RICH_MICRO` value.
124    pub fn parse(value: &str) -> Option<MicroMode> {
125        match value.trim().to_ascii_lowercase().as_str() {
126            "kitty" => Some(MicroMode::Kitty),
127            "iterm" | "iterm2" => Some(MicroMode::Iterm),
128            "sixel" => Some(MicroMode::Sixel),
129            "blocks" | "block" | "halfblocks" => Some(MicroMode::Blocks),
130            "text" | "emoji" | "none" | "off" | "0" => Some(MicroMode::Text),
131            _ => None,
132        }
133    }
134
135    /// Whether this mode draws with a graphics protocol.
136    pub fn is_graphics(self) -> bool {
137        matches!(self, MicroMode::Kitty | MicroMode::Iterm | MicroMode::Sixel)
138    }
139}
140
141impl fmt::Display for MicroMode {
142    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143        f.write_str(self.name())
144    }
145}
146
147/// What [`select`] chose, and why.
148#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct Selection {
150    pub mode: MicroMode,
151    /// Why this mode, for `rich doctor`.
152    pub reason: String,
153    /// The cell size images are fitted to.
154    pub cell: CellPixels,
155    /// Whether the cell size was reported (otherwise it is 8×16).
156    pub cell_known: bool,
157    /// Whether animations play.
158    pub animate: bool,
159    /// Which fallback text to show where nothing is drawn.
160    pub preference: FallbackPreference,
161    /// Block cells before the emoji or text (`RICH_MICRO=blocks`).
162    pub blocks_first: bool,
163    /// The colour system block and Kitty cells are written in.
164    pub color_system: Option<ColorSystem>,
165    /// A `RICH_MICRO` value that was not understood.
166    pub warning: Option<String>,
167}
168
169impl Selection {
170    /// The text fallback, with nothing drawn: what a pipe gets.
171    pub fn text(reason: &str) -> Selection {
172        Selection {
173            mode: MicroMode::Text,
174            reason: reason.to_string(),
175            cell: CellPixels::DEFAULT,
176            cell_known: false,
177            animate: false,
178            preference: FallbackPreference::Emoji,
179            blocks_first: false,
180            color_system: None,
181            warning: None,
182        }
183    }
184
185    /// A given mode, for tests and callers that know their terminal. The
186    /// caller vouches that the terminal can show it.
187    pub fn forced(mode: MicroMode, cell: CellPixels) -> Selection {
188        Selection {
189            mode,
190            reason: "set by the caller".to_string(),
191            cell,
192            cell_known: true,
193            animate: false,
194            preference: FallbackPreference::Emoji,
195            blocks_first: mode == MicroMode::Blocks,
196            color_system: Some(ColorSystem::Truecolor),
197            warning: None,
198        }
199    }
200
201    /// With animation on or off.
202    #[must_use]
203    pub fn animate(mut self, animate: bool) -> Selection {
204        self.animate = animate;
205        self
206    }
207}
208
209/// Choose how to draw micro assets on the terminal `environment`
210/// describes, reading `RICH_MICRO` from `env`. See the [module
211/// docs](self).
212pub fn select(environment: &GraphicsEnvironment, env: &dyn Environment) -> Selection {
213    let cell = environment
214        .cell_pixels
215        .value
216        .map(|c| CellPixels::new(u32::from(c.width), u32::from(c.height)));
217    let color_system = environment.color.color_system();
218    let preference = if environment.unicode {
219        FallbackPreference::Emoji
220    } else {
221        FallbackPreference::Text
222    };
223    let base = |mode: MicroMode, reason: String| Selection {
224        mode,
225        reason,
226        cell: cell.unwrap_or_default(),
227        cell_known: cell.is_some(),
228        animate: environment.animation.value,
229        preference,
230        blocks_first: false,
231        color_system,
232        warning: None,
233    };
234    if !environment.interactive {
235        let mut selection = base(MicroMode::Text, "stdout is not a terminal".into());
236        selection.animate = false;
237        return selection;
238    }
239    let blocks_ok = environment.color != ColorDepth::None && environment.unicode;
240    let mut warning = None;
241    if let Some(value) = env.var("RICH_MICRO").filter(|v| !v.trim().is_empty()) {
242        match MicroMode::parse(&value) {
243            Some(MicroMode::Blocks) if !blocks_ok => {
244                return base(
245                    MicroMode::Text,
246                    format!("RICH_MICRO={} but the terminal has no colour", value.trim()),
247                );
248            }
249            Some(mode) => {
250                let mut selection = base(mode, format!("RICH_MICRO={}", value.trim()));
251                selection.blocks_first = mode == MicroMode::Blocks;
252                return selection;
253            }
254            None => {
255                warning = Some(format!(
256                    "RICH_MICRO={value:?} is not kitty, iterm, sixel, blocks or text"
257                ))
258            }
259        }
260    }
261    let mut selection = match environment.graphics.value {
262        Graphics::Kitty => base(MicroMode::Kitty, environment.graphics.reason.clone()),
263        Graphics::Iterm => base(MicroMode::Iterm, environment.graphics.reason.clone()),
264        _ if environment.sixel.value && cell.is_some() => {
265            base(MicroMode::Sixel, environment.sixel.reason.clone())
266        }
267        _ if blocks_ok => base(
268            MicroMode::Blocks,
269            if environment.sixel.value {
270                "Sixel, but the cell size in pixels is unknown".into()
271            } else {
272                "no graphics protocol; colour cells".into()
273            },
274        ),
275        _ => base(MicroMode::Text, "no graphics protocol and no colour".into()),
276    };
277    selection.warning = warning;
278    selection
279}
280
281/// The animation clock: time since the session started.
282type Clock = Arc<dyn Fn() -> Duration + Send + Sync>;
283
284/// Kitty image ids: in the high byte a per-process base, so programs sharing
285/// a terminal rarely collide.
286fn kitty_base() -> u32 {
287    ((std::process::id() % 200) + 32) << 16
288}
289
290#[derive(Default)]
291struct KittyState {
292    /// Image key to id.
293    ids: HashMap<u64, u32>,
294    /// Ids the terminal holds.
295    transmitted: HashSet<u32>,
296    next: u32,
297}
298
299struct Inner {
300    registry: Arc<MicroRegistry>,
301    selection: Selection,
302    limits: Limits,
303    cache: Mutex<ImageCache>,
304    kitty: Mutex<KittyState>,
305    clock: Clock,
306    /// The animations shown lately, and when, for
307    /// [`PlacementSource::next_change`].
308    moving: Mutex<Vec<(Arc<Prepared>, Duration)>>,
309    /// For rendering block cells and the like outside a print.
310    console: Console,
311}
312
313fn lock<T>(mutex: &Mutex<T>) -> MutexGuard<'_, T> {
314    mutex
315        .lock()
316        .unwrap_or_else(|poisoned| poisoned.into_inner())
317}
318
319/// One terminal's micro-asset drawing state. Cheap to clone: clones share
320/// it. See the [module docs](self).
321#[derive(Clone)]
322pub struct MicroGraphics {
323    inner: Arc<Inner>,
324}
325
326impl fmt::Debug for MicroGraphics {
327    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
328        f.debug_struct("MicroGraphics")
329            .field("selection", &self.inner.selection)
330            .finish_non_exhaustive()
331    }
332}
333
334impl MicroGraphics {
335    /// Drawing state for `selection`, with an in-memory cache only.
336    pub fn new(registry: Arc<MicroRegistry>, selection: Selection) -> MicroGraphics {
337        let started = Instant::now();
338        let console = Console::builder()
339            .force_terminal(true)
340            .color_system(selection.color_system)
341            .width(80)
342            .build();
343        MicroGraphics {
344            inner: Arc::new(Inner {
345                registry,
346                selection,
347                limits: Limits::default(),
348                cache: Mutex::new(ImageCache::new(ImageCache::DEFAULT_BUDGET, None)),
349                kitty: Mutex::new(KittyState {
350                    next: 0,
351                    ..KittyState::default()
352                }),
353                clock: Arc::new(move || started.elapsed()),
354                moving: Mutex::new(Vec::new()),
355                console,
356            }),
357        }
358    }
359
360    /// Detect the process's terminal ([`GraphicsEnvironment::system`]),
361    /// select a mode, and cache optimised images under the user cache
362    /// directory.
363    pub fn detect(registry: Arc<MicroRegistry>) -> MicroGraphics {
364        let selection = select(&GraphicsEnvironment::system(), &SystemEnvironment);
365        MicroGraphics::new(registry, selection).with_disk_cache(user_cache_dir())
366    }
367
368    fn rebuild(self, change: impl FnOnce(&mut Inner)) -> MicroGraphics {
369        match Arc::try_unwrap(self.inner) {
370            Ok(mut inner) => {
371                change(&mut inner);
372                MicroGraphics {
373                    inner: Arc::new(inner),
374                }
375            }
376            // Shared already: settings are fixed.
377            Err(inner) => MicroGraphics { inner },
378        }
379    }
380
381    /// Cache optimised images in `dir` (or not, with `None`). Only before
382    /// the state is shared.
383    #[must_use]
384    pub fn with_disk_cache(self, dir: Option<std::path::PathBuf>) -> MicroGraphics {
385        self.rebuild(|inner| {
386            let budget = ImageCache::DEFAULT_BUDGET;
387            inner.cache = Mutex::new(ImageCache::new(budget, dir));
388        })
389    }
390
391    /// Read packages under `limits`. Only before the state is shared.
392    #[must_use]
393    pub fn with_limits(self, limits: Limits) -> MicroGraphics {
394        self.rebuild(|inner| inner.limits = limits)
395    }
396
397    /// Bound the in-memory image cache to `bytes`. Only before the state is
398    /// shared.
399    #[must_use]
400    pub fn with_cache_budget(self, bytes: usize) -> MicroGraphics {
401        self.rebuild(|inner| inner.cache = Mutex::new(ImageCache::new(bytes, None)))
402    }
403
404    /// Drive animation from `clock` (time since start) instead of the wall
405    /// clock. Only before the state is shared.
406    #[must_use]
407    pub fn with_clock(self, clock: impl Fn() -> Duration + Send + Sync + 'static) -> MicroGraphics {
408        self.rebuild(|inner| inner.clock = Arc::new(clock))
409    }
410
411    /// What was selected.
412    pub fn selection(&self) -> &Selection {
413        &self.inner.selection
414    }
415
416    /// The registry assets resolve in.
417    pub fn registry(&self) -> &Arc<MicroRegistry> {
418        &self.inner.registry
419    }
420
421    /// A renderer for printing: see the [module docs](self).
422    pub fn renderer(&self) -> Arc<dyn MicroRenderer> {
423        Arc::new(ModeRenderer {
424            inner: Arc::clone(&self.inner),
425            inline: true,
426        })
427    }
428
429    /// `inner` with its micro assets drawn by [`renderer`](Self::renderer).
430    pub fn view<R: Renderable>(&self, inner: R) -> MicroView<R> {
431        MicroView::new(inner, Arc::clone(&self.inner.registry)).renderer(self.renderer())
432    }
433
434    /// The graphics source for `LiveCoordinator` and the interactive
435    /// painter: see the [module docs](self).
436    pub fn source(&self) -> Arc<dyn PlacementSource> {
437        Arc::new(Source {
438            inner: Arc::clone(&self.inner),
439        })
440    }
441
442    /// The escape that deletes every Kitty image this session transmitted,
443    /// for a program that is done with everything it drew (printed images
444    /// disappear with it). Empty unless Kitty is in use.
445    pub fn close(&self) -> String {
446        self.inner.release(&[])
447    }
448
449    /// The Kitty ids the terminal holds for this session.
450    pub fn kitty_ids(&self) -> Vec<u32> {
451        let mut ids: Vec<u32> = lock(&self.inner.kitty)
452            .transmitted
453            .iter()
454            .copied()
455            .collect();
456        ids.sort_unstable();
457        ids
458    }
459
460    /// Bytes the in-memory image cache holds.
461    pub fn cache_bytes(&self) -> usize {
462        lock(&self.inner.cache).used()
463    }
464}
465
466impl Inner {
467    fn prepared(&self, asset: &MicroAsset) -> Option<Arc<Prepared>> {
468        let animate = self.selection.animate;
469        lock(&self.cache).prepare(asset, self.selection.cell, animate, &self.limits)
470    }
471
472    fn now(&self) -> Duration {
473        (self.clock)()
474    }
475
476    /// Kitty's id for `prepared`, and the transmission it still needs (only
477    /// when `transmit`).
478    fn kitty(&self, prepared: &Prepared, transmit: bool) -> Option<(u32, Option<String>)> {
479        let truecolor = self.selection.color_system == Some(ColorSystem::Truecolor);
480        let mut state = lock(&self.kitty);
481        let id = match state.ids.get(&prepared.key) {
482            Some(id) => *id,
483            None => {
484                let id = if truecolor {
485                    kitty_base() | (state.next + 1)
486                } else {
487                    16 + state.next
488                };
489                if !truecolor && id > 255 || truecolor && state.next >= 0xFFFF {
490                    return None;
491                }
492                state.next += 1;
493                state.ids.insert(prepared.key, id);
494                id
495            }
496        };
497        if !transmit || state.transmitted.contains(&id) {
498            return Some((id, None));
499        }
500        let escape = rich_art::kitty::transmit_animation(
501            id,
502            &prepared.frames,
503            prepared.cols,
504            prepared.rows,
505        )?;
506        state.transmitted.insert(id);
507        Some((id, Some(escape)))
508    }
509
510    /// Kitty placeholder cells for row `row` of image `id`, tagged with
511    /// `style`.
512    fn kitty_cells(
513        &self,
514        prepared: &Prepared,
515        id: u32,
516        style: Option<&Style>,
517        row: usize,
518    ) -> Option<Vec<Segment>> {
519        if row >= prepared.rows {
520            return None;
521        }
522        let truecolor = self.selection.color_system == Some(ColorSystem::Truecolor);
523        let color = rich_art::kitty::id_color(id, truecolor)?;
524        let style = style
525            .cloned()
526            .unwrap_or_default()
527            .combine(&Style::new().with_color(color));
528        let cells = rich_art::kitty::placeholder_row(row, prepared.cols)?;
529        Some(vec![Segment::new(cells, Some(style))])
530    }
531
532    /// The protocol escapes of each frame (iTerm2: one, possibly a GIF;
533    /// Sixel: one per frame).
534    fn escapes<'a>(&self, prepared: &'a Prepared) -> &'a [String] {
535        prepared.escapes.get_or_init(|| match self.selection.mode {
536            MicroMode::Iterm => {
537                let bytes = if prepared.animated() {
538                    rich_art::graphics::encode_gif(&prepared.frames)
539                } else {
540                    rich_art::graphics::encode_png(&prepared.frames[0].image)
541                };
542                bytes
543                    .map(|bytes| {
544                        vec![rich_art::iterm::inline_image(
545                            &bytes,
546                            prepared.cols,
547                            prepared.rows,
548                        )]
549                    })
550                    .unwrap_or_default()
551            }
552            MicroMode::Sixel => prepared
553                .frames
554                .iter()
555                .map_while(|frame| {
556                    rich_art::SixelArt::new(rich_art::image::DynamicImage::ImageRgba8(
557                        frame.image.clone(),
558                    ))
559                    .cell_px(prepared.cell.width, prepared.cell.height)
560                    .encode_cells(prepared.cols, prepared.rows)
561                })
562                .collect(),
563            _ => Vec::new(),
564        })
565    }
566
567    /// The overlay frame to show now: iTerm2 plays its GIF itself.
568    fn overlay_frame(&self, prepared: &Prepared) -> usize {
569        match self.selection.mode {
570            MicroMode::Sixel => prepared.frame_at(self.now()).0,
571            _ => 0,
572        }
573    }
574
575    /// Row `row` of the block cells for the frame the clock is on, tagged
576    /// with `style`.
577    fn block_cells(
578        &self,
579        prepared: &Prepared,
580        style: Option<&Style>,
581        row: usize,
582    ) -> Option<Vec<Segment>> {
583        let frames = prepared.blocks.get_or_init(|| {
584            prepared
585                .frames
586                .iter()
587                .map_while(|frame| {
588                    blocks(&self.console, &frame.image, prepared.cols, prepared.rows)
589                })
590                .collect()
591        });
592        if frames.len() != prepared.frames.len() {
593            return None;
594        }
595        let index = prepared.frame_at(self.now()).0;
596        let tag = style.cloned().unwrap_or_default();
597        Some(
598            frames[index]
599                .get(row)?
600                .iter()
601                .map(|segment| {
602                    let style = segment
603                        .style
604                        .as_ref()
605                        .map_or(tag.clone(), |s| s.combine(&tag));
606                    Segment::new(segment.text.clone(), Some(style))
607                })
608                .collect(),
609        )
610    }
611
612    /// Whether `asset` shows its emoji or text here rather than blocks.
613    fn prefers_fallback(&self, asset: &MicroAsset) -> bool {
614        let fallback = asset.fallback();
615        let has = match self.selection.preference {
616            FallbackPreference::Emoji => fallback.emoji.is_some() || fallback.text.is_some(),
617            FallbackPreference::Text => fallback.text.is_some(),
618            FallbackPreference::Alt => false,
619        };
620        has && !self.selection.blocks_first
621    }
622
623    /// Cells for `placement` (one row of its asset) in this mode, and the
624    /// escapes to write before and after them when printing inline.
625    fn draw(&self, placement: &Placement<'_>, inline: bool) -> Option<Drawn> {
626        let style = placement.style.as_ref();
627        let asset = placement.asset;
628        let row = placement.meta.row;
629        let fallback = || Drawn {
630            before: None,
631            cells: vec![Segment::new(
632                fallback_row(asset, self.selection.preference, row),
633                placement.style.clone(),
634            )],
635            after: None,
636        };
637        match self.selection.mode {
638            MicroMode::Text => Some(fallback()),
639            MicroMode::Blocks => {
640                if self.prefers_fallback(asset) {
641                    return Some(fallback());
642                }
643                match self.prepared(asset) {
644                    Some(prepared) => {
645                        self.moving(&prepared);
646                        match self.block_cells(&prepared, style, row) {
647                            Some(cells) => Some(Drawn::cells(cells)),
648                            None => Some(fallback()),
649                        }
650                    }
651                    None => Some(fallback()),
652                }
653            }
654            MicroMode::Kitty => {
655                let prepared = self.prepared(asset)?;
656                // The first row transmits the image (once a session); the
657                // row below only shows its own placeholders.
658                let (id, transmit) = self.kitty(&prepared, inline && row == 0)?;
659                let cells = self.kitty_cells(&prepared, id, style, row)?;
660                Some(Drawn {
661                    before: transmit,
662                    cells,
663                    after: None,
664                })
665            }
666            MicroMode::Iterm | MicroMode::Sixel => {
667                let prepared = self.prepared(asset)?;
668                let blank = vec![Segment::new(
669                    PAD_CELL.to_string().repeat(asset.cols()),
670                    placement.style.clone(),
671                )];
672                if !inline {
673                    self.moving(&prepared);
674                    return Some(Drawn::cells(blank));
675                }
676                // One image for the whole box, drawn from its last row, when
677                // every row's blank cells are on the screen.
678                if row + 1 < prepared.rows {
679                    return Some(Drawn::cells(blank));
680                }
681                let frame = self.overlay_frame(&prepared);
682                let escape = self.escapes(&prepared).get(frame)?;
683                let (before, after) = overlay_rows(escape, asset.cols(), prepared.rows);
684                Some(Drawn {
685                    before: Some(before),
686                    cells: blank,
687                    after: Some(after),
688                })
689            }
690        }
691    }
692
693    /// Note an animation shown, for [`PlacementSource::next_change`].
694    fn moving(&self, prepared: &Arc<Prepared>) {
695        let animated = prepared.animated()
696            && matches!(self.selection.mode, MicroMode::Blocks | MicroMode::Sixel);
697        if animated {
698            let now = self.now();
699            let mut moving = lock(&self.moving);
700            match moving.iter_mut().find(|(p, _)| Arc::ptr_eq(p, prepared)) {
701                Some(entry) => entry.1 = now,
702                None => moving.push((Arc::clone(prepared), now)),
703            }
704        }
705    }
706
707    fn release(&self, retained: &[FramePlacement]) -> String {
708        let keep: HashSet<u64> = retained.iter().map(|p| p.graphic.key()).collect();
709        let mut state = lock(&self.kitty);
710        let mut ids: Vec<u32> = state
711            .transmitted
712            .iter()
713            .copied()
714            .filter(|id| !keep.contains(&u64::from(*id)))
715            .collect();
716        ids.sort_unstable();
717        let mut out = String::new();
718        for id in ids {
719            out.push_str(&rich_art::kitty::delete(id));
720            state.transmitted.remove(&id);
721            state.ids.retain(|_, v| *v != id);
722        }
723        out
724    }
725}
726
727/// `overlay` for an image `rows` high, written after the cells of its last
728/// row: the cursor goes up to the first row to draw it, then back past the
729/// cells. One row is exactly `overlay`.
730fn overlay_rows(image: &str, cols: usize, rows: usize) -> (String, String) {
731    if rows <= 1 {
732        return overlay(image, cols);
733    }
734    overlay(&format!("\x1b[{}A{image}", rows - 1), cols)
735}
736
737/// Block cells for `image` at exactly `cols × rows` cells, untagged: one
738/// line of `cols` cells per row, two pixel rows to a cell.
739fn blocks(
740    console: &Console,
741    image: &rich_art::image::RgbaImage,
742    cols: usize,
743    rows: usize,
744) -> Option<Vec<Vec<Segment>>> {
745    use rich_art::{ImageArt, ImageBackground, ImageFit, ImageMode};
746    let art = ImageArt::new(rich_art::image::DynamicImage::ImageRgba8(image.clone()))
747        .mode(ImageMode::Blocks)
748        .width(cols)
749        .height(rows)
750        .fit(ImageFit::Contain)
751        .background_mode(ImageBackground::TerminalDefault);
752    let options = console.options().update_width(cols);
753    let segments = art.render(console, &options).ok()?;
754    let mut lines = Segment::split_lines(&segments);
755    if lines.is_empty() || lines.len() > rows {
756        return None;
757    }
758    // A shorter rendering (an image wider than the box) is padded below.
759    lines.resize_with(rows, Vec::new);
760    lines
761        .iter()
762        .map(|line| {
763            let line = Segment::adjust_line_length(line, cols, None);
764            (line.iter().map(Segment::cell_length).sum::<usize>() == cols).then_some(line)
765        })
766        .collect()
767}
768
769/// What a mode draws for one placement.
770struct Drawn {
771    before: Option<String>,
772    cells: Vec<Segment>,
773    after: Option<String>,
774}
775
776impl Drawn {
777    fn cells(cells: Vec<Segment>) -> Drawn {
778        Drawn {
779            before: None,
780            cells,
781            after: None,
782        }
783    }
784
785    fn into_segments(self) -> Vec<Segment> {
786        let mut out = Vec::with_capacity(self.cells.len() + 2);
787        out.extend(self.before.map(Segment::control));
788        out.extend(self.cells);
789        out.extend(self.after.map(Segment::control));
790        out
791    }
792}
793
794/// The [`MicroRenderer`] of a mode. `inline` writes escapes into the output
795/// (printing); otherwise only cells are swapped (frames).
796struct ModeRenderer {
797    inner: Arc<Inner>,
798    inline: bool,
799}
800
801impl MicroRenderer for ModeRenderer {
802    fn name(&self) -> &str {
803        self.inner.selection.mode.name()
804    }
805
806    fn render(&self, placement: &Placement<'_>, console: &Console) -> Option<Vec<Segment>> {
807        // An export, a capture or a pipe keeps the fallback.
808        if !console.is_terminal() {
809            return None;
810        }
811        let drawn = self.inner.draw(placement, self.inline)?;
812        if !self.inline {
813            return Some(drawn.cells);
814        }
815        // Kitty's colour-coded cells need the colour system they were made
816        // for.
817        if self.inner.selection.mode == MicroMode::Kitty
818            && console.color_system() != self.inner.selection.color_system
819        {
820            return None;
821        }
822        Some(drawn.into_segments())
823    }
824}
825
826/// A Kitty image, shown by the placeholder cells themselves: drawing it
827/// only uploads it, once.
828struct KittyGraphic {
829    inner: Arc<Inner>,
830    prepared: Arc<Prepared>,
831    id: u32,
832}
833
834impl fmt::Debug for KittyGraphic {
835    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
836        f.debug_struct("KittyGraphic")
837            .field("id", &self.id)
838            .finish()
839    }
840}
841
842impl Graphic for KittyGraphic {
843    fn key(&self) -> u64 {
844        u64::from(self.id)
845    }
846    fn draw(&self, _frame: usize) -> String {
847        self.inner
848            .kitty(&self.prepared, true)
849            .and_then(|(_, transmit)| transmit)
850            .unwrap_or_default()
851    }
852    fn in_cells(&self) -> bool {
853        true
854    }
855}
856
857/// An iTerm2 or Sixel image drawn over blank cells.
858struct OverlayGraphic {
859    inner: Arc<Inner>,
860    prepared: Arc<Prepared>,
861}
862
863impl fmt::Debug for OverlayGraphic {
864    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
865        f.debug_struct("OverlayGraphic")
866            .field("key", &self.prepared.key)
867            .finish()
868    }
869}
870
871impl Graphic for OverlayGraphic {
872    fn key(&self) -> u64 {
873        self.prepared.key
874    }
875    fn draw(&self, frame: usize) -> String {
876        match self.inner.escapes(&self.prepared).get(frame) {
877            Some(escape) => format!("\x1b7{escape}\x1b8"),
878            None => String::new(),
879        }
880    }
881}
882
883/// The [`PlacementSource`] of a [`MicroGraphics`].
884struct Source {
885    inner: Arc<Inner>,
886}
887
888impl PlacementSource for Source {
889    fn prepare(&self, line: Vec<Segment>) -> Vec<Segment> {
890        if self.inner.selection.mode == MicroMode::Text {
891            return line;
892        }
893        let renderer: Arc<dyn MicroRenderer> = Arc::new(ModeRenderer {
894            inner: Arc::clone(&self.inner),
895            inline: false,
896        });
897        // One line at a time: each row of a two-row asset gets its own cells
898        // here, and `placements` finds the whole box in the frame.
899        crate::render::rewrite_rows(
900            line,
901            &self.inner.registry,
902            &[renderer],
903            &self.inner.console,
904            true,
905        )
906    }
907
908    fn placements(&self, frame: &Frame) -> Vec<FramePlacement> {
909        let mode = self.inner.selection.mode;
910        if !mode.is_graphics() {
911            return Vec::new();
912        }
913        // Every tagged run: (frame row, column, width, tag).
914        let mut found: Vec<(usize, usize, usize, MicroMeta)> = Vec::new();
915        for row in 0..frame.height() {
916            let mut column = 0;
917            let mut open: Option<(MicroMeta, usize, usize)> = None;
918            let mut runs: Vec<(Option<MicroMeta>, usize)> = frame
919                .row(row)
920                .iter()
921                .map(|run| {
922                    let meta = frame
923                        .styles()
924                        .get(run.style())
925                        .and_then(MicroMeta::from_style);
926                    (meta, run.cells())
927                })
928                .collect();
929            runs.push((None, 0));
930            for (meta, cells) in runs {
931                let continues = matches!(
932                    (&open, &meta),
933                    (Some((o, _, _)), Some(m)) if o.id == m.id && o.row == m.row
934                );
935                if continues {
936                    if let Some(open) = open.as_mut() {
937                        open.2 += cells;
938                    }
939                } else {
940                    if let Some((meta, start, width)) = open.take() {
941                        found.push((row, start, width, meta));
942                    }
943                    open = meta.map(|meta| (meta, column, cells));
944                }
945                column += cells;
946            }
947        }
948        let at = |row: usize, column: usize, meta: &MicroMeta, which: usize| {
949            found.iter().any(|(r, c, w, m)| {
950                *r == row && *c == column && m.id == meta.id && m.row == which && *w == meta.cols
951            })
952        };
953        let mut out = Vec::new();
954        for (row, column, width, meta) in &found {
955            let (row, column, width) = (*row, *column, *width);
956            if meta.row > 0 {
957                // A second row whose first is not above it (scrolled off a
958                // viewport): Kitty's placeholders still need the image.
959                let first = row >= meta.row && at(row - meta.row, column, meta, 0);
960                if mode == MicroMode::Kitty && !first {
961                    out.extend(self.placement(row, column, width, meta, 1));
962                }
963                continue;
964            }
965            // An image over several rows only where they are all there.
966            let whole = (1..meta.rows).all(|below| at(row + below, column, meta, below));
967            let rows = match (whole, mode) {
968                (true, _) => meta.rows,
969                (false, MicroMode::Kitty) => 1,
970                (false, _) => continue,
971            };
972            out.extend(self.placement(row, column, width, meta, rows));
973        }
974        out
975    }
976
977    fn next_change(&self) -> Option<Duration> {
978        if !self.inner.selection.animate {
979            return None;
980        }
981        let now = self.inner.now();
982        let mut moving = lock(&self.inner.moving);
983        // An animation not drawn for a whole cycle (and a second) has left
984        // the view.
985        moving.retain(|(prepared, seen)| {
986            now.saturating_sub(*seen) <= prepared.duration() + Duration::from_secs(1)
987        });
988        moving
989            .iter()
990            .filter_map(|(prepared, _)| prepared.frame_at(now).1)
991            .min()
992    }
993
994    fn release(&self, retained: &[FramePlacement]) -> String {
995        self.inner.release(retained)
996    }
997}
998
999impl Source {
1000    /// The placement of the asset `meta` tags, `rows` high from `row`.
1001    fn placement(
1002        &self,
1003        row: usize,
1004        column: usize,
1005        width: usize,
1006        meta: &MicroMeta,
1007        rows: usize,
1008    ) -> Option<FramePlacement> {
1009        let asset = self.inner.registry.resolve(&meta.name)?;
1010        if asset.cols() != meta.cols || asset.rows() != meta.rows || width != meta.cols {
1011            return None;
1012        }
1013        let prepared = self.inner.prepared(asset)?;
1014        let (graphic, frame): (Arc<dyn Graphic>, usize) = match self.inner.selection.mode {
1015            MicroMode::Kitty => {
1016                let (id, _) = self.inner.kitty(&prepared, false)?;
1017                (
1018                    Arc::new(KittyGraphic {
1019                        inner: Arc::clone(&self.inner),
1020                        prepared,
1021                        id,
1022                    }),
1023                    0,
1024                )
1025            }
1026            MicroMode::Iterm | MicroMode::Sixel => {
1027                let frame = self.inner.overlay_frame(&prepared);
1028                self.inner.moving(&prepared);
1029                (
1030                    Arc::new(OverlayGraphic {
1031                        inner: Arc::clone(&self.inner),
1032                        prepared,
1033                    }),
1034                    frame,
1035                )
1036            }
1037            _ => return None,
1038        };
1039        Some(FramePlacement {
1040            row,
1041            column,
1042            cols: meta.cols,
1043            rows,
1044            graphic,
1045            frame,
1046        })
1047    }
1048}