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