import discord from "discord-rpc"; import vscode from "vscode"; import fs from "fs/promises"; import path from "path"; type WakatimeTotal = { decimal: string; // 1.23 digital: string; // 1:23 seconds: number; // 123.456 text: string; // 1 hr 4 mins } const DISCORD_CLIENT_ID = "1323609388442849280", DISCORD_RPC = new discord.Client({ transport: "ipc" }), APP_CONFIG = vscode.workspace.getConfiguration("trackpad"), APP_CACHE_LOC: Record = {}, DEFAULT_WAKATIME_DATA: WakatimeTotal = { "decimal": "0", "digital": "0:00", "seconds": 0.00, "text": "0s", } discord.register(DISCORD_CLIENT_ID) /** Calculate the Lines of Code for a Given Directory */ async function directoryCountLOC( rootDirectory: string, allowedExtensions: Set, ignoredDirectories: Set, ): Promise { // Scan Directory Recursively let totalCount = 0 async function checkDirectory(directory: string) { const entries = await fs.readdir(directory, { withFileTypes: true }) for await (const file of entries) { const filepath = path.join(directory, file.name) // Sanity Checks if (file.isSymbolicLink()) continue if (file.isDirectory()) { if (ignoredDirectories.has(file.name)) continue await checkDirectory(filepath) continue } if (!file.isFile()) continue if (!allowedExtensions.has(path.extname(file.name))) continue // Use Cache if file was unmodified const filestat = await fs.stat(filepath) const cached = APP_CACHE_LOC[filepath] if (cached && cached.modtime == filestat.mtimeMs) { totalCount += cached.count continue } // Calculate LOC for Document const lineCount = (await fs.readFile(filepath, "utf8")) .replaceAll(/\/\*[\s\S]*?\*\//g, "") // Ignore Comments: Multiline .split("\n") .filter(l => l.length !== 0) // Ignore Empty Lines .filter(l => !l.startsWith("#")) // Ignore Comments: Python .filter(l => !l.startsWith("//")) // Ignore Comments: JavaScript .filter(l => !l.startsWith("--")) // Ignore Comments: SQL .filter(l => !l.startsWith("@REM")) // Ignore Comments: Batch .length // Cache and Append Result APP_CACHE_LOC[filepath] = { modtime: filestat.mtimeMs, count: lineCount, } totalCount += lineCount } } await checkDirectory(rootDirectory) return totalCount } /** Fetch Todays Cummulative Time from Wakatime API */ async function wakatimeFetchSummary(apiKey?: string): Promise { if (!apiKey) return DEFAULT_WAKATIME_DATA try { // Fetch Data from API const today = new Date() const range = `${today.getFullYear()}-${today.getMonth() + 1}-${today.getDate()}` const response = await fetch( `https://wakatime.com/api/v1/users/current/summaries?start=${range}&end=${range}`, { headers: { "Authorization": `Basic ${btoa(apiKey)}` }, }) if (response.status >= 200 && response.status <= 299) { // Parse Content const data: any = await response.json() return data?.cumulative_total ?? DEFAULT_WAKATIME_DATA } else { vscode.window.showErrorMessage(`Wakatime API Returned Status Code ${response.status}\n`) return DEFAULT_WAKATIME_DATA } } catch (err) { // Fatal Network Error vscode.window.showErrorMessage(`Wakatime Error: ${err}`) return DEFAULT_WAKATIME_DATA } } /** Use Templates and Setup Discord Rich Presence */ async function updateRPC() { let activityDetails = "Idle" let activityState: string | undefined const workspaceFolders = vscode.workspace.workspaceFolders if (workspaceFolders && workspaceFolders.length > 0) { // Prepare Template Data const workspaceName = vscode.workspace.name ?? path.basename(workspaceFolders[0].uri.fsPath) let workspaceLOC = 0 for await (const folder of workspaceFolders) { workspaceLOC += await directoryCountLOC( folder.uri.fsPath, new Set(APP_CONFIG.get("allowedExtensions")), new Set(APP_CONFIG.get("ignoredDirectories")), ) } const wakatimeData = await wakatimeFetchSummary( APP_CONFIG.get("wakatimeKey"), ) // Apply Template function useTemplate(givenText: string): string { return givenText .replaceAll("{workspace.name}", workspaceName) .replaceAll("{workspace.loc}", workspaceLOC.toLocaleString()) .replaceAll("{wakatime.decimal}", wakatimeData.decimal) .replaceAll("{wakatime.digital}", wakatimeData.digital) .replaceAll("{wakatime.seconds}", (wakatimeData.seconds | 0).toString()) .replaceAll("{wakatime.text}", wakatimeData.text) } activityDetails = useTemplate(APP_CONFIG.get("activityDetails", "")) activityState = useTemplate(APP_CONFIG.get("activityState", "")) } // Update Activity DISCORD_RPC.setActivity({ details: activityDetails, state: activityState, largeImageKey: "vscode", largeImageText: "Visual Studio Code", smallImageKey: APP_CONFIG.get("overrideSmallIcon") || undefined, smallImageText: APP_CONFIG.get("overrideSmallText") || undefined, instance: false }) } /** Extension Entrypoints */ export async function activate(context: vscode.ExtensionContext) { // Setup Discord Presence DISCORD_RPC.login({ clientId: DISCORD_CLIENT_ID }).catch(error => { vscode.window.showErrorMessage("Discord RPC Error: " + String(error)) }) DISCORD_RPC.on("ready", () => { setInterval(updateRPC, 300_000) updateRPC() }) // Setup Extension vscode.commands.registerCommand("trackpad.refresh", () => { vscode.window.showInformationMessage("Refreshing Discord Rich Presence!") updateRPC() }) context.subscriptions.push( vscode.Disposable.from({ dispose: () => DISCORD_RPC.destroy() }) ) } export async function deactivate() { if (DISCORD_RPC) DISCORD_RPC.destroy() }