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