1use super::{
2 BranchAction, CalendarAction, Cli, ClientAction, Commands, ConfigAction, EstimateAction,
3 GoalAction, IssueAction, ProjectAction, SessionAction, TagAction, TemplateAction,
4 WorkspaceAction,
5};
6use crate::cli::formatter::{CliFormatter, format_duration_clean, truncate_string};
7use crate::cli::reports::ReportGenerator;
8use crate::db::advanced_queries::{
9 GitBranchQueries, GoalQueries, InsightQueries, TemplateQueries, TimeEstimateQueries,
10 WorkspaceQueries,
11};
12use crate::db::queries::{ProjectQueries, SessionEditQueries, SessionQueries, TagQueries};
13use crate::db::{get_connection, get_database_path, get_pool_stats, Database};
14use crate::models::{Goal, Project, ProjectTemplate, Tag, TimeEstimate, Workspace};
15use crate::utils::config::{load_config, save_config};
16use crate::utils::ipc::{get_socket_path, is_daemon_running, IpcClient, IpcMessage, IpcResponse};
17use crate::utils::paths::{
18 canonicalize_path, detect_project_name, get_git_hash, is_git_repository, validate_project_path,
19};
20use crate::utils::validation::{validate_project_description, validate_project_name};
21use anyhow::{Context, Result};
22use chrono::{TimeZone, Utc};
23use serde::Deserialize;
24use std::env;
25use std::path::PathBuf;
26use std::process::{Command, Stdio};
27
28use crate::ui::dashboard::Dashboard;
29use crate::ui::history::SessionHistoryBrowser;
30use crate::ui::timer::InteractiveTimer;
31use crossterm::{
32 execute,
33 terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
34};
35use ratatui::{backend::CrosstermBackend, Terminal};
36use std::io;
37use tokio::runtime::Handle;
38
39pub async fn handle_command(cli: Cli) -> Result<()> {
40 match cli.command {
41 Commands::Start => start_daemon().await,
42
43 Commands::Stop => stop_daemon().await,
44
45 Commands::Restart => restart_daemon().await,
46
47 Commands::Status => status_daemon().await,
48
49 Commands::Init {
50 name,
51 path,
52 description,
53 } => init_project(name, path, description).await,
54
55 Commands::List { all, tag } => list_projects(all, tag).await,
56
57 Commands::Report {
58 project,
59 from,
60 to,
61 format,
62 group,
63 } => generate_report(project, from, to, format, group).await,
64
65 Commands::Project { action } => handle_project_action(action).await,
66
67 Commands::Session { action } => handle_session_action(action).await,
68
69 Commands::Tag { action } => handle_tag_action(action).await,
70
71 Commands::Config { action } => handle_config_action(action).await,
72
73 Commands::Dashboard => launch_dashboard().await,
74
75 Commands::Tui => launch_dashboard().await,
76
77 Commands::Timer => launch_timer().await,
78
79 Commands::History => launch_history().await,
80
81 Commands::Goal { action } => handle_goal_action(action).await,
82
83 Commands::Insights { period, project } => show_insights(period, project).await,
84
85 Commands::Summary { period, from } => show_summary(period, from).await,
86
87 Commands::Compare { projects, from, to } => compare_projects(projects, from, to).await,
88
89 Commands::PoolStats => show_pool_stats().await,
90
91 Commands::Estimate { action } => handle_estimate_action(action).await,
92
93 Commands::Branch { action } => handle_branch_action(action).await,
94
95 Commands::Template { action } => handle_template_action(action).await,
96
97 Commands::Workspace { action } => handle_workspace_action(action).await,
98
99 Commands::Calendar { action } => handle_calendar_action(action).await,
100
101 Commands::Issue { action } => handle_issue_action(action).await,
102
103 Commands::Client { action } => handle_client_action(action).await,
104
105 Commands::Update {
106 check,
107 force,
108 verbose,
109 } => handle_update(check, force, verbose).await,
110
111 Commands::Completions { shell } => {
112 Cli::generate_completions(shell);
113 Ok(())
114 }
115 }
116}
117
118async fn handle_project_action(action: ProjectAction) -> Result<()> {
119 match action {
120 ProjectAction::Archive { project } => archive_project(project).await,
121
122 ProjectAction::Unarchive { project } => unarchive_project(project).await,
123
124 ProjectAction::UpdatePath { project, path } => update_project_path(project, path).await,
125
126 ProjectAction::AddTag { project, tag } => add_tag_to_project(project, tag).await,
127
128 ProjectAction::RemoveTag { project, tag } => remove_tag_from_project(project, tag).await,
129 }
130}
131
132async fn handle_session_action(action: SessionAction) -> Result<()> {
133 match action {
134 SessionAction::Start { project, context } => start_session(project, context).await,
135
136 SessionAction::Stop => stop_session().await,
137
138 SessionAction::Pause => pause_session().await,
139
140 SessionAction::Resume => resume_session().await,
141
142 SessionAction::Current => current_session().await,
143
144 SessionAction::List { limit, project } => list_sessions(limit, project).await,
145
146 SessionAction::Edit {
147 id,
148 start,
149 end,
150 project,
151 reason,
152 } => edit_session(id, start, end, project, reason).await,
153
154 SessionAction::Delete { id, force } => delete_session(id, force).await,
155
156 SessionAction::Merge {
157 session_ids,
158 project,
159 notes,
160 } => merge_sessions(session_ids, project, notes).await,
161
162 SessionAction::Split {
163 session_id,
164 split_times,
165 notes,
166 } => split_session(session_id, split_times, notes).await,
167 }
168}
169
170async fn handle_tag_action(action: TagAction) -> Result<()> {
171 match action {
172 TagAction::Create {
173 name,
174 color,
175 description,
176 } => create_tag(name, color, description).await,
177
178 TagAction::List => list_tags().await,
179
180 TagAction::Delete { name } => delete_tag(name).await,
181 }
182}
183
184async fn handle_config_action(action: ConfigAction) -> Result<()> {
185 match action {
186 ConfigAction::Show => show_config().await,
187
188 ConfigAction::Set { key, value } => set_config(key, value).await,
189
190 ConfigAction::Get { key } => get_config(key).await,
191
192 ConfigAction::Reset => reset_config().await,
193 }
194}
195
196async fn start_daemon() -> Result<()> {
198 if is_daemon_running() {
199 println!("Daemon is already running");
200 return Ok(());
201 }
202
203 println!("Starting tempo daemon...");
204
205 let current_exe = std::env::current_exe()?;
206 let daemon_exe = current_exe.with_file_name("tempo-daemon");
207
208 if !daemon_exe.exists() {
209 return Err(anyhow::anyhow!(
210 "tempo-daemon executable not found at {:?}",
211 daemon_exe
212 ));
213 }
214
215 let mut cmd = Command::new(&daemon_exe);
216 cmd.stdout(Stdio::null())
217 .stderr(Stdio::null())
218 .stdin(Stdio::null());
219
220 #[cfg(unix)]
221 {
222 use std::os::unix::process::CommandExt;
223 cmd.process_group(0);
224 }
225
226 let child = cmd.spawn()?;
227
228 tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
230
231 if is_daemon_running() {
232 println!("Daemon started successfully (PID: {})", child.id());
233 Ok(())
234 } else {
235 Err(anyhow::anyhow!("Failed to start daemon"))
236 }
237}
238
239async fn stop_daemon() -> Result<()> {
240 if !is_daemon_running() {
241 println!("Daemon is not running");
242 return Ok(());
243 }
244
245 println!("Stopping tempo daemon...");
246
247 if let Ok(socket_path) = get_socket_path() {
249 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
250 match client.send_message(&IpcMessage::Shutdown).await {
251 Ok(_) => {
252 println!("Daemon stopped successfully");
253 return Ok(());
254 }
255 Err(e) => {
256 eprintln!("Failed to send shutdown message: {}", e);
257 }
258 }
259 }
260 }
261
262 if let Ok(Some(pid)) = crate::utils::ipc::read_pid_file() {
264 #[cfg(unix)]
265 {
266 let result = Command::new("kill").arg(pid.to_string()).output();
267
268 match result {
269 Ok(_) => println!("Daemon stopped via kill signal"),
270 Err(e) => eprintln!("Failed to kill daemon: {}", e),
271 }
272 }
273
274 #[cfg(windows)]
275 {
276 let result = Command::new("taskkill")
277 .args(&["/PID", &pid.to_string(), "/F"])
278 .output();
279
280 match result {
281 Ok(_) => println!("Daemon stopped via taskkill"),
282 Err(e) => eprintln!("Failed to kill daemon: {}", e),
283 }
284 }
285 }
286
287 Ok(())
288}
289
290async fn restart_daemon() -> Result<()> {
291 println!("Restarting tempo daemon...");
292 stop_daemon().await?;
293 tokio::time::sleep(tokio::time::Duration::from_secs(1)).await;
294 start_daemon().await
295}
296
297async fn status_daemon() -> Result<()> {
298 if !is_daemon_running() {
299 print_daemon_not_running();
300 return Ok(());
301 }
302
303 if let Ok(socket_path) = get_socket_path() {
304 match IpcClient::connect(&socket_path).await {
305 Ok(mut client) => {
306 match client.send_message(&IpcMessage::GetStatus).await {
307 Ok(IpcResponse::Status {
308 daemon_running: _,
309 active_session,
310 uptime,
311 }) => {
312 print_daemon_status(uptime, active_session.as_ref());
313 }
314 Ok(IpcResponse::Pong) => {
315 print_daemon_status(0, None); }
317 Ok(other) => {
318 println!("Daemon is running (unexpected response: {:?})", other);
319 }
320 Err(e) => {
321 println!("Daemon is running but not responding: {}", e);
322 }
323 }
324 }
325 Err(e) => {
326 println!("Daemon appears to be running but cannot connect: {}", e);
327 }
328 }
329 } else {
330 println!("Cannot determine socket path");
331 }
332
333 Ok(())
334}
335
336async fn start_session(project: Option<String>, context: Option<String>) -> Result<()> {
338 if !is_daemon_running() {
339 println!("Daemon is not running. Start it with 'tempo start'");
340 return Ok(());
341 }
342
343 let project_path = if let Some(proj) = project {
344 PathBuf::from(proj)
345 } else {
346 env::current_dir()?
347 };
348
349 let context = context.unwrap_or_else(|| "manual".to_string());
350
351 let socket_path = get_socket_path()?;
352 let mut client = IpcClient::connect(&socket_path).await?;
353
354 let message = IpcMessage::StartSession {
355 project_path: Some(project_path.clone()),
356 context,
357 };
358
359 match client.send_message(&message).await {
360 Ok(IpcResponse::Ok) => {
361 println!("Session started for project at {:?}", project_path);
362 }
363 Ok(IpcResponse::Error(message)) => {
364 println!("Failed to start session: {}", message);
365 }
366 Ok(other) => {
367 println!("Unexpected response: {:?}", other);
368 }
369 Err(e) => {
370 println!("Failed to communicate with daemon: {}", e);
371 }
372 }
373
374 Ok(())
375}
376
377async fn stop_session() -> Result<()> {
378 if !is_daemon_running() {
379 println!("Daemon is not running");
380 return Ok(());
381 }
382
383 let socket_path = get_socket_path()?;
384 let mut client = IpcClient::connect(&socket_path).await?;
385
386 match client.send_message(&IpcMessage::StopSession).await {
387 Ok(IpcResponse::Ok) => {
388 println!("Session stopped");
389 }
390 Ok(IpcResponse::Error(message)) => {
391 println!("Failed to stop session: {}", message);
392 }
393 Ok(other) => {
394 println!("Unexpected response: {:?}", other);
395 }
396 Err(e) => {
397 println!("Failed to communicate with daemon: {}", e);
398 }
399 }
400
401 Ok(())
402}
403
404async fn pause_session() -> Result<()> {
405 if !is_daemon_running() {
406 println!("Daemon is not running");
407 return Ok(());
408 }
409
410 let socket_path = get_socket_path()?;
411 let mut client = IpcClient::connect(&socket_path).await?;
412
413 match client.send_message(&IpcMessage::PauseSession).await {
414 Ok(IpcResponse::Ok) => {
415 println!("Session paused");
416 }
417 Ok(IpcResponse::Error(message)) => {
418 println!("Failed to pause session: {}", message);
419 }
420 Ok(other) => {
421 println!("Unexpected response: {:?}", other);
422 }
423 Err(e) => {
424 println!("Failed to communicate with daemon: {}", e);
425 }
426 }
427
428 Ok(())
429}
430
431async fn resume_session() -> Result<()> {
432 if !is_daemon_running() {
433 println!("Daemon is not running");
434 return Ok(());
435 }
436
437 let socket_path = get_socket_path()?;
438 let mut client = IpcClient::connect(&socket_path).await?;
439
440 match client.send_message(&IpcMessage::ResumeSession).await {
441 Ok(IpcResponse::Ok) => {
442 println!("Session resumed");
443 }
444 Ok(IpcResponse::Error(message)) => {
445 println!("Failed to resume session: {}", message);
446 }
447 Ok(other) => {
448 println!("Unexpected response: {:?}", other);
449 }
450 Err(e) => {
451 println!("Failed to communicate with daemon: {}", e);
452 }
453 }
454
455 Ok(())
456}
457
458async fn current_session() -> Result<()> {
459 if !is_daemon_running() {
460 print_daemon_not_running();
461 return Ok(());
462 }
463
464 let socket_path = get_socket_path()?;
465 let mut client = IpcClient::connect(&socket_path).await?;
466
467 match client.send_message(&IpcMessage::GetActiveSession).await {
468 Ok(IpcResponse::SessionInfo(session)) => {
469 print_formatted_session(&session)?;
470 }
471 Ok(IpcResponse::Error(message)) => {
472 print_no_active_session(&message);
473 }
474 Ok(other) => {
475 println!("Unexpected response: {:?}", other);
476 }
477 Err(e) => {
478 println!("Failed to communicate with daemon: {}", e);
479 }
480 }
481
482 Ok(())
483}
484
485async fn generate_report(
487 project: Option<String>,
488 from: Option<String>,
489 to: Option<String>,
490 format: Option<String>,
491 group: Option<String>,
492) -> Result<()> {
493 println!("Generating time report...");
494
495 let generator = ReportGenerator::new()?;
496 let report = generator.generate_report(project, from, to, group)?;
497
498 match format.as_deref() {
499 Some("csv") => {
500 let output_path = PathBuf::from("tempo-report.csv");
501 generator.export_csv(&report, &output_path)?;
502 println!("Report exported to: {:?}", output_path);
503 }
504 Some("json") => {
505 let output_path = PathBuf::from("tempo-report.json");
506 generator.export_json(&report, &output_path)?;
507 println!("Report exported to: {:?}", output_path);
508 }
509 _ => {
510 print_formatted_report(&report)?;
512 }
513 }
514
515 Ok(())
516}
517
518fn print_formatted_session(session: &crate::utils::ipc::SessionInfo) -> Result<()> {
520 CliFormatter::print_section_header("Current Session");
521 CliFormatter::print_status("Active", true);
522 CliFormatter::print_field_bold("Project", &session.project_name, Some("yellow"));
523 CliFormatter::print_field_bold("Duration", &format_duration_clean(session.duration), Some("green"));
524 CliFormatter::print_field("Started", &session.start_time.format("%H:%M:%S").to_string(), None);
525 CliFormatter::print_field("Context", &session.context, Some(get_context_color(&session.context)));
526 CliFormatter::print_field("Path", &session.project_path.to_string_lossy(), Some("gray"));
527 Ok(())
528}
529
530fn get_context_color(context: &str) -> &str {
531 match context {
532 "terminal" => "cyan",
533 "ide" => "magenta",
534 "linked" => "yellow",
535 "manual" => "blue",
536 _ => "white",
537 }
538}
539
540fn print_formatted_report(report: &crate::cli::reports::TimeReport) -> Result<()> {
541 CliFormatter::print_section_header("Time Report");
542
543 for (project_name, project_summary) in &report.projects {
544 CliFormatter::print_project_entry(project_name, &format_duration_clean(project_summary.total_duration));
545
546 for (context, duration) in &project_summary.contexts {
547 CliFormatter::print_context_entry(context, &format_duration_clean(*duration));
548 }
549 println!(); }
551
552 CliFormatter::print_summary("Total Time", &format_duration_clean(report.total_duration));
553 Ok(())
554}
555
556fn print_daemon_not_running() {
560 CliFormatter::print_section_header("Daemon Status");
561 CliFormatter::print_status("Offline", false);
562 CliFormatter::print_warning("Daemon is not running.");
563 CliFormatter::print_info("Start it with: tempo start");
564}
565
566fn print_no_active_session(message: &str) {
567 CliFormatter::print_section_header("Current Session");
568 CliFormatter::print_status("Idle", false);
569 CliFormatter::print_empty_state(message);
570 CliFormatter::print_info("Start tracking: tempo session start");
571}
572
573fn print_daemon_status(uptime: u64, active_session: Option<&crate::utils::ipc::SessionInfo>) {
574 CliFormatter::print_section_header("Daemon Status");
575 CliFormatter::print_status("Online", true);
576 CliFormatter::print_field("Uptime", &format_duration_clean(uptime as i64), None);
577
578 if let Some(session) = active_session {
579 println!();
580 CliFormatter::print_field_bold("Active Session", "", None);
581 CliFormatter::print_field(" Project", &session.project_name, Some("yellow"));
582 CliFormatter::print_field(" Duration", &format_duration_clean(session.duration), Some("green"));
583 CliFormatter::print_field(" Context", &session.context, Some(get_context_color(&session.context)));
584 } else {
585 CliFormatter::print_field("Session", "No active session", Some("yellow"));
586 }
587}
588
589async fn init_project(
591 name: Option<String>,
592 path: Option<PathBuf>,
593 description: Option<String>,
594) -> Result<()> {
595 let validated_name = if let Some(n) = name.as_ref() {
597 Some(validate_project_name(n).with_context(|| format!("Invalid project name '{}'", n))?)
598 } else {
599 None
600 };
601
602 let validated_description = if let Some(d) = description.as_ref() {
603 Some(validate_project_description(d).with_context(|| "Invalid project description")?)
604 } else {
605 None
606 };
607
608 let project_path =
609 path.unwrap_or_else(|| env::current_dir().expect("Failed to get current directory"));
610
611 let canonical_path = validate_project_path(&project_path)
613 .with_context(|| format!("Invalid project path: {}", project_path.display()))?;
614
615 let project_name = validated_name.clone().unwrap_or_else(|| {
616 let detected = detect_project_name(&canonical_path);
617 validate_project_name(&detected).unwrap_or_else(|_| "project".to_string())
618 });
619
620 let conn = match get_connection().await {
622 Ok(conn) => conn,
623 Err(_) => {
624 let db_path = get_database_path()?;
626 let db = Database::new(&db_path)?;
627 return init_project_with_db(
628 validated_name,
629 Some(canonical_path),
630 validated_description,
631 &db.connection,
632 )
633 .await;
634 }
635 };
636
637 if let Some(existing) = ProjectQueries::find_by_path(conn.connection(), &canonical_path)? {
639 eprintln!(
640 "\x1b[33m! Warning:\x1b[0m A project named '{}' already exists at this path.",
641 existing.name
642 );
643 eprintln!("Use 'tempo list' to see all projects or choose a different location.");
644 return Ok(());
645 }
646
647 init_project_with_db(
649 Some(project_name.clone()),
650 Some(canonical_path.clone()),
651 validated_description,
652 conn.connection(),
653 )
654 .await?;
655
656 println!(
657 "\x1b[32m+ Success:\x1b[0m Project '{}' initialized at {}",
658 project_name,
659 canonical_path.display()
660 );
661 println!("Start tracking time with: \x1b[36mtempo start\x1b[0m");
662
663 Ok(())
664}
665
666async fn list_projects(include_archived: bool, tag_filter: Option<String>) -> Result<()> {
667 let db_path = get_database_path()?;
669 let db = Database::new(&db_path)?;
670
671 let projects = ProjectQueries::list_all(&db.connection, include_archived)?;
673
674 if projects.is_empty() {
675 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
676 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Projects\x1b[0m \x1b[36m│\x1b[0m");
677 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
678 println!("\x1b[36m│\x1b[0m No projects found. \x1b[36m│\x1b[0m");
679 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
680 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a project:\x1b[0m \x1b[36m│\x1b[0m");
681 println!("\x1b[36m│\x1b[0m \x1b[96mtempo init [project-name]\x1b[0m \x1b[36m│\x1b[0m");
682 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
683 return Ok(());
684 }
685
686 let filtered_projects = if let Some(_tag) = tag_filter {
688 projects
690 } else {
691 projects
692 };
693
694 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
695 println!("\x1b[36m│\x1b[0m \x1b[1;37mProjects\x1b[0m \x1b[36m│\x1b[0m");
696 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
697
698 for project in &filtered_projects {
699 let status_icon = if project.is_archived { "[A]" } else { "[P]" };
700 let status_color = if project.is_archived {
701 "\x1b[90m"
702 } else {
703 "\x1b[37m"
704 };
705 let git_indicator = if project.git_hash.is_some() {
706 " (git)"
707 } else {
708 ""
709 };
710
711 println!(
712 "\x1b[36m│\x1b[0m {} {}{:<25}\x1b[0m \x1b[36m│\x1b[0m",
713 status_icon,
714 status_color,
715 format!("{}{}", truncate_string(&project.name, 20), git_indicator)
716 );
717
718 if let Some(description) = &project.description {
719 println!(
720 "\x1b[36m│\x1b[0m \x1b[2;37m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
721 truncate_string(description, 35)
722 );
723 }
724
725 let path_display = project.path.to_string_lossy();
726 if path_display.len() > 35 {
727 let home_dir = dirs::home_dir();
728 let display_path = if let Some(home) = home_dir {
729 if let Ok(stripped) = project.path.strip_prefix(&home) {
730 format!("~/{}", stripped.display())
731 } else {
732 path_display.to_string()
733 }
734 } else {
735 path_display.to_string()
736 };
737 println!(
738 "\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
739 truncate_string(&display_path, 35)
740 );
741 } else {
742 println!(
743 "\x1b[36m│\x1b[0m \x1b[90m{:<35}\x1b[0m \x1b[36m│\x1b[0m",
744 truncate_string(&path_display, 35)
745 );
746 }
747
748 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
749 }
750
751 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
752 println!(
753 "\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
754 format!("{} projects", filtered_projects.len())
755 );
756 if include_archived {
757 let active_count = filtered_projects.iter().filter(|p| !p.is_archived).count();
758 let archived_count = filtered_projects.iter().filter(|p| p.is_archived).count();
759 println!("\x1b[36m│\x1b[0m \x1b[37mActive:\x1b[0m {:<15} \x1b[90mArchived:\x1b[0m {:<8} \x1b[36m│\x1b[0m",
760 active_count, archived_count
761 );
762 }
763 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
764
765 Ok(())
766}
767
768async fn create_tag(
770 name: String,
771 color: Option<String>,
772 description: Option<String>,
773) -> Result<()> {
774 let db_path = get_database_path()?;
776 let db = Database::new(&db_path)?;
777
778 let mut tag = Tag::new(name);
780 if let Some(c) = color {
781 tag = tag.with_color(c);
782 }
783 if let Some(d) = description {
784 tag = tag.with_description(d);
785 }
786
787 tag.validate()?;
789
790 let existing_tags = TagQueries::list_all(&db.connection)?;
792 if existing_tags.iter().any(|t| t.name == tag.name) {
793 println!("\x1b[33m⚠ Tag already exists:\x1b[0m {}", tag.name);
794 return Ok(());
795 }
796
797 let tag_id = TagQueries::create(&db.connection, &tag)?;
799
800 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
801 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Created\x1b[0m \x1b[36m│\x1b[0m");
802 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
803 println!(
804 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
805 truncate_string(&tag.name, 27)
806 );
807 if let Some(color_val) = &tag.color {
808 println!(
809 "\x1b[36m│\x1b[0m Color: \x1b[37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
810 truncate_string(color_val, 27)
811 );
812 }
813 if let Some(desc) = &tag.description {
814 println!(
815 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
816 truncate_string(desc, 27)
817 );
818 }
819 println!(
820 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
821 tag_id
822 );
823 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
824 println!(
825 "\x1b[36m│\x1b[0m \x1b[32m✓ Tag created successfully\x1b[0m \x1b[36m│\x1b[0m"
826 );
827 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
828
829 Ok(())
830}
831
832async fn list_tags() -> Result<()> {
833 let db_path = get_database_path()?;
835 let db = Database::new(&db_path)?;
836
837 let tags = TagQueries::list_all(&db.connection)?;
839
840 if tags.is_empty() {
841 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
842 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Tags\x1b[0m \x1b[36m│\x1b[0m");
843 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
844 println!("\x1b[36m│\x1b[0m No tags found. \x1b[36m│\x1b[0m");
845 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
846 println!("\x1b[36m│\x1b[0m \x1b[37mCreate a tag:\x1b[0m \x1b[36m│\x1b[0m");
847 println!("\x1b[36m│\x1b[0m \x1b[96mtempo tag create <name>\x1b[0m \x1b[36m│\x1b[0m");
848 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
849 return Ok(());
850 }
851
852 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
853 println!("\x1b[36m│\x1b[0m \x1b[1;37mTags\x1b[0m \x1b[36m│\x1b[0m");
854 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
855
856 for tag in &tags {
857 let color_indicator = if let Some(color) = &tag.color {
858 format!(" ({})", color)
859 } else {
860 String::new()
861 };
862
863 println!(
864 "\x1b[36m│\x1b[0m 🏷️ \x1b[1;33m{:<30}\x1b[0m \x1b[36m│\x1b[0m",
865 format!("{}{}", truncate_string(&tag.name, 25), color_indicator)
866 );
867
868 if let Some(description) = &tag.description {
869 println!(
870 "\x1b[36m│\x1b[0m \x1b[2;37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
871 truncate_string(description, 33)
872 );
873 }
874
875 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
876 }
877
878 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
879 println!(
880 "\x1b[36m│\x1b[0m \x1b[1;37mTotal:\x1b[0m {:<30} \x1b[36m│\x1b[0m",
881 format!("{} tags", tags.len())
882 );
883 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
884
885 Ok(())
886}
887
888async fn delete_tag(name: String) -> Result<()> {
889 let db_path = get_database_path()?;
891 let db = Database::new(&db_path)?;
892
893 if TagQueries::find_by_name(&db.connection, &name)?.is_none() {
895 println!("\x1b[31m✗ Tag '{}' not found\x1b[0m", name);
896 return Ok(());
897 }
898
899 let deleted = TagQueries::delete_by_name(&db.connection, &name)?;
901
902 if deleted {
903 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
904 println!("\x1b[36m│\x1b[0m \x1b[1;37mTag Deleted\x1b[0m \x1b[36m│\x1b[0m");
905 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
906 println!(
907 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
908 truncate_string(&name, 27)
909 );
910 println!(
911 "\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m"
912 );
913 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
914 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Tag deleted successfully\x1b[0m \x1b[36m│\x1b[0m");
915 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
916 } else {
917 println!("\x1b[31m✗ Failed to delete tag '{}'\x1b[0m", name);
918 }
919
920 Ok(())
921}
922
923async fn show_config() -> Result<()> {
925 let config = load_config()?;
926
927 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
928 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration\x1b[0m \x1b[36m│\x1b[0m");
929 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
930 println!(
931 "\x1b[36m│\x1b[0m idle_timeout_minutes: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
932 config.idle_timeout_minutes
933 );
934 println!(
935 "\x1b[36m│\x1b[0m auto_pause_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
936 config.auto_pause_enabled
937 );
938 println!(
939 "\x1b[36m│\x1b[0m default_context: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
940 config.default_context
941 );
942 println!(
943 "\x1b[36m│\x1b[0m max_session_hours: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
944 config.max_session_hours
945 );
946 println!(
947 "\x1b[36m│\x1b[0m backup_enabled: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
948 config.backup_enabled
949 );
950 println!(
951 "\x1b[36m│\x1b[0m log_level: \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
952 config.log_level
953 );
954
955 if !config.custom_settings.is_empty() {
956 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
957 println!("\x1b[36m│\x1b[0m \x1b[1;37mCustom Settings:\x1b[0m \x1b[36m│\x1b[0m");
958 for (key, value) in &config.custom_settings {
959 println!(
960 "\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
961 truncate_string(key, 20),
962 truncate_string(value, 16)
963 );
964 }
965 }
966
967 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
968
969 Ok(())
970}
971
972async fn get_config(key: String) -> Result<()> {
973 let config = load_config()?;
974
975 let value = match key.as_str() {
976 "idle_timeout_minutes" => Some(config.idle_timeout_minutes.to_string()),
977 "auto_pause_enabled" => Some(config.auto_pause_enabled.to_string()),
978 "default_context" => Some(config.default_context),
979 "max_session_hours" => Some(config.max_session_hours.to_string()),
980 "backup_enabled" => Some(config.backup_enabled.to_string()),
981 "log_level" => Some(config.log_level),
982 _ => config.custom_settings.get(&key).cloned(),
983 };
984
985 match value {
986 Some(val) => {
987 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
988 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Value\x1b[0m \x1b[36m│\x1b[0m");
989 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
990 println!(
991 "\x1b[36m│\x1b[0m {:<20} \x1b[33m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
992 truncate_string(&key, 20),
993 truncate_string(&val, 16)
994 );
995 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
996 }
997 None => {
998 println!("\x1b[31m✗ Configuration key not found:\x1b[0m {}", key);
999 }
1000 }
1001
1002 Ok(())
1003}
1004
1005async fn set_config(key: String, value: String) -> Result<()> {
1006 let mut config = load_config()?;
1007
1008 let display_value = value.clone(); match key.as_str() {
1011 "idle_timeout_minutes" => {
1012 config.idle_timeout_minutes = value.parse()?;
1013 }
1014 "auto_pause_enabled" => {
1015 config.auto_pause_enabled = value.parse()?;
1016 }
1017 "default_context" => {
1018 config.default_context = value;
1019 }
1020 "max_session_hours" => {
1021 config.max_session_hours = value.parse()?;
1022 }
1023 "backup_enabled" => {
1024 config.backup_enabled = value.parse()?;
1025 }
1026 "log_level" => {
1027 config.log_level = value;
1028 }
1029 _ => {
1030 config.set_custom(key.clone(), value);
1031 }
1032 }
1033
1034 config.validate()?;
1035 save_config(&config)?;
1036
1037 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1038 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Updated\x1b[0m \x1b[36m│\x1b[0m");
1039 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1040 println!(
1041 "\x1b[36m│\x1b[0m {:<20} \x1b[32m{:<16}\x1b[0m \x1b[36m│\x1b[0m",
1042 truncate_string(&key, 20),
1043 truncate_string(&display_value, 16)
1044 );
1045 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1046 println!(
1047 "\x1b[36m│\x1b[0m \x1b[32m✓ Configuration saved successfully\x1b[0m \x1b[36m│\x1b[0m"
1048 );
1049 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1050
1051 Ok(())
1052}
1053
1054async fn reset_config() -> Result<()> {
1055 let default_config = crate::models::Config::default();
1056 save_config(&default_config)?;
1057
1058 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1059 println!("\x1b[36m│\x1b[0m \x1b[1;37mConfiguration Reset\x1b[0m \x1b[36m│\x1b[0m");
1060 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1061 println!(
1062 "\x1b[36m│\x1b[0m \x1b[32m✓ Configuration reset to defaults\x1b[0m \x1b[36m│\x1b[0m"
1063 );
1064 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1065 println!(
1066 "\x1b[36m│\x1b[0m \x1b[37mView current config:\x1b[0m \x1b[36m│\x1b[0m"
1067 );
1068 println!(
1069 "\x1b[36m│\x1b[0m \x1b[96mtempo config show\x1b[0m \x1b[36m│\x1b[0m"
1070 );
1071 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1072
1073 Ok(())
1074}
1075
1076async fn list_sessions(limit: Option<usize>, project_filter: Option<String>) -> Result<()> {
1078 let db_path = get_database_path()?;
1080 let db = Database::new(&db_path)?;
1081
1082 let session_limit = limit.unwrap_or(10);
1083
1084 let project_id = if let Some(project_name) = &project_filter {
1086 match ProjectQueries::find_by_name(&db.connection, project_name)? {
1087 Some(project) => Some(project.id.unwrap()),
1088 None => {
1089 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1090 return Ok(());
1091 }
1092 }
1093 } else {
1094 None
1095 };
1096
1097 let sessions = SessionQueries::list_with_filter(
1098 &db.connection,
1099 project_id,
1100 None,
1101 None,
1102 Some(session_limit),
1103 )?;
1104
1105 if sessions.is_empty() {
1106 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1107 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Sessions\x1b[0m \x1b[36m│\x1b[0m");
1108 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1109 println!("\x1b[36m│\x1b[0m No sessions found. \x1b[36m│\x1b[0m");
1110 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1111 println!("\x1b[36m│\x1b[0m \x1b[37mStart a session:\x1b[0m \x1b[36m│\x1b[0m");
1112 println!("\x1b[36m│\x1b[0m \x1b[96mtempo session start\x1b[0m \x1b[36m│\x1b[0m");
1113 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1114 return Ok(());
1115 }
1116
1117 let filtered_sessions = if let Some(_project) = project_filter {
1119 sessions
1121 } else {
1122 sessions
1123 };
1124
1125 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1126 println!("\x1b[36m│\x1b[0m \x1b[1;37mRecent Sessions\x1b[0m \x1b[36m│\x1b[0m");
1127 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1128
1129 for session in &filtered_sessions {
1130 let status_icon = if session.end_time.is_some() {
1131 "✅"
1132 } else {
1133 "🔄"
1134 };
1135 let duration = if let Some(end) = session.end_time {
1136 (end - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1137 } else {
1138 (Utc::now() - session.start_time).num_seconds() - session.paused_duration.num_seconds()
1139 };
1140
1141 let context_color = match session.context {
1142 crate::models::SessionContext::Terminal => "\x1b[96m",
1143 crate::models::SessionContext::IDE => "\x1b[95m",
1144 crate::models::SessionContext::Linked => "\x1b[93m",
1145 crate::models::SessionContext::Manual => "\x1b[94m",
1146 };
1147
1148 println!(
1149 "\x1b[36m│\x1b[0m {} \x1b[1;37m{:<32}\x1b[0m \x1b[36m│\x1b[0m",
1150 status_icon,
1151 format!("Session {}", session.id.unwrap_or(0))
1152 );
1153 println!(
1154 "\x1b[36m│\x1b[0m Duration: \x1b[32m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1155 format_duration_clean(duration)
1156 );
1157 println!(
1158 "\x1b[36m│\x1b[0m Context: {}{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1159 context_color, session.context
1160 );
1161 println!(
1162 "\x1b[36m│\x1b[0m Started: \x1b[37m{:<24}\x1b[0m \x1b[36m│\x1b[0m",
1163 session.start_time.format("%Y-%m-%d %H:%M:%S")
1164 );
1165 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
1166 }
1167
1168 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1169 println!(
1170 "\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(
1179 id: i64,
1180 start: Option<String>,
1181 end: Option<String>,
1182 project: Option<String>,
1183 reason: Option<String>,
1184) -> Result<()> {
1185 let db_path = get_database_path()?;
1187 let db = Database::new(&db_path)?;
1188
1189 let session = SessionQueries::find_by_id(&db.connection, id)?;
1191 let session = match session {
1192 Some(s) => s,
1193 None => {
1194 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1195 return Ok(());
1196 }
1197 };
1198
1199 let original_start = session.start_time;
1200 let original_end = session.end_time;
1201
1202 let mut new_start = original_start;
1204 let mut new_end = original_end;
1205 let mut new_project_id = session.project_id;
1206
1207 if let Some(start_str) = &start {
1209 new_start = match chrono::DateTime::parse_from_rfc3339(start_str) {
1210 Ok(dt) => dt.with_timezone(&chrono::Utc),
1211 Err(_) => match chrono::NaiveDateTime::parse_from_str(start_str, "%Y-%m-%d %H:%M:%S") {
1212 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1213 Err(_) => {
1214 return Err(anyhow::anyhow!(
1215 "Invalid start time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"
1216 ))
1217 }
1218 },
1219 };
1220 }
1221
1222 if let Some(end_str) = &end {
1224 if end_str.to_lowercase() == "null" || end_str.to_lowercase() == "none" {
1225 new_end = None;
1226 } else {
1227 new_end = Some(match chrono::DateTime::parse_from_rfc3339(end_str) {
1228 Ok(dt) => dt.with_timezone(&chrono::Utc),
1229 Err(_) => {
1230 match chrono::NaiveDateTime::parse_from_str(end_str, "%Y-%m-%d %H:%M:%S") {
1231 Ok(dt) => chrono::Utc.from_utc_datetime(&dt),
1232 Err(_) => {
1233 return Err(anyhow::anyhow!(
1234 "Invalid end time format. Use RFC3339 or 'YYYY-MM-DD HH:MM:SS'"
1235 ))
1236 }
1237 }
1238 }
1239 });
1240 }
1241 }
1242
1243 if let Some(project_name) = &project {
1245 if let Some(proj) = ProjectQueries::find_by_name(&db.connection, project_name)? {
1246 new_project_id = proj.id.unwrap();
1247 } else {
1248 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1249 return Ok(());
1250 }
1251 }
1252
1253 if new_start >= new_end.unwrap_or(chrono::Utc::now()) {
1255 println!("\x1b[31m✗ Start time must be before end time\x1b[0m");
1256 return Ok(());
1257 }
1258
1259 SessionEditQueries::create_edit_record(
1261 &db.connection,
1262 id,
1263 original_start,
1264 original_end,
1265 new_start,
1266 new_end,
1267 reason.clone(),
1268 )?;
1269
1270 SessionQueries::update_session(
1272 &db.connection,
1273 id,
1274 if start.is_some() {
1275 Some(new_start)
1276 } else {
1277 None
1278 },
1279 if end.is_some() { Some(new_end) } else { None },
1280 if project.is_some() {
1281 Some(new_project_id)
1282 } else {
1283 None
1284 },
1285 None,
1286 )?;
1287
1288 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1289 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Updated\x1b[0m \x1b[36m│\x1b[0m");
1290 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1291 println!(
1292 "\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1293 id
1294 );
1295
1296 if start.is_some() {
1297 println!(
1298 "\x1b[36m│\x1b[0m Start: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1299 truncate_string(&new_start.format("%Y-%m-%d %H:%M:%S").to_string(), 27)
1300 );
1301 }
1302
1303 if end.is_some() {
1304 let end_str = if let Some(e) = new_end {
1305 e.format("%Y-%m-%d %H:%M:%S").to_string()
1306 } else {
1307 "Ongoing".to_string()
1308 };
1309 println!(
1310 "\x1b[36m│\x1b[0m End: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1311 truncate_string(&end_str, 27)
1312 );
1313 }
1314
1315 if let Some(r) = &reason {
1316 println!(
1317 "\x1b[36m│\x1b[0m Reason: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1318 truncate_string(r, 27)
1319 );
1320 }
1321
1322 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1323 println!(
1324 "\x1b[36m│\x1b[0m \x1b[32m✓ Session updated with audit trail\x1b[0m \x1b[36m│\x1b[0m"
1325 );
1326 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1327
1328 Ok(())
1329}
1330
1331async fn delete_session(id: i64, force: bool) -> Result<()> {
1332 let db_path = get_database_path()?;
1334 let db = Database::new(&db_path)?;
1335
1336 let session = SessionQueries::find_by_id(&db.connection, id)?;
1338 let session = match session {
1339 Some(s) => s,
1340 None => {
1341 println!("\x1b[31m✗ Session {} not found\x1b[0m", id);
1342 return Ok(());
1343 }
1344 };
1345
1346 if session.end_time.is_none() && !force {
1348 println!("\x1b[33m⚠ Cannot delete active session without --force flag\x1b[0m");
1349 println!(" Use: tempo session delete {} --force", id);
1350 return Ok(());
1351 }
1352
1353 SessionQueries::delete_session(&db.connection, id)?;
1355
1356 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1357 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Deleted\x1b[0m \x1b[36m│\x1b[0m");
1358 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1359 println!(
1360 "\x1b[36m│\x1b[0m Session: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1361 id
1362 );
1363 println!(
1364 "\x1b[36m│\x1b[0m Status: \x1b[32mDeleted\x1b[0m \x1b[36m│\x1b[0m"
1365 );
1366
1367 if session.end_time.is_none() {
1368 println!("\x1b[36m│\x1b[0m Type: \x1b[33mActive session (forced)\x1b[0m \x1b[36m│\x1b[0m");
1369 } else {
1370 println!("\x1b[36m│\x1b[0m Type: \x1b[37mCompleted session\x1b[0m \x1b[36m│\x1b[0m");
1371 }
1372
1373 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1374 println!(
1375 "\x1b[36m│\x1b[0m \x1b[32m✓ Session and audit trail removed\x1b[0m \x1b[36m│\x1b[0m"
1376 );
1377 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1378
1379 Ok(())
1380}
1381
1382async fn archive_project(project_name: String) -> Result<()> {
1384 let db_path = get_database_path()?;
1385 let db = Database::new(&db_path)?;
1386
1387 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1388 Some(p) => p,
1389 None => {
1390 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1391 return Ok(());
1392 }
1393 };
1394
1395 if project.is_archived {
1396 println!(
1397 "\x1b[33m⚠ Project '{}' is already archived\x1b[0m",
1398 project_name
1399 );
1400 return Ok(());
1401 }
1402
1403 let success = ProjectQueries::archive_project(&db.connection, project.id.unwrap())?;
1404
1405 if success {
1406 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1407 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Archived\x1b[0m \x1b[36m│\x1b[0m");
1408 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1409 println!(
1410 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1411 truncate_string(&project_name, 27)
1412 );
1413 println!(
1414 "\x1b[36m│\x1b[0m Status: \x1b[90mArchived\x1b[0m \x1b[36m│\x1b[0m"
1415 );
1416 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1417 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project archived successfully\x1b[0m \x1b[36m│\x1b[0m");
1418 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1419 } else {
1420 println!(
1421 "\x1b[31m✗ Failed to archive project '{}'\x1b[0m",
1422 project_name
1423 );
1424 }
1425
1426 Ok(())
1427}
1428
1429async fn unarchive_project(project_name: String) -> Result<()> {
1430 let db_path = get_database_path()?;
1431 let db = Database::new(&db_path)?;
1432
1433 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1434 Some(p) => p,
1435 None => {
1436 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1437 return Ok(());
1438 }
1439 };
1440
1441 if !project.is_archived {
1442 println!(
1443 "\x1b[33m⚠ Project '{}' is not archived\x1b[0m",
1444 project_name
1445 );
1446 return Ok(());
1447 }
1448
1449 let success = ProjectQueries::unarchive_project(&db.connection, project.id.unwrap())?;
1450
1451 if success {
1452 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1453 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Unarchived\x1b[0m \x1b[36m│\x1b[0m");
1454 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1455 println!(
1456 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1457 truncate_string(&project_name, 27)
1458 );
1459 println!(
1460 "\x1b[36m│\x1b[0m Status: \x1b[32mActive\x1b[0m \x1b[36m│\x1b[0m"
1461 );
1462 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1463 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Project unarchived successfully\x1b[0m \x1b[36m│\x1b[0m");
1464 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1465 } else {
1466 println!(
1467 "\x1b[31m✗ Failed to unarchive project '{}'\x1b[0m",
1468 project_name
1469 );
1470 }
1471
1472 Ok(())
1473}
1474
1475async fn update_project_path(project_name: String, new_path: PathBuf) -> Result<()> {
1476 let db_path = get_database_path()?;
1477 let db = Database::new(&db_path)?;
1478
1479 let project = match ProjectQueries::find_by_name(&db.connection, &project_name)? {
1480 Some(p) => p,
1481 None => {
1482 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", project_name);
1483 return Ok(());
1484 }
1485 };
1486
1487 let canonical_path = canonicalize_path(&new_path)?;
1488 let success =
1489 ProjectQueries::update_project_path(&db.connection, project.id.unwrap(), &canonical_path)?;
1490
1491 if success {
1492 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1493 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Path Updated\x1b[0m \x1b[36m│\x1b[0m");
1494 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1495 println!(
1496 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1497 truncate_string(&project_name, 27)
1498 );
1499 println!(
1500 "\x1b[36m│\x1b[0m Old Path: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1501 truncate_string(&project.path.to_string_lossy(), 27)
1502 );
1503 println!(
1504 "\x1b[36m│\x1b[0m New Path: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1505 truncate_string(&canonical_path.to_string_lossy(), 27)
1506 );
1507 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1508 println!("\x1b[36m│\x1b[0m \x1b[32m✓ Path updated successfully\x1b[0m \x1b[36m│\x1b[0m");
1509 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1510 } else {
1511 println!(
1512 "\x1b[31m✗ Failed to update path for project '{}'\x1b[0m",
1513 project_name
1514 );
1515 }
1516
1517 Ok(())
1518}
1519
1520async fn add_tag_to_project(project_name: String, tag_name: String) -> Result<()> {
1521 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1522 println!("Would add tag '{}' to project '{}'", tag_name, project_name);
1523 println!("This requires implementing project_tags table operations.");
1524 Ok(())
1525}
1526
1527async fn remove_tag_from_project(project_name: String, tag_name: String) -> Result<()> {
1528 println!("\x1b[33m⚠ Project-tag associations not yet implemented\x1b[0m");
1529 println!(
1530 "Would remove tag '{}' from project '{}'",
1531 tag_name, project_name
1532 );
1533 println!("This requires implementing project_tags table operations.");
1534 Ok(())
1535}
1536
1537#[allow(dead_code)]
1539async fn bulk_update_sessions_project(
1540 session_ids: Vec<i64>,
1541 new_project_name: String,
1542) -> Result<()> {
1543 let db_path = get_database_path()?;
1544 let db = Database::new(&db_path)?;
1545
1546 let project = match ProjectQueries::find_by_name(&db.connection, &new_project_name)? {
1548 Some(p) => p,
1549 None => {
1550 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", new_project_name);
1551 return Ok(());
1552 }
1553 };
1554
1555 let updated =
1556 SessionQueries::bulk_update_project(&db.connection, &session_ids, project.id.unwrap())?;
1557
1558 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1559 println!(
1560 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Update\x1b[0m \x1b[36m│\x1b[0m"
1561 );
1562 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1563 println!(
1564 "\x1b[36m│\x1b[0m Sessions: \x1b[1;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1565 updated
1566 );
1567 println!(
1568 "\x1b[36m│\x1b[0m Project: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
1569 truncate_string(&new_project_name, 27)
1570 );
1571 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1572 println!(
1573 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions updated\x1b[0m {:<12} \x1b[36m│\x1b[0m",
1574 updated, ""
1575 );
1576 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1577
1578 Ok(())
1579}
1580
1581#[allow(dead_code)]
1582async fn bulk_delete_sessions(session_ids: Vec<i64>) -> Result<()> {
1583 let db_path = get_database_path()?;
1584 let db = Database::new(&db_path)?;
1585
1586 let deleted = SessionQueries::bulk_delete(&db.connection, &session_ids)?;
1587
1588 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1589 println!(
1590 "\x1b[36m│\x1b[0m \x1b[1;37mBulk Session Delete\x1b[0m \x1b[36m│\x1b[0m"
1591 );
1592 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1593 println!(
1594 "\x1b[36m│\x1b[0m Requested: \x1b[1;37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1595 session_ids.len()
1596 );
1597 println!(
1598 "\x1b[36m│\x1b[0m Deleted: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
1599 deleted
1600 );
1601 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1602 println!(
1603 "\x1b[36m│\x1b[0m \x1b[32m✓ {} sessions deleted\x1b[0m {:<10} \x1b[36m│\x1b[0m",
1604 deleted, ""
1605 );
1606 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1607
1608 Ok(())
1609}
1610
1611async fn launch_dashboard() -> Result<()> {
1612 if !is_tty() {
1614 return show_dashboard_fallback().await;
1615 }
1616
1617 enable_raw_mode()
1619 .context("Failed to enable raw mode - terminal may not support interactive features")?;
1620 let mut stdout = io::stdout();
1621
1622 execute!(stdout, EnterAlternateScreen)
1623 .context("Failed to enter alternate screen - terminal may not support full-screen mode")?;
1624
1625 let backend = CrosstermBackend::new(stdout);
1626 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal backend")?;
1627
1628 terminal.clear().context("Failed to clear terminal")?;
1630
1631 let result = async {
1633 let mut dashboard = Dashboard::new().await?;
1634 dashboard.run(&mut terminal).await
1635 };
1636
1637 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1638
1639 let cleanup_result = cleanup_terminal(&mut terminal);
1641
1642 if let Err(e) = cleanup_result {
1644 eprintln!("Warning: Failed to restore terminal: {}", e);
1645 }
1646
1647 result
1648}
1649
1650fn is_tty() -> bool {
1651 use std::os::unix::io::AsRawFd;
1652 unsafe { libc::isatty(std::io::stdin().as_raw_fd()) == 1 }
1653}
1654
1655async fn show_dashboard_fallback() -> Result<()> {
1656 println!("📊 Tempo Dashboard (Text Mode)");
1657 println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
1658 println!();
1659
1660 if is_daemon_running() {
1662 println!("🟢 Daemon Status: Running");
1663 } else {
1664 println!("🔴 Daemon Status: Offline");
1665 println!(" Start with: tempo start");
1666 println!();
1667 return Ok(());
1668 }
1669
1670 let socket_path = get_socket_path()?;
1672 if let Ok(mut client) = IpcClient::connect(&socket_path).await {
1673 match client.send_message(&IpcMessage::GetActiveSession).await {
1674 Ok(IpcResponse::ActiveSession(Some(session))) => {
1675 println!("⏱️ Active Session:");
1676 println!(" Started: {}", session.start_time.format("%H:%M:%S"));
1677 println!(
1678 " Duration: {}",
1679 format_duration_simple(
1680 (chrono::Utc::now().timestamp() - session.start_time.timestamp())
1681 - session.paused_duration.num_seconds()
1682 )
1683 );
1684 println!(" Context: {}", session.context);
1685 println!();
1686
1687 match client
1689 .send_message(&IpcMessage::GetProject(session.project_id))
1690 .await
1691 {
1692 Ok(IpcResponse::Project(Some(project))) => {
1693 println!("📁 Current Project: {}", project.name);
1694 println!(" Path: {}", project.path.display());
1695 println!();
1696 }
1697 _ => {
1698 println!("📁 Project: Unknown");
1699 println!();
1700 }
1701 }
1702 }
1703 _ => {
1704 println!("⏸️ No active session");
1705 println!(" Start tracking with: tempo session start");
1706 println!();
1707 }
1708 }
1709
1710 let today = chrono::Local::now().date_naive();
1712 match client.send_message(&IpcMessage::GetDailyStats(today)).await {
1713 Ok(IpcResponse::DailyStats {
1714 sessions_count,
1715 total_seconds,
1716 avg_seconds,
1717 }) => {
1718 println!("📈 Today's Summary:");
1719 println!(" Sessions: {}", sessions_count);
1720 println!(" Total time: {}", format_duration_simple(total_seconds));
1721 if sessions_count > 0 {
1722 println!(
1723 " Average session: {}",
1724 format_duration_simple(avg_seconds)
1725 );
1726 }
1727 let progress = (total_seconds as f64 / (8.0 * 3600.0)) * 100.0;
1728 println!(" Daily goal (8h): {:.1}%", progress);
1729 println!();
1730 }
1731 _ => {
1732 println!("📈 Today's Summary: No data available");
1733 println!();
1734 }
1735 }
1736 } else {
1737 println!("❌ Unable to connect to daemon");
1738 println!(" Try: tempo restart");
1739 println!();
1740 }
1741
1742 println!("💡 For interactive dashboard, run in a terminal:");
1743 println!(" • Terminal.app, iTerm2, or other terminal emulators");
1744 println!(" • SSH sessions with TTY allocation (ssh -t)");
1745 println!(" • Interactive shell environments");
1746
1747 Ok(())
1748}
1749
1750fn format_duration_simple(seconds: i64) -> String {
1751 let hours = seconds / 3600;
1752 let minutes = (seconds % 3600) / 60;
1753 let secs = seconds % 60;
1754
1755 if hours > 0 {
1756 format!("{}h {}m {}s", hours, minutes, secs)
1757 } else if minutes > 0 {
1758 format!("{}m {}s", minutes, secs)
1759 } else {
1760 format!("{}s", secs)
1761 }
1762}
1763
1764fn cleanup_terminal<B>(terminal: &mut Terminal<B>) -> Result<()>
1765where
1766 B: ratatui::backend::Backend + std::io::Write,
1767{
1768 disable_raw_mode().context("Failed to disable raw mode")?;
1770 execute!(terminal.backend_mut(), LeaveAlternateScreen)
1771 .context("Failed to leave alternate screen")?;
1772 terminal.show_cursor().context("Failed to show cursor")?;
1773 Ok(())
1774}
1775
1776async fn launch_timer() -> Result<()> {
1777 if !is_tty() {
1779 return Err(anyhow::anyhow!(
1780 "Interactive timer requires an interactive terminal (TTY).\n\
1781 \n\
1782 This command needs to run in a proper terminal environment.\n\
1783 Try running this command directly in your terminal application."
1784 ));
1785 }
1786
1787 enable_raw_mode().context("Failed to enable raw mode")?;
1789 let mut stdout = io::stdout();
1790 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
1791 let backend = CrosstermBackend::new(stdout);
1792 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
1793 terminal.clear().context("Failed to clear terminal")?;
1794
1795 let result = async {
1797 let mut timer = InteractiveTimer::new().await?;
1798 timer.run(&mut terminal).await
1799 };
1800
1801 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1802
1803 let cleanup_result = cleanup_terminal(&mut terminal);
1805 if let Err(e) = cleanup_result {
1806 eprintln!("Warning: Failed to restore terminal: {}", e);
1807 }
1808
1809 result
1810}
1811
1812async fn merge_sessions(
1813 session_ids_str: String,
1814 project_name: Option<String>,
1815 notes: Option<String>,
1816) -> Result<()> {
1817 let session_ids: Result<Vec<i64>, _> = session_ids_str
1819 .split(',')
1820 .map(|s| s.trim().parse::<i64>())
1821 .collect();
1822
1823 let session_ids = session_ids.map_err(|_| {
1824 anyhow::anyhow!("Invalid session IDs format. Use comma-separated numbers like '1,2,3'")
1825 })?;
1826
1827 if session_ids.len() < 2 {
1828 return Err(anyhow::anyhow!(
1829 "At least 2 sessions are required for merging"
1830 ));
1831 }
1832
1833 let mut target_project_id = None;
1835 if let Some(project) = project_name {
1836 let db_path = get_database_path()?;
1837 let db = Database::new(&db_path)?;
1838
1839 if let Ok(project_id) = project.parse::<i64>() {
1841 if ProjectQueries::find_by_id(&db.connection, project_id)?.is_some() {
1842 target_project_id = Some(project_id);
1843 }
1844 } else if let Some(proj) = ProjectQueries::find_by_name(&db.connection, &project)? {
1845 target_project_id = proj.id;
1846 }
1847
1848 if target_project_id.is_none() {
1849 return Err(anyhow::anyhow!("Project '{}' not found", project));
1850 }
1851 }
1852
1853 let db_path = get_database_path()?;
1855 let db = Database::new(&db_path)?;
1856
1857 let merged_id =
1858 SessionQueries::merge_sessions(&db.connection, &session_ids, target_project_id, notes)?;
1859
1860 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1861 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Merge Complete\x1b[0m \x1b[36m│\x1b[0m");
1862 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1863 println!(
1864 "\x1b[36m│\x1b[0m Merged sessions: \x1b[33m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1865 session_ids
1866 .iter()
1867 .map(|id| id.to_string())
1868 .collect::<Vec<_>>()
1869 .join(", ")
1870 );
1871 println!(
1872 "\x1b[36m│\x1b[0m New session ID: \x1b[32m{:<22}\x1b[0m \x1b[36m│\x1b[0m",
1873 merged_id
1874 );
1875 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1876 println!(
1877 "\x1b[36m│\x1b[0m \x1b[32m✓ Sessions successfully merged\x1b[0m \x1b[36m│\x1b[0m"
1878 );
1879 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1880
1881 Ok(())
1882}
1883
1884async fn split_session(
1885 session_id: i64,
1886 split_times_str: String,
1887 notes: Option<String>,
1888) -> Result<()> {
1889 let split_time_strings: Vec<&str> = split_times_str.split(',').map(|s| s.trim()).collect();
1891 let mut split_times = Vec::new();
1892
1893 for time_str in split_time_strings {
1894 let datetime = if time_str.contains(':') {
1896 let today = chrono::Local::now().date_naive();
1898 let time = chrono::NaiveTime::parse_from_str(time_str, "%H:%M")
1899 .or_else(|_| chrono::NaiveTime::parse_from_str(time_str, "%H:%M:%S"))
1900 .map_err(|_| {
1901 anyhow::anyhow!("Invalid time format '{}'. Use HH:MM or HH:MM:SS", time_str)
1902 })?;
1903 today.and_time(time).and_utc()
1904 } else {
1905 chrono::DateTime::parse_from_rfc3339(time_str)
1907 .map_err(|_| {
1908 anyhow::anyhow!(
1909 "Invalid datetime format '{}'. Use HH:MM or RFC3339 format",
1910 time_str
1911 )
1912 })?
1913 .to_utc()
1914 };
1915
1916 split_times.push(datetime);
1917 }
1918
1919 if split_times.is_empty() {
1920 return Err(anyhow::anyhow!("No valid split times provided"));
1921 }
1922
1923 let notes_list = notes.map(|n| {
1925 n.split(',')
1926 .map(|s| s.trim().to_string())
1927 .collect::<Vec<String>>()
1928 });
1929
1930 let db_path = get_database_path()?;
1932 let db = Database::new(&db_path)?;
1933
1934 let new_session_ids =
1935 SessionQueries::split_session(&db.connection, session_id, &split_times, notes_list)?;
1936
1937 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
1938 println!("\x1b[36m│\x1b[0m \x1b[1;37mSession Split Complete\x1b[0m \x1b[36m│\x1b[0m");
1939 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1940 println!(
1941 "\x1b[36m│\x1b[0m Original session: \x1b[33m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1942 session_id
1943 );
1944 println!(
1945 "\x1b[36m│\x1b[0m Split points: \x1b[90m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1946 split_times.len()
1947 );
1948 println!(
1949 "\x1b[36m│\x1b[0m New sessions: \x1b[32m{:<20}\x1b[0m \x1b[36m│\x1b[0m",
1950 new_session_ids
1951 .iter()
1952 .map(|id| id.to_string())
1953 .collect::<Vec<_>>()
1954 .join(", ")
1955 );
1956 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
1957 println!(
1958 "\x1b[36m│\x1b[0m \x1b[32m✓ Session successfully split\x1b[0m \x1b[36m│\x1b[0m"
1959 );
1960 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
1961
1962 Ok(())
1963}
1964
1965async fn launch_history() -> Result<()> {
1966 if !is_tty() {
1968 return Err(anyhow::anyhow!(
1969 "Session history browser requires an interactive terminal (TTY).\n\
1970 \n\
1971 This command needs to run in a proper terminal environment.\n\
1972 Try running this command directly in your terminal application."
1973 ));
1974 }
1975
1976 enable_raw_mode().context("Failed to enable raw mode")?;
1978 let mut stdout = io::stdout();
1979 execute!(stdout, EnterAlternateScreen).context("Failed to enter alternate screen")?;
1980 let backend = CrosstermBackend::new(stdout);
1981 let mut terminal = Terminal::new(backend).context("Failed to initialize terminal")?;
1982 terminal.clear().context("Failed to clear terminal")?;
1983
1984 let result = async {
1985 let mut browser = SessionHistoryBrowser::new().await?;
1986 browser.run(&mut terminal).await
1987 };
1988
1989 let result = tokio::task::block_in_place(|| Handle::current().block_on(result));
1990
1991 let cleanup_result = cleanup_terminal(&mut terminal);
1993 if let Err(e) = cleanup_result {
1994 eprintln!("Warning: Failed to restore terminal: {}", e);
1995 }
1996
1997 result
1998}
1999
2000async fn handle_goal_action(action: GoalAction) -> Result<()> {
2001 match action {
2002 GoalAction::Create {
2003 name,
2004 target_hours,
2005 project,
2006 description,
2007 start_date,
2008 end_date,
2009 } => {
2010 create_goal(
2011 name,
2012 target_hours,
2013 project,
2014 description,
2015 start_date,
2016 end_date,
2017 )
2018 .await
2019 }
2020 GoalAction::List { project } => list_goals(project).await,
2021 GoalAction::Update { id, hours } => update_goal_progress(id, hours).await,
2022 }
2023}
2024
2025async fn create_goal(
2026 name: String,
2027 target_hours: f64,
2028 project: Option<String>,
2029 description: Option<String>,
2030 start_date: Option<String>,
2031 end_date: Option<String>,
2032) -> Result<()> {
2033 let db_path = get_database_path()?;
2034 let db = Database::new(&db_path)?;
2035
2036 let project_id = if let Some(proj_name) = project {
2037 match ProjectQueries::find_by_name(&db.connection, &proj_name)? {
2038 Some(p) => p.id,
2039 None => {
2040 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2041 return Ok(());
2042 }
2043 }
2044 } else {
2045 None
2046 };
2047
2048 let start = start_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2049 let end = end_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2050
2051 let mut goal = Goal::new(name.clone(), target_hours);
2052 if let Some(pid) = project_id {
2053 goal = goal.with_project(pid);
2054 }
2055 if let Some(desc) = description {
2056 goal = goal.with_description(desc);
2057 }
2058 goal = goal.with_dates(start, end);
2059
2060 goal.validate()?;
2061 let goal_id = GoalQueries::create(&db.connection, &goal)?;
2062
2063 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2064 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoal Created\x1b[0m \x1b[36m│\x1b[0m");
2065 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2066 println!(
2067 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2068 truncate_string(&name, 27)
2069 );
2070 println!(
2071 "\x1b[36m│\x1b[0m Target: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2072 format!("{} hours", target_hours)
2073 );
2074 println!(
2075 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2076 goal_id
2077 );
2078 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2079 println!(
2080 "\x1b[36m│\x1b[0m \x1b[32m✓ Goal created successfully\x1b[0m \x1b[36m│\x1b[0m"
2081 );
2082 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2083
2084 Ok(())
2085}
2086
2087async fn list_goals(project: Option<String>) -> Result<()> {
2088 let db_path = get_database_path()?;
2089 let db = Database::new(&db_path)?;
2090
2091 let project_id = if let Some(proj_name) = &project {
2092 match ProjectQueries::find_by_name(&db.connection, proj_name)? {
2093 Some(p) => p.id,
2094 None => {
2095 println!("\x1b[31m✗ Project '{}' not found\x1b[0m", proj_name);
2096 return Ok(());
2097 }
2098 }
2099 } else {
2100 None
2101 };
2102
2103 let goals = GoalQueries::list_by_project(&db.connection, project_id)?;
2104
2105 if goals.is_empty() {
2106 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2107 println!("\x1b[36m│\x1b[0m \x1b[1;37mNo Goals\x1b[0m \x1b[36m│\x1b[0m");
2108 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2109 return Ok(());
2110 }
2111
2112 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2113 println!("\x1b[36m│\x1b[0m \x1b[1;37mGoals\x1b[0m \x1b[36m│\x1b[0m");
2114 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2115
2116 for goal in &goals {
2117 let progress_pct = goal.progress_percentage();
2118 println!(
2119 "\x1b[36m│\x1b[0m 🎯 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2120 truncate_string(&goal.name, 25)
2121 );
2122 println!("\x1b[36m│\x1b[0m Progress: \x1b[32m{:.1}%\x1b[0m ({:.1}h / {:.1}h) \x1b[36m│\x1b[0m",
2123 progress_pct, goal.current_progress, goal.target_hours);
2124 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2125 }
2126
2127 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2128 Ok(())
2129}
2130
2131async fn update_goal_progress(id: i64, hours: f64) -> Result<()> {
2132 let db_path = get_database_path()?;
2133 let db = Database::new(&db_path)?;
2134
2135 GoalQueries::update_progress(&db.connection, id, hours)?;
2136 println!(
2137 "\x1b[32m✓ Updated goal {} progress by {} hours\x1b[0m",
2138 id, hours
2139 );
2140 Ok(())
2141}
2142
2143async fn show_insights(period: Option<String>, project: Option<String>) -> Result<()> {
2144 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2145 println!("\x1b[36m│\x1b[0m \x1b[1;37mProductivity Insights\x1b[0m \x1b[36m│\x1b[0m");
2146 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2147 println!(
2148 "\x1b[36m│\x1b[0m Period: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2149 period.as_deref().unwrap_or("all")
2150 );
2151 if let Some(proj) = project {
2152 println!(
2153 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2154 truncate_string(&proj, 27)
2155 );
2156 }
2157 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2158 println!(
2159 "\x1b[36m│\x1b[0m \x1b[33m⚠ Insights calculation in progress...\x1b[0m \x1b[36m│\x1b[0m"
2160 );
2161 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2162 Ok(())
2163}
2164
2165async fn show_summary(period: String, from: Option<String>) -> Result<()> {
2166 let db_path = get_database_path()?;
2167 let db = Database::new(&db_path)?;
2168
2169 let start_date = if let Some(from_str) = from {
2170 chrono::NaiveDate::parse_from_str(&from_str, "%Y-%m-%d")?
2171 } else {
2172 match period.as_str() {
2173 "week" => chrono::Local::now().date_naive() - chrono::Duration::days(7),
2174 "month" => chrono::Local::now().date_naive() - chrono::Duration::days(30),
2175 _ => chrono::Local::now().date_naive(),
2176 }
2177 };
2178
2179 let insight_data = match period.as_str() {
2180 "week" => InsightQueries::calculate_weekly_summary(&db.connection, start_date)?,
2181 "month" => InsightQueries::calculate_monthly_summary(&db.connection, start_date)?,
2182 _ => return Err(anyhow::anyhow!("Invalid period. Use 'week' or 'month'")),
2183 };
2184
2185 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2186 println!(
2187 "\x1b[36m│\x1b[0m \x1b[1;37m{} Summary\x1b[0m \x1b[36m│\x1b[0m",
2188 period
2189 );
2190 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2191 println!(
2192 "\x1b[36m│\x1b[0m Total Hours: \x1b[32m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2193 format!("{:.1}h", insight_data.total_hours)
2194 );
2195 println!(
2196 "\x1b[36m│\x1b[0m Sessions: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2197 insight_data.sessions_count
2198 );
2199 println!(
2200 "\x1b[36m│\x1b[0m Avg Session: \x1b[33m{:<23}\x1b[0m \x1b[36m│\x1b[0m",
2201 format!("{:.1}h", insight_data.avg_session_duration)
2202 );
2203 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2204 Ok(())
2205}
2206
2207async fn compare_projects(
2208 projects: String,
2209 _from: Option<String>,
2210 _to: Option<String>,
2211) -> Result<()> {
2212 let _project_names: Vec<&str> = projects.split(',').map(|s| s.trim()).collect();
2213
2214 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2215 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Comparison\x1b[0m \x1b[36m│\x1b[0m");
2216 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2217 println!(
2218 "\x1b[36m│\x1b[0m Projects: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2219 truncate_string(&projects, 27)
2220 );
2221 println!(
2222 "\x1b[36m│\x1b[0m \x1b[33m⚠ Comparison feature in development\x1b[0m \x1b[36m│\x1b[0m"
2223 );
2224 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2225 Ok(())
2226}
2227
2228async fn handle_estimate_action(action: EstimateAction) -> Result<()> {
2229 match action {
2230 EstimateAction::Create {
2231 project,
2232 task,
2233 hours,
2234 due_date,
2235 } => create_estimate(project, task, hours, due_date).await,
2236 EstimateAction::Record { id, hours } => record_actual_time(id, hours).await,
2237 EstimateAction::List { project } => list_estimates(project).await,
2238 }
2239}
2240
2241async fn create_estimate(
2242 project: String,
2243 task: String,
2244 hours: f64,
2245 due_date: Option<String>,
2246) -> Result<()> {
2247 let db_path = get_database_path()?;
2248 let db = Database::new(&db_path)?;
2249
2250 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2251 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2252
2253 let due = due_date.and_then(|d| chrono::NaiveDate::parse_from_str(&d, "%Y-%m-%d").ok());
2254
2255 let mut estimate = TimeEstimate::new(project_obj.id.unwrap(), task.clone(), hours);
2256 estimate.due_date = due;
2257
2258 let estimate_id = TimeEstimateQueries::create(&db.connection, &estimate)?;
2259
2260 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2261 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimate Created\x1b[0m \x1b[36m│\x1b[0m");
2262 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2263 println!(
2264 "\x1b[36m│\x1b[0m Task: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2265 truncate_string(&task, 27)
2266 );
2267 println!(
2268 "\x1b[36m│\x1b[0m Estimate: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2269 format!("{} hours", hours)
2270 );
2271 println!(
2272 "\x1b[36m│\x1b[0m ID: \x1b[90m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2273 estimate_id
2274 );
2275 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2276 Ok(())
2277}
2278
2279async fn record_actual_time(id: i64, hours: f64) -> Result<()> {
2280 let db_path = get_database_path()?;
2281 let db = Database::new(&db_path)?;
2282
2283 TimeEstimateQueries::record_actual(&db.connection, id, hours)?;
2284 println!(
2285 "\x1b[32m✓ Recorded {} hours for estimate {}\x1b[0m",
2286 hours, id
2287 );
2288 Ok(())
2289}
2290
2291async fn list_estimates(project: String) -> Result<()> {
2292 let db_path = get_database_path()?;
2293 let db = Database::new(&db_path)?;
2294
2295 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2296 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2297
2298 let estimates = TimeEstimateQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2299
2300 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2301 println!("\x1b[36m│\x1b[0m \x1b[1;37mTime Estimates\x1b[0m \x1b[36m│\x1b[0m");
2302 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2303
2304 for est in &estimates {
2305 let variance = est.variance();
2306 let variance_str = if let Some(v) = variance {
2307 if v > 0.0 {
2308 format!("\x1b[31m+{:.1}h over\x1b[0m", v)
2309 } else {
2310 format!("\x1b[32m{:.1}h under\x1b[0m", v.abs())
2311 }
2312 } else {
2313 "N/A".to_string()
2314 };
2315
2316 println!(
2317 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2318 truncate_string(&est.task_name, 25)
2319 );
2320 let actual_str = est
2321 .actual_hours
2322 .map(|h| format!("{:.1}h", h))
2323 .unwrap_or_else(|| "N/A".to_string());
2324 println!(
2325 "\x1b[36m│\x1b[0m Est: {}h | Actual: {} | {} \x1b[36m│\x1b[0m",
2326 est.estimated_hours, actual_str, variance_str
2327 );
2328 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2329 }
2330
2331 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2332 Ok(())
2333}
2334
2335async fn handle_branch_action(action: BranchAction) -> Result<()> {
2336 match action {
2337 BranchAction::List { project } => list_branches(project).await,
2338 BranchAction::Stats { project, branch } => show_branch_stats(project, branch).await,
2339 }
2340}
2341
2342async fn list_branches(project: String) -> Result<()> {
2343 let db_path = get_database_path()?;
2344 let db = Database::new(&db_path)?;
2345
2346 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2347 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2348
2349 let branches = GitBranchQueries::list_by_project(&db.connection, project_obj.id.unwrap())?;
2350
2351 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2352 println!("\x1b[36m│\x1b[0m \x1b[1;37mGit Branches\x1b[0m \x1b[36m│\x1b[0m");
2353 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2354
2355 for branch in &branches {
2356 println!(
2357 "\x1b[36m│\x1b[0m 🌿 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2358 truncate_string(&branch.branch_name, 25)
2359 );
2360 println!(
2361 "\x1b[36m│\x1b[0m Time: \x1b[32m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2362 format!("{:.1}h", branch.total_hours())
2363 );
2364 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2365 }
2366
2367 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2368 Ok(())
2369}
2370
2371async fn show_branch_stats(project: String, branch: Option<String>) -> Result<()> {
2372 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2373 println!("\x1b[36m│\x1b[0m \x1b[1;37mBranch Statistics\x1b[0m \x1b[36m│\x1b[0m");
2374 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2375 println!(
2376 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2377 truncate_string(&project, 27)
2378 );
2379 if let Some(b) = branch {
2380 println!(
2381 "\x1b[36m│\x1b[0m Branch: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2382 truncate_string(&b, 27)
2383 );
2384 }
2385 println!(
2386 "\x1b[36m│\x1b[0m \x1b[33m⚠ Branch stats in development\x1b[0m \x1b[36m│\x1b[0m"
2387 );
2388 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2389 Ok(())
2390}
2391
2392async fn handle_template_action(action: TemplateAction) -> Result<()> {
2394 match action {
2395 TemplateAction::Create {
2396 name,
2397 description,
2398 tags,
2399 workspace_path,
2400 } => create_template(name, description, tags, workspace_path).await,
2401 TemplateAction::List => list_templates().await,
2402 TemplateAction::Delete { template } => delete_template(template).await,
2403 TemplateAction::Use {
2404 template,
2405 project_name,
2406 path,
2407 } => use_template(template, project_name, path).await,
2408 }
2409}
2410
2411async fn create_template(
2412 name: String,
2413 description: Option<String>,
2414 tags: Option<String>,
2415 workspace_path: Option<PathBuf>,
2416) -> Result<()> {
2417 let db_path = get_database_path()?;
2418 let db = Database::new(&db_path)?;
2419
2420 let default_tags = tags
2421 .map(|t| t.split(',').map(|s| s.trim().to_string()).collect())
2422 .unwrap_or_default();
2423
2424 let mut template = ProjectTemplate::new(name.clone()).with_tags(default_tags);
2425
2426 let desc_clone = description.clone();
2427 if let Some(desc) = description {
2428 template = template.with_description(desc);
2429 }
2430 if let Some(path) = workspace_path {
2431 template = template.with_workspace_path(path);
2432 }
2433
2434 let _template_id = TemplateQueries::create(&db.connection, &template)?;
2435
2436 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2437 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplate Created\x1b[0m \x1b[36m│\x1b[0m");
2438 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2439 println!(
2440 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2441 truncate_string(&name, 27)
2442 );
2443 if let Some(desc) = &desc_clone {
2444 println!(
2445 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2446 truncate_string(desc, 27)
2447 );
2448 }
2449 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2450 Ok(())
2451}
2452
2453async fn list_templates() -> Result<()> {
2454 let db_path = get_database_path()?;
2455 let db = Database::new(&db_path)?;
2456
2457 let templates = TemplateQueries::list_all(&db.connection)?;
2458
2459 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2460 println!("\x1b[36m│\x1b[0m \x1b[1;37mTemplates\x1b[0m \x1b[36m│\x1b[0m");
2461 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2462
2463 if templates.is_empty() {
2464 println!("\x1b[36m│\x1b[0m No templates found. \x1b[36m│\x1b[0m");
2465 } else {
2466 for template in &templates {
2467 println!(
2468 "\x1b[36m│\x1b[0m 📋 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2469 truncate_string(&template.name, 25)
2470 );
2471 if let Some(desc) = &template.description {
2472 println!(
2473 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2474 truncate_string(desc, 27)
2475 );
2476 }
2477 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2478 }
2479 }
2480
2481 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2482 Ok(())
2483}
2484
2485async fn delete_template(_template: String) -> Result<()> {
2486 println!("\x1b[33m⚠ Template deletion not yet implemented\x1b[0m");
2487 Ok(())
2488}
2489
2490async fn use_template(template: String, project_name: String, path: Option<PathBuf>) -> Result<()> {
2491 let db_path = get_database_path()?;
2492 let db = Database::new(&db_path)?;
2493
2494 let templates = TemplateQueries::list_all(&db.connection)?;
2495 let selected_template = templates
2496 .iter()
2497 .find(|t| t.name == template || t.id.map(|id| id.to_string()) == Some(template.clone()))
2498 .ok_or_else(|| anyhow::anyhow!("Template '{}' not found", template))?;
2499
2500 let project_path = path.unwrap_or_else(|| env::current_dir().unwrap());
2502 let canonical_path = canonicalize_path(&project_path)?;
2503
2504 if ProjectQueries::find_by_path(&db.connection, &canonical_path)?.is_some() {
2506 return Err(anyhow::anyhow!("Project already exists at this path"));
2507 }
2508
2509 let git_hash = if is_git_repository(&canonical_path) {
2510 get_git_hash(&canonical_path)
2511 } else {
2512 None
2513 };
2514
2515 let template_desc = selected_template.description.clone();
2516 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2517 .with_git_hash(git_hash)
2518 .with_description(template_desc);
2519
2520 let project_id = ProjectQueries::create(&db.connection, &project)?;
2521 project.id = Some(project_id);
2522
2523 for goal_def in &selected_template.default_goals {
2528 let mut goal =
2529 Goal::new(goal_def.name.clone(), goal_def.target_hours).with_project(project_id);
2530 if let Some(desc) = &goal_def.description {
2531 goal = goal.with_description(desc.clone());
2532 }
2533 GoalQueries::create(&db.connection, &goal)?;
2534 }
2535
2536 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2537 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Created from Template\x1b[0m \x1b[36m│\x1b[0m");
2538 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2539 println!(
2540 "\x1b[36m│\x1b[0m Template: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2541 truncate_string(&selected_template.name, 27)
2542 );
2543 println!(
2544 "\x1b[36m│\x1b[0m Project: \x1b[33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2545 truncate_string(&project_name, 27)
2546 );
2547 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2548 Ok(())
2549}
2550
2551async fn handle_workspace_action(action: WorkspaceAction) -> Result<()> {
2553 match action {
2554 WorkspaceAction::Create {
2555 name,
2556 description,
2557 path,
2558 } => create_workspace(name, description, path).await,
2559 WorkspaceAction::List => list_workspaces().await,
2560 WorkspaceAction::AddProject { workspace, project } => {
2561 add_project_to_workspace(workspace, project).await
2562 }
2563 WorkspaceAction::RemoveProject { workspace, project } => {
2564 remove_project_from_workspace(workspace, project).await
2565 }
2566 WorkspaceAction::Projects { workspace } => list_workspace_projects(workspace).await,
2567 WorkspaceAction::Delete { workspace } => delete_workspace(workspace).await,
2568 }
2569}
2570
2571async fn create_workspace(
2572 name: String,
2573 description: Option<String>,
2574 path: Option<PathBuf>,
2575) -> Result<()> {
2576 let db_path = get_database_path()?;
2577 let db = Database::new(&db_path)?;
2578
2579 let mut workspace = Workspace::new(name.clone());
2580 let desc_clone = description.clone();
2581 if let Some(desc) = description {
2582 workspace = workspace.with_description(desc);
2583 }
2584 if let Some(p) = path {
2585 workspace = workspace.with_path(p);
2586 }
2587
2588 let _workspace_id = WorkspaceQueries::create(&db.connection, &workspace)?;
2589
2590 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2591 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Created\x1b[0m \x1b[36m│\x1b[0m");
2592 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2593 println!(
2594 "\x1b[36m│\x1b[0m Name: \x1b[1;33m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2595 truncate_string(&name, 27)
2596 );
2597 if let Some(desc) = &desc_clone {
2598 println!(
2599 "\x1b[36m│\x1b[0m Desc: \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2600 truncate_string(desc, 27)
2601 );
2602 }
2603 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2604 Ok(())
2605}
2606
2607async fn list_workspaces() -> Result<()> {
2608 let db_path = get_database_path()?;
2609 let db = Database::new(&db_path)?;
2610
2611 let workspaces = WorkspaceQueries::list_all(&db.connection)?;
2612
2613 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2614 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspaces\x1b[0m \x1b[36m│\x1b[0m");
2615 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2616
2617 if workspaces.is_empty() {
2618 println!("\x1b[36m│\x1b[0m No workspaces found. \x1b[36m│\x1b[0m");
2619 } else {
2620 for workspace in &workspaces {
2621 println!(
2622 "\x1b[36m│\x1b[0m 📁 \x1b[1;33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2623 truncate_string(&workspace.name, 25)
2624 );
2625 if let Some(desc) = &workspace.description {
2626 println!(
2627 "\x1b[36m│\x1b[0m \x1b[2;37m{:<27}\x1b[0m \x1b[36m│\x1b[0m",
2628 truncate_string(desc, 27)
2629 );
2630 }
2631 println!("\x1b[36m│\x1b[0m \x1b[36m│\x1b[0m");
2632 }
2633 }
2634
2635 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2636 Ok(())
2637}
2638
2639async fn add_project_to_workspace(workspace: String, project: String) -> Result<()> {
2640 let db_path = get_database_path()?;
2641 let db = Database::new(&db_path)?;
2642
2643 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2645 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2646
2647 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2649 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2650
2651 let workspace_id = workspace_obj
2652 .id
2653 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2654 let project_id = project_obj
2655 .id
2656 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2657
2658 if WorkspaceQueries::add_project(&db.connection, workspace_id, project_id)? {
2659 println!(
2660 "\x1b[32m✓\x1b[0m Added project '\x1b[33m{}\x1b[0m' to workspace '\x1b[33m{}\x1b[0m'",
2661 project, workspace
2662 );
2663 } else {
2664 println!("\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' is already in workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2665 }
2666
2667 Ok(())
2668}
2669
2670async fn remove_project_from_workspace(workspace: String, project: String) -> Result<()> {
2671 let db_path = get_database_path()?;
2672 let db = Database::new(&db_path)?;
2673
2674 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2676 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2677
2678 let project_obj = ProjectQueries::find_by_name(&db.connection, &project)?
2680 .ok_or_else(|| anyhow::anyhow!("Project '{}' not found", project))?;
2681
2682 let workspace_id = workspace_obj
2683 .id
2684 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2685 let project_id = project_obj
2686 .id
2687 .ok_or_else(|| anyhow::anyhow!("Project ID is missing"))?;
2688
2689 if WorkspaceQueries::remove_project(&db.connection, workspace_id, project_id)? {
2690 println!("\x1b[32m✓\x1b[0m Removed project '\x1b[33m{}\x1b[0m' from workspace '\x1b[33m{}\x1b[0m'", project, workspace);
2691 } else {
2692 println!(
2693 "\x1b[33m⚠\x1b[0m Project '\x1b[33m{}\x1b[0m' was not in workspace '\x1b[33m{}\x1b[0m'",
2694 project, workspace
2695 );
2696 }
2697
2698 Ok(())
2699}
2700
2701async fn list_workspace_projects(workspace: String) -> Result<()> {
2702 let db_path = get_database_path()?;
2703 let db = Database::new(&db_path)?;
2704
2705 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2707 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2708
2709 let workspace_id = workspace_obj
2710 .id
2711 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2712 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2713
2714 if projects.is_empty() {
2715 println!(
2716 "\x1b[33m⚠\x1b[0m No projects found in workspace '\x1b[33m{}\x1b[0m'",
2717 workspace
2718 );
2719 return Ok(());
2720 }
2721
2722 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2723 println!("\x1b[36m│\x1b[0m \x1b[1;37mWorkspace Projects\x1b[0m \x1b[36m│\x1b[0m");
2724 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2725 println!(
2726 "\x1b[36m│\x1b[0m Workspace: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2727 truncate_string(&workspace, 25)
2728 );
2729 println!(
2730 "\x1b[36m│\x1b[0m Projects: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2731 format!("{} projects", projects.len())
2732 );
2733 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2734
2735 for project in &projects {
2736 let status_indicator = if !project.is_archived {
2737 "\x1b[32m●\x1b[0m"
2738 } else {
2739 "\x1b[31m○\x1b[0m"
2740 };
2741 println!(
2742 "\x1b[36m│\x1b[0m {} \x1b[37m{:<33}\x1b[0m \x1b[36m│\x1b[0m",
2743 status_indicator,
2744 truncate_string(&project.name, 33)
2745 );
2746 }
2747
2748 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2749 Ok(())
2750}
2751
2752async fn delete_workspace(workspace: String) -> Result<()> {
2753 let db_path = get_database_path()?;
2754 let db = Database::new(&db_path)?;
2755
2756 let workspace_obj = WorkspaceQueries::find_by_name(&db.connection, &workspace)?
2758 .ok_or_else(|| anyhow::anyhow!("Workspace '{}' not found", workspace))?;
2759
2760 let workspace_id = workspace_obj
2761 .id
2762 .ok_or_else(|| anyhow::anyhow!("Workspace ID is missing"))?;
2763
2764 let projects = WorkspaceQueries::list_projects(&db.connection, workspace_id)?;
2766 if !projects.is_empty() {
2767 println!("\x1b[33m⚠\x1b[0m Cannot delete workspace '\x1b[33m{}\x1b[0m' - it contains {} project(s). Remove projects first.",
2768 workspace, projects.len());
2769 return Ok(());
2770 }
2771
2772 if WorkspaceQueries::delete(&db.connection, workspace_id)? {
2773 println!(
2774 "\x1b[32m✓\x1b[0m Deleted workspace '\x1b[33m{}\x1b[0m'",
2775 workspace
2776 );
2777 } else {
2778 println!(
2779 "\x1b[31m✗\x1b[0m Failed to delete workspace '\x1b[33m{}\x1b[0m'",
2780 workspace
2781 );
2782 }
2783
2784 Ok(())
2785}
2786
2787async fn handle_calendar_action(action: CalendarAction) -> Result<()> {
2789 match action {
2790 CalendarAction::Add {
2791 name,
2792 start,
2793 end,
2794 event_type,
2795 project,
2796 description,
2797 } => add_calendar_event(name, start, end, event_type, project, description).await,
2798 CalendarAction::List { from, to, project } => list_calendar_events(from, to, project).await,
2799 CalendarAction::Delete { id } => delete_calendar_event(id).await,
2800 }
2801}
2802
2803async fn add_calendar_event(
2804 _name: String,
2805 _start: String,
2806 _end: Option<String>,
2807 _event_type: Option<String>,
2808 _project: Option<String>,
2809 _description: Option<String>,
2810) -> Result<()> {
2811 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2812 Ok(())
2813}
2814
2815async fn list_calendar_events(
2816 _from: Option<String>,
2817 _to: Option<String>,
2818 _project: Option<String>,
2819) -> Result<()> {
2820 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2821 Ok(())
2822}
2823
2824async fn delete_calendar_event(_id: i64) -> Result<()> {
2825 println!("\x1b[33m⚠ Calendar integration in development\x1b[0m");
2826 Ok(())
2827}
2828
2829async fn handle_issue_action(action: IssueAction) -> Result<()> {
2831 match action {
2832 IssueAction::Sync {
2833 project,
2834 tracker_type,
2835 } => sync_issues(project, tracker_type).await,
2836 IssueAction::List { project, status } => list_issues(project, status).await,
2837 IssueAction::Link {
2838 session_id,
2839 issue_id,
2840 } => link_session_to_issue(session_id, issue_id).await,
2841 }
2842}
2843
2844async fn sync_issues(_project: String, _tracker_type: Option<String>) -> Result<()> {
2845 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2846 Ok(())
2847}
2848
2849async fn list_issues(_project: String, _status: Option<String>) -> Result<()> {
2850 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2851 Ok(())
2852}
2853
2854async fn link_session_to_issue(_session_id: i64, _issue_id: String) -> Result<()> {
2855 println!("\x1b[33m⚠ Issue tracker integration in development\x1b[0m");
2856 Ok(())
2857}
2858
2859async fn handle_client_action(action: ClientAction) -> Result<()> {
2861 match action {
2862 ClientAction::Generate {
2863 client,
2864 from,
2865 to,
2866 projects,
2867 format,
2868 } => generate_client_report(client, from, to, projects, format).await,
2869 ClientAction::List { client } => list_client_reports(client).await,
2870 ClientAction::View { id } => view_client_report(id).await,
2871 }
2872}
2873
2874async fn generate_client_report(
2875 _client: String,
2876 _from: String,
2877 _to: String,
2878 _projects: Option<String>,
2879 _format: Option<String>,
2880) -> Result<()> {
2881 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2882 Ok(())
2883}
2884
2885async fn list_client_reports(_client: Option<String>) -> Result<()> {
2886 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2887 Ok(())
2888}
2889
2890async fn view_client_report(_id: i64) -> Result<()> {
2891 println!("\x1b[33m⚠ Client reporting in development\x1b[0m");
2892 Ok(())
2893}
2894
2895#[allow(dead_code)]
2896fn should_quit(event: crossterm::event::Event) -> bool {
2897 match event {
2898 crossterm::event::Event::Key(key) if key.kind == crossterm::event::KeyEventKind::Press => {
2899 matches!(
2900 key.code,
2901 crossterm::event::KeyCode::Char('q') | crossterm::event::KeyCode::Esc
2902 )
2903 }
2904 _ => false,
2905 }
2906}
2907
2908async fn init_project_with_db(
2910 name: Option<String>,
2911 canonical_path: Option<PathBuf>,
2912 description: Option<String>,
2913 conn: &rusqlite::Connection,
2914) -> Result<()> {
2915 let canonical_path =
2916 canonical_path.ok_or_else(|| anyhow::anyhow!("Canonical path required"))?;
2917 let project_name = name.unwrap_or_else(|| detect_project_name(&canonical_path));
2918
2919 if let Some(existing) = ProjectQueries::find_by_path(conn, &canonical_path)? {
2921 println!(
2922 "\x1b[33m⚠ Project already exists:\x1b[0m {}",
2923 existing.name
2924 );
2925 return Ok(());
2926 }
2927
2928 let git_hash = if is_git_repository(&canonical_path) {
2930 get_git_hash(&canonical_path)
2931 } else {
2932 None
2933 };
2934
2935 let mut project = Project::new(project_name.clone(), canonical_path.clone())
2937 .with_git_hash(git_hash.clone())
2938 .with_description(description.clone());
2939
2940 let project_id = ProjectQueries::create(conn, &project)?;
2942 project.id = Some(project_id);
2943
2944 let marker_path = canonical_path.join(".tempo");
2946 if !marker_path.exists() {
2947 std::fs::write(
2948 &marker_path,
2949 format!("# Tempo time tracking project\nname: {}\n", project_name),
2950 )?;
2951 }
2952
2953 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2954 println!("\x1b[36m│\x1b[0m \x1b[1;37mProject Initialized\x1b[0m \x1b[36m│\x1b[0m");
2955 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
2956 println!(
2957 "\x1b[36m│\x1b[0m Name: \x1b[33m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2958 truncate_string(&project_name, 25)
2959 );
2960 println!(
2961 "\x1b[36m│\x1b[0m Path: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2962 truncate_string(&canonical_path.display().to_string(), 25)
2963 );
2964
2965 if let Some(desc) = &description {
2966 println!(
2967 "\x1b[36m│\x1b[0m Description: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2968 truncate_string(desc, 25)
2969 );
2970 }
2971
2972 if is_git_repository(&canonical_path) {
2973 println!(
2974 "\x1b[36m│\x1b[0m Git: \x1b[32m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2975 "Repository detected"
2976 );
2977 if let Some(hash) = &git_hash {
2978 println!(
2979 "\x1b[36m│\x1b[0m Git Hash: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2980 truncate_string(hash, 25)
2981 );
2982 }
2983 }
2984
2985 println!(
2986 "\x1b[36m│\x1b[0m ID: \x1b[37m{:<25}\x1b[0m \x1b[36m│\x1b[0m",
2987 project_id
2988 );
2989 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
2990
2991 Ok(())
2992}
2993
2994async fn show_pool_stats() -> Result<()> {
2996 match get_pool_stats() {
2997 Ok(stats) => {
2998 println!("\x1b[36m┌─────────────────────────────────────────┐\x1b[0m");
2999 println!("\x1b[36m│\x1b[0m \x1b[1;37mDatabase Pool Statistics\x1b[0m \x1b[36m│\x1b[0m");
3000 println!("\x1b[36m├─────────────────────────────────────────┤\x1b[0m");
3001 println!(
3002 "\x1b[36m│\x1b[0m Total Created: \x1b[32m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3003 stats.total_connections_created
3004 );
3005 println!(
3006 "\x1b[36m│\x1b[0m Active: \x1b[33m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3007 stats.active_connections
3008 );
3009 println!(
3010 "\x1b[36m│\x1b[0m Available in Pool:\x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3011 stats.connections_in_pool
3012 );
3013 println!(
3014 "\x1b[36m│\x1b[0m Total Requests: \x1b[37m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3015 stats.connection_requests
3016 );
3017 println!(
3018 "\x1b[36m│\x1b[0m Timeouts: \x1b[31m{:<19}\x1b[0m \x1b[36m│\x1b[0m",
3019 stats.connection_timeouts
3020 );
3021 println!("\x1b[36m└─────────────────────────────────────────┘\x1b[0m");
3022 }
3023 Err(_) => {
3024 println!("\x1b[33m⚠ Database pool not initialized or not available\x1b[0m");
3025 println!(" Using direct database connections as fallback");
3026 }
3027 }
3028 Ok(())
3029}
3030
3031#[derive(Deserialize)]
3032#[allow(dead_code)]
3033struct GitHubRelease {
3034 tag_name: String,
3035 name: String,
3036 body: String,
3037 published_at: String,
3038 prerelease: bool,
3039}
3040
3041async fn handle_update(check: bool, force: bool, verbose: bool) -> Result<()> {
3042 let current_version = env!("CARGO_PKG_VERSION");
3043
3044 if verbose {
3045 println!("🔍 Current version: v{}", current_version);
3046 println!("📡 Checking for updates...");
3047 } else {
3048 println!("🔍 Checking for updates...");
3049 }
3050
3051 let client = reqwest::Client::new();
3053 let response = client
3054 .get("https://api.github.com/repos/own-path/vibe/releases/latest")
3055 .header("User-Agent", format!("tempo-cli/{}", current_version))
3056 .send()
3057 .await
3058 .context("Failed to fetch release information")?;
3059
3060 if !response.status().is_success() {
3061 return Err(anyhow::anyhow!(
3062 "Failed to fetch release information: HTTP {}",
3063 response.status()
3064 ));
3065 }
3066
3067 let release: GitHubRelease = response
3068 .json()
3069 .await
3070 .context("Failed to parse release information")?;
3071
3072 let latest_version = release.tag_name.trim_start_matches('v');
3073
3074 if verbose {
3075 println!("📦 Latest version: v{}", latest_version);
3076 println!("📅 Released: {}", release.published_at);
3077 }
3078
3079 let current_semver =
3081 semver::Version::parse(current_version).context("Failed to parse current version")?;
3082 let latest_semver =
3083 semver::Version::parse(latest_version).context("Failed to parse latest version")?;
3084
3085 if current_semver >= latest_semver && !force {
3086 println!(
3087 "✅ You're already running the latest version (v{})",
3088 current_version
3089 );
3090 if check {
3091 return Ok(());
3092 }
3093
3094 if !force {
3095 println!("💡 Use --force to reinstall the current version");
3096 return Ok(());
3097 }
3098 }
3099
3100 if check {
3101 if current_semver < latest_semver {
3102 println!(
3103 "📦 Update available: v{} → v{}",
3104 current_version, latest_version
3105 );
3106 println!("🔗 Run `tempo update` to install the latest version");
3107
3108 if verbose && !release.body.is_empty() {
3109 println!("\n📝 Release Notes:");
3110 println!("{}", release.body);
3111 }
3112 }
3113 return Ok(());
3114 }
3115
3116 if current_semver < latest_semver || force {
3117 println!(
3118 "⬇️ Updating tempo from v{} to v{}",
3119 current_version, latest_version
3120 );
3121
3122 if verbose {
3123 println!("🔧 Installing via cargo...");
3124 }
3125
3126 let mut cmd = Command::new("cargo");
3128 cmd.args(["install", "tempo-cli", "--force"]);
3129
3130 if verbose {
3131 cmd.stdout(Stdio::inherit()).stderr(Stdio::inherit());
3132 } else {
3133 cmd.stdout(Stdio::null()).stderr(Stdio::null());
3134 }
3135
3136 let status = cmd
3137 .status()
3138 .context("Failed to run cargo install command")?;
3139
3140 if status.success() {
3141 println!("✅ Successfully updated tempo to v{}", latest_version);
3142 println!("🎉 You can now use the latest features!");
3143
3144 if !release.body.is_empty() && verbose {
3145 println!("\n📝 What's new in v{}:", latest_version);
3146 println!("{}", release.body);
3147 }
3148 } else {
3149 return Err(anyhow::anyhow!(
3150 "Failed to install update. Try running manually: cargo install tempo-cli --force"
3151 ));
3152 }
3153 }
3154
3155 Ok(())
3156}