"""Time-based analysis command.""" from collections import defaultdict from datetime import datetime, timedelta from typing import Optional import click from rich.console import Console from rich.table import Table from shellhist.core import HistoryLoader @click.command("analyze-time") @click.option( "--history", "-H", type=str, help="Path to history file", ) @click.option( "--daily/--no-daily", default=False, help="Show commands run at similar times daily", ) @click.option( "--weekly/--no-weekly", default=False, help="Show commands run at similar times weekly", ) @click.option( "--time-range", "-t", type=str, default="7d", help="Time range filter (e.g., '7d', '24h', '30d')", ) @click.option( "--shell", "-s", type=click.Choice(["bash", "zsh"]), help="Shell type for parsing", ) @click.pass_context def analyze_time_command( ctx: click.Context, history: Optional[str], daily: bool, weekly: bool, time_range: str, shell: Optional[str], ) -> None: """Analyze time-based patterns in your shell history. Examples: \b shellhist analyze-time --daily shellhist analyze-time --weekly --time-range 30d shellhist analyze-time --daily --time-range 7d """ console = Console() try: loader = HistoryLoader(history_path=history) store = loader.load() if not store.entries: console.print("[yellow]No entries found in history.[/yellow]") return entries_with_time = [ e for e in store.entries if e.timestamp is not None ] if not entries_with_time: console.print( "[yellow]No timestamps found in history. " "Ensure your history includes timestamps.[/yellow]" ) return cutoff = _parse_time_range(time_range) recent_entries = [ e for e in entries_with_time if e.timestamp >= cutoff ] if daily: _analyze_daily_patterns(console, recent_entries) elif weekly: _analyze_weekly_patterns(console, recent_entries) else: _analyze_hourly_distribution(console, recent_entries) except FileNotFoundError as e: console.print(f"[red]Error: {e}[/red]") ctx.exit(1) except Exception as e: console.print(f"[red]Error analyzing time patterns: {e}[/red]") ctx.exit(1) def _parse_time_range(time_range: str) -> datetime: """Parse time range string like '7d', '24h', '30d'.""" value = int(time_range[:-1]) unit = time_range[-1].lower() now = datetime.now() if unit == 'h': return now - timedelta(hours=value) elif unit == 'd': return now - timedelta(days=value) elif unit == 'w': return now - timedelta(weeks=value) else: return now - timedelta(days=7) def _analyze_hourly_distribution(console: Console, entries: list) -> None: """Analyze command distribution by hour of day.""" hourly = defaultdict(list) for entry in entries: hour = entry.timestamp.hour hourly[hour].append(entry.command) console.print("\n[bold cyan]Hourly Command Distribution[/bold cyan]") table = Table(show_header=True, header_style="bold magenta") table.add_column("Hour", width=10) table.add_column("Commands", width=10) table.add_column("Top Commands", width=60) for hour in range(24): cmds = hourly.get(hour, []) if cmds: top = defaultdict(int) for cmd in cmds: top[cmd] += 1 top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:2] top_str = ", ".join(f"{c}({n})" for c, n in top_sorted) else: top_str = "-" hour_label = f"{hour:02d}:00" table.add_row(hour_label, str(len(cmds)), top_str[:58]) console.print(table) def _analyze_daily_patterns(console: Console, entries: list) -> None: """Analyze commands run at similar times daily.""" daily_patterns = defaultdict(list) for entry in entries: hour = entry.timestamp.hour key = (entry.timestamp.weekday(), hour) daily_patterns[key].append(entry.command) console.print("\n[bold cyan]Daily Time Patterns[/bold cyan]") day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] patterns_found = [] for (weekday, hour), cmds in daily_patterns.items(): if len(cmds) >= 2: top = defaultdict(int) for cmd in cmds: top[cmd] += 1 top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:3] patterns_found.append({ "day": day_names[weekday], "hour": hour, "count": len(cmds), "top": top_sorted, }) if not patterns_found: console.print("[yellow]No significant daily patterns found.[/yellow]") return patterns_found.sort(key=lambda x: x["count"], reverse=True) table = Table(show_header=True, header_style="bold magenta") table.add_column("Day", width=8) table.add_column("Hour", width=8) table.add_column("Times", width=8) table.add_column("Common Commands", width=60) for p in patterns_found[:20]: top_str = ", ".join(f"{c}({n})" for c, n in p["top"]) table.add_row( p["day"], f"{p['hour']:02d}:00", str(p["count"]), top_str[:58], ) console.print(table) def _analyze_weekly_patterns(console: Console, entries: list) -> None: """Analyze commands run at similar times weekly.""" weekly_patterns = defaultdict(list) for entry in entries: key = ( entry.timestamp.isocalendar().year, entry.timestamp.isocalendar().week, entry.timestamp.weekday(), entry.timestamp.hour, ) weekly_patterns[key].append(entry.command) console.print("\n[bold cyan]Weekly Time Patterns[/bold cyan]") day_names = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] patterns_found = [] for key, cmds in weekly_patterns.items(): year, week, weekday, hour = key if len(cmds) >= 2: top = defaultdict(int) for cmd in cmds: top[cmd] += 1 top_sorted = sorted(top.items(), key=lambda x: x[1], reverse=True)[:3] patterns_found.append({ "week": week, "day": day_names[weekday], "hour": hour, "count": len(cmds), "top": top_sorted, }) if not patterns_found: console.print("[yellow]No significant weekly patterns found.[/yellow]") return patterns_found.sort(key=lambda x: x["count"], reverse=True) table = Table(show_header=True, header_style="bold magenta") table.add_column("Week", width=8) table.add_column("Day", width=8) table.add_column("Hour", width=8) table.add_column("Times", width=8) table.add_column("Common Commands", width=50) for p in patterns_found[:20]: top_str = ", ".join(f"{c}({n})" for c, n in p["top"]) table.add_row( str(p["week"]), p["day"], f"{p['hour']:02d}:00", str(p["count"]), top_str[:48], ) console.print(table)