elio 1.3.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::*;
use crate::app::FileClass;
use crate::file_info;
use crate::preview::{PreviewWorkClass, preview_work_class, should_build_preview_in_background};

const AUDIO_ENTRY_PREFETCH_OFFSETS: [isize; 4] = [1, -1, 2, -2];
const VISIBLE_HEAVY_PREVIEW_PREFETCH_LIMIT: usize = 4;
const VISIBLE_MIXED_HEAVY_PREVIEW_PREFETCH_LIMIT: usize = 3;

impl App {
    pub(in crate::app) fn prefetch_visible_heavy_preview_entries(&mut self) {
        if !self.uses_sixel_image_protocol()
            || self.preview.state.deferred_refresh_at.is_some()
            || self.browser_wheel_burst_active()
        {
            return;
        }

        self.prefetch_visible_mixed_heavy_previews(VISIBLE_MIXED_HEAVY_PREVIEW_PREFETCH_LIMIT);
        self.prefetch_visible_nearby_comic_entries(VISIBLE_HEAVY_PREVIEW_PREFETCH_LIMIT);
        self.prefetch_visible_nearby_epub_entries(VISIBLE_HEAVY_PREVIEW_PREFETCH_LIMIT);
        self.prefetch_visible_nearby_pdf_entries(VISIBLE_HEAVY_PREVIEW_PREFETCH_LIMIT);
    }

    pub(crate) fn process_preview_prefetch_timers(&mut self) -> bool {
        let Some(deadline) = self.preview.state.prefetch_ready_at else {
            return false;
        };
        if Instant::now() < deadline
            || self.preview.state.deferred_refresh_at.is_some()
            || self.browser_wheel_burst_active()
        {
            return false;
        }

        self.preview.state.prefetch_ready_at = None;
        self.prefetch_nearby_comic_pages();
        self.prefetch_nearby_comic_entries();
        self.prefetch_nearby_epub_sections();
        self.prefetch_nearby_audio_previews();
        self.prefetch_nearby_previews();
        false
    }

    pub(crate) fn pending_preview_prefetch_timer(&self) -> Option<std::time::Duration> {
        self.preview
            .state
            .prefetch_ready_at
            .map(|deadline| deadline.saturating_duration_since(Instant::now()))
    }

    pub(in crate::app) fn schedule_preview_prefetch(&mut self) {
        self.preview.state.prefetch_ready_at = self
            .selected_entry()
            .map(|_| Instant::now() + PREVIEW_PREFETCH_IDLE_DELAY);
    }

    fn prefetch_nearby_previews(&mut self) {
        let mut queued = 0;
        for offset in [1isize, -1, 2, -2, 3, -3] {
            if queued >= PREVIEW_PREFETCH_LIMIT {
                break;
            }

            let target = self.navigation.selected as isize + offset;
            if target < 0 {
                continue;
            }
            let Some(entry) = self.navigation.entries.get(target as usize).cloned() else {
                continue;
            };
            let variant = self.preview_request_options_for_entry(&entry);
            let work_class = preview_work_class(&entry, &variant);
            if !should_build_preview_in_background(&entry)
                || work_class == PreviewWorkClass::Heavy
                || self.cached_preview_for(&entry, &variant).is_some()
            {
                continue;
            }

            let request = self.build_full_preview_request(
                entry,
                variant,
                PreviewPriority::Low,
                PreviewWorkClass::Light,
            );
            if self.jobs.scheduler.submit_preview(request) {
                queued += 1;
            }
        }
    }

    pub(in crate::app) fn prefetch_nearby_audio_previews(&mut self) {
        let Some(current_entry) = self.selected_entry() else {
            return;
        };
        if !is_audio_entry(current_entry) {
            return;
        }
        let current_variant = self.current_preview_request_options();
        if self
            .cached_preview_for(current_entry, &current_variant)
            .is_none()
        {
            return;
        }

        for entry in self.nearby_audio_candidates() {
            let variant = self.preview_request_options_for_entry(&entry);
            if self.cached_preview_for(&entry, &variant).is_some() {
                continue;
            }

            let request = self.build_full_preview_request(
                entry.clone(),
                variant.clone(),
                PreviewPriority::Low,
                preview_work_class(&entry, &variant),
            );
            let _ = self.jobs.scheduler.submit_preview(request);
        }
    }

    pub(in crate::app) fn nearby_audio_preview_visual_overlay_requests(
        &self,
    ) -> Vec<crate::app::overlays::images::StaticImageOverlayRequest> {
        let Some(entry) = self.selected_entry() else {
            return Vec::new();
        };
        if !is_audio_entry(entry) {
            return Vec::new();
        }
        let Some(area) = self.input.frame_state.preview_media_area else {
            return Vec::new();
        };

        self.nearby_audio_candidates()
            .into_iter()
            .filter_map(|entry| {
                let variant = self.preview_request_options_for_entry(&entry);
                let cached = self.cached_preview_for(&entry, &variant)?;
                let visual = cached.preview_visual.as_ref()?;
                (cached.kind == crate::preview::PreviewKind::Audio
                    && visual.kind == crate::preview::PreviewVisualKind::Cover)
                    .then(|| {
                        self.preview_visual_overlay_request_for_visual(cached.kind, visual, area)
                    })
            })
            .collect()
    }

    pub(in crate::app) fn refreshes_image_preloads_for_nearby_audio_preview(
        &self,
        entry: &Entry,
        variant: &crate::preview::PreviewRequestOptions,
    ) -> bool {
        self.nearby_audio_candidates().into_iter().any(|candidate| {
            candidate.path == entry.path
                && candidate.size == entry.size
                && candidate.modified == entry.modified
                && variant == &self.preview_request_options_for_entry(&candidate)
        })
    }

    fn nearby_audio_candidates(&self) -> Vec<Entry> {
        AUDIO_ENTRY_PREFETCH_OFFSETS
            .into_iter()
            .filter_map(|offset| {
                let target = self.navigation.selected as isize + offset;
                if target < 0 {
                    return None;
                }
                let entry = self.navigation.entries.get(target as usize)?.clone();
                is_audio_entry(&entry).then_some(entry)
            })
            .collect()
    }

    fn prefetch_visible_mixed_heavy_previews(&mut self, limit: usize) {
        let mut queued = 0usize;
        let mut candidates = self
            .visible_entry_indices()
            .into_iter()
            .filter(|&index| index != self.navigation.selected)
            .filter_map(|index| {
                self.navigation.entries.get(index).cloned().map(|entry| {
                    let distance = index.abs_diff(self.navigation.selected);
                    (distance, entry)
                })
            })
            .collect::<Vec<_>>();
        candidates.sort_by_key(|(distance, _)| *distance);

        for (_, entry) in candidates {
            if queued >= limit {
                break;
            }

            let variant = self.preview_request_options_for_entry(&entry);
            let work_class = preview_work_class(&entry, &variant);
            if !should_build_preview_in_background(&entry)
                || work_class != PreviewWorkClass::Heavy
                || self.cached_preview_for(&entry, &variant).is_some()
                || self.should_defer_pdf_document_preview(&entry)
            {
                continue;
            }

            let request = self.build_full_preview_request(
                entry,
                variant,
                PreviewPriority::Low,
                PreviewWorkClass::Heavy,
            );
            if self.jobs.scheduler.submit_preview(request) {
                queued += 1;
            }
        }
    }
}

fn is_audio_entry(entry: &Entry) -> bool {
    file_info::inspect_entry_cached(entry).builtin_class == FileClass::Audio
}