elio 1.1.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::types::StaticImagePreloadViewport;
use super::{StaticImageKey, StaticImageOverlayRequest};
use crate::app::overlays::inline_image::ImageProtocol;
use crate::app::{App, jobs};
use std::collections::HashSet;

impl App {
    pub(in crate::app) fn refresh_static_image_preloads(&mut self) {
        let current_static = self.active_static_image_overlay_request();
        // When a deferred preview refresh is pending, preview_state.content still reflects the
        // previous selection.  Using its visual as "current" would cancel image-prepare jobs for
        // the actual current entry and retain or re-queue jobs for the wrong one.  Skip the
        // preview visual entirely; the correct job will be submitted once refresh_preview() fires
        // and preview_state.content is updated.
        let current_preview_visual = if self.preview.state.deferred_refresh_at.is_none() {
            self.active_preview_visual_overlay_request()
        } else {
            None
        };
        let current = current_static
            .as_ref()
            .cloned()
            .or(current_preview_visual.as_ref().cloned());
        let nearby = if let Some(request) = current_static.as_ref() {
            self.nearby_static_image_overlay_requests(Some(request))
        } else if current_preview_visual.is_some() {
            let mut requests = self.nearby_comic_preview_visual_overlay_requests();
            requests.extend(self.nearby_comic_entry_preview_visual_overlay_requests());
            requests.extend(self.nearby_epub_preview_visual_overlay_requests());
            requests.extend(self.nearby_epub_entry_preview_visual_overlay_requests());
            requests.extend(self.nearby_audio_preview_visual_overlay_requests());
            requests
        } else if self.preview.terminal_images.protocol == ImageProtocol::Sixel {
            self.nearby_static_image_overlay_requests(None)
        } else {
            Vec::new()
        };
        let desired = current
            .iter()
            .map(StaticImageKey::from_request)
            .chain(nearby.iter().map(StaticImageKey::from_request))
            .collect::<HashSet<_>>();
        // When a deferred preview refresh is pending, `desired` is computed without a
        // preview-visual current entry (see above), so it may be empty even though a
        // valid image-prepare job is already in-flight for the current entry.  Cancelling
        // that job here would starve it: every rapid-navigation keystroke would cancel and
        // re-queue the job, and it would never complete — leaving the preview blank until
        // the user takes a non-rapid action (folder navigation, preview scroll, etc.).
        // Skip the cancellation step while deferred; the correct jobs will be pruned on
        // the next non-deferred refresh_static_image_preloads() call.
        if self.preview.state.deferred_refresh_at.is_none() {
            self.preview
                .image
                .pending_prepares
                .retain(|key| desired.contains(key));
            let current_job = current
                .as_ref()
                .map(|request| self.image_prepare_request_for_overlay(request));
            let nearby_jobs = nearby
                .iter()
                .map(|request| self.image_prepare_request_for_overlay(request))
                .collect::<Vec<_>>();
            self.jobs
                .scheduler
                .retain_image_prepares(current_job.as_ref(), &nearby_jobs);
        }

        if let Some(request) = current.as_ref()
            && self.static_image_requires_prepare(request)
        {
            self.ensure_static_image_preload(request, jobs::ImageJobPriority::Current);
        }
        for request in &nearby {
            if self.static_image_requires_prepare(request) {
                self.ensure_static_image_preload(request, jobs::ImageJobPriority::Nearby);
            }
        }
        self.prefetch_visible_heavy_preview_entries();
    }

    pub(in crate::app) fn refresh_static_image_preloads_if_needed(&mut self) {
        let viewport = StaticImagePreloadViewport {
            selected: self.navigation.selected,
            scroll_row: self.navigation.scroll_row,
            cols: self.input.frame_state.metrics.cols.max(1),
            rows_visible: self.input.frame_state.metrics.rows_visible.max(1),
            preview_content_area: self.input.frame_state.preview_content_area,
            preview_media_area: self.input.frame_state.preview_media_area,
            protocol: self.preview.terminal_images.protocol,
            window: self.cached_terminal_window(),
        };
        if self.preview.image.preload_viewport == Some(viewport) {
            return;
        }
        self.preview.image.preload_viewport = Some(viewport);
        self.refresh_static_image_preloads();
    }

    pub(in crate::app) fn apply_image_prepare_build(
        &mut self,
        build: jobs::ImagePrepareBuild,
    ) -> bool {
        let key = StaticImageKey::from_parts(
            build.path.clone(),
            build.size,
            build.modified,
            build.target_width_px,
            build.target_height_px,
            build.force_render_to_cache,
            build.prepare_inline_payload,
        );
        self.preview.image.pending_prepares.remove(&key);
        let is_current = self
            .active_static_image_overlay_request()
            .as_ref()
            .is_some_and(|request| StaticImageKey::from_request(request) == key)
            || self
                .active_preview_visual_overlay_request_unchecked()
                .as_ref()
                .is_some_and(|request| StaticImageKey::from_request(request) == key);
        if build.canceled {
            self.refresh_static_image_preloads();
            return is_current;
        }

        match build.result {
            Some(prepared) => {
                self.preview.image.failed_images.remove(&key);
                self.preview
                    .image
                    .dimensions
                    .insert(key.clone(), prepared.dimensions);
                if let Some(payload) = prepared.inline_payload {
                    self.remember_static_image_inline_payload(key.clone(), payload);
                }
                if let (Some(dcs), Some(dcs_key)) = (prepared.sixel_dcs, prepared.sixel_dcs_key) {
                    self.remember_sixel_dcs(dcs_key, dcs);
                }
                if prepared.display_path != build.path {
                    self.remember_rendered_static_image(key, prepared.display_path);
                }
                self.refresh_static_image_preloads();
                is_current
            }
            None => {
                self.preview.image.failed_images.insert(key);
                if is_current {
                    self.refresh_preview();
                    true
                } else {
                    false
                }
            }
        }
    }

    fn nearby_static_image_overlay_requests(
        &self,
        current: Option<&StaticImageOverlayRequest>,
    ) -> Vec<StaticImageOverlayRequest> {
        let preload_limit = if self.needs_slow_sixel_navigation_workaround() {
            super::STATIC_IMAGE_PRELOAD_LIMIT_SLOW_SIXEL
        } else {
            super::STATIC_IMAGE_PRELOAD_LIMIT
        };
        let current_path = current.as_ref().map(|request| &request.path);
        let mut requests = self
            .visible_entry_indices()
            .into_iter()
            .filter(|&index| index != self.navigation.selected)
            .filter_map(|index| {
                self.navigation
                    .entries
                    .get(index)
                    .and_then(|entry| self.static_image_overlay_request_for_entry(entry))
                    .map(|request| (index.abs_diff(self.navigation.selected), request))
            })
            .filter(|(_, request)| current_path != Some(&request.path))
            .collect::<Vec<_>>();
        requests.sort_by_key(|(distance, _)| *distance);
        requests
            .into_iter()
            .map(|(_, request)| request)
            .take(preload_limit)
            .collect()
    }

    fn ensure_static_image_preload(
        &mut self,
        request: &StaticImageOverlayRequest,
        priority: jobs::ImageJobPriority,
    ) {
        let key = StaticImageKey::from_request(request);
        if self.preview.image.failed_images.contains(&key)
            || self.preview.image.pending_prepares.contains(&key)
            || self
                .cached_prepared_static_image_for_overlay(&key, request)
                .is_some()
        {
            return;
        }

        let job = self.image_prepare_request_for_overlay(request);
        let submit = match priority {
            jobs::ImageJobPriority::Current => self.jobs.scheduler.submit_image_prepare(job),
            jobs::ImageJobPriority::Nearby => self.jobs.scheduler.submit_nearby_image_prepare(job),
        };
        if submit {
            self.preview.image.pending_prepares.insert(key);
        }
    }

    fn image_prepare_request_for_overlay(
        &mut self,
        request: &StaticImageOverlayRequest,
    ) -> jobs::ImagePrepareRequest {
        let sixel_prepare = if self.preview.terminal_images.protocol == ImageProtocol::Sixel {
            self.cached_terminal_window()
                .map(|window_size| jobs::SixelPrepareConfig {
                    area_width: request.area.width,
                    area_height: request.area.height,
                    window_size,
                })
        } else {
            None
        };
        jobs::ImagePrepareRequest {
            path: request.path.clone(),
            size: request.size,
            modified: request.modified,
            target_width_px: request.target_width_px,
            target_height_px: request.target_height_px,
            ffmpeg_available: self.ffmpeg_available(),
            resvg_available: self.resvg_available(),
            magick_available: self.magick_available(),
            force_render_to_cache: request.force_render_to_cache,
            prepare_inline_payload: request.prepare_inline_payload,
            sixel_prepare,
        }
    }
}