1use anyhow::Result;
2use crossterm::{
3 event::{self, DisableMouseCapture, EnableMouseCapture, Event, KeyCode, KeyModifiers},
4 execute,
5 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
6};
7use ratatui::{backend::CrosstermBackend, Terminal};
8use std::io;
9
10pub mod app;
11pub mod error;
12pub mod models;
13pub mod services;
14mod ui;
15
16use app::state::ConfirmationType;
17use app::App;
18use error::AppError;
19use models::AppState;
20
21pub async fn run() -> Result<()> {
23 enable_raw_mode()?;
25 let mut stdout = io::stdout();
26 execute!(stdout, EnterAlternateScreen, EnableMouseCapture)?;
27 let backend = CrosstermBackend::new(stdout);
28 let mut terminal = Terminal::new(backend)?;
29
30 let mut app = App::new();
32
33 let res = run_app(&mut terminal, &mut app).await;
35
36 disable_raw_mode()?;
38 execute!(
39 terminal.backend_mut(),
40 LeaveAlternateScreen,
41 DisableMouseCapture
42 )?;
43 terminal.show_cursor()?;
44
45 if let Err(err) = res {
46 println!("{err:?}");
47 }
48
49 Ok(())
50}
51
52async fn handle_normal_state_input(app: &mut App, key: crossterm::event::KeyEvent) -> Result<bool> {
56 if !matches!(key.code, KeyCode::Char(' ')) {
58 app.clear_messages();
59 }
60
61 match key.code {
62 KeyCode::Char('q') => return Ok(true), KeyCode::Tab => {
64 if app.is_ready() {
65 app.next_board();
66 }
67 }
68 KeyCode::BackTab => {
69 if app.is_ready() {
70 app.previous_board();
71 }
72 }
73 KeyCode::Left => {
74 if app.is_ready() {
75 app.move_selection_left();
76 }
77 }
78 KeyCode::Right => {
79 if app.is_ready() {
80 app.move_selection_right();
81 }
82 }
83 KeyCode::Up => {
84 if app.is_ready() {
85 app.move_selection_up();
86 }
87 }
88 KeyCode::Down => {
89 if app.is_ready() {
90 app.move_selection_down();
91 }
92 }
93 KeyCode::Char('n') => {
94 if app.is_ready() && key.modifiers.contains(KeyModifiers::CONTROL) {
95 app.start_smart_document_creation();
96 }
97 }
98 KeyCode::Char('d') | KeyCode::Char('D') => {
99 if app.is_ready() && app.get_selected_item().is_some() {
100 app.start_delete_confirmation();
101 }
102 }
103 KeyCode::Char('t') | KeyCode::Char('T') => {
104 if app.is_ready() && app.get_selected_item().is_some() {
105 app.start_transition_confirmation();
106 }
107 }
108 KeyCode::Char('1') => {
109 if app.is_ready() {
110 app.jump_to_strategy_board();
111 }
112 }
113 KeyCode::Char('2') => {
114 if app.is_ready() {
115 app.jump_to_initiative_board();
116 }
117 }
118 KeyCode::Char('3') => {
119 if app.is_ready() {
120 app.jump_to_task_board();
121 }
122 }
123 KeyCode::Char('4') => {
124 if app.is_ready() {
125 app.jump_to_adr_board();
126 }
127 }
128 KeyCode::Char('5') => {
129 if app.is_ready() {
130 app.jump_to_backlog_board();
131 }
132 }
133 KeyCode::Char('v') | KeyCode::Char('V') => {
134 if app.is_ready() {
135 app.view_vision_document();
136 }
137 }
138 KeyCode::Char(' ') => {
139 app.ui_state.message_state.clear_message();
140 }
141 KeyCode::Char('r') | KeyCode::Char('R') => {
142 if app.is_ready() && app.get_selected_item().is_some() {
143 if let Err(e) = app.archive_selected_document().await {
144 app.error_handler
145 .handle_with_context(AppError::from(e), "Archive operation");
146 }
147 }
148 }
149 KeyCode::Char('y') | KeyCode::Char('Y') => {
150 if app.is_ready() {
151 if let Err(e) = app.sync_and_reload().await {
152 app.add_error_message(format!("Sync failed: {}", e));
153 app.error_handler
154 .handle_with_context(AppError::from(e), "Sync operation");
155 }
156 }
157 }
158 KeyCode::Enter => {
159 if app.is_ready() {
160 app.view_selected_ticket();
161 }
162 }
163 _ => {}
164 }
165 Ok(false) }
167
168async fn handle_creation_state_input(
170 app: &mut App,
171 key: crossterm::event::KeyEvent,
172 state: AppState,
173) -> Result<()> {
174 match key.code {
175 KeyCode::Esc => {
176 app.cancel_document_creation();
177 }
178 KeyCode::Enter => {
179 let result = match state {
180 AppState::CreatingDocument => app.create_new_document().await,
181 AppState::CreatingChildDocument => app.create_child_document().await,
182 AppState::CreatingAdr => app.create_adr_from_ticket().await,
183 _ => return Ok(()),
184 };
185
186 if let Err(e) = result {
187 let context = match state {
188 AppState::CreatingDocument => "Document creation",
189 AppState::CreatingChildDocument => "Child document creation",
190 AppState::CreatingAdr => "ADR creation",
191 _ => "Creation",
192 };
193 app.error_handler
194 .handle_with_context(AppError::from(e), context);
195 }
196 }
197 _ => {
198 app.handle_key_event(key);
199 }
200 }
201 Ok(())
202}
203
204async fn handle_confirmation_input(app: &mut App, key: crossterm::event::KeyEvent) -> Result<()> {
206 match key.code {
207 KeyCode::Char('y') | KeyCode::Char('Y') => {
208 match app.ui_state.confirmation_type {
209 Some(ConfirmationType::Delete) => {
210 if let Err(e) = app.delete_selected_document().await {
211 app.error_handler
212 .handle_with_context(AppError::from(e), "Document deletion");
213 }
214 }
215 Some(ConfirmationType::Transition) => {
216 if let Err(e) = app.transition_selected_document().await {
217 app.error_handler
218 .handle_with_context(AppError::from(e), "Document transition");
219 }
220 }
221 None => {}
222 }
223 app.cancel_confirmation();
224 }
225 KeyCode::Char('n') | KeyCode::Char('N') | KeyCode::Esc => {
226 app.cancel_confirmation();
227 }
228 _ => {}
229 }
230 Ok(())
231}
232
233fn handle_backlog_category_input(app: &mut App, key: crossterm::event::KeyEvent) {
235 match key.code {
236 KeyCode::Esc => {
237 app.cancel_document_creation();
238 }
239 KeyCode::Up => {
240 app.move_category_selection_up();
241 }
242 KeyCode::Down => {
243 app.move_category_selection_down();
244 }
245 KeyCode::Enter => {
246 app.confirm_category_selection();
247 }
248 _ => {}
249 }
250}
251
252async fn handle_content_editing_input(
254 app: &mut App,
255 key: crossterm::event::KeyEvent,
256) -> Result<()> {
257 match key.code {
258 KeyCode::Esc => {
259 app.cancel_content_editing();
260 }
261 KeyCode::Char('s') if key.modifiers.contains(KeyModifiers::CONTROL) => {
262 if let Err(e) = app.save_content_edit().await {
263 app.error_handler
264 .handle_with_context(AppError::from(e), "Document save");
265 }
266 app.cancel_content_editing();
267 }
268 _ => {
269 if let Some(ref mut textarea) = app.ui_state.strategy_editor {
270 use tui_textarea::Input;
271 let input = Input::from(key);
272 textarea.input(input);
273 }
274 }
275 }
276 Ok(())
277}
278
279async fn run_app<B: ratatui::backend::Backend>(
280 terminal: &mut Terminal<B>,
281 app: &mut App,
282) -> Result<()> {
283 if let Err(e) = app.initialize().await {
285 app.add_error_message("Failed to initialize application".to_string());
286 app.error_handler
287 .handle_with_context(AppError::from(e), "Initialization");
288 }
289
290 loop {
291 app.clear_expired_messages();
293
294 terminal.draw(|f| ui::draw(f, app))?;
296
297 if crossterm::event::poll(std::time::Duration::from_millis(50))? {
299 if let Event::Key(key) = event::read()? {
300 let current_state = app.app_state();
301
302 match current_state {
303 AppState::Normal => {
304 if handle_normal_state_input(app, key).await? {
305 return Ok(()); }
307 }
308 AppState::CreatingDocument => {
309 handle_creation_state_input(app, key, AppState::CreatingDocument).await?;
310 }
311 AppState::CreatingChildDocument => {
312 handle_creation_state_input(app, key, AppState::CreatingChildDocument)
313 .await?;
314 }
315 AppState::CreatingAdr => {
316 handle_creation_state_input(app, key, AppState::CreatingAdr).await?;
317 }
318 AppState::Confirming => {
319 handle_confirmation_input(app, key).await?;
320 }
321 AppState::SelectingBacklogCategory => {
322 handle_backlog_category_input(app, key);
323 }
324 AppState::EditingContent => {
325 handle_content_editing_input(app, key).await?;
326 }
327 }
328 }
329 }
330 }
331}