4b7ddcb1c1
Integrates with Zed editor history and Git commits to track daily coding activity. Writes idempotent `## Coding Work` sections into Obsidian daily notes with project breakdowns and commit stats.
141 lines
4.2 KiB
Python
141 lines
4.2 KiB
Python
"""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
|