leaf-markdown-viewer 1.26.2

Terminal Markdown previewer with a GUI-like experience
use crate::{
    markdown::{parse_markdown_with_width, toc::TocEntry},
    theme::{
        app_theme, current_syntect_theme, current_theme_selection, set_theme_preset,
        set_theme_selection, theme_preset_index, ThemePreset, ThemeSelection, THEME_PRESETS,
    },
};
use ratatui::text::Line;
use syntect::{highlighting::ThemeSet, parsing::SyntaxSet};

use super::App;

#[derive(Clone)]
pub(crate) struct ThemePreviewCacheEntry {
    pub(super) lines: Vec<Line<'static>>,
    pub(super) toc: Vec<TocEntry>,
}

pub(crate) struct ThemePickerState {
    pub(super) open: bool,
    pub(super) index: usize,
    pub(super) original: Option<ThemeSelection>,
    pub(super) original_preview: Option<ThemePreviewCacheEntry>,
    pub(super) preview_cache: Vec<Option<ThemePreviewCacheEntry>>,
}

impl App {
    pub(crate) fn open_theme_picker(&mut self) {
        self.theme_picker.open = true;
        let current = current_theme_selection();
        self.theme_picker.index = theme_preset_index(current.preset_hint());
        self.theme_picker.original = Some(current);
        self.theme_picker.original_preview = Some(ThemePreviewCacheEntry {
            lines: self.lines.clone(),
            toc: self.toc.clone(),
        });
        self.store_current_theme_preview();
    }

    pub(crate) fn close_theme_picker(&mut self) {
        self.theme_picker.open = false;
        self.theme_picker.original = None;
        self.theme_picker.original_preview = None;
    }

    pub(crate) fn is_theme_picker_open(&self) -> bool {
        self.theme_picker.open
    }

    pub(crate) fn theme_picker_index(&self) -> usize {
        self.theme_picker.index
    }

    #[cfg(test)]
    pub(crate) fn theme_picker_original(&self) -> Option<ThemeSelection> {
        self.theme_picker.original.clone()
    }

    pub(crate) fn theme_picker_reference_preset(&self) -> Option<ThemePreset> {
        self.theme_picker
            .original
            .as_ref()
            .and_then(ThemeSelection::as_preset)
            .or_else(|| current_theme_selection().as_preset())
    }

    pub(crate) fn move_theme_picker_up(&mut self) {
        let total = THEME_PRESETS.len();
        if total == 0 {
            return;
        }
        if self.theme_picker.index == 0 {
            self.theme_picker.index = total - 1;
        } else {
            self.theme_picker.index -= 1;
        }
    }

    pub(crate) fn move_theme_picker_down(&mut self) {
        let total = THEME_PRESETS.len();
        if total == 0 {
            return;
        }
        self.theme_picker.index = (self.theme_picker.index + 1) % total;
    }

    pub(crate) fn set_theme_picker_index(&mut self, idx: usize) -> bool {
        if idx < THEME_PRESETS.len() {
            self.theme_picker.index = idx;
            true
        } else {
            false
        }
    }

    pub(crate) fn selected_theme_preset(&self) -> Option<ThemePreset> {
        THEME_PRESETS.get(self.theme_picker.index).copied()
    }

    pub(crate) fn preview_theme_preset(
        &mut self,
        preset: ThemePreset,
        ss: &SyntaxSet,
        themes: &ThemeSet,
    ) {
        if current_theme_selection().as_preset() == Some(preset) {
            return;
        }
        set_theme_preset(preset);
        let cached = self
            .theme_picker
            .preview_cache
            .get(theme_preset_index(preset))
            .and_then(|entry| entry.as_ref())
            .cloned();
        if let Some(entry) = cached {
            self.replace_content(crate::markdown::ParseResult::preview(
                entry.lines,
                entry.toc,
            ));
            return;
        }

        let theme = current_syntect_theme(themes);
        let at = app_theme();
        let parsed = parse_markdown_with_width(
            &self.source,
            ss,
            theme,
            self.render_width,
            &at.markdown,
            self.file_mode,
            self.code_line_numbers,
        );
        self.store_theme_preview(preset, &parsed.lines, &parsed.toc);
        self.replace_content(parsed);
    }

    pub(crate) fn restore_theme_picker_preview(&mut self, ss: &SyntaxSet, themes: &ThemeSet) {
        if let Some(original) = self.theme_picker.original.take() {
            set_theme_selection(original);
            if let Some(entry) = self.theme_picker.original_preview.take() {
                self.replace_content(crate::markdown::ParseResult::preview(
                    entry.lines,
                    entry.toc,
                ));
            } else {
                let theme = current_syntect_theme(themes);
                let at = app_theme();
                let parsed = parse_markdown_with_width(
                    &self.source,
                    ss,
                    theme,
                    self.render_width,
                    &at.markdown,
                    self.file_mode,
                    self.code_line_numbers,
                );
                self.replace_content(parsed);
            }
        }
        self.close_theme_picker();
    }

    pub(crate) fn store_theme_preview(
        &mut self,
        preset: ThemePreset,
        lines: &[Line<'static>],
        toc: &[TocEntry],
    ) {
        let idx = theme_preset_index(preset);
        if let Some(slot) = self.theme_picker.preview_cache.get_mut(idx) {
            *slot = Some(ThemePreviewCacheEntry {
                lines: lines.to_vec(),
                toc: toc.to_vec(),
            });
        }
    }

    pub(crate) fn store_current_theme_preview(&mut self) {
        let Some(preset) = current_theme_selection().as_preset() else {
            return;
        };
        let idx = theme_preset_index(preset);
        if let Some(slot) = self.theme_picker.preview_cache.get_mut(idx) {
            *slot = Some(ThemePreviewCacheEntry {
                lines: self.lines.clone(),
                toc: self.toc.clone(),
            });
        }
    }

    pub(crate) fn store_current_theme_preview_from(
        &mut self,
        lines: &[Line<'static>],
        toc: &[TocEntry],
    ) {
        let Some(preset) = current_theme_selection().as_preset() else {
            return;
        };
        self.store_theme_preview(preset, lines, toc);
    }

    pub(crate) fn invalidate_theme_preview_cache(&mut self) {
        self.theme_picker.preview_cache.fill(None);
    }

    #[cfg(test)]
    pub(crate) fn has_cached_theme_preview(&self, preset: ThemePreset) -> bool {
        self.theme_picker
            .preview_cache
            .get(theme_preset_index(preset))
            .and_then(|entry| entry.as_ref())
            .is_some()
    }
}