1use 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#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
98pub enum MicroMode {
99 Kitty,
101 Iterm,
103 Sixel,
105 Blocks,
107 Text,
109}
110
111impl MicroMode {
112 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 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 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#[derive(Clone, Debug, PartialEq, Eq)]
149pub struct Selection {
150 pub mode: MicroMode,
151 pub reason: String,
153 pub cell: CellPixels,
155 pub cell_known: bool,
157 pub animate: bool,
159 pub preference: FallbackPreference,
161 pub blocks_first: bool,
163 pub color_system: Option<ColorSystem>,
165 pub warning: Option<String>,
167}
168
169impl Selection {
170 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 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 #[must_use]
203 pub fn animate(mut self, animate: bool) -> Selection {
204 self.animate = animate;
205 self
206 }
207}
208
209pub 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
281type Clock = Arc<dyn Fn() -> Duration + Send + Sync>;
283
284fn kitty_base() -> u32 {
287 ((std::process::id() % 200) + 32) << 16
288}
289
290#[derive(Default)]
291struct KittyState {
292 ids: HashMap<u64, u32>,
294 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 moving: Mutex<Vec<(Arc<Prepared>, Duration)>>,
309 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#[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 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 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 Err(inner) => MicroGraphics { inner },
378 }
379 }
380
381 #[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 #[must_use]
393 pub fn with_limits(self, limits: Limits) -> MicroGraphics {
394 self.rebuild(|inner| inner.limits = limits)
395 }
396
397 #[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 #[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 pub fn selection(&self) -> &Selection {
413 &self.inner.selection
414 }
415
416 pub fn registry(&self) -> &Arc<MicroRegistry> {
418 &self.inner.registry
419 }
420
421 pub fn renderer(&self) -> Arc<dyn MicroRenderer> {
423 Arc::new(ModeRenderer {
424 inner: Arc::clone(&self.inner),
425 inline: true,
426 })
427 }
428
429 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 pub fn source(&self) -> Arc<dyn PlacementSource> {
437 Arc::new(Source {
438 inner: Arc::clone(&self.inner),
439 })
440 }
441
442 pub fn close(&self) -> String {
446 self.inner.release(&[])
447 }
448
449 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 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 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 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 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 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 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 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 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 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 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 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
727fn 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
737fn 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 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
769struct 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
794struct 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 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 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
826struct 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
857struct 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
883struct 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 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 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 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 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 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 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}