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