metis_docs_tui/app/
mod.rs1pub mod actions;
2pub mod mapping;
3pub mod operations;
4pub mod state;
5
6use crate::app::mapping::*;
7use crate::error::*;
8use crate::models::*;
9use crate::services::*;
10use anyhow::Result;
11use metis_core::{domain::documents::types::DocumentType, Initiative, Strategy, Task};
12
13pub struct App {
14 pub core_state: state::CoreAppState,
16 pub ui_state: state::UiState,
18 pub selection_state: state::SelectionState,
20 pub error_handler: ErrorHandler,
22 pub workspace_service: WorkspaceService,
24 pub document_service: Option<DocumentService>,
25 pub sync_service: Option<SyncService>,
26 pub transition_service: Option<TransitionService>,
27}
28
29impl Default for App {
30 fn default() -> Self {
31 Self::new()
32 }
33}
34
35impl App {
36 pub fn new() -> Self {
37 Self {
38 core_state: state::CoreAppState::new(),
39 ui_state: state::UiState::new(),
40 selection_state: state::SelectionState::new(),
41 error_handler: ErrorHandler::new(),
42 workspace_service: WorkspaceService::new(),
43 document_service: None,
44 sync_service: None,
45 transition_service: None,
46 }
47 }
48
49 pub async fn initialize(&mut self) -> Result<()> {
50 match self.workspace_service.check_workspace().await {
52 Ok(Some(workspace_dir)) => {
53 self.core_state.set_workspace(workspace_dir.clone());
54
55 self.document_service = Some(DocumentService::new(workspace_dir.clone()));
57 self.sync_service = Some(SyncService::new(workspace_dir.clone()));
58 self.transition_service = Some(TransitionService::new(workspace_dir));
59
60 if let Some(sync_service) = &self.sync_service {
62 match sync_service.sync_database().await {
63 Ok(_) => {
64 self.core_state.set_sync_complete();
65
66 self.load_flight_config().await?;
68
69 self.load_documents().await?;
71 }
72 Err(e) => {
73 self.error_handler.handle_error(AppError::from(e));
74 }
75 }
76 }
77 }
78 Ok(None) => {
79 self.error_handler
80 .handle_error(AppError::WorkspaceError("No workspace found".to_string()));
81 }
82 Err(e) => {
83 self.error_handler.handle_error(AppError::from(e));
84 }
85 }
86
87 Ok(())
88 }
89
90 pub fn is_ready(&self) -> bool {
91 self.core_state.is_ready()
92 }
93
94 pub fn get_current_board(&self) -> &KanbanBoard {
95 self.ui_state.get_current_board()
96 }
97
98 pub fn app_state(&self) -> &AppState {
100 &self.ui_state.app_state
101 }
102
103 pub fn error_message(&self) -> Option<String> {
104 self.ui_state
105 .message_state
106 .get_current_message()
107 .map(|msg| msg.content.clone())
108 }
109
110 pub fn get_selected_item(&self) -> Option<&KanbanItem> {
111 let current_board = self.ui_state.current_board;
112 let (col_idx, item_idx) = self.selection_state.get_current_selection(current_board);
113 let board = self.ui_state.get_current_board();
114
115 if col_idx < board.columns.len() && item_idx < board.columns[col_idx].items.len() {
116 Some(&board.columns[col_idx].items[item_idx])
117 } else {
118 None
119 }
120 }
121
122 pub fn view_selected_ticket(&mut self) {
123 let current_board = self.ui_state.current_board;
124 let selection = self.selection_state.get_current_selection(current_board);
125 self.ui_state.viewing_ticket = Some((current_board, selection.0, selection.1));
126 self.start_content_editing();
128 }
129
130 pub fn get_viewed_ticket(&self) -> Option<&KanbanItem> {
131 if let Some((board_type, col_idx, item_idx)) = self.ui_state.viewing_ticket {
132 let board = match board_type {
133 BoardType::Strategy => &self.ui_state.strategy_board,
134 BoardType::Initiative => &self.ui_state.initiative_board,
135 BoardType::Task => &self.ui_state.task_board,
136 BoardType::Adr => &self.ui_state.adr_board,
137 BoardType::Backlog => &self.ui_state.backlog_board,
138 };
139
140 if col_idx < board.columns.len() && item_idx < board.columns[col_idx].items.len() {
141 Some(&board.columns[col_idx].items[item_idx])
142 } else {
143 None
144 }
145 } else {
146 None
147 }
148 }
149
150 pub fn handle_key_event(&mut self, key: crossterm::event::KeyEvent) {
152 use tui_input::backend::crossterm::EventHandler;
153 self.ui_state
154 .input_title
155 .handle_event(&crossterm::event::Event::Key(key));
156 }
157
158 pub async fn load_flight_config(&mut self) -> Result<()> {
159 if let Some(workspace_dir) = &self.core_state.workspace_dir {
160 let db_path = workspace_dir.join("metis.db");
163 if let Ok(db) = metis_core::Database::new(db_path.to_str().unwrap()) {
164 if let Ok(mut config_repo) = db.configuration_repository() {
165 match config_repo.get_flight_level_config() {
166 Ok(config) => {
167 self.core_state.set_flight_config(config);
168 self.ui_state
170 .ensure_valid_board(&self.core_state.flight_config);
171 }
172 Err(e) => {
173 eprintln!("Warning: Failed to load flight level configuration: {}", e);
175 eprintln!("Using default (full) configuration");
176 self.ui_state
178 .ensure_valid_board(&self.core_state.flight_config);
179 }
180 }
181 } else {
182 eprintln!("Warning: Failed to create configuration repository, using default configuration");
183 self.ui_state
185 .ensure_valid_board(&self.core_state.flight_config);
186 }
187 } else {
188 eprintln!("Warning: Failed to connect to database, using default configuration");
189 self.ui_state
191 .ensure_valid_board(&self.core_state.flight_config);
192 }
193 }
194 Ok(())
195 }
196
197 pub async fn load_documents(&mut self) -> Result<()> {
198 if let Some(document_service) = &self.document_service {
199 for column in &mut self.ui_state.strategy_board.columns {
201 column.items.clear();
202 }
203 for column in &mut self.ui_state.initiative_board.columns {
204 column.items.clear();
205 }
206 for column in &mut self.ui_state.task_board.columns {
207 column.items.clear();
208 }
209 for column in &mut self.ui_state.adr_board.columns {
210 column.items.clear();
211 }
212 for column in &mut self.ui_state.backlog_board.columns {
213 column.items.clear();
214 }
215
216 self.selection_state.strategy_selection = (0, 0);
218 self.selection_state.initiative_selection = (0, 0);
219 self.selection_state.task_selection = (0, 0);
220 self.selection_state.adr_selection = (0, 0);
221 self.selection_state.backlog_selection = (0, 0);
222
223 let mut documents = document_service.load_documents_from_database().await?;
224
225 documents.sort_by(|a, b| {
227 use std::cmp::Ordering;
228
229 let type_order = |doc_type: &DocumentType| -> u8 {
231 match doc_type {
232 DocumentType::Vision => 0,
233 DocumentType::Strategy => 1,
234 DocumentType::Initiative => 2,
235 DocumentType::Task => 3,
236 DocumentType::Adr => 4,
237 }
238 };
239
240 let a_type_order = type_order(&a.document_type);
242 let b_type_order = type_order(&b.document_type);
243
244 match a_type_order.cmp(&b_type_order) {
245 Ordering::Equal => {
246 match (&a.document_type, &b.document_type) {
248 (DocumentType::Adr, DocumentType::Adr) => {
249 let a_num = extract_adr_number(&a.id);
251 let b_num = extract_adr_number(&b.id);
252 a_num.cmp(&b_num)
253 }
254 _ => a.title.cmp(&b.title), }
256 }
257 other => other, }
259 });
260
261 self.ui_state.strategy_board = KanbanBoard::create_strategy_board();
263 self.ui_state.initiative_board = KanbanBoard::create_initiative_board();
264 self.ui_state.task_board = KanbanBoard::create_task_board();
265 self.ui_state.adr_board = KanbanBoard::create_adr_board();
266 self.ui_state.backlog_board = KanbanBoard::create_backlog_board();
267
268 for doc in documents {
270 match doc.document_type {
271 DocumentType::Strategy => {
272 if let Ok(strategy) =
273 Strategy::from_file(std::path::Path::new(&doc.filepath)).await
274 {
275 let column_index = get_strategy_column_index(&strategy);
276 let item = KanbanItem {
277 document: DocumentObject::Strategy(strategy),
278 prelude: doc.title.clone(),
279 risk_complexity: None,
280 file_path: doc.filepath,
281 };
282 if column_index < self.ui_state.strategy_board.columns.len() {
283 self.ui_state.strategy_board.columns[column_index]
284 .items
285 .push(item);
286 }
287 }
288 }
289 DocumentType::Initiative => {
290 if let Ok(initiative) =
291 Initiative::from_file(std::path::Path::new(&doc.filepath)).await
292 {
293 let column_index = get_initiative_column_index(&initiative);
294 let item = KanbanItem {
295 document: DocumentObject::Initiative(initiative),
296 prelude: doc.title.clone(),
297 risk_complexity: None,
298 file_path: doc.filepath,
299 };
300 if column_index < self.ui_state.initiative_board.columns.len() {
301 self.ui_state.initiative_board.columns[column_index]
302 .items
303 .push(item);
304 }
305 }
306 }
307 DocumentType::Task => {
308 if let Ok(task) = Task::from_file(std::path::Path::new(&doc.filepath)).await
309 {
310 use metis_core::{domain::documents::types::Phase, Document};
311
312 let is_backlog = task.phase() == Ok(Phase::Backlog);
314
315 if is_backlog {
316 let column_index = get_backlog_column_index(&task);
318 let item = KanbanItem {
319 document: DocumentObject::Task(task),
320 prelude: doc.title.clone(),
321 risk_complexity: None,
322 file_path: doc.filepath,
323 };
324 if column_index < self.ui_state.backlog_board.columns.len() {
325 self.ui_state.backlog_board.columns[column_index]
326 .items
327 .push(item);
328 }
329 } else {
330 let column_index = get_task_column_index(&task);
332 let item = KanbanItem {
333 document: DocumentObject::Task(task),
334 prelude: doc.title.clone(),
335 risk_complexity: None,
336 file_path: doc.filepath,
337 };
338 if column_index < self.ui_state.task_board.columns.len() {
339 self.ui_state.task_board.columns[column_index]
340 .items
341 .push(item);
342 }
343 }
344 }
345 }
346 DocumentType::Adr => {
347 if let Ok(adr) =
348 metis_core::Adr::from_file(std::path::Path::new(&doc.filepath)).await
349 {
350 let column_index = get_adr_column_index(&adr);
351 let item = KanbanItem {
352 document: DocumentObject::Adr(adr),
353 prelude: doc.title.clone(),
354 risk_complexity: None,
355 file_path: doc.filepath,
356 };
357 if column_index < self.ui_state.adr_board.columns.len() {
358 self.ui_state.adr_board.columns[column_index]
359 .items
360 .push(item);
361 }
362 }
363 }
364 _ => {
365 }
367 }
368 }
369 }
370
371 Ok(())
372 }
373}