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