"""Read recent editor activity from Zed's SQLite database.""" import sqlite3 from collections import defaultdict from dataclasses import dataclass from pathlib import Path from typing import Dict, List, Optional, Tuple @dataclass class EditorSession: path: str mtime: int language: Optional[str] @dataclass class ProjectSummary: project: str files: List[Tuple[str, int]] # (relative_path, mtime) languages: set def _find_zed_db() -> Optional[Path]: """Locate the most recent Zed stable database.""" zed_db_dir = Path.home() / ".local" / "share" / "zed" / "db" if not zed_db_dir.exists(): return None candidates = [] for sub in zed_db_dir.iterdir(): db_file = sub / "db.sqlite" if db_file.exists(): # Prefer stable, then preview, then anything else priority = 0 if "stable" in sub.name: priority = 2 elif "preview" in sub.name: priority = 1 candidates.append((priority, db_file.stat().st_mtime, db_file)) if not candidates: return None candidates.sort(key=lambda x: (x[0], x[1]), reverse=True) return candidates[0][2] def _resolve_project(file_path: Path, code_dirs: List[Path]) -> Optional[str]: """Return the project name for a file path based on code directories.""" for code_dir in code_dirs: try: rel = file_path.relative_to(code_dir) parts = rel.parts if parts: return parts[0] except ValueError: continue # Fallback: use parent directory name return file_path.parent.name if file_path.parent != Path("/") else None def fetch_zed_activity( start_ts: int, end_ts: int, code_dirs: Optional[List[Path]] = None, ) -> List[ProjectSummary]: """Fetch editor activity from Zed for the given time range.""" db_path = _find_zed_db() if not db_path: return [] if code_dirs is None: code_dirs = [Path.home() / "Code"] code_dirs = [d.expanduser().resolve() for d in code_dirs if d.exists()] sessions: List[EditorSession] = [] try: conn = sqlite3.connect(str(db_path)) cursor = conn.cursor() cursor.execute( """ SELECT path, mtime_seconds, language FROM editors WHERE mtime_seconds >= ? AND mtime_seconds < ? ORDER BY mtime_seconds DESC """, (start_ts, end_ts), ) for row in cursor.fetchall(): path_blob, mtime, language = row if not path_blob: continue try: path_str = ( path_blob.decode("utf-8") if isinstance(path_blob, bytes) else str(path_blob) ) except (UnicodeDecodeError, AttributeError): continue sessions.append( EditorSession(path=path_str, mtime=mtime, language=language) ) except sqlite3.Error as e: print(f"Warning: could not read Zed database: {e}") return [] # Group by project by_project: Dict[str, List[EditorSession]] = defaultdict(list) for s in sessions: fp = Path(s.path) project = _resolve_project(fp, code_dirs) if not project: project = fp.parent.name by_project[project].append(s) summaries = [] for project, items in by_project.items(): files = [] seen = set() languages = set() for item in items: if item.language: languages.add(item.language) rel = item.path # Store unique files with their latest mtime key = rel if key not in seen: seen.add(key) files.append((rel, item.mtime)) # Sort by mtime descending files.sort(key=lambda x: x[1], reverse=True) summaries.append( ProjectSummary(project=project, files=files, languages=languages) ) # Sort projects by number of files, then by latest mtime summaries.sort(key=lambda s: (-len(s.files), -s.files[0][1] if s.files else 0)) return summaries