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