const vscode = require("vscode"); const fs = require("fs"); const path = require("path"); const HEARTBEAT_MS = 30000; const HEAT_COLORS = ["charts.blue", "charts.green", "charts.yellow", "charts.orange", "charts.red"]; let store = null; let statusBarItem = null; let decorationProvider = null; let trackingEnabled = true; let lastActivityAt = Date.now(); let currentProjectKey = null; let currentFileKey = null; let heartbeatTimer = null; function loadStore(dataFile) { try { const raw = fs.readFileSync(dataFile, "utf8"); return JSON.parse(raw); } catch (err) { return { projects: {} }; } } function saveStore(dataFile) { fs.mkdirSync(path.dirname(dataFile), { recursive: true }); fs.writeFileSync(dataFile, JSON.stringify(store, null, 2), "utf8"); } function getProjectKey(uri) { const folder = vscode.workspace.getWorkspaceFolder(uri); if (!folder) { return null; } return folder.uri.fsPath; } function ensureProject(projectKey, label) { if (!store.projects[projectKey]) { store.projects[projectKey] = { label, totalSeconds: 0, files: {} }; } return store.projects[projectKey]; } function addSeconds(seconds) { if (!currentProjectKey || !currentFileKey) { return; } const folder = vscode.workspace.workspaceFolders?.find((f) => f.uri.fsPath === currentProjectKey); const label = folder ? folder.name : path.basename(currentProjectKey); const project = ensureProject(currentProjectKey, label); project.totalSeconds += seconds; project.files[currentFileKey] = (project.files[currentFileKey] || 0) + seconds; project.label = label; } function idleThresholdMs() { const minutes = vscode.workspace.getConfiguration("codeheat").get("idleThresholdMinutes", 3); return minutes * 60 * 1000; } function heartbeat(dataFile) { if (!trackingEnabled) { return; } const idleFor = Date.now() - lastActivityAt; if (idleFor >= idleThresholdMs()) { return; } addSeconds(HEARTBEAT_MS / 1000); saveStore(dataFile); refreshStatusBar(); decorationProvider?.refresh(); } function refreshStatusBar() { if (!statusBarItem) { return; } if (!trackingEnabled) { statusBarItem.text = "$(circle-slash) CodeHeat paused"; statusBarItem.tooltip = "CodeHeat: tracking paused, click to resume"; return; } const project = currentProjectKey ? store.projects[currentProjectKey] : null; const seconds = project ? project.totalSeconds : 0; statusBarItem.text = `$(clock) ${formatDuration(seconds)}`; statusBarItem.tooltip = project ? `CodeHeat: ${formatDuration(seconds)} tracked in ${project.label}` : "CodeHeat: no workspace folder open"; } function formatDuration(totalSeconds) { const hours = Math.floor(totalSeconds / 3600); const minutes = Math.floor((totalSeconds % 3600) / 60); if (hours === 0 && minutes === 0) { return "<1m"; } if (hours === 0) { return `${minutes}m`; } return `${hours}h ${minutes}m`; } function onEditorActivity(uri) { lastActivityAt = Date.now(); if (!uri) { return; } const projectKey = getProjectKey(uri); if (!projectKey) { currentProjectKey = null; currentFileKey = null; refreshStatusBar(); return; } currentProjectKey = projectKey; currentFileKey = path.relative(projectKey, uri.fsPath); refreshStatusBar(); } class HeatDecorationProvider { constructor() { this._emitter = new vscode.EventEmitter(); this.onDidChangeFileDecorations = this._emitter.event; } refresh() { this._emitter.fire(undefined); } provideFileDecoration(uri) { if (!vscode.workspace.getConfiguration("codeheat").get("heatmapEnabled", true)) { return undefined; } const projectKey = getProjectKey(uri); if (!projectKey) { return undefined; } const project = store.projects[projectKey]; if (!project) { return undefined; } const relPath = path.relative(projectKey, uri.fsPath); const seconds = project.files[relPath]; if (!seconds) { return undefined; } const values = Object.values(project.files).filter((value) => value > 0); const maxSeconds = Math.max(...values); const bucket = maxSeconds > 0 ? Math.min(HEAT_COLORS.length - 1, Math.floor((seconds / maxSeconds) * HEAT_COLORS.length)) : 0; return { badge: String(bucket + 1), color: new vscode.ThemeColor(HEAT_COLORS[bucket]), tooltip: `CodeHeat: ${formatDuration(seconds)} tracked in this file`, propagate: false }; } } function buildSummaryHtml(projectKey) { const project = store.projects[projectKey]; if (!project) { return "
No time tracked in this project yet.
"; } const files = Object.entries(project.files).sort((a, b) => b[1] - a[1]).slice(0, 25); const maxSeconds = files.length > 0 ? files[0][1] : 1; const rows = files .map(([relPath, seconds]) => { const widthPercent = Math.max(2, Math.round((seconds / maxSeconds) * 100)); const safePath = relPath.replace(/&/g, "&").replace(/Total tracked: ${formatDuration(project.totalSeconds)}