Initial Release

This commit is contained in:
2026-05-23 17:25:30 -07:00
commit 9c5084edca
9 changed files with 571 additions and 0 deletions
+3
View File
@@ -0,0 +1,3 @@
node_modules
*.vsix
*.js
+21
View File
@@ -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.
+53
View File
@@ -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.
<p align="center">
<img src="preview.jpg" height=320>
<h6 align="center">yep thats my profile</h6>
</p>
---
### 🚀 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",
```
+185
View File
@@ -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<string, { modtime: number; count: number; }> = {},
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<string>,
ignoredDirectories: Set<string>,
): Promise<number> {
// 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<WakatimeTotal> {
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<string>(APP_CONFIG.get("allowedExtensions")),
new Set<string>(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()
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 8.6 KiB

+190
View File
@@ -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
}
}
}
}
}
+105
View File
@@ -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"
}
]
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"module": "Node16",
"target": "ES2022",
"lib": [
"ES2022"
],
"sourceMap": false,
"strict": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedParameters": true,
}
}