Skip to main content

orbok_ui/
state.rs

1//! Headless UI state (view models) and the message vocabulary.
2//!
3//! Everything here is plain data — testable without a display server.
4//! `orbok-app` populates these structs from backend services; views
5//! render them; `update` mutates them. No iced types appear in this
6//! module so state logic stays UI-framework-agnostic.
7
8use crate::i18n::Locale;
9use orbok_models::SearchCapability;
10use orbok_search::SearchMode;
11
12/// Top-level pages (GUI external design §3.1 order).
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
14pub enum ViewId {
15    Search,
16    Sources,
17    Indexing,
18    Storage,
19    Models,
20    Settings,
21}
22
23impl ViewId {
24    pub const ALL: &'static [ViewId] = &[
25        ViewId::Search,
26        ViewId::Sources,
27        ViewId::Indexing,
28        ViewId::Storage,
29        ViewId::Models,
30        ViewId::Settings,
31    ];
32}
33
34/// Sidebar index-health summary.
35#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
36pub struct IndexHealth {
37    pub indexed: u64,
38    pub stale: u64,
39    pub failed: u64,
40    pub queued: u64,
41}
42
43/// One source card for the Sources view.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct SourceCard {
46    pub display_name: String,
47    pub display_path: String,
48    pub indexed: u64,
49    pub stale: u64,
50    pub failed: u64,
51    pub active: bool,
52}
53
54/// A search result ready for display — pure data, no backend types
55/// (RFC-027 boundary rule).
56#[derive(Debug, Clone, PartialEq)]
57pub struct SearchResultDisplay {
58    pub display_path: String,
59    pub title: Option<String>,
60    pub heading_path: Option<String>,
61    pub snippet: Option<String>,
62    pub keyword_rank: u32,
63    pub badges: Vec<String>,
64}
65
66/// The whole-app view model.
67#[derive(Debug, Clone)]
68pub struct AppState {
69    pub active_view: ViewId,
70    pub locale: Locale,
71    pub query: String,
72    pub last_query: Option<String>,
73    pub search_mode: SearchMode,
74    pub search_results: Vec<SearchResultDisplay>,
75    pub search_running: bool,
76    pub selected_result: Option<usize>,
77    pub storage_rows: Vec<(String, u64, u64)>,
78    pub health: IndexHealth,
79    pub sources: Vec<SourceCard>,
80    pub capability: SearchCapability,
81    pub storage_total_bytes: u64,
82}
83
84impl Default for AppState {
85    fn default() -> Self {
86        Self {
87            active_view: ViewId::Search,
88            locale: Locale::default(),
89            query: String::new(),
90            last_query: None,
91            search_mode: SearchMode::Auto,
92            search_results: Vec::new(),
93            search_running: false,
94            selected_result: None,
95            storage_rows: Vec::new(),
96            health: IndexHealth::default(),
97            sources: Vec::new(),
98            capability: SearchCapability::KeywordOnly,
99            storage_total_bytes: 0,
100        }
101    }
102}
103
104/// UI messages.
105#[derive(Debug, Clone)]
106pub enum Message {
107    Switch(ViewId),
108    QueryChanged(String),
109    SubmitSearch,
110    SearchResultsReady(Vec<SearchResultDisplay>),
111    SearchError(String),
112    SelectResult(usize),
113    OpenSourceFile(String),
114    SetSearchMode(SearchMode),
115    PersistLocale(Locale),
116    SetLocale(Locale),
117    StorageDataReady(Vec<(String, u64, u64)>),
118}
119
120impl AppState {
121    pub fn update(&mut self, message: &Message) {
122        match message {
123            Message::Switch(view) => self.active_view = *view,
124            Message::QueryChanged(query) => self.query = query.clone(),
125            Message::SubmitSearch => {
126                let trimmed = self.query.trim();
127                if !trimmed.is_empty() {
128                    self.last_query = Some(trimmed.to_string());
129                    self.search_running = true;
130                    self.search_results.clear();
131                    self.selected_result = None;
132                }
133            }
134            Message::SearchResultsReady(results) => {
135                self.search_results = results.clone();
136                self.search_running = false;
137                self.selected_result = None;
138            }
139            Message::SearchError(_) => {
140                self.search_running = false;
141            }
142            Message::SelectResult(idx) => self.selected_result = Some(*idx),
143            Message::OpenSourceFile(_) => {} // handled by orbok-app
144            Message::SetSearchMode(mode) => self.search_mode = *mode,
145            Message::PersistLocale(locale) | Message::SetLocale(locale) => self.locale = *locale,
146            Message::StorageDataReady(rows) => self.storage_rows = rows.clone(),
147        }
148    }
149}