1use super::{Cli, Commands, ProjectAction, SessionAction, TagAction, ConfigAction, GoalAction, EstimateAction, BranchAction, TemplateAction, WorkspaceAction, CalendarAction, IssueAction, ClientAction};
2use crate::utils::ipc::{IpcClient, IpcMessage, IpcResponse, get_socket_path, is_daemon_running};
3use crate::db::queries::{ProjectQueries, SessionQueries, TagQueries, SessionEditQueries};
4use crate::db::advanced_queries::{GoalQueries, GitBranchQueries, TimeEstimateQueries, InsightQueries, TemplateQueries, WorkspaceQueries};
5use crate::db::{Database, get_database_path, get_connection, get_pool_stats};
6use crate::models::{Project, Tag, Goal, TimeEstimate, ProjectTemplate, Workspace};
7use crate::utils::paths::{canonicalize_path, detect_project_name, get_git_hash, is_git_repository, validate_project_path};
8use crate::utils::validation::{validate_project_name, validate_project_description};
9use crate::utils::config::{load_config, save_config};
10use crate::cli::reports::ReportGenerator;
11use anyhow::{Result, Context};
12use std::env;
13use std::path::PathBuf;
14use std::process::{Command, Stdio};
15use chrono::{Utc, TimeZone};
16
17use crate::ui::dashboard::Dashboard;
18use crate::ui::timer::InteractiveTimer;
19use crate::ui::history::SessionHistoryBrowser;
20use crossterm::{execute, terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen}};
21use ratatui::{backend::CrosstermBackend, Terminal};
22use std::io;
23use tokio::runtime::Handle;
24
25
26pub async fn handle_command(cli: Cli) -> Result<()> {
27 match cli.command {
28 Commands::Start => {
29 start_daemon().await
30 }
31
32 Commands::Stop => {
33 stop_daemon().await
34 }
35
36 Commands::Restart => {
37 restart_daemon().await
38 }
39
40 Commands::Status => {
41 status_daemon().await
42 }
43
44 Commands::Init { name, path, description } => {
45 init_project(name, path, description).await
46 }
47
48 Commands::List { all, tag } => {
49 list_projects(all, tag).await
50 }
51
52 Commands::Report { project, from, to, format, group } => {
53 generate_report(project, from, to, format, group).await
54 }
55
56 Commands::Project { action } => {
57 handle_project_action(action).await
58 }
59
60 Commands::Session { action } => {
61 handle_session_action(action).await
62 }
63
64 Commands::Tag { action } => {
65 handle_tag_action(action).await
66 }
67
68 Commands::Config { action } => {
69 handle_config_action(action).await
70 }
71
72 Commands::Dashboard => {
73 launch_dashboard().await
74 }
75
76 Commands::Tui => {
77 launch_dashboard().await
78 }
79
80 Commands::Timer => {
81 launch_timer().await
82 }
83
84 Commands::History => {
85 launch_history().await
86 }
87
88 Commands::Goal { action } => {
89 handle_goal_action(action).await
90 }
91
92 Commands::Insights { period, project } => {
93 show_insights(period, project).await
94 }
95
96 Commands::Summary { period, from } => {
97 show_summary(period, from).await
98 }
99
100 Commands::Compare { projects, from, to } => {
101 compare_projects(projects, from, to).await
102 }
103
104 Commands::PoolStats => {
105 show_pool_stats().await
106 }
107
108 Commands::Estimate { action } => {
109 handle_estimate_action(action).await
110 }
111
112 Commands::Branch { action } => {
113 handle_branch_action(action).await
114 }
115
116 Commands::Template { action } => {
117 handle_template_action(action).await
118 }
119
120 Commands::Workspace { action } => {
121 handle_workspace_action(action).await
122 }
123
124 Commands::Calendar { action } => {
125 handle_calendar_action(action).await
126 }
127
128 Commands::Issue { action } => {
129 handle_issue_action(action).await
130 }
131
132 Commands::Client { action } => {
133 handle_client_action(action).await
134 }
135
136 Commands::Completions { shell } => {
137 Cli::generate_completions(shell);
138 Ok(())
139 }
140 }
141}
142
143async fn handle_project_action(action: ProjectAction) -> Result<()> {
144 match action {
145 ProjectAction::Archive { project } => {
146 archive_project(project).await
147 }
148
149 ProjectAction::Unarchive { project } => {
150 unarchive_project(project).await
151 }
152
153 ProjectAction::UpdatePath { project, path } => {
154 update_project_path(project, path).await
155 }
156
157 ProjectAction::AddTag { project, tag } => {
158 add_tag_to_project(project, tag).await
159 }
160
161 ProjectAction::RemoveTag { project, tag } => {
162 remove_tag_from_project(project, tag).await
163 }
164 }
165}
166
167async fn handle_session_action(action: SessionAction) -> Result<()> {
168 match action {
169 SessionAction::Start { project, context } => {
170 start_session(project, context).await
171 }
172
173 SessionAction::Stop => {
174 stop_session().await
175 }
176
177 SessionAction::Pause => {
178 pause_session().await
179 }
180
181 SessionAction::Resume => {
182 resume_session().await
183 }
184
185 SessionAction::Current => {
186 current_session().await
187 }
188
189 SessionAction::List { limit, project } => {
190 list_sessions(limit, project).await
191 }
192
193 SessionAction::Edit { id, start, end, project, reason } => {
194 edit_session(id, start, end, project, reason).await
195 }
196
197 SessionAction::Delete { id, force } => {
198 delete_session(id, force).await
199 }
200
201 SessionAction::Merge { session_ids, project, notes } => {
202 merge_sessions(session_ids, project, notes).await
203 }
204
205 SessionAction::Split { session_id, split_times, notes } => {
206 split_session(session_id, split_times, notes).await
207 }
208 }
209}
210
211async fn handle_tag_action(action: TagAction) -> Result<()> {
212 match action {
213 TagAction::Create { name, color, description } => {
214 create_tag(name, color, description).await
215 }
216
217 TagAction::List => {
218 list_tags().await
219 }
220
221 TagAction::Delete { name } => {
222 delete_tag(name).await
223 }
224 }
225}
226
227async fn handle_config_action(action: ConfigAction) -> Result<()> {
228 match action {
229 ConfigAction::Show => {
230 show_config().await
231 }
232
233 ConfigAction::Set { key, value } => {
234 set_config(key, value).await
235 }
236
237 ConfigAction::Get { key } => {
238 get_config(key).await
239 }
240
241 ConfigAction::Reset => {
242 reset_config().await
243 }
244 }
245}
246
247async fn start_daemon() -> Result<()> {
249 if is_daemon_running() {
250 println!("Daemon is already running");
251 return Ok(());
252 }
253
254 println!("Starting tempo daemon...");
255
256 let current_exe = std::env::current_exe()?;
257 let daemon_exe = current_exe.with_file_name("tempo-daemon");
258
259 if !daemon_exe.exists() {
260 return Err(anyhow::anyhow!("tempo-daemon executable not found at {:?}", daemon_exe));
261 }
262
263 let mut cmd = Command::new(&daemon_exe);
264 cmd.stdout(Stdio::null())
265 .stderr(Stdio::null())
266 .stdin(Stdio::null());
267
268 #[cfg(unix)]
269 {
270 use std::os::unix::process::CommandExt;
271 cmd.process_group(0);
272 }
273
274 let child = cmd.spawn()?;
275
276 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
278
279 if is_daemon_running() {
280 println!("Daemon started successfully (PID: {})", child.id());
281 Ok(())
282 } else {
283 Err(anyhow::anyhow!("Failed to start daemon"))
284 }
285}
286
287async fn stop_daemon() -> Result<()> {
288 if !is_daemon_running() {
289 println!("Daemon is not running");
290 return Ok(());
291 }
292
293 println!("Stopping tempo daemon...");
294
295 if let Ok(socket_path) = get_socket_path() {
297 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
298 match client.send_message(&IpcMessage::Shutdown).await {
299 Ok(_) => {
300 println!("Daemon stopped successfully");
301 return Ok(());
302 }
303 Err(e) => {
304 eprintln!("Failed to send shutdown message: {}", e);
305 }
306 }
307 }
308 }
309
310 if let Ok(Some(pid)) = crate::utils::ipc::read_pid_file() {
312 #[cfg(unix)]
313 {
314 let result = Command::new("kill")
315 .arg(pid.to_string())
316 .output();
317
318 match result {
319 Ok(_) => println!("Daemon stopped via kill signal"),
320 Err(e) => eprintln!("Failed to kill daemon: {}", e),
321 }
322 }
323
324 #[cfg(windows)]
325 {
326 let result = Command::new("taskkill")
327 .args(&["/PID", &pid.to_string(), "/F"])
328 .output();
329
330 match result {
331 Ok(_) => println!("Daemon stopped via taskkill"),
332 Err(e) => eprintln!("Failed to kill daemon: {}", e),
333 }
334 }
335 }
336
337 Ok(())
338}
339
340async fn restart_daemon() -> Result<()> {
341 println!("Restarting tempo daemon...");
342 stop_daemon().await?;
343 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
344 start_daemon().await
345}
346
347async fn status_daemon() -> Result<()> {
348 if !is_daemon_running() {
349 print_daemon_not_running();
350 return Ok(());
351 }
352
353 if let Ok(socket_path) = get_socket_path() {
354 match IpcClient::connect(&socket_path).await {
355 Ok(mut client) => {
356 match client.send_message(&IpcMessage::GetStatus).await {
357 Ok(IpcResponse::Status { daemon_running: _, active_session, uptime }) => {
358 print_daemon_status(uptime, active_session.as_ref());
359 }
360 Ok(IpcResponse::Pong) => {
361 print_daemon_status(0, None); }
363 Ok(other) => {
364 println!("Daemon is running (unexpected response: {:?})", other);
365 }
366 Err(e) => {
367 println!("Daemon is running but not responding: {}", e);
368 }
369 }
370 }
371 Err(e) => {
372 println!("Daemon appears to be running but cannot connect: {}", e);
373 }
374 }
375 } else {
376 println!("Cannot determine socket path");
377 }
378
379 Ok(())
380}
381
382async fn start_session(project: Option<String>, context: Option<String>) -> Result<()> {
384 if !is_daemon_running() {
385 println!("Daemon is not running. Start it with 'tempo start'");
386 return Ok(());
387 }
388
389 let project_path = if let Some(proj) = project {
390 PathBuf::from(proj)
391 } else {
392 env::current_dir()?
393 };
394
395 let context = context.unwrap_or_else(|| "manual".to_string());
396
397 let socket_path = get_socket_path()?;
398 let mut client = IpcClient::connect(&socket_path).await?;
399
400 let message = IpcMessage::StartSession {
401 project_path: Some(project_path.clone()),
402 context
403 };
404
405 match client.send_message(&message).await {
406 Ok(IpcResponse::Ok) => {
407 println!("Session started for project at {:?}", project_path);
408 }
409 Ok(IpcResponse::Error(message)) => {
410 println!("Failed to start session: {}", message);
411 }
412 Ok(other) => {
413 println!("Unexpected response: {:?}", other);
414 }
415 Err(e) => {
416 println!("Failed to communicate with daemon: {}", e);
417 }
418 }
419
420 Ok(())
421}
422
423async fn stop_session() -> Result<()> {
424 if !is_daemon_running() {
425 println!("Daemon is not running");
426 return Ok(());
427 }
428
429 let socket_path = get_socket_path()?;
430 let mut client = IpcClient::connect(&socket_path).await?;
431
432 match client.send_message(&IpcMessage::StopSession).await {
433 Ok(IpcResponse::Ok) => {
434 println!("Session stopped");
435 }
436 Ok(IpcResponse::Error(message)) => {
437 println!("Failed to stop session: {}", message);
438 }
439 Ok(other) => {
440 println!("Unexpected response: {:?}", other);
441 }
442 Err(e) => {
443 println!("Failed to communicate with daemon: {}", e);
444 }
445 }
446
447 Ok(())
448}
449
450async fn pause_session() -> Result<()> {
451 if !is_daemon_running() {
452 println!("Daemon is not running");
453 return Ok(());
454 }
455
456 let socket_path = get_socket_path()?;
457 let mut client = IpcClient::connect(&socket_path).await?;
458
459 match client.send_message(&IpcMessage::PauseSession).await {
460 Ok(IpcResponse::Ok) => {
461 println!("Session paused");
462 }
463 Ok(IpcResponse::Error(message)) => {
464 println!("Failed to pause session: {}", message);
465 }
466 Ok(other) => {
467 println!("Unexpected response: {:?}", other);
468 }
469 Err(e) => {
470 println!("Failed to communicate with daemon: {}", e);
471 }
472 }
473
474 Ok(())
475}
476
477async fn resume_session() -> Result<()> {
478 if !is_daemon_running() {
479 println!("Daemon is not running");
480 return Ok(());
481 }
482
483 let socket_path = get_socket_path()?;
484 let mut client = IpcClient::connect(&socket_path).await?;
485
486 match client.send_message(&IpcMessage::ResumeSession).await {
487 Ok(IpcResponse::Ok) => {
488 println!("Session resumed");
489 }
490 Ok(IpcResponse::Error(message)) => {
491 println!("Failed to resume session: {}", message);
492 }
493 Ok(other) => {
494 println!("Unexpected response: {:?}", other);
495 }
496 Err(e) => {
497 println!("Failed to communicate with daemon: {}", e);
498 }
499 }
500
501 Ok(())
502}
503
504async fn current_session() -> Result<()> {
505 if !is_daemon_running() {
506 print_daemon_not_running();
507 return Ok(());
508 }
509
510 let socket_path = get_socket_path()?;
511 let mut client = IpcClient::connect(&socket_path).await?;
512
513 match client.send_message(&IpcMessage::GetActiveSession).await {
514 Ok(IpcResponse::SessionInfo(session)) => {
515 print_formatted_session(&session)?;
516 }
517 Ok(IpcResponse::Error(message)) => {
518 print_no_active_session(&message);
519 }
520 Ok(other) => {
521 println!("Unexpected response: {:?}", other);
522 }
523 Err(e) => {
524 println!("Failed to communicate with daemon: {}", e);
525 }
526 }
527
528 Ok(())
529}
530
531async fn generate_report(
533 project: Option<String>,
534 from: Option<String>,
535 to: Option<String>,
536 format: Option<String>,
537 group: Option<String>,
538) -> Result<()> {
539 println!("Generating time report...");
540
541 let generator = ReportGenerator::new()?;
542 let report = generator.generate_report(project, from, to, group)?;
543
544 match format.as_deref() {
545 Some("csv") => {
546 let output_path = PathBuf::from("tempo-report.csv");
547 generator.export_csv(&report, &output_path)?;
548 println!("Report exported to: {:?}", output_path);
549 }
550 Some("json") => {
551 let output_path = PathBuf::from("tempo-report.json");
552 generator.export_json(&report, &output_path)?;
553 println!("Report exported to: {:?}", output_path);
554 }
555 _ => {
556 print_formatted_report(&report)?;
558 }
559 }
560
561 Ok(())
562}
563
564fn print_formatted_session(session: &crate::utils::ipc::SessionInfo) -> Result<()> {
566 let context_color = match session.context.as_str() {
568 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", };
574
575 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
576 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
577 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
578 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m●\x1b[0m \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m");
579 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_name, 25));
580 println!("\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(session.duration));
581 println!("\x1b[36m│\x1b[0m Started: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", session.start_time.format("%H:%M:%S").to_string());
582 println!("\x1b[36m│\x1b[0m Context: {}{:<25}\x1b[0m \x1b[36m│\x1b[0m", context_color, truncate_string(&session.context, 25));
583 println!("\x1b[36m│\x1b[0m Path: \x1b[2;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_path.to_string_lossy(), 25));
584 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
585 Ok(())
586}
587
588fn print_formatted_report(report: &crate::cli::reports::TimeReport) -> Result<()> {
589 let get_context_color = |context: &str| -> &str {
591 match context {
592 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m", "manual" => "\x1b[94m", _ => "\x1b[97m", }
598 };
599
600 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
601 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Report\x1b[0m \x1b[36m│\x1b[0m");
602 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
603
604 for (project_name, project_summary) in &report.projects {
605 println!("\x1b[36m│\x1b[0m \x1b[1;33m{:<20}\x1b[0m \x1b[1;32m{:>15}\x1b[0m \x1b[36m│\x1b[0m",
606 truncate_string(project_name, 20),
607 format_duration_fancy(project_summary.total_duration)
608 );
609
610 for (context, duration) in &project_summary.contexts {
611 let context_color = get_context_color(context);
612 println!("\x1b[36m│\x1b[0m {}{:<15}\x1b[0m \x1b[32m{:>20}\x1b[0m \x1b[36m│\x1b[0m",
613 context_color,
614 truncate_string(context, 15),
615 format_duration_fancy(*duration)
616 );
617 }
618 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
619 }
620
621 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
622 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal Time:\x1b[0m \x1b[1;32m{:>26}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(report.total_duration));
623 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
624 Ok(())
625}
626
627fn format_duration_fancy(seconds: i64) -> String {
628 let hours = seconds / 3600;
629 let minutes = (seconds % 3600) / 60;
630 let secs = seconds % 60;
631
632 if hours > 0 {
633 format!("{}h {}m {}s", hours, minutes, secs)
634 } else if minutes > 0 {
635 format!("{}m {}s", minutes, secs)
636 } else {
637 format!("{}s", secs)
638 }
639}
640
641fn truncate_string(s: &str, max_len: usize) -> String {
642 if s.len() <= max_len {
643 format!("{:<width$}", s, width = max_len)
644 } else {
645 format!("{:.width$}...", s, width = max_len.saturating_sub(3))
646 }
647}
648
649fn print_daemon_not_running() {
651 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
652 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
653 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
654 println!("\x1b[36m│\x1b[0m Status: \x1b[1;31m●\x1b[0m \x1b[31mOffline\x1b[0m \x1b[36m│\x1b[0m");
655 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
656 println!("\x1b[36m│\x1b[0m \x1b[33mDaemon is not running.\x1b[0m \x1b[36m│\x1b[0m");
657 println!("\x1b[36m│\x1b[0m \x1b[37mStart it with:\x1b[0m \x1b[96mtempo start\x1b[0m \x1b[36m│\x1b[0m");
658 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
659}
660
661fn print_no_active_session(message: &str) {
662 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
663 println!("\x1b[36m│\x1b[0m \x1b[1;37mCurrent Session\x1b[0m \x1b[36m│\x1b[0m");
664 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
665 println!("\x1b[36m│\x1b[0m Status: \x1b[1;33m○\x1b[0m \x1b[33mIdle\x1b[0m \x1b[36m│\x1b[0m");
666 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
667 println!("\x1b[36m│\x1b[0m \x1b[90m{:<37}\x1b[0m \x1b[36m│\x1b[0m", message);
668 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
669 println!("\x1b[36m│\x1b[0m \x1b[37mStart tracking:\x1b[0m \x1b[36m│\x1b[0m");
670 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
671 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
672}
673
674fn print_daemon_status(uptime: u64, active_session: Option<&crate::utils::ipc::SessionInfo>) {
675 let uptime_formatted = format_duration_fancy(uptime as i64);
676
677 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
678 println!("\x1b[36m│\x1b[0m \x1b[1;37mDaemon Status\x1b[0m \x1b[36m│\x1b[0m");
679 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
680 println!("\x1b[36m│\x1b[0m Status: \x1b[1;32m●\x1b[0m \x1b[32mOnline\x1b[0m \x1b[36m│\x1b[0m");
681 println!("\x1b[36m│\x1b[0m Uptime: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", uptime_formatted);
682
683 if let Some(session) = active_session {
684 let context_color = match session.context.as_str() {
685 "terminal" => "\x1b[96m", "ide" => "\x1b[95m", "linked" => "\x1b[93m",
686 "manual" => "\x1b[94m", _ => "\x1b[97m",
687 };
688
689 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
690 println!("\x1b[36m│\x1b[0m \x1b[1;37mActive Session:\x1b[0m \x1b[36m│\x1b[0m");
691 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&session.project_name, 23));
692 println!("\x1b[36m│\x1b[0m Duration: \x1b[1;32m{:<22}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(session.duration));
693 println!("\x1b[36m│\x1b[0m Context: {}{:<23}\x1b[0m \x1b[36m│\x1b[0m", context_color, session.context);
694 } else {
695 println!("\x1b[36m│\x1b[0m Session: \x1b[33mNo active session\x1b[0m \x1b[36m│\x1b[0m");
696 }
697
698 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
699}
700
701async fn init_project(name: Option<String>, path: Option<PathBuf>, description: Option<String>) -> Result<()> {
703 let validated_name = if let Some(n) = name.as_ref() {
705 Some(validate_project_name(n)
706 .with_context(|| format!("Invalid project name '{}'", n))?)
707 } else {
708 None
709 };
710
711 let validated_description = if let Some(d) = description.as_ref() {
712 Some(validate_project_description(d)
713 .with_context(|| "Invalid project description")?)
714 } else {
715 None
716 };
717
718 let project_path = path.unwrap_or_else(|| {
719 env::current_dir().expect("Failed to get current directory")
720 });
721
722 let canonical_path = validate_project_path(&project_path)
724 .with_context(|| format!("Invalid project path: {}", project_path.display()))?;
725
726 let project_name = validated_name.clone().unwrap_or_else(|| {
727 let detected = detect_project_name(&canonical_path);
728 validate_project_name(&detected).unwrap_or_else(|_| "project".to_string())
729 });
730
731 let conn = match get_connection().await {
733 Ok(conn) => conn,
734 Err(_) => {
735 let db_path = get_database_path()?;
737 let db = Database::new(&db_path)?;
738 return init_project_with_db(validated_name, Some(canonical_path), validated_description, &db.connection).await;
739 }
740 };
741
742 if let Some(existing) = ProjectQueries::find_by_path(conn.connection(), &canonical_path)? {
744 eprintln!("\x1b[33m⚠ Warning:\x1b[0m A project named '{}' already exists at this path.", existing.name);
745 eprintln!("Use 'tempo list' to see all projects or choose a different location.");
746 return Ok(());
747 }
748
749 init_project_with_db(Some(project_name.clone()), Some(canonical_path.clone()), validated_description, conn.connection()).await?;
751
752 println!("\x1b[32m✓ Success:\x1b[0m Project '{}' initialized at {}", project_name, canonical_path.display());
753 println!("Start tracking time with: \x1b[36mtempo start\x1b[0m");
754
755 Ok(())
756}
757
758async fn list_projects(include_archived: bool, tag_filter: Option<String>) -> Result<()> {
759 let db_path = get_database_path()?;
761 let db = Database::new(&db_path)?;
762
763 let projects = ProjectQueries::list_all(&db.connection, include_archived)?;
765
766 if projects.is_empty() {
767 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
768 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Projects\x1b[0m \x1b[36m│\x1b[0m");
769 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
770 println!("\x1b[36m│\x1b[0m No projects found. \x1b[36m│\x1b[0m");
771 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
772 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a project:\x1b[0m \x1b[36m│\x1b[0m");
773 println!("\x1b[36m│\x1b[0m \x1b[96mtempo init [project-name]\x1b[0m \x1b[36m│\x1b[0m");
774 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
775 return Ok(());
776 }
777
778 let filtered_projects = if let Some(_tag) = tag_filter {
780 projects
782 } else {
783 projects
784 };
785
786 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
787 println!("\x1b[36m│\x1b[0m \x1b[1;37mProjects\x1b[0m \x1b[36m│\x1b[0m");
788 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
789
790 for project in &filtered_projects {
791 let status_icon = if project.is_archived { "📦" } else { "📁" };
792 let status_color = if project.is_archived { "\x1b[90m" } else { "\x1b[37m" };
793 let git_indicator = if project.git_hash.is_some() { " (git)" } else { "" };
794
795 println!("\x1b[36m│\x1b[0m {} {}{:<25}\x1b[0m \x1b[36m│\x1b[0m",
796 status_icon,
797 status_color,
798 format!("{}{}", truncate_string(&project.name, 20), git_indicator)
799 );
800
801 if let Some(description) = &project.description {
802 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(description, 35));
803 }
804
805 let path_display = project.path.to_string_lossy();
806 if path_display.len() > 35 {
807 let home_dir = dirs::home_dir();
808 let display_path = if let Some(home) = home_dir {
809 if let Ok(stripped) = project.path.strip_prefix(&home) {
810 format!("~/{}", stripped.display())
811 } else {
812 path_display.to_string()
813 }
814 } else {
815 path_display.to_string()
816 };
817 println!("\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&display_path, 35));
818 } else {
819 println!("\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&path_display, 35));
820 }
821
822 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
823 }
824
825 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
826 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
827 format!("{} projects", filtered_projects.len())
828 );
829 if include_archived {
830 let active_count = filtered_projects.iter().filter(|p| !p.is_archived).count();
831 let archived_count = filtered_projects.iter().filter(|p| p.is_archived).count();
832 println!("\x1b[36m│\x1b[0m \x1b[37mActive:\x1b[0m {:<15} \x1b[90mArchived:\x1b[0m {:<8} \x1b[36m│\x1b[0m",
833 active_count, archived_count
834 );
835 }
836 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
837
838 Ok(())
839}
840
841async fn create_tag(name: String, color: Option<String>, description: Option<String>) -> Result<()> {
843 let db_path = get_database_path()?;
845 let db = Database::new(&db_path)?;
846
847 let mut tag = Tag::new(name);
849 if let Some(c) = color {
850 tag = tag.with_color(c);
851 }
852 if let Some(d) = description {
853 tag = tag.with_description(d);
854 }
855
856 tag.validate()?;
858
859 let existing_tags = TagQueries::list_all(&db.connection)?;
861 if existing_tags.iter().any(|t| t.name == tag.name) {
862 println!("\x1b[33m⚠ Tag already exists:\x1b[0m {}", tag.name);
863 return Ok(());
864 }
865
866 let tag_id = TagQueries::create(&db.connection, &tag)?;
868
869 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
870 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Created\x1b[0m \x1b[36m│\x1b[0m");
871 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
872 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&tag.name, 27));
873 if let Some(color_val) = &tag.color {
874 println!("\x1b[36m│\x1b[0m Color: \x1b[37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(color_val, 27));
875 }
876 if let Some(desc) = &tag.description {
877 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
878 }
879 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", tag_id);
880 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
881 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag created successfully\x1b[0m \x1b[36m│\x1b[0m");
882 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
883
884 Ok(())
885}
886
887async fn list_tags() -> Result<()> {
888 let db_path = get_database_path()?;
890 let db = Database::new(&db_path)?;
891
892 let tags = TagQueries::list_all(&db.connection)?;
894
895 if tags.is_empty() {
896 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
897 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Tags\x1b[0m \x1b[36m│\x1b[0m");
898 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
899 println!("\x1b[36m│\x1b[0m No tags found. \x1b[36m│\x1b[0m");
900 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
901 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a tag:\x1b[0m \x1b[36m│\x1b[0m");
902 println!("\x1b[36m│\x1b[0m \x1b[96mtempo tag create <name>\x1b[0m \x1b[36m│\x1b[0m");
903 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
904 return Ok(());
905 }
906
907 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
908 println!("\x1b[36m│\x1b[0m \x1b[1;37mTags\x1b[0m \x1b[36m│\x1b[0m");
909 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
910
911 for tag in &tags {
912 let color_indicator = if let Some(color) = &tag.color {
913 format!(" ({})", color)
914 } else {
915 String::new()
916 };
917
918 println!("\x1b[36m│\x1b[0m 🏷️ \x1b[1;33m{:<30}\x1b[0m \x1b[36m│\x1b[0m",
919 format!("{}{}", truncate_string(&tag.name, 25), color_indicator)
920 );
921
922 if let Some(description) = &tag.description {
923 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<33}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(description, 33));
924 }
925
926 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
927 }
928
929 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
930 println!("\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
931 format!("{} tags", tags.len())
932 );
933 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
934
935 Ok(())
936}
937
938async fn delete_tag(name: String) -> Result<()> {
939 let db_path = get_database_path()?;
941 let db = Database::new(&db_path)?;
942
943 if TagQueries::find_by_name(&db.connection, &name)?.is_none() {
945 println!("\x1b[31m✗ Tag '{}' not found\x1b[0m", name);
946 return Ok(());
947 }
948
949 let deleted = TagQueries::delete_by_name(&db.connection, &name)?;
951
952 if deleted {
953 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
954 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Deleted\x1b[0m \x1b[36m│\x1b[0m");
955 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
956 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
957 println!("\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m");
958 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
959 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag deleted successfully\x1b[0m \x1b[36m│\x1b[0m");
960 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
961 } else {
962 println!("\x1b[31m✗ Failed to delete tag '{}'\x1b[0m", name);
963 }
964
965 Ok(())
966}
967
968async fn show_config() -> Result<()> {
970 let config = load_config()?;
971
972 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
973 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration\x1b[0m \x1b[36m│\x1b[0m");
974 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
975 println!("\x1b[36m│\x1b[0m idle_timeout_minutes: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.idle_timeout_minutes);
976 println!("\x1b[36m│\x1b[0m auto_pause_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.auto_pause_enabled);
977 println!("\x1b[36m│\x1b[0m default_context: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.default_context);
978 println!("\x1b[36m│\x1b[0m max_session_hours: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.max_session_hours);
979 println!("\x1b[36m│\x1b[0m backup_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.backup_enabled);
980 println!("\x1b[36m│\x1b[0m log_level: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m", config.log_level);
981
982 if !config.custom_settings.is_empty() {
983 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
984 println!("\x1b[36m│\x1b[0m \x1b[1;37mCustom Settings:\x1b[0m \x1b[36m│\x1b[0m");
985 for (key, value) in &config.custom_settings {
986 println!("\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
987 truncate_string(key, 20),
988 truncate_string(value, 16)
989 );
990 }
991 }
992
993 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
994
995 Ok(())
996}
997
998async fn get_config(key: String) -> Result<()> {
999 let config = load_config()?;
1000
1001 let value = match key.as_str() {
1002 "idle_timeout_minutes" => Some(config.idle_timeout_minutes.to_string()),
1003 "auto_pause_enabled" => Some(config.auto_pause_enabled.to_string()),
1004 "default_context" => Some(config.default_context),
1005 "max_session_hours" => Some(config.max_session_hours.to_string()),
1006 "backup_enabled" => Some(config.backup_enabled.to_string()),
1007 "log_level" => Some(config.log_level),
1008 _ => config.custom_settings.get(&key).cloned(),
1009 };
1010
1011 match value {
1012 Some(val) => {
1013 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1014 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Value\x1b[0m \x1b[36m│\x1b[0m");
1015 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1016 println!("\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1017 truncate_string(&key, 20),
1018 truncate_string(&val, 16)
1019 );
1020 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1021 }
1022 None => {
1023 println!("\x1b[31m✗ Configuration key not found:\x1b[0m {}", key);
1024 }
1025 }
1026
1027 Ok(())
1028}
1029
1030async fn set_config(key: String, value: String) -> Result<()> {
1031 let mut config = load_config()?;
1032
1033 let display_value = value.clone(); match key.as_str() {
1036 "idle_timeout_minutes" => {
1037 config.idle_timeout_minutes = value.parse()?;
1038 }
1039 "auto_pause_enabled" => {
1040 config.auto_pause_enabled = value.parse()?;
1041 }
1042 "default_context" => {
1043 config.default_context = value;
1044 }
1045 "max_session_hours" => {
1046 config.max_session_hours = value.parse()?;
1047 }
1048 "backup_enabled" => {
1049 config.backup_enabled = value.parse()?;
1050 }
1051 "log_level" => {
1052 config.log_level = value;
1053 }
1054 _ => {
1055 config.set_custom(key.clone(), value);
1056 }
1057 }
1058
1059 config.validate()?;
1060 save_config(&config)?;
1061
1062 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1063 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Updated\x1b[0m \x1b[36m│\x1b[0m");
1064 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1065 println!("\x1b[36m│\x1b[0m {:<20} \x1b[32m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1066 truncate_string(&key, 20),
1067 truncate_string(&display_value, 16)
1068 );
1069 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1070 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Configuration saved successfully\x1b[0m \x1b[36m│\x1b[0m");
1071 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1072
1073 Ok(())
1074}
1075
1076async fn reset_config() -> Result<()> {
1077 let default_config = crate::models::Config::default();
1078 save_config(&default_config)?;
1079
1080 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1081 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Reset\x1b[0m \x1b[36m│\x1b[0m");
1082 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1083 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Configuration reset to defaults\x1b[0m \x1b[36m│\x1b[0m");
1084 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1085 println!("\x1b[36m│\x1b[0m \x1b[37mView current config:\x1b[0m \x1b[36m│\x1b[0m");
1086 println!("\x1b[36m│\x1b[0m \x1b[96mtempo config show\x1b[0m \x1b[36m│\x1b[0m");
1087 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1088
1089 Ok(())
1090}
1091
1092async fn list_sessions(limit: Option<usize>, project_filter: Option<String>) -> Result<()> {
1094 let db_path = get_database_path()?;
1096 let db = Database::new(&db_path)?;
1097
1098 let session_limit = limit.unwrap_or(10);
1099
1100 let project_id = if let Some(project_name) = &project_filter {
1102 match ProjectQueries::find_by_name(&db.connection, project_name)? {
1103 Some(project) => Some(project.id.unwrap()),
1104 None => {
1105 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1106 return Ok(());
1107 }
1108 }
1109 } else {
1110 None
1111 };
1112
1113 let sessions = SessionQueries::list_with_filter(&db.connection, project_id, None, None, Some(session_limit))?;
1114
1115 if sessions.is_empty() {
1116 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1117 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Sessions\x1b[0m \x1b[36m│\x1b[0m");
1118 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1119 println!("\x1b[36m│\x1b[0m No sessions found. \x1b[36m│\x1b[0m");
1120 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1121 println!("\x1b[36m│\x1b[0m \x1b[37mStart a session:\x1b[0m \x1b[36m│\x1b[0m");
1122 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
1123 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1124 return Ok(());
1125 }
1126
1127 let filtered_sessions = if let Some(_project) = project_filter {
1129 sessions
1131 } else {
1132 sessions
1133 };
1134
1135 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1136 println!("\x1b[36m│\x1b[0m \x1b[1;37mRecent Sessions\x1b[0m \x1b[36m│\x1b[0m");
1137 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1138
1139 for session in &filtered_sessions {
1140 let status_icon = if session.end_time.is_some() { "✅" } else { "🔄" };
1141 let duration = if let Some(end) = session.end_time {
1142 (end - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1143 } else {
1144 (Utc::now() - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1145 };
1146
1147 let context_color = match session.context {
1148 crate::models::SessionContext::Terminal => "\x1b[96m",
1149 crate::models::SessionContext::IDE => "\x1b[95m",
1150 crate::models::SessionContext::Linked => "\x1b[93m",
1151 crate::models::SessionContext::Manual => "\x1b[94m",
1152 };
1153
1154 println!("\x1b[36m│\x1b[0m {} \x1b[1;37m{:<32}\x1b[0m \x1b[36m│\x1b[0m",
1155 status_icon,
1156 format!("Session {}", session.id.unwrap_or(0))
1157 );
1158 println!("\x1b[36m│\x1b[0m Duration: \x1b[32m{:<24}\x1b[0m \x1b[36m│\x1b[0m", format_duration_fancy(duration));
1159 println!("\x1b[36m│\x1b[0m Context: {}{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1160 context_color,
1161 session.context
1162 );
1163 println!("\x1b[36m│\x1b[0m Started: \x1b[37m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1164 session.start_time.format("%Y-%m-%d %H:%M:%S")
1165 );
1166 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1167 }
1168
1169 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1170 println!("\x1b[36m│\x1b[0m \x1b[1;37mShowing:\x1b[0m {:<28} \x1b[36m│\x1b[0m",
1171 format!("{} recent sessions", filtered_sessions.len())
1172 );
1173 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1174
1175 Ok(())
1176}
1177
1178async fn edit_session(id: i64, start: Option<String>, end: Option<String>, project: Option<String>, reason: Option<String>) -> Result<()> {
1179 let db_path = get_database_path()?;
1181 let db = Database::new(&db_path)?;
1182
1183 let session = SessionQueries::find_by_id(&db.connection, id)?;
1185 let session = match session {
1186 Some(s) => s,
1187 None => {
1188 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1189 return Ok(());
1190 }
1191 };
1192
1193 let original_start = session.start_time;
1194 let original_end = session.end_time;
1195
1196 let mut new_start = original_start;
1198 let mut new_end = original_end;
1199 let mut new_project_id = session.project_id;
1200
1201 if let Some(start_str) = &start {
1203 new_start = match chrono::DateTime::parse_from_rfc3339(start_str) {
1204 Ok(dt) => dt.with_timezone(&chrono::Utc),
1205 Err(_) => {
1206 match chrono::NaiveDateTime::parse_from_str(start_str, "%Y-%m-%d %H:%M:%S") {
1207 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1208 Err(_) => return Err(anyhow::anyhow!("Invalid start time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"))
1209 }
1210 }
1211 };
1212 }
1213
1214 if let Some(end_str) = &end {
1216 if end_str.to_lowercase() == "null" || end_str.to_lowercase() == "none" {
1217 new_end = None;
1218 } else {
1219 new_end = Some(match chrono::DateTime::parse_from_rfc3339(end_str) {
1220 Ok(dt) => dt.with_timezone(&chrono::Utc),
1221 Err(_) => {
1222 match chrono::NaiveDateTime::parse_from_str(end_str, "%Y-%m-%d %H:%M:%S") {
1223 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1224 Err(_) => return Err(anyhow::anyhow!("Invalid end time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"))
1225 }
1226 }
1227 });
1228 }
1229 }
1230
1231 if let Some(project_name) = &project {
1233 if let Some(proj) = ProjectQueries::find_by_name(&db.connection, project_name)? {
1234 new_project_id = proj.id.unwrap();
1235 } else {
1236 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1237 return Ok(());
1238 }
1239 }
1240
1241 if new_start >= new_end.unwrap_or(chrono::Utc::now()) {
1243 println!("\x1b[31m✗ Start time must be before end time\x1b[0m");
1244 return Ok(());
1245 }
1246
1247 SessionEditQueries::create_edit_record(
1249 &db.connection,
1250 id,
1251 original_start,
1252 original_end,
1253 new_start,
1254 new_end,
1255 reason.clone()
1256 )?;
1257
1258 SessionQueries::update_session(
1260 &db.connection,
1261 id,
1262 if start.is_some() { Some(new_start) } else { None },
1263 if end.is_some() { Some(new_end) } else { None },
1264 if project.is_some() { Some(new_project_id) } else { None },
1265 None
1266 )?;
1267
1268 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1269 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Updated\x1b[0m \x1b[36m│\x1b[0m");
1270 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1271 println!("\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", id);
1272
1273 if start.is_some() {
1274 println!("\x1b[36m│\x1b[0m Start: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1275 truncate_string(&new_start.format("%Y-%m-%d %H:%M:%S").to_string(), 27)
1276 );
1277 }
1278
1279 if end.is_some() {
1280 let end_str = if let Some(e) = new_end {
1281 e.format("%Y-%m-%d %H:%M:%S").to_string()
1282 } else {
1283 "Ongoing".to_string()
1284 };
1285 println!("\x1b[36m│\x1b[0m End: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&end_str, 27));
1286 }
1287
1288 if let Some(r) = &reason {
1289 println!("\x1b[36m│\x1b[0m Reason: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(r, 27));
1290 }
1291
1292 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1293 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session updated with audit trail\x1b[0m \x1b[36m│\x1b[0m");
1294 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1295
1296 Ok(())
1297}
1298
1299async fn delete_session(id: i64, force: bool) -> Result<()> {
1300 let db_path = get_database_path()?;
1302 let db = Database::new(&db_path)?;
1303
1304 let session = SessionQueries::find_by_id(&db.connection, id)?;
1306 let session = match session {
1307 Some(s) => s,
1308 None => {
1309 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1310 return Ok(());
1311 }
1312 };
1313
1314 if session.end_time.is_none() && !force {
1316 println!("\x1b[33m⚠ Cannot delete active session without --force flag\x1b[0m");
1317 println!(" Use: tempo session delete {} --force", id);
1318 return Ok(());
1319 }
1320
1321 SessionQueries::delete_session(&db.connection, id)?;
1323
1324 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1325 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Deleted\x1b[0m \x1b[36m│\x1b[0m");
1326 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1327 println!("\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", id);
1328 println!("\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m");
1329
1330 if session.end_time.is_none() {
1331 println!("\x1b[36m│\x1b[0m Type: \x1b[33mActive session (forced)\x1b[0m \x1b[36m│\x1b[0m");
1332 } else {
1333 println!("\x1b[36m│\x1b[0m Type: \x1b[37mCompleted session\x1b[0m \x1b[36m│\x1b[0m");
1334 }
1335
1336 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1337 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session and audit trail removed\x1b[0m \x1b[36m│\x1b[0m");
1338 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1339
1340 Ok(())
1341}
1342
1343async fn archive_project(project_name: String) -> Result<()> {
1345 let db_path = get_database_path()?;
1346 let db = Database::new(&db_path)?;
1347
1348 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1349 Some(p) => p,
1350 None => {
1351 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1352 return Ok(());
1353 }
1354 };
1355
1356 if project.is_archived {
1357 println!("\x1b[33m⚠ Project '{}' is already archived\x1b[0m", project_name);
1358 return Ok(());
1359 }
1360
1361 let success = ProjectQueries::archive_project(&db.connection, project.id.unwrap())?;
1362
1363 if success {
1364 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1365 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Archived\x1b[0m \x1b[36m│\x1b[0m");
1366 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1367 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1368 println!("\x1b[36m│\x1b[0m Status: \x1b[90mArchived\x1b[0m \x1b[36m│\x1b[0m");
1369 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1370 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project archived successfully\x1b[0m \x1b[36m│\x1b[0m");
1371 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1372 } else {
1373 println!("\x1b[31m✗ Failed to archive project '{}'\x1b[0m", project_name);
1374 }
1375
1376 Ok(())
1377}
1378
1379async fn unarchive_project(project_name: String) -> Result<()> {
1380 let db_path = get_database_path()?;
1381 let db = Database::new(&db_path)?;
1382
1383 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1384 Some(p) => p,
1385 None => {
1386 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1387 return Ok(());
1388 }
1389 };
1390
1391 if !project.is_archived {
1392 println!("\x1b[33m⚠ Project '{}' is not archived\x1b[0m", project_name);
1393 return Ok(());
1394 }
1395
1396 let success = ProjectQueries::unarchive_project(&db.connection, project.id.unwrap())?;
1397
1398 if success {
1399 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1400 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Unarchived\x1b[0m \x1b[36m│\x1b[0m");
1401 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1402 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1403 println!("\x1b[36m│\x1b[0m Status: \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m");
1404 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1405 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project unarchived successfully\x1b[0m \x1b[36m│\x1b[0m");
1406 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1407 } else {
1408 println!("\x1b[31m✗ Failed to unarchive project '{}'\x1b[0m", project_name);
1409 }
1410
1411 Ok(())
1412}
1413
1414async fn update_project_path(project_name: String, new_path: PathBuf) -> Result<()> {
1415 let db_path = get_database_path()?;
1416 let db = Database::new(&db_path)?;
1417
1418 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1419 Some(p) => p,
1420 None => {
1421 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1422 return Ok(());
1423 }
1424 };
1425
1426 let canonical_path = canonicalize_path(&new_path)?;
1427 let success = ProjectQueries::update_project_path(&db.connection, project.id.unwrap(), &canonical_path)?;
1428
1429 if success {
1430 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1431 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Path Updated\x1b[0m \x1b[36m│\x1b[0m");
1432 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1433 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
1434 println!("\x1b[36m│\x1b[0m Old Path: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project.path.to_string_lossy(), 27));
1435 println!("\x1b[36m│\x1b[0m New Path: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&canonical_path.to_string_lossy(), 27));
1436 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1437 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Path updated successfully\x1b[0m \x1b[36m│\x1b[0m");
1438 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1439 } else {
1440 println!("\x1b[31m✗ Failed to update path for project '{}'\x1b[0m", project_name);
1441 }
1442
1443 Ok(())
1444}
1445
1446async fn add_tag_to_project(project_name: String, tag_name: String) -> Result<()> {
1447 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1448 println!("Would add tag '{}' to project '{}'", tag_name, project_name);
1449 println!("This requires implementing project_tags table operations.");
1450 Ok(())
1451}
1452
1453async fn remove_tag_from_project(project_name: String, tag_name: String) -> Result<()> {
1454 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1455 println!("Would remove tag '{}' from project '{}'", tag_name, project_name);
1456 println!("This requires implementing project_tags table operations.");
1457 Ok(())
1458}
1459
1460async fn bulk_update_sessions_project(session_ids: Vec<i64>, new_project_name: String) -> Result<()> {
1462 let db_path = get_database_path()?;
1463 let db = Database::new(&db_path)?;
1464
1465 let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1467 Some(p) => p,
1468 None => {
1469 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1470 return Ok(());
1471 }
1472 };
1473
1474 let updated = SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1475
1476 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1477 println!("\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Update\x1b[0m \x1b[36m│\x1b[0m");
1478 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1479 println!("\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", updated);
1480 println!("\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&new_project_name, 27));
1481 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1482 println!("\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m", updated, "");
1483 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1484
1485 Ok(())
1486}
1487
1488async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1489 let db_path = get_database_path()?;
1490 let db = Database::new(&db_path)?;
1491
1492 let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1493
1494 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1495 println!("\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Delete\x1b[0m \x1b[36m│\x1b[0m");
1496 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1497 println!("\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", session_ids.len());
1498 println!("\x1b[36m│\x1b[0m Deleted: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", deleted);
1499 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1500 println!("\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m", deleted, "");
1501 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1502
1503 Ok(())
1504}
1505
1506async fn launch_dashboard() -> Result<()> {
1507 enable_raw_mode()?;
1509 let mut stdout = io::stdout();
1510 execute!(stdout, EnterAlternateScreen)?;
1511 let backend = CrosstermBackend::new(stdout);
1512 let mut terminal = Terminal::new(backend)?;
1513
1514 let result = async {
1516 let mut dashboard = Dashboard::new().await?;
1517 dashboard.run(&mut terminal).await
1518 };
1519
1520 let result = tokio::task::block_in_place(|| {
1521 Handle::current().block_on(result)
1522 });
1523
1524 disable_raw_mode()?;
1526 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1527 terminal.show_cursor()?;
1528
1529 result
1530}
1531
1532async fn launch_timer() -> Result<()> {
1533 enable_raw_mode()?;
1535 let mut stdout = io::stdout();
1536 execute!(stdout, EnterAlternateScreen)?;
1537 let backend = CrosstermBackend::new(stdout);
1538 let mut terminal = Terminal::new(backend)?;
1539
1540 let result = async {
1542 let mut timer = InteractiveTimer::new().await?;
1543 timer.run(&mut terminal).await
1544 };
1545
1546 let result = tokio::task::block_in_place(|| {
1547 Handle::current().block_on(result)
1548 });
1549
1550 disable_raw_mode()?;
1552 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1553 terminal.show_cursor()?;
1554
1555 result
1556}
1557
1558async fn merge_sessions(session_ids_str: String, project_name: Option<String>, notes: Option<String>) -> Result<()> {
1559 let session_ids: Result<Vec<i64>, _> = session_ids_str
1561 .split(',')
1562 .map(|s| s.trim().parse::<i64>())
1563 .collect();
1564
1565 let session_ids = session_ids.map_err(|_| anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'"))?;
1566
1567 if session_ids.len() < 2 {
1568 return Err(anyhow::anyhow!("At least 2 sessions are required for merging"));
1569 }
1570
1571 let mut target_project_id = None;
1573 if let Some(project) = project_name {
1574 let db_path = get_database_path()?;
1575 let db = Database::new(&db_path)?;
1576
1577 if let Ok(project_id) = project.parse::<i64>() {
1579 if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1580 target_project_id = Some(project_id);
1581 }
1582 } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1583 target_project_id = proj.id;
1584 }
1585
1586 if target_project_id.is_none() {
1587 return Err(anyhow::anyhow!("Project '{}' not found", project));
1588 }
1589 }
1590
1591 let db_path = get_database_path()?;
1593 let db = Database::new(&db_path)?;
1594
1595 let merged_id = SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1596
1597 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1598 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Merge Complete\x1b[0m \x1b[36m│\x1b[0m");
1599 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1600 println!("\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m", session_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", "));
1601 println!("\x1b[36m│\x1b[0m New session ID: \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m", merged_id);
1602 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1603 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m \x1b[36m│\x1b[0m");
1604 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1605
1606 Ok(())
1607}
1608
1609async fn split_session(session_id: i64, split_times_str: String, notes: Option<String>) -> Result<()> {
1610 let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
1612 let mut split_times = Vec::new();
1613
1614 for time_str in split_time_strings {
1615 let datetime = if time_str.contains(':') {
1617 let today = chrono::Local::now().date_naive();
1619 let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
1620 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
1621 .map_err(|_| anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str))?;
1622 today.and_time(time).and_utc()
1623 } else {
1624 chrono::DateTime::parse_from_rfc3339(time_str)
1626 .map_err(|_| anyhow::anyhow!("Invalid datetime format '{}'. Use HH:MM or RFC3339 format", time_str))?
1627 .to_utc()
1628 };
1629
1630 split_times.push(datetime);
1631 }
1632
1633 if split_times.is_empty() {
1634 return Err(anyhow::anyhow!("No valid split times provided"));
1635 }
1636
1637 let notes_list = notes.map(|n| {
1639 n.split(',').map(|s| s.trim().to_string()).collect::<Vec<String>>()
1640 });
1641
1642 let db_path = get_database_path()?;
1644 let db = Database::new(&db_path)?;
1645
1646 let new_session_ids = SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
1647
1648 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1649 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Split Complete\x1b[0m \x1b[36m│\x1b[0m");
1650 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1651 println!("\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m", session_id);
1652 println!("\x1b[36m│\x1b[0m Split points: \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m", split_times.len());
1653 println!("\x1b[36m│\x1b[0m New sessions: \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m", new_session_ids.iter().map(|id| id.to_string()).collect::<Vec<_>>().join(", "));
1654 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1655 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m \x1b[36m│\x1b[0m");
1656 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1657
1658 Ok(())
1659}
1660
1661async fn launch_history() -> Result<()> {
1662 enable_raw_mode()?;
1663 let mut stdout = io::stdout();
1664 execute!(stdout, EnterAlternateScreen)?;
1665 let backend = CrosstermBackend::new(stdout);
1666 let mut terminal = Terminal::new(backend)?;
1667
1668 let result = async {
1669 let mut browser = SessionHistoryBrowser::new().await?;
1670 browser.run(&mut terminal).await
1671 };
1672
1673 let result = tokio::task::block_in_place(|| {
1674 Handle::current().block_on(result)
1675 });
1676
1677 disable_raw_mode()?;
1678 execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
1679 terminal.show_cursor()?;
1680
1681 result
1682}
1683
1684async fn handle_goal_action(action: GoalAction) -> Result<()> {
1685 match action {
1686 GoalAction::Create { name, target_hours, project, description, start_date, end_date } => {
1687 create_goal(name, target_hours, project, description, start_date, end_date).await
1688 }
1689 GoalAction::List { project } => {
1690 list_goals(project).await
1691 }
1692 GoalAction::Update { id, hours } => {
1693 update_goal_progress(id, hours).await
1694 }
1695 }
1696}
1697
1698async fn create_goal(name: String, target_hours: f64, project: Option<String>, description: Option<String>, start_date: Option<String>, end_date: Option<String>) -> Result<()> {
1699 let db_path = get_database_path()?;
1700 let db = Database::new(&db_path)?;
1701
1702 let project_id = if let Some(proj_name) = project {
1703 match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
1704 Some(p) => p.id,
1705 None => {
1706 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
1707 return Ok(());
1708 }
1709 }
1710 } else {
1711 None
1712 };
1713
1714 let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1715 let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1716
1717 let mut goal = Goal::new(name.clone(), target_hours);
1718 if let Some(pid) = project_id {
1719 goal = goal.with_project(pid);
1720 }
1721 if let Some(desc) = description {
1722 goal = goal.with_description(desc);
1723 }
1724 goal = goal.with_dates(start, end);
1725
1726 goal.validate()?;
1727 let goal_id = GoalQueries::create(&db.connection, &goal)?;
1728
1729 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1730 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoal Created\x1b[0m \x1b[36m│\x1b[0m");
1731 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1732 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
1733 println!("\x1b[36m│\x1b[0m Target: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{} hours", target_hours));
1734 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", goal_id);
1735 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1736 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m \x1b[36m│\x1b[0m");
1737 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1738
1739 Ok(())
1740}
1741
1742async fn list_goals(project: Option<String>) -> Result<()> {
1743 let db_path = get_database_path()?;
1744 let db = Database::new(&db_path)?;
1745
1746 let project_id = if let Some(proj_name) = &project {
1747 match ProjectQueries::find_by_name(&db.connection, proj_name)? {
1748 Some(p) => p.id,
1749 None => {
1750 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
1751 return Ok(());
1752 }
1753 }
1754 } else {
1755 None
1756 };
1757
1758 let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
1759
1760 if goals.is_empty() {
1761 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1762 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Goals\x1b[0m \x1b[36m│\x1b[0m");
1763 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1764 return Ok(());
1765 }
1766
1767 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1768 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoals\x1b[0m \x1b[36m│\x1b[0m");
1769 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1770
1771 for goal in &goals {
1772 let progress_pct = goal.progress_percentage();
1773 println!("\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&goal.name, 25));
1774 println!("\x1b[36m│\x1b[0m Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h) \x1b[36m│\x1b[0m",
1775 progress_pct, goal.current_progress, goal.target_hours);
1776 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1777 }
1778
1779 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1780 Ok(())
1781}
1782
1783async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
1784 let db_path = get_database_path()?;
1785 let db = Database::new(&db_path)?;
1786
1787 GoalQueries::update_progress(&db.connection, id, hours)?;
1788 println!("\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m", id, hours);
1789 Ok(())
1790}
1791
1792async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
1793 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1794 println!("\x1b[36m│\x1b[0m \x1b[1;37mProductivity Insights\x1b[0m \x1b[36m│\x1b[0m");
1795 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1796 println!("\x1b[36m│\x1b[0m Period: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", period.as_deref().unwrap_or("all"));
1797 if let Some(proj) = project {
1798 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&proj, 27));
1799 }
1800 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1801 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Insights calculation in progress...\x1b[0m \x1b[36m│\x1b[0m");
1802 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1803 Ok(())
1804}
1805
1806async fn show_summary(period: String, from: Option<String>) -> Result<()> {
1807 let db_path = get_database_path()?;
1808 let db = Database::new(&db_path)?;
1809
1810 let start_date = if let Some(from_str) = from {
1811 chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
1812 } else {
1813 match period.as_str() {
1814 "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
1815 "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
1816 _ => chrono::Local::now().date_naive(),
1817 }
1818 };
1819
1820 let insight_data = match period.as_str() {
1821 "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
1822 "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
1823 _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
1824 };
1825
1826 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1827 println!("\x1b[36m│\x1b[0m \x1b[1;37m{} Summary\x1b[0m \x1b[36m│\x1b[0m", period);
1828 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1829 println!("\x1b[36m│\x1b[0m Total Hours: \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", insight_data.total_hours));
1830 println!("\x1b[36m│\x1b[0m Sessions: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", insight_data.sessions_count);
1831 println!("\x1b[36m│\x1b[0m Avg Session: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", insight_data.avg_session_duration));
1832 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1833 Ok(())
1834}
1835
1836async fn compare_projects(projects: String, _from: Option<String>, _to: Option<String>) -> Result<()> {
1837 let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
1838
1839 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1840 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Comparison\x1b[0m \x1b[36m│\x1b[0m");
1841 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1842 println!("\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&projects, 27));
1843 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Comparison feature in development\x1b[0m \x1b[36m│\x1b[0m");
1844 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1845 Ok(())
1846}
1847
1848async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
1849 match action {
1850 EstimateAction::Create { project, task, hours, due_date } => {
1851 create_estimate(project, task, hours, due_date).await
1852 }
1853 EstimateAction::Record { id, hours } => {
1854 record_actual_time(id, hours).await
1855 }
1856 EstimateAction::List { project } => {
1857 list_estimates(project).await
1858 }
1859 }
1860}
1861
1862async fn create_estimate(project: String, task: String, hours: f64, due_date: Option<String>) -> Result<()> {
1863 let db_path = get_database_path()?;
1864 let db = Database::new(&db_path)?;
1865
1866 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1867 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1868
1869 let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
1870
1871 let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
1872 estimate.due_date = due;
1873
1874 let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
1875
1876 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1877 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimate Created\x1b[0m \x1b[36m│\x1b[0m");
1878 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1879 println!("\x1b[36m│\x1b[0m Task: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&task, 27));
1880 println!("\x1b[36m│\x1b[0m Estimate: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{} hours", hours));
1881 println!("\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m", estimate_id);
1882 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1883 Ok(())
1884}
1885
1886async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
1887 let db_path = get_database_path()?;
1888 let db = Database::new(&db_path)?;
1889
1890 TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
1891 println!("\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m", hours, id);
1892 Ok(())
1893}
1894
1895async fn list_estimates(project: String) -> Result<()> {
1896 let db_path = get_database_path()?;
1897 let db = Database::new(&db_path)?;
1898
1899 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1900 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1901
1902 let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
1903
1904 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1905 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimates\x1b[0m \x1b[36m│\x1b[0m");
1906 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1907
1908 for est in &estimates {
1909 let variance = est.variance();
1910 let variance_str = if let Some(v) = variance {
1911 if v > 0.0 {
1912 format!("\x1b[31m+{:.1}h over\x1b[0m", v)
1913 } else {
1914 format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
1915 }
1916 } else {
1917 "N/A".to_string()
1918 };
1919
1920 println!("\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&est.task_name, 25));
1921 let actual_str = est.actual_hours.map(|h| format!("{:.1}h", h)).unwrap_or_else(|| "N/A".to_string());
1922 println!("\x1b[36m│\x1b[0m Est: {}h | Actual: {} | {} \x1b[36m│\x1b[0m",
1923 est.estimated_hours,
1924 actual_str,
1925 variance_str
1926 );
1927 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1928 }
1929
1930 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1931 Ok(())
1932}
1933
1934async fn handle_branch_action(action: BranchAction) -> Result<()> {
1935 match action {
1936 BranchAction::List { project } => {
1937 list_branches(project).await
1938 }
1939 BranchAction::Stats { project, branch } => {
1940 show_branch_stats(project, branch).await
1941 }
1942 }
1943}
1944
1945async fn list_branches(project: String) -> Result<()> {
1946 let db_path = get_database_path()?;
1947 let db = Database::new(&db_path)?;
1948
1949 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
1950 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
1951
1952 let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
1953
1954 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1955 println!("\x1b[36m│\x1b[0m \x1b[1;37mGit Branches\x1b[0m \x1b[36m│\x1b[0m");
1956 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1957
1958 for branch in &branches {
1959 println!("\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&branch.branch_name, 25));
1960 println!("\x1b[36m│\x1b[0m Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m", format!("{:.1}h", branch.total_hours()));
1961 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1962 }
1963
1964 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1965 Ok(())
1966}
1967
1968async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
1969 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1970 println!("\x1b[36m│\x1b[0m \x1b[1;37mBranch Statistics\x1b[0m \x1b[36m│\x1b[0m");
1971 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1972 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project, 27));
1973 if let Some(b) = branch {
1974 println!("\x1b[36m│\x1b[0m Branch: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&b, 27));
1975 }
1976 println!("\x1b[36m│\x1b[0m \x1b[33m⚠ Branch stats in development\x1b[0m \x1b[36m│\x1b[0m");
1977 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1978 Ok(())
1979}
1980
1981async fn handle_template_action(action: TemplateAction) -> Result<()> {
1983 match action {
1984 TemplateAction::Create { name, description, tags, workspace_path } => {
1985 create_template(name, description, tags, workspace_path).await
1986 }
1987 TemplateAction::List => {
1988 list_templates().await
1989 }
1990 TemplateAction::Delete { template } => {
1991 delete_template(template).await
1992 }
1993 TemplateAction::Use { template, project_name, path } => {
1994 use_template(template, project_name, path).await
1995 }
1996 }
1997}
1998
1999async fn create_template(name: String, description: Option<String>, tags: Option<String>, workspace_path: Option<PathBuf>) -> Result<()> {
2000 let db_path = get_database_path()?;
2001 let db = Database::new(&db_path)?;
2002
2003 let default_tags = tags
2004 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2005 .unwrap_or_default();
2006
2007 let mut template = ProjectTemplate::new(name.clone())
2008 .with_tags(default_tags);
2009
2010 let desc_clone = description.clone();
2011 if let Some(desc) = description {
2012 template = template.with_description(desc);
2013 }
2014 if let Some(path) = workspace_path {
2015 template = template.with_workspace_path(path);
2016 }
2017
2018 let _template_id = TemplateQueries::create(&db.connection, &template)?;
2019
2020 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2021 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplate Created\x1b[0m \x1b[36m│\x1b[0m");
2022 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2023 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
2024 if let Some(desc) = &desc_clone {
2025 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2026 }
2027 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2028 Ok(())
2029}
2030
2031async fn list_templates() -> Result<()> {
2032 let db_path = get_database_path()?;
2033 let db = Database::new(&db_path)?;
2034
2035 let templates = TemplateQueries::list_all(&db.connection)?;
2036
2037 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2038 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplates\x1b[0m \x1b[36m│\x1b[0m");
2039 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2040
2041 if templates.is_empty() {
2042 println!("\x1b[36m│\x1b[0m No templates found. \x1b[36m│\x1b[0m");
2043 } else {
2044 for template in &templates {
2045 println!("\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&template.name, 25));
2046 if let Some(desc) = &template.description {
2047 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2048 }
2049 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2050 }
2051 }
2052
2053 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2054 Ok(())
2055}
2056
2057async fn delete_template(_template: String) -> Result<()> {
2058 println!("\x1b[33m⚠ Template deletion not yet implemented\x1b[0m");
2059 Ok(())
2060}
2061
2062async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2063 let db_path = get_database_path()?;
2064 let db = Database::new(&db_path)?;
2065
2066 let templates = TemplateQueries::list_all(&db.connection)?;
2067 let selected_template = templates.iter()
2068 .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2069 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2070
2071 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2073 let canonical_path = canonicalize_path(&project_path)?;
2074
2075 if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2077 return Err(anyhow::anyhow!("Project already exists at this path"));
2078 }
2079
2080 let git_hash = if is_git_repository(&canonical_path) {
2081 get_git_hash(&canonical_path)
2082 } else {
2083 None
2084 };
2085
2086 let template_desc = selected_template.description.clone();
2087 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2088 .with_git_hash(git_hash)
2089 .with_description(template_desc);
2090
2091 let project_id = ProjectQueries::create(&db.connection, &project)?;
2092 project.id = Some(project_id);
2093
2094 for goal_def in &selected_template.default_goals {
2099 let mut goal = Goal::new(goal_def.name.clone(), goal_def.target_hours)
2100 .with_project(project_id);
2101 if let Some(desc) = &goal_def.description {
2102 goal = goal.with_description(desc.clone());
2103 }
2104 GoalQueries::create(&db.connection, &goal)?;
2105 }
2106
2107 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2108 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Created from Template\x1b[0m \x1b[36m│\x1b[0m");
2109 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2110 println!("\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&selected_template.name, 27));
2111 println!("\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 27));
2112 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2113 Ok(())
2114}
2115
2116async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2118 match action {
2119 WorkspaceAction::Create { name, description, path } => {
2120 create_workspace(name, description, path).await
2121 }
2122 WorkspaceAction::List => {
2123 list_workspaces().await
2124 }
2125 WorkspaceAction::AddProject { workspace, project } => {
2126 add_project_to_workspace(workspace, project).await
2127 }
2128 WorkspaceAction::RemoveProject { workspace, project } => {
2129 remove_project_from_workspace(workspace, project).await
2130 }
2131 WorkspaceAction::Projects { workspace } => {
2132 list_workspace_projects(workspace).await
2133 }
2134 WorkspaceAction::Delete { workspace } => {
2135 delete_workspace(workspace).await
2136 }
2137 }
2138}
2139
2140async fn create_workspace(name: String, description: Option<String>, path: Option<PathBuf>) -> Result<()> {
2141 let db_path = get_database_path()?;
2142 let db = Database::new(&db_path)?;
2143
2144 let mut workspace = Workspace::new(name.clone());
2145 let desc_clone = description.clone();
2146 if let Some(desc) = description {
2147 workspace = workspace.with_description(desc);
2148 }
2149 if let Some(p) = path {
2150 workspace = workspace.with_path(p);
2151 }
2152
2153 let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2154
2155 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2156 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Created\x1b[0m \x1b[36m│\x1b[0m");
2157 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2158 println!("\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&name, 27));
2159 if let Some(desc) = &desc_clone {
2160 println!("\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2161 }
2162 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2163 Ok(())
2164}
2165
2166async fn list_workspaces() -> Result<()> {
2167 let db_path = get_database_path()?;
2168 let db = Database::new(&db_path)?;
2169
2170 let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2171
2172 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2173 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspaces\x1b[0m \x1b[36m│\x1b[0m");
2174 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2175
2176 if workspaces.is_empty() {
2177 println!("\x1b[36m│\x1b[0m No workspaces found. \x1b[36m│\x1b[0m");
2178 } else {
2179 for workspace in &workspaces {
2180 println!("\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&workspace.name, 25));
2181 if let Some(desc) = &workspace.description {
2182 println!("\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 27));
2183 }
2184 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2185 }
2186 }
2187
2188 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2189 Ok(())
2190}
2191
2192async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2193 let db_path = get_database_path()?;
2194 let db = Database::new(&db_path)?;
2195
2196 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2198 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2199
2200 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2202 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2203
2204 let workspace_id = workspace_obj.id
2205 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2206 let project_id = project_obj.id
2207 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2208
2209 if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2210 println!("\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2211 } else {
2212 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2213 }
2214
2215 Ok(())
2216}
2217
2218async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2219 let db_path = get_database_path()?;
2220 let db = Database::new(&db_path)?;
2221
2222 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2224 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2225
2226 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2228 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2229
2230 let workspace_id = workspace_obj.id
2231 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2232 let project_id = project_obj.id
2233 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2234
2235 if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2236 println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2237 } else {
2238 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2239 }
2240
2241 Ok(())
2242}
2243
2244async fn list_workspace_projects(workspace: String) -> Result<()> {
2245 let db_path = get_database_path()?;
2246 let db = Database::new(&db_path)?;
2247
2248 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2250 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2251
2252 let workspace_id = workspace_obj.id
2253 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2254 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2255
2256 if projects.is_empty() {
2257 println!("\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'", workspace);
2258 return Ok(());
2259 }
2260
2261 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2262 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Projects\x1b[0m \x1b[36m│\x1b[0m");
2263 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2264 println!("\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&workspace, 25));
2265 println!("\x1b[36m│\x1b[0m Projects: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", format!("{} projects", projects.len()));
2266 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2267
2268 for project in &projects {
2269 let status_indicator = if !project.is_archived { "\x1b[32m●\x1b[0m" } else { "\x1b[31m○\x1b[0m" };
2270 println!("\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2271 status_indicator,
2272 truncate_string(&project.name, 33));
2273 }
2274
2275 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2276 Ok(())
2277}
2278
2279async fn delete_workspace(workspace: String) -> Result<()> {
2280 let db_path = get_database_path()?;
2281 let db = Database::new(&db_path)?;
2282
2283 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2285 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2286
2287 let workspace_id = workspace_obj.id
2288 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2289
2290 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2292 if !projects.is_empty() {
2293 println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.",
2294 workspace, projects.len());
2295 return Ok(());
2296 }
2297
2298 if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2299 println!("\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'", workspace);
2300 } else {
2301 println!("\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'", workspace);
2302 }
2303
2304 Ok(())
2305}
2306
2307async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2309 match action {
2310 CalendarAction::Add { name, start, end, event_type, project, description } => {
2311 add_calendar_event(name, start, end, event_type, project, description).await
2312 }
2313 CalendarAction::List { from, to, project } => {
2314 list_calendar_events(from, to, project).await
2315 }
2316 CalendarAction::Delete { id } => {
2317 delete_calendar_event(id).await
2318 }
2319 }
2320}
2321
2322async fn add_calendar_event(_name: String, _start: String, _end: Option<String>, _event_type: Option<String>, _project: Option<String>, _description: Option<String>) -> Result<()> {
2323 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2324 Ok(())
2325}
2326
2327async fn list_calendar_events(_from: Option<String>, _to: Option<String>, _project: Option<String>) -> Result<()> {
2328 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2329 Ok(())
2330}
2331
2332async fn delete_calendar_event(_id: i64) -> Result<()> {
2333 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2334 Ok(())
2335}
2336
2337async fn handle_issue_action(action: IssueAction) -> Result<()> {
2339 match action {
2340 IssueAction::Sync { project, tracker_type } => {
2341 sync_issues(project, tracker_type).await
2342 }
2343 IssueAction::List { project, status } => {
2344 list_issues(project, status).await
2345 }
2346 IssueAction::Link { session_id, issue_id } => {
2347 link_session_to_issue(session_id, issue_id).await
2348 }
2349 }
2350}
2351
2352async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2353 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2354 Ok(())
2355}
2356
2357async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2358 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2359 Ok(())
2360}
2361
2362async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2363 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2364 Ok(())
2365}
2366
2367async fn handle_client_action(action: ClientAction) -> Result<()> {
2369 match action {
2370 ClientAction::Generate { client, from, to, projects, format } => {
2371 generate_client_report(client, from, to, projects, format).await
2372 }
2373 ClientAction::List { client } => {
2374 list_client_reports(client).await
2375 }
2376 ClientAction::View { id } => {
2377 view_client_report(id).await
2378 }
2379 }
2380}
2381
2382async fn generate_client_report(_client: String, _from: String, _to: String, _projects: Option<String>, _format: Option<String>) -> Result<()> {
2383 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2384 Ok(())
2385}
2386
2387async fn list_client_reports(_client: Option<String>) -> Result<()> {
2388 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2389 Ok(())
2390}
2391
2392async fn view_client_report(_id: i64) -> Result<()> {
2393 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2394 Ok(())
2395}
2396
2397fn should_quit(event: crossterm::event::Event) -> bool {
2398 match event {
2399 crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
2400 matches!(key.code, crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc)
2401 }
2402 _ => false,
2403 }
2404}
2405
2406async fn init_project_with_db(
2408 name: Option<String>,
2409 canonical_path: Option<PathBuf>,
2410 description: Option<String>,
2411 conn: &rusqlite::Connection,
2412) -> Result<()> {
2413 let canonical_path = canonical_path.ok_or_else(|| anyhow::anyhow!("Canonical path required"))?;
2414 let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
2415
2416 if let Some(existing) = ProjectQueries::find_by_path(conn, &canonical_path)? {
2418 println!("\x1b[33m⚠ Project already exists:\x1b[0m {}", existing.name);
2419 return Ok(());
2420 }
2421
2422 let git_hash = if is_git_repository(&canonical_path) {
2424 get_git_hash(&canonical_path)
2425 } else {
2426 None
2427 };
2428
2429 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2431 .with_git_hash(git_hash.clone())
2432 .with_description(description.clone());
2433
2434 let project_id = ProjectQueries::create(conn, &project)?;
2436 project.id = Some(project_id);
2437
2438 let marker_path = canonical_path.join(".tempo");
2440 if !marker_path.exists() {
2441 std::fs::write(&marker_path, format!("# Tempo time tracking project\nname: {}\n", project_name))?;
2442 }
2443
2444 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2445 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Initialized\x1b[0m \x1b[36m│\x1b[0m");
2446 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2447 println!("\x1b[36m│\x1b[0m Name: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&project_name, 25));
2448 println!("\x1b[36m│\x1b[0m Path: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(&canonical_path.display().to_string(), 25));
2449
2450 if let Some(desc) = &description {
2451 println!("\x1b[36m│\x1b[0m Description: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(desc, 25));
2452 }
2453
2454 if is_git_repository(&canonical_path) {
2455 println!("\x1b[36m│\x1b[0m Git: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m", "Repository detected");
2456 if let Some(hash) = &git_hash {
2457 println!("\x1b[36m│\x1b[0m Git Hash: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", truncate_string(hash, 25));
2458 }
2459 }
2460
2461 println!("\x1b[36m│\x1b[0m ID: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m", project_id);
2462 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2463
2464 Ok(())
2465}
2466
2467async fn show_pool_stats() -> Result<()> {
2469 match get_pool_stats() {
2470 Ok(stats) => {
2471 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2472 println!("\x1b[36m│\x1b[0m \x1b[1;37mDatabase Pool Statistics\x1b[0m \x1b[36m│\x1b[0m");
2473 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2474 println!("\x1b[36m│\x1b[0m Total Created: \x1b[32m{:<19}\x1b[0m \x1b[36m│\x1b[0m", stats.total_connections_created);
2475 println!("\x1b[36m│\x1b[0m Active: \x1b[33m{:<19}\x1b[0m \x1b[36m│\x1b[0m", stats.active_connections);
2476 println!("\x1b[36m│\x1b[0m Available in Pool:\x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m", stats.connections_in_pool);
2477 println!("\x1b[36m│\x1b[0m Total Requests: \x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m", stats.connection_requests);
2478 println!("\x1b[36m│\x1b[0m Timeouts: \x1b[31m{:<19}\x1b[0m \x1b[36m│\x1b[0m", stats.connection_timeouts);
2479 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2480 }
2481 Err(_) => {
2482 println!("\x1b[33m⚠ Database pool not initialized or not available\x1b[0m");
2483 println!(" Using direct database connections as fallback");
2484 }
2485 }
2486 Ok(())
2487}