use std::sync::Arc;
use std::sync::mpsc::{self, TryRecvError};
use std::thread;
use ratatui::text::Text;
use crate::engine::{cache, viewer_async};
use crate::layout::setup::{RightPaneContent, UblxState};
use crate::render::viewers::{csv_handler, markdown, syntect_text};
use crate::themes;
#[must_use]
pub fn build_markdown_cache_entry(
path: &str,
raw: &str,
content_width: u16,
theme_key: String,
content_identity: cache::ViewerContentIdentity,
) -> cache::ViewerTextCacheEntry {
let doc = markdown::parse_markdown(raw);
let text = doc.to_text(content_width);
let line_count = text.lines.len();
cache::ViewerTextCacheEntry {
path: path.to_string(),
content_width,
theme_name: theme_key,
content_identity,
line_count,
text,
syntect: None,
}
}
fn syntect_cache_entry_from_text(
p: &cache::CodeViewerCacheParams<'_>,
text: Text<'static>,
content_identity: cache::ViewerContentIdentity,
) -> cache::ViewerTextCacheEntry {
let line_count = text.lines.len();
cache::ViewerTextCacheEntry {
path: p.path.to_string(),
content_width: p.content_width,
theme_name: p.theme_name.to_string(),
content_identity,
line_count,
text,
syntect: Some((p.appearance, p.category)),
}
}
#[must_use]
pub fn build_syntect_cache_entry(
p: &cache::CodeViewerCacheParams<'_>,
content_identity: cache::ViewerContentIdentity,
) -> cache::ViewerTextCacheEntry {
let text =
syntect_text::highlight_viewer_with_appearance(p.raw, p.path, p.category, p.appearance);
syntect_cache_entry_from_text(p, text, content_identity)
}
#[must_use]
pub fn build_csv_cache_entry(
path: &str,
raw: &str,
content_width: u16,
theme_key: String,
content_identity: cache::ViewerContentIdentity,
) -> Option<cache::ViewerTextCacheEntry> {
let rows = csv_handler::parse_csv(raw, Some(path)).ok()?;
if rows.is_empty() {
return None;
}
let (table_string, line_count) = csv_handler::table_string_and_line_count(&rows, content_width);
let text = csv_handler::table_string_to_text(&table_string);
debug_assert_eq!(line_count, text.lines.len());
Some(cache::ViewerTextCacheEntry {
path: path.to_string(),
content_width,
theme_name: theme_key,
content_identity,
line_count,
text,
syntect: None,
})
}
fn job_key_for(
rc: &RightPaneContent,
content_width: u16,
kind: viewer_async::ViewerAsyncJobKind,
) -> Option<viewer_async::ViewerAsyncJobKey> {
let path = rc.snap_meta.path.as_deref()?.to_string();
Some(viewer_async::ViewerAsyncJobKey {
path,
content_width,
theme_name: themes::current().name.to_string(),
kind,
})
}
fn spawn_job(
tx: mpsc::Sender<viewer_async::ViewerAsyncDone>,
done_key: viewer_async::ViewerAsyncJobKey,
work: impl FnOnce() -> viewer_async::ViewerAsyncResult + Send + 'static,
) {
let _ = thread::Builder::new()
.name("ublx-viewer-async".into())
.spawn(move || {
let result = work();
let _ = tx.send(viewer_async::ViewerAsyncDone {
key: done_key,
result,
});
});
}
fn sync_worker_palette(theme_name: &str) {
themes::set_current(Some(theme_name));
}
fn begin_viewer_async_job(
state: &mut UblxState,
rc: &RightPaneContent,
content_width: u16,
kind: viewer_async::ViewerAsyncJobKind,
) -> Option<(
mpsc::Sender<viewer_async::ViewerAsyncDone>,
viewer_async::ViewerAsyncJobKey,
)> {
let key = job_key_for(rc, content_width, kind)?;
if state.viewer_async.pending_key.as_ref() == Some(&key) && state.viewer_async.rx.is_some() {
return None;
}
state.viewer_text_cache = None;
state.viewer_async.clear();
let (tx, rx) = mpsc::channel();
state.viewer_async.rx = Some(rx);
state.viewer_async.pending_key = Some(key.clone());
Some((tx, key))
}
pub fn poll_viewer_async(state: &mut UblxState, rc: &RightPaneContent) {
let recv = match state.viewer_async.rx.as_ref() {
Some(rx) => rx.try_recv(),
None => return,
};
match recv {
Ok(done) => {
if state.viewer_async.pending_key.as_ref() != Some(&done.key) {
state.viewer_async.rx = None;
state.viewer_async.pending_key = None;
return;
}
let path_ok = rc
.snap_meta
.path
.as_deref()
.is_some_and(|p| p == done.key.path);
if !path_ok {
state.viewer_async.rx = None;
state.viewer_async.pending_key = None;
return;
}
match done.result {
viewer_async::ViewerAsyncResult::Markdown(e)
| viewer_async::ViewerAsyncResult::Code(e) => {
state.viewer_text_cache = Some(e);
state.viewer_async.pending_key = None;
state.viewer_async.rx = None;
}
viewer_async::ViewerAsyncResult::Csv(key, Some(e)) => {
state.csv_table_text_lru.insert(key, e);
state.viewer_async.pending_key = None;
state.viewer_async.rx = None;
}
viewer_async::ViewerAsyncResult::Csv(_, None) => {
state.viewer_async.pending_key = None;
state.viewer_async.rx = None;
}
}
}
Err(TryRecvError::Empty) => {}
Err(TryRecvError::Disconnected) => {
state.viewer_async.rx = None;
state.viewer_async.pending_key = None;
}
}
}
pub fn reset_viewer_async(state: &mut UblxState) {
state.viewer_async.clear();
}
pub fn schedule_markdown(
state: &mut UblxState,
rc: &RightPaneContent,
content_width: u16,
path: &str,
raw: Arc<str>,
theme_key: String,
) {
let Some((tx, done_key)) = begin_viewer_async_job(
state,
rc,
content_width,
viewer_async::ViewerAsyncJobKind::Markdown,
) else {
return;
};
let content_identity = cache::viewer_content_identity(raw.as_ref(), rc.snap_meta.mtime_ns);
let path = path.to_string();
spawn_job(tx, done_key, move || {
sync_worker_palette(theme_key.as_str());
viewer_async::ViewerAsyncResult::Markdown(build_markdown_cache_entry(
&path,
raw.as_ref(),
content_width,
theme_key,
content_identity,
))
});
}
pub fn schedule_syntect(
state: &mut UblxState,
rc: &RightPaneContent,
content_width: u16,
path: &str,
raw: Arc<str>,
theme_name: String,
) {
let Some((tx, done_key)) = begin_viewer_async_job(
state,
rc,
content_width,
viewer_async::ViewerAsyncJobKind::Code,
) else {
return;
};
let content_identity = cache::viewer_content_identity(raw.as_ref(), rc.snap_meta.mtime_ns);
let path_owned = path.to_string();
let appearance = themes::current().appearance;
let category = rc.ublx_db_category();
let mtime_ns = rc.snap_meta.mtime_ns;
spawn_job(tx, done_key, move || {
let p = cache::CodeViewerCacheParams {
path: path_owned.as_str(),
raw: raw.as_ref(),
content_width,
theme_name: theme_name.as_str(),
appearance,
category,
mtime_ns,
};
viewer_async::ViewerAsyncResult::Code(build_syntect_cache_entry(&p, content_identity))
});
}
pub fn schedule_csv(
state: &mut UblxState,
rc: &RightPaneContent,
content_width: u16,
path: &str,
raw: Arc<str>,
theme_key: String,
table_key: cache::ViewerTableCacheKey,
) {
let Some((tx, done_key)) = begin_viewer_async_job(
state,
rc,
content_width,
viewer_async::ViewerAsyncJobKind::Csv,
) else {
return;
};
let path = path.to_string();
let identity = table_key.identity.clone();
let cache_key = table_key;
spawn_job(tx, done_key, move || {
sync_worker_palette(theme_key.as_str());
let entry = build_csv_cache_entry(&path, raw.as_ref(), content_width, theme_key, identity);
viewer_async::ViewerAsyncResult::Csv(cache_key, entry)
});
}
#[must_use]
pub fn viewer_async_placeholder_active(state: &UblxState, rc: &RightPaneContent) -> bool {
let Some(pending) = state.viewer_async.pending_key.as_ref() else {
return false;
};
if state.viewer_async.rx.is_none() {
return false;
}
let Some(path) = rc.snap_meta.path.as_deref() else {
return false;
};
if path != pending.path.as_str() || themes::current().name != pending.theme_name {
return false;
}
let w = pending.content_width;
let theme = themes::current().name;
let raw = rc.viewer.as_deref().unwrap_or("");
match pending.kind {
viewer_async::ViewerAsyncJobKind::Code => {
let p = cache::CodeViewerCacheParams {
path,
raw,
content_width: w,
theme_name: theme,
appearance: themes::current().appearance,
category: rc.ublx_db_category(),
mtime_ns: rc.snap_meta.mtime_ns,
};
!state
.viewer_text_cache
.as_ref()
.is_some_and(|e| e.matches_syntect_viewer(&p))
}
viewer_async::ViewerAsyncJobKind::Markdown => !state
.viewer_text_cache
.as_ref()
.is_some_and(|e| e.matches_markdown_viewer(path, w, theme, raw)),
viewer_async::ViewerAsyncJobKind::Csv => {
let key = cache::viewer_table_cache_key(path, w, theme, raw, rc.snap_meta.mtime_ns);
!state
.csv_table_text_lru
.entries
.iter()
.any(|(k, _)| k == &key)
}
}
}