diff --git a/package-lock.json b/package-lock.json index 6c93cab..dc32da7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "": { "name": "keystrokemanager", "version": "0.0.1", + "dependencies": { + "moment": "^2.29.4" + }, "devDependencies": { "@types/glob": "^7.2.0", "@types/mocha": "^9.1.1", @@ -1778,6 +1781,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==", + "engines": { + "node": "*" + } + }, "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", @@ -3792,6 +3803,11 @@ } } }, + "moment": { + "version": "2.29.4", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.4.tgz", + "integrity": "sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==" + }, "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", diff --git a/package.json b/package.json index c2a9a1e..c969a48 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,18 @@ "Other" ], "activationEvents": [ - "onCommand:keystrokemanager.helloWorld" + "onStartupFinished" ], "main": "./out/extension.js", "contributes": { "commands": [ { - "command": "keystrokemanager.helloWorld", - "title": "Hello World" + "command": "keystrokemanager.showKeystrokeCountAnalytics", + "title": "Keystrokes: Show Count-Analytics" + }, + { + "command": "keystrokemanager.mostOftenPressedKeys", + "title": "Keystrokes: Show most often pressed Keys" } ] }, @@ -30,16 +34,19 @@ "test": "node ./out/test/runTest.js" }, "devDependencies": { - "@types/vscode": "^1.71.0", "@types/glob": "^7.2.0", "@types/mocha": "^9.1.1", "@types/node": "16.x", + "@types/vscode": "^1.71.0", "@typescript-eslint/eslint-plugin": "^5.31.0", "@typescript-eslint/parser": "^5.31.0", + "@vscode/test-electron": "^2.1.5", "eslint": "^8.20.0", "glob": "^8.0.3", "mocha": "^10.0.0", - "typescript": "^4.7.4", - "@vscode/test-electron": "^2.1.5" + "typescript": "^4.7.4" + }, + "dependencies": { + "moment": "^2.29.4" } } diff --git a/src/extension.ts b/src/extension.ts index 22f469b..f7b6c52 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -1,26 +1,180 @@ -// The module 'vscode' contains the VS Code extensibility API -// Import the module and reference it with the alias vscode in your code below import * as vscode from 'vscode'; -// this method is called when your extension is activated -// your extension is activated the very first time the command is executed -export function activate(context: vscode.ExtensionContext) { +// General +const KEYSTROKE_DEFAULT_VALUE = 0; + +// Icons +const KEYBOARD_ICON = '$(keyboard)'; +const FIRST_PLACE_ICON = '🥇'; +const SECOND_PLACE_ICON = '🥈'; +const THIRD_PLACE_ICON = '🥉'; + +// Time +const SECOND_AS_MILLISECONDS = 1000; +const MINUTE_AS_MILLISECONDS = SECOND_AS_MILLISECONDS * 60; +const HOUR_AS_MILLISECONDS = MINUTE_AS_MILLISECONDS * 60; +const DAY_AS_MILLISECONDS = HOUR_AS_MILLISECONDS * 24; +const WEEK_AS_MILLISECONDS = DAY_AS_MILLISECONDS * 7; +const MONTH_AS_MILLISECONDS = DAY_AS_MILLISECONDS * 30; // @todo: could be problematic +const YEAR_AS_MILLISECONDS = MONTH_AS_MILLISECONDS * 12; + +/// general @todos: +// - comment functions +// - refactor code (with Neo for the best result I guess) +// - create project-structure +// - write read-me +// - write changelog +// - add cicd to project on github +// - add feature to display the whole analytics of which keys were pressed in a json-file or something like this +// - add feature to the keystroke-counts, etc. + +let statusBarItem: vscode.StatusBarItem; +let pressedKeyMap = new Map(); +let amountsOfKeystrokes = new Map([ + ['second', KEYSTROKE_DEFAULT_VALUE], + ['minute', KEYSTROKE_DEFAULT_VALUE], + ['hour', KEYSTROKE_DEFAULT_VALUE], + ['day', KEYSTROKE_DEFAULT_VALUE], + ['week', KEYSTROKE_DEFAULT_VALUE], + ['month', KEYSTROKE_DEFAULT_VALUE], + ['year', KEYSTROKE_DEFAULT_VALUE], + ['total', KEYSTROKE_DEFAULT_VALUE], +]); +let wpmWords = new Array(); +let wordsPerMinute = 0; + +export function activate({ subscriptions }: vscode.ExtensionContext) { + const showKeystrokeCountAnalyticsCommandId = 'keystrokemanager.showKeystrokeCountAnalytics'; + subscriptions.push(vscode.commands.registerCommand(showKeystrokeCountAnalyticsCommandId, () => { + const map = amountsOfKeystrokes; + const message = `You collected so far ${map.get('total')} keystrokes in total. + ${map.get('year')} of them this year, + ${map.get('month')} this month, + ${map.get('week')} this week, + ${map.get('day')} today, + ${map.get('hour')} this hour and + ${map.get('minute')} this minute!`; + + vscode.window.showInformationMessage(`😊 ${getPraisingWord()}! ${message}`); + })); + + const mostOftenPressedKeysCommandId = 'keystrokemanager.mostOftenPressedKeys'; + subscriptions.push(vscode.commands.registerCommand(mostOftenPressedKeysCommandId, () => { + const mostOftenPressedKeys = getMostOftenPressedKeys(); + const message = printMostOftenPressedKeysMessage(mostOftenPressedKeys); + + vscode.window.showInformationMessage(message); + })); - // Use the console to output diagnostic information (console.log) and errors (console.error) - // This line of code will only be executed once when your extension is activated - console.log('Congratulations, your extension "keystrokemanager" is now active!'); - - // The command has been defined in the package.json file - // Now provide the implementation of the command with registerCommand - // The commandId parameter must match the command field in package.json - let disposable = vscode.commands.registerCommand('keystrokemanager.helloWorld', () => { - // The code you place here will be executed every time your command is executed - // Display a message box to the user - vscode.window.showInformationMessage('Hello World from KeystrokeManager!'); + const ITEM_PRIORITY = 101; + statusBarItem = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Right, ITEM_PRIORITY); + statusBarItem.command = showKeystrokeCountAnalyticsCommandId; + statusBarItem.text = `${KEYBOARD_ICON} Keystrokes: ${amountsOfKeystrokes.get('total')}`; + statusBarItem.tooltip = 'Select Timespan'; + statusBarItem.show(); + subscriptions.push(statusBarItem); + + setInterval(() => { + const keystrokesPerSecond = amountsOfKeystrokes.get('second'); + + if(keystrokesPerSecond !== undefined) { + // keystrokesPerSecond / 5 -> one word is on average 5 chars long + // [...] * 60 -> estimation, that this rate is for the next 60 seconds + const tempWordsPerMinute = (keystrokesPerSecond / 5) * 60; + if(wpmWords.length === 60) { + wpmWords.shift(); + } + wpmWords.push(tempWordsPerMinute); + + wordsPerMinute = wpmWords.reduce((prev, curr) => prev + curr) / wpmWords.length; + statusBarItem.text = `${KEYBOARD_ICON} Keystrokes: ${amountsOfKeystrokes.get('total')} | ${wordsPerMinute} WPM`; + } + + resetOneAmountOfKeystrokes('second'); + }, SECOND_AS_MILLISECONDS); + + setInterval(() => resetOneAmountOfKeystrokes('minute'), MINUTE_AS_MILLISECONDS); + setInterval(() => resetOneAmountOfKeystrokes('hour'), HOUR_AS_MILLISECONDS); + setInterval(() => resetOneAmountOfKeystrokes('day'), DAY_AS_MILLISECONDS); + setInterval(() => resetOneAmountOfKeystrokes('week'), WEEK_AS_MILLISECONDS); + setLongInterval(() => resetOneAmountOfKeystrokes('month'), MONTH_AS_MILLISECONDS); + setLongInterval(() => resetOneAmountOfKeystrokes('year'), YEAR_AS_MILLISECONDS); + + subscriptions.push(vscode.workspace.onDidChangeTextDocument((event: vscode.TextDocumentChangeEvent) => updateStatusBarItem(event))); +} + +function updateStatusBarItem(event: vscode.TextDocumentChangeEvent): void { + // the last check is because of the first change in the document at the beginning + // -> counted instantly to 2 before + if(event && + event.contentChanges && + event.contentChanges[0].text !== undefined) { + amountsOfKeystrokes.forEach((value, key, map) => map.set(key, value + 1)); + statusBarItem.text = `${KEYBOARD_ICON} Keystrokes: ${amountsOfKeystrokes.get('total')} | ${wordsPerMinute} WPM`; + + collectPressedKey(event); + } +} + +function collectPressedKey(event: vscode.TextDocumentChangeEvent): void { + const pressedKey = event.contentChanges[0].text; + const prevCount = pressedKeyMap.get(pressedKey) ?? KEYSTROKE_DEFAULT_VALUE; + + pressedKeyMap.set(pressedKey, prevCount + 1); +} + +function getMostOftenPressedKeys(): Map { + const pressedKeysSortedDescending = new Map([...pressedKeyMap].sort((prev, curr) => prev[1] - curr[1]).reverse()); + + const targetSize = 3; + const mostOftenPressedKeys = new Map([...pressedKeysSortedDescending].slice(0, targetSize)); + + return mostOftenPressedKeys; +} + +function printMostOftenPressedKeysMessage(keyMap: Map): string { + const messageBeginning = new String('You pressed'); + let result = messageBeginning; + + // @todo: beautify this code-piece!!! disgusting! + const placementIcons = new Map([ + [1, FIRST_PLACE_ICON], + [2, SECOND_PLACE_ICON], + [3, THIRD_PLACE_ICON], + ]); + let placement = 1; + + keyMap.forEach((value, key, map) => { + const placementLine = new String(` ${placementIcons.get(placement++)} '${key}' ${value} times`); + result += placementLine.toString(); }); + result += '!'; + + return result.toString(); +} - context.subscriptions.push(disposable); +function getPraisingWord(): string { + const WORDS = ['Awesome', 'Wonderful', 'Great', 'Fantastic', 'Cool']; + return WORDS[Math.floor(Math.random() * WORDS.length)]; } -// this method is called when your extension is deactivated -export function deactivate() {} +const setLongInterval = (callback: any, timeout: number) => { + let count = 0; + const MAX_32_BIT_SIGNED = 2147483647; + const maxIterations = timeout / MAX_32_BIT_SIGNED; + + const onInterval = () => { + ++count; + if (count > maxIterations) { + count = 0; + callback(); + } + }; + + return setInterval(onInterval, Math.min(timeout, MAX_32_BIT_SIGNED)); +}; + +function resetOneAmountOfKeystrokes(key: string): void { + console.log("here: " + key); + amountsOfKeystrokes.set(key, KEYSTROKE_DEFAULT_VALUE); +} \ No newline at end of file