use std::num::NonZeroU64;
use std::sync::Arc;
use std::time::Duration;
use ratatui::crossterm::event::{KeyEvent, MouseEvent};
use tokio::sync::mpsc::UnboundedSender;
use kimun_core::{NoteVault, nfs::VaultPath};
use crate::components::file_list::{SortField, SortOrder};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortTarget {
Sidebar,
Query,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SaveSource {
QueryPanel,
NoteBrowser,
}
#[derive(Debug)]
pub enum AppEvent {
Input(InputEvent),
OpenScreen(ScreenEvent),
Quit,
Redraw,
RagStatus(crate::rag::RagStatus),
Autosave,
AutosaveCompleted {
path: VaultPath,
saved_revision: Option<NonZeroU64>,
title: Option<String>,
},
OpenPath {
path: VaultPath,
emphasis: Option<Vec<String>>,
},
OpenAttachment(VaultPath),
FocusSidebar,
OpenDrawerView(crate::components::drawer::DrawerView),
RunTagQuery(String),
JumpToHeading(String),
ExecuteLeaderAction(crate::keys::leader::LeaderAction),
FlashMessage(String),
Update(UpdateFlow),
ApplyTheme {
theme: Box<crate::settings::themes::Theme>,
persist: bool,
},
LinkTargetMeta {
target: String,
count: usize,
},
BacklinkCountLoaded {
path: VaultPath,
count: usize,
},
GitStatusLoaded(Option<String>),
PreferencesSaved,
OnboardingFinished,
ClosePreferences,
OpenFileBrowser,
TriggerFastReindex,
TriggerFullReindex,
IndexingDone(Result<Duration, String>),
OpenJournal,
CloseOverlay,
FollowLink(String),
FollowLabel(String),
InsertAtCursor(String),
FileOp(FileOp),
OverlayData(OverlayData),
Ask(AskData),
VaultConflict(String),
WorkspaceSwitched(String),
SavedSearch(SavedSearchFlow),
SortChanged {
target: SortTarget,
field: SortField,
order: SortOrder,
group_directories: bool,
persist: bool,
},
}
#[derive(Debug)]
pub enum AskData {
AnswerReady {
turn_id: u64,
result: Result<(String, Vec<crate::ask::AskSource>), String>,
},
ReaderNote {
path: VaultPath,
text: Option<String>,
},
}
#[derive(Debug, Clone)]
pub enum UpdateFlow {
Available(crate::update::UpdateStatus),
Apply,
Dismiss(String),
ShowDialog,
Applied,
}
#[derive(Debug, Clone)]
pub enum FileOp {
ShowMenu(VaultPath),
ShowDelete(VaultPath),
ShowRename(VaultPath),
ShowMove(VaultPath),
ShowCreateWithContent { path: VaultPath, content: String },
Created(VaultPath),
Deleted(VaultPath),
Renamed { from: VaultPath, to: VaultPath },
Moved { from: VaultPath, to: VaultPath },
}
#[derive(Debug, Clone)]
pub enum OverlayData {
RenameValidation { available: bool },
MoveDirectoriesLoaded(Vec<VaultPath>),
MoveFilterResults(Vec<VaultPath>),
MoveDestValidation { available: bool },
SavedSearchNamesLoaded(Vec<String>),
Error(String),
}
#[derive(Debug, Clone)]
pub enum SavedSearchFlow {
Confirmed {
name: String,
query: String,
source: SaveSource,
},
Persisted {
name: String,
query: String,
source: SaveSource,
},
SaveFailed { name: String },
Selected { query: String, name: String },
}
impl AppEvent {
pub fn send_input(event: InputEvent) -> Self {
AppEvent::Input(event)
}
pub fn open(path: kimun_core::nfs::VaultPath) -> Self {
AppEvent::OpenPath {
path,
emphasis: None,
}
}
}
#[derive(Debug, Clone)]
pub enum InputEvent {
Key(KeyEvent),
Mouse(MouseEvent),
Paste(String),
}
#[derive(Debug, Clone)]
pub enum ScreenEvent {
Start,
OpenPreferences,
OpenOnboarding,
OpenPreferencesWithError(String),
OpenEditor(Arc<NoteVault>, VaultPath),
OpenBrowse(Arc<NoteVault>, VaultPath),
}
pub type AppTx = UnboundedSender<AppEvent>;
pub trait AppTxExt {
fn announce_and_open(&self, path: VaultPath, created: bool);
}
impl AppTxExt for AppTx {
fn announce_and_open(&self, path: VaultPath, created: bool) {
if created {
self.send(AppEvent::FileOp(FileOp::Created(path.clone())))
.ok();
}
self.send(AppEvent::open(path)).ok();
}
}
pub fn redraw_callback(tx: AppTx) -> Arc<dyn Fn() + Send + Sync + 'static> {
Arc::new(move || {
let _ = tx.send(AppEvent::Redraw);
})
}
#[cfg(test)]
mod tests {
use super::*;
fn _assert_new_variants_exist(e: AppEvent) {
match e {
AppEvent::FileOp(FileOp::ShowDelete(_)) => {}
AppEvent::FileOp(FileOp::ShowRename(_)) => {}
AppEvent::FileOp(FileOp::ShowMove(_)) => {}
AppEvent::FileOp(FileOp::ShowCreateWithContent {
path: _,
content: _,
}) => {}
AppEvent::FileOp(FileOp::Deleted(_)) => {}
AppEvent::FileOp(FileOp::Renamed { from: _, to: _ }) => {}
AppEvent::FileOp(FileOp::Moved { from: _, to: _ }) => {}
AppEvent::OverlayData(OverlayData::Error(_)) => {}
AppEvent::Ask(AskData::AnswerReady {
turn_id: _,
result: _,
}) => {}
AppEvent::Ask(AskData::ReaderNote { path: _, text: _ }) => {}
_ => {}
}
}
#[test]
fn ask_data_variants_construct() {
let _ = AppEvent::Ask(AskData::AnswerReady {
turn_id: 1,
result: Ok(("answer".to_string(), vec![])),
});
let _ = AppEvent::Ask(AskData::AnswerReady {
turn_id: 2,
result: Err("failed".to_string()),
});
let _ = AppEvent::Ask(AskData::ReaderNote {
path: VaultPath::new("note.md"),
text: Some("body".to_string()),
});
let _ = AppEvent::Ask(AskData::ReaderNote {
path: VaultPath::new("missing.md"),
text: None,
});
}
#[test]
fn sort_events_construct() {
use crate::components::file_list::{SortField, SortOrder};
let _ = AppEvent::SortChanged {
target: SortTarget::Sidebar,
field: SortField::Name,
order: SortOrder::Ascending,
group_directories: true,
persist: false,
};
let _ = AppEvent::SortChanged {
target: SortTarget::Query,
field: SortField::Title,
order: SortOrder::Descending,
group_directories: false,
persist: true,
};
}
}