lazyspec 0.8.0

A little TUI & CLI for project documentation.
Documentation
use crate::engine::cache::DiskCache;
use crate::engine::document::DocMeta;
use crate::engine::refs::RefExpander;
use std::fs;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;

use super::{App, AppEvent};

impl App {
    pub fn request_expansion(&mut self, tx: &crossbeam_channel::Sender<AppEvent>) {
        let doc_path = match self.selected_doc_meta() {
            Some(meta) => meta.path.clone(),
            None => return,
        };

        if self.expanded_body_cache.contains_key(&doc_path) {
            return;
        }

        if self.expansion_in_flight.as_ref() == Some(&doc_path) {
            return;
        }

        if let Some(cancel) = &self.expansion_cancel {
            cancel.store(true, Ordering::Relaxed);
        }

        let cancel = Arc::new(AtomicBool::new(false));
        self.expansion_cancel = Some(cancel.clone());
        self.expansion_in_flight = Some(doc_path.clone());

        let root = self.store.root().to_path_buf();
        let tx = tx.clone();
        let disk_cache = self.disk_cache.clone();
        std::thread::spawn(move || {
            let full_path = root.join(&doc_path);
            let content = match fs::read_to_string(&full_path) {
                Ok(c) => c,
                Err(_) => return,
            };
            let body = match DocMeta::extract_body(&content) {
                Ok(b) => b,
                Err(_) => return,
            };

            if !body.contains("@ref ") {
                let body_hash = DiskCache::body_hash(&body);
                let _ = tx.send(AppEvent::ExpansionResult {
                    path: doc_path,
                    body,
                    body_hash,
                });
                return;
            }

            let body_hash = DiskCache::body_hash(&body);

            if let Some(cached) = disk_cache.read(&doc_path, body_hash) {
                let _ = tx.send(AppEvent::ExpansionResult {
                    path: doc_path,
                    body: cached,
                    body_hash,
                });
                return;
            }

            let expander = RefExpander::new(root);
            match expander.expand_cancellable(&body, &cancel) {
                Ok(Some(expanded)) => {
                    let _ = tx.send(AppEvent::ExpansionResult {
                        path: doc_path,
                        body: expanded,
                        body_hash,
                    });
                }
                Ok(None) => {}
                Err(_) => {
                    let _ = tx.send(AppEvent::ExpansionResult {
                        path: doc_path,
                        body,
                        body_hash,
                    });
                }
            }
        });
    }

    pub fn request_diagram_render(
        &mut self,
        block: &crate::tui::content::diagram::DiagramBlock,
        tx: &crossbeam_channel::Sender<AppEvent>,
    ) {
        let hash = crate::tui::content::diagram::source_hash(&block.source);

        if self.diagram_cache.get(hash).is_some() {
            return;
        }

        if !self.tool_availability.is_available(&block.language) {
            return;
        }

        self.diagram_cache.mark_rendering(hash);

        let source = block.source.clone();
        let language = block.language.clone();
        let cache_dir = self.diagram_cache.cache_dir().to_path_buf();
        let tx = tx.clone();
        let ascii = self.ascii_diagrams;

        std::thread::spawn(move || {
            let block = crate::tui::content::diagram::DiagramBlock {
                language,
                source,
                byte_range: 0..0,
            };

            let entry =
                if ascii && block.language == crate::tui::content::diagram::DiagramLanguage::D2 {
                    match crate::tui::content::diagram::render_diagram_text(&block, &cache_dir) {
                        Ok(text) => crate::tui::content::diagram::DiagramCacheEntry::Text(text),
                        Err(err) => {
                            crate::tui::content::diagram::DiagramCacheEntry::Failed(err.to_string())
                        }
                    }
                } else {
                    match crate::tui::content::diagram::render_diagram(&block, &cache_dir) {
                        Ok(path) => crate::tui::content::diagram::DiagramCacheEntry::Image(path),
                        Err(err) => {
                            crate::tui::content::diagram::DiagramCacheEntry::Failed(err.to_string())
                        }
                    }
                };

            let _ = tx.send(AppEvent::DiagramRendered {
                source_hash: hash,
                entry,
            });
        });
    }

    pub fn filtered_docs(&mut self) -> Vec<&DocMeta> {
        use crate::engine::store::Filter;

        if self.filtered_docs_cache.is_none() {
            let mut docs = self.store.list(&Filter {
                doc_type: None,
                status: self.filter_status.clone(),
                tag: self.filter_tag.clone(),
            });
            docs.sort_by(|a, b| DocMeta::sort_by_date(a, b));
            self.filtered_docs_cache = Some(docs.iter().map(|d| d.path.clone()).collect());
        }
        self.filtered_docs_cache
            .as_deref()
            .unwrap_or_default()
            .iter()
            .filter_map(|p| self.store.get(p))
            .collect()
    }

    pub fn filtered_docs_count(&mut self) -> usize {
        if self.filtered_docs_cache.is_none() {
            self.filtered_docs();
        }
        self.filtered_docs_cache.as_ref().map_or(0, |c| c.len())
    }

    pub fn selected_filtered_doc(&mut self) -> Option<&DocMeta> {
        if self.filtered_docs_cache.is_none() {
            self.filtered_docs();
        }
        self.filtered_docs_cache
            .as_deref()
            .unwrap_or_default()
            .get(self.selected_doc)
            .and_then(|p| self.store.get(p))
    }
}