"""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, HistoryEntry @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 is not None and 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[HistoryEntry]) -> None: """Analyze command distribution by hour of day.""" hourly: dict[int, list[str]] = defaultdict(list) for entry in entries: assert entry.timestamp is not None 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: dict[str, int] = 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[HistoryEntry]) -> None: """Analyze commands run at similar times daily.""" daily_patterns: dict[tuple[int, int], list[str]] = defaultdict(list) for entry in entries: assert entry.timestamp is not None 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"] from typing import TypedDict class DailyPattern(TypedDict): day: str hour: int count: int top: list[tuple[str, int]] patterns_found: list[DailyPattern] = [] for (weekday, hour), cmds in daily_patterns.items(): if len(cmds) >= 2: top: dict[str, int] = 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[HistoryEntry]) -> None: """Analyze commands run at similar times weekly.""" weekly_patterns: dict[tuple[int, int, int, int], list[str]] = defaultdict(list) for entry in entries: assert entry.timestamp is not None 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"] from typing import TypedDict class WeeklyPattern(TypedDict): week: int day: str hour: int count: int top: list[tuple[str, int]] patterns_found: list[WeeklyPattern] = [] for key, cmds in weekly_patterns.items(): year, week, weekday, hour = key if len(cmds) >= 2: top: dict[str, int] = 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)