├── assets ├── icon.png ├── KeystrokeCountBanner.png └── KeystrokeCountPreviewImg.png ├── app ├── assets │ ├── appIcons │ │ ├── icon.png │ │ └── icon.icns │ ├── fonts │ │ ├── Jost │ │ │ ├── Jost-Light.ttf │ │ │ ├── Jost-Medium.ttf │ │ │ └── Jost-Regular.ttf │ │ └── Poppins │ │ │ ├── Poppins-Italic.ttf │ │ │ ├── Poppins-Light.ttf │ │ │ ├── Poppins-Regular.ttf │ │ │ └── Poppins-MediumItalic.ttf │ └── trayIcons │ │ ├── keystrokeCountTrayIcon.png │ │ ├── keystrokeCountTrayIcon@2x.png │ │ └── keystrokeCountTrayIcon@4x.png ├── clearData.js ├── package.json ├── setData.js ├── style.css ├── index.js ├── keyboard.js ├── heatmap.js ├── main.js ├── keyboard.css ├── index.html └── keys.js ├── docs ├── assets │ ├── KeystrokeCountLogo.png │ ├── KeystrokeCountBanner.png │ └── KeystrokeCountPreviewImg.png ├── downloads.html ├── index.html └── style.css ├── .gitignore ├── TODO.md ├── LICENSE ├── README.md └── test ├── index.html └── style.css /assets/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/assets/icon.png -------------------------------------------------------------------------------- /app/assets/appIcons/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/appIcons/icon.png -------------------------------------------------------------------------------- /app/assets/appIcons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/appIcons/icon.icns -------------------------------------------------------------------------------- /assets/KeystrokeCountBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/assets/KeystrokeCountBanner.png -------------------------------------------------------------------------------- /assets/KeystrokeCountPreviewImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/assets/KeystrokeCountPreviewImg.png -------------------------------------------------------------------------------- /docs/assets/KeystrokeCountLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/docs/assets/KeystrokeCountLogo.png -------------------------------------------------------------------------------- /app/assets/fonts/Jost/Jost-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Jost/Jost-Light.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Jost/Jost-Medium.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Jost/Jost-Medium.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Jost/Jost-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Jost/Jost-Regular.ttf -------------------------------------------------------------------------------- /docs/assets/KeystrokeCountBanner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/docs/assets/KeystrokeCountBanner.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.DS_Store 2 | 3 | app/node_modules 4 | app/.vscode 5 | 6 | builds/**/*.zip 7 | 8 | assets/KeystrokeCountPreviewVideo.mp4 9 | -------------------------------------------------------------------------------- /docs/assets/KeystrokeCountPreviewImg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/docs/assets/KeystrokeCountPreviewImg.png -------------------------------------------------------------------------------- /app/assets/fonts/Poppins/Poppins-Italic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Poppins/Poppins-Italic.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Poppins/Poppins-Light.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Poppins/Poppins-Light.ttf -------------------------------------------------------------------------------- /app/assets/fonts/Poppins/Poppins-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Poppins/Poppins-Regular.ttf -------------------------------------------------------------------------------- /app/assets/trayIcons/keystrokeCountTrayIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/trayIcons/keystrokeCountTrayIcon.png -------------------------------------------------------------------------------- /app/assets/fonts/Poppins/Poppins-MediumItalic.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/fonts/Poppins/Poppins-MediumItalic.ttf -------------------------------------------------------------------------------- /app/assets/trayIcons/keystrokeCountTrayIcon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/trayIcons/keystrokeCountTrayIcon@2x.png -------------------------------------------------------------------------------- /app/assets/trayIcons/keystrokeCountTrayIcon@4x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/virejdasani/KeystrokeCount/HEAD/app/assets/trayIcons/keystrokeCountTrayIcon@4x.png -------------------------------------------------------------------------------- /app/clearData.js: -------------------------------------------------------------------------------- 1 | let clearDataButton = document.getElementById("clearData"); 2 | 3 | // When the clear keystroke data button is pressed, localStorage.clear() and reload the page so the new data is reflected 4 | clearDataButton.addEventListener("click", () => { 5 | localStorage.clear(); 6 | location.reload(); 7 | }); 8 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | - Make a Windows version with windows keyboard 2 | - For some reason, capslock crashes the app. Fix that 3 | - Give more stats when haver or clicking a key like times clicked 4 | - Make it so that when clicking the virtual key twice, it returns back to the keyName 5 | - Add option to toggle timesClicked on keyboard keys 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Virej Dasani 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "keystrokecount", 3 | "version": "1.0.0", 4 | "description": "Data for your key strokes", 5 | "main": "main.js", 6 | 7 | "scripts": { 8 | "start": "electron .", 9 | "pack-darwin-x64": "electron-packager . 'Keystroke Count' --platform=darwin --arch=x64 --icon=assets/appIcons/icon.icns", 10 | "pack-win32-x64": "electron-packager . 'Keystroke Count' --platform=win32 --arch=x64 --icon=assets/appIcons/icon.ico", 11 | "pack-linux-x64": "electron-packager . 'Keystroke Count' --platform=linux --arch=x64 --icon=assets/appIcons/icon.png" 12 | }, 13 | "keywords": [ 14 | "keystroke-count", 15 | "count", 16 | "key", 17 | "keyboard-count" 18 | ], 19 | "author": "Virej Dasani", 20 | "license": "MIT", 21 | "dependencies": { 22 | "electron": "^11.0.7", 23 | "iohook": "^0.9.3", 24 | "open": "^8.2.0" 25 | }, 26 | "iohook": { 27 | "targets": [ 28 | "node-72", 29 | "electron-85" 30 | ], 31 | "platforms": [ 32 | "win32", 33 | "darwin", 34 | "linux" 35 | ], 36 | "arches": [ 37 | "x64", 38 | "ia32" 39 | ] 40 | } 41 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Github All Releases](https://img.shields.io/github/downloads/virejdasani/keystrokecount/total.svg)]() 2 | [![GitHub license](https://img.shields.io/github/license/virejdasani/keystrokecount)](https://github.com/virejdasani/keystrokecount/blob/master/LICENSE) 3 | [![GitHub release](https://img.shields.io/github/release/virejdasani/keystrokecount)](https://GitHub.com/virejdasani/keystrokecount/releases/) 4 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](http://makeapullrequest.com) 5 | [![Open Source? Yes!](https://badgen.net/badge/Open%20Source%20%3F/Yes%21/blue?icon=github)](https://github.com/virejdasani/keystrokecount/) 6 | 7 | # [KeystrokeCount](https://virejdasani.github.io/KeystrokeCount/) 8 | Data for your Keystrokes 9 | 10 | ![](https://raw.githubusercontent.com/virejdasani/KeystrokeCount/master/assets/KeystrokeCountBanner.png) 11 | 12 | - Check it out on YouTube [here](https://www.youtube.com/watch?v=rFWJ2cmRuBM) 13 | ## Keystroke Count keeps all your keystroke data locally stored and NO data including your keystrokes is ever collected. 14 | 15 | ![](https://raw.githubusercontent.com/virejdasani/KeystrokeCount/master/assets/KeystrokeCountPreviewImg.png) 16 | 17 | -------------------------------------------------------------------------------- /docs/downloads.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Keystroke Count 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 56 | 57 |
58 | 59 | 60 |

Keystroke Count keeps all your keystroke data locally stored and NO data 61 | including your keystrokes 62 | is 63 | ever collected.

64 |
65 | 66 |

Installation on MacOS

67 |

1. Download Keystroke Count For MacOS from above 68 |

69 |

2. Once downloaded, double-click the file

70 |

3. This will extract the app. Simply drag the app into the Applications folder on your machine.

71 |

4. If you are prompted with something like “Keystroke Count” 72 | cannot be opened because the developer cannot 73 | be verified., click cancel.

74 |

5. Go to system preferences -> Security and Privacy -> General -> and click Open Anyway.

76 | 77 | 83 |
84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /app/setData.js: -------------------------------------------------------------------------------- 1 | var totalKeys = document.getElementById("totalKeys"); 2 | var totalClicks = document.getElementById("totalClicks"); 3 | var mostUsedKey = document.getElementById("mostUsedKey"); 4 | var mostClicks = document.getElementById("mostClicks"); 5 | 6 | // This counts the number of keys pressed by looping over the timesClicked in the keys array 7 | function setTotalKeysClicked() { 8 | let totalKeysNum = 0; 9 | // Get the keys array from localStorage 10 | let keysArr = JSON.parse(localStorage.getItem("localKeys")); 11 | 12 | // Loop over all the objects in the keysArr 13 | for (let i in keysArr) { 14 | // This is a check to make sure that mouse clicks are not counted as clicks because the key code for rightClick and leftClick is over 100000000 15 | if (keysArr[i].keyCode < 100000000) { 16 | // Add the timesClicked of all the keys to totalKeysNum 17 | totalKeysNum += keysArr[i].timesClicked; 18 | } 19 | } 20 | 21 | // Now show the totalKeysNum in the totalKeys h2 element 22 | totalKeys.innerHTML = ` 23 | Total Keys Pressed Today: ${totalKeysNum.toLocaleString()} 24 | `; 25 | } 26 | 27 | // This counts the number of keys pressed by looping over the timesClicked in the keys array 28 | function setTotalMouseClicks() { 29 | let totalClicksNum = 0; 30 | // Get the keys array from localStorage 31 | let keysArr = JSON.parse(localStorage.getItem("localKeys")); 32 | 33 | // Loop over all the objects in the keysArr 34 | for (let i in keysArr) { 35 | // The only keyCodes greater than 100000000 are for leftClick and rightClick 36 | if (keysArr[i].keyCode > 100000000) { 37 | // Add the timesClicked of all the keys to totalKeysNum 38 | totalClicksNum += keysArr[i].timesClicked; 39 | } 40 | } 41 | 42 | // Now show the totalClickedNum in the totalKeys h2 element 43 | totalClicks.innerHTML = ` 44 | Total Mouse Clicks Today: ${totalClicksNum.toLocaleString()} 45 | `; 46 | } 47 | 48 | // This loops over the localKeys and returns the key that has the largest timesClicked 49 | function setMostUsedKey() { 50 | let keysArr = JSON.parse(localStorage.getItem("localKeys")); 51 | 52 | // This removes the leftClick and rightClick keys from keysArr 53 | keysArr = removeByAttr(keysArr, "keyCode", 134761167); 54 | keysArr = removeByAttr(keysArr, "keyCode", 4164761167); 55 | 56 | // This sorts keysArr in descending order of timeClicked and sets it to sortedArr 57 | let sortedArr = keysArr.sort((a, b) => b.timesClicked - a.timesClicked); 58 | 59 | // This gets the first item from sortedArr i.e. key with most presses and sets the keyName to mostUsedKey 60 | let mostUsedKeyName = sortedArr[0].keyName; 61 | // This gets the first item from sortedArr i.e. key with most presses and sets the timesClicked to mostUsedKeyPresses 62 | let mostUsedKeyPresses = sortedArr[0].timesClicked; 63 | 64 | mostUsedKey.innerHTML = ` 65 | Your most used key today was: ${mostUsedKeyName} 66 | `; 67 | mostClicks.innerHTML = ` 68 | You pressed it ${mostUsedKeyPresses.toLocaleString()} times 69 | `; 70 | } 71 | 72 | // This is run on page startup or refresh 73 | function onStartup() { 74 | setTotalKeysClicked(); 75 | setTotalMouseClicks(); 76 | setMostUsedKey(); 77 | setHeatmap(); 78 | virtualKeyClick(); 79 | } 80 | 81 | function reloadKeys() { 82 | setTotalKeysClicked(); 83 | setMostUsedKey(); 84 | setHeatmap(); 85 | virtualKeyClick(); 86 | } 87 | 88 | var removeByAttr = function (arr, attr, value) { 89 | var i = arr.length; 90 | while (i--) { 91 | if ( 92 | arr[i] && 93 | arr[i].hasOwnProperty(attr) && 94 | arguments.length > 2 && 95 | arr[i][attr] === value 96 | ) { 97 | arr.splice(i, 1); 98 | } 99 | } 100 | return arr; 101 | }; 102 | -------------------------------------------------------------------------------- /app/style.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: "Poppins"; 3 | src: url("./assets/fonts/Poppins/Poppins-Regular.ttf"); 4 | } 5 | 6 | @font-face { 7 | font-family: "Jost"; 8 | src: url("./assets/fonts/Jost/Jost-Regular.ttf"); 9 | } 10 | 11 | @font-face { 12 | font-family: "JostThin"; 13 | src: url("./assets/fonts/Jost/Jost-Light.ttf"); 14 | } 15 | 16 | html, 17 | body { 18 | color: #fff; 19 | background-color: #232e32; 20 | font-family: Arial, Helvetica, sans-serif; 21 | text-align: center; 22 | user-select: none; 23 | /* overflow: hidden; */ 24 | } 25 | 26 | #headTitle { 27 | font-family: "Poppins"; 28 | text-align: center; 29 | color: #FFF2E9; 30 | font-size: 44px; 31 | margin-bottom: 0; 32 | margin-top: 20px; 33 | } 34 | 35 | #topData { 36 | font-size: 16px; 37 | margin-top: 42px; 38 | } 39 | 40 | #bottomData { 41 | font-size: 16px; 42 | margin-top: 40px; 43 | } 44 | 45 | #clearData { 46 | margin-top: 0px; 47 | margin-bottom: 18px; 48 | background: rgba(190, 190, 190, 0.19); 49 | box-shadow: 0px 4px 4px rgba(0, 0, 0, 0.25); 50 | border-radius: 15px; 51 | width: 145px; 52 | height: 55px; 53 | border: none; 54 | font-size: 14px; 55 | line-height: 51px; 56 | color: #c2c2c2; 57 | outline: none; 58 | } 59 | 60 | #clearData:hover { 61 | transition: 0.1s ease-in-out; 62 | color: #d6d6d6; 63 | box-shadow: 0px 6px 6px rgba(0, 0, 0, 0.25); 64 | background: rgba(190, 190, 190, 0.26); 65 | } 66 | 67 | #clearData:active { 68 | transition: 0.1s ease-in-out; 69 | color: #c7c7c7; 70 | box-shadow: 0px 5px 5px rgba(0, 0, 0, 0.25); 71 | background: rgba(190, 190, 190, 0.22); 72 | } 73 | 74 | #credits { 75 | padding-bottom: 5px; 76 | font-size: 15px; 77 | font-family: "JostThin"; 78 | } 79 | 80 | .separator { 81 | background-color: rgba(255, 255, 255, 0.4); 82 | border: none; 83 | height: 1px; 84 | width: 70%; 85 | margin-left: 15%; 86 | margin-top: 10px; 87 | } 88 | 89 | .titleSeparator { 90 | background-color: rgba(255, 255, 255, 0.75); 91 | border: none; 92 | height: 1px; 93 | width: 56%; 94 | margin-left: 22%; 95 | } 96 | 97 | .Jost { 98 | font-family: "Jost"; 99 | } 100 | 101 | #heatmapLegend { 102 | width: 40%; 103 | height: 20px; 104 | margin-left: 30%; 105 | margin-top: 6px; 106 | border-radius: 50px; 107 | 108 | /* Gradient from https://coolors.co/gradient-maker/e87a76-c8a3e2-99ceea-ffffff?position=0,48,89,100&opacity=100,100,100,100&type=linear&rotation=270 */ 109 | background: hsla(2, 71%, 69%, 1); 110 | background: linear-gradient(180deg, hsla(2, 71%, 69%, 1) 0%, hsla(275, 52%, 76%, 1) 48%, hsla(201, 66%, 76%, 1) 89%, hsla(0, 0%, 100%, 1) 100%); 111 | background: -moz-linear-gradient(180deg, hsla(2, 71%, 69%, 1) 0%, hsla(275, 52%, 76%, 1) 48%, hsla(201, 66%, 76%, 1) 89%, hsla(0, 0%, 100%, 1) 100%); 112 | background: -webkit-linear-gradient(180deg, hsla(2, 71%, 69%, 1) 0%, hsla(275, 52%, 76%, 1) 48%, hsla(201, 66%, 76%, 1) 89%, hsla(0, 0%, 100%, 1) 100%); 113 | } 114 | 115 | #heatmapText { 116 | margin-top: 20px; 117 | } 118 | 119 | #heatmapBarLeft { 120 | display: inline-block; 121 | margin-right: 14%; 122 | } 123 | 124 | #heatmapBarRight { 125 | display: inline-block; 126 | margin-left: 14%; 127 | } 128 | 129 | #update { 130 | margin-top: 30px; 131 | margin-bottom: 30px; 132 | } 133 | 134 | ::-webkit-scrollbar { 135 | background-color: #181e21; 136 | width: 12px !important; 137 | } 138 | 139 | ::-webkit-scrollbar-thumb { 140 | background-color: rgb(44, 56, 65); 141 | border-radius: 12px; 142 | background-clip: content-box; 143 | min-height: 26px; 144 | } 145 | 146 | a:link { 147 | color: rgb(0, 187, 255); 148 | background-color: transparent; 149 | text-decoration: underline; 150 | } 151 | 152 | a:visited { 153 | color: rgb(0, 187, 255); 154 | background-color: transparent; 155 | text-decoration: underline; 156 | } -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Keystroke Count 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 29 | 30 | 31 | 32 | 33 | 34 | 40 | 41 | 42 | 43 | 44 | 45 | 55 | 56 |
57 | 58 | 59 |

Keystroke Count keeps all your keystroke data locally stored and NO data 60 | including your keystrokes 61 | is 62 | ever collected.

63 |
64 | 65 |

What is Keystroke Count?

66 |

Keystroke Count is an open-source, cross-platform, desktop app that you can use to get data for your keystrokes! 67 |

68 | 69 |

This is Keystroke Count

70 | 73 | 74 |
75 |

Keystroke Count preview video 👆

76 |
77 | 78 | 79 |
80 |

Keystroke Count Home Page 👆

81 |
82 | 83 |

Get Keystroke Count

84 | 91 | 92 | 98 |
99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /app/index.js: -------------------------------------------------------------------------------- 1 | var appVersion = "1.0.0"; 2 | 3 | const { ipcRenderer } = require("electron"); 4 | 5 | // Get the localKeys from localStorage 6 | // localKeys is the keys array from keys.js 7 | var localKeys = localStorage.getItem("localKeys"); 8 | 9 | // If localKeys is not found in localStorage 10 | if (localKeys === null) { 11 | // Set the keys array to localStorage by id of "localKeys" 12 | localStorage.setItem("localKeys", JSON.stringify(keys)); 13 | console.log("Set keys array to localStorage"); 14 | } 15 | 16 | // When a key press is detected in the main process this happens 17 | ipcRenderer.on("keydownEvent", (event, keyCodesPressed) => { 18 | // console.log(keyCodesPressed); 19 | // Set localKeys to "localKeys" from localStorage 20 | localKeys = JSON.parse(localStorage.getItem("localKeys")); 21 | // If localKeys doesn't exist in localStorage, we initialize it to the keys array from keys.js 22 | 23 | // Loop over all the objects in the keys array (from keys.js) 24 | for (let i in localKeys) { 25 | // If the keyCode in the keys array matches the last pressed key 26 | if (localKeys[i].keyCode == keyCodesPressed[0]) { 27 | // For that key, the timesClicked in that object is incremented by 1 28 | localKeys[i].timesClicked += 1; 29 | // Now update the localKeys array in localStorage to reflect the +1 in timesClicked 30 | localStorage.setItem("localKeys", JSON.stringify(localKeys)); 31 | // This updates all the keys from setData.js and heatmap.js 32 | reloadKeys(); 33 | // Break out of the loop 34 | break; 35 | } 36 | } 37 | }); 38 | 39 | // For mouse clicks 40 | // When left click is detected 41 | ipcRenderer.on("leftClickEvent", (event, leftClicks) => { 42 | // Get the keys array from localStorage 43 | localKeys = JSON.parse(localStorage.getItem("localKeys")); 44 | 45 | // Loop over all the objects in the keys array (from keys.js) 46 | for (let i in localKeys) { 47 | // Check if the 'i'th keyCode is 134761167, which is the keyCode for leftClick (this code is randomly set by me in the keys array) 48 | if (localKeys[i].keyCode == 134761167) { 49 | // Increment the timesClicked for leftClick 50 | localKeys[i].timesClicked += 1; 51 | // Update the keys array in localStorage 52 | localStorage.setItem("localKeys", JSON.stringify(localKeys)); 53 | // This shows the total number of mouse clicks today (from setData.js) 54 | setTotalMouseClicks(); 55 | // Break out of the loop 56 | break; 57 | } 58 | } 59 | }); 60 | 61 | // When right click is detected 62 | ipcRenderer.on("rightClickEvent", (event, rightClicks) => { 63 | // Get the keys array from localStorage 64 | localKeys = JSON.parse(localStorage.getItem("localKeys")); 65 | 66 | // Loop over all the objects in the keys array (from keys.js) 67 | for (let i in localKeys) { 68 | // Check if the 'i'th keyCode is 4164761167, which is the keyCode for rightClick (this code is randomly set by me in the keys array) 69 | if (localKeys[i].keyCode == 4164761167) { 70 | // Increment the timesClicked for rightClick 71 | localKeys[i].timesClicked += 1; 72 | // Update the keys array in localStorage 73 | localStorage.setItem("localKeys", JSON.stringify(localKeys)); 74 | // This shows the total number of mouse clicks today (from setData.js) 75 | setTotalMouseClicks(); 76 | // Break out of the loop 77 | break; 78 | } 79 | } 80 | }); 81 | 82 | // For app update, if an update is available, the updateAvailable in the RemoteJSON repo will be updated to yes. That will result in the code below being executed 83 | fetch("https://virejdasani.github.io/RemoteJSON/KeystrokeCount/index.html") 84 | .then((response) => { 85 | return response.json(); 86 | }) 87 | .then((data) => { 88 | // If update is available, and this version is not the latest one, the update div will no longer be empty. It will have the following HTML 89 | if (data.updateAvailable == "yes" && data.latestVersion != appVersion) { 90 | document.getElementById("update").innerHTML = ` 91 |
92 | ${data.updateText} 93 | Download update here 94 |
95 | `; 96 | } 97 | }) 98 | .catch((err) => { 99 | // console.log(err) 100 | }); 101 | -------------------------------------------------------------------------------- /app/keyboard.js: -------------------------------------------------------------------------------- 1 | function virtualKeyClick() { 2 | // Get the array from localStorage 3 | let keysArr = JSON.parse(localStorage.getItem("localKeys")); 4 | 5 | // This removes the leftClick and rightClick keys from keysArr 6 | keysArr = removeByAttr(keysArr, "keyCode", 134761167); 7 | keysArr = removeByAttr(keysArr, "keyCode", 4164761167); 8 | 9 | // Loop over all keys from keysArr 10 | for (let i in keysArr) { 11 | // This is the current or 'i'th object in the array 12 | let thisKey = keysArr[i]; 13 | 14 | // Check if thisKey is an element in the html 15 | if (document.getElementById(thisKey.keyName)) { 16 | // Check if the button is not disabled. Only if it isn't disabled, addEventListener 17 | if ( 18 | !document.getElementById(thisKey.keyName).className.includes("disabled") 19 | ) { 20 | // When that key is pressed, to this 21 | document 22 | .getElementById(thisKey.keyName) 23 | .addEventListener("click", () => { 24 | // This is the innerHTML of the key 25 | let keyInnerHTML = document.getElementById( 26 | thisKey.keyName 27 | ).innerHTML; 28 | if (keyInnerHTML.includes("")) { 29 | // The innerHTML is changed to the times that key is pressed 30 | document.getElementById( 31 | thisKey.keyName 32 | ).innerHTML = `
${thisKey.timesClicked}`; 33 | // In the 5 seconds, the initial innerHTML is restored in that key 34 | setTimeout(() => { 35 | document.getElementById(thisKey.keyName).innerHTML = 36 | keyInnerHTML; 37 | }, 5000); 38 | // If it is an arrow key, do this because for some reason without this, it doesn't work 39 | } else if ( 40 | document 41 | .getElementById(thisKey.keyName) 42 | .className.includes("Arrow") 43 | ) { 44 | var arrow; 45 | 46 | // For arrow keys, set the arrow code to the arrow variable 47 | if ( 48 | document 49 | .getElementById(thisKey.keyName) 50 | .className.includes("leftArrow") 51 | ) { 52 | arrow = "◀"; 53 | } else if ( 54 | document 55 | .getElementById(thisKey.keyName) 56 | .className.includes("downArrow") 57 | ) { 58 | arrow = "▼"; 59 | } else if ( 60 | document 61 | .getElementById(thisKey.keyName) 62 | .className.includes("upArrow") 63 | ) { 64 | arrow = "▲"; 65 | } else if ( 66 | document 67 | .getElementById(thisKey.keyName) 68 | .className.includes("rightArrow") 69 | ) { 70 | arrow = "▶"; 71 | } 72 | 73 | // The innerHTML is changed to the times that key is pressed 74 | document.getElementById( 75 | thisKey.keyName 76 | ).innerHTML = `${thisKey.timesClicked}`; 77 | // In the 5 seconds, the initial innerHTML is restored in that key 78 | setTimeout(() => { 79 | document.getElementById( 80 | thisKey.keyName 81 | ).innerHTML = `
${arrow}`; 82 | }, 5000); 83 | } else if ( 84 | document 85 | .getElementById(thisKey.keyName) 86 | .className.includes("option") 87 | ) { 88 | // The innerHTML is changed to the times that key is pressed 89 | document.getElementById( 90 | thisKey.keyName 91 | ).innerHTML = `${thisKey.timesClicked}`; 92 | // In the 5 seconds, the initial innerHTML is restored in that key 93 | setTimeout(() => { 94 | document.getElementById(thisKey.keyName).innerHTML = 95 | "option"; 96 | }, 5000); 97 | } else { 98 | // The innerHTML is changed to the times that key is pressed 99 | document.getElementById( 100 | thisKey.keyName 101 | ).innerHTML = `${thisKey.timesClicked}`; 102 | // In the 5 seconds, the initial innerHTML is restored in that key 103 | setTimeout(() => { 104 | document.getElementById(thisKey.keyName).innerHTML = 105 | keyInnerHTML; 106 | }, 5000); 107 | } 108 | }); 109 | } 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /app/heatmap.js: -------------------------------------------------------------------------------- 1 | const heatmapColors = [ 2 | "#fa6e6e", 3 | "#fc7290", 4 | "#f57cb0", 5 | "#e78acc", 6 | "#d399e2", 7 | "#bca7f0", 8 | "#a5b3f7", 9 | "#94bef8", 10 | "#8ac7f3", 11 | "#8bceec", 12 | ]; 13 | 14 | // This calculates and sets colors to the keyboard keys based on how many times they have been clicked 15 | function setHeatmap() { 16 | // Get the array from localStorage 17 | let keysArr = JSON.parse(localStorage.getItem("localKeys")); 18 | 19 | // This removes the leftClick and rightClick keys from keysArr 20 | keysArr = removeByAttr(keysArr, "keyCode", 134761167); 21 | keysArr = removeByAttr(keysArr, "keyCode", 4164761167); 22 | 23 | // This sorts keysArr in descending order of timeClicked and sets it to sortedArr 24 | let sortedArr = keysArr.sort((a, b) => b.timesClicked - a.timesClicked); 25 | 26 | // This gets the first item from sortedArr i.e. key with most presses 27 | let topKey = sortedArr[0].timesClicked; 28 | 29 | // Loop over all keys from keysArr 30 | for (let i in keysArr) { 31 | // This is the current or 'i'th object in the array 32 | let thisKey = keysArr[i]; 33 | 34 | // Check if thisKey exits on the keyboard 35 | if (document.getElementById(thisKey.keyName)) { 36 | // This is to give colors to keys based on how many relative clicks to the topKey they have 37 | // The topKey is always the most intense color 38 | if (thisKey.timesClicked > 0.9 * topKey) { 39 | document.getElementById(thisKey.keyName).style.backgroundColor = 40 | heatmapColors[0]; 41 | document.getElementById(thisKey.keyName).style.textShadow = 42 | "0px 1px 0px #4b4848"; 43 | document.getElementById(thisKey.keyName).style.color = "#fff"; 44 | } else if (thisKey.timesClicked > 0.8 * topKey) { 45 | document.getElementById(thisKey.keyName).style.backgroundColor = 46 | heatmapColors[1]; 47 | document.getElementById(thisKey.keyName).style.textShadow = 48 | "0px 1px 0px #4b4848"; 49 | document.getElementById(thisKey.keyName).style.color = "#fff"; 50 | } else if (thisKey.timesClicked > 0.7 * topKey) { 51 | document.getElementById(thisKey.keyName).style.backgroundColor = 52 | heatmapColors[2]; 53 | document.getElementById(thisKey.keyName).style.textShadow = 54 | "0px 1px 0px #4b4848"; 55 | document.getElementById(thisKey.keyName).style.color = "#fff"; 56 | } else if (thisKey.timesClicked > 0.6 * topKey) { 57 | document.getElementById(thisKey.keyName).style.backgroundColor = 58 | heatmapColors[3]; 59 | document.getElementById(thisKey.keyName).style.textShadow = 60 | "0px 1px 0px #4b4848"; 61 | document.getElementById(thisKey.keyName).style.color = "#fff"; 62 | } else if (thisKey.timesClicked > 0.5 * topKey) { 63 | document.getElementById(thisKey.keyName).style.backgroundColor = 64 | heatmapColors[4]; 65 | document.getElementById(thisKey.keyName).style.textShadow = 66 | "0px 1px 0px #4b4848"; 67 | document.getElementById(thisKey.keyName).style.color = "#fff"; 68 | } else if (thisKey.timesClicked > 0.4 * topKey) { 69 | document.getElementById(thisKey.keyName).style.backgroundColor = 70 | heatmapColors[5]; 71 | document.getElementById(thisKey.keyName).style.textShadow = 72 | "0px 1px 0px #4b4848"; 73 | document.getElementById(thisKey.keyName).style.color = "#fff"; 74 | } else if (thisKey.timesClicked > 0.3 * topKey) { 75 | document.getElementById(thisKey.keyName).style.backgroundColor = 76 | heatmapColors[6]; 77 | document.getElementById(thisKey.keyName).style.textShadow = 78 | "0px 1px 0px #4b4848"; 79 | document.getElementById(thisKey.keyName).style.color = "#fff"; 80 | } else if (thisKey.timesClicked > 0.2 * topKey) { 81 | document.getElementById(thisKey.keyName).style.backgroundColor = 82 | heatmapColors[7]; 83 | document.getElementById(thisKey.keyName).style.textShadow = 84 | "0px 1px 0px #4b4848"; 85 | document.getElementById(thisKey.keyName).style.color = "#fff"; 86 | } else if (thisKey.timesClicked > 0.1 * topKey) { 87 | document.getElementById(thisKey.keyName).style.backgroundColor = 88 | heatmapColors[8]; 89 | document.getElementById(thisKey.keyName).style.textShadow = 90 | "0px 1px 0px #4b4848"; 91 | document.getElementById(thisKey.keyName).style.color = "#fff"; 92 | } else if (thisKey.timesClicked > 0) { 93 | document.getElementById(thisKey.keyName).style.backgroundColor = 94 | heatmapColors[9]; 95 | document.getElementById(thisKey.keyName).style.textShadow = 96 | "0px 1px 0px #4b4848"; 97 | document.getElementById(thisKey.keyName).style.color = "#fff"; 98 | } else if (thisKey.timesClicked == 0) { 99 | // console.log("thisKey never clicked") 100 | } else { 101 | // console.log("KEYERR"); 102 | } 103 | } else { 104 | // console.log("This key not found:"); 105 | // console.log(thisKey.keyName); 106 | } 107 | } 108 | } 109 | 110 | // For heatmapColors 111 | // https://colordesigner.io/gradient-generator 112 | -------------------------------------------------------------------------------- /app/main.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const { 4 | app, 5 | BrowserWindow, 6 | Tray, 7 | globalShortcut, 8 | Menu, 9 | ipcMain, 10 | } = require("electron"); 11 | 12 | // This is the npm package `open`, it is used here to open all links in an external browser 13 | const open = require("open"); 14 | 15 | const path = require("path"); 16 | 17 | const ioHook = require("iohook"); 18 | 19 | const assetsDirectory = path.join(__dirname, "assets"); 20 | 21 | let tray = undefined; 22 | let window = undefined; 23 | 24 | // Hide the menu and dev tools 25 | Menu.setApplicationMenu(null); 26 | 27 | // Quit the app when the window is closed 28 | app.on("window-all-closed", () => { 29 | hideWindow(); 30 | }); 31 | 32 | const createTray = () => { 33 | tray = new Tray( 34 | path.join(assetsDirectory, "trayIcons/keystrokeCountTrayIcon@2x.png") 35 | ); 36 | tray.on("right-click", toggleWindow); 37 | tray.on("double-click", toggleWindow); 38 | tray.on("click", function (event) { 39 | toggleWindow(); 40 | }); 41 | }; 42 | 43 | const createWindow = () => { 44 | window = new BrowserWindow({ 45 | width: 900, 46 | height: 780, 47 | minWidth: 800, 48 | minHeight: 700, 49 | show: false, 50 | frame: false, 51 | 52 | // fullscreenable: false, 53 | // resizable: false, 54 | // alwaysOnTop: true, 55 | webPreferences: { 56 | nodeIntegration: true, 57 | contextIsolation: false, 58 | enableRemoteModule: true, 59 | }, 60 | }); 61 | 62 | // This is so that app shows up on all desktops/workspaces 63 | window.setVisibleOnAllWorkspaces(true); 64 | 65 | // Load index.html 66 | window.loadURL(`file://${path.join(__dirname, "index.html")}`); 67 | 68 | // If 'esc' is pressed, hide the app window 69 | window.webContents.on("before-input-event", (event, input) => { 70 | if (input.key === "Escape") { 71 | hideWindow(); 72 | } 73 | }); 74 | 75 | // This opens all links with `target="_blank"` in external browser 76 | window.webContents.on("new-window", function (event, url) { 77 | event.preventDefault(); 78 | open(url); 79 | }); 80 | 81 | // Hide the window when it loses focus 82 | window.on("blur", () => { 83 | hideWindow(); 84 | }); 85 | 86 | // This is a global shortcut to activate app with hotkey(s) 87 | globalShortcut.register("Alt+Shift+k", () => { 88 | if (window.isVisible()) { 89 | hideWindow(); 90 | } else { 91 | showWindow(); 92 | } 93 | }); 94 | if (process.platform == "darwin") { 95 | // Don't show the app in the dock for macOS 96 | app.dock.hide(); 97 | } else { 98 | // To hide the app in the dock for windows and linux 99 | window.setSkipTaskbar(true); 100 | } 101 | }; 102 | 103 | const toggleWindow = () => { 104 | if (window.isVisible()) { 105 | hideWindow(); 106 | } else { 107 | showWindow(); 108 | } 109 | }; 110 | 111 | const showWindow = () => { 112 | window.show(); 113 | window.reload(); 114 | }; 115 | 116 | const hideWindow = () => { 117 | // This is required because app.hide() is not defined in windows and linux 118 | if (process.platform == "darwin") { 119 | // This is so that when reopening the window, the previous state is not remembered 120 | // window.reload() 121 | // Both of these are needed because they help restore focus back to the previous window 122 | app.hide(); 123 | window.hide(); 124 | } else { 125 | // This is so that when reopening the window, the previous state is not remembered 126 | // window.reload() 127 | // Both of these are needed because they help restore focus back to the previous window 128 | window.minimize(); 129 | window.hide(); 130 | } 131 | }; 132 | 133 | // ioHook 134 | // docs: https://wilix-team.github.io/iohook/usage.html#generic-node-application 135 | 136 | var leftClicks = 0; 137 | var rightClicks = 0; 138 | var keysPressed = []; 139 | 140 | app.on("ready", () => { 141 | createTray(); 142 | createWindow(); 143 | 144 | // When a mouse button is clicked 145 | ioHook.on("mouseclick", (event) => { 146 | // Check if it is a left-click 147 | if (event["button"] === 1) { 148 | leftClicks += 1; 149 | // This sends the clicks to index.js where this num of clicks are handled 150 | window.webContents.send("leftClickEvent", leftClicks); 151 | } 152 | 153 | // Check if it is a right-click 154 | else if (event["button"] === 2) { 155 | rightClicks += 1; 156 | // This sends the clicks to index.js where this num of clicks are handled 157 | window.webContents.send("rightClickEvent", rightClicks); 158 | } 159 | }); 160 | 161 | // When a keyboard button is pressed 162 | ioHook.on("keydown", (event) => { 163 | // console.log(event); 164 | // Append the keycode for the key pressed to the keysPressed array 165 | keysPressed.unshift(event["keycode"]); 166 | // Send the message with the new array to the renderer process 167 | window.webContents.send("keydownEvent", keysPressed); 168 | }); 169 | 170 | // Register and start hook 171 | ioHook.start(); 172 | 173 | // Alternatively, pass true to start in DEBUG mode. 174 | ioHook.start(true); 175 | 176 | // False to disable DEBUG. Cleaner terminal output. 177 | // ioHook.start(false) 178 | }); 179 | -------------------------------------------------------------------------------- /app/keyboard.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | #keyboard { 7 | z-index: 20; 8 | margin: 20px auto 40px; 9 | width: 800px; 10 | height: 315px; 11 | background: #bbb; 12 | background-image: -webkit-gradient(linear, 13 | left bottom, 14 | left top, 15 | color-stop(0.27, rgb(212, 216, 219)), 16 | color-stop(0.64, rgb(213, 217, 220)), 17 | color-stop(0.95, rgb(230, 233, 235)), 18 | color-stop(1, rgb(191, 191, 191))); 19 | background-image: -moz-linear-gradient(center bottom, 20 | rgb(212, 216, 219) 27%, 21 | rgb(213, 217, 220) 64%, 22 | rgb(230, 233, 235) 95%, 23 | rgb(191, 191, 191) 100%); 24 | -moz-border-radius-topleft: 7px 21px; 25 | -moz-border-radius-topright: 7px 21px; 26 | -moz-border-radius-bottomright: 10px; 27 | -moz-border-radius-bottomleft: 10px; 28 | border-top-left-radius: 7px 21px; 29 | border-top-right-radius: 7px 21px; 30 | border-bottom-right-radius: 10px; 31 | border-bottom-left-radius: 10px; 32 | padding: 50px 0 0; 33 | } 34 | 35 | ul { 36 | list-style-type: none; 37 | width: 784px; 38 | margin: 0 auto; 39 | } 40 | 41 | li { 42 | float: left; 43 | } 44 | 45 | .key { 46 | display: block; 47 | color: #aaa; 48 | font: bold 9pt arial; 49 | text-decoration: none; 50 | text-align: center; 51 | width: 44px; 52 | height: 41px; 53 | margin: 5px; 54 | background: #eff0f2; 55 | -moz-border-radius: 4px; 56 | border-radius: 4px; 57 | border-top: 1px solid #696969; 58 | box-shadow: 59 | 0 1px 0 #c3c3c3, 60 | 0 2px 0 #c9c9c9, 61 | 0 2px 3px #333; 62 | text-shadow: 0px 1px 0px #f5f5f5; 63 | filter: dropshadow(color=#f5f5f5, offx=0, offy=1); 64 | } 65 | 66 | .key:active, 67 | .keydown { 68 | color: #aaa; 69 | background: #ebeced; 70 | margin: 7px 5px 3px; 71 | box-shadow: 72 | 0 0 3px #333; 73 | border-top: 1px solid #eee; 74 | } 75 | 76 | .fn span { 77 | display: block; 78 | margin: 14px 5px 0 0; 79 | text-align: right; 80 | font: bold 6pt arial; 81 | text-transform: uppercase; 82 | } 83 | 84 | #numbers li a span { 85 | display: block; 86 | } 87 | 88 | #numbers li a b { 89 | margin: 3px 0 3px; 90 | display: block; 91 | } 92 | 93 | #numbers li .alt b { 94 | display: block; 95 | margin: 0 0 3px; 96 | } 97 | 98 | #numbers li #Backspace span { 99 | text-align: right; 100 | margin: 23px 10px 0 0; 101 | font-size: 7.5pt; 102 | text-transform: lowercase; 103 | } 104 | 105 | #qwerty li a span { 106 | display: block; 107 | margin: 13px 0 0; 108 | text-transform: uppercase; 109 | } 110 | 111 | #qwerty li #Tab span { 112 | text-align: left; 113 | margin: 23px 0 0 10px; 114 | font-size: 7.5pt; 115 | text-transform: lowercase; 116 | } 117 | 118 | #qwerty li .alt b { 119 | display: block; 120 | margin: 3px 0 0; 121 | } 122 | 123 | #qwerty li .alt span { 124 | margin: 2px 0 0; 125 | } 126 | 127 | 128 | #asdfg li a span { 129 | display: block; 130 | margin: 13px 0 0; 131 | text-transform: uppercase; 132 | } 133 | 134 | #asdfg li .alt span { 135 | margin: 0; 136 | text-transform: lowercase; 137 | } 138 | 139 | #asdfg li .alt b { 140 | display: block; 141 | margin: 3px 0 0; 142 | } 143 | 144 | #asdfg li #CapsLock b { 145 | display: block; 146 | background: #999; 147 | width: 4px; 148 | height: 4px; 149 | border-radius: 10px; 150 | margin: 9px 0 0 10px; 151 | -webkit-box-shadow: inset 0 1px 0 #666; 152 | -moz-box-shadow: inset 0 1px 0 #666; 153 | box-shadow: inset 0 1px 0 #666; 154 | } 155 | 156 | #asdfg li #CapsLock span { 157 | text-align: left; 158 | margin: 10px 0 0 10px; 159 | font-size: 7.5pt; 160 | } 161 | 162 | #asdfg li #Enter span { 163 | text-align: right; 164 | margin: 23px 10px 0 0; 165 | font-size: 7.5pt; 166 | } 167 | 168 | 169 | #zxcvb li a span { 170 | display: block; 171 | margin: 13px 0 0; 172 | text-transform: uppercase; 173 | } 174 | 175 | #zxcvb li .shiftleft span { 176 | text-align: left; 177 | margin: 23px 0 0 10px; 178 | font-size: 7.5pt; 179 | text-transform: lowercase; 180 | } 181 | 182 | #zxcvb li .shiftright span { 183 | text-align: right; 184 | margin: 23px 10px 0 0; 185 | font-size: 7.5pt; 186 | text-transform: lowercase; 187 | } 188 | 189 | #zxcvb li .alt b { 190 | display: block; 191 | margin: 4px 0 0; 192 | } 193 | 194 | #zxcvb li .alt span { 195 | margin: 0; 196 | } 197 | 198 | #bottomrow li #fn span, 199 | #bottomrow li #LeftControl span, 200 | #bottomrow li #LeftAlt span, 201 | #bottomrow li #LeftCommand span { 202 | display: block; 203 | text-align: left; 204 | margin: 31px 0 0 8px; 205 | font-size: 7.5pt; 206 | text-transform: lowercase; 207 | } 208 | 209 | #bottomrow li #RightAlt span, 210 | #bottomrow li #RightCommand span { 211 | display: block; 212 | text-align: right; 213 | margin: 31px 8px 0 0; 214 | font-size: 7.5pt; 215 | text-transform: lowercase; 216 | } 217 | 218 | #bottomrow ol li #LeftArrow span, 219 | #bottomrow ol li #RightArrow span, 220 | #bottomrow ol li #UpArrow span, 221 | #bottomrow ol li #DownArrow span { 222 | display: block; 223 | margin: 9px 0 0; 224 | } 225 | 226 | .fn { 227 | height: 26px; 228 | width: 46px; 229 | } 230 | 231 | #Backspace { 232 | width: 72px; 233 | } 234 | 235 | #Tab { 236 | width: 72px; 237 | } 238 | 239 | #CapsLock { 240 | width: 85px; 241 | } 242 | 243 | #Enter { 244 | width: 85px; 245 | } 246 | 247 | .shiftleft, 248 | .shiftright { 249 | width: 112px; 250 | } 251 | 252 | #fn, 253 | #LeftControl, 254 | .option, 255 | .command, 256 | #Space { 257 | height: 49px; 258 | } 259 | 260 | #LeftControl { 261 | width: 56px; 262 | } 263 | 264 | .option { 265 | width: 46px; 266 | } 267 | 268 | .command { 269 | width: 67px; 270 | } 271 | 272 | #Space { 273 | width: 226px; 274 | } 275 | 276 | #LeftArrow img, 277 | #UpArrow img, 278 | #DownArrow img, 279 | #RightArrow img { 280 | border: none; 281 | } 282 | 283 | ul ol { 284 | list-style-type: none; 285 | } 286 | 287 | #DownArrow { 288 | height: 23px; 289 | font-size: 8px; 290 | } 291 | 292 | #UpArrow, 293 | #LeftArrow, 294 | #RightArrow { 295 | height: 24px; 296 | font-size: 8px; 297 | } 298 | 299 | #LeftArrow, 300 | #RightArrow { 301 | margin: 30px 5px 5px; 302 | } 303 | 304 | #LeftArrow:active, 305 | #RightArrow:active { 306 | margin: 32px 5px 3px; 307 | } 308 | 309 | #UpArrow { 310 | margin: 5px 5px 1px; 311 | border-bottom-right-radius: 0px; 312 | border-bottom-left-radius: 0px; 313 | } 314 | 315 | #UpArrow:active { 316 | margin: 8px 5px -2px; 317 | } 318 | 319 | #DownArrow { 320 | margin: 0 5px 5px; 321 | border-top-left-radius: 0px; 322 | border-top-right-radius: 0px; 323 | } 324 | 325 | #DownArrow:Active { 326 | margin: 3px 5px 4px; 327 | } 328 | 329 | #timesClickedKeyText { 330 | color: #333; 331 | } 332 | 333 | .disabledButton { 334 | background-color: #a4a4a4; 335 | text-decoration: line-through; 336 | color: rgb(81, 81, 81); 337 | box-shadow: none; 338 | } 339 | 340 | .disabledButton:active { 341 | background-color: #a4a4a4; 342 | text-decoration: line-through; 343 | color: rgb(81, 81, 81); 344 | box-shadow: none; 345 | } -------------------------------------------------------------------------------- /test/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Document 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 32 | 48 | 64 | 79 | 93 | 110 |
111 |

HN commenter farlington's response to the CSS challenge on this 113 | “Apple Keyboard in pure CSS” Hacker News thread.
114 | Found auditing the “Help & Inspiration” citations of Mathew Preziotte's very excellent Party Mode repo.
Originally modified from https://dl.dropbox.com/u/921159/Keyboard/page.html 117 | by probablyinteractive.com.

118 | 119 | 120 | 121 | 122 | 123 | -------------------------------------------------------------------------------- /app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Keystroke Count 9 | 10 | 11 | 12 | 13 | 14 | 15 |

Keystroke Count

16 |
17 | 18 |
19 |

Total Keys Pressed Today: 1,090,383

20 |

Total Mouse Clicks Today: 109,230,980

21 |
22 |
23 |
24 |

Your most used key today was ‘E’.

25 |

You pressed it 6,174 times

26 |
27 |
28 | 29 |
30 |
Less clicks
31 |
More clicks
32 |
33 | 34 |
35 |
36 | 37 | 38 |
39 | 55 | 71 | 87 | 102 | 116 | 133 |
134 | 135 | 136 | 137 |
138 | Keystroke Count is 100% open 139 | source. 140 | It is maintained by Virej Dasani. 141 |
142 | Report bugs and request features 143 | here 144 | (You can also open a Pull 145 | Request!) 146 |
147 | You can launch Keystroke Count with the shortcut: Alt + Shift + K and press Escape to safely close it 148 | 149 |
150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | -------------------------------------------------------------------------------- /test/style.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | body { 7 | background: #f8f8f8; 8 | } 9 | 10 | #keyboard { 11 | z-index: 20; 12 | margin: 50px auto 20px; 13 | width: 800px; 14 | height: 315px; 15 | background: #bbb; 16 | background-image: -webkit-gradient(linear, 17 | left bottom, 18 | left top, 19 | color-stop(0.27, rgb(212, 216, 219)), 20 | color-stop(0.64, rgb(213, 217, 220)), 21 | color-stop(0.95, rgb(230, 233, 235)), 22 | color-stop(1, rgb(191, 191, 191))); 23 | background-image: -moz-linear-gradient(center bottom, 24 | rgb(212, 216, 219) 27%, 25 | rgb(213, 217, 220) 64%, 26 | rgb(230, 233, 235) 95%, 27 | rgb(191, 191, 191) 100%); 28 | -moz-border-radius-topleft: 7px 21px; 29 | -moz-border-radius-topright: 7px 21px; 30 | -moz-border-radius-bottomright: 10px; 31 | -moz-border-radius-bottomleft: 10px; 32 | border-top-left-radius: 7px 21px; 33 | border-top-right-radius: 7px 21px; 34 | border-bottom-right-radius: 10px; 35 | border-bottom-left-radius: 10px; 36 | padding: 50px 0 0; 37 | -webkit-box-shadow: 38 | inset 0 0 8px #bbb, 39 | 0 1px 0 #aaa, 40 | 0 4px 0 #bbb, 41 | 0 10px 30px #ddd; 42 | -moz-box-shadow: 43 | inset 0 0 8px #bbb, 44 | 0 1px 0 #aaa, 45 | 0 4px 0 #bbb, 46 | 0 10px 30px #ddd; 47 | box-shadow: 48 | inset 0 0 8px #bbb, 49 | 0 1px 0 #aaa, 50 | 0 4px 0 #bbb, 51 | 0 10px 30px #ddd; 52 | } 53 | 54 | ul { 55 | list-style-type: none; 56 | width: 784px; 57 | margin: 0 auto; 58 | } 59 | 60 | li { 61 | float: left; 62 | } 63 | 64 | 65 | .key { 66 | display: block; 67 | color: #aaa; 68 | font: bold 9pt arial; 69 | text-decoration: none; 70 | text-align: center; 71 | width: 44px; 72 | height: 41px; 73 | margin: 5px; 74 | background: #eff0f2; 75 | -moz-border-radius: 4px; 76 | border-radius: 4px; 77 | border-top: 1px solid #f5f5f5; 78 | -webkit-box-shadow: 79 | inset 0 0 25px #e8e8e8, 80 | 0 1px 0 #c3c3c3, 81 | 0 2px 0 #c9c9c9, 82 | 0 2px 3px #333; 83 | -moz-box-shadow: 84 | inset 0 0 25px #e8e8e8, 85 | 0 1px 0 #c3c3c3, 86 | 0 2px 0 #c9c9c9, 87 | 0 2px 3px #333; 88 | box-shadow: 89 | inset 0 0 25px #e8e8e8, 90 | 0 1px 0 #c3c3c3, 91 | 0 2px 0 #c9c9c9, 92 | 0 2px 3px #333; 93 | text-shadow: 0px 1px 0px #f5f5f5; 94 | filter: dropshadow(color=#f5f5f5, offx=0, offy=1); 95 | } 96 | 97 | .key:active, 98 | .keydown { 99 | color: #888; 100 | background: #ebeced; 101 | margin: 7px 5px 3px; 102 | -webkit-box-shadow: 103 | inset 0 0 25px #ddd, 104 | 0 0 3px #333; 105 | -moz-box-shadow: 106 | inset 0 0 25px #ddd, 107 | 0 0 3px #333; 108 | box-shadow: 109 | inset 0 0 25px #ddd, 110 | 0 0 3px #333; 111 | border-top: 1px solid #eee; 112 | } 113 | 114 | .fn span { 115 | display: block; 116 | margin: 14px 5px 0 0; 117 | text-align: right; 118 | font: bold 6pt arial; 119 | text-transform: uppercase; 120 | } 121 | 122 | #esc { 123 | margin: 6px 15px 0 0; 124 | font-size: 7.5pt; 125 | text-transform: lowercase; 126 | } 127 | 128 | 129 | #numbers li a span { 130 | display: block; 131 | } 132 | 133 | #numbers li a b { 134 | margin: 3px 0 3px; 135 | display: block; 136 | } 137 | 138 | #numbers li .alt b { 139 | display: block; 140 | margin: 0 0 3px; 141 | } 142 | 143 | #numbers li #delete span { 144 | text-align: right; 145 | margin: 23px 10px 0 0; 146 | font-size: 7.5pt; 147 | text-transform: lowercase; 148 | } 149 | 150 | #qwerty li a span { 151 | display: block; 152 | margin: 13px 0 0; 153 | text-transform: uppercase; 154 | } 155 | 156 | #qwerty li #tab span { 157 | text-align: left; 158 | margin: 23px 0 0 10px; 159 | font-size: 7.5pt; 160 | text-transform: lowercase; 161 | } 162 | 163 | #qwerty li .alt b { 164 | display: block; 165 | margin: 3px 0 0; 166 | } 167 | 168 | #qwerty li .alt span { 169 | margin: 2px 0 0; 170 | } 171 | 172 | 173 | #asdfg li a span { 174 | display: block; 175 | margin: 13px 0 0; 176 | text-transform: uppercase; 177 | } 178 | 179 | #asdfg li .alt span { 180 | margin: 0; 181 | text-transform: lowercase; 182 | } 183 | 184 | #asdfg li .alt b { 185 | display: block; 186 | margin: 3px 0 0; 187 | } 188 | 189 | #asdfg li #caps b { 190 | display: block; 191 | background: #999; 192 | width: 4px; 193 | height: 4px; 194 | border-radius: 10px; 195 | margin: 9px 0 0 10px; 196 | -webkit-box-shadow: inset 0 1px 0 #666; 197 | -moz-box-shadow: inset 0 1px 0 #666; 198 | box-shadow: inset 0 1px 0 #666; 199 | } 200 | 201 | #asdfg li #caps span { 202 | text-align: left; 203 | margin: 10px 0 0 10px; 204 | font-size: 7.5pt; 205 | } 206 | 207 | #asdfg li #enter span { 208 | text-align: right; 209 | margin: 23px 10px 0 0; 210 | font-size: 7.5pt; 211 | } 212 | 213 | 214 | #zxcvb li a span { 215 | display: block; 216 | margin: 13px 0 0; 217 | text-transform: uppercase; 218 | } 219 | 220 | #zxcvb li .shiftleft span { 221 | text-align: left; 222 | margin: 23px 0 0 10px; 223 | font-size: 7.5pt; 224 | text-transform: lowercase; 225 | } 226 | 227 | #zxcvb li .shiftright span { 228 | text-align: right; 229 | margin: 23px 10px 0 0; 230 | font-size: 7.5pt; 231 | text-transform: lowercase; 232 | } 233 | 234 | #zxcvb li .alt b { 235 | display: block; 236 | margin: 4px 0 0; 237 | } 238 | 239 | #zxcvb li .alt span { 240 | margin: 0; 241 | } 242 | 243 | 244 | #bottomrow li #fn span, 245 | #bottomrow li #control span, 246 | #bottomrow li #optionleft span, 247 | #bottomrow li #commandleft span { 248 | display: block; 249 | text-align: left; 250 | margin: 31px 0 0 8px; 251 | font-size: 7.5pt; 252 | text-transform: lowercase; 253 | } 254 | 255 | #bottomrow li #optionright span, 256 | #bottomrow li #commandright span { 257 | display: block; 258 | text-align: right; 259 | margin: 31px 8px 0 0; 260 | font-size: 7.5pt; 261 | text-transform: lowercase; 262 | } 263 | 264 | #bottomrow ol li #left span, 265 | #bottomrow ol li #right span, 266 | #bottomrow ol li #up span, 267 | #bottomrow ol li #down span { 268 | display: block; 269 | margin: 9px 0 0; 270 | } 271 | 272 | .fn { 273 | height: 26px; 274 | width: 46px; 275 | } 276 | 277 | #delete { 278 | width: 72px; 279 | } 280 | 281 | #tab { 282 | width: 72px; 283 | } 284 | 285 | #caps { 286 | width: 85px; 287 | } 288 | 289 | #enter { 290 | width: 85px; 291 | } 292 | 293 | .shiftleft, 294 | .shiftright { 295 | width: 112px; 296 | } 297 | 298 | #fn, 299 | #control, 300 | .option, 301 | .command, 302 | #spacebar { 303 | height: 49px; 304 | } 305 | 306 | #control { 307 | width: 56px; 308 | } 309 | 310 | .option { 311 | width: 46px; 312 | } 313 | 314 | .command { 315 | width: 67px; 316 | } 317 | 318 | #spacebar { 319 | width: 226px; 320 | } 321 | 322 | #left img, 323 | #up img, 324 | #down img, 325 | #right img { 326 | border: none; 327 | } 328 | 329 | ul ol { 330 | list-style-type: none; 331 | } 332 | 333 | #down { 334 | height: 23px; 335 | font-size: 8px; 336 | } 337 | 338 | #up, 339 | #left, 340 | #right { 341 | height: 24px; 342 | font-size: 8px; 343 | } 344 | 345 | #left, 346 | #right { 347 | margin: 30px 5px 5px; 348 | } 349 | 350 | #left:active, 351 | #right:active { 352 | margin: 32px 5px 3px; 353 | } 354 | 355 | #up { 356 | margin: 5px 5px 1px; 357 | border-bottom-right-radius: 0px; 358 | border-bottom-left-radius: 0px; 359 | } 360 | 361 | #up:active { 362 | margin: 8px 5px -2px; 363 | } 364 | 365 | #down { 366 | margin: 0 5px 5px; 367 | border-top-left-radius: 0px; 368 | border-top-right-radius: 0px; 369 | } 370 | 371 | #down:Active { 372 | margin: 3px 5px 4px; 373 | } 374 | 375 | #paper1, 376 | #paper2, 377 | #paper3 { 378 | display: block; 379 | width: 500px; 380 | height: 200px; 381 | background: #fff; 382 | margin: 50px auto 20px; 383 | border: 1px solid #fff; 384 | padding: 20px; 385 | -webkit-box-shadow: 386 | inset 0 0 15px #f9f9f9, 387 | 0 0 5px #e5e5e5; 388 | -moz-box-shadow: 389 | inset 0 0 15px #f9f9f9, 390 | 0 0 5px #e5e5e5; 391 | box-shadow: 392 | inset 0 0 15px #f9f9f9, 393 | 0 0 5px #e5e5e5; 394 | } 395 | 396 | #paper1 { 397 | margin: 50px auto 20px; 398 | -moz-transform: rotate(-3deg); 399 | -webkit-transform: rotate(-3deg); 400 | -o-transform: rotate(-3deg); 401 | -ms-transform: rotate(-3deg); 402 | transform: rotate(-3deg); 403 | zoom: 1; 404 | } 405 | 406 | #paper2 { 407 | margin: -265px auto 0; 408 | -moz-transform: rotate(0.5deg); 409 | -webkit-transform: rotate(0.5deg); 410 | -o-transform: rotate(0.5deg); 411 | -ms-transform: rotate(0.5deg); 412 | transform: rotate(0.5deg); 413 | zoom: 1; 414 | } 415 | 416 | #paper3 { 417 | margin: -240px auto 100px; 418 | -moz-transform: rotate(-1.5deg); 419 | -webkit-transform: rotate(-1.5deg); 420 | -o-transform: rotate(-1.5deg); 421 | -me-transform: rotate(-1.5deg); 422 | transform: rotate(-1.5deg); 423 | zoom: 1; 424 | } 425 | 426 | #paper3:focus { 427 | outline: none; 428 | } -------------------------------------------------------------------------------- /app/keys.js: -------------------------------------------------------------------------------- 1 | // https://gist.github.com/rickyzhang82/8581a762c9f9fc6ddb8390872552c250 2 | // https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h 3 | // To get any other key, console.log(event) inside the ioHook.on("keydown") 4 | const keys = [ 5 | // keyName "Reserved" seems to be: '§' 6 | { 7 | keyCode: 0, 8 | keyName: "fn", 9 | timesClicked: 0, 10 | }, 11 | { 12 | keyCode: 1, 13 | keyName: "Esc", 14 | timesClicked: 0, 15 | }, 16 | { 17 | keyCode: 2, 18 | keyName: "1", 19 | timesClicked: 0, 20 | }, 21 | { 22 | keyCode: 3, 23 | keyName: "2", 24 | timesClicked: 0, 25 | }, 26 | { 27 | keyCode: 4, 28 | keyName: "3", 29 | timesClicked: 0, 30 | }, 31 | { 32 | keyCode: 5, 33 | keyName: "4", 34 | timesClicked: 0, 35 | }, 36 | { 37 | keyCode: 6, 38 | keyName: "5", 39 | timesClicked: 0, 40 | }, 41 | { 42 | keyCode: 7, 43 | keyName: "6", 44 | timesClicked: 0, 45 | }, 46 | { 47 | keyCode: 8, 48 | keyName: "7", 49 | timesClicked: 0, 50 | }, 51 | { 52 | keyCode: 9, 53 | keyName: "8", 54 | timesClicked: 0, 55 | }, 56 | { 57 | keyCode: 10, 58 | keyName: "9", 59 | timesClicked: 0, 60 | }, 61 | { 62 | keyCode: 11, 63 | keyName: "0", 64 | timesClicked: 0, 65 | }, 66 | { 67 | keyCode: 12, 68 | keyName: "-", 69 | timesClicked: 0, 70 | }, 71 | { 72 | keyCode: 13, 73 | keyName: "=", 74 | timesClicked: 0, 75 | }, 76 | { 77 | keyCode: 14, 78 | keyName: "Backspace", 79 | timesClicked: 0, 80 | }, 81 | { 82 | keyCode: 15, 83 | keyName: "Tab", 84 | timesClicked: 0, 85 | }, 86 | { 87 | keyCode: 16, 88 | keyName: "Q", 89 | timesClicked: 0, 90 | }, 91 | { 92 | keyCode: 17, 93 | keyName: "W", 94 | timesClicked: 0, 95 | }, 96 | { 97 | keyCode: 18, 98 | keyName: "E", 99 | timesClicked: 0, 100 | }, 101 | { 102 | keyCode: 19, 103 | keyName: "R", 104 | timesClicked: 0, 105 | }, 106 | { 107 | keyCode: 20, 108 | keyName: "T", 109 | timesClicked: 0, 110 | }, 111 | { 112 | keyCode: 21, 113 | keyName: "Y", 114 | timesClicked: 0, 115 | }, 116 | { 117 | keyCode: 22, 118 | keyName: "U", 119 | timesClicked: 0, 120 | }, 121 | { 122 | keyCode: 23, 123 | keyName: "I", 124 | timesClicked: 0, 125 | }, 126 | { 127 | keyCode: 24, 128 | keyName: "O", 129 | timesClicked: 0, 130 | }, 131 | { 132 | keyCode: 25, 133 | keyName: "P", 134 | timesClicked: 0, 135 | }, 136 | { 137 | keyCode: 26, 138 | keyName: "{", 139 | timesClicked: 0, 140 | }, 141 | { 142 | keyCode: 27, 143 | keyName: "}", 144 | timesClicked: 0, 145 | }, 146 | { 147 | keyCode: 28, 148 | keyName: "Enter", 149 | timesClicked: 0, 150 | }, 151 | { 152 | keyCode: 29, 153 | keyName: "LeftControl", 154 | timesClicked: 0, 155 | }, 156 | { 157 | keyCode: 30, 158 | keyName: "A", 159 | timesClicked: 0, 160 | }, 161 | { 162 | keyCode: 31, 163 | keyName: "S", 164 | timesClicked: 0, 165 | }, 166 | { 167 | keyCode: 32, 168 | keyName: "D", 169 | timesClicked: 0, 170 | }, 171 | { 172 | keyCode: 33, 173 | keyName: "F", 174 | timesClicked: 0, 175 | }, 176 | { 177 | keyCode: 34, 178 | keyName: "G", 179 | timesClicked: 0, 180 | }, 181 | { 182 | keyCode: 35, 183 | keyName: "H", 184 | timesClicked: 0, 185 | }, 186 | { 187 | keyCode: 36, 188 | keyName: "J", 189 | timesClicked: 0, 190 | }, 191 | { 192 | keyCode: 37, 193 | keyName: "K", 194 | timesClicked: 0, 195 | }, 196 | { 197 | keyCode: 38, 198 | keyName: "L", 199 | timesClicked: 0, 200 | }, 201 | { 202 | keyCode: 39, 203 | keyName: ";", 204 | timesClicked: 0, 205 | }, 206 | { 207 | keyCode: 40, 208 | keyName: "'", 209 | timesClicked: 0, 210 | }, 211 | { 212 | keyCode: 41, 213 | keyName: "`", 214 | timesClicked: 0, 215 | }, 216 | { 217 | keyCode: 42, 218 | keyName: "LeftShift", 219 | timesClicked: 0, 220 | }, 221 | { 222 | keyCode: 43, 223 | keyName: "\\", 224 | timesClicked: 0, 225 | }, 226 | { 227 | keyCode: 44, 228 | keyName: "Z", 229 | timesClicked: 0, 230 | }, 231 | { 232 | keyCode: 45, 233 | keyName: "X", 234 | timesClicked: 0, 235 | }, 236 | { 237 | keyCode: 46, 238 | keyName: "C", 239 | timesClicked: 0, 240 | }, 241 | { 242 | keyCode: 47, 243 | keyName: "V", 244 | timesClicked: 0, 245 | }, 246 | { 247 | keyCode: 48, 248 | keyName: "B", 249 | timesClicked: 0, 250 | }, 251 | { 252 | keyCode: 49, 253 | keyName: "N", 254 | timesClicked: 0, 255 | }, 256 | { 257 | keyCode: 50, 258 | keyName: "M", 259 | timesClicked: 0, 260 | }, 261 | { 262 | keyCode: 51, 263 | keyName: ",", 264 | timesClicked: 0, 265 | }, 266 | { 267 | keyCode: 52, 268 | keyName: ".", 269 | timesClicked: 0, 270 | }, 271 | { 272 | keyCode: 53, 273 | keyName: "/", 274 | timesClicked: 0, 275 | }, 276 | { 277 | keyCode: 54, 278 | keyName: "RightShift", 279 | timesClicked: 0, 280 | }, 281 | { 282 | keyCode: 55, 283 | keyName: "KP*", 284 | timesClicked: 0, 285 | }, 286 | { 287 | keyCode: 56, 288 | keyName: "LeftAlt", 289 | timesClicked: 0, 290 | }, 291 | { 292 | keyCode: 57, 293 | keyName: "Space", 294 | timesClicked: 0, 295 | }, 296 | { 297 | keyCode: 58, 298 | keyName: "CapsLock", 299 | timesClicked: 0, 300 | }, 301 | { 302 | keyCode: 59, 303 | keyName: "F1", 304 | timesClicked: 0, 305 | }, 306 | { 307 | keyCode: 60, 308 | keyName: "F2", 309 | timesClicked: 0, 310 | }, 311 | { 312 | keyCode: 61, 313 | keyName: "F3", 314 | timesClicked: 0, 315 | }, 316 | { 317 | keyCode: 62, 318 | keyName: "F4", 319 | timesClicked: 0, 320 | }, 321 | { 322 | keyCode: 63, 323 | keyName: "F5", 324 | timesClicked: 0, 325 | }, 326 | { 327 | keyCode: 64, 328 | keyName: "F6", 329 | timesClicked: 0, 330 | }, 331 | { 332 | keyCode: 65, 333 | keyName: "F7", 334 | timesClicked: 0, 335 | }, 336 | { 337 | keyCode: 66, 338 | keyName: "F8", 339 | timesClicked: 0, 340 | }, 341 | { 342 | keyCode: 67, 343 | keyName: "F9", 344 | timesClicked: 0, 345 | }, 346 | { 347 | keyCode: 68, 348 | keyName: "F10", 349 | timesClicked: 0, 350 | }, 351 | { 352 | keyCode: 69, 353 | keyName: "NumLock", 354 | timesClicked: 0, 355 | }, 356 | { 357 | keyCode: 70, 358 | keyName: "ScrollLock", 359 | timesClicked: 0, 360 | }, 361 | { 362 | keyCode: 71, 363 | keyName: "7 KP", 364 | timesClicked: 0, 365 | }, 366 | { 367 | keyCode: 72, 368 | keyName: "8 KP", 369 | timesClicked: 0, 370 | }, 371 | { 372 | keyCode: 73, 373 | keyName: "9 KP", 374 | timesClicked: 0, 375 | }, 376 | { 377 | keyCode: 74, 378 | keyName: "- KP", 379 | timesClicked: 0, 380 | }, 381 | { 382 | keyCode: 75, 383 | keyName: "4 KP", 384 | timesClicked: 0, 385 | }, 386 | { 387 | keyCode: 76, 388 | keyName: "5 KP", 389 | timesClicked: 0, 390 | }, 391 | { 392 | keyCode: 77, 393 | keyName: "6 KP", 394 | timesClicked: 0, 395 | }, 396 | { 397 | keyCode: 78, 398 | keyName: "+ KP", 399 | timesClicked: 0, 400 | }, 401 | { 402 | keyCode: 79, 403 | keyName: "1 KP", 404 | timesClicked: 0, 405 | }, 406 | { 407 | keyCode: 80, 408 | keyName: "2 KP", 409 | timesClicked: 0, 410 | }, 411 | { 412 | keyCode: 81, 413 | keyName: "3 KP", 414 | timesClicked: 0, 415 | }, 416 | { 417 | keyCode: 82, 418 | keyName: "0 KP", 419 | timesClicked: 0, 420 | }, 421 | { 422 | keyCode: 83, 423 | keyName: ". KP", 424 | timesClicked: 0, 425 | }, 426 | // For some reason keyCode 84, this doesn't exists 427 | // { 428 | // keyCode: 84, 429 | // keyName: "", 430 | // timesClicked: 0, 431 | // }, 432 | { 433 | keyCode: 85, 434 | keyName: "KEY_ZENKAKUHANKAKU", 435 | timesClicked: 0, 436 | }, 437 | { 438 | keyCode: 86, 439 | keyName: "KEY_102ND", 440 | timesClicked: 0, 441 | }, 442 | { 443 | keyCode: 87, 444 | keyName: "F11", 445 | timesClicked: 0, 446 | }, 447 | { 448 | keyCode: 88, 449 | keyName: "F12", 450 | timesClicked: 0, 451 | }, 452 | { 453 | keyCode: 89, 454 | keyName: "KEY_RO", 455 | timesClicked: 0, 456 | }, 457 | { 458 | keyCode: 90, 459 | keyName: "KEY_KATAKANA", 460 | timesClicked: 0, 461 | }, 462 | { 463 | keyCode: 91, 464 | keyName: "KEY_HIRAGANA", 465 | timesClicked: 0, 466 | }, 467 | { 468 | keyCode: 92, 469 | keyName: "KEY_HENKAN", 470 | timesClicked: 0, 471 | }, 472 | { 473 | keyCode: 93, 474 | keyName: "KEY_KATAKANAHIRAGANA", 475 | timesClicked: 0, 476 | }, 477 | { 478 | keyCode: 94, 479 | keyName: "KEY_MUHENKAN", 480 | timesClicked: 0, 481 | }, 482 | { 483 | keyCode: 95, 484 | keyName: "KEY_KPJPCOMMA", 485 | timesClicked: 0, 486 | }, 487 | { 488 | keyCode: 96, 489 | keyName: "KEY_KPENTER", 490 | timesClicked: 0, 491 | }, 492 | { 493 | keyCode: 97, 494 | keyName: "RightControl", 495 | timesClicked: 0, 496 | }, 497 | { 498 | keyCode: 98, 499 | keyName: "/ KP", 500 | timesClicked: 0, 501 | }, 502 | { 503 | keyCode: 99, 504 | keyName: "KEY_SYSRQ", 505 | timesClicked: 0, 506 | }, 507 | { 508 | keyCode: 100, 509 | keyName: "RightAlt", 510 | timesClicked: 0, 511 | }, 512 | // This is the same code as above because MacOS and Windows have some difference 513 | { 514 | keyCode: 3640, 515 | keyName: "RightAlt", 516 | timesClicked: 0, 517 | }, 518 | { 519 | keyCode: 101, 520 | keyName: "LineFeed", 521 | timesClicked: 0, 522 | }, 523 | { 524 | keyCode: 102, 525 | keyName: "Home", 526 | timesClicked: 0, 527 | }, 528 | { 529 | keyCode: 103, 530 | keyName: "UpArrow", 531 | timesClicked: 0, 532 | }, 533 | // Same keyName different keyCode is because of the difference between MacOS and Windows 534 | { 535 | keyCode: 57416, 536 | keyName: "UpArrow", 537 | timesClicked: 0, 538 | }, 539 | { 540 | keyCode: 104, 541 | keyName: "PageUp", 542 | timesClicked: 0, 543 | }, 544 | { 545 | keyCode: 105, 546 | keyName: "LeftArrow", 547 | timesClicked: 0, 548 | }, 549 | { 550 | keyCode: 57419, 551 | keyName: "LeftArrow", 552 | timesClicked: 0, 553 | }, 554 | { 555 | keyCode: 106, 556 | keyName: "RightArrow", 557 | timesClicked: 0, 558 | }, 559 | { 560 | keyCode: 57421, 561 | keyName: "RightArrow", 562 | timesClicked: 0, 563 | }, 564 | { 565 | keyCode: 107, 566 | keyName: "End", 567 | timesClicked: 0, 568 | }, 569 | { 570 | keyCode: 108, 571 | keyName: "DownArrow", 572 | timesClicked: 0, 573 | }, 574 | { 575 | keyCode: 57424, 576 | keyName: "DownArrow", 577 | timesClicked: 0, 578 | }, 579 | { 580 | keyCode: 109, 581 | keyName: "PageDown", 582 | timesClicked: 0, 583 | }, 584 | { 585 | keyCode: 110, 586 | keyName: "Insert", 587 | timesClicked: 0, 588 | }, 589 | { 590 | keyCode: 111, 591 | keyName: "Delete", 592 | timesClicked: 0, 593 | }, 594 | { 595 | keyCode: 112, 596 | keyName: "Macro", 597 | timesClicked: 0, 598 | }, 599 | { 600 | keyCode: 113, 601 | keyName: "Mute", 602 | timesClicked: 0, 603 | }, 604 | { 605 | keyCode: 114, 606 | keyName: "VolumeDown", 607 | timesClicked: 0, 608 | }, 609 | { 610 | keyCode: 115, 611 | keyName: "VolumeUp", 612 | timesClicked: 0, 613 | }, 614 | { 615 | keyCode: 3675, 616 | keyName: "LeftCommand", 617 | timesClicked: 0, 618 | }, 619 | { 620 | keyCode: 3676, 621 | keyName: "RightCommand", 622 | timesClicked: 0, 623 | }, 624 | // The keyCode for leftClick is randomly chosen by me 625 | { 626 | keyCode: 134761167, 627 | keyName: "leftClick", 628 | timesClicked: 0, 629 | }, 630 | // The keyCode for rightClick is randomly chosen by me 631 | { 632 | keyCode: 4164761167, 633 | keyName: "rightClick", 634 | timesClicked: 0, 635 | }, 636 | ]; 637 | -------------------------------------------------------------------------------- /docs/style.css: -------------------------------------------------------------------------------- 1 | /* normalize.css v3.0.2 | MIT License | git.io/normalize */ 2 | @import url("https://fonts.googleapis.com/css?family=Open+Sans:400,700"); 3 | 4 | html { 5 | font-family: sans-serif; 6 | -ms-text-size-adjust: 100%; 7 | -webkit-text-size-adjust: 100% 8 | } 9 | 10 | body { 11 | margin: 0 12 | } 13 | 14 | article, 15 | aside, 16 | details, 17 | figcaption, 18 | figure, 19 | footer, 20 | header, 21 | hgroup, 22 | main, 23 | menu, 24 | nav, 25 | section, 26 | summary { 27 | display: block 28 | } 29 | 30 | audio, 31 | canvas, 32 | progress, 33 | video { 34 | display: inline-block; 35 | vertical-align: baseline 36 | } 37 | 38 | audio:not([controls]) { 39 | display: none; 40 | height: 0 41 | } 42 | 43 | [hidden], 44 | template { 45 | display: none 46 | } 47 | 48 | a { 49 | background-color: transparent 50 | } 51 | 52 | a:active, 53 | a:hover { 54 | outline: 0 55 | } 56 | 57 | abbr[title] { 58 | border-bottom: 1px dotted 59 | } 60 | 61 | b, 62 | strong { 63 | font-weight: bold 64 | } 65 | 66 | dfn { 67 | font-style: italic 68 | } 69 | 70 | h1 { 71 | font-size: 2em; 72 | margin: 0.67em 0 73 | } 74 | 75 | mark { 76 | background: #ff0; 77 | color: #000 78 | } 79 | 80 | small { 81 | font-size: 80% 82 | } 83 | 84 | sub, 85 | sup { 86 | font-size: 75%; 87 | line-height: 0; 88 | position: relative; 89 | vertical-align: baseline 90 | } 91 | 92 | sup { 93 | top: -0.5em 94 | } 95 | 96 | sub { 97 | bottom: -0.25em 98 | } 99 | 100 | img { 101 | border: 0 102 | } 103 | 104 | svg:not(:root) { 105 | overflow: hidden 106 | } 107 | 108 | figure { 109 | margin: 1em 40px 110 | } 111 | 112 | hr { 113 | box-sizing: content-box; 114 | height: 0 115 | } 116 | 117 | pre { 118 | overflow: auto 119 | } 120 | 121 | code, 122 | kbd, 123 | pre, 124 | samp { 125 | font-family: monospace, monospace; 126 | font-size: 1em 127 | } 128 | 129 | button, 130 | input, 131 | optgroup, 132 | select, 133 | textarea { 134 | color: inherit; 135 | font: inherit; 136 | margin: 0 137 | } 138 | 139 | button { 140 | overflow: visible 141 | } 142 | 143 | button, 144 | select { 145 | text-transform: none 146 | } 147 | 148 | button, 149 | html input[type="button"], 150 | input[type="reset"], 151 | input[type="submit"] { 152 | -webkit-appearance: button; 153 | cursor: pointer 154 | } 155 | 156 | button[disabled], 157 | html input[disabled] { 158 | cursor: default 159 | } 160 | 161 | button::-moz-focus-inner, 162 | input::-moz-focus-inner { 163 | border: 0; 164 | padding: 0 165 | } 166 | 167 | input { 168 | line-height: normal 169 | } 170 | 171 | input[type="checkbox"], 172 | input[type="radio"] { 173 | box-sizing: border-box; 174 | padding: 0 175 | } 176 | 177 | input[type="number"]::-webkit-inner-spin-button, 178 | input[type="number"]::-webkit-outer-spin-button { 179 | height: auto 180 | } 181 | 182 | input[type="search"] { 183 | -webkit-appearance: textfield; 184 | box-sizing: content-box 185 | } 186 | 187 | input[type="search"]::-webkit-search-cancel-button, 188 | input[type="search"]::-webkit-search-decoration { 189 | -webkit-appearance: none 190 | } 191 | 192 | fieldset { 193 | border: 1px solid #c0c0c0; 194 | margin: 0 2px; 195 | padding: 0.35em 0.625em 0.75em 196 | } 197 | 198 | legend { 199 | border: 0; 200 | padding: 0 201 | } 202 | 203 | textarea { 204 | overflow: auto 205 | } 206 | 207 | optgroup { 208 | font-weight: bold 209 | } 210 | 211 | table { 212 | border-collapse: collapse; 213 | border-spacing: 0 214 | } 215 | 216 | td, 217 | th { 218 | padding: 0 219 | } 220 | 221 | .highlight table td { 222 | padding: 5px 223 | } 224 | 225 | .highlight table pre { 226 | margin: 0 227 | } 228 | 229 | .highlight .cm { 230 | color: #999988; 231 | font-style: italic 232 | } 233 | 234 | .highlight .cp { 235 | color: #999999; 236 | font-weight: bold 237 | } 238 | 239 | .highlight .c1 { 240 | color: #999988; 241 | font-style: italic 242 | } 243 | 244 | .highlight .cs { 245 | color: #999999; 246 | font-weight: bold; 247 | font-style: italic 248 | } 249 | 250 | .highlight .c, 251 | .highlight .cd { 252 | color: #999988; 253 | font-style: italic 254 | } 255 | 256 | .highlight .err { 257 | color: #a61717; 258 | background-color: #e3d2d2 259 | } 260 | 261 | .highlight .gd { 262 | color: #000000; 263 | background-color: #ffdddd 264 | } 265 | 266 | .highlight .ge { 267 | color: #000000; 268 | font-style: italic 269 | } 270 | 271 | .highlight .gr { 272 | color: #aa0000 273 | } 274 | 275 | .highlight .gh { 276 | color: #999999 277 | } 278 | 279 | .highlight .gi { 280 | color: #000000; 281 | background-color: #ddffdd 282 | } 283 | 284 | .highlight .go { 285 | color: #888888 286 | } 287 | 288 | .highlight .gp { 289 | color: #555555 290 | } 291 | 292 | .highlight .gs { 293 | font-weight: bold 294 | } 295 | 296 | .highlight .gu { 297 | color: #aaaaaa 298 | } 299 | 300 | .highlight .gt { 301 | color: #aa0000 302 | } 303 | 304 | .highlight .kc { 305 | color: #000000; 306 | font-weight: bold 307 | } 308 | 309 | .highlight .kd { 310 | color: #000000; 311 | font-weight: bold 312 | } 313 | 314 | .highlight .kn { 315 | color: #000000; 316 | font-weight: bold 317 | } 318 | 319 | .highlight .kp { 320 | color: #000000; 321 | font-weight: bold 322 | } 323 | 324 | .highlight .kr { 325 | color: #000000; 326 | font-weight: bold 327 | } 328 | 329 | .highlight .kt { 330 | color: #445588; 331 | font-weight: bold 332 | } 333 | 334 | .highlight .k, 335 | .highlight .kv { 336 | color: #000000; 337 | font-weight: bold 338 | } 339 | 340 | .highlight .mf { 341 | color: #009999 342 | } 343 | 344 | .highlight .mh { 345 | color: #009999 346 | } 347 | 348 | .highlight .il { 349 | color: #009999 350 | } 351 | 352 | .highlight .mi { 353 | color: #009999 354 | } 355 | 356 | .highlight .mo { 357 | color: #009999 358 | } 359 | 360 | .highlight .m, 361 | .highlight .mb, 362 | .highlight .mx { 363 | color: #009999 364 | } 365 | 366 | .highlight .sb { 367 | color: #d14 368 | } 369 | 370 | .highlight .sc { 371 | color: #d14 372 | } 373 | 374 | .highlight .sd { 375 | color: #d14 376 | } 377 | 378 | .highlight .s2 { 379 | color: #d14 380 | } 381 | 382 | .highlight .se { 383 | color: #d14 384 | } 385 | 386 | .highlight .sh { 387 | color: #d14 388 | } 389 | 390 | .highlight .si { 391 | color: #d14 392 | } 393 | 394 | .highlight .sx { 395 | color: #d14 396 | } 397 | 398 | .highlight .sr { 399 | color: #009926 400 | } 401 | 402 | .highlight .s1 { 403 | color: #d14 404 | } 405 | 406 | .highlight .ss { 407 | color: #990073 408 | } 409 | 410 | .highlight .s { 411 | color: #d14 412 | } 413 | 414 | .highlight .na { 415 | color: #008080 416 | } 417 | 418 | .highlight .bp { 419 | color: #999999 420 | } 421 | 422 | .highlight .nb { 423 | color: #0086B3 424 | } 425 | 426 | .highlight .nc { 427 | color: #445588; 428 | font-weight: bold 429 | } 430 | 431 | .highlight .no { 432 | color: #008080 433 | } 434 | 435 | .highlight .nd { 436 | color: #3c5d5d; 437 | font-weight: bold 438 | } 439 | 440 | .highlight .ni { 441 | color: #800080 442 | } 443 | 444 | .highlight .ne { 445 | color: #990000; 446 | font-weight: bold 447 | } 448 | 449 | .highlight .nf { 450 | color: #990000; 451 | font-weight: bold 452 | } 453 | 454 | .highlight .nl { 455 | color: #990000; 456 | font-weight: bold 457 | } 458 | 459 | .highlight .nn { 460 | color: #555555 461 | } 462 | 463 | .highlight .nt { 464 | color: #000080 465 | } 466 | 467 | .highlight .vc { 468 | color: #008080 469 | } 470 | 471 | .highlight .vg { 472 | color: #008080 473 | } 474 | 475 | .highlight .vi { 476 | color: #008080 477 | } 478 | 479 | .highlight .nv { 480 | color: #008080 481 | } 482 | 483 | .highlight .ow { 484 | color: #000000; 485 | font-weight: bold 486 | } 487 | 488 | .highlight .o { 489 | color: #000000; 490 | font-weight: bold 491 | } 492 | 493 | .highlight .w { 494 | color: #bbbbbb 495 | } 496 | 497 | .highlight { 498 | background-color: #f8f8f8 499 | } 500 | 501 | * { 502 | box-sizing: border-box 503 | } 504 | 505 | body { 506 | padding: 0; 507 | margin: 0; 508 | font-family: "Open Sans", "Helvetica Neue", Helvetica, Arial, sans-serif; 509 | font-size: 16px; 510 | line-height: 1.5; 511 | color: #606c71 512 | } 513 | 514 | a { 515 | color: #1e6bb8; 516 | text-decoration: none 517 | } 518 | 519 | a:hover { 520 | text-decoration: underline 521 | } 522 | 523 | .btn { 524 | display: inline-block; 525 | margin-bottom: 1rem; 526 | color: rgba(255, 255, 255, 0.7); 527 | background-color: rgba(255, 255, 255, 0.08); 528 | border-color: rgba(255, 255, 255, 0.2); 529 | border-style: solid; 530 | border-width: 1px; 531 | border-radius: 0.3rem; 532 | transition: color 0.2s, background-color 0.2s, border-color 0.2s 533 | } 534 | 535 | .btn:hover { 536 | color: rgba(255, 255, 255, 0.8); 537 | text-decoration: none; 538 | background-color: rgba(255, 255, 255, 0.2); 539 | border-color: rgba(255, 255, 255, 0.3) 540 | } 541 | 542 | .btn+.btn { 543 | margin-left: 1rem 544 | } 545 | 546 | @media screen and (min-width: 64em) { 547 | .btn { 548 | padding: 0.75rem 1rem 549 | } 550 | } 551 | 552 | @media screen and (min-width: 42em) and (max-width: 64em) { 553 | .btn { 554 | padding: 0.6rem 0.9rem; 555 | font-size: 0.9rem 556 | } 557 | } 558 | 559 | @media screen and (max-width: 42em) { 560 | .btn { 561 | display: block; 562 | width: 100%; 563 | padding: 0.75rem; 564 | font-size: 0.9rem 565 | } 566 | 567 | .btn+.btn { 568 | margin-top: 1rem; 569 | margin-left: 0 570 | } 571 | } 572 | 573 | .page-header { 574 | color: #fff; 575 | text-align: center; 576 | background-color: #159957; 577 | background-image: linear-gradient(120deg, #155799, #159957) 578 | } 579 | 580 | @media screen and (min-width: 64em) { 581 | .page-header { 582 | padding: 5rem 6rem 583 | } 584 | } 585 | 586 | @media screen and (min-width: 42em) and (max-width: 64em) { 587 | .page-header { 588 | padding: 3rem 4rem 589 | } 590 | } 591 | 592 | @media screen and (max-width: 42em) { 593 | .page-header { 594 | padding: 2rem 1rem 595 | } 596 | } 597 | 598 | .project-name { 599 | margin-top: 0; 600 | margin-bottom: 0.1rem 601 | } 602 | 603 | @media screen and (min-width: 64em) { 604 | .project-name { 605 | font-size: 3.25rem 606 | } 607 | } 608 | 609 | @media screen and (min-width: 42em) and (max-width: 64em) { 610 | .project-name { 611 | font-size: 2.25rem 612 | } 613 | } 614 | 615 | @media screen and (max-width: 42em) { 616 | .project-name { 617 | font-size: 1.75rem 618 | } 619 | } 620 | 621 | .project-tagline { 622 | margin-bottom: 2rem; 623 | font-weight: normal; 624 | opacity: 0.7 625 | } 626 | 627 | @media screen and (min-width: 64em) { 628 | .project-tagline { 629 | font-size: 1.25rem 630 | } 631 | } 632 | 633 | @media screen and (min-width: 42em) and (max-width: 64em) { 634 | .project-tagline { 635 | font-size: 1.15rem 636 | } 637 | } 638 | 639 | @media screen and (max-width: 42em) { 640 | .project-tagline { 641 | font-size: 1rem 642 | } 643 | } 644 | 645 | .main-content { 646 | word-wrap: break-word 647 | } 648 | 649 | .main-content :first-child { 650 | margin-top: 0 651 | } 652 | 653 | @media screen and (min-width: 64em) { 654 | .main-content { 655 | max-width: 64rem; 656 | padding: 2rem 6rem; 657 | margin: 0 auto; 658 | font-size: 1.1rem 659 | } 660 | } 661 | 662 | @media screen and (min-width: 42em) and (max-width: 64em) { 663 | .main-content { 664 | padding: 2rem 4rem; 665 | font-size: 1.1rem 666 | } 667 | } 668 | 669 | @media screen and (max-width: 42em) { 670 | .main-content { 671 | padding: 2rem 1rem; 672 | font-size: 1rem 673 | } 674 | } 675 | 676 | .main-content img { 677 | max-width: 100% 678 | } 679 | 680 | .main-content h1, 681 | .main-content h2, 682 | .main-content h3, 683 | .main-content h4, 684 | .main-content h5, 685 | .main-content h6 { 686 | margin-top: 2rem; 687 | margin-bottom: 1rem; 688 | font-weight: normal; 689 | color: #159957 690 | } 691 | 692 | .main-content p { 693 | margin-bottom: 1em 694 | } 695 | 696 | .main-content code { 697 | padding: 2px 4px; 698 | font-family: Consolas, "Liberation Mono", Menlo, Courier, monospace; 699 | font-size: 0.9rem; 700 | color: #567482; 701 | background-color: #f3f6fa; 702 | border-radius: 0.3rem 703 | } 704 | 705 | .main-content pre { 706 | padding: 0.8rem; 707 | margin-top: 0; 708 | margin-bottom: 1rem; 709 | font: 1rem Consolas, "Liberation Mono", Menlo, Courier, monospace; 710 | color: #567482; 711 | word-wrap: normal; 712 | background-color: #f3f6fa; 713 | border: solid 1px #dce6f0; 714 | border-radius: 0.3rem 715 | } 716 | 717 | .main-content pre>code { 718 | padding: 0; 719 | margin: 0; 720 | font-size: 0.9rem; 721 | color: #567482; 722 | word-break: normal; 723 | white-space: pre; 724 | background: transparent; 725 | border: 0 726 | } 727 | 728 | .main-content .highlight { 729 | margin-bottom: 1rem 730 | } 731 | 732 | .main-content .highlight pre { 733 | margin-bottom: 0; 734 | word-break: normal 735 | } 736 | 737 | .main-content .highlight pre, 738 | .main-content pre { 739 | padding: 0.8rem; 740 | overflow: auto; 741 | font-size: 0.9rem; 742 | line-height: 1.45; 743 | border-radius: 0.3rem; 744 | -webkit-overflow-scrolling: touch 745 | } 746 | 747 | .main-content pre code, 748 | .main-content pre tt { 749 | display: inline; 750 | max-width: initial; 751 | padding: 0; 752 | margin: 0; 753 | overflow: initial; 754 | line-height: inherit; 755 | word-wrap: normal; 756 | background-color: transparent; 757 | border: 0 758 | } 759 | 760 | .main-content pre code:before, 761 | .main-content pre code:after, 762 | .main-content pre tt:before, 763 | .main-content pre tt:after { 764 | content: normal 765 | } 766 | 767 | .main-content ul, 768 | .main-content ol { 769 | margin-top: 0 770 | } 771 | 772 | .main-content blockquote { 773 | padding: 0 1rem; 774 | margin-left: 0; 775 | color: #819198; 776 | border-left: 0.3rem solid #dce6f0 777 | } 778 | 779 | .main-content blockquote>:first-child { 780 | margin-top: 0 781 | } 782 | 783 | .main-content blockquote>:last-child { 784 | margin-bottom: 0 785 | } 786 | 787 | .main-content table { 788 | display: block; 789 | width: 100%; 790 | overflow: auto; 791 | word-break: normal; 792 | word-break: keep-all; 793 | -webkit-overflow-scrolling: touch 794 | } 795 | 796 | .main-content table th { 797 | font-weight: bold 798 | } 799 | 800 | .main-content table th, 801 | .main-content table td { 802 | padding: 0.5rem 1rem; 803 | border: 1px solid #e9ebec 804 | } 805 | 806 | .main-content dl { 807 | padding: 0 808 | } 809 | 810 | .main-content dl dt { 811 | padding: 0; 812 | margin-top: 1rem; 813 | font-size: 1rem; 814 | font-weight: bold 815 | } 816 | 817 | .main-content dl dd { 818 | padding: 0; 819 | margin-bottom: 1rem 820 | } 821 | 822 | .main-content hr { 823 | height: 2px; 824 | padding: 0; 825 | margin: 1rem 0; 826 | background-color: #eff0f1; 827 | border: 0 828 | } 829 | 830 | .site-footer { 831 | padding-top: 2rem; 832 | margin-top: 2rem; 833 | border-top: solid 1px #eff0f1 834 | } 835 | 836 | @media screen and (min-width: 64em) { 837 | .site-footer { 838 | font-size: 1rem 839 | } 840 | } 841 | 842 | @media screen and (min-width: 42em) and (max-width: 64em) { 843 | .site-footer { 844 | font-size: 1rem 845 | } 846 | } 847 | 848 | @media screen and (max-width: 42em) { 849 | .site-footer { 850 | font-size: 0.9rem 851 | } 852 | } 853 | 854 | .site-footer-owner { 855 | display: block; 856 | font-weight: bold 857 | } 858 | 859 | .site-footer-credits { 860 | color: #819198 861 | } --------------------------------------------------------------------------------