use std::path::PathBuf;
use tui_textarea::TextArea;
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,
pub language: Option<String>,
}
#[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,
post_accept_snapshot: Option<String>,
wrapped_total: 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,
},
KillRingPicker {
cursor: usize,
},
ShellPane {
input: TextInput,
command_history_cursor: Option<usize>,
selection_mode: bool,
selection_cursor: usize,
scroll: usize,
show_help: bool,
},
HjsonEditor {
textarea: TextArea<'static>,
original_content: String,
path: PathBuf,
restart_required: bool,
scroll_row: usize,
scroll_col: usize,
},
ConfirmQuit,
TtsUnavailable {
title: String,
reason: String,
},
WritingStreakHeatmap {
daily_words: Vec<i64>,
streak_days: u32,
longest_streak: u32,
today_ymd: (i32, u32, u32),
},
TtsSaveAsAudio {
input: super::input::TextInput,
body: String,
voice: String,
wpm: u16,
voice_label: String,
},
TtsPlayback {
started_at: std::time::Instant,
preview: String,
voice_label: String,
},
SentenceRhythm {
stats: super::sentence_rhythm::RhythmStats,
scroll: usize,
},
Concordance {
data: super::concordance::ConcordanceData,
filter: super::input::TextInput,
cursor: usize,
scroll: usize,
sort: super::concordance::SortMode,
visible: Vec<usize>,
},
TranslationLanguagePicker {
entries: Vec<(Uuid, String)>,
cursor: usize,
direction: TranslationDirection,
source_title: String,
source_body: String,
},
ThreadsPicker {
entries: Vec<ThreadsPickerEntry>,
cursor: usize,
filter: super::input::TextInput,
filter_active: bool,
visible: Vec<usize>,
},
CommentsPanel {
entries: Vec<CommentsPanelEntry>,
cursor: usize,
filter: super::input::TextInput,
filter_active: bool,
hide_resolved: bool,
visible: Vec<usize>,
},
FootnoteEditor {
textarea: tui_textarea::TextArea<'static>,
paragraph_id: Uuid,
},
ProjectGoalModal {
data: super::project_goal::ProjectGoalData,
},
StyleTransferPicker {
entries: Vec<(Uuid, String)>,
cursor: usize,
filter: super::input::TextInput,
filter_active: bool,
visible: Vec<usize>,
target_paragraph_id: Uuid,
},
CommentEditor {
textarea: tui_textarea::TextArea<'static>,
anchor_start: usize,
anchor_end: usize,
anchor_preview: String,
paragraph_id: Uuid,
},
ThreadDoctor {
data: ThreadDoctorData,
},
DoctorPanel {
findings: Vec<crate::cli::doctor_scan::ScanFinding>,
cursor: usize,
scroll: usize,
last_status: Option<String>,
},
Journal {
snapshot: super::journal::JournalSnapshot,
scroll: usize,
last_status: Option<String>,
},
SnippetPicker {
kind: super::snippets::SnippetPickerKind,
input: super::input::TextInput,
candidates: Vec<String>,
matches: Vec<usize>,
cursor: usize,
tail: String,
},
ThreadWeaveView {
threads: Vec<ThreadsPickerEntry>,
chapters: Vec<(Uuid, String, String)>,
grid: Vec<Vec<Vec<Uuid>>>,
cursor_row: usize,
cursor_col: usize,
scroll_row: usize,
scroll_col: usize,
return_to: Box<Modal>,
},
}
#[derive(Debug, Clone)]
pub(super) struct CommentsPanelEntry {
pub paragraph_id: Uuid,
pub paragraph_breadcrumb: String,
pub typ_abs_path: std::path::PathBuf,
pub comment_index: usize,
pub author: String,
pub created_at: chrono::DateTime<chrono::Utc>,
pub resolved: bool,
pub text: String,
pub char_start: usize,
#[allow(dead_code)]
pub char_end: usize,
pub paragraph_position: usize,
pub paragraph_total_comments: usize,
}
#[derive(Debug, Clone)]
pub(super) struct ThreadDoctorData {
pub thread_count: usize,
pub avg_tension: f32,
pub status_distribution: Vec<(String, usize)>,
pub weight_distribution: Vec<(String, usize)>,
pub zero_links: Vec<String>,
pub payoff_unfired: Vec<String>,
pub dormant: Vec<String>,
}
#[derive(Debug, Clone)]
pub(super) struct ThreadsPickerEntry {
pub id: Uuid,
pub name: String,
pub title_field: String,
pub status: String,
pub weight: String,
pub tension: i32,
pub character_count: usize,
pub place_count: usize,
pub link_count: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum TranslationDirection {
ToInvented,
FromInvented,
}
#[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");
}
}