tempo_cli/cli/
commands.rs

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