Skip to main content

rec/session/
show.rs

1//! Session show command implementation.
2//!
3//! Displays session details including header info and numbered command list,
4//! with optional grep filtering and JSON output.
5
6use crate::cli::Output;
7use crate::error::{RecError, Result};
8use crate::models::Session;
9
10/// Display session details with optional grep filtering and JSON output.
11///
12/// # Normal mode
13/// Prints session header info (name, ID, date, shell, OS, hostname, etc.)
14/// followed by a numbered command list. If `grep` is provided, only commands
15/// matching the regex pattern are shown.
16///
17/// # JSON mode
18/// Serializes the entire session to pretty-printed JSON.
19///
20/// # Errors
21/// Returns an error if the grep pattern is an invalid regex.
22pub fn show_session(
23    session: &Session,
24    grep: Option<&str>,
25    json: bool,
26    output: &Output,
27) -> Result<()> {
28    if json {
29        let json_str = serde_json::to_string_pretty(session)
30            .map_err(|e| RecError::InvalidSession(format!("Failed to serialize session: {e}")))?;
31        println!("{json_str}");
32        return Ok(());
33    }
34
35    // Compile grep regex if provided
36    let re = match grep {
37        Some(pattern) => {
38            let compiled = regex::Regex::new(pattern).map_err(|e| {
39                RecError::InvalidSession(format!("Invalid grep pattern '{pattern}': {e}"))
40            })?;
41            Some(compiled)
42        }
43        None => None,
44    };
45
46    // Print session header block
47    println!();
48    output.success(&format!("Session: {}", session.name()));
49    println!();
50    println!("  ID:        {}", session.id());
51
52    // Format date from started_at timestamp
53    if let Some(dt) = chrono::DateTime::from_timestamp(
54        session.header.started_at as i64,
55        ((session.header.started_at.fract()) * 1_000_000_000.0) as u32,
56    ) {
57        let local: chrono::DateTime<chrono::Local> = dt.into();
58        println!("  Date:      {}", local.format("%Y-%m-%d %H:%M"));
59    }
60
61    println!("  Shell:     {}", session.header.shell);
62    println!("  OS:        {}", session.header.os);
63    println!("  Hostname:  {}", session.header.hostname);
64
65    // Working directory from env
66    if let Some(pwd) = session.header.env.get("PWD") {
67        println!("  Directory: {pwd}");
68    }
69
70    // Duration (if footer exists)
71    if let Some(ref footer) = session.footer {
72        let duration_secs = footer.ended_at - session.header.started_at;
73        println!("  Duration:  {}", format_duration(duration_secs));
74        println!("  Commands:  {}", footer.command_count);
75    } else {
76        println!(
77            "  Commands:  {} (session still recording)",
78            session.commands.len()
79        );
80    }
81
82    // Tags
83    if !session.header.tags.is_empty() {
84        println!("  Tags:      {}", session.header.tags.join(", "));
85    }
86
87    // Separator
88    println!();
89    println!("  {}", "-".repeat(60));
90    println!();
91
92    // Print commands
93    if session.commands.is_empty() {
94        output.info("  No commands recorded in this session.");
95        return Ok(());
96    }
97
98    let mut shown_count = 0;
99    for cmd in &session.commands {
100        // Apply grep filter
101        if let Some(ref re) = re {
102            if !re.is_match(&cmd.command) {
103                continue;
104            }
105        }
106
107        shown_count += 1;
108        let index = cmd.index + 1; // 1-based display
109        let cwd_str = cmd.cwd.display().to_string();
110
111        // Exit code with color hint
112        let exit_str = match cmd.exit_code {
113            Some(0) => output.style_success("exit:0"),
114            Some(code) => output.style_error(&format!("exit:{code}")),
115            None => "running".to_string(),
116        };
117
118        // Duration
119        let dur_str = match cmd.duration_ms {
120            Some(ms) if ms >= 1000 => format!("{:.1}s", ms as f64 / 1000.0),
121            Some(ms) => format!("{ms}ms"),
122            None => String::new(),
123        };
124
125        println!(
126            "  {:>3}. {}  [{}]  {}  {}",
127            index, cmd.command, cwd_str, exit_str, dur_str
128        );
129    }
130
131    if re.is_some() {
132        println!();
133        output.info(&format!(
134            "  Showing {} of {} commands (filtered by grep)",
135            shown_count,
136            session.commands.len()
137        ));
138    }
139
140    println!();
141    Ok(())
142}
143
144/// Format a duration in seconds as a human-readable string.
145fn format_duration(seconds: f64) -> String {
146    let total_secs = seconds as u64;
147    let hours = total_secs / 3600;
148    let minutes = (total_secs % 3600) / 60;
149    let secs = total_secs % 60;
150
151    if hours > 0 {
152        format!("{hours}h {minutes}m {secs}s")
153    } else if minutes > 0 {
154        format!("{minutes}m {secs}s")
155    } else {
156        format!("{secs}s")
157    }
158}