use uuid::Uuid;
use crate::store::node::NodeKind;
use crate::store::{InsertPosition, Snapshot};
use super::diff_utils::SnapshotDiffRow;
use super::file_picker::FilePicker;
use super::focus::Focus;
use super::inference::InferenceAction;
use super::input::TextInput;
use super::timeline_state::TimelineViewState;
#[derive(Debug, Clone)]
pub(super) struct PromptCandidate {
pub name: String,
pub description: String,
pub body: PromptBody,
pub source: PromptSource,
}
#[derive(Debug, Clone)]
pub(super) enum PromptBody {
Static(String),
BookParagraph(Uuid),
}
#[derive(Debug, Clone, Copy)]
pub(super) enum PromptSource {
System,
Book,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScriptPickerScope {
Branch,
ScriptsBook,
}
#[derive(Debug, Clone)]
pub(super) struct ScriptPickerEntry {
pub id: Uuid,
pub title: String,
pub slug_path: String,
}
#[derive(Debug, Clone)]
pub(super) struct SimilarPickerEntry {
pub id: Uuid,
pub title: String,
pub slug_path: String,
pub score: f64,
pub snippet: String,
}
pub(super) struct RenderedPageProto {
pub proto: ratatui_image::protocol::StatefulProtocol,
pub width: u32,
pub height: u32,
}
#[derive(Debug, Clone)]
pub(super) enum TagPickerTarget {
EditorParagraph { id: Uuid, title: String },
TreeSelection(Vec<Uuid>),
Search,
}
#[derive(Debug, Clone)]
pub(super) enum PagesToSave {
Single(usize),
All,
}
#[derive(Debug, Clone)]
pub(super) struct ImagePickerEntry {
pub fname: String,
pub title: String,
pub size_bytes: u64,
}
#[derive(Debug, Clone)]
pub(super) struct StatusFilterEntry {
pub id: Uuid,
pub title: String,
pub breadcrumb: String,
}
#[derive(Debug, Clone)]
pub(super) struct EventPickerEntry {
pub id: Uuid,
pub title: String,
pub start_ticks: i64,
pub start_str: String,
pub glyph: String,
pub track: Option<String>,
pub is_orphan: bool,
}
pub(super) fn visible_event_entries<'a>(
entries: &'a [EventPickerEntry],
filter: Option<&str>,
) -> Vec<&'a EventPickerEntry> {
match filter {
Some(t) => entries
.iter()
.filter(|e| {
e.track
.as_deref()
.map(|track| track.eq_ignore_ascii_case(t))
.unwrap_or(false)
})
.collect(),
None => entries.iter().collect(),
}
}
pub(super) enum Modal {
None,
Adding {
kind: NodeKind,
parent_id: Option<Uuid>,
parent_label: String,
input: TextInput,
position: InsertPosition,
},
Deleting {
root_id: Uuid,
root_kind: NodeKind,
title: String,
descendant_count: usize,
ids: Vec<Uuid>,
},
Renaming {
node_id: Uuid,
kind: NodeKind,
input: TextInput,
},
BundEval {
input: TextInput,
},
BundInput {
prompt: String,
input: TextInput,
hook: String,
},
BundPane {
title: String,
lines: Vec<String>,
scroll: usize,
},
ScriptPicker {
scope: ScriptPickerScope,
entries: Vec<ScriptPickerEntry>,
cursor: usize,
scroll: usize,
},
Progress {
scroll: usize,
},
ParagraphTarget {
input: TextInput,
},
SaveMarkdown {
input: TextInput,
body: String,
label: String,
},
LinkPicker {
owner: Uuid,
entries: Vec<ScriptPickerEntry>,
cursor: usize,
scroll: usize,
},
BacklinkPicker {
target: Uuid,
entries: Vec<ScriptPickerEntry>,
cursor: usize,
scroll: usize,
},
BookmarkPicker {
entries: Vec<ScriptPickerEntry>,
cursor: usize,
scroll: usize,
},
FuzzyParagraphPicker {
input: TextInput,
entries: Vec<ScriptPickerEntry>,
cursor: usize,
scroll: usize,
},
SnapshotDiff {
paragraph_title: String,
when: String,
rows: Vec<SnapshotDiffRow>,
scroll: usize,
return_to: Box<Modal>,
},
SimilarPicker {
entries: Vec<SimilarPickerEntry>,
cursor: usize,
scroll: usize,
},
FilePicker(FilePicker),
QuickRef {
focus: Focus,
scroll: usize,
},
Credits {
scroll: usize,
logo: Option<ratatui_image::protocol::StatefulProtocol>,
},
BookInfo {
scroll: usize,
},
LlmPicker {
providers: Vec<String>,
cursor: usize,
initial_default: String,
},
ImagePicker {
entries: Vec<ImagePickerEntry>,
cursor: usize,
close_quote: bool,
},
ImagePreview {
title: String,
fs_rel: String,
size_bytes: u64,
proto: ratatui_image::protocol::StatefulProtocol,
},
RenderedPreview {
title: String,
body: String,
settings: crate::typst_world::WorldSettings,
pages: Vec<RenderedPageProto>,
current_page: usize,
ppi: f32,
},
TagPicker {
target: TagPickerTarget,
all_tags: Vec<String>,
cursor: usize,
selected: std::collections::BTreeSet<String>,
},
TagAddPrompt {
input: TextInput,
return_to: Box<Modal>,
},
TagDeleteConfirm {
tag: String,
affected: usize,
return_to: Box<Modal>,
},
TagRenamePrompt {
input: TextInput,
old_tag: String,
affected: usize,
return_to: Box<Modal>,
},
TagSearchResults {
tag: String,
filter: TextInput,
all_results: Vec<ScriptPickerEntry>,
cursor: usize,
},
DiagnosticsList {
cursor: usize,
},
EventPicker {
entries: Vec<EventPickerEntry>,
cursor: usize,
track_filter: Option<String>,
},
TimelineView {
state: TimelineViewState,
},
TimelineNewEventPrompt {
input: TextInput,
book_id: Uuid,
cursor_ticks: i64,
track: Option<String>,
return_to: Box<Modal>,
},
TimelineEditEventPrompt {
input: TextInput,
event_id: Uuid,
},
AiDiffReview {
before_lines: Vec<String>,
after_lines: Vec<String>,
action: InferenceAction,
scroll: usize,
},
SnapshotAnnotation {
input: TextInput,
parent_id: Uuid,
parent_title: String,
body: Vec<u8>,
},
StoryView {
book_title: String,
width: u32,
height: u32,
png_bytes: Vec<u8>,
proto: ratatui_image::protocol::StatefulProtocol,
},
SaveStoryPng {
input: TextInput,
png_bytes: Vec<u8>,
book_title: String,
return_to: Box<Modal>,
},
SaveRenderedPng {
input: TextInput,
body: String,
settings: crate::typst_world::WorldSettings,
title: String,
pages: PagesToSave,
return_to: Box<Modal>,
},
FunctionPicker {
filter: TextInput,
cursor: usize,
},
ChatSearchPrompt {
input: TextInput,
},
StatusFilter {
status_label: &'static str,
scope: String,
entries: Vec<StatusFilterEntry>,
cursor: usize,
},
HelpQuery {
input: TextInput,
},
FindReplace {
search_input: TextInput,
replace_input: Option<TextInput>,
focus_replace: bool,
},
SnapshotPicker {
#[allow(dead_code)]
paragraph_id: Uuid,
paragraph_title: String,
snapshots: Vec<Snapshot>,
cursor: usize,
},
}
#[cfg(test)]
mod tests {
use super::*;
fn entry(title: &str, ticks: i64, track: Option<&str>) -> EventPickerEntry {
EventPickerEntry {
id: Uuid::nil(),
title: title.into(),
start_ticks: ticks,
start_str: format!("{ticks}"),
glyph: "●".into(),
track: track.map(str::to_owned),
is_orphan: false,
}
}
#[test]
fn filter_none_passes_all() {
let es = vec![
entry("A", 0, Some("main")),
entry("B", 1, Some("flashback")),
];
assert_eq!(visible_event_entries(&es, None).len(), 2);
}
#[test]
fn filter_track_case_insensitive() {
let es = vec![
entry("A", 0, Some("main")),
entry("B", 1, Some("flashback")),
entry("C", 2, None),
];
let hits = visible_event_entries(&es, Some("MAIN"));
assert_eq!(hits.len(), 1);
assert_eq!(hits[0].title, "A");
}
}