use std::collections::{HashMap, HashSet, VecDeque};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::sync::{Arc, mpsc};
use std::time::Instant;
use ratatui::style::Style;
use ratatui::widgets::ListState;
use crate::engine::{
cache,
db_ops::{DeltaType, UblxDbCategory},
viewer_async::ViewerAsyncState,
};
use crate::integrations::{ZahirFT, file_type_from_metadata_name};
use crate::render::viewers::pdf_preview::PDFPrefetch;
use crate::utils::{ClipboardCopyCommand, ToastSlot};
use super::style;
pub use crate::engine::db_ops::SnapshotTuiRow as TuiRow;
pub const CATEGORY_DIRECTORY: &str = "Directory";
#[derive(Debug, Default)]
pub struct ContentMarqueeState {
pub offset: usize,
pub last_advance: Option<Instant>,
pub anchor: Option<(usize, String)>,
}
impl ContentMarqueeState {
pub fn reset(&mut self) {
self.offset = 0;
self.last_advance = None;
self.anchor = None;
}
}
#[derive(Default)]
pub struct PanelState {
pub category_state: ListState,
pub content_state: ListState,
pub focus: PanelFocus,
pub preview_scroll: u16,
pub prev_preview_key: Option<(usize, Option<usize>)>,
pub highlight_style: Style,
pub content_sort: ContentSort,
pub sort_anchor_path: Option<String>,
pub right_pane_text_w: Option<u16>,
pub category_marquee: ContentMarqueeState,
pub content_marquee: ContentMarqueeState,
}
impl PanelState {
fn new() -> Self {
let mut p = Self {
highlight_style: style::list_highlight(),
..Default::default()
};
p.category_state.select(Some(0));
p.content_state.select(Some(0));
p
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SortDirection {
#[default]
Asc,
Desc,
}
impl SortDirection {
#[must_use]
pub fn next(self) -> Self {
match self {
Self::Asc => Self::Desc,
Self::Desc => Self::Asc,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SnapshotSortKey {
#[default]
Name,
Size,
Mod,
}
impl SnapshotSortKey {
#[must_use]
pub fn next(self) -> Self {
match self {
Self::Name => Self::Size,
Self::Size => Self::Mod,
Self::Mod => Self::Name,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ContentSort {
pub snapshot_key: SnapshotSortKey,
pub snapshot_dir: SortDirection,
pub delta_dir: SortDirection,
}
impl ContentSort {
#[must_use]
pub fn cycle_for_mode(self, main_mode: MainMode) -> Self {
match main_mode {
MainMode::Snapshot | MainMode::Duplicates => {
if self.snapshot_dir == SortDirection::Asc {
Self {
snapshot_dir: SortDirection::Desc,
..self
}
} else {
Self {
snapshot_key: self.snapshot_key.next(),
snapshot_dir: SortDirection::Asc,
..self
}
}
}
MainMode::Delta => Self {
delta_dir: self.delta_dir.next(),
..self
},
MainMode::Lenses | MainMode::Settings => self,
}
}
}
#[derive(Default)]
pub struct SearchState {
pub query: String,
pub active: bool,
}
#[derive(Default)]
pub struct ViewerFindState {
pub query: String,
pub active: bool,
pub committed: bool,
pub ranges: Vec<(usize, usize)>,
pub current: usize,
pub last_sync_token: Option<u64>,
pub pending_scroll: bool,
}
#[derive(Default)]
pub struct ThemeState {
pub selector_visible: bool,
pub selector_index: usize,
pub before_selector: Option<String>,
pub override_name: Option<String>,
}
#[derive(Default)]
pub struct ToastState {
pub slots: Vec<ToastSlot>,
pub consumed_per_operation: HashMap<String, usize>,
}
#[derive(Default)]
pub struct OpenMenuState {
pub visible: bool,
pub path: Option<String>,
pub can_terminal: bool,
pub selected_index: usize,
}
#[derive(Default)]
pub struct LensMenuState {
pub visible: bool,
pub paths: Vec<String>,
pub exclude_lens_name: Option<String>,
pub selected_index: usize,
pub name_input: Option<String>,
}
#[derive(Default)]
pub struct QAMenuState {
pub visible: bool,
pub selected_index: usize,
pub kind: Option<SpaceMenuKind>,
}
#[derive(Default)]
pub struct EnhancePolicyMenuState {
pub visible: bool,
pub path: Option<String>,
pub selected_index: usize,
}
#[derive(Default)]
pub struct LensConfirmState {
pub rename_input: Option<(String, String)>,
pub delete_visible: bool,
pub delete_lens_name: Option<String>,
pub delete_selected: usize,
}
#[derive(Default)]
pub struct FileDeleteConfirmState {
pub visible: bool,
pub rel_path: Option<String>,
pub bulk_paths: Option<Vec<String>>,
pub selected_index: usize,
}
#[derive(Debug, Default)]
pub struct MultiselectState {
pub active: bool,
pub selected: HashSet<String>,
pub bulk_menu_visible: bool,
pub bulk_menu_selected: usize,
pub bulk_menu_zahir_row: bool,
}
impl MultiselectState {
pub fn clear(&mut self) {
self.active = false;
self.selected.clear();
self.bulk_menu_visible = false;
self.bulk_menu_selected = 0;
self.bulk_menu_zahir_row = false;
}
}
#[derive(Clone, Debug, Default)]
pub struct CtrlChordState {
pub pending: bool,
pub menu_visible: bool,
pub started: Option<std::time::Instant>,
}
impl CtrlChordState {
#[must_use]
pub fn is_active(&self) -> bool {
self.pending || self.menu_visible
}
}
#[derive(Default)]
pub struct UblxSwitchPickerState {
pub visible: bool,
pub selected_index: usize,
pub roots: Vec<PathBuf>,
}
#[derive(Default)]
pub struct ViewerChrome {
pub help_visible: bool,
pub help_tab: u8,
pub viewer_fullscreen: bool,
pub ctrl_chord: CtrlChordState,
pub ublx_switch: UblxSwitchPickerState,
}
#[derive(Debug, Clone)]
pub struct StartupPromptState {
pub phase: StartupPromptPhase,
}
#[derive(Debug, Clone)]
pub enum StartupPromptPhase {
RootChoice {
selected_index: usize,
roots: Vec<PathBuf>,
},
PreviousSettings { selected_index: usize },
Enhance { selected_index: usize },
}
#[derive(Default)]
pub struct BackgroundSnapshot {
pub requested: bool,
pub poll_deadline: Option<std::time::Instant>,
pub done_received: bool,
pub defer_snapshot_after_current: bool,
}
#[derive(Default)]
pub struct DuplicateLoadGate {
pub requested: bool,
}
#[derive(Default)]
pub struct ZahirExportGate {
pub requested: bool,
}
#[derive(Default)]
pub struct LensExportGate {
pub requested: bool,
}
#[derive(Clone, Copy, Debug)]
pub struct SessionTickFlags {
pub first_tick: bool,
pub refresh_terminal_after_editor: bool,
}
impl Default for SessionTickFlags {
fn default() -> Self {
Self {
first_tick: true,
refresh_terminal_after_editor: false,
}
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SessionReloadFlags {
pub snapshot_rows: bool,
pub force_full_enhance_toast_shown: bool,
pub duplicate_groups: bool,
}
#[derive(Default)]
pub struct SessionFlow {
pub tick: SessionTickFlags,
pub reload: SessionReloadFlags,
pub pending_switch_to: Option<PathBuf>,
}
pub struct PDF {
pub page: u32,
pub page_count: Option<u32>,
pub for_path: Option<PathBuf>,
pub page_count_rx: Option<mpsc::Receiver<Result<u32, String>>>,
pub prefetch_cancel: Arc<AtomicU64>,
pub prefetch_earliest: Option<Instant>,
pub prefetch_rx: Option<mpsc::Receiver<(String, Result<image::DynamicImage, String>)>>,
}
impl Default for PDF {
fn default() -> Self {
Self {
page: 1,
page_count: None,
for_path: None,
page_count_rx: None,
prefetch_cancel: Arc::new(AtomicU64::new(0)),
prefetch_earliest: None,
prefetch_rx: None,
}
}
}
#[derive(Default)]
pub struct ViewerImageState {
pub protocol: Option<ratatui_image::protocol::StatefulProtocol>,
pub picker: Option<ratatui_image::picker::Picker>,
pub key: Option<String>,
pub decode_rx: Option<mpsc::Receiver<Result<image::DynamicImage, String>>>,
pub err: Option<String>,
pub image_lru: VecDeque<(String, ratatui_image::protocol::StatefulProtocol)>,
pub pdf: PDF,
}
impl ViewerImageState {
pub const LRU_EXTRA_SLOTS: usize = 4;
pub const LRU_CAP: usize = PDFPrefetch::MAX_EXTRA_PAGES as usize + Self::LRU_EXTRA_SLOTS;
pub fn push_lru(&mut self, path: String, proto: ratatui_image::protocol::StatefulProtocol) {
while self.image_lru.len() >= Self::LRU_CAP {
self.image_lru.pop_front();
}
self.image_lru.push_back((path, proto));
}
pub fn take_from_lru(
&mut self,
path: &str,
) -> Option<ratatui_image::protocol::StatefulProtocol> {
let pos = self.image_lru.iter().position(|(k, _)| k == path)?;
self.image_lru.remove(pos).map(|(_, proto)| proto)
}
pub fn remove_lru_key(&mut self, key: &str) {
if let Some(pos) = self.image_lru.iter().position(|(k, _)| k == key) {
self.image_lru.remove(pos);
}
}
pub fn clear(&mut self) {
self.pdf.prefetch_cancel.fetch_add(1, Ordering::SeqCst);
self.pdf.prefetch_rx = None;
self.pdf.prefetch_earliest = None;
self.decode_rx = None;
self.pdf.page_count_rx = None;
self.err = None;
let k = self.key.take();
let p = self.protocol.take();
if let (Some(k), Some(p)) = (k, p) {
self.push_lru(k, p);
}
self.pdf.for_path = None;
self.pdf.page = 1;
self.pdf.page_count = None;
}
}
#[derive(Debug, Clone)]
pub struct ViewerDiskContentCache {
pub rel_path: String,
pub category: String,
pub file_len: u64,
pub modified: Option<std::time::SystemTime>,
pub viewer_str: Option<String>,
pub embedded_cover_raster: Option<Vec<u8>>,
pub viewer_can_open: bool,
}
impl ViewerDiskContentCache {
#[must_use]
pub fn matches(&self, path: &str, category: &str, meta: &std::fs::Metadata) -> bool {
self.rel_path == path
&& self.category == category
&& self.file_len == meta.len()
&& self.modified == meta.modified().ok()
}
}
#[derive(Default)]
pub struct RightPaneAsync {
pub generation: u64,
pub last_spawn_path: String,
pub displayed: RightPaneContent,
pub rx: Option<tokio::sync::mpsc::UnboundedReceiver<RightPaneAsyncReady>>,
}
pub struct UblxState {
pub main_mode: MainMode,
pub right_pane_mode: RightPaneMode,
pub panels: PanelState,
pub search: SearchState,
pub viewer_find: ViewerFindState,
pub theme: ThemeState,
pub toasts: ToastState,
pub open_menu: OpenMenuState,
pub lens_menu: LensMenuState,
pub qa_menu: QAMenuState,
pub enhance_policy_menu: EnhancePolicyMenuState,
pub lens_confirm: LensConfirmState,
pub file_rename_input: Option<(String, String)>,
pub file_delete_confirm: FileDeleteConfirmState,
pub multiselect: MultiselectState,
pub chrome: ViewerChrome,
pub cached_tree: Option<(String, String)>,
pub viewer_disk_cache: Option<ViewerDiskContentCache>,
pub viewer_text_cache: Option<cache::ViewerTextCacheEntry>,
pub viewer_preview_source: Option<(String, cache::ViewerContentIdentity)>,
pub csv_table_text_lru:
cache::LruCache<cache::ViewerTableCacheKey, cache::ViewerTextCacheEntry>,
pub viewer_async: ViewerAsyncState,
pub viewer_image: ViewerImageState,
pub last_key_for_double: Option<char>,
pub snapshot_bg: BackgroundSnapshot,
pub duplicate_load: DuplicateLoadGate,
pub zahir_export_load: ZahirExportGate,
pub lens_export_load: LensExportGate,
pub duplicate_ignored_paths: HashSet<String>,
pub config_written_by_us_at: Option<std::time::Instant>,
pub session: SessionFlow,
pub clipboard_copy: Option<ClipboardCopyCommand>,
pub startup_prompt: Option<StartupPromptState>,
pub settings: SettingsPaneState,
pub right_pane_async: RightPaneAsync,
}
impl Default for UblxState {
fn default() -> Self {
Self::new()
}
}
impl UblxState {
#[must_use]
pub fn new() -> Self {
Self {
main_mode: MainMode::default(),
right_pane_mode: RightPaneMode::default(),
panels: PanelState::new(),
search: SearchState::default(),
viewer_find: ViewerFindState::default(),
theme: ThemeState::default(),
toasts: ToastState::default(),
open_menu: OpenMenuState::default(),
lens_menu: LensMenuState::default(),
qa_menu: QAMenuState::default(),
enhance_policy_menu: EnhancePolicyMenuState::default(),
lens_confirm: LensConfirmState::default(),
file_rename_input: None,
file_delete_confirm: FileDeleteConfirmState::default(),
multiselect: MultiselectState::default(),
chrome: ViewerChrome::default(),
cached_tree: None,
viewer_disk_cache: None,
viewer_text_cache: None,
viewer_preview_source: None,
csv_table_text_lru: cache::LruCache::default(),
viewer_async: ViewerAsyncState::default(),
viewer_image: ViewerImageState::default(),
last_key_for_double: None,
snapshot_bg: BackgroundSnapshot::default(),
duplicate_load: DuplicateLoadGate::default(),
zahir_export_load: ZahirExportGate::default(),
lens_export_load: LensExportGate::default(),
duplicate_ignored_paths: HashSet::new(),
config_written_by_us_at: None,
session: SessionFlow::default(),
clipboard_copy: ClipboardCopyCommand::detect(),
startup_prompt: None,
settings: SettingsPaneState::default(),
right_pane_async: RightPaneAsync::default(),
}
}
pub fn close_open_menu(&mut self) {
self.open_menu.visible = false;
self.open_menu.path = None;
self.open_menu.can_terminal = false;
}
pub fn open_open_menu(&mut self, path: String, can_open_in_terminal: bool) {
self.open_menu.visible = true;
self.open_menu.path = Some(path);
self.open_menu.can_terminal = can_open_in_terminal;
self.open_menu.selected_index = 0;
}
pub fn close_lens_menu(&mut self) {
self.lens_menu.visible = false;
self.lens_menu.paths.clear();
self.lens_menu.selected_index = 0;
}
pub fn close_qa_menu(&mut self) {
self.qa_menu.visible = false;
self.qa_menu.selected_index = 0;
self.qa_menu.kind = None;
}
pub fn close_enhance_policy_menu(&mut self) {
self.enhance_policy_menu.visible = false;
self.enhance_policy_menu.path = None;
self.enhance_policy_menu.selected_index = 0;
}
pub fn close_lens_delete_confirm(&mut self) {
self.lens_confirm.delete_visible = false;
self.lens_confirm.delete_lens_name = None;
self.lens_confirm.delete_selected = 0;
}
pub fn open_lens_menu(&mut self, paths: Vec<String>, exclude_current_lens: Option<String>) {
if paths.is_empty() {
return;
}
self.lens_menu.visible = true;
self.lens_menu.paths = paths;
self.lens_menu.exclude_lens_name = exclude_current_lens;
self.lens_menu.selected_index = 0;
}
pub fn open_qa_menu(&mut self, kind: SpaceMenuKind) {
self.qa_menu.visible = true;
self.qa_menu.selected_index = 0;
self.qa_menu.kind = Some(kind);
}
pub fn open_lens_delete_confirm(&mut self, lens_name: String) {
self.lens_confirm.delete_visible = true;
self.lens_confirm.delete_lens_name = Some(lens_name);
self.lens_confirm.delete_selected = 0;
}
pub fn open_file_rename_input(&mut self, rel_path: String) {
let base = std::path::Path::new(&rel_path)
.file_name()
.and_then(|s| s.to_str())
.unwrap_or("")
.to_string();
self.file_rename_input = Some((rel_path, base));
}
pub fn close_file_delete_confirm(&mut self) {
self.file_delete_confirm.visible = false;
self.file_delete_confirm.rel_path = None;
self.file_delete_confirm.bulk_paths = None;
self.file_delete_confirm.selected_index = 0;
}
pub fn open_file_delete_confirm(&mut self, rel_path: String) {
self.file_delete_confirm.visible = true;
self.file_delete_confirm.rel_path = Some(rel_path);
self.file_delete_confirm.bulk_paths = None;
self.file_delete_confirm.selected_index = 0;
}
pub fn open_file_delete_confirm_bulk(&mut self, paths: Vec<String>) {
self.file_delete_confirm.visible = true;
self.file_delete_confirm.rel_path = None;
self.file_delete_confirm.bulk_paths = Some(paths);
self.file_delete_confirm.selected_index = 0;
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum SettingsConfigScope {
#[default]
Global,
Local,
}
#[derive(Clone, Debug)]
pub struct SettingsPaneState {
pub scope: SettingsConfigScope,
pub left_cursor: usize,
pub right_scroll: u16,
pub layout_unlocked: bool,
pub layout_left_buf: String,
pub layout_mid_buf: String,
pub layout_right_buf: String,
pub opacity_unlocked: bool,
pub opacity_buf: String,
pub editing_path: Option<std::path::PathBuf>,
}
impl Default for SettingsPaneState {
fn default() -> Self {
Self {
scope: SettingsConfigScope::Global,
left_cursor: 0,
right_scroll: 0,
layout_unlocked: false,
layout_left_buf: String::new(),
layout_mid_buf: String::new(),
layout_right_buf: String::new(),
opacity_unlocked: false,
opacity_buf: String::new(),
editing_path: None,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum MainMode {
#[default]
Snapshot,
Delta,
Settings,
Duplicates,
Lenses,
}
impl MainMode {
#[must_use]
pub fn next(self, has_duplicates: bool, has_lenses: bool) -> MainMode {
match self {
MainMode::Snapshot => {
if has_lenses {
MainMode::Lenses
} else {
MainMode::Delta
}
}
MainMode::Lenses => MainMode::Delta,
MainMode::Delta => {
if has_duplicates {
MainMode::Duplicates
} else {
MainMode::Settings
}
}
MainMode::Duplicates => MainMode::Settings,
MainMode::Settings => MainMode::Snapshot,
}
}
}
#[derive(Clone, Copy, Default, PartialEq)]
pub enum PanelFocus {
#[default]
Categories,
Contents,
}
#[derive(Clone, Debug)]
pub enum SpaceMenuKind {
FileActions {
path: String,
can_open_in_terminal: bool,
show_enhance_directory_policy: bool,
show_enhance_zahir: bool,
show_copy_zahir_json: bool,
},
LensPanelActions { lens_name: String },
DuplicateMemberActions { path: String },
}
pub struct SectionedPreview {
pub templates: String,
pub metadata: Option<String>,
pub writing: Option<String>,
}
#[derive(Clone)]
pub enum ViewContents {
SnapshotIndices(Vec<usize>),
DeltaRows(Vec<TuiRow>),
}
pub struct ViewData {
pub filtered_categories: Vec<String>,
pub contents: ViewContents,
pub category_list_len: usize,
pub content_len: usize,
}
impl ViewData {
#[must_use]
pub fn row_at<'a>(&'a self, i: usize, all_rows: Option<&'a [TuiRow]>) -> Option<&'a TuiRow> {
match &self.contents {
ViewContents::SnapshotIndices(indices) => indices
.get(i)
.and_then(|&pos| all_rows.and_then(|r| r.get(pos))),
ViewContents::DeltaRows(rows) => rows.get(i),
}
}
#[must_use]
pub fn iter_contents<'a>(
&'a self,
all_rows: Option<&'a [TuiRow]>,
) -> Box<dyn Iterator<Item = &'a TuiRow> + 'a> {
match &self.contents {
ViewContents::SnapshotIndices(indices) => {
let iter = indices
.iter()
.filter_map(move |&pos| all_rows.and_then(|r| r.get(pos)));
Box::new(iter)
}
ViewContents::DeltaRows(rows) => Box::new(rows.iter()),
}
}
}
pub type DeltaRow = (i64, String);
pub struct DeltaViewData {
pub overview_text: String,
pub added_rows: Vec<DeltaRow>,
pub mod_rows: Vec<DeltaRow>,
pub removed_rows: Vec<DeltaRow>,
}
impl DeltaViewData {
#[must_use]
pub fn rows_by_index(&self, idx: usize) -> &[DeltaRow] {
match DeltaType::from_index(idx) {
DeltaType::Added => &self.added_rows,
DeltaType::Mod => &self.mod_rows,
DeltaType::Removed => &self.removed_rows,
}
}
}
#[derive(Debug)]
pub struct RightPaneAsyncReady {
pub generation: u64,
pub path: String,
pub content: RightPaneContent,
pub disk_cache: Option<ViewerDiskContentCache>,
}
#[derive(Clone, Debug, Default)]
pub struct SnapshotEntryMeta {
pub path: Option<String>,
pub category: Option<String>,
pub size: Option<u64>,
pub mtime_ns: Option<i64>,
pub has_zahir_json: bool,
}
#[derive(Clone, Debug, Default)]
pub struct RightPaneContentDerived {
pub abs_path: Option<PathBuf>,
pub can_open: bool,
pub offer_enhance_zahir: bool,
pub offer_enhance_directory_policy: bool,
pub embedded_cover_raster: Option<Vec<u8>>,
}
#[derive(Default, Clone, Debug)]
pub struct RightPaneContent {
pub templates: String,
pub metadata: Option<String>,
pub writing: Option<String>,
pub viewer: Option<Arc<str>>,
pub viewer_directory_policy_line: Option<String>,
pub snap_meta: SnapshotEntryMeta,
pub derived: RightPaneContentDerived,
}
impl RightPaneContent {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub fn zahir_file_type(&self) -> Option<ZahirFT> {
file_type_from_metadata_name(self.snap_meta.category.as_deref().unwrap_or(""))
}
#[must_use]
pub fn ublx_db_category(&self) -> UblxDbCategory {
UblxDbCategory::from_snapshot_category(self.snap_meta.category.as_deref().unwrap_or(""))
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq)]
pub enum RightPaneMode {
#[default]
Viewer,
Templates,
Metadata,
Writing,
}