elio 1.3.0

Snappy, batteries-included terminal file manager with rich previews, inline images, bulk actions, and trash support.
Documentation
use super::super::*;
use super::images;
use super::inline_image::ImageProtocol;
use ratatui::layout::{Constraint, Direction, Layout, Rect};

const PREVIEW_INLINE_COVER_MIN_HEIGHT: u16 = 6;
const PREVIEW_INLINE_COVER_MAX_HEIGHT: u16 = 12;
const PREVIEW_LARGE_INLINE_COVER_MAX_HEIGHT: u16 = 18;
const PREVIEW_INLINE_VIDEO_COVER_MAX_HEIGHT: u16 = 18;
const PREVIEW_INLINE_MIN_TEXT_HEIGHT: u16 = 6;
const PREVIEW_INLINE_PAGE_MIN_HEIGHT: u16 = 8;
const PREVIEW_INLINE_PAGE_MIN_TEXT_HEIGHT: u16 = 6;

impl App {
    pub(crate) fn preview_visual_rows(&self, area: Rect) -> Option<u16> {
        if !self.terminal_image_overlay_available()
            || self.preview.state.content.preview_visual.is_none()
        {
            return None;
        }
        let layout = self.current_preview_visual_layout()?;
        match layout {
            preview::PreviewVisualLayout::FullHeight => {
                let rows = (area.width >= 12 && area.height > 0).then_some(area.height)?;
                return (!self.preview_visual_failed_for_rows(area, rows)).then_some(rows);
            }
            preview::PreviewVisualLayout::Inline | preview::PreviewVisualLayout::LargeInline => {}
        }
        if self.current_preview_visual_kind() == Some(preview::PreviewVisualKind::PageImage) {
            if area.width < 12
                || area.height
                    < PREVIEW_INLINE_PAGE_MIN_HEIGHT + PREVIEW_INLINE_PAGE_MIN_TEXT_HEIGHT
            {
                return None;
            }
            let rows = area
                .height
                .saturating_sub(PREVIEW_INLINE_PAGE_MIN_TEXT_HEIGHT);
            return (!self.preview_visual_failed_for_rows(area, rows)).then_some(rows);
        }
        if area.width < 12
            || area.height < PREVIEW_INLINE_COVER_MIN_HEIGHT + PREVIEW_INLINE_MIN_TEXT_HEIGHT
        {
            return None;
        }

        let (height_divisor, max_height) = match layout {
            preview::PreviewVisualLayout::LargeInline => (2, PREVIEW_LARGE_INLINE_COVER_MAX_HEIGHT),
            preview::PreviewVisualLayout::Inline
                if self.preview.state.content.kind == preview::PreviewKind::Video =>
            {
                (2, PREVIEW_INLINE_VIDEO_COVER_MAX_HEIGHT)
            }
            preview::PreviewVisualLayout::Inline => (3, PREVIEW_INLINE_COVER_MAX_HEIGHT),
            preview::PreviewVisualLayout::FullHeight => unreachable!(),
        };
        let rows = (area.height / height_divisor)
            .clamp(PREVIEW_INLINE_COVER_MIN_HEIGHT, max_height)
            .min(area.height.saturating_sub(PREVIEW_INLINE_MIN_TEXT_HEIGHT));
        (!self.preview_visual_failed_for_rows(area, rows)).then_some(rows)
    }

    pub(in crate::app) fn active_preview_visual_overlay_request(
        &self,
    ) -> Option<images::StaticImageOverlayRequest> {
        if self.preview_uses_image_overlay() {
            return None;
        }

        self.active_preview_visual_overlay_request_unchecked()
    }

    pub(in crate::app) fn active_preview_visual_overlay_request_unchecked(
        &self,
    ) -> Option<images::StaticImageOverlayRequest> {
        if !self.terminal_image_overlay_available() {
            return None;
        }

        let area = self.current_preview_visual_area()?;
        if area.width == 0 || area.height == 0 {
            return None;
        }

        self.preview_visual_overlay_request_for_area(area)
    }

    fn current_preview_visual_kind(&self) -> Option<preview::PreviewVisualKind> {
        self.preview
            .state
            .content
            .preview_visual
            .as_ref()
            .map(|visual| visual.kind)
    }

    fn current_preview_visual_layout(&self) -> Option<preview::PreviewVisualLayout> {
        self.preview
            .state
            .content
            .preview_visual
            .as_ref()
            .map(|visual| visual.layout)
    }

    fn preview_visual_failed_for_rows(&self, area: Rect, rows: u16) -> bool {
        let request = match self.preview_visual_overlay_request_for_area(Rect {
            x: area.x,
            y: area.y,
            width: area.width,
            height: rows,
        }) {
            Some(request) => request,
            None => return false,
        };
        self.preview
            .image
            .failed_images
            .contains(&images::StaticImageKey::from_request(&request))
    }

    fn preview_visual_overlay_request_for_area(
        &self,
        area: Rect,
    ) -> Option<images::StaticImageOverlayRequest> {
        let visual = self.preview.state.content.preview_visual.as_ref()?;
        Some(self.preview_visual_overlay_request_for_visual(
            self.preview.state.content.kind,
            visual,
            area,
        ))
    }

    pub(in crate::app) fn preview_visual_overlay_request_for_visual(
        &self,
        _preview_kind: preview::PreviewKind,
        visual: &preview::PreviewVisual,
        area: Rect,
    ) -> images::StaticImageOverlayRequest {
        images::StaticImageOverlayRequest {
            path: visual.path.clone(),
            size: visual.size,
            modified: visual.modified,
            area,
            target_width_px: images::image_target_width_px(area, self.cached_terminal_window()),
            target_height_px: images::image_target_height_px(area, self.cached_terminal_window()),
            mode: images::StaticImageOverlayMode::Inline,
            force_render_to_cache: self.preview_visual_force_render_to_cache(visual),
            prepare_inline_payload: self.preview.terminal_images.protocol
                == ImageProtocol::ItermInline,
        }
    }

    fn current_preview_visual_area(&self) -> Option<Rect> {
        if let Some(body_area) = self.current_preview_body_render_area() {
            let media_rows = self.preview_visual_rows(body_area)?;
            return Some(
                Layout::default()
                    .direction(Direction::Vertical)
                    .constraints([Constraint::Length(media_rows), Constraint::Min(0)])
                    .split(body_area)[0],
            );
        }
        self.input.frame_state.preview_media_area
    }

    fn current_preview_body_render_area(&self) -> Option<Rect> {
        let body_area = self.input.frame_state.preview_body_area?;
        Some(if body_area.width >= 6 {
            Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Min(0), Constraint::Length(1)])
                .split(body_area)[0]
        } else {
            Layout::default()
                .direction(Direction::Horizontal)
                .constraints([Constraint::Min(0)])
                .split(body_area)[0]
        })
    }
}

#[cfg(test)]
mod tests;