commit 9c5084edcad258ddd21eba8ce393817d50206624 Author: bakonpancakz Date: Sat May 23 17:25:30 2026 -0700 Initial Release diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8d23896 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules +*.vsix +*.js \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6a5decc --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2026 bakonpancakz + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..fb80372 --- /dev/null +++ b/README.md @@ -0,0 +1,53 @@ +# ⌚ `trackpad` +Visual Studio Code Extension that displays your current [Wakatime](https://wakatime.com/) and Lines of Code as your Discord Activity. So you can finally prove to all your friends that you are, in fact, a 10x dev. + +

+ +

yep thats my profile
+

+ +--- + +### 🚀 Setup +If you trust me, you can grab the compiled extension from the [Releases](/releases) tab. Or if you're rightfully paranoid (respect), you can install dependencies and build it yourself: + +```bash +npm install +npm run build +``` + +After installion search for `Trackpad` in your VS Code settings and paste in your Wakatime API key. +Otherwise, it'll always show **0s**, and everyone will think you're a *pleb*. + +*Note: This extension doesn't actually track anything so you'll still need the [Wakatime Extension](https://wakatime.com/vs-code)* + +*Double Note: Your Discord activity updates every five minutes in order to keep overhead low. If it's too slow, open the Command Palette and run `Trackpad: Refresh Discord Rich Presence`* + + +### 🔧 Configuration +You can customize the Discord Activity Details and State fields using simple templates: +```yaml +{workspace.name} : trackpad +{workspace.loc} : 1,234 +{wakatime.decimal} : 1.5 +{wakatime.digital} : 1:30 +{wakatime.seconds} : 5400 +{wakatime.text} : 1hr 30mins +``` +All configuration can be done in your VS Code settings UI or directly in settings.json. + +Heres the defaults in-case you lost them: +```json +"trackpad.activityDetails": "Project: {workspace.name}", +"trackpad.activityState": "{workspace.loc} LOC - {wakatime.text}", +``` + +--- + +### 💡 Bonus +If you also stan Teto, throw this in your settings.json: + +```json +"trackpad.overrideSmallText": "Science!", +"trackpad.overrideSmallIcon": "https://c.pancakz.net/images/teto_science.gif", +``` \ No newline at end of file diff --git a/extension.ts b/extension.ts new file mode 100644 index 0000000..ff9b355 --- /dev/null +++ b/extension.ts @@ -0,0 +1,185 @@ +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() +} diff --git a/icon.png b/icon.png new file mode 100644 index 0000000..ca493e7 Binary files /dev/null and b/icon.png differ diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..c8485b8 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,190 @@ +{ + "name": "trackpad", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "trackpad", + "version": "1.0.0", + "dependencies": { + "discord-rpc": "^4.0.1" + }, + "devDependencies": { + "@types/discord-rpc": "^4.0.8", + "@types/node": "^22.10.2", + "@types/vscode": "^1.96.0", + "typescript": "^5.9.3" + }, + "engines": { + "vscode": "^1.96.0" + } + }, + "node_modules/@types/discord-rpc": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/@types/discord-rpc/-/discord-rpc-4.0.8.tgz", + "integrity": "sha512-1tZf217Natkj+TziNXRRLwNmdm5GNa1bnrQr8VWowquo/Su5hMjdhobj8URxW1COMk2da28XCU1ahsYCAlxirA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/events": "*" + } + }, + "node_modules/@types/events": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/events/-/events-3.0.3.tgz", + "integrity": "sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/vscode": { + "version": "1.96.0", + "resolved": "https://registry.npmjs.org/@types/vscode/-/vscode-1.96.0.tgz", + "integrity": "sha512-qvZbSZo+K4ZYmmDuaodMbAa67Pl6VDQzLKFka6rq+3WUTY4Kro7Bwoi0CuZLO/wema0ygcmpwow7zZfPJTs5jg==", + "dev": true, + "license": "MIT" + }, + "node_modules/bindings": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", + "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "file-uri-to-path": "1.0.0" + } + }, + "node_modules/discord-rpc": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/discord-rpc/-/discord-rpc-4.0.1.tgz", + "integrity": "sha512-HOvHpbq5STRZJjQIBzwoKnQ0jHplbEWFWlPDwXXKm/bILh4nzjcg7mNqll0UY7RsjFoaXA7e/oYb/4lvpda2zA==", + "license": "MIT", + "dependencies": { + "node-fetch": "^2.6.1", + "ws": "^7.3.1" + }, + "optionalDependencies": { + "register-scheme": "github:devsnek/node-register-scheme" + } + }, + "node_modules/file-uri-to-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", + "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "license": "MIT", + "optional": true + }, + "node_modules/node-addon-api": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-1.7.2.tgz", + "integrity": "sha512-ibPK3iA+vaY1eEjESkQkM0BbCqFOaZMiXRTtdB0u7b4djtY6JnsjvPdUHVMg6xQt3B8fpTTWHI9A+ADjM9frzg==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/register-scheme": { + "version": "0.0.2", + "resolved": "git+ssh://git@github.com/devsnek/node-register-scheme.git#e7cc9a63a1f512565da44cb57316d9fb10750e17", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "bindings": "^1.3.0", + "node-addon-api": "^1.3.0" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..a91d7c8 --- /dev/null +++ b/package.json @@ -0,0 +1,105 @@ +{ + "name": "trackpad", + "displayName": "Discord Trackpad", + "description": "Showcase your Wakatime and Lines of Code on Discord", + "version": "1.0.0", + "main": "./extension.js", + "publisher": "bakonpancakz", + "icon": "icon.png", + "files": [ + "node_modules", + "extension.js", + "README.md", + "icon.png", + "LICENSE" + ], + "repository": { + "url": "https://github.com/bakonpancakz/trackpad" + }, + "scripts": { + "build": "npm install && npx tsc && npm prune --omit=dev && npx @vscode/vsce package" + }, + "engines": { + "vscode": "^1.96.0" + }, + "categories": [ + "Other" + ], + "activationEvents": [ + "onStartupFinished" + ], + "dependencies": { + "discord-rpc": "^4.0.1" + }, + "devDependencies": { + "@types/discord-rpc": "^4.0.8", + "@types/node": "^22.10.2", + "@types/vscode": "^1.96.0", + "typescript": "^5.9.3" + }, + "contributes": { + "configuration": { + "title": "Trackpad", + "properties": { + "trackpad.allowedExtensions": { + "description": "Allowed File Extensions for Line Counter", + "type": "array", + "uniqueItems": true, + "default": [ + ".js", + ".ts", + ".pug", + ".html", + ".css", + ".sql", + ".xml", + ".yml", + ".md", + ".go" + ] + }, + "trackpad.ignoredDirectories": { + "type": "array", + "description": "Ignored Directory Names for Line Counter", + "uniqueItems": true, + "default": [ + "node_modules", + ".git", + "dist", + "build", + "out" + ] + }, + "trackpad.activityDetails": { + "type": "string", + "description": "Activity Details (Upper) with template literal support", + "default": "Project: {workspace.name}" + }, + "trackpad.activityState": { + "type": "string", + "description": "Activity State (Lower) with template literal support", + "default": "{workspace.loc} LOC - {wakatime.text}" + }, + "trackpad.overrideSmallIcon": { + "type": "string", + "description": "Set Small Icon URL for Activity" + }, + "trackpad.overrideSmallText": { + "description": "Set Small Text for Activity", + "type": "string" + }, + "trackpad.wakatimeKey": { + "type": "string", + "description": "", + "ignoreSync": true + } + } + }, + "commands": [ + { + "command": "trackpad.refresh", + "title": "Trackpad: Refresh Discord Rich Presence" + } + ] + } +} \ No newline at end of file diff --git a/preview.jpg b/preview.jpg new file mode 100644 index 0000000..e4e1bb2 Binary files /dev/null and b/preview.jpg differ diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..708a05a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "module": "Node16", + "target": "ES2022", + "lib": [ + "ES2022" + ], + "sourceMap": false, + "strict": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUnusedParameters": true, + } +} \ No newline at end of file