├── .gitignore ├── .prettierrc.json ├── .vscode ├── commands.json ├── launch.json └── settings.json ├── LICENSE ├── README.md ├── demo ├── .keep ├── demo.md └── demo.ts ├── docs └── demo.gif ├── extension-vscode ├── .vscodeignore ├── CHANGELOG.md ├── README.md ├── docs │ ├── logo.png │ └── logo.svg ├── package.json ├── src │ ├── Config.ts │ ├── Server.ts │ ├── WebViews.ts │ ├── extension.ts │ ├── openBrowserView.ts │ └── types.d.ts ├── tsconfig.json ├── webpack.config.js ├── webpack.config.ts └── yarn.lock ├── frontend ├── data │ ├── extract-from-cldr.ts │ ├── extract.ts │ ├── fix_deo_neo.ts │ ├── functional-layouts │ │ ├── de.json │ │ ├── de_neo.json │ │ └── us.json │ └── physical-layouts │ │ ├── ansi.drawio.xml │ │ ├── ansi.json │ │ ├── extract-from-drawio.ts │ │ ├── iso.drawio.xml │ │ └── iso.json ├── index.d.ts ├── index.js ├── package.json ├── src │ ├── Components │ │ ├── AutoResize.tsx │ │ ├── GUI.tsx │ │ ├── KeyComponent.tsx │ │ ├── KeyboardComponent.tsx │ │ ├── ResizeObserver.d.ts │ │ └── Select.tsx │ ├── Model │ │ ├── Config.ts │ │ ├── Keyboard │ │ │ ├── FunctionalLayout.ts │ │ │ ├── FunctionalLayoutsProvider.ts │ │ │ ├── Keyboard.ts │ │ │ ├── PhysicalLayout.ts │ │ │ ├── PhysicalLayoutsProvider.ts │ │ │ ├── VirtualKey.ts │ │ │ ├── index.ts │ │ │ └── primitives.ts │ │ ├── Model.ts │ │ ├── ServerConnectionController.ts │ │ ├── UrlQueryController.ts │ │ ├── WindowKeyHandler.ts │ │ ├── alternative-scancodes.md │ │ ├── index.ts │ │ └── keybindings │ │ │ ├── KeyBindingsProvider.ts │ │ │ ├── index.ts │ │ │ └── vscode-data │ │ │ ├── linux.ts │ │ │ ├── mac.ts │ │ │ └── win.ts │ ├── index.tsx │ ├── std │ │ ├── DragBehavior.ts │ │ ├── Point.ts │ │ └── utils.ts │ ├── style.scss │ └── types.d.ts ├── tsconfig.json ├── webpack.config.js └── webpack.config.ts ├── key-listener-cli ├── package.json ├── src │ ├── KeyboardHook.ts │ ├── Server.ts │ ├── api.ts │ ├── contract.ts │ └── index.ts └── tsconfig.json ├── package.json ├── tslint.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | *.pid.lock 11 | 12 | # Directory for instrumented libs generated by jscoverage/JSCover 13 | lib-cov 14 | 15 | # Coverage directory used by tools like istanbul 16 | coverage 17 | 18 | # nyc test coverage 19 | .nyc_output 20 | 21 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 22 | .grunt 23 | 24 | # node-waf configuration 25 | .lock-wscript 26 | 27 | # Compiled binary addons (http://nodejs.org/api/addons.html) 28 | build/Release 29 | 30 | # Dependency directories 31 | node_modules 32 | jspm_packages 33 | 34 | # Optional npm cache directory 35 | .npm 36 | 37 | # Optional REPL history 38 | .node_repl_history 39 | 40 | api 41 | 42 | dist -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "tabWidth": 4, 4 | "semi": true, 5 | "useTabs": true, 6 | "printWidth": 100 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "commands": [ 3 | { 4 | "command": "commands.refresh", 5 | "text": "$(sync)", 6 | "tooltip": "Refresh commands", 7 | "color": "#FFCC00" 8 | } 9 | ] 10 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Launch", 11 | "program": "${workspaceFolder}/key-listener-cli/dist/index", 12 | "skipFiles": ["/**"] 13 | }, 14 | { 15 | "type": "chrome", 16 | "request": "launch", 17 | "name": "Launch Chrome against localhost", 18 | "url": "http://localhost:8080", 19 | "webRoot": "${workspaceFolder}/frontend" 20 | }, 21 | { 22 | "name": "Run Extension", 23 | "type": "extensionHost", 24 | "request": "launch", 25 | "runtimeExecutable": "${execPath}", 26 | "args": [ 27 | "--extensionDevelopmentPath=${workspaceFolder}/extension-vscode", 28 | "${workspaceFolder}\\demo" 29 | ], 30 | "env": { 31 | "HOT_RELOAD": "", 32 | "USE_DEV_UI": "" 33 | }, 34 | "outFiles": ["${workspaceFolder}/extension-vscode/dist/**/*.js"] 35 | } 36 | ] 37 | } 38 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "tasksStatusbar.taskLabelFilter": "dev" 3 | } 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Keybindings Viewer 2 | 3 | [![](https://img.shields.io/twitter/follow/hediet_dev.svg?style=social)](https://twitter.com/intent/follow?screen_name=hediet_dev) 4 | 5 | You can find the online playground [here](https://hediet.github.io/visual-keyboard/) and the extension for VS Code [in the marketplace](https://marketplace.visualstudio.com/items?itemName=hediet.key-bindings-viewer). 6 | 7 | See the [readme](./extension-vscode/README.md) of the extension for more details. 8 | 9 | # Related 10 | 11 | - http://waldobronchart.github.io/ShortcutMapper/ 12 | - https://en.wikipedia.org/wiki/Keyboard_layout#Mechanical,_visual,_and_functional_layouts 13 | - https://github.com/wilix-team/iohook#readme 14 | - http://kbdlayout.info/KBDUSX/scancodes 15 | -------------------------------------------------------------------------------- /demo/.keep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hediet/visual-keyboard/4249eee06b8c5e30f0999318cda92a6ac40f2e54/demo/.keep -------------------------------------------------------------------------------- /demo/demo.md: -------------------------------------------------------------------------------- 1 | # Key Bindings Viewer 2 | 3 | Hello 4 | 5 | Hello 6 | -------------------------------------------------------------------------------- /demo/demo.ts: -------------------------------------------------------------------------------- 1 | asdfdsf; 2 | -------------------------------------------------------------------------------- /docs/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hediet/visual-keyboard/4249eee06b8c5e30f0999318cda92a6ac40f2e54/docs/demo.gif -------------------------------------------------------------------------------- /extension-vscode/.vscodeignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | ui/node_modules -------------------------------------------------------------------------------- /extension-vscode/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | ## 0.1.0 4 | 5 | - Initial release 6 | -------------------------------------------------------------------------------- /extension-vscode/README.md: -------------------------------------------------------------------------------- 1 | # Key Bindings Viewer 2 | 3 | [![](https://img.shields.io/twitter/follow/hediet_dev.svg?style=social)](https://twitter.com/intent/follow?screen_name=hediet_dev) 4 | 5 | A VS Code extension for viewing key bindings. 6 | You can play with the online version [here](https://hediet.github.io/visual-keyboard/). 7 | 8 | ![](../docs/demo.gif) 9 | 10 | ## Usage 11 | 12 | Install this extension and execute the command `Open Keyboard View`. 13 | 14 | ## Caveats 15 | 16 | - Does not consider contexts (`when` clause of a key binding), as there is no VS Code API for that. 17 | - Key Bindings are static and pre-bundled, as there is no VS Code API to retrieve all currently bound key bindings. 18 | - Supported (functional) keyboard layouts: 19 | - US 20 | - German 21 | - German Neo 2 22 | - Supported physical layouts: 23 | - ANSI 24 | - ISO 25 | - Registers a global keyboard hook to listen for key events (even those triggered outside of VS Code), 26 | as VS Code does not expose key events with all required data. 27 | Uses a secured and local websocket connection to send those to the webview (but only if the VS Code window has focus). 28 | Please review the code if you have security concerns. 29 | No data leaves your computer. 30 | - Linux key detection needs to be tuned. Modifiers work though. 31 | - Tested on windows & linux (not tested on Mac). 32 | 33 | ## Settings 34 | 35 | You can configure the physical and functional layout in VS Code's settings pane. 36 | 37 | # Used Libraries 38 | 39 | - iohook is used to listen for key events 40 | -------------------------------------------------------------------------------- /extension-vscode/docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hediet/visual-keyboard/4249eee06b8c5e30f0999318cda92a6ac40f2e54/extension-vscode/docs/logo.png -------------------------------------------------------------------------------- /extension-vscode/docs/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 22 | 24 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 67 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /extension-vscode/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "key-bindings-viewer", 3 | "private": true, 4 | "displayName": "Key Bindings Viewer", 5 | "description": "Displays a keyboard with applicable keybindings on the keys.", 6 | "icon": "docs/logo.png", 7 | "version": "0.1.3", 8 | "license": "MIT", 9 | "engines": { 10 | "vscode": "^1.35.0" 11 | }, 12 | "preview": true, 13 | "publisher": "hediet", 14 | "author": { 15 | "email": "henning.dieterichs@live.de", 16 | "name": "Henning Dieterichs" 17 | }, 18 | "readme": "./README.md", 19 | "repository": { 20 | "type": "git", 21 | "url": "https://github.com/hediet/visual-keyboard.git" 22 | }, 23 | "categories": [ 24 | "Other" 25 | ], 26 | "activationEvents": [ 27 | "onCommand:key-bindings-viewer.open-view", 28 | "onWebviewPanel:key-bindings-viewer" 29 | ], 30 | "main": "./dist/extension.js", 31 | "contributes": { 32 | "commands": [ 33 | { 34 | "command": "key-bindings-viewer.open-view", 35 | "title": "Open Keyboard View" 36 | }, 37 | { 38 | "command": "key-bindings-viewer.open-external-view", 39 | "title": "Open Keyboard View In Browser" 40 | } 41 | ], 42 | "configuration": { 43 | "title": "Key Bindings Viewer", 44 | "properties": { 45 | "key-bindings-viewer.useChromeKioskMode": { 46 | "type": "boolean", 47 | "default": true, 48 | "description": "Open The Keyboard View with Chrome in Kiosk Mode." 49 | }, 50 | "key-bindings-viewer.physicalLayout": { 51 | "type": "string", 52 | "default": "ANSI", 53 | "enum": [ 54 | "ANSI", 55 | "ISO" 56 | ] 57 | }, 58 | "key-bindings-viewer.functionalLayout": { 59 | "type": "string", 60 | "default": "US", 61 | "enum": [ 62 | "German", 63 | "German - Neo 2", 64 | "US" 65 | ] 66 | } 67 | } 68 | } 69 | }, 70 | "scripts": { 71 | "pub": "vsce publish --yarn --baseImagesUrl https://github.com/hediet/visual-keyboard/raw/master/extension-vscode --baseContentUrl https://github.com/hediet/visual-keyboard/raw/master/extension-vscode", 72 | "package": "vsce package --yarn", 73 | "vscode:prepublish": "yarn build", 74 | "build": "webpack --mode production", 75 | "dev": "shx rm -rf dist && tsc -watch -p ./" 76 | }, 77 | "dependencies": { 78 | "@hediet/std": "^0.6.0", 79 | "@hediet/typed-json-rpc": "^0.7.7", 80 | "@hediet/typed-json-rpc-websocket-server": "^0.7.7", 81 | "chrome-launcher": "^0.12.0", 82 | "@hediet/node-reload": "^0.7.3", 83 | "express": "^4.17.1", 84 | "open": "^7.0.2", 85 | "serve-static": "^1.14.1", 86 | "ws": "^7.2.1", 87 | "crypto-random-string": "^3.1.0" 88 | }, 89 | "devDependencies": { 90 | "copy-webpack-plugin": "^5.1.1", 91 | "@types/copy-webpack-plugin": "^5.0.0", 92 | "@types/express": "^4.17.2", 93 | "@types/serve-static": "^1.13.3", 94 | "@types/node": "^13.7.4", 95 | "@types/vscode": "1.35.0", 96 | "tslint": "^6.0.0", 97 | "typescript": "^3.8.2", 98 | "webpack": "^4.41.6", 99 | "webpack-cli": "^3.3.11", 100 | "ts-loader": "^6.2.1", 101 | "shx": "^0.3.2" 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /extension-vscode/src/Config.ts: -------------------------------------------------------------------------------- 1 | import { workspace } from "vscode"; 2 | import { Disposable } from "@hediet/std/disposable"; 3 | import { observable } from "mobx"; 4 | 5 | const extensionId = "key-bindings-viewer"; 6 | const useChromeKioskModeKey = `${extensionId}.useChromeKioskMode`; 7 | const physicalLayoutKey = `${extensionId}.physicalLayout`; 8 | const functionalLayoutKey = `${extensionId}.functionalLayout`; 9 | 10 | export class Config { 11 | public dispose = Disposable.fn(); 12 | 13 | @observable 14 | private _useChromeKioskMode!: boolean; 15 | 16 | public get useChromeKioskMode(): boolean { 17 | return this._useChromeKioskMode; 18 | } 19 | 20 | @observable 21 | private _physicalLayout: string | null = null; 22 | 23 | public get physicalLayout() { 24 | return this._physicalLayout; 25 | } 26 | 27 | @observable 28 | private _functionalLayout: string | null = null; 29 | 30 | public get functionalLayout() { 31 | return this._functionalLayout; 32 | } 33 | 34 | constructor() { 35 | this.updateConfig(); 36 | this.dispose.track( 37 | workspace.onDidChangeConfiguration(() => { 38 | this.updateConfig(); 39 | }) 40 | ); 41 | } 42 | 43 | private updateConfig(): void { 44 | const c = workspace.getConfiguration(); 45 | 46 | this._useChromeKioskMode = mapUndefined(c.get(useChromeKioskModeKey), true); 47 | this._physicalLayout = mapUndefined(c.get(physicalLayoutKey), null); 48 | this._functionalLayout = mapUndefined(c.get(functionalLayoutKey), null); 49 | } 50 | } 51 | 52 | function mapUndefined(val: T | undefined, defaultVal: T) { 53 | if (val === undefined) { 54 | return defaultVal; 55 | } 56 | return val; 57 | } 58 | -------------------------------------------------------------------------------- /extension-vscode/src/Server.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketStream } from "@hediet/typed-json-rpc-websocket"; 2 | import { AddressInfo } from "net"; 3 | import WebSocket = require("ws"); 4 | import * as express from "express"; 5 | import * as http from "http"; 6 | import * as serveStatic from "serve-static"; 7 | import { Config } from "./Config"; 8 | import { distPath } from "@hediet/visual-keyboard-frontend"; 9 | import { Server as WebsocketServer } from "@hediet/key-listener"; 10 | import { autorun } from "mobx"; 11 | import * as vscode from "vscode"; 12 | import { Disposable } from "@hediet/std/disposable"; 13 | 14 | export class Server { 15 | private server: http.Server; 16 | private readonly wsServer: WebsocketServer; 17 | public readonly dispose = Disposable.fn(); 18 | 19 | public get secret(): string { 20 | return this.wsServer.serverSecret; 21 | } 22 | 23 | public get wsPort(): number { 24 | return this.wsServer.port; 25 | } 26 | 27 | constructor(config: Config) { 28 | const keyBindingSetPerPlatform: Record = { 29 | aix: "VS Code-linux", 30 | android: "VS Code-linux", 31 | darwin: "VS Code-mac", 32 | freebsd: "VS Code-linux", 33 | linux: "VS Code-linux", 34 | openbsd: "VS Code-linux", 35 | sunos: "VS Code-linux", 36 | win32: "VS Code-win", 37 | cygwin: "VS Code-linux", 38 | netbsd: "VS Code-linux", 39 | }; 40 | 41 | this.wsServer = new WebsocketServer({ 42 | handleClient: client => { 43 | client.disposer.track({ 44 | dispose: autorun(() => { 45 | client.connection.updateSettings({ 46 | physicalLayout: config.physicalLayout, 47 | functionalLayout: config.functionalLayout, 48 | keyBindingSet: keyBindingSetPerPlatform[process.platform], 49 | }); 50 | }), 51 | }); 52 | }, 53 | handleAction: async action => { 54 | await vscode.commands.executeCommand(action); 55 | }, 56 | }); 57 | 58 | this.dispose.track( 59 | vscode.window.onDidChangeWindowState(s => { 60 | this.updateState(); 61 | }) 62 | ); 63 | 64 | const app = express(); 65 | app.use(serveStatic(distPath)); 66 | this.server = app.listen(); 67 | console.log(`Serving "${distPath}" on port ${(this.server.address() as AddressInfo).port}`); 68 | } 69 | 70 | private updateState() { 71 | const focused = vscode.window.state.focused; 72 | this.wsServer.enableKeyboardHook = focused; 73 | } 74 | 75 | public getIndexUrl(args: { mode: "standalone" | "webViewIFrame" }): string { 76 | const port = process.env.USE_DEV_UI ? 8080 : this.port; 77 | //const inWebView = args.mode === "standalone" ? "" : "&mode=webViewIFrame"; 78 | const headless = args.mode === "standalone" ? "" : "&headless"; 79 | return `http://localhost:${port}/index.html?serverPort=${this.wsPort}&serverSecret=${this.secret}${headless}`; 80 | } 81 | 82 | public get mainBundleUrl(): string { 83 | return `http://localhost:${this.port}/main.js`; 84 | } 85 | 86 | public get port(): number { 87 | const httpPort = (this.server.address() as AddressInfo).port; 88 | return httpPort; 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /extension-vscode/src/WebViews.ts: -------------------------------------------------------------------------------- 1 | import { window, ViewColumn, WebviewPanel } from "vscode"; 2 | import { Server } from "./Server"; 3 | import { Disposable } from "@hediet/std/disposable"; 4 | 5 | export const keyBindingsViewer = "key-bindings-viewer"; 6 | 7 | export class WebViews { 8 | private readonly views = new Map(); 9 | 10 | public readonly dispose = Disposable.fn(); 11 | 12 | constructor(private readonly server: Server) { 13 | this.dispose.track( 14 | window.registerWebviewPanelSerializer(keyBindingsViewer, { 15 | deserializeWebviewPanel: async (panel, state) => { 16 | this.restore(panel); 17 | }, 18 | }) 19 | ); 20 | 21 | this.dispose.track({ 22 | dispose: () => { 23 | for (const panel of this.views.keys()) { 24 | panel.dispose(); 25 | } 26 | }, 27 | }); 28 | } 29 | 30 | public createNew() { 31 | const panel = window.createWebviewPanel( 32 | keyBindingsViewer, 33 | "Keyboard", 34 | { viewColumn: ViewColumn.Two, preserveFocus: true }, 35 | { enableScripts: true } 36 | ); 37 | 38 | this.setupView(panel); 39 | } 40 | 41 | public restore(webviewPanel: WebviewPanel) { 42 | this.setupView(webviewPanel); 43 | } 44 | 45 | private setupView(webviewPanel: WebviewPanel) { 46 | webviewPanel.webview.html = getHtml(this.server); 47 | const view = new WebView(webviewPanel); 48 | this.views.set(webviewPanel, view); 49 | webviewPanel.onDidDispose(() => { 50 | this.views.delete(webviewPanel); 51 | }); 52 | } 53 | } 54 | 55 | export class WebView { 56 | constructor(private readonly webviewPanel: WebviewPanel) {} 57 | } 58 | 59 | export function getHtml(server: Server) { 60 | const isDev = !!process.env.USE_DEV_UI; 61 | return ` 62 | 63 | 64 | 65 | 66 | 71 | 72 | 73 | 80 | ${ 81 | isDev 82 | ? `` 85 | : `` 86 | } 87 | 88 | 89 | `; 90 | } 91 | -------------------------------------------------------------------------------- /extension-vscode/src/extension.ts: -------------------------------------------------------------------------------- 1 | import { window, ExtensionContext, commands, Uri } from "vscode"; 2 | import { Disposable } from "@hediet/std/disposable"; 3 | import { 4 | enableHotReload, 5 | hotRequireExportedFn, 6 | registerUpdateReconciler, 7 | getReloadCount, 8 | } from "@hediet/node-reload"; 9 | 10 | if (process.env.HOT_RELOAD) { 11 | enableHotReload({ entryModule: module, loggingEnabled: true }); 12 | } 13 | registerUpdateReconciler(module); 14 | 15 | import { WebViews } from "./WebViews"; 16 | import { Server } from "./Server"; 17 | import { Config } from "./Config"; 18 | import { openBrowserView } from "./openBrowserView"; 19 | 20 | export class Extension { 21 | public readonly dispose = Disposable.fn(); 22 | 23 | private readonly config = new Config(); 24 | 25 | private readonly server = new Server(this.config); 26 | private readonly views = this.dispose.track(new WebViews(this.server)); 27 | 28 | constructor() { 29 | if (getReloadCount(module) > 0) { 30 | const i = this.dispose.track(window.createStatusBarItem()); 31 | i.text = "reload" + getReloadCount(module); 32 | i.show(); 33 | } 34 | 35 | this.dispose.track([ 36 | commands.registerCommand("key-bindings-viewer.open-view", () => { 37 | this.views.createNew(); 38 | }), 39 | commands.registerCommand("key-bindings-viewer.open-external-view", () => { 40 | openBrowserView(this.server, this.config); 41 | }), 42 | ]); 43 | } 44 | } 45 | 46 | export function activate(context: ExtensionContext) { 47 | context.subscriptions.push( 48 | hotRequireExportedFn(module, Extension, Extension => new Extension()) 49 | ); 50 | } 51 | 52 | export function deactivate() {} 53 | -------------------------------------------------------------------------------- /extension-vscode/src/openBrowserView.ts: -------------------------------------------------------------------------------- 1 | import { Server } from "./Server"; 2 | import * as open from "open"; 3 | import chromeLauncher = require("chrome-launcher"); 4 | import { Config } from "./Config"; 5 | 6 | export async function openBrowserView(server: Server, config: Config): Promise { 7 | const url = server.getIndexUrl({ 8 | mode: "standalone", 9 | }); 10 | 11 | let opened = false; 12 | if (config.useChromeKioskMode) { 13 | opened = await launchChrome(url); 14 | } 15 | if (!opened) { 16 | open(url); 17 | } 18 | } 19 | 20 | async function launchChrome(url: string): Promise { 21 | try { 22 | const _chrome = await chromeLauncher.launch({ 23 | startingUrl: url, 24 | // `--window-size=${width},${height}` 25 | chromeFlags: ["--app=" + url], 26 | }); 27 | return true; 28 | } catch (e) { 29 | return false; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /extension-vscode/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace NodeJS { 2 | declare interface ProcessEnv { 3 | HOT_RELOAD?: "true"; 4 | USE_DEV_UI?: "true"; 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /extension-vscode/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es6", 5 | "outDir": "dist", 6 | "lib": ["es6"], 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "strict": true, 10 | "skipLibCheck": true, 11 | "experimentalDecorators": true 12 | }, 13 | "include": [ 14 | "src/**/*", 15 | "../js-data-extractors/src/Extractors/TypeScriptDataExtractors.ts" 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /extension-vscode/webpack.config.js: -------------------------------------------------------------------------------- 1 | require("ts-node").register(); 2 | module.exports = require("./webpack.config.ts"); 3 | -------------------------------------------------------------------------------- /extension-vscode/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | import path = require("path"); 3 | import { CleanWebpackPlugin } from "clean-webpack-plugin"; 4 | import CopyPlugin = require("copy-webpack-plugin"); 5 | import { readFileSync } from "fs"; 6 | 7 | const r = (file: string) => path.resolve(__dirname, file); 8 | 9 | module.exports = { 10 | target: "node", 11 | entry: r("./src/extension"), 12 | output: { 13 | path: r("./dist"), 14 | filename: "extension.js", 15 | libraryTarget: "commonjs2", 16 | devtoolModuleFilenameTemplate: "../[resource-path]", 17 | }, 18 | devtool: "source-map", 19 | externals: { 20 | vscode: "commonjs vscode", 21 | "@hediet/visual-keyboard-frontend": "@hediet/visual-keyboard-frontend", 22 | iohook: "iohook", 23 | }, 24 | resolve: { 25 | extensions: [".ts", ".js"], 26 | }, 27 | module: { 28 | rules: [ 29 | { 30 | test: /\.ts$/, 31 | exclude: /node_modules/, 32 | use: [ 33 | { 34 | loader: "ts-loader", 35 | }, 36 | ], 37 | }, 38 | ], 39 | }, 40 | node: { 41 | __dirname: false, 42 | }, 43 | plugins: [ 44 | new CleanWebpackPlugin(), 45 | includeDependency("../frontend"), 46 | includeDependency("iohook"), 47 | ], 48 | } as webpack.Configuration; 49 | 50 | function includeDependency(pkg: string) { 51 | const pkgJson = path.join(pkg, "package.json"); 52 | const pkgJsonPath = require.resolve(pkgJson); 53 | const pkgPath = path.join(pkgJsonPath, "../"); 54 | 55 | const content = readFileSync(pkgJsonPath, { 56 | encoding: "utf8", 57 | }); 58 | const pkgName = JSON.parse(content).name; 59 | 60 | return new CopyPlugin([ 61 | { 62 | from: pkgPath, 63 | to: r(`./dist/node_modules/${pkgName}`), 64 | ignore: ["**/node_modules/**/*"], 65 | }, 66 | ]); 67 | } 68 | -------------------------------------------------------------------------------- /frontend/data/extract-from-cldr.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { 3 | FunctionalLayoutData, 4 | FunctionalLayoutModeData, 5 | } from "../src/model/Keyboard"; 6 | import xml2js = require("xml2js"); 7 | 8 | const table = { 9 | E01: "2", 10 | E02: "3", 11 | E03: "4", 12 | E04: "5", 13 | E05: "6", 14 | E06: "7", 15 | E07: "8", 16 | E08: "9", 17 | E09: "10", 18 | E10: "11", 19 | E11: "12", 20 | E12: "13", 21 | D01: "16", 22 | D02: "17", 23 | D03: "18", 24 | D04: "19", 25 | D05: "20", 26 | D06: "21", 27 | D07: "22", 28 | D08: "23", 29 | D09: "24", 30 | D10: "25", 31 | D11: "26", 32 | D12: "27", 33 | C01: "30", 34 | C02: "31", 35 | C03: "32", 36 | C04: "33", 37 | C05: "34", 38 | C06: "35", 39 | C07: "36", 40 | C08: "37", 41 | C09: "38", 42 | C10: "39", 43 | C11: "40", 44 | E00: "41", 45 | C12: "43", 46 | B01: "44", 47 | B02: "45", 48 | B03: "46", 49 | B04: "47", 50 | B05: "48", 51 | B06: "49", 52 | B07: "50", 53 | B08: "51", 54 | B09: "52", 55 | B10: "53", 56 | A03: "57", 57 | B00: "86", 58 | B11: "115", 59 | }; 60 | 61 | const str = readFileSync( 62 | "./unicode/cldr-keyboards.36.0/keyboards/windows/de-t-k0-windows.xml", 63 | { encoding: "utf8" } 64 | ); 65 | 66 | function mapTextToVirtualKey(text: string): string { 67 | if (text.match(/[a-z0-9]/)) { 68 | return text.toUpperCase(); 69 | } 70 | switch (text) { 71 | case "ü": 72 | return "Oem1"; 73 | case "#": 74 | return "Oem2"; 75 | case "ö": 76 | return "Oem3"; 77 | case "ß": 78 | return "Oem4"; 79 | case "^": 80 | return "Oem5"; 81 | case "´": 82 | return "Oem6"; 83 | case "ä": 84 | return "Oem7"; 85 | case "<": 86 | return "Oem102"; 87 | case ",": 88 | return "OemComma"; 89 | case ".": 90 | return "OemPeriod"; 91 | case "-": 92 | return "OemMinus"; 93 | case "+": 94 | return "OemPlus"; 95 | case " ": 96 | return "Space"; 97 | } 98 | throw new Error(`Didn't handle "${text}"`); 99 | } 100 | 101 | async function main(): Promise { 102 | const parser = new xml2js.Parser(); 103 | const result = await parser.parseStringPromise(str); 104 | 105 | const data: FunctionalLayoutData = { 106 | name: "test", 107 | modes: { 108 | default: { 109 | mapping: { 110 | "0x36": { virtualKey: "ShiftR" }, 111 | "0x2a": { virtualKey: "ShiftL" }, 112 | "0x3b": { virtualKey: "F1" }, 113 | "0x3c": { virtualKey: "F2" }, 114 | "0x3d": { virtualKey: "F3" }, 115 | "0x3e": { virtualKey: "F4" }, 116 | "0x3f": { virtualKey: "F5" }, 117 | "0x40": { virtualKey: "F6" }, 118 | "0x41": { virtualKey: "F7" }, 119 | "0x42": { virtualKey: "F8" }, 120 | "0x43": { virtualKey: "F9" }, 121 | "0x44": { virtualKey: "F10" }, 122 | "0x57": { virtualKey: "F11" }, 123 | "0x58": { virtualKey: "F12" }, 124 | "0x01": { virtualKey: "Escape" }, 125 | "0x1d": { virtualKey: "CtrlL" }, 126 | "0xe01d": { virtualKey: "CtrlR" }, 127 | "0x38": { virtualKey: "AltL" }, 128 | "0x1c": { virtualKey: "Return" }, 129 | "0x0f": { virtualKey: "Tab" }, 130 | "0x0e": { virtualKey: "BackSpace" }, 131 | "0xe05b": { virtualKey: "OsL" }, 132 | "0xe05c": { virtualKey: "OsR" }, 133 | "0xe05d": { virtualKey: "Apps" }, 134 | "0x3a": { virtualKey: "Caps" }, 135 | "0xe038": { virtualKey: "AltR" }, 136 | 137 | "0xe048": { virtualKey: "Up" }, 138 | "0xe04b": { virtualKey: "Left" }, 139 | "0xe050": { virtualKey: "Down" }, 140 | "0xe04d": { virtualKey: "Right" }, 141 | 142 | "0xe052": { virtualKey: "Insert" }, 143 | "0xe047": { virtualKey: "Home" }, 144 | "0xe049": { virtualKey: "Prior" }, 145 | 146 | "0xe053": { virtualKey: "Delete" }, 147 | "0xe04f": { virtualKey: "End" }, 148 | "0xe051": { virtualKey: "Next" }, 149 | }, 150 | }, 151 | }, 152 | }; 153 | 154 | let idx = 1; 155 | for (const mode of result.keyboard.keyMap) { 156 | const modifiersStr = (mode.$ || {}).modifiers as string; 157 | 158 | const map = { 159 | altR: ["0xe038"], 160 | alt: ["0x38"], 161 | caps: ["0x3a"], 162 | ctrl: ["0xe01d", "0x1d"], 163 | shift: ["0x36", "0x2a"], 164 | }; 165 | 166 | const modeData: FunctionalLayoutModeData = { 167 | mapping: {}, 168 | modifiers: [], 169 | includes: "default", 170 | }; 171 | 172 | if (modifiersStr) { 173 | for (const modifierStr of modifiersStr.split(" ")) { 174 | let modifiers = new Array([]); 175 | for (let part of modifierStr.split("+")) { 176 | let optional = false; 177 | if (part.endsWith("?")) { 178 | optional = true; 179 | part = part.substr(0, part.length - 1); 180 | } 181 | const codes = map[part]; 182 | if (!codes) { 183 | throw new Error(`${part} not known.`); 184 | } 185 | 186 | const newModifiers = new Array(); 187 | for (const m of modifiers) { 188 | for (const p of codes) { 189 | const i = m.slice(); 190 | i.push(p); 191 | newModifiers.push(i); 192 | } 193 | if (optional) { 194 | const i = m.slice(); 195 | newModifiers.push(i); 196 | } 197 | } 198 | modifiers = newModifiers; 199 | } 200 | modeData.modifiers.push(...modifiers); 201 | } 202 | } else { 203 | modeData.modifiers.push([]); 204 | } 205 | 206 | for (const mapping of mode.map) { 207 | const iso = mapping.$.iso; 208 | let to = mapping.$.to; 209 | 210 | const r = /\\u\{(..)\}/.exec(to); 211 | if (r) { 212 | to = JSON.parse(`"\\u00${r[1]}"`); 213 | } 214 | 215 | const scanCode = table[iso]; 216 | const mod1To = data.modes["mod1"] 217 | ? data.modes["mod1"].mapping[scanCode].text 218 | : to; 219 | modeData.mapping[scanCode] = { 220 | text: to, 221 | virtualKey: mapTextToVirtualKey(mod1To), 222 | }; 223 | } 224 | data.modes["mod" + idx++] = modeData; 225 | } 226 | 227 | writeFileSync("./layouts/de.json", JSON.stringify(data)); 228 | } 229 | 230 | main(); 231 | -------------------------------------------------------------------------------- /frontend/data/extract.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { FunctionalLayoutData } from "../src/model/Keyboard/"; 3 | 4 | const str = readFileSync("./foo.json", { encoding: "utf8" }); 5 | 6 | const data = JSON.parse(str).Mappings; 7 | 8 | const x: FunctionalLayoutData = { 9 | name: "test", 10 | modes: {}, 11 | }; 12 | 13 | for (const m of data) { 14 | const mode = m.Layer; 15 | const scanCode = m.ScanCode as number; 16 | const scanCodeHex = scanCode.toString(16); 17 | const mapsTo = m.MapsTo; 18 | 19 | let l = x.modes[mode]; 20 | if (!l) { 21 | l = {}; 22 | x.modes[mode] = l; 23 | } 24 | l[scanCodeHex] = mapsTo; 25 | } 26 | 27 | const outStr = JSON.stringify(x); 28 | writeFileSync("./out-foo.json", outStr); 29 | -------------------------------------------------------------------------------- /frontend/data/fix_deo_neo.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { FunctionalLayoutData } from "../src/model/Keyboard/FunctionalLayout"; 3 | import { ScanCode } from "../src/model/Keyboard/primitives"; 4 | import { getJsCodeFromScanCode } from "../src/model/JsKeycodes"; 5 | 6 | export class Main { 7 | run() { 8 | /*const path = 9 | "S:\\dev\\2019\\Neo2Net\\KeyboardMapper\\Data\\KeyDefinitions.tyml"; 10 | 11 | const content = readFileSync(path, { encoding: "utf8" }); 12 | 13 | const r = /KeyDefinition <(.*)>[\}]*? Text:<(.*)>/g; 14 | 15 | const map = new Map();*/ 16 | /* 17 | while (true) { 18 | const m = r.exec(content); 19 | if (!m) { 20 | break; 21 | } 22 | const name = m[1]; 23 | const text = m[2]; 24 | map.set(name, text); 25 | console.log(name, text); 26 | }*/ 27 | 28 | const path = "./functional-layouts/us.json"; 29 | const neoJson = JSON.parse( 30 | readFileSync(path, { 31 | encoding: "utf8", 32 | }) 33 | ) as FunctionalLayoutData; 34 | 35 | function translate(str: string) { 36 | const s = ScanCode.from(str); 37 | const newKey = getJsCodeFromScanCode(s); 38 | return newKey; 39 | } 40 | 41 | for (const [key, val] of Object.entries(neoJson.modes)) { 42 | const newMapping = {}; 43 | for (const [k, v] of Object.entries(val.mapping)) { 44 | newMapping[translate(k)] = v; 45 | } 46 | 47 | val.mapping = newMapping; 48 | if (val.modifiers) { 49 | for (const arr of val.modifiers) { 50 | for (let i = 0; i < arr.length; i++) { 51 | arr[i] = translate(arr[i]); 52 | } 53 | } 54 | } 55 | } 56 | 57 | writeFileSync(path, JSON.stringify(neoJson), { 58 | encoding: "utf8", 59 | }); 60 | } 61 | } 62 | 63 | //require("C:\\Users\\henni\\AppData\\Local\\Yarn\\Data\\global\\node_modules\\easy-attach\\")(); 64 | 65 | new Main().run(); 66 | -------------------------------------------------------------------------------- /frontend/data/functional-layouts/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "German", 3 | "modes": { 4 | "default": { 5 | "mapping": { 6 | "ShiftRight": { "virtualKey": "ShiftR" }, 7 | "ShiftLeft": { "virtualKey": "ShiftL" }, 8 | "F1": { "virtualKey": "F1" }, 9 | "F2": { "virtualKey": "F2" }, 10 | "F3": { "virtualKey": "F3" }, 11 | "F4": { "virtualKey": "F4" }, 12 | "F5": { "virtualKey": "F5" }, 13 | "F6": { "virtualKey": "F6" }, 14 | "F7": { "virtualKey": "F7" }, 15 | "F8": { "virtualKey": "F8" }, 16 | "F9": { "virtualKey": "F9" }, 17 | "F10": { "virtualKey": "F10" }, 18 | "F11": { "virtualKey": "F11" }, 19 | "F12": { "virtualKey": "F12" }, 20 | "Pause": { "virtualKey": "Pause" }, 21 | "Escape": { "virtualKey": "Escape" }, 22 | "ControlLeft": { "virtualKey": "CtrlL" }, 23 | "ControlRight": { "virtualKey": "CtrlR" }, 24 | "AltLeft": { "virtualKey": "AltL" }, 25 | "Enter": { "virtualKey": "Return" }, 26 | "Tab": { "virtualKey": "Tab" }, 27 | "Backspace": { "virtualKey": "Backspace" }, 28 | "MetaLeft": { "virtualKey": "MetaL" }, 29 | "AltRight": { "virtualKey": "AltR" }, 30 | "CapsLock": { "virtualKey": "Caps" }, 31 | "ArrowUp": { "virtualKey": "Up" }, 32 | "ArrowLeft": { "virtualKey": "Left" }, 33 | "ArrowDown": { "virtualKey": "Down" }, 34 | "ArrowRight": { "virtualKey": "Right" }, 35 | "Insert": { "virtualKey": "Insert" }, 36 | "Home": { "virtualKey": "Home" }, 37 | "PageUp": { "virtualKey": "Prior" }, 38 | "Delete": { "virtualKey": "Delete" }, 39 | "End": { "virtualKey": "End" }, 40 | "PageDown": { "virtualKey": "Next" }, 41 | "NumLock": { "virtualKey": "NumLock" }, 42 | "NumpadDivide": { "virtualKey": "NumDiv", "text": "/" }, 43 | "NumpadSubtract": { "virtualKey": "NumSub", "text": "-" }, 44 | "NumpadAdd": { "virtualKey": "NumAdd", "text": "+" }, 45 | "NumpadMultiply": { "virtualKey": "NumMul", "text": "*" }, 46 | "Numpad7": { "virtualKey": "Num7", "text": "7" }, 47 | "Numpad8": { "virtualKey": "Num8", "text": "8" }, 48 | "Numpad9": { "virtualKey": "Num9", "text": "9" }, 49 | "Numpad4": { "virtualKey": "Num4", "text": "4" }, 50 | "Numpad5": { "virtualKey": "Num5", "text": "5" }, 51 | "Numpad6": { "virtualKey": "Num6", "text": "6" }, 52 | "Numpad1": { "virtualKey": "Num1", "text": "1" }, 53 | "Numpad2": { "virtualKey": "Num2", "text": "2" }, 54 | "Numpad3": { "virtualKey": "Num3", "text": "3" }, 55 | "Numpad0": { "virtualKey": "Num0", "text": "0" }, 56 | "NumpadEnter": { "virtualKey": "NumEnter" }, 57 | "NumpadDecimal": { "virtualKey": "NumDec", "text": "," } 58 | } 59 | }, 60 | "mod1": { 61 | "mapping": { 62 | "Digit1": { "text": "1", "virtualKey": "1" }, 63 | "Digit2": { "text": "2", "virtualKey": "2" }, 64 | "Digit3": { "text": "3", "virtualKey": "3" }, 65 | "Digit4": { "text": "4", "virtualKey": "4" }, 66 | "Digit5": { "text": "5", "virtualKey": "5" }, 67 | "Digit6": { "text": "6", "virtualKey": "6" }, 68 | "Digit7": { "text": "7", "virtualKey": "7" }, 69 | "Digit8": { "text": "8", "virtualKey": "8" }, 70 | "Digit9": { "text": "9", "virtualKey": "9" }, 71 | "Digit0": { "text": "0", "virtualKey": "0" }, 72 | "Minus": { "text": "ß", "virtualKey": "Oem4" }, 73 | "Equal": { "text": "´", "virtualKey": "Oem6" }, 74 | "KeyQ": { "text": "q", "virtualKey": "Q" }, 75 | "KeyW": { "text": "w", "virtualKey": "W" }, 76 | "KeyE": { "text": "e", "virtualKey": "E" }, 77 | "KeyR": { "text": "r", "virtualKey": "R" }, 78 | "KeyT": { "text": "t", "virtualKey": "T" }, 79 | "KeyY": { "text": "z", "virtualKey": "Z" }, 80 | "KeyU": { "text": "u", "virtualKey": "U" }, 81 | "KeyI": { "text": "i", "virtualKey": "I" }, 82 | "KeyO": { "text": "o", "virtualKey": "O" }, 83 | "KeyP": { "text": "p", "virtualKey": "P" }, 84 | "BracketLeft": { "text": "ü", "virtualKey": "Oem1" }, 85 | "BracketRight": { "text": "+", "virtualKey": "OemPlus" }, 86 | "KeyA": { "text": "a", "virtualKey": "A" }, 87 | "KeyS": { "text": "s", "virtualKey": "S" }, 88 | "KeyD": { "text": "d", "virtualKey": "D" }, 89 | "KeyF": { "text": "f", "virtualKey": "F" }, 90 | "KeyG": { "text": "g", "virtualKey": "G" }, 91 | "KeyH": { "text": "h", "virtualKey": "H" }, 92 | "KeyJ": { "text": "j", "virtualKey": "J" }, 93 | "KeyK": { "text": "k", "virtualKey": "K" }, 94 | "KeyL": { "text": "l", "virtualKey": "L" }, 95 | "Semicolon": { "text": "ö", "virtualKey": "Oem3" }, 96 | "Quote": { "text": "ä", "virtualKey": "Oem7" }, 97 | "Backquote": { "text": "^", "virtualKey": "Oem5" }, 98 | "Backslash": { "text": "#", "virtualKey": "Oem2" }, 99 | "KeyZ": { "text": "y", "virtualKey": "Y" }, 100 | "KeyX": { "text": "x", "virtualKey": "X" }, 101 | "KeyC": { "text": "c", "virtualKey": "C" }, 102 | "KeyV": { "text": "v", "virtualKey": "V" }, 103 | "KeyB": { "text": "b", "virtualKey": "B" }, 104 | "KeyN": { "text": "n", "virtualKey": "N" }, 105 | "KeyM": { "text": "m", "virtualKey": "M" }, 106 | "Comma": { "text": ",", "virtualKey": "OemComma" }, 107 | "Period": { "text": ".", "virtualKey": "OemPeriod" }, 108 | "Slash": { "text": "-", "virtualKey": "OemMinus" }, 109 | "Space": { "text": " ", "virtualKey": "Space" }, 110 | "IntlBackslash": { "text": "<", "virtualKey": "Oem102" } 111 | }, 112 | "modifiers": [[]], 113 | "includes": "default" 114 | }, 115 | "mod2": { 116 | "mapping": { 117 | "Digit1": { "text": "!", "virtualKey": "1" }, 118 | "Digit2": { "text": "\"", "virtualKey": "2" }, 119 | "Digit3": { "text": "§", "virtualKey": "3" }, 120 | "Digit4": { "text": "$", "virtualKey": "4" }, 121 | "Digit5": { "text": "%", "virtualKey": "5" }, 122 | "Digit6": { "text": "&", "virtualKey": "6" }, 123 | "Digit7": { "text": "/", "virtualKey": "7" }, 124 | "Digit8": { "text": "(", "virtualKey": "8" }, 125 | "Digit9": { "text": ")", "virtualKey": "9" }, 126 | "Digit0": { "text": "=", "virtualKey": "0" }, 127 | "Minus": { "text": "?", "virtualKey": "Oem4" }, 128 | "Equal": { "text": "`", "virtualKey": "Oem6" }, 129 | "KeyQ": { "text": "Q", "virtualKey": "Q" }, 130 | "KeyW": { "text": "W", "virtualKey": "W" }, 131 | "KeyE": { "text": "E", "virtualKey": "E" }, 132 | "KeyR": { "text": "R", "virtualKey": "R" }, 133 | "KeyT": { "text": "T", "virtualKey": "T" }, 134 | "KeyY": { "text": "Z", "virtualKey": "Z" }, 135 | "KeyU": { "text": "U", "virtualKey": "U" }, 136 | "KeyI": { "text": "I", "virtualKey": "I" }, 137 | "KeyO": { "text": "O", "virtualKey": "O" }, 138 | "KeyP": { "text": "P", "virtualKey": "P" }, 139 | "BracketLeft": { "text": "Ü", "virtualKey": "Oem1" }, 140 | "BracketRight": { "text": "*", "virtualKey": "OemPlus" }, 141 | "KeyA": { "text": "A", "virtualKey": "A" }, 142 | "KeyS": { "text": "S", "virtualKey": "S" }, 143 | "KeyD": { "text": "D", "virtualKey": "D" }, 144 | "KeyF": { "text": "F", "virtualKey": "F" }, 145 | "KeyG": { "text": "G", "virtualKey": "G" }, 146 | "KeyH": { "text": "H", "virtualKey": "H" }, 147 | "KeyJ": { "text": "J", "virtualKey": "J" }, 148 | "KeyK": { "text": "K", "virtualKey": "K" }, 149 | "KeyL": { "text": "L", "virtualKey": "L" }, 150 | "Semicolon": { "text": "Ö", "virtualKey": "Oem3" }, 151 | "Quote": { "text": "Ä", "virtualKey": "Oem7" }, 152 | "Backquote": { "text": "°", "virtualKey": "Oem5" }, 153 | "Backslash": { "text": "'", "virtualKey": "Oem2" }, 154 | "KeyZ": { "text": "Y", "virtualKey": "Y" }, 155 | "KeyX": { "text": "X", "virtualKey": "X" }, 156 | "KeyC": { "text": "C", "virtualKey": "C" }, 157 | "KeyV": { "text": "V", "virtualKey": "V" }, 158 | "KeyB": { "text": "B", "virtualKey": "B" }, 159 | "KeyN": { "text": "N", "virtualKey": "N" }, 160 | "KeyM": { "text": "M", "virtualKey": "M" }, 161 | "Comma": { "text": ";", "virtualKey": "OemComma" }, 162 | "Period": { "text": ":", "virtualKey": "OemPeriod" }, 163 | "Slash": { "text": "_", "virtualKey": "OemMinus" }, 164 | "Space": { "text": " ", "virtualKey": "Space" }, 165 | "IntlBackslash": { "text": ">", "virtualKey": "Oem102" } 166 | }, 167 | "modifiers": [["ShiftRight"], ["ShiftLeft"]], 168 | "includes": "default" 169 | }, 170 | "mod3": { 171 | "mapping": { 172 | "Digit1": { "text": "!", "virtualKey": "1" }, 173 | "Digit2": { "text": "\"", "virtualKey": "2" }, 174 | "Digit3": { "text": "§", "virtualKey": "3" }, 175 | "Digit4": { "text": "$", "virtualKey": "4" }, 176 | "Digit5": { "text": "%", "virtualKey": "5" }, 177 | "Digit6": { "text": "&", "virtualKey": "6" }, 178 | "Digit7": { "text": "/", "virtualKey": "7" }, 179 | "Digit8": { "text": "(", "virtualKey": "8" }, 180 | "Digit9": { "text": ")", "virtualKey": "9" }, 181 | "Digit0": { "text": "=", "virtualKey": "0" }, 182 | "Minus": { "text": "?", "virtualKey": "Oem4" }, 183 | "Equal": { "text": "´", "virtualKey": "Oem6" }, 184 | "KeyQ": { "text": "Q", "virtualKey": "Q" }, 185 | "KeyW": { "text": "W", "virtualKey": "W" }, 186 | "KeyE": { "text": "E", "virtualKey": "E" }, 187 | "KeyR": { "text": "R", "virtualKey": "R" }, 188 | "KeyT": { "text": "T", "virtualKey": "T" }, 189 | "KeyY": { "text": "Z", "virtualKey": "Z" }, 190 | "KeyU": { "text": "U", "virtualKey": "U" }, 191 | "KeyI": { "text": "I", "virtualKey": "I" }, 192 | "KeyO": { "text": "O", "virtualKey": "O" }, 193 | "KeyP": { "text": "P", "virtualKey": "P" }, 194 | "BracketLeft": { "text": "Ü", "virtualKey": "Oem1" }, 195 | "BracketRight": { "text": "*", "virtualKey": "OemPlus" }, 196 | "KeyA": { "text": "A", "virtualKey": "A" }, 197 | "KeyS": { "text": "S", "virtualKey": "S" }, 198 | "KeyD": { "text": "D", "virtualKey": "D" }, 199 | "KeyF": { "text": "F", "virtualKey": "F" }, 200 | "KeyG": { "text": "G", "virtualKey": "G" }, 201 | "KeyH": { "text": "H", "virtualKey": "H" }, 202 | "KeyJ": { "text": "J", "virtualKey": "J" }, 203 | "KeyK": { "text": "K", "virtualKey": "K" }, 204 | "KeyL": { "text": "L", "virtualKey": "L" }, 205 | "Semicolon": { "text": "Ö", "virtualKey": "Oem3" }, 206 | "Quote": { "text": "Ä", "virtualKey": "Oem7" }, 207 | "Backquote": { "text": "^", "virtualKey": "Oem5" }, 208 | "Backslash": { "text": "'", "virtualKey": "Oem2" }, 209 | "KeyZ": { "text": "Y", "virtualKey": "Y" }, 210 | "KeyX": { "text": "X", "virtualKey": "X" }, 211 | "KeyC": { "text": "C", "virtualKey": "C" }, 212 | "KeyV": { "text": "V", "virtualKey": "V" }, 213 | "KeyB": { "text": "B", "virtualKey": "B" }, 214 | "KeyN": { "text": "N", "virtualKey": "N" }, 215 | "KeyM": { "text": "M", "virtualKey": "M" }, 216 | "Comma": { "text": ";", "virtualKey": "OemComma" }, 217 | "Period": { "text": ":", "virtualKey": "OemPeriod" }, 218 | "Slash": { "text": "-", "virtualKey": "OemMinus" }, 219 | "Space": { "text": " ", "virtualKey": "Space" }, 220 | "IntlBackslash": { "text": "<", "virtualKey": "Oem102" } 221 | }, 222 | "modifiers": [["CapsLock"]], 223 | "includes": "default" 224 | }, 225 | "mod4": { 226 | "mapping": { 227 | "Digit1": { "text": "1", "virtualKey": "1" }, 228 | "Digit2": { "text": "2", "virtualKey": "2" }, 229 | "Digit3": { "text": "3", "virtualKey": "3" }, 230 | "Digit4": { "text": "4", "virtualKey": "4" }, 231 | "Digit5": { "text": "5", "virtualKey": "5" }, 232 | "Digit6": { "text": "6", "virtualKey": "6" }, 233 | "Digit7": { "text": "7", "virtualKey": "7" }, 234 | "Digit8": { "text": "8", "virtualKey": "8" }, 235 | "Digit9": { "text": "9", "virtualKey": "9" }, 236 | "Digit0": { "text": "0", "virtualKey": "0" }, 237 | "Minus": { "text": "ß", "virtualKey": "Oem4" }, 238 | "Equal": { "text": "`", "virtualKey": "Oem6" }, 239 | "KeyQ": { "text": "q", "virtualKey": "Q" }, 240 | "KeyW": { "text": "w", "virtualKey": "W" }, 241 | "KeyE": { "text": "e", "virtualKey": "E" }, 242 | "KeyR": { "text": "r", "virtualKey": "R" }, 243 | "KeyT": { "text": "t", "virtualKey": "T" }, 244 | "KeyY": { "text": "z", "virtualKey": "Z" }, 245 | "KeyU": { "text": "u", "virtualKey": "U" }, 246 | "KeyI": { "text": "i", "virtualKey": "I" }, 247 | "KeyO": { "text": "o", "virtualKey": "O" }, 248 | "KeyP": { "text": "p", "virtualKey": "P" }, 249 | "BracketLeft": { "text": "ü", "virtualKey": "Oem1" }, 250 | "BracketRight": { "text": "+", "virtualKey": "OemPlus" }, 251 | "KeyA": { "text": "a", "virtualKey": "A" }, 252 | "KeyS": { "text": "s", "virtualKey": "S" }, 253 | "KeyD": { "text": "d", "virtualKey": "D" }, 254 | "KeyF": { "text": "f", "virtualKey": "F" }, 255 | "KeyG": { "text": "g", "virtualKey": "G" }, 256 | "KeyH": { "text": "h", "virtualKey": "H" }, 257 | "KeyJ": { "text": "j", "virtualKey": "J" }, 258 | "KeyK": { "text": "k", "virtualKey": "K" }, 259 | "KeyL": { "text": "l", "virtualKey": "L" }, 260 | "Semicolon": { "text": "ö", "virtualKey": "Oem3" }, 261 | "Quote": { "text": "ä", "virtualKey": "Oem7" }, 262 | "Backquote": { "text": "°", "virtualKey": "Oem5" }, 263 | "Backslash": { "text": "#", "virtualKey": "Oem2" }, 264 | "KeyZ": { "text": "y", "virtualKey": "Y" }, 265 | "KeyX": { "text": "x", "virtualKey": "X" }, 266 | "KeyC": { "text": "c", "virtualKey": "C" }, 267 | "KeyV": { "text": "v", "virtualKey": "V" }, 268 | "KeyB": { "text": "b", "virtualKey": "B" }, 269 | "KeyN": { "text": "n", "virtualKey": "N" }, 270 | "KeyM": { "text": "m", "virtualKey": "M" }, 271 | "Comma": { "text": ",", "virtualKey": "OemComma" }, 272 | "Period": { "text": ".", "virtualKey": "OemPeriod" }, 273 | "Slash": { "text": "_", "virtualKey": "OemMinus" }, 274 | "Space": { "text": " ", "virtualKey": "Space" }, 275 | "IntlBackslash": { "text": ">", "virtualKey": "Oem102" } 276 | }, 277 | "modifiers": [ 278 | ["CapsLock", "ShiftRight"], 279 | ["CapsLock", "ShiftLeft"] 280 | ], 281 | "includes": "default" 282 | }, 283 | "mod5": { 284 | "mapping": { 285 | "Digit2": { "text": "²", "virtualKey": "2" }, 286 | "Digit3": { "text": "³", "virtualKey": "3" }, 287 | "Digit7": { "text": "{", "virtualKey": "7" }, 288 | "Digit8": { "text": "[", "virtualKey": "8" }, 289 | "Digit9": { "text": "]", "virtualKey": "9" }, 290 | "Digit0": { "text": "}", "virtualKey": "0" }, 291 | "Minus": { "text": "\\", "virtualKey": "Oem4" }, 292 | "KeyQ": { "text": "@", "virtualKey": "Q" }, 293 | "KeyE": { "text": "€", "virtualKey": "E" }, 294 | "BracketRight": { "text": "~", "virtualKey": "OemPlus" }, 295 | "KeyM": { "text": "µ", "virtualKey": "M" }, 296 | "IntlBackslash": { "text": "|", "virtualKey": "Oem102" } 297 | }, 298 | "modifiers": [ 299 | [null, "CapsLock"], 300 | [null], 301 | ["ControlRight", "AltLeft", "CapsLock"], 302 | ["ControlRight", "AltLeft"], 303 | ["ControlLeft", "AltLeft", "CapsLock"], 304 | ["ControlLeft", "AltLeft"] 305 | ], 306 | "includes": "mod1" 307 | }, 308 | "mod6": { 309 | "mapping": { "Minus": { "text": "ẞ", "virtualKey": "Oem4" } }, 310 | "modifiers": [ 311 | [null, "ShiftRight", "CapsLock"], 312 | [null, "ShiftRight"], 313 | [null, "ShiftLeft", "CapsLock"], 314 | [null, "ShiftLeft"], 315 | ["ControlRight", "AltLeft", "ShiftRight", "CapsLock"], 316 | ["ControlRight", "AltLeft", "ShiftRight"], 317 | ["ControlRight", "AltLeft", "ShiftLeft", "CapsLock"], 318 | ["ControlRight", "AltLeft", "ShiftLeft"], 319 | ["ControlLeft", "AltLeft", "ShiftRight", "CapsLock"], 320 | ["ControlLeft", "AltLeft", "ShiftRight"], 321 | ["ControlLeft", "AltLeft", "ShiftLeft", "CapsLock"], 322 | ["ControlLeft", "AltLeft", "ShiftLeft"] 323 | ], 324 | "includes": "mod1" 325 | } 326 | } 327 | } 328 | -------------------------------------------------------------------------------- /frontend/data/functional-layouts/de_neo.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "German - Neo 2", 3 | "modes": { 4 | "default": { 5 | "mapping": { 6 | "AltLeft": { "virtualKey": "AltL" }, 7 | "AltRight": { "virtualKey": "AltR" }, 8 | "ArrowDown": { "virtualKey": "Down" }, 9 | "ArrowLeft": { "virtualKey": "Left" }, 10 | "ArrowRight": { "virtualKey": "Right" }, 11 | "ArrowUp": { "virtualKey": "Up" }, 12 | "Backspace": { "virtualKey": "BackSpace" }, 13 | "CapsLock": { "virtualKey": "Caps" }, 14 | "ControlLeft": { "virtualKey": "CtrlL" }, 15 | "ControlRight": { "virtualKey": "CtrlR" }, 16 | "Delete": { "virtualKey": "Delete" }, 17 | "End": { "virtualKey": "End" }, 18 | "Enter": { "virtualKey": "Return" }, 19 | "Escape": { "virtualKey": "Escape" }, 20 | "F1": { "virtualKey": "F1" }, 21 | "F10": { "virtualKey": "F10" }, 22 | "F11": { "virtualKey": "F11" }, 23 | "F12": { "virtualKey": "F12" }, 24 | "F2": { "virtualKey": "F2" }, 25 | "F3": { "virtualKey": "F3" }, 26 | "F4": { "virtualKey": "F4" }, 27 | "F5": { "virtualKey": "F5" }, 28 | "F6": { "virtualKey": "F6" }, 29 | "F7": { "virtualKey": "F7" }, 30 | "F8": { "virtualKey": "F8" }, 31 | "F9": { "virtualKey": "F9" }, 32 | "Home": { "virtualKey": "Home" }, 33 | "Insert": { "virtualKey": "Insert" }, 34 | "MetaLeft": { "virtualKey": "MetaL" }, 35 | "NumLock": { "virtualKey": "NumLock" }, 36 | "Numpad0": { "virtualKey": "Num0", "text": "0" }, 37 | "Numpad1": { "virtualKey": "Num1", "text": "1" }, 38 | "Numpad2": { "virtualKey": "Num2", "text": "2" }, 39 | "Numpad3": { "virtualKey": "Num3", "text": "3" }, 40 | "Numpad4": { "virtualKey": "Num4", "text": "4" }, 41 | "Numpad5": { "virtualKey": "Num5", "text": "5" }, 42 | "Numpad6": { "virtualKey": "Num6", "text": "6" }, 43 | "Numpad7": { "virtualKey": "Num7", "text": "7" }, 44 | "Numpad8": { "virtualKey": "Num8", "text": "8" }, 45 | "Numpad9": { "virtualKey": "Num9", "text": "9" }, 46 | "NumpadAdd": { "virtualKey": "NumAdd", "text": "+" }, 47 | "NumpadDecimal": { "virtualKey": "NumDec", "text": "," }, 48 | "NumpadDivide": { "virtualKey": "NumDiv", "text": "/" }, 49 | "NumpadEnter": { "virtualKey": "NumEnter" }, 50 | "NumpadMultiply": { "virtualKey": "NumMul", "text": "*" }, 51 | "NumpadSubtract": { "virtualKey": "NumSub", "text": "-" }, 52 | "PageDown": { "virtualKey": "Next" }, 53 | "PageUp": { "virtualKey": "Prior" }, 54 | "ShiftLeft": { "virtualKey": "ShiftL" }, 55 | "ShiftRight": { "virtualKey": "ShiftR" }, 56 | "Tab": { "virtualKey": "Tab" } 57 | } 58 | }, 59 | "neo-default": { 60 | "includes": "default", 61 | "mapping": { 62 | "CapsLock": { "virtualKey": "Mod3" }, 63 | "Backslash": { "virtualKey": "Mod3" }, 64 | "undefined": { "virtualKey": "Mod4" }, 65 | "IntlBackslash": { "virtualKey": "Mod4" } 66 | } 67 | }, 68 | "mod1": { 69 | "includes": "neo-default", 70 | "modifiers": [[]], 71 | "mapping": { 72 | "Backquote": { "text": "ˆ" }, 73 | "BracketLeft": { "text": "ß", "virtualKey": "Oem4" }, 74 | "BracketRight": { "text": "´", "virtualKey": "Oem6" }, 75 | "Comma": { "text": ",", "virtualKey": "OemComma" }, 76 | "Digit0": { "text": "0", "virtualKey": "0" }, 77 | "Digit1": { "text": "1", "virtualKey": "1" }, 78 | "Digit2": { "text": "2", "virtualKey": "2" }, 79 | "Digit3": { "text": "3", "virtualKey": "3" }, 80 | "Digit4": { "text": "4", "virtualKey": "4" }, 81 | "Digit5": { "text": "5", "virtualKey": "5" }, 82 | "Digit6": { "text": "6", "virtualKey": "6" }, 83 | "Digit7": { "text": "7", "virtualKey": "7" }, 84 | "Digit8": { "text": "8", "virtualKey": "8" }, 85 | "Digit9": { "text": "9", "virtualKey": "9" }, 86 | "Equal": { "text": "`" }, 87 | "KeyA": { "text": "u", "virtualKey": "U" }, 88 | "KeyB": { "text": "z", "virtualKey": "Z" }, 89 | "KeyC": { "text": "ä", "virtualKey": "Oem7" }, 90 | "KeyD": { "text": "a", "virtualKey": "A" }, 91 | "KeyE": { "text": "l", "virtualKey": "L" }, 92 | "KeyF": { "text": "e", "virtualKey": "E" }, 93 | "KeyG": { "text": "o", "virtualKey": "O" }, 94 | "KeyH": { "text": "s", "virtualKey": "S" }, 95 | "KeyI": { "text": "g", "virtualKey": "G" }, 96 | "KeyJ": { "text": "n", "virtualKey": "N" }, 97 | "KeyK": { "text": "r", "virtualKey": "R" }, 98 | "KeyL": { "text": "t", "virtualKey": "T" }, 99 | "KeyM": { "text": "m", "virtualKey": "M" }, 100 | "KeyN": { "text": "b", "virtualKey": "B" }, 101 | "KeyO": { "text": "f", "virtualKey": "F" }, 102 | "KeyP": { "text": "q", "virtualKey": "Q" }, 103 | "KeyQ": { "text": "x", "virtualKey": "X" }, 104 | "KeyR": { "text": "c", "virtualKey": "C" }, 105 | "KeyS": { "text": "i", "virtualKey": "I" }, 106 | "KeyT": { "text": "w", "virtualKey": "W" }, 107 | "KeyU": { "text": "h", "virtualKey": "H" }, 108 | "KeyV": { "text": "p", "virtualKey": "P" }, 109 | "KeyW": { "text": "v", "virtualKey": "V" }, 110 | "KeyX": { "text": "ö", "virtualKey": "Oem3" }, 111 | "KeyY": { "text": "k", "virtualKey": "K" }, 112 | "KeyZ": { "text": "ü", "virtualKey": "Oem1" }, 113 | "Minus": { "text": "-", "virtualKey": "OemMinus" }, 114 | "Period": { "text": ".", "virtualKey": "OemPeriod" }, 115 | "Quote": { "text": "y", "virtualKey": "Y" }, 116 | "Semicolon": { "text": "d", "virtualKey": "D" }, 117 | "ShiftLeft": { "virtualKey": "ShiftL" }, 118 | "ShiftRight": { "virtualKey": "ShiftR" }, 119 | "Slash": { "text": "j", "virtualKey": "J" }, 120 | "Space": { "text": " ", "virtualKey": "Space" } 121 | } 122 | }, 123 | "mod2": { 124 | "includes": "neo-default", 125 | "modifiers": [["ShiftRight"], ["ShiftLeft"]], 126 | "mapping": { 127 | "Backquote": { "text": "ˇ" }, 128 | "BracketLeft": { "text": "ẞ", "virtualKey": "Oem4" }, 129 | "BracketRight": { "text": "~", "virtualKey": "Oem6" }, 130 | "Comma": { "text": "–", "virtualKey": "OemComma" }, 131 | "Digit0": { "text": "”", "virtualKey": "0" }, 132 | "Digit1": { "text": "°", "virtualKey": "1" }, 133 | "Digit2": { "text": "§", "virtualKey": "2" }, 134 | "Digit3": { "text": "ℓ", "virtualKey": "3" }, 135 | "Digit4": { "text": "»", "virtualKey": "4" }, 136 | "Digit5": { "text": "«", "virtualKey": "5" }, 137 | "Digit6": { "text": "$", "virtualKey": "6" }, 138 | "Digit7": { "text": "€", "virtualKey": "7" }, 139 | "Digit8": { "text": "„", "virtualKey": "8" }, 140 | "Digit9": { "text": "“", "virtualKey": "9" }, 141 | "Equal": { "text": "¸" }, 142 | "KeyA": { "text": "U", "virtualKey": "U" }, 143 | "KeyB": { "text": "Z", "virtualKey": "Z" }, 144 | "KeyC": { "text": "Ä", "virtualKey": "Oem7" }, 145 | "KeyD": { "text": "A", "virtualKey": "A" }, 146 | "KeyE": { "text": "L", "virtualKey": "L" }, 147 | "KeyF": { "text": "E", "virtualKey": "E" }, 148 | "KeyG": { "text": "O", "virtualKey": "O" }, 149 | "KeyH": { "text": "S", "virtualKey": "S" }, 150 | "KeyI": { "text": "G", "virtualKey": "G" }, 151 | "KeyJ": { "text": "N", "virtualKey": "N" }, 152 | "KeyK": { "text": "R", "virtualKey": "R" }, 153 | "KeyL": { "text": "T", "virtualKey": "T" }, 154 | "KeyM": { "text": "M", "virtualKey": "M" }, 155 | "KeyN": { "text": "B", "virtualKey": "B" }, 156 | "KeyO": { "text": "F", "virtualKey": "F" }, 157 | "KeyP": { "text": "Q", "virtualKey": "Q" }, 158 | "KeyQ": { "text": "X", "virtualKey": "X" }, 159 | "KeyR": { "text": "C", "virtualKey": "C" }, 160 | "KeyS": { "text": "I", "virtualKey": "I" }, 161 | "KeyT": { "text": "W", "virtualKey": "W" }, 162 | "KeyU": { "text": "H", "virtualKey": "H" }, 163 | "KeyV": { "text": "P", "virtualKey": "P" }, 164 | "KeyW": { "text": "V", "virtualKey": "V" }, 165 | "KeyX": { "text": "Ö", "virtualKey": "Oem3" }, 166 | "KeyY": { "text": "K", "virtualKey": "K" }, 167 | "KeyZ": { "text": "Ü", "virtualKey": "Oem1" }, 168 | "Minus": { "text": "—", "virtualKey": "OemMinus" }, 169 | "Period": { "text": "•", "virtualKey": "OemPeriod" }, 170 | "Quote": { "text": "Y", "virtualKey": "Y" }, 171 | "Semicolon": { "text": "D", "virtualKey": "D" }, 172 | "ShiftLeft": { "virtualKey": "CapsLock" }, 173 | "ShiftRight": { "virtualKey": "CapsLock" }, 174 | "Slash": { "text": "J", "virtualKey": "J" }, 175 | "Space": { "text": " ", "virtualKey": "Space" } 176 | } 177 | }, 178 | "mod3": { 179 | "includes": "neo-default", 180 | "modifiers": [["CapsLock"], ["Backslash"]], 181 | "mapping": { 182 | "Backquote": { "text": "↻" }, 183 | "BracketLeft": { "text": "ſ" }, 184 | "BracketRight": { "text": "/" }, 185 | "Comma": { "text": "\"" }, 186 | "Digit0": { "text": "’" }, 187 | "Digit1": { "text": "¹" }, 188 | "Digit2": { "text": "²" }, 189 | "Digit3": { "text": "³" }, 190 | "Digit4": { "text": "›" }, 191 | "Digit5": { "text": "‹" }, 192 | "Digit6": { "text": "¢" }, 193 | "Digit7": { "text": "¥" }, 194 | "Digit8": { "text": "‚" }, 195 | "Digit9": { "text": "‘" }, 196 | "Equal": { "text": "°" }, 197 | "KeyA": { "text": "\\" }, 198 | "KeyB": { "text": "`" }, 199 | "KeyC": { "text": "|" }, 200 | "KeyD": { "text": "{" }, 201 | "KeyE": { "text": "[" }, 202 | "KeyF": { "text": "}" }, 203 | "KeyG": { "text": "*" }, 204 | "KeyH": { "text": "?" }, 205 | "KeyI": { "text": ">" }, 206 | "KeyJ": { "text": "(" }, 207 | "KeyK": { "text": ")" }, 208 | "KeyL": { "text": "-" }, 209 | "KeyM": { "text": "%" }, 210 | "KeyN": { "text": "+" }, 211 | "KeyO": { "text": "=" }, 212 | "KeyP": { "text": "&" }, 213 | "KeyQ": { "text": "…" }, 214 | "KeyR": { "text": "]" }, 215 | "KeyS": { "text": "/" }, 216 | "KeyT": { "text": "^" }, 217 | "KeyU": { "text": "<" }, 218 | "KeyV": { "text": "~" }, 219 | "KeyW": { "text": "_" }, 220 | "KeyX": { "text": "$" }, 221 | "KeyY": { "text": "!" }, 222 | "KeyZ": { "text": "#" }, 223 | "Minus": {}, 224 | "Period": { "text": "'" }, 225 | "Quote": { "text": "@" }, 226 | "Semicolon": { "text": ":" }, 227 | "Slash": { "text": ";" }, 228 | "Tab": { "text": "Compose" } 229 | } 230 | }, 231 | "mod4": { 232 | "includes": "neo-default", 233 | "modifiers": [ 234 | ["AltRight"], 235 | ["IntlBackslash"], 236 | ["AltRight", "ShiftRight"], 237 | ["IntlBackslash", "ShiftLeft"] 238 | ], 239 | "mapping": { 240 | "Backquote": { "text": "˙" }, 241 | "BracketLeft": { "text": "−" }, 242 | "BracketRight": { "text": "˝" }, 243 | "Comma": { "text": "2", "virtualKey": "Num2" }, 244 | "Digit0": { "text": "*" }, 245 | "Digit1": { "text": "ª" }, 246 | "Digit2": { "text": "º" }, 247 | "Digit3": { "text": "№" }, 248 | "Digit4": {}, 249 | "Digit5": { "text": "·" }, 250 | "Digit6": { "text": "£" }, 251 | "Digit7": { "text": "¤" }, 252 | "Digit8": { "text": "⇥" }, 253 | "Digit9": { "text": "/" }, 254 | "Equal": { "text": "¨" }, 255 | "KeyA": { "virtualKey": "Home" }, 256 | "KeyB": {}, 257 | "KeyD": { "virtualKey": "Down" }, 258 | "KeyE": { "virtualKey": "Up" }, 259 | "KeyF": { "virtualKey": "Right" }, 260 | "KeyG": { "virtualKey": "End" }, 261 | "KeyH": { "text": "¿" }, 262 | "KeyI": { "text": "8", "virtualKey": "Num8" }, 263 | "KeyJ": { "text": "4", "virtualKey": "Num4" }, 264 | "KeyK": { "text": "5", "virtualKey": "Num5" }, 265 | "KeyL": { "text": "6", "virtualKey": "Num6" }, 266 | "KeyM": { "text": "1", "virtualKey": "Num1" }, 267 | "KeyN": { "text": ":" }, 268 | "KeyO": { "text": "9", "virtualKey": "Num9" }, 269 | "KeyP": { "text": "+" }, 270 | "KeyQ": { "virtualKey": "PageUp" }, 271 | "KeyR": { "virtualKey": "Delete" }, 272 | "KeyS": { "virtualKey": "Right" }, 273 | "KeyT": { "text": "PageDown" }, 274 | "KeyU": { "text": "7", "virtualKey": "Num7" }, 275 | "KeyV": { "virtualKey": "Enter" }, 276 | "KeyW": { "virtualKey": "Backspace" }, 277 | "KeyX": { "virtualKey": "Tab" }, 278 | "KeyY": { "text": "¡" }, 279 | "KeyZ": { "virtualKey": "Escape" }, 280 | "Minus": { "text": "-" }, 281 | "Period": { "text": "3", "virtualKey": "Num3" }, 282 | "Quote": { "text": "." }, 283 | "Semicolon": { "text": "," }, 284 | "Slash": { "text": ";" }, 285 | "Space": { "text": "0", "virtualKey": "Num0" } 286 | } 287 | }, 288 | "mod5": { 289 | "includes": "neo-default", 290 | "modifiers": [ 291 | ["CapsLock", "ShiftLeft"], 292 | ["Backslash", "ShiftLeft"], 293 | ["CapsLock", "ShiftRight"], 294 | ["Backslash", "ShiftRight"] 295 | ], 296 | "mapping": { 297 | "Backquote": { "text": "˞" }, 298 | "BracketLeft": { "text": "ς" }, 299 | "BracketRight": { "text": "᾿" }, 300 | "Comma": { "text": "ϱ" }, 301 | "Digit0": { "text": "₀" }, 302 | "Digit1": { "text": "₁" }, 303 | "Digit2": { "text": "₂" }, 304 | "Digit3": { "text": "₃" }, 305 | "Digit4": { "text": "♀" }, 306 | "Digit5": { "text": "♂" }, 307 | "Digit6": { "text": "⚥" }, 308 | "Digit7": { "text": "ϰ" }, 309 | "Digit8": { "text": "⟨" }, 310 | "Digit9": { "text": "⟩" }, 311 | "Equal": { "text": "῾" }, 312 | "KeyA": {}, 313 | "KeyB": { "text": "ζ" }, 314 | "KeyC": { "text": "η" }, 315 | "KeyD": { "text": "α" }, 316 | "KeyE": { "text": "λ" }, 317 | "KeyF": { "text": "ε" }, 318 | "KeyG": { "text": "ο" }, 319 | "KeyH": { "text": "σ" }, 320 | "KeyI": { "text": "γ" }, 321 | "KeyJ": { "text": "ν" }, 322 | "KeyK": { "text": "ρ" }, 323 | "KeyL": { "text": "τ" }, 324 | "KeyM": { "text": "μ" }, 325 | "KeyN": { "text": "β" }, 326 | "KeyO": { "text": "φ" }, 327 | "KeyP": { "text": "ϕ" }, 328 | "KeyQ": { "text": "ξ" }, 329 | "KeyR": { "text": "χ" }, 330 | "KeyS": { "text": "ι" }, 331 | "KeyT": { "text": "ω" }, 332 | "KeyU": { "text": "ψ" }, 333 | "KeyV": { "text": "π" }, 334 | "KeyW": { "text": "" }, 335 | "KeyX": { "text": "ϵ" }, 336 | "KeyY": { "text": "κ" }, 337 | "KeyZ": {}, 338 | "Minus": { "text": "‑" }, 339 | "Period": { "text": "ϑ" }, 340 | "Quote": { "text": "υ" }, 341 | "Semicolon": { "text": "δ" }, 342 | "Slash": { "text": "θ" }, 343 | "Space": { "text": " " } 344 | } 345 | }, 346 | "mod6": { 347 | "includes": "neo-default", 348 | "modifiers": [ 349 | ["CapsLock", "AltRight"], 350 | ["Backslash", "AltRight"], 351 | ["CapsLock", "IntlBackslash"], 352 | ["Backslash", "IntlBackslash"] 353 | ], 354 | "mapping": { 355 | "Backquote": { "text": "." }, 356 | "BracketLeft": { "text": "∘" }, 357 | "BracketRight": { "text": "˘" }, 358 | "Comma": { "text": "⇒" }, 359 | "Digit0": { "text": "∅" }, 360 | "Digit1": { "text": "¬" }, 361 | "Digit2": { "text": "∨" }, 362 | "Digit3": { "text": "∧" }, 363 | "Digit4": { "text": "⊥" }, 364 | "Digit5": { "text": "∡" }, 365 | "Digit6": { "text": "∥" }, 366 | "Digit7": { "text": "→" }, 367 | "Digit8": { "text": "∞" }, 368 | "Digit9": { "text": "∝" }, 369 | "Equal": { "text": "¯" }, 370 | "KeyA": { "text": "⊂" }, 371 | "KeyB": { "text": "ℤ" }, 372 | "KeyC": { "text": "ℵ" }, 373 | "KeyD": { "text": "∀" }, 374 | "KeyE": { "text": "Λ" }, 375 | "KeyF": { "text": "∃" }, 376 | "KeyG": { "text": "∈" }, 377 | "KeyH": { "text": "Σ" }, 378 | "KeyI": { "text": "Γ" }, 379 | "KeyJ": { "text": "ℕ" }, 380 | "KeyK": { "text": "ℝ" }, 381 | "KeyL": { "text": "∂" }, 382 | "KeyM": { "text": "⇔" }, 383 | "KeyN": { "text": "⇐" }, 384 | "KeyO": { "text": "Φ" }, 385 | "KeyP": { "text": "ℚ" }, 386 | "KeyQ": { "text": "Ξ" }, 387 | "KeyR": { "text": "ℂ" }, 388 | "KeyS": { "text": "∫" }, 389 | "KeyT": { "text": "Ω" }, 390 | "KeyU": { "text": "Ψ" }, 391 | "KeyV": { "text": "Π" }, 392 | "KeyW": { "text": "√" }, 393 | "KeyX": { "text": "∩" }, 394 | "KeyY": { "text": "×" }, 395 | "KeyZ": { "text": "∪" }, 396 | "Minus": { "text": "╌" }, 397 | "Period": { "text": "↦" }, 398 | "Quote": { "text": "∇" }, 399 | "Semicolon": { "text": "Δ" }, 400 | "Slash": { "text": "Θ" }, 401 | "Space": { "text": " " } 402 | } 403 | } 404 | } 405 | } 406 | -------------------------------------------------------------------------------- /frontend/data/functional-layouts/us.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "US", 3 | "modes": { 4 | "default": { 5 | "mapping": { 6 | "ShiftRight": { "virtualKey": "ShiftR" }, 7 | "ShiftLeft": { "virtualKey": "ShiftL" }, 8 | "F1": { "virtualKey": "F1" }, 9 | "F2": { "virtualKey": "F2" }, 10 | "F3": { "virtualKey": "F3" }, 11 | "F4": { "virtualKey": "F4" }, 12 | "F5": { "virtualKey": "F5" }, 13 | "F6": { "virtualKey": "F6" }, 14 | "F7": { "virtualKey": "F7" }, 15 | "F8": { "virtualKey": "F8" }, 16 | "F9": { "virtualKey": "F9" }, 17 | "F10": { "virtualKey": "F10" }, 18 | "F11": { "virtualKey": "F11" }, 19 | "F12": { "virtualKey": "F12" }, 20 | "Pause": { "virtualKey": "Pause" }, 21 | "Escape": { "virtualKey": "Escape" }, 22 | "ControlLeft": { "virtualKey": "CtrlL" }, 23 | "ControlRight": { "virtualKey": "CtrlR" }, 24 | "AltLeft": { "virtualKey": "AltL" }, 25 | "Enter": { "virtualKey": "Return" }, 26 | "Tab": { "virtualKey": "Tab" }, 27 | "Backspace": { "virtualKey": "Backspace" }, 28 | "MetaLeft": { "virtualKey": "MetaL" }, 29 | "AltRight": { "virtualKey": "AltR" }, 30 | "undefined": { "virtualKey": "AltR" }, 31 | "CapsLock": { "virtualKey": "Caps" }, 32 | "ArrowUp": { "virtualKey": "Up" }, 33 | "ArrowLeft": { "virtualKey": "Left" }, 34 | "ArrowDown": { "virtualKey": "Down" }, 35 | "ArrowRight": { "virtualKey": "Right" }, 36 | "Insert": { "virtualKey": "Insert" }, 37 | "Home": { "virtualKey": "Home" }, 38 | "PageUp": { "virtualKey": "Prior" }, 39 | "Delete": { "virtualKey": "Delete" }, 40 | "End": { "virtualKey": "End" }, 41 | "PageDown": { "virtualKey": "Next" }, 42 | "NumLock": { "virtualKey": "NumLock" }, 43 | "NumpadDivide": { "virtualKey": "NumDiv", "text": "/" }, 44 | "NumpadSubtract": { "virtualKey": "NumSub", "text": "-" }, 45 | "NumpadAdd": { "virtualKey": "NumAdd", "text": "+" }, 46 | "NumpadMultiply": { "virtualKey": "NumMul", "text": "*" }, 47 | "Numpad7": { "virtualKey": "Num7", "text": "7" }, 48 | "Numpad8": { "virtualKey": "Num8", "text": "8" }, 49 | "Numpad9": { "virtualKey": "Num9", "text": "9" }, 50 | "Numpad4": { "virtualKey": "Num4", "text": "4" }, 51 | "Numpad5": { "virtualKey": "Num5", "text": "5" }, 52 | "Numpad6": { "virtualKey": "Num6", "text": "6" }, 53 | "Numpad1": { "virtualKey": "Num1", "text": "1" }, 54 | "Numpad2": { "virtualKey": "Num2", "text": "2" }, 55 | "Numpad3": { "virtualKey": "Num3", "text": "3" }, 56 | "Numpad0": { "virtualKey": "Num0", "text": "0" }, 57 | "NumpadEnter": { "virtualKey": "NumEnter" }, 58 | "NumpadDecimal": { "virtualKey": "NumDec", "text": "," } 59 | } 60 | }, 61 | "mod1": { 62 | "mapping": { 63 | "Digit1": { "text": "1", "virtualKey": "1" }, 64 | "Digit2": { "text": "2", "virtualKey": "2" }, 65 | "Digit3": { "text": "3", "virtualKey": "3" }, 66 | "Digit4": { "text": "4", "virtualKey": "4" }, 67 | "Digit5": { "text": "5", "virtualKey": "5" }, 68 | "Digit6": { "text": "6", "virtualKey": "6" }, 69 | "Digit7": { "text": "7", "virtualKey": "7" }, 70 | "Digit8": { "text": "8", "virtualKey": "8" }, 71 | "Digit9": { "text": "9", "virtualKey": "9" }, 72 | "Digit0": { "text": "0", "virtualKey": "0" }, 73 | "Minus": { "text": "-", "virtualKey": "OemMinus" }, 74 | "Equal": { "text": "=", "virtualKey": "OemPlus" }, 75 | "KeyQ": { "text": "q", "virtualKey": "Q" }, 76 | "KeyW": { "text": "w", "virtualKey": "W" }, 77 | "KeyE": { "text": "e", "virtualKey": "E" }, 78 | "KeyR": { "text": "r", "virtualKey": "R" }, 79 | "KeyT": { "text": "t", "virtualKey": "T" }, 80 | "KeyY": { "text": "y", "virtualKey": "Y" }, 81 | "KeyU": { "text": "u", "virtualKey": "U" }, 82 | "KeyI": { "text": "i", "virtualKey": "I" }, 83 | "KeyO": { "text": "o", "virtualKey": "O" }, 84 | "KeyP": { "text": "p", "virtualKey": "P" }, 85 | "BracketLeft": { "text": "[", "virtualKey": "Oem4" }, 86 | "BracketRight": { "text": "]", "virtualKey": "Oem6" }, 87 | "KeyA": { "text": "a", "virtualKey": "A" }, 88 | "KeyS": { "text": "s", "virtualKey": "S" }, 89 | "KeyD": { "text": "d", "virtualKey": "D" }, 90 | "KeyF": { "text": "f", "virtualKey": "F" }, 91 | "KeyG": { "text": "g", "virtualKey": "G" }, 92 | "KeyH": { "text": "h", "virtualKey": "H" }, 93 | "KeyJ": { "text": "j", "virtualKey": "J" }, 94 | "KeyK": { "text": "k", "virtualKey": "K" }, 95 | "KeyL": { "text": "l", "virtualKey": "L" }, 96 | "Semicolon": { "text": ";", "virtualKey": "Oem1" }, 97 | "Quote": { "text": "'", "virtualKey": "Oem7" }, 98 | "Backquote": { "text": "`", "virtualKey": "Oem3" }, 99 | "Backslash": { "text": "\\", "virtualKey": "Oem5" }, 100 | "KeyZ": { "text": "z", "virtualKey": "Z" }, 101 | "KeyX": { "text": "x", "virtualKey": "X" }, 102 | "KeyC": { "text": "c", "virtualKey": "C" }, 103 | "KeyV": { "text": "v", "virtualKey": "V" }, 104 | "KeyB": { "text": "b", "virtualKey": "B" }, 105 | "KeyN": { "text": "n", "virtualKey": "N" }, 106 | "KeyM": { "text": "m", "virtualKey": "M" }, 107 | "Comma": { "text": ",", "virtualKey": "OemComma" }, 108 | "Period": { "text": ".", "virtualKey": "OemPeriod" }, 109 | "Slash": { "text": "-", "virtualKey": "Oem2" }, 110 | "Space": { "text": " ", "virtualKey": "Space" }, 111 | "IntlBackslash": { "text": "\\", "virtualKey": "Oem102" } 112 | }, 113 | "modifiers": [[]], 114 | "includes": "default" 115 | }, 116 | "mod2": { 117 | "mapping": { 118 | "Digit1": { "text": "!", "virtualKey": "1" }, 119 | "Digit2": { "text": "@", "virtualKey": "2" }, 120 | "Digit3": { "text": "#", "virtualKey": "3" }, 121 | "Digit4": { "text": "$", "virtualKey": "4" }, 122 | "Digit5": { "text": "%", "virtualKey": "5" }, 123 | "Digit6": { "text": "^", "virtualKey": "6" }, 124 | "Digit7": { "text": "&", "virtualKey": "7" }, 125 | "Digit8": { "text": "*", "virtualKey": "8" }, 126 | "Digit9": { "text": "(", "virtualKey": "9" }, 127 | "Digit0": { "text": ")", "virtualKey": "0" }, 128 | "Minus": { "text": "_", "virtualKey": "OemMinus" }, 129 | "Equal": { "text": "+", "virtualKey": "OemPlus" }, 130 | "KeyQ": { "text": "Q", "virtualKey": "Q" }, 131 | "KeyW": { "text": "W", "virtualKey": "W" }, 132 | "KeyE": { "text": "E", "virtualKey": "E" }, 133 | "KeyR": { "text": "R", "virtualKey": "R" }, 134 | "KeyT": { "text": "T", "virtualKey": "T" }, 135 | "KeyY": { "text": "Y", "virtualKey": "Y" }, 136 | "KeyU": { "text": "U", "virtualKey": "U" }, 137 | "KeyI": { "text": "I", "virtualKey": "I" }, 138 | "KeyO": { "text": "O", "virtualKey": "O" }, 139 | "KeyP": { "text": "P", "virtualKey": "P" }, 140 | "BracketLeft": { "text": "{", "virtualKey": "Oem4" }, 141 | "BracketRight": { "text": "}", "virtualKey": "Oem6" }, 142 | "KeyA": { "text": "A", "virtualKey": "A" }, 143 | "KeyS": { "text": "S", "virtualKey": "S" }, 144 | "KeyD": { "text": "D", "virtualKey": "D" }, 145 | "KeyF": { "text": "F", "virtualKey": "F" }, 146 | "KeyG": { "text": "G", "virtualKey": "G" }, 147 | "KeyH": { "text": "H", "virtualKey": "H" }, 148 | "KeyJ": { "text": "J", "virtualKey": "J" }, 149 | "KeyK": { "text": "K", "virtualKey": "K" }, 150 | "KeyL": { "text": "L", "virtualKey": "L" }, 151 | "Semicolon": { "text": ":", "virtualKey": "Oem1" }, 152 | "Quote": { "text": "\"", "virtualKey": "Oem7" }, 153 | "Backquote": { "text": "~", "virtualKey": "Oem3" }, 154 | "Backslash": { "text": "|", "virtualKey": "Oem5" }, 155 | "KeyZ": { "text": "Z", "virtualKey": "Z" }, 156 | "KeyX": { "text": "X", "virtualKey": "X" }, 157 | "KeyC": { "text": "C", "virtualKey": "C" }, 158 | "KeyV": { "text": "V", "virtualKey": "V" }, 159 | "KeyB": { "text": "B", "virtualKey": "B" }, 160 | "KeyN": { "text": "N", "virtualKey": "N" }, 161 | "KeyM": { "text": "M", "virtualKey": "M" }, 162 | "Comma": { "text": "<", "virtualKey": "OemComma" }, 163 | "Period": { "text": ">", "virtualKey": "OemPeriod" }, 164 | "Slash": { "text": "?", "virtualKey": "Oem2" }, 165 | "Space": { "text": " ", "virtualKey": "Space" }, 166 | "IntlBackslash": { "text": "|", "virtualKey": "Oem102" } 167 | }, 168 | "modifiers": [["ShiftRight"], ["ShiftLeft"]], 169 | "includes": "default" 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /frontend/data/physical-layouts/ansi.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ANSI", 3 | "width": 1440, 4 | "height": 450, 5 | "keys": [ 6 | { "physicalKey": "Escape", "x": 30, "y": 30, "width": 60, "height": 60 }, 7 | { "physicalKey": "Backquote", "x": 30, "y": 120, "width": 60, "height": 60 }, 8 | { "physicalKey": "F1", "x": 150, "y": 30, "width": 60, "height": 60 }, 9 | { "physicalKey": "F2", "x": 210, "y": 30, "width": 60, "height": 60 }, 10 | { "physicalKey": "F3", "x": 270, "y": 30, "width": 60, "height": 60 }, 11 | { "physicalKey": "F4", "x": 330, "y": 30, "width": 60, "height": 60 }, 12 | { "physicalKey": "Digit1", "x": 90, "y": 120, "width": 60, "height": 60 }, 13 | { "physicalKey": "Digit2", "x": 150, "y": 120, "width": 60, "height": 60 }, 14 | { "physicalKey": "Digit3", "x": 210, "y": 120, "width": 60, "height": 60 }, 15 | { "physicalKey": "Digit4", "x": 270, "y": 120, "width": 60, "height": 60 }, 16 | { "physicalKey": "Digit5", "x": 330, "y": 120, "width": 60, "height": 60 }, 17 | { "physicalKey": "Digit6", "x": 390, "y": 120, "width": 60, "height": 60 }, 18 | { "physicalKey": "Digit7", "x": 450, "y": 120, "width": 60, "height": 60 }, 19 | { "physicalKey": "Digit8", "x": 510, "y": 120, "width": 60, "height": 60 }, 20 | { "physicalKey": "Digit9", "x": 570, "y": 120, "width": 60, "height": 60 }, 21 | { "physicalKey": "Digit0", "x": 630, "y": 120, "width": 60, "height": 60 }, 22 | { "physicalKey": "F5", "x": 420, "y": 30, "width": 60, "height": 60 }, 23 | { "physicalKey": "F6", "x": 480, "y": 30, "width": 60, "height": 60 }, 24 | { "physicalKey": "F7", "x": 540, "y": 30, "width": 60, "height": 60 }, 25 | { "physicalKey": "F8", "x": 600, "y": 30, "width": 60, "height": 60 }, 26 | { "physicalKey": "F9", "x": 690, "y": 30, "width": 60, "height": 60 }, 27 | { "physicalKey": "F10", "x": 750, "y": 30, "width": 60, "height": 60 }, 28 | { "physicalKey": "F11", "x": 810, "y": 30, "width": 60, "height": 60 }, 29 | { "physicalKey": "F12", "x": 870, "y": 30, "width": 60, "height": 60 }, 30 | { "physicalKey": "KeyQ", "x": 120, "y": 180, "width": 60, "height": 60 }, 31 | { "physicalKey": "KeyW", "x": 180, "y": 180, "width": 60, "height": 60 }, 32 | { "physicalKey": "KeyE", "x": 240, "y": 180, "width": 60, "height": 60 }, 33 | { "physicalKey": "KeyR", "x": 300, "y": 180, "width": 60, "height": 60 }, 34 | { "physicalKey": "KeyT", "x": 360, "y": 180, "width": 60, "height": 60 }, 35 | { "physicalKey": "KeyY", "x": 420, "y": 180, "width": 60, "height": 60 }, 36 | { "physicalKey": "KeyU", "x": 480, "y": 180, "width": 60, "height": 60 }, 37 | { "physicalKey": "KeyI", "x": 540, "y": 180, "width": 60, "height": 60 }, 38 | { "physicalKey": "KeyO", "x": 600, "y": 180, "width": 60, "height": 60 }, 39 | { "physicalKey": "KeyP", "x": 660, "y": 180, "width": 60, "height": 60 }, 40 | { "physicalKey": "BracketLeft", "x": 720, "y": 180, "width": 60, "height": 60 }, 41 | { "physicalKey": "Tab", "x": 30, "y": 180, "width": 90, "height": 60 }, 42 | { "physicalKey": "KeyA", "x": 135, "y": 240, "width": 60, "height": 60 }, 43 | { "physicalKey": "KeyS", "x": 195, "y": 240, "width": 60, "height": 60 }, 44 | { "physicalKey": "KeyD", "x": 255, "y": 240, "width": 60, "height": 60 }, 45 | { "physicalKey": "KeyF", "x": 315, "y": 240, "width": 60, "height": 60 }, 46 | { "physicalKey": "KeyG", "x": 375, "y": 240, "width": 60, "height": 60 }, 47 | { "physicalKey": "KeyH", "x": 435, "y": 240, "width": 60, "height": 60 }, 48 | { "physicalKey": "KeyJ", "x": 495, "y": 240, "width": 60, "height": 60 }, 49 | { "physicalKey": "KeyK", "x": 555, "y": 240, "width": 60, "height": 60 }, 50 | { "physicalKey": "KeyL", "x": 615, "y": 240, "width": 60, "height": 60 }, 51 | { "physicalKey": "Semicolon", "x": 675, "y": 240, "width": 60, "height": 60 }, 52 | { "physicalKey": "Quote", "x": 735, "y": 240, "width": 60, "height": 60 }, 53 | { "physicalKey": "KeyZ", "x": 165, "y": 300, "width": 60, "height": 60 }, 54 | { "physicalKey": "KeyX", "x": 225, "y": 300, "width": 60, "height": 60 }, 55 | { "physicalKey": "KeyC", "x": 285, "y": 300, "width": 60, "height": 60 }, 56 | { "physicalKey": "KeyV", "x": 345, "y": 300, "width": 60, "height": 60 }, 57 | { "physicalKey": "KeyB", "x": 405, "y": 300, "width": 60, "height": 60 }, 58 | { "physicalKey": "KeyN", "x": 465, "y": 300, "width": 60, "height": 60 }, 59 | { "physicalKey": "KeyM", "x": 525, "y": 300, "width": 60, "height": 60 }, 60 | { "physicalKey": "Comma", "x": 585, "y": 300, "width": 60, "height": 60 }, 61 | { "physicalKey": "Period", "x": 645, "y": 300, "width": 60, "height": 60 }, 62 | { "physicalKey": "Slash", "x": 705, "y": 300, "width": 60, "height": 60 }, 63 | { "physicalKey": "CapsLock", "x": 30, "y": 240, "width": 105, "height": 60 }, 64 | { "physicalKey": "ControlLeft", "x": 30, "y": 360, "width": 75, "height": 60 }, 65 | { "physicalKey": "0x54", "x": 960, "y": 30, "width": 60, "height": 60 }, 66 | { "physicalKey": "0x46", "x": 1020, "y": 30, "width": 60, "height": 60 }, 67 | { "physicalKey": "Pause", "x": 1080, "y": 30, "width": 60, "height": 60 }, 68 | { "physicalKey": "Minus", "x": 690, "y": 120, "width": 60, "height": 60 }, 69 | { "physicalKey": "Equal", "x": 750, "y": 120, "width": 60, "height": 60 }, 70 | { "physicalKey": "Backspace", "x": 810, "y": 120, "width": 120, "height": 60 }, 71 | { "physicalKey": "BracketRight", "x": 780, "y": 180, "width": 60, "height": 60 }, 72 | { "physicalKey": "Backslash", "x": 840, "y": 180, "width": 90, "height": 60 }, 73 | { "physicalKey": "ShiftRight", "x": 765, "y": 300, "width": 165, "height": 60 }, 74 | { "physicalKey": "ShiftLeft", "x": 30, "y": 300, "width": 135, "height": 60 }, 75 | { "physicalKey": "MetaLeft", "x": 105, "y": 360, "width": 75, "height": 60 }, 76 | { "physicalKey": "AltLeft", "x": 180, "y": 360, "width": 75, "height": 60 }, 77 | { "physicalKey": "Space", "x": 255, "y": 360, "width": 375, "height": 60 }, 78 | { "physicalKey": "0xE038", "x": 630, "y": 360, "width": 75, "height": 60 }, 79 | { "physicalKey": "AltRight", "x": 705, "y": 360, "width": 75, "height": 60 }, 80 | { "physicalKey": "0xE05D", "x": 780, "y": 360, "width": 75, "height": 60 }, 81 | { "physicalKey": "ControlRight", "x": 855, "y": 360, "width": 75, "height": 60 }, 82 | { "physicalKey": "Enter", "x": 795, "y": 240, "width": 135, "height": 60 }, 83 | { "physicalKey": "Insert", "x": 960, "y": 120, "width": 60, "height": 60 }, 84 | { "physicalKey": "Home", "x": 1020, "y": 120, "width": 60, "height": 60 }, 85 | { "physicalKey": "PageUp", "x": 1080, "y": 120, "width": 60, "height": 60 }, 86 | { "physicalKey": "Delete", "x": 960, "y": 180, "width": 60, "height": 60 }, 87 | { "physicalKey": "End", "x": 1020, "y": 180, "width": 60, "height": 60 }, 88 | { "physicalKey": "PageDown", "x": 1080, "y": 180, "width": 60, "height": 60 }, 89 | { "physicalKey": "ArrowLeft", "x": 960, "y": 360, "width": 60, "height": 60 }, 90 | { "physicalKey": "ArrowDown", "x": 1020, "y": 360, "width": 60, "height": 60 }, 91 | { "physicalKey": "ArrowRight", "x": 1080, "y": 360, "width": 60, "height": 60 }, 92 | { "physicalKey": "ArrowUp", "x": 1020, "y": 300, "width": 60, "height": 60 }, 93 | { "physicalKey": "NumLock", "x": 1170, "y": 120, "width": 60, "height": 60 }, 94 | { "physicalKey": "NumpadDivide", "x": 1230, "y": 120, "width": 60, "height": 60 }, 95 | { "physicalKey": "NumpadMultiply", "x": 1290, "y": 120, "width": 60, "height": 60 }, 96 | { "physicalKey": "NumpadSubtract", "x": 1350, "y": 120, "width": 60, "height": 60 }, 97 | { "physicalKey": "Numpad7", "x": 1170, "y": 180, "width": 60, "height": 60 }, 98 | { "physicalKey": "Numpad8", "x": 1230, "y": 180, "width": 60, "height": 60 }, 99 | { "physicalKey": "Numpad9", "x": 1290, "y": 180, "width": 60, "height": 60 }, 100 | { "physicalKey": "Numpad4", "x": 1170, "y": 240, "width": 60, "height": 60 }, 101 | { "physicalKey": "Numpad5", "x": 1230, "y": 240, "width": 60, "height": 60 }, 102 | { "physicalKey": "Numpad6", "x": 1290, "y": 240, "width": 60, "height": 60 }, 103 | { "physicalKey": "Numpad1", "x": 1170, "y": 300, "width": 60, "height": 60 }, 104 | { "physicalKey": "Numpad2", "x": 1230, "y": 300, "width": 60, "height": 60 }, 105 | { "physicalKey": "Numpad3", "x": 1290, "y": 300, "width": 60, "height": 60 }, 106 | { "physicalKey": "NumpadDecimal", "x": 1290, "y": 360, "width": 60, "height": 60 }, 107 | { "physicalKey": "Numpad0", "x": 1170, "y": 360, "width": 120, "height": 60 }, 108 | { "physicalKey": "NumpadEnter", "x": 1350, "y": 300, "width": 60, "height": 120 }, 109 | { "physicalKey": "NumpadAdd", "x": 1350, "y": 180, "width": 60, "height": 120 } 110 | ] 111 | } 112 | -------------------------------------------------------------------------------- /frontend/data/physical-layouts/extract-from-drawio.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync, writeFileSync } from "fs"; 2 | import { PhysicalLayoutData } from "../../src/model/Keyboard"; 3 | 4 | const r = /value="(.*?)".*?\n.*x="(.*?)" y="(.*?)" width="(.*?)" height="(.*?)"/g; 5 | const str = readFileSync("./ansi.drawio.xml", { encoding: "utf8" }); 6 | 7 | /* 8 | const r2 = /value="(.*?)"/g; 9 | const newStr = str.replace(r2, (val, ...args) => { 10 | const v = args[0]; 11 | const s = ScanCode.from(v); 12 | const v2 = getJsCodeFromScanCode(s); 13 | if (v2) { 14 | return `value="${v2}"`; 15 | } 16 | return val; 17 | }); 18 | 19 | writeFileSync("./iso.drawio.xml", newStr, { encoding: "utf8" }); 20 | 21 | throw ""; 22 | */ 23 | 24 | const extractionResults: { 25 | value: string; 26 | x: number; 27 | y: number; 28 | width: number; 29 | height: number; 30 | }[] = []; 31 | 32 | while (true) { 33 | const m = r.exec(str); 34 | if (!m) { 35 | break; 36 | } 37 | 38 | const [_, value, xStr, yStr, widthStr, heightStr] = m; 39 | 40 | const x = parseInt(xStr, 10); 41 | const y = parseInt(yStr, 10); 42 | const width = parseInt(widthStr, 10); 43 | const height = parseInt(heightStr, 10); 44 | extractionResults.push({ 45 | value, 46 | x, 47 | y, 48 | width, 49 | height, 50 | }); 51 | } 52 | 53 | const keyboard = extractionResults.find(r => r.value === "keyboard")!; 54 | 55 | const keyboardDef: PhysicalLayoutData = { 56 | name: "test", 57 | width: keyboard.width, 58 | height: keyboard.height, 59 | keys: [], 60 | }; 61 | 62 | for (const r of extractionResults) { 63 | if (r.value === "keyboard") { 64 | continue; 65 | } 66 | keyboardDef.keys.push({ 67 | physicalKey: r.value, 68 | x: r.x - keyboard.x, 69 | y: r.y - keyboard.y, 70 | width: r.width, 71 | height: r.height, 72 | }); 73 | } 74 | 75 | const outStr = JSON.stringify(keyboardDef); 76 | writeFileSync("./out.json", outStr); 77 | -------------------------------------------------------------------------------- /frontend/data/physical-layouts/iso.drawio.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | 273 | 274 | 275 | 276 | 277 | 278 | 279 | 280 | 281 | 282 | 283 | 284 | 285 | 286 | 287 | 288 | 289 | 290 | 291 | 292 | 293 | 294 | 295 | 296 | 297 | 298 | 299 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308 | 309 | 310 | 311 | 312 | 313 | 314 | 315 | 316 | 317 | 318 | 319 | 320 | 321 | 322 | 323 | 324 | 325 | 326 | 327 | 328 | 329 | 330 | -------------------------------------------------------------------------------- /frontend/data/physical-layouts/iso.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ISO", 3 | "width": 1440, 4 | "height": 450, 5 | "keys": [ 6 | { "physicalKey": "Escape", "x": 30, "y": 30, "width": 60, "height": 60 }, 7 | { "physicalKey": "Backquote", "x": 30, "y": 120, "width": 60, "height": 60 }, 8 | { "physicalKey": "F1", "x": 150, "y": 30, "width": 60, "height": 60 }, 9 | { "physicalKey": "F2", "x": 210, "y": 30, "width": 60, "height": 60 }, 10 | { "physicalKey": "F3", "x": 270, "y": 30, "width": 60, "height": 60 }, 11 | { "physicalKey": "F4", "x": 330, "y": 30, "width": 60, "height": 60 }, 12 | { "physicalKey": "Digit1", "x": 90, "y": 120, "width": 60, "height": 60 }, 13 | { "physicalKey": "Digit2", "x": 150, "y": 120, "width": 60, "height": 60 }, 14 | { "physicalKey": "Digit3", "x": 210, "y": 120, "width": 60, "height": 60 }, 15 | { "physicalKey": "Digit4", "x": 270, "y": 120, "width": 60, "height": 60 }, 16 | { "physicalKey": "Digit5", "x": 330, "y": 120, "width": 60, "height": 60 }, 17 | { "physicalKey": "Digit6", "x": 390, "y": 120, "width": 60, "height": 60 }, 18 | { "physicalKey": "Digit7", "x": 450, "y": 120, "width": 60, "height": 60 }, 19 | { "physicalKey": "Digit8", "x": 510, "y": 120, "width": 60, "height": 60 }, 20 | { "physicalKey": "Digit9", "x": 570, "y": 120, "width": 60, "height": 60 }, 21 | { "physicalKey": "Digit0", "x": 630, "y": 120, "width": 60, "height": 60 }, 22 | { "physicalKey": "F5", "x": 420, "y": 30, "width": 60, "height": 60 }, 23 | { "physicalKey": "F6", "x": 480, "y": 30, "width": 60, "height": 60 }, 24 | { "physicalKey": "F7", "x": 540, "y": 30, "width": 60, "height": 60 }, 25 | { "physicalKey": "F8", "x": 600, "y": 30, "width": 60, "height": 60 }, 26 | { "physicalKey": "F9", "x": 690, "y": 30, "width": 60, "height": 60 }, 27 | { "physicalKey": "F10", "x": 750, "y": 30, "width": 60, "height": 60 }, 28 | { "physicalKey": "F11", "x": 810, "y": 30, "width": 60, "height": 60 }, 29 | { "physicalKey": "F12", "x": 870, "y": 30, "width": 60, "height": 60 }, 30 | { "physicalKey": "KeyQ", "x": 120, "y": 180, "width": 60, "height": 60 }, 31 | { "physicalKey": "KeyW", "x": 180, "y": 180, "width": 60, "height": 60 }, 32 | { "physicalKey": "KeyE", "x": 240, "y": 180, "width": 60, "height": 60 }, 33 | { "physicalKey": "KeyR", "x": 300, "y": 180, "width": 60, "height": 60 }, 34 | { "physicalKey": "KeyT", "x": 360, "y": 180, "width": 60, "height": 60 }, 35 | { "physicalKey": "KeyY", "x": 420, "y": 180, "width": 60, "height": 60 }, 36 | { "physicalKey": "KeyU", "x": 480, "y": 180, "width": 60, "height": 60 }, 37 | { "physicalKey": "KeyI", "x": 540, "y": 180, "width": 60, "height": 60 }, 38 | { "physicalKey": "KeyO", "x": 600, "y": 180, "width": 60, "height": 60 }, 39 | { "physicalKey": "KeyP", "x": 660, "y": 180, "width": 60, "height": 60 }, 40 | { "physicalKey": "BracketLeft", "x": 720, "y": 180, "width": 60, "height": 60 }, 41 | { "physicalKey": "Tab", "x": 30, "y": 180, "width": 90, "height": 60 }, 42 | { "physicalKey": "KeyA", "x": 135, "y": 240, "width": 60, "height": 60 }, 43 | { "physicalKey": "KeyS", "x": 195, "y": 240, "width": 60, "height": 60 }, 44 | { "physicalKey": "KeyD", "x": 255, "y": 240, "width": 60, "height": 60 }, 45 | { "physicalKey": "KeyF", "x": 315, "y": 240, "width": 60, "height": 60 }, 46 | { "physicalKey": "KeyG", "x": 375, "y": 240, "width": 60, "height": 60 }, 47 | { "physicalKey": "KeyH", "x": 435, "y": 240, "width": 60, "height": 60 }, 48 | { "physicalKey": "KeyJ", "x": 495, "y": 240, "width": 60, "height": 60 }, 49 | { "physicalKey": "KeyK", "x": 555, "y": 240, "width": 60, "height": 60 }, 50 | { "physicalKey": "KeyL", "x": 615, "y": 240, "width": 60, "height": 60 }, 51 | { "physicalKey": "Semicolon", "x": 675, "y": 240, "width": 60, "height": 60 }, 52 | { "physicalKey": "Quote", "x": 735, "y": 240, "width": 60, "height": 60 }, 53 | { "physicalKey": "IntlBackslash", "x": 105, "y": 300, "width": 60, "height": 60 }, 54 | { "physicalKey": "KeyZ", "x": 165, "y": 300, "width": 60, "height": 60 }, 55 | { "physicalKey": "KeyX", "x": 225, "y": 300, "width": 60, "height": 60 }, 56 | { "physicalKey": "KeyC", "x": 285, "y": 300, "width": 60, "height": 60 }, 57 | { "physicalKey": "KeyV", "x": 345, "y": 300, "width": 60, "height": 60 }, 58 | { "physicalKey": "KeyB", "x": 405, "y": 300, "width": 60, "height": 60 }, 59 | { "physicalKey": "KeyN", "x": 465, "y": 300, "width": 60, "height": 60 }, 60 | { "physicalKey": "KeyM", "x": 525, "y": 300, "width": 60, "height": 60 }, 61 | { "physicalKey": "Comma", "x": 585, "y": 300, "width": 60, "height": 60 }, 62 | { "physicalKey": "Period", "x": 645, "y": 300, "width": 60, "height": 60 }, 63 | { "physicalKey": "Slash", "x": 705, "y": 300, "width": 60, "height": 60 }, 64 | { "physicalKey": "CapsLock", "x": 30, "y": 240, "width": 105, "height": 60 }, 65 | { "physicalKey": "ControlLeft", "x": 30, "y": 360, "width": 75, "height": 60 }, 66 | { "physicalKey": "0x54", "x": 960, "y": 30, "width": 60, "height": 60 }, 67 | { "physicalKey": "0x46", "x": 1020, "y": 30, "width": 60, "height": 60 }, 68 | { "physicalKey": "Pause", "x": 1080, "y": 30, "width": 60, "height": 60 }, 69 | { "physicalKey": "Minus", "x": 690, "y": 120, "width": 60, "height": 60 }, 70 | { "physicalKey": "Equal", "x": 750, "y": 120, "width": 60, "height": 60 }, 71 | { "physicalKey": "Backspace", "x": 810, "y": 120, "width": 120, "height": 60 }, 72 | { "physicalKey": "BracketRight", "x": 780, "y": 180, "width": 60, "height": 60 }, 73 | { "physicalKey": "Backslash", "x": 795, "y": 240, "width": 60, "height": 60 }, 74 | { "physicalKey": "ShiftRight", "x": 765, "y": 300, "width": 165, "height": 60 }, 75 | { "physicalKey": "ShiftLeft", "x": 30, "y": 300, "width": 75, "height": 60 }, 76 | { "physicalKey": "MetaLeft", "x": 105, "y": 360, "width": 75, "height": 60 }, 77 | { "physicalKey": "AltLeft", "x": 180, "y": 360, "width": 75, "height": 60 }, 78 | { "physicalKey": "Space", "x": 255, "y": 360, "width": 375, "height": 60 }, 79 | { "physicalKey": "0xE038", "x": 630, "y": 360, "width": 75, "height": 60 }, 80 | { "physicalKey": "AltRight", "x": 705, "y": 360, "width": 75, "height": 60 }, 81 | { "physicalKey": "0xE05D", "x": 780, "y": 360, "width": 75, "height": 60 }, 82 | { "physicalKey": "ControlRight", "x": 855, "y": 360, "width": 75, "height": 60 }, 83 | { "physicalKey": "Enter", "x": 855, "y": 180, "width": 75, "height": 120 }, 84 | { "physicalKey": "Insert", "x": 960, "y": 120, "width": 60, "height": 60 }, 85 | { "physicalKey": "Home", "x": 1020, "y": 120, "width": 60, "height": 60 }, 86 | { "physicalKey": "PageUp", "x": 1080, "y": 120, "width": 60, "height": 60 }, 87 | { "physicalKey": "Delete", "x": 960, "y": 180, "width": 60, "height": 60 }, 88 | { "physicalKey": "End", "x": 1020, "y": 180, "width": 60, "height": 60 }, 89 | { "physicalKey": "PageDown", "x": 1080, "y": 180, "width": 60, "height": 60 }, 90 | { "physicalKey": "ArrowLeft", "x": 960, "y": 360, "width": 60, "height": 60 }, 91 | { "physicalKey": "ArrowDown", "x": 1020, "y": 360, "width": 60, "height": 60 }, 92 | { "physicalKey": "ArrowRight", "x": 1080, "y": 360, "width": 60, "height": 60 }, 93 | { "physicalKey": "ArrowUp", "x": 1020, "y": 300, "width": 60, "height": 60 }, 94 | { "physicalKey": "NumLock", "x": 1170, "y": 120, "width": 60, "height": 60 }, 95 | { "physicalKey": "NumpadDivide", "x": 1230, "y": 120, "width": 60, "height": 60 }, 96 | { "physicalKey": "NumpadMultiply", "x": 1290, "y": 120, "width": 60, "height": 60 }, 97 | { "physicalKey": "NumpadSubtract", "x": 1350, "y": 120, "width": 60, "height": 60 }, 98 | { "physicalKey": "Numpad7", "x": 1170, "y": 180, "width": 60, "height": 60 }, 99 | { "physicalKey": "Numpad8", "x": 1230, "y": 180, "width": 60, "height": 60 }, 100 | { "physicalKey": "Numpad9", "x": 1290, "y": 180, "width": 60, "height": 60 }, 101 | { "physicalKey": "Numpad4", "x": 1170, "y": 240, "width": 60, "height": 60 }, 102 | { "physicalKey": "Numpad5", "x": 1230, "y": 240, "width": 60, "height": 60 }, 103 | { "physicalKey": "Numpad6", "x": 1290, "y": 240, "width": 60, "height": 60 }, 104 | { "physicalKey": "Numpad1", "x": 1170, "y": 300, "width": 60, "height": 60 }, 105 | { "physicalKey": "Numpad2", "x": 1230, "y": 300, "width": 60, "height": 60 }, 106 | { "physicalKey": "Numpad3", "x": 1290, "y": 300, "width": 60, "height": 60 }, 107 | { "physicalKey": "NumpadDecimal", "x": 1290, "y": 360, "width": 60, "height": 60 }, 108 | { "physicalKey": "Numpad0", "x": 1170, "y": 360, "width": 120, "height": 60 }, 109 | { "physicalKey": "NumpadEnter", "x": 1350, "y": 300, "width": 60, "height": 120 }, 110 | { "physicalKey": "NumpadAdd", "x": 1350, "y": 180, "width": 60, "height": 120 } 111 | ] 112 | } 113 | -------------------------------------------------------------------------------- /frontend/index.d.ts: -------------------------------------------------------------------------------- 1 | export const distPath: string; 2 | -------------------------------------------------------------------------------- /frontend/index.js: -------------------------------------------------------------------------------- 1 | // This is the entry point when this package is used as library from the extension. 2 | // This package MUST NOT be bundled. 3 | const path = require("path"); 4 | 5 | module.exports.distPath = path.join(__dirname, "dist"); 6 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hediet/visual-keyboard-frontend", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "webpack-dev-server --hot", 6 | "build": "webpack", 7 | "pub": "gh-pages -d dist" 8 | }, 9 | "main": "./index", 10 | "types": "./index.d.ts", 11 | "dependencies": { 12 | "@blueprintjs/core": "^3.24.0", 13 | "@blueprintjs/select": "^3.10.0", 14 | "@hediet/std": "^0.6.0", 15 | "@types/node": "^13.7.0", 16 | "base64-js": "^1.3.1", 17 | "blueprintjs": "^0.0.8", 18 | "classnames": "^2.2.6", 19 | "mobx": "^5.10.1", 20 | "mobx-react": "^6.1.1", 21 | "popper.js": "^1.15.0", 22 | "react": "^16.8.6", 23 | "react-dom": "^16.8.6", 24 | "simple-icons": "^2.5.0", 25 | "@hediet/typed-json-rpc": "^0.7.7", 26 | "@hediet/typed-json-rpc-websocket": "^0.7.7" 27 | }, 28 | "devDependencies": { 29 | "terser-webpack-plugin": "^2.3.5", 30 | "@types/terser-webpack-plugin": "^2.2.0", 31 | "@types/classnames": "^2.2.8", 32 | "@types/html-webpack-plugin": "^3.2.0", 33 | "@types/react": "^16.8.21", 34 | "@types/react-dom": "^16.8.4", 35 | "@types/webpack": "^4.4.33", 36 | "clean-webpack-plugin": "^3.0.0", 37 | "css-loader": "^3.0.0", 38 | "file-loader": "^4.0.0", 39 | "fork-ts-checker-webpack-plugin": "^1.4.2", 40 | "gh-pages": "^2.1.1", 41 | "html-webpack-plugin": "^3.2.0", 42 | "node-sass": "^4.12.0", 43 | "raw-loader": "^3.0.0", 44 | "sass": "^1.20.1", 45 | "sass-loader": "^7.1.0", 46 | "style-loader": "^0.23.1", 47 | "ts-loader": "^6.0.3", 48 | "ts-node": "^8.3.0", 49 | "typescript": "^3.5.2", 50 | "webpack": "^4.35.0", 51 | "webpack-cli": "^3.3.4", 52 | "webpack-dev-server": "^3.7.2", 53 | "xml2js": "^0.4.23" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /frontend/src/Components/AutoResize.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import { observable, computed } from "mobx"; 4 | import { observer } from "mobx-react"; 5 | 6 | export enum Stretch { 7 | None = 1, 8 | Uniform, 9 | UniformToFill, 10 | Fill, 11 | } 12 | 13 | const actions = new Set<() => void>(); 14 | const resizeObserver = new ResizeObserver(() => { 15 | for (const a of actions) { 16 | a(); 17 | } 18 | }); 19 | 20 | class Size { 21 | constructor( 22 | public readonly width: number, 23 | public readonly height: number 24 | ) {} 25 | } 26 | 27 | class ElementSizeProvider { 28 | @observable public size!: Size; 29 | 30 | constructor(private readonly elem: HTMLElement) { 31 | this.handleResize(); 32 | actions.add(this.handleResize); 33 | resizeObserver.observe(elem); 34 | } 35 | 36 | private readonly handleResize = () => { 37 | this.size = new Size(this.elem.clientWidth, this.elem.clientHeight); 38 | }; 39 | 40 | dispose(): void { 41 | actions.delete(this.handleResize); 42 | resizeObserver.unobserve(this.elem); 43 | } 44 | } 45 | 46 | @observer 47 | export class AutoResize extends React.Component<{ 48 | stretch?: Stretch; 49 | maxZoom?: number; 50 | alignVertical?: "center" | "top"; 51 | }> { 52 | @observable 53 | private availableSpace: ElementSizeProvider | undefined; 54 | @observable 55 | private contentSpace: ElementSizeProvider | undefined; 56 | 57 | render() { 58 | const zoom = this.zoom; 59 | 60 | return ( 61 |
71 |
79 |
86 | {this.props.children} 87 |
88 |
89 |
90 | ); 91 | } 92 | 93 | componentWillUnmount() { 94 | if (this.availableSpace) { 95 | this.availableSpace.dispose(); 96 | } 97 | if (this.contentSpace) { 98 | this.contentSpace.dispose(); 99 | } 100 | } 101 | 102 | @computed.struct 103 | private get zoom(): { x: number; y: number; dx: number; dy: number } { 104 | if (!this.availableSpace || !this.contentSpace) { 105 | return { x: 1, y: 1, dx: 0, dy: 0 }; 106 | } 107 | const availableSpace = this.availableSpace.size; 108 | const contentSpace = this.contentSpace.size; 109 | 110 | let zoomX = availableSpace.width / contentSpace.width; 111 | let zoomY = availableSpace.height / contentSpace.height; 112 | 113 | const maxZoom = this.props.maxZoom || Infinity; 114 | 115 | const stretch = this.props.stretch || Stretch.Uniform; 116 | 117 | if (stretch === Stretch.Uniform) { 118 | zoomX = zoomY = Math.min(zoomX, zoomY); 119 | } else if (stretch === Stretch.UniformToFill) { 120 | zoomX = zoomY = Math.max(zoomX, zoomY); 121 | } 122 | 123 | if (zoomX > maxZoom) { 124 | zoomX = maxZoom; 125 | } 126 | if (zoomY > maxZoom) { 127 | zoomY = maxZoom; 128 | } 129 | 130 | const newContentWidth = contentSpace.width * zoomX; 131 | 132 | const dx = { 133 | center: (availableSpace.width - newContentWidth) / 2, 134 | }["center"]; 135 | 136 | const newContentHeight = contentSpace.height * zoomY; 137 | const dy = { 138 | center: (availableSpace.height - newContentHeight) / 2, 139 | top: 0, 140 | }[this.props.alignVertical || "center"]; 141 | 142 | return { 143 | x: zoomX, 144 | y: zoomY, 145 | dx, 146 | dy, 147 | }; 148 | } 149 | 150 | private readonly setAvailableDiv = (div: HTMLDivElement | null) => { 151 | if (this.availableSpace) { 152 | this.availableSpace.dispose(); 153 | } 154 | if (div) { 155 | this.availableSpace = new ElementSizeProvider(div); 156 | } else { 157 | this.availableSpace = undefined; 158 | } 159 | }; 160 | 161 | private readonly setContentSpaceDiv = (div: HTMLDivElement | null) => { 162 | if (this.contentSpace) { 163 | this.contentSpace.dispose(); 164 | } 165 | if (div) { 166 | this.contentSpace = new ElementSizeProvider(div); 167 | } else { 168 | this.contentSpace = undefined; 169 | } 170 | }; 171 | } 172 | -------------------------------------------------------------------------------- /frontend/src/Components/GUI.tsx: -------------------------------------------------------------------------------- 1 | import { AnchorButton, Button, Checkbox, Tag } from "@blueprintjs/core"; 2 | import { observer } from "mobx-react"; 3 | import * as React from "react"; 4 | import { svg as githubSvg } from "simple-icons/icons/github"; 5 | import { svg as twitterSvg } from "simple-icons/icons/twitter"; 6 | import { FunctionalLayout, PhysicalLayout, Model } from "../Model"; 7 | import { KeyBindingSet } from "../Model/keybindings/KeyBindingsProvider"; 8 | import { AutoResize, Stretch } from "./AutoResize"; 9 | import { KeyboardComponent } from "./KeyboardComponent"; 10 | import { Select } from "./Select"; 11 | 12 | @observer 13 | export class GUI extends React.Component<{ model: Model }, {}> { 14 | render() { 15 | const model = this.props.model; 16 | const headless = model.config.headless; 17 | return ( 18 |
19 | {!headless && ( 20 | <> 21 |
27 |
28 | 29 | value={model.keyboard.physicalLayout} 30 | values={model.physicalLayoutsProvider.getLayouts()} 31 | getLabel={v => `${v.name} Keyboard`} 32 | onChange={e => (model.keyboard.physicalLayout = e)} 33 | /> 34 |
35 |
36 | 37 | value={model.keyboard.functionalLayout} 38 | values={model.functionalLayoutsProvider.getLayouts()} 39 | getLabel={v => `${v.name} Layout`} 40 | onChange={e => model.keyboard.setFunctionalLayout(e)} 41 | /> 42 |
43 |
44 | 45 | value={model.currentKeyBindingSet} 46 | values={model.keyBindingsProvider.getKeyBindingSets()} 47 | getLabel={v => `${v.applicationName} ${v.osName}`} 48 | onChange={e => model.setCurrentKeyBindingSet(e)} 49 | /> 50 |
51 | 52 |
56 | (model.preventDefault = e.currentTarget.checked)} 61 | /> 62 |
63 | 64 | 65 |
66 | 70 | Get The VS Code Extension 71 | 72 |
73 | 74 |
75 | } 77 | href="https://github.com/hediet/visual-keyboard" 78 | > 79 | Github 80 | 81 |
82 |
83 | } 85 | href="https://twitter.com/intent/follow?screen_name=hediet_dev" 86 | > 87 | Twitter 88 | 89 |
90 |
91 | 92 |
93 |

Interactive VS Code Keybindings

94 |
95 | 96 | )} 97 | 98 |
99 | {this.props.model.initialized && ( 100 | 101 | 105 | 106 | )} 107 |
108 | {headless && ( 109 |
117 | 121 |
122 | )} 123 |
124 | ); 125 | } 126 | } 127 | 128 | function SVGImage(props: { src: string }) { 129 | return ( 130 | 138 | ); 139 | } 140 | 141 | @observer 142 | class Tags extends React.Component<{ model: Model; style?: React.CSSProperties }> { 143 | render() { 144 | return ( 145 | <> 146 | {this.props.model.activeKeyBindingsPath.map((key, idx) => ( 147 |
148 | this.props.model.resetCurrentKeyBindingPath()} 153 | /> 154 |
155 | ))} 156 | 157 | ); 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /frontend/src/Components/KeyComponent.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import React = require("react"); 3 | import { Model, Keyboard, PhysicalKeyDef } from "../Model"; 4 | import classNames = require("classnames"); 5 | import { AutoResize, Stretch } from "./AutoResize"; 6 | import { autorun, observable, computed } from "mobx"; 7 | 8 | @observer 9 | export class KeyComponent extends React.Component<{ 10 | model: Model; 11 | keyboard: Keyboard; 12 | keyDef: PhysicalKeyDef; 13 | }> { 14 | //@observable private isPressedDelayed = false; 15 | 16 | @computed get isPressed() { 17 | return this.props.keyboard.isKeyPressed(this.props.keyDef.physicalKey); 18 | } 19 | 20 | /* 21 | TODO find a better way to implement `isPressedDelayed`! 22 | 23 | constructor(props: KeyComponent["props"]) { 24 | super(props); 25 | 26 | let pressed = new Date(); 27 | autorun(() => { 28 | if (this.isPressed) { 29 | this.isPressedDelayed = true; 30 | pressed = new Date(); 31 | } else { 32 | let requiredWaitingTime = 700 - (new Date().getTime() - pressed.getTime()); 33 | 34 | const keyFn = this.props.keyboard.currentFunctionalLayoutState.getFunction( 35 | this.props.keyDef.physicalKey 36 | ); 37 | if ( 38 | keyFn && 39 | keyFn.virtualKey && 40 | this.props.model.isKeyBindingsModifier(keyFn.virtualKey) 41 | ) { 42 | // modifiers don't need delays 43 | requiredWaitingTime = 0; 44 | } 45 | 46 | setTimeout(() => { 47 | this.isPressedDelayed = false; 48 | }, Math.max(0, requiredWaitingTime)); 49 | } 50 | }); 51 | }*/ 52 | 53 | render() { 54 | const { keyDef, keyboard, model } = this.props; 55 | 56 | const keyFn = keyboard.currentFunctionalLayoutState.getFunction(keyDef.physicalKey); 57 | 58 | let action = undefined; 59 | 60 | if (keyFn && keyFn.virtualKey) { 61 | const r = model.findKeyBindings(keyFn.virtualKey); 62 | if (r.bindings.length > 0) { 63 | action = r.bindings.map(b => b.action.shortName)[0]; 64 | } 65 | if (r.followingKeyBindings) { 66 | if (!action) { 67 | action = ""; 68 | } 69 | action += " ..."; 70 | } 71 | } 72 | 73 | let virtualKey; 74 | let text = ""; 75 | if (keyFn) { 76 | if (keyFn.virtualKey) { 77 | text = keyFn.virtualKey.format(); 78 | virtualKey = keyFn.virtualKey.id; 79 | } 80 | if (keyFn.text) { 81 | text = keyFn.text; 82 | } 83 | } 84 | 85 | return ( 86 | 108 | ); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /frontend/src/Components/KeyboardComponent.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import { Keyboard, Model, PhysicalKeyDef } from "../Model"; 3 | import React = require("react"); 4 | import { KeyComponent } from "./KeyComponent"; 5 | 6 | @observer 7 | export class KeyboardComponent extends React.Component<{ keyboard: Keyboard; model: Model }, {}> { 8 | render() { 9 | const kbd = this.props.keyboard; 10 | const l = kbd.physicalLayout; 11 | const f = kbd.functionalLayout; 12 | 13 | function getTag(key: PhysicalKeyDef): string { 14 | const fn = f.defaultState.getFunction(key.physicalKey); 15 | if (fn) { 16 | if (fn.virtualKey) { 17 | return fn.virtualKey.id; 18 | } 19 | } 20 | return key.id; 21 | } 22 | 23 | const set = new Set(); 24 | function getId(key: PhysicalKeyDef): string { 25 | const tag = getTag(key); 26 | let i = 0; 27 | while (true) { 28 | const id = `${tag}${i++}`; 29 | if (!set.has(id)) { 30 | set.add(id); 31 | return id; 32 | } 33 | } 34 | } 35 | 36 | const keys = l.keys; 37 | const sortedKeys = keys 38 | .map(key => ({ key, id: getId(key) })) 39 | .sort((a, b) => a.id.localeCompare(b.id)); 40 | 41 | return ( 42 |
43 | {sortedKeys.map(({ id, key }) => ( 44 |
55 | 56 |
57 | ))} 58 |
59 | ); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /frontend/src/Components/ResizeObserver.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * The **ResizeObserver** interface reports changes to the dimensions of an 3 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element)'s content 4 | * or border box, or the bounding box of an 5 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). 6 | * 7 | * > **Note**: The content box is the box in which content can be placed, 8 | * > meaning the border box minus the padding and border width. The border box 9 | * > encompasses the content, padding, and border. See 10 | * > [The box model](https://developer.mozilla.org/en-US/docs/Learn/CSS/Building_blocks/The_box_model) 11 | * > for further explanation. 12 | * 13 | * `ResizeObserver` avoids infinite callback loops and cyclic dependencies that 14 | * are often created when resizing via a callback function. It does this by only 15 | * processing elements deeper in the DOM in subsequent frames. Implementations 16 | * should, if they follow the specification, invoke resize events before paint 17 | * and after layout. 18 | * 19 | * @see https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver 20 | */ 21 | declare class ResizeObserver { 22 | /** 23 | * The **ResizeObserver** constructor creates a new `ResizeObserver` object, 24 | * which can be used to report changes to the content or border box of an 25 | * `Element` or the bounding box of an `SVGElement`. 26 | * 27 | * @example 28 | * var ResizeObserver = new ResizeObserver(callback) 29 | * 30 | * @param callback 31 | * The function called whenever an observed resize occurs. The function is 32 | * called with two parameters: 33 | * * **entries** 34 | * An array of 35 | * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) 36 | * objects that can be used to access the new dimensions of the element 37 | * after each change. 38 | * * **observer** 39 | * A reference to the `ResizeObserver` itself, so it will definitely be 40 | * accessible from inside the callback, should you need it. This could be 41 | * used for example to automatically unobserve the observer when a certain 42 | * condition is reached, but you can omit it if you don't need it. 43 | * 44 | * The callback will generally follow a pattern along the lines of: 45 | * ```js 46 | * function(entries, observer) { 47 | * for (let entry of entries) { 48 | * // Do something to each entry 49 | * // and possibly something to the observer itself 50 | * } 51 | * } 52 | * ``` 53 | * 54 | * The following snippet is taken from the 55 | * [resize-observer-text.html](https://mdn.github.io/dom-examples/resize-observer/resize-observer-text.html) 56 | * ([see source](https://github.com/mdn/dom-examples/blob/master/resize-observer/resize-observer-text.html)) 57 | * example: 58 | * @example 59 | * const resizeObserver = new ResizeObserver(entries => { 60 | * for (let entry of entries) { 61 | * if(entry.contentBoxSize) { 62 | * h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem'; 63 | * pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem'; 64 | * } else { 65 | * h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem'; 66 | * pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem'; 67 | * } 68 | * } 69 | * }); 70 | * 71 | * resizeObserver.observe(divElem); 72 | */ 73 | constructor(callback: ResizeObserverCallback); 74 | 75 | /** 76 | * The **disconnect()** method of the 77 | * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) 78 | * interface unobserves all observed 79 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 80 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) 81 | * targets. 82 | */ 83 | disconnect: () => void; 84 | 85 | /** 86 | * The `observe()` method of the 87 | * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) 88 | * interface starts observing the specified 89 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 90 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). 91 | * 92 | * @example 93 | * resizeObserver.observe(target, options); 94 | * 95 | * @param target 96 | * A reference to an 97 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 98 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) 99 | * to be observed. 100 | * 101 | * @param options 102 | * An options object allowing you to set options for the observation. 103 | * Currently this only has one possible option that can be set. 104 | */ 105 | observe: (target: Element, options?: ResizeObserverObserveOptions) => void; 106 | 107 | /** 108 | * The **unobserve()** method of the 109 | * [ResizeObserver](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver) 110 | * interface ends the observing of a specified 111 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 112 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement). 113 | */ 114 | unobserve: (target: Element) => void; 115 | } 116 | 117 | interface ResizeObserverObserveOptions { 118 | /** 119 | * Sets which box model the observer will observe changes to. Possible values 120 | * are `content-box` (the default), and `border-box`. 121 | * 122 | * @default "content-box" 123 | */ 124 | box?: "content-box" | "border-box"; 125 | } 126 | 127 | /** 128 | * The function called whenever an observed resize occurs. The function is 129 | * called with two parameters: 130 | * 131 | * @param entries 132 | * An array of 133 | * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) 134 | * objects that can be used to access the new dimensions of the element after 135 | * each change. 136 | * 137 | * @param observer 138 | * A reference to the `ResizeObserver` itself, so it will definitely be 139 | * accessible from inside the callback, should you need it. This could be used 140 | * for example to automatically unobserve the observer when a certain condition 141 | * is reached, but you can omit it if you don't need it. 142 | * 143 | * The callback will generally follow a pattern along the lines of: 144 | * @example 145 | * function(entries, observer) { 146 | * for (let entry of entries) { 147 | * // Do something to each entry 148 | * // and possibly something to the observer itself 149 | * } 150 | * } 151 | * 152 | * @example 153 | * const resizeObserver = new ResizeObserver(entries => { 154 | * for (let entry of entries) { 155 | * if(entry.contentBoxSize) { 156 | * h1Elem.style.fontSize = Math.max(1.5, entry.contentBoxSize.inlineSize/200) + 'rem'; 157 | * pElem.style.fontSize = Math.max(1, entry.contentBoxSize.inlineSize/600) + 'rem'; 158 | * } else { 159 | * h1Elem.style.fontSize = Math.max(1.5, entry.contentRect.width/200) + 'rem'; 160 | * pElem.style.fontSize = Math.max(1, entry.contentRect.width/600) + 'rem'; 161 | * } 162 | * } 163 | * }); 164 | * 165 | * resizeObserver.observe(divElem); 166 | */ 167 | type ResizeObserverCallback = ( 168 | entries: ResizeObserverEntry[], 169 | observer: ResizeObserver 170 | ) => void; 171 | 172 | /** 173 | * The **ResizeObserverEntry** interface represents the object passed to the 174 | * [ResizeObserver()](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserver/ResizeObserver) 175 | * constructor's callback function, which allows you to access the new 176 | * dimensions of the 177 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 178 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) 179 | * being observed. 180 | */ 181 | interface ResizeObserverEntry { 182 | /** 183 | * An object containing the new border box size of the observed element when 184 | * the callback is run. 185 | */ 186 | readonly borderBoxSize: ResizeObserverEntryBoxSize; 187 | 188 | /** 189 | * An object containing the new content box size of the observed element when 190 | * the callback is run. 191 | */ 192 | readonly contentBoxSize: ResizeObserverEntryBoxSize; 193 | 194 | /** 195 | * A [DOMRectReadOnly](https://developer.mozilla.org/en-US/docs/Web/API/DOMRectReadOnly) 196 | * object containing the new size of the observed element when the callback is 197 | * run. Note that this is better supported than the above two properties, but 198 | * it is left over from an earlier implementation of the Resize Observer API, 199 | * is still included in the spec for web compat reasons, and may be deprecated 200 | * in future versions. 201 | */ 202 | // node_modules/typescript/lib/lib.dom.d.ts 203 | readonly contentRect: DOMRectReadOnly; 204 | 205 | /** 206 | * A reference to the 207 | * [Element](https://developer.mozilla.org/en-US/docs/Web/API/Element) or 208 | * [SVGElement](https://developer.mozilla.org/en-US/docs/Web/API/SVGElement) 209 | * being observed. 210 | */ 211 | readonly target: Element; 212 | } 213 | 214 | /** 215 | * The **borderBoxSize** read-only property of the 216 | * [ResizeObserverEntry](https://developer.mozilla.org/en-US/docs/Web/API/ResizeObserverEntry) 217 | * interface returns an object containing the new border box size of the 218 | * observed element when the callback is run. 219 | */ 220 | interface ResizeObserverEntryBoxSize { 221 | /** 222 | * The length of the observed element's border box in the block dimension. For 223 | * boxes with a horizontal 224 | * [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), 225 | * this is the vertical dimension, or height; if the writing-mode is vertical, 226 | * this is the horizontal dimension, or width. 227 | */ 228 | blockSize: number; 229 | 230 | /** 231 | * The length of the observed element's border box in the inline dimension. 232 | * For boxes with a horizontal 233 | * [writing-mode](https://developer.mozilla.org/en-US/docs/Web/CSS/writing-mode), 234 | * this is the horizontal dimension, or width; if the writing-mode is 235 | * vertical, this is the vertical dimension, or height. 236 | */ 237 | inlineSize: number; 238 | } 239 | 240 | interface Window { 241 | ResizeObserver: ResizeObserver; 242 | } 243 | -------------------------------------------------------------------------------- /frontend/src/Components/Select.tsx: -------------------------------------------------------------------------------- 1 | import { observer } from "mobx-react"; 2 | import * as React from "react"; 3 | import { HTMLSelect } from "@blueprintjs/core"; 4 | 5 | export class Select extends React.Component<{ 6 | value: T; 7 | values: T[]; 8 | getLabel: (val: T) => string; 9 | onChange: (newVal: T) => void; 10 | }> { 11 | render() { 12 | const { value, values, onChange, getLabel } = this.props; 13 | return ( 14 | { 17 | const selectedIdx = +e.currentTarget.value; 18 | const selected = values[selectedIdx]; 19 | onChange(selected); 20 | }} 21 | > 22 | {values.map((l, idx) => ( 23 | 26 | ))} 27 | 28 | ); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /frontend/src/Model/Config.ts: -------------------------------------------------------------------------------- 1 | declare const window: Window & { 2 | webViewData?: { serverSecret: string; serverPort: number; headless: boolean }; 3 | }; 4 | 5 | export interface Config { 6 | server?: { secret: string; port: number }; 7 | headless: boolean; 8 | } 9 | 10 | export function getConfig(): Config { 11 | const d = window.webViewData; 12 | if (d) { 13 | return { 14 | server: { 15 | secret: d.serverSecret, 16 | port: d.serverPort, 17 | }, 18 | headless: d.headless, 19 | }; 20 | } 21 | 22 | const searchParams = new URLSearchParams(window.location.search); 23 | 24 | const portStr = searchParams.get("serverPort"); 25 | let port: number | undefined; 26 | if (portStr !== null) { 27 | port = parseInt(portStr); 28 | } 29 | let secret = searchParams.get("serverSecret"); 30 | if (secret === null) { 31 | if (port !== undefined) { 32 | console.error("No Server Secret given."); 33 | } 34 | secret = ""; 35 | } 36 | const headless = searchParams.get("headless") !== null; 37 | 38 | return { 39 | headless, 40 | server: 41 | port === undefined 42 | ? undefined 43 | : { 44 | port, 45 | secret, 46 | }, 47 | }; 48 | } 49 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/FunctionalLayout.ts: -------------------------------------------------------------------------------- 1 | import { PhysicalKey, FunctionSymbol, VirtualKey } from "./primitives"; 2 | 3 | export abstract class FunctionalLayout { 4 | constructor( 5 | public readonly name: string, 6 | public readonly defaultState: FunctionalLayoutState 7 | ) {} 8 | } 9 | 10 | export abstract class FunctionalLayoutState { 11 | public abstract getFunction(scanCode: PhysicalKey): KeyFunction | undefined; 12 | public abstract findFunction( 13 | pred: (f: KeyFunction) => boolean 14 | ): { key: PhysicalKey; function: KeyFunction } | undefined; 15 | } 16 | 17 | export interface KeyFunction { 18 | text?: string; 19 | functionSymbol?: FunctionSymbol; 20 | virtualKey?: VirtualKey; 21 | stateAfterKeyPressed?: FunctionalLayoutState; 22 | stateAfterKeyReleased?: FunctionalLayoutState; 23 | } 24 | 25 | export interface FunctionalLayoutData { 26 | name: string; 27 | modes: Record; 28 | } 29 | 30 | export interface FunctionalLayoutModeData { 31 | includes?: string; 32 | modifiers?: string[][]; 33 | mapping: Record; 34 | } 35 | 36 | export class FunctionalLayoutImpl extends FunctionalLayout { 37 | constructor(data: FunctionalLayoutData) { 38 | const modes = new Map(); 39 | const modifierKeys = new Set(); 40 | function lookupState(name: string): BaseFunctionalLayoutStateImpl { 41 | let v = modes.get(name); 42 | if (!v) { 43 | v = new BaseFunctionalLayoutStateImpl(name, data.modes[name], lookupState); 44 | modes.set(name, v); 45 | } 46 | return v; 47 | } 48 | 49 | for (const modeName of Object.keys(data.modes)) { 50 | const s = lookupState(modeName); 51 | for (const modifierCombination of s.modifiers) { 52 | for (const mod of modifierCombination) { 53 | modifierKeys.add(mod); 54 | } 55 | } 56 | } 57 | 58 | function getState(keys: Set): FunctionalLayoutStateImpl | undefined { 59 | if ([...keys.values()].some(k => !modifierKeys.has(k))) { 60 | return undefined; 61 | } 62 | 63 | let best = undefined; 64 | let length = -1; 65 | for (const m of modes.values()) { 66 | for (const mod of m.modifiers) { 67 | if (mod.size > length && [...mod.values()].every(v => keys.has(v))) { 68 | length = mod.size; 69 | best = new FunctionalLayoutStateImpl(m, keys, getState); 70 | } 71 | } 72 | } 73 | return best; 74 | } 75 | 76 | super(data.name, getState(new Set())!); 77 | } 78 | } 79 | 80 | class BaseFunctionalLayoutStateImpl extends FunctionalLayoutState { 81 | private readonly functions = new Map(); 82 | public readonly modifiers: ReadonlyArray>; 83 | 84 | constructor( 85 | public readonly name: string, 86 | data: FunctionalLayoutModeData, 87 | getLayout: (name: string) => BaseFunctionalLayoutStateImpl 88 | ) { 89 | super(); 90 | 91 | if (data.includes) { 92 | const includes = getLayout(data.includes); 93 | for (const [k, v] of includes.functions) { 94 | this.functions.set(k, v); 95 | } 96 | } 97 | 98 | this.modifiers = data.modifiers 99 | ? data.modifiers.map(m => new Set(m.map(k => PhysicalKey.from(k)))) 100 | : []; 101 | 102 | for (const [scanCodeStr, def] of Object.entries(data.mapping)) { 103 | let f: KeyFunction; 104 | 105 | f = { 106 | text: def.text, 107 | virtualKey: def.virtualKey ? VirtualKey.get(def.virtualKey) : undefined, 108 | }; 109 | 110 | this.functions.set(PhysicalKey.from(scanCodeStr), f); 111 | } 112 | } 113 | 114 | public getFunction(scanCode: PhysicalKey): KeyFunction | undefined { 115 | const f = this.functions.get(scanCode); 116 | return f; 117 | } 118 | 119 | public findFunction( 120 | pred: (f: KeyFunction) => boolean 121 | ): { key: PhysicalKey; function: KeyFunction } | undefined { 122 | const f = [...this.functions.entries()].find(v => pred(v[1])); 123 | if (!f) { 124 | return undefined; 125 | } 126 | return { key: f[0], function: f[1] }; 127 | } 128 | } 129 | 130 | class FunctionalLayoutStateImpl extends FunctionalLayoutState { 131 | constructor( 132 | private readonly base: BaseFunctionalLayoutStateImpl, 133 | private readonly pressedKeys: ReadonlySet, 134 | private readonly getState: (keys: Set) => FunctionalLayoutStateImpl | undefined 135 | ) { 136 | super(); 137 | } 138 | 139 | public getFunction(scanCode: PhysicalKey): KeyFunction | undefined { 140 | let f = this.base.getFunction(scanCode); 141 | 142 | const s1 = new Set(this.pressedKeys); 143 | s1.add(scanCode); 144 | const stateAfterKeyPressed = this.getState(s1); 145 | if (stateAfterKeyPressed && f) { 146 | f.stateAfterKeyPressed = stateAfterKeyPressed; 147 | } 148 | 149 | const s2 = new Set(this.pressedKeys); 150 | if (s2.delete(scanCode)) { 151 | const stateAfterKeyReleased = this.getState(s2); 152 | if (stateAfterKeyReleased && f) { 153 | f.stateAfterKeyReleased = stateAfterKeyReleased; 154 | } 155 | } 156 | 157 | return f; 158 | } 159 | 160 | public findFunction( 161 | pred: (f: KeyFunction) => boolean 162 | ): { key: PhysicalKey; function: KeyFunction } | undefined { 163 | return this.base.findFunction(pred); 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/FunctionalLayoutsProvider.ts: -------------------------------------------------------------------------------- 1 | import { FunctionalLayout, FunctionalLayoutImpl } from "./FunctionalLayout"; 2 | import * as usQwerty from "../../../data/functional-layouts/us.json"; 3 | 4 | export const UsQwertyLayout = new FunctionalLayoutImpl(usQwerty); 5 | 6 | export class FunctionalLayoutsProvider { 7 | private layouts: FunctionalLayout[]; 8 | constructor() { 9 | const context = (require as any).context( 10 | "../../../data/functional-layouts", 11 | true, 12 | /\.json/ 13 | ); 14 | 15 | this.layouts = context.keys().map((filename: string) => { 16 | const content = context(filename); 17 | return new FunctionalLayoutImpl(content); 18 | }); 19 | } 20 | 21 | public findLayout(name: string): FunctionalLayout | undefined { 22 | return this.layouts.find(l => l.name === name); 23 | } 24 | 25 | getLayouts(): FunctionalLayout[] { 26 | return this.layouts; 27 | } 28 | 29 | get defaultLayout(): FunctionalLayout { 30 | return this.layouts[0]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/Keyboard.ts: -------------------------------------------------------------------------------- 1 | import { observable, action, computed } from "mobx"; 2 | import { PhysicalKey, VirtualKey } from "./primitives"; 3 | import { PhysicalLayout } from "./PhysicalLayout"; 4 | import { FunctionalLayout, FunctionalLayoutState, KeyFunction } from "./FunctionalLayout"; 5 | import { EventEmitter } from "@hediet/std/events"; 6 | 7 | export class Keyboard { 8 | @observable 9 | public physicalLayout: PhysicalLayout; 10 | 11 | @observable 12 | private _functionalLayout!: FunctionalLayout; 13 | 14 | public get functionalLayout(): FunctionalLayout { 15 | return this._functionalLayout; 16 | } 17 | 18 | @action 19 | public setFunctionalLayout(newLayout: FunctionalLayout) { 20 | this._functionalLayout = newLayout; 21 | const s = [...this.pressedKeys.values()]; 22 | this.reset(); 23 | for (const k of s) { 24 | this.handleKeyPress(k, "key"); 25 | } 26 | } 27 | 28 | @observable 29 | public currentFunctionalLayoutState!: FunctionalLayoutState; 30 | 31 | @observable 32 | private readonly _pressedKeys = new Map void>(); 33 | 34 | @computed 35 | public get pressedKeys(): ReadonlySet { 36 | return new Set(this._pressedKeys.keys()); 37 | } 38 | 39 | @observable 40 | public readonly pressedVirtualKeys = new Set(); 41 | 42 | private readonly _onKeyPressed = new EventEmitter<{ 43 | mode: "button" | "key"; 44 | key: PhysicalKey; 45 | keyFunction: KeyFunction; 46 | }>(); 47 | public readonly onKeyPressed = this._onKeyPressed.asEvent(); 48 | 49 | constructor(physicalLayout: PhysicalLayout, functionalLayout: FunctionalLayout) { 50 | this.physicalLayout = physicalLayout; 51 | this.setFunctionalLayout(functionalLayout); 52 | } 53 | 54 | @action 55 | public reset() { 56 | this._pressedKeys.clear(); 57 | this.pressedVirtualKeys.clear(); 58 | this.currentFunctionalLayoutState = this.functionalLayout.defaultState; 59 | } 60 | 61 | @action 62 | public handleKeyPress(key: PhysicalKey, mode: "button" | "key"): void { 63 | if (this._pressedKeys.has(key)) { 64 | return; 65 | } 66 | 67 | this._pressedKeys.set(key, () => { 68 | if (f && f.virtualKey) { 69 | this.pressedVirtualKeys.delete(f.virtualKey); 70 | } 71 | }); 72 | 73 | const f = this.currentFunctionalLayoutState.getFunction(key); 74 | 75 | if (f && f.virtualKey) { 76 | this.pressedVirtualKeys.add(f.virtualKey); 77 | } 78 | 79 | if (f && f.stateAfterKeyPressed) { 80 | this.currentFunctionalLayoutState = f.stateAfterKeyPressed; 81 | } 82 | 83 | this._onKeyPressed.emit({ 84 | key: key, 85 | keyFunction: f || {}, 86 | mode, 87 | }); 88 | } 89 | 90 | @action 91 | public handleKeyRelease(key: PhysicalKey, mode: "button" | "key"): void { 92 | const fn = this._pressedKeys.get(key); 93 | if (!fn) { 94 | return; 95 | } 96 | 97 | fn(); 98 | this._pressedKeys.delete(key); 99 | 100 | const f = this.currentFunctionalLayoutState.getFunction(key); 101 | if (f && f.stateAfterKeyReleased) { 102 | this.currentFunctionalLayoutState = f.stateAfterKeyReleased; 103 | } 104 | } 105 | 106 | @action 107 | public handleButtonToggle(key: PhysicalKey): void { 108 | if (this._pressedKeys.has(key)) { 109 | this.handleKeyRelease(key, "button"); 110 | } else { 111 | this.handleKeyPress(key, "button"); 112 | } 113 | } 114 | 115 | public isKeyPressed(keyDef: PhysicalKey): boolean { 116 | return this.pressedKeys.has(keyDef); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/PhysicalLayout.ts: -------------------------------------------------------------------------------- 1 | import { PhysicalKey } from "./primitives"; 2 | 3 | export class PhysicalLayout { 4 | public constructor( 5 | public readonly name: string, 6 | public readonly width: number, 7 | public readonly height: number, 8 | public readonly keys: readonly PhysicalKeyDef[] 9 | ) {} 10 | 11 | public get keysSortedByPosition(): PhysicalKeyDef[] { 12 | return this.keys.slice().sort((a, b) => { 13 | if (a.y !== b.y) { 14 | return a.y - b.y; 15 | } 16 | if (a.x !== b.x) { 17 | return a.x - b.x; 18 | } 19 | return 0; 20 | }); 21 | } 22 | } 23 | 24 | export class PhysicalKeyDef { 25 | constructor( 26 | public readonly id: string, 27 | public readonly x: number, 28 | public readonly y: number, 29 | public readonly width: number, 30 | public readonly height: number, 31 | public readonly physicalKey: PhysicalKey 32 | ) {} 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/PhysicalLayoutsProvider.ts: -------------------------------------------------------------------------------- 1 | import { PhysicalLayout, PhysicalKeyDef } from "./PhysicalLayout"; 2 | import { PhysicalKey } from "./primitives"; 3 | 4 | export class PhysicalLayoutsProvider { 5 | private layouts: PhysicalLayout[]; 6 | constructor() { 7 | const context = (require as any).context("../../../data/physical-layouts", true, /\.json/); 8 | 9 | this.layouts = context.keys().map((filename: string) => { 10 | const content = context(filename); 11 | return getPhysicalKeyboardLayout(content); 12 | }); 13 | } 14 | 15 | public findLayout(name: string): PhysicalLayout | undefined { 16 | return this.layouts.find(l => l.name === name); 17 | } 18 | 19 | public getLayouts(): PhysicalLayout[] { 20 | return this.layouts; 21 | } 22 | 23 | public get defaultLayout(): PhysicalLayout { 24 | return this.layouts[0]; 25 | } 26 | } 27 | 28 | function getPhysicalKeyboardLayout(def: PhysicalLayoutData) { 29 | return new PhysicalLayout( 30 | def.name, 31 | def.width, 32 | def.height, 33 | def.keys.map((k, idx) => { 34 | const physicalKey = PhysicalKey.from(k.physicalKey); 35 | return new PhysicalKeyDef( 36 | physicalKey.toString(), 37 | k.x, 38 | k.y, 39 | k.width, 40 | k.height, 41 | physicalKey 42 | ); 43 | }) 44 | ); 45 | } 46 | 47 | export interface PhysicalLayoutData { 48 | name: string; 49 | width: number; 50 | height: number; 51 | keys: PhysicalKeyDefData[]; 52 | } 53 | 54 | export interface PhysicalKeyDefData { 55 | x: number; 56 | y: number; 57 | width: number; 58 | height: number; 59 | physicalKey: string; 60 | } 61 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/VirtualKey.ts: -------------------------------------------------------------------------------- 1 | export class VirtualKey { 2 | private static readonly instances = new Map(); 3 | 4 | public static register(options: { 5 | id: string; 6 | simpleName?: string; 7 | icon?: string; 8 | alternativeIds?: string[]; 9 | }): VirtualKey { 10 | const id = options.id.toLowerCase(); 11 | const existing = this.instances.get(id); 12 | if (existing) { 13 | throw new Error(`Key with id "${existing.id}" already exists.`); 14 | } 15 | const key = new VirtualKey(options.id, options.simpleName, options.icon); 16 | this.instances.set(id, key); 17 | if (options.alternativeIds) { 18 | for (const a of options.alternativeIds) { 19 | this.instances.set(a.toLowerCase(), key); 20 | } 21 | } 22 | return key; 23 | } 24 | 25 | public static get(id: string): VirtualKey { 26 | const key = this.find(id); 27 | if (!key) { 28 | throw new Error(`There is no key with id "${id}".`); 29 | } 30 | return key; 31 | } 32 | 33 | public static find(id: string): VirtualKey | undefined { 34 | const key = this.instances.get(id.toLowerCase()); 35 | return key; 36 | } 37 | 38 | private readonly _brand = "VirtualKey"; 39 | 40 | private constructor( 41 | public readonly id: string, 42 | public readonly simpleName?: string, 43 | public readonly icon?: string 44 | ) {} 45 | 46 | format(): string { 47 | if (this.icon) { 48 | return this.icon; 49 | } 50 | if (this.simpleName) { 51 | return this.simpleName; 52 | } 53 | return this.id; 54 | } 55 | } 56 | 57 | export const KnownVirtualKeys = { 58 | ShiftR: VirtualKey.register({ id: "ShiftR", simpleName: "Shift", icon: "⇧" }), 59 | ShiftL: VirtualKey.register({ id: "ShiftL", simpleName: "Shift", icon: "⇧" }), 60 | F1: VirtualKey.register({ id: "F1" }), 61 | F2: VirtualKey.register({ id: "F2" }), 62 | F3: VirtualKey.register({ id: "F3" }), 63 | F4: VirtualKey.register({ id: "F4" }), 64 | F5: VirtualKey.register({ id: "F5" }), 65 | F6: VirtualKey.register({ id: "F6" }), 66 | F7: VirtualKey.register({ id: "F7" }), 67 | F8: VirtualKey.register({ id: "F8" }), 68 | F9: VirtualKey.register({ id: "F9" }), 69 | F10: VirtualKey.register({ id: "F10" }), 70 | F11: VirtualKey.register({ id: "F11" }), 71 | F12: VirtualKey.register({ id: "F12" }), 72 | Escape: VirtualKey.register({ id: "Escape" }), 73 | CtrlL: VirtualKey.register({ id: "CtrlL", simpleName: "Ctrl" }), 74 | CtrlR: VirtualKey.register({ id: "CtrlR", simpleName: "Ctrl" }), 75 | Return: VirtualKey.register({ id: "Return", icon: "↵", alternativeIds: ["Enter"] }), 76 | Tab: VirtualKey.register({ id: "Tab", icon: "⭾" }), 77 | BackSpace: VirtualKey.register({ id: "Backspace", icon: "Backspace ⌫" }), 78 | MetaL: VirtualKey.register({ 79 | id: "MetaL", 80 | icon: "⌘", 81 | simpleName: "Super", 82 | alternativeIds: ["win", "super", "meta"], 83 | }), 84 | MetaR: VirtualKey.register({ id: "MetaR", icon: "⌘", simpleName: "Super" }), 85 | Apps: VirtualKey.register({ id: "Apps" }), 86 | Caps: VirtualKey.register({ id: "Caps" }), 87 | AltL: VirtualKey.register({ id: "AltL", simpleName: "Alt" }), 88 | AltR: VirtualKey.register({ id: "AltR", simpleName: "Alt Gr" }), 89 | Up: VirtualKey.register({ id: "Up", icon: "↑" }), 90 | Left: VirtualKey.register({ id: "Left", icon: "←" }), 91 | Down: VirtualKey.register({ id: "Down", icon: "↓ " }), 92 | Right: VirtualKey.register({ id: "Right", icon: "→" }), 93 | Insert: VirtualKey.register({ id: "Insert" }), 94 | Home: VirtualKey.register({ id: "Home", icon: "Home ⤒" }), 95 | End: VirtualKey.register({ id: "End", icon: "End ⤓" }), 96 | Prior: VirtualKey.register({ id: "Prior", icon: "Prior ⇞", alternativeIds: ["PageUp"] }), 97 | Next: VirtualKey.register({ id: "Next", icon: "Next ⇟", alternativeIds: ["PageDown"] }), 98 | Delete: VirtualKey.register({ id: "Delete", icon: "Del ⌦" }), 99 | Pause: VirtualKey.register({ id: "Pause" }), 100 | 101 | Key1: VirtualKey.register({ id: "1" }), 102 | Key2: VirtualKey.register({ id: "2" }), 103 | Key3: VirtualKey.register({ id: "3" }), 104 | Key4: VirtualKey.register({ id: "4" }), 105 | Key5: VirtualKey.register({ id: "5" }), 106 | Key6: VirtualKey.register({ id: "6" }), 107 | Key7: VirtualKey.register({ id: "7" }), 108 | Key8: VirtualKey.register({ id: "8" }), 109 | Key9: VirtualKey.register({ id: "9" }), 110 | Key0: VirtualKey.register({ id: "0" }), 111 | KeyA: VirtualKey.register({ id: "A" }), 112 | KeyB: VirtualKey.register({ id: "B" }), 113 | KeyC: VirtualKey.register({ id: "C" }), 114 | KeyD: VirtualKey.register({ id: "D" }), 115 | KeyE: VirtualKey.register({ id: "E" }), 116 | KeyF: VirtualKey.register({ id: "F" }), 117 | KeyG: VirtualKey.register({ id: "G" }), 118 | KeyH: VirtualKey.register({ id: "H" }), 119 | KeyI: VirtualKey.register({ id: "I" }), 120 | KeyJ: VirtualKey.register({ id: "J" }), 121 | KeyK: VirtualKey.register({ id: "K" }), 122 | KeyL: VirtualKey.register({ id: "L" }), 123 | KeyM: VirtualKey.register({ id: "M" }), 124 | KeyN: VirtualKey.register({ id: "N" }), 125 | KeyO: VirtualKey.register({ id: "O" }), 126 | KeyP: VirtualKey.register({ id: "P" }), 127 | KeyQ: VirtualKey.register({ id: "Q" }), 128 | KeyR: VirtualKey.register({ id: "R" }), 129 | KeyS: VirtualKey.register({ id: "S" }), 130 | KeyT: VirtualKey.register({ id: "T" }), 131 | KeyU: VirtualKey.register({ id: "U" }), 132 | KeyV: VirtualKey.register({ id: "V" }), 133 | KeyW: VirtualKey.register({ id: "W" }), 134 | KeyX: VirtualKey.register({ id: "X" }), 135 | KeyY: VirtualKey.register({ id: "Y" }), 136 | KeyZ: VirtualKey.register({ id: "Z" }), 137 | 138 | Oem1: VirtualKey.register({ id: "Oem1" }), 139 | Oem102: VirtualKey.register({ id: "Oem102" }), 140 | Oem2: VirtualKey.register({ id: "Oem2" }), 141 | Oem3: VirtualKey.register({ id: "Oem3" }), 142 | Oem5: VirtualKey.register({ id: "Oem5" }), 143 | Oem6: VirtualKey.register({ id: "Oem6" }), 144 | Oem7: VirtualKey.register({ id: "Oem7" }), 145 | OemComma: VirtualKey.register({ id: "OemComma" }), 146 | OemMinus: VirtualKey.register({ id: "OemMinus" }), 147 | OemPeriod: VirtualKey.register({ id: "OemPeriod" }), 148 | OemPlus: VirtualKey.register({ id: "OemPlus" }), 149 | Oem4: VirtualKey.register({ id: "Oem4" }), 150 | Space: VirtualKey.register({ id: "Space" }), 151 | 152 | NumLock: VirtualKey.register({ id: "NumLock", simpleName: "Num" }), 153 | NumDiv: VirtualKey.register({ id: "NumDiv" }), 154 | NumMul: VirtualKey.register({ 155 | id: "NumMul", 156 | alternativeIds: ["NumpadMultiply"], 157 | }), 158 | NumSub: VirtualKey.register({ 159 | id: "NumSub", 160 | alternativeIds: ["NumpadSubtract"], 161 | }), 162 | NumAdd: VirtualKey.register({ 163 | id: "NumAdd", 164 | alternativeIds: ["NumpadAdd"], 165 | }), 166 | NumEnter: VirtualKey.register({ id: "NumEnter", simpleName: "Enter" }), 167 | NumDec: VirtualKey.register({ id: "NumDec" }), 168 | Num1: VirtualKey.register({ id: "Num1", alternativeIds: ["Numpad1"] }), 169 | Num2: VirtualKey.register({ id: "Num2", alternativeIds: ["Numpad2"] }), 170 | Num3: VirtualKey.register({ id: "Num3", alternativeIds: ["Numpad3"] }), 171 | Num4: VirtualKey.register({ id: "Num4", alternativeIds: ["Numpad4"] }), 172 | Num5: VirtualKey.register({ id: "Num5", alternativeIds: ["Numpad5"] }), 173 | Num6: VirtualKey.register({ id: "Num6", alternativeIds: ["Numpad6"] }), 174 | Num7: VirtualKey.register({ id: "Num7", alternativeIds: ["Numpad7"] }), 175 | Num8: VirtualKey.register({ id: "Num8", alternativeIds: ["Numpad8"] }), 176 | Num9: VirtualKey.register({ id: "Num9", alternativeIds: ["Numpad9"] }), 177 | Num0: VirtualKey.register({ id: "Num0", alternativeIds: ["Numpad0"] }), 178 | 179 | CapsLock: VirtualKey.register({ id: "CapsLock", simpleName: "Caps Lock" }), 180 | 181 | Mod3: VirtualKey.register({ id: "Mod3", simpleName: "Mod 3" }), 182 | Mod4: VirtualKey.register({ id: "Mod4", simpleName: "Mod 4" }), 183 | }; 184 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./primitives"; 2 | export * from "./Keyboard"; 3 | export * from "./PhysicalLayout"; 4 | export * from "./FunctionalLayout"; 5 | export * from "./VirtualKey"; 6 | export * from "./FunctionalLayoutsProvider"; 7 | export * from "./PhysicalLayoutsProvider"; 8 | -------------------------------------------------------------------------------- /frontend/src/Model/Keyboard/primitives.ts: -------------------------------------------------------------------------------- 1 | export * from "./VirtualKey"; 2 | 3 | export class PhysicalKey { 4 | private static readonly instances = new Map(); 5 | public static from(id: string): PhysicalKey { 6 | let existing = this.instances.get(id); 7 | if (!existing) { 8 | existing = new PhysicalKey(id); 9 | this.instances.set(id, existing); 10 | } 11 | return existing; 12 | } 13 | 14 | private readonly _brand = "PhysicalKey"; 15 | 16 | private constructor(public readonly name: string) {} 17 | 18 | public toString() { 19 | return this.name; 20 | } 21 | } 22 | 23 | export class FunctionSymbol { 24 | private static readonly instances = new Map(); 25 | 26 | public static from(name: string) { 27 | let existing = this.instances.get(name); 28 | if (!existing) { 29 | existing = new FunctionSymbol(name); 30 | this.instances.set(name, existing); 31 | } 32 | return existing; 33 | } 34 | 35 | private readonly _brand = "FunctionSymbol"; 36 | 37 | constructor(public readonly name: string) {} 38 | } 39 | -------------------------------------------------------------------------------- /frontend/src/Model/Model.ts: -------------------------------------------------------------------------------- 1 | import { action, computed, observable } from "mobx"; 2 | import { 3 | KeyBindingsResult, 4 | KeyBindingTrie, 5 | KeyWithModifiers, 6 | Modifiers, 7 | Action, 8 | } from "./keybindings"; 9 | import { KeyBindingSet, KeyBindingsProvider } from "./keybindings/KeyBindingsProvider"; 10 | import { 11 | FunctionalLayoutsProvider, 12 | Keyboard, 13 | KnownVirtualKeys, 14 | PhysicalKey, 15 | VirtualKey, 16 | } from "./Keyboard"; 17 | import { PhysicalLayoutsProvider } from "./Keyboard/PhysicalLayoutsProvider"; 18 | import { UrlQueryController } from "./UrlQueryController"; 19 | import { WindowKeyHandler } from "./WindowKeyHandler"; 20 | import { EventEmitter } from "@hediet/std/events"; 21 | import { getConfig } from "./Config"; 22 | import { ServerConnectionController } from "./ServerConnectionController"; 23 | 24 | export class Model { 25 | public readonly physicalLayoutsProvider = new PhysicalLayoutsProvider(); 26 | public readonly functionalLayoutsProvider = new FunctionalLayoutsProvider(); 27 | 28 | public readonly keyboard = new Keyboard( 29 | this.physicalLayoutsProvider.defaultLayout, 30 | this.functionalLayoutsProvider.defaultLayout 31 | ); 32 | 33 | public readonly keyBindingsProvider = new KeyBindingsProvider(); 34 | 35 | @observable 36 | private _currentKeyBindingSet = this.keyBindingsProvider.getKeyBindingSets()[0]; 37 | 38 | public get currentKeyBindingSet(): KeyBindingSet { 39 | return this._currentKeyBindingSet; 40 | } 41 | 42 | @computed 43 | public get currentKeyBindingTrieRoot(): KeyBindingTrie { 44 | return KeyBindingTrie.from(this.currentKeyBindingSet.keyBindings); 45 | } 46 | 47 | @observable 48 | public activeKeyBindings = this.currentKeyBindingTrieRoot; 49 | 50 | @observable 51 | public activeKeyBindingsPath: KeyWithModifiers[] = []; 52 | 53 | @observable 54 | public preventDefault = true; 55 | 56 | public readonly config = getConfig(); 57 | 58 | @observable 59 | public initialized = false; 60 | 61 | private readonly actionEmitter = new EventEmitter<{ action: Action }>(); 62 | public readonly onAction = this.actionEmitter.asEvent(); 63 | 64 | private readonly urlQueryController = new UrlQueryController(this); 65 | private readonly windowKeyHandler = new WindowKeyHandler(this); 66 | private readonly serverConnectionController = new ServerConnectionController(this); 67 | 68 | constructor() { 69 | this.keyboard.onKeyPressed.sub(({ key, keyFunction, mode }) => { 70 | if (keyFunction.virtualKey) { 71 | const bindings = this.findKeyBindings(keyFunction.virtualKey); 72 | 73 | const keyWithModfs = new KeyWithModifiers( 74 | keyFunction.virtualKey, 75 | this.activeModifiers 76 | ); 77 | 78 | if (bindings.followingKeyBindings) { 79 | this.activeKeyBindings = bindings.followingKeyBindings; 80 | this.activeKeyBindingsPath.push(keyWithModfs); 81 | this.keyboard.reset(); 82 | } else { 83 | if ( 84 | !keyFunction.stateAfterKeyPressed && 85 | !this.isKeyBindingsModifier(keyFunction.virtualKey) 86 | ) { 87 | if (mode === "button") { 88 | this.keyboard.handleKeyRelease(key, "button"); 89 | const b = bindings.bindings[0]; 90 | if (b) { 91 | this.actionEmitter.emit({ action: b.action }); 92 | } 93 | } else { 94 | this.resetCurrentKeyBindingPath(); 95 | } 96 | } 97 | } 98 | } 99 | 100 | if (keyFunction.virtualKey === KnownVirtualKeys.Escape) { 101 | this.resetCurrentKeyBindingPath(); 102 | } 103 | }); 104 | } 105 | 106 | get activeModifiers(): Modifiers { 107 | const vks = this.keyboard.pressedVirtualKeys; 108 | const hasCtrl = vks.has(KnownVirtualKeys.CtrlL) || vks.has(KnownVirtualKeys.CtrlR); 109 | const hasAlt = vks.has(KnownVirtualKeys.AltL) || vks.has(KnownVirtualKeys.AltR); 110 | const hasShift = vks.has(KnownVirtualKeys.ShiftL) || vks.has(KnownVirtualKeys.ShiftR); 111 | const hasMeta = vks.has(KnownVirtualKeys.MetaL) || vks.has(KnownVirtualKeys.MetaR); 112 | 113 | return new Modifiers(hasShift, hasAlt, hasCtrl, hasMeta); 114 | } 115 | 116 | public isKeyBindingsModifier(m: VirtualKey): boolean { 117 | const s = new Set([ 118 | KnownVirtualKeys.CtrlL, 119 | KnownVirtualKeys.CtrlR, 120 | KnownVirtualKeys.AltL, 121 | KnownVirtualKeys.AltR, 122 | KnownVirtualKeys.ShiftL, 123 | KnownVirtualKeys.ShiftR, 124 | KnownVirtualKeys.MetaL, 125 | KnownVirtualKeys.MetaR, 126 | ]); 127 | return s.has(m); 128 | } 129 | 130 | public findKeyBindings(key: VirtualKey): KeyBindingsResult { 131 | return this.activeKeyBindings.findKeyBindings( 132 | new KeyWithModifiers(key, this.activeModifiers) 133 | ); 134 | } 135 | 136 | public resetCurrentKeyBindingPath() { 137 | this.activeKeyBindings = this.currentKeyBindingTrieRoot; 138 | this.activeKeyBindingsPath.length = 0; 139 | } 140 | 141 | @action 142 | public setCurrentKeyBindingSet(set: KeyBindingSet) { 143 | this._currentKeyBindingSet = set; 144 | this.activeKeyBindings = this.currentKeyBindingTrieRoot; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /frontend/src/Model/ServerConnectionController.ts: -------------------------------------------------------------------------------- 1 | import { keyboardContract } from "@hediet/key-listener/dist/contract"; 2 | import { ConsoleRpcLogger } from "@hediet/typed-json-rpc"; 3 | import { WebSocketStream } from "@hediet/typed-json-rpc-websocket"; 4 | import { Model } from "./Model"; 5 | import { observable, computed } from "mobx"; 6 | import { PhysicalKey } from "./Keyboard"; 7 | 8 | export class ServerConnectionController { 9 | private server: typeof keyboardContract.TServerInterface | undefined; 10 | 11 | @observable idx = -1; 12 | 13 | @computed get keysSorted() { 14 | return this.model.keyboard.physicalLayout.keysSortedByPosition; 15 | } 16 | 17 | get activeKey(): PhysicalKey | undefined { 18 | if (this.idx < 0) { 19 | return undefined; 20 | } 21 | return this.keysSorted[this.idx % this.keysSorted.length].physicalKey; 22 | } 23 | 24 | constructor(private readonly model: Model) { 25 | model.onAction.sub(({ action }) => { 26 | if (this.server) { 27 | this.server.executeAction({ action: action.name }); 28 | } 29 | }); 30 | 31 | this.stayConnected(); 32 | } 33 | 34 | async stayConnected(): Promise { 35 | const serverInfo = this.model.config.server; 36 | if (!serverInfo) { 37 | this.model.initialized = true; 38 | return; 39 | } 40 | 41 | while (true) { 42 | try { 43 | this.idx = -1; 44 | const stream = await WebSocketStream.connectTo({ 45 | host: "localhost", 46 | port: serverInfo.port, 47 | }); 48 | const { server } = keyboardContract.getServerFromStream( 49 | stream, 50 | new ConsoleRpcLogger(), 51 | { 52 | selectNextKey: async ({}) => { 53 | this.idx++; 54 | return { 55 | physicalKey: this.activeKey!.name, 56 | }; 57 | }, 58 | onKeyEvent: async ({ action, physicalKey }) => { 59 | const key = PhysicalKey.from(physicalKey); 60 | if (action === "pressed") { 61 | this.model.keyboard.handleKeyPress(key, "key"); 62 | } else { 63 | this.model.keyboard.handleKeyRelease(key, "key"); 64 | } 65 | }, 66 | updateSettings: async ({ 67 | functionalLayout, 68 | physicalLayout, 69 | keyBindingSet, 70 | }) => { 71 | if (functionalLayout) { 72 | const l = this.model.functionalLayoutsProvider.findLayout( 73 | functionalLayout 74 | ); 75 | if (l) { 76 | this.model.keyboard.setFunctionalLayout(l); 77 | } 78 | } 79 | 80 | if (physicalLayout) { 81 | const l = this.model.physicalLayoutsProvider.findLayout( 82 | physicalLayout 83 | ); 84 | if (l) { 85 | this.model.keyboard.physicalLayout = l; 86 | } 87 | } 88 | if (keyBindingSet) { 89 | const l = this.model.keyBindingsProvider.findKeyBindingSet( 90 | keyBindingSet 91 | ); 92 | if (l) { 93 | this.model.setCurrentKeyBindingSet(l); 94 | } 95 | } 96 | 97 | this.model.initialized = true; 98 | }, 99 | } 100 | ); 101 | try { 102 | await server.authenticate({ secret: serverInfo.secret }); 103 | } catch (e) { 104 | console.error(e); 105 | } 106 | this.server = server; 107 | 108 | await stream.onClosed; 109 | } catch (e) {} 110 | } 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /frontend/src/Model/UrlQueryController.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "."; 2 | import { autorun } from "mobx"; 3 | 4 | export class UrlQueryController { 5 | constructor(private model: Model) { 6 | this.update(); 7 | 8 | autorun(() => { 9 | const kbd = model.keyboard; 10 | 11 | const searchParams = new URLSearchParams(window.location.search); 12 | 13 | searchParams.set("physicalLayout", kbd.physicalLayout.name); 14 | searchParams.set("functionalLayout", kbd.functionalLayout.name); 15 | searchParams.set("app", model.currentKeyBindingSet.id); 16 | 17 | window.history.pushState(undefined, "", "?" + searchParams.toString()); 18 | }); 19 | } 20 | 21 | private update() { 22 | const kbd = this.model.keyboard; 23 | 24 | const searchParams = new URLSearchParams(window.location.search); 25 | 26 | const physicalLayoutName = searchParams.get("physicalLayout"); 27 | if (physicalLayoutName) { 28 | const l = this.model.physicalLayoutsProvider.findLayout(physicalLayoutName); 29 | if (l) { 30 | kbd.physicalLayout = l; 31 | } 32 | } 33 | 34 | const functionalLayoutName = searchParams.get("functionalLayout"); 35 | if (functionalLayoutName) { 36 | const l = this.model.functionalLayoutsProvider.findLayout(functionalLayoutName); 37 | if (l) { 38 | kbd.setFunctionalLayout(l); 39 | } 40 | } 41 | 42 | const app = searchParams.get("app"); 43 | if (app) { 44 | const curSet = this.model.keyBindingsProvider.findKeyBindingSet(app); 45 | if (curSet) { 46 | this.model.setCurrentKeyBindingSet(curSet); 47 | } 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /frontend/src/Model/WindowKeyHandler.ts: -------------------------------------------------------------------------------- 1 | import { Model } from "./Model"; 2 | import { PhysicalKey } from "."; 3 | 4 | export class WindowKeyHandler { 5 | constructor(model: Model) { 6 | window.addEventListener("keydown", e => { 7 | const physicalKey = PhysicalKey.from(e.code); 8 | model.keyboard.handleKeyPress(physicalKey, "key"); 9 | if (model.preventDefault) { 10 | e.preventDefault(); 11 | } 12 | e.stopPropagation(); 13 | }); 14 | window.addEventListener("keyup", e => { 15 | const physicalKey = PhysicalKey.from(e.code); 16 | model.keyboard.handleKeyRelease(physicalKey, "key"); 17 | if (model.preventDefault) { 18 | e.preventDefault(); 19 | } 20 | e.stopPropagation(); 21 | }); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /frontend/src/Model/alternative-scancodes.md: -------------------------------------------------------------------------------- 1 | ```ts 2 | const str = `ESC FK01 FK02 FK03 FK04 FK05 FK06 FK07 FK08 FK09 FK10 FK11 FK12 PRSC SCLK PAUS 3 | TLDE AE01 AE02 AE03 AE04 AE05 AE06 AE07 AE08 AE09 AE10 AE11 AE12 BKSP INS HOME PGUP NMLK KPDV KPMU KPSU 4 | TAB AD01 AD02 AD03 AD04 AD05 AD06 AD07 AD08 AD09 AD10 AD11 AD12 BKSL DELE END PGDN KP7 KP8 KP9 KPAD 5 | CAPS AC01 AC02 AC03 AC04 AC05 AC06 AC07 AC08 AC09 AC10 AC11 RTRN KP4 KP5 KP6 6 | LFSH LSGT AB01 AB02 AB03 AB04 AB05 AB06 AB07 AB08 AB09 AB10 RTSH UP KP1 KP2 KP3 KPEN 7 | LCTL LWIN LALT SPCE RALT RWIN MENU RCTL LEFT DOWN RIGHT KP0 KPDL 8 | `; 9 | const map = {} as any; 10 | 11 | const parts = str.split(" ").filter(e => e.trim() !== ""); 12 | for (const key of keysSorted) { 13 | const name = parts.shift(); 14 | this.map[key.scanCode.toString()] = name; 15 | } 16 | ``` 17 | 18 | ```ts 19 | let i = 0; 20 | this.activeKey = keysSorted[i].scanCode; 21 | const map: Record = {}; 22 | 23 | this.keyboard.onKeyPressed.sub(({ keyCodeName }) => { 24 | i++; 25 | map[keyCodeName] = this.activeKey!.toString(); 26 | if (i === keysSorted.length) { 27 | console.log(JSON.stringify(map)); 28 | } else { 29 | runInAction(() => { 30 | this.activeKey = keysSorted[i].scanCode; 31 | }); 32 | } 33 | }); 34 | ``` 35 | -------------------------------------------------------------------------------- /frontend/src/Model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./Keyboard"; 2 | export * from "./Model"; 3 | -------------------------------------------------------------------------------- /frontend/src/Model/keybindings/KeyBindingsProvider.ts: -------------------------------------------------------------------------------- 1 | import { KeyBinding, Action, KeyWithModifiers, Modifiers } from "."; 2 | import { bindings as winBindings } from "./vscode-data/win"; 3 | import { bindings as macBindings } from "./vscode-data/mac"; 4 | import { bindings as linuxBindings } from "./vscode-data/linux"; 5 | import { VirtualKey, UsQwertyLayout } from ".."; 6 | 7 | export class KeyBindingsProvider { 8 | private readonly keyBindingSets = [ 9 | new KeyBindingSet("VS Code", "win", getVsCodeBindings(winBindings)), 10 | new KeyBindingSet("VS Code", "mac", getVsCodeBindings(macBindings)), 11 | new KeyBindingSet("VS Code", "linux", getVsCodeBindings(linuxBindings)), 12 | ]; 13 | 14 | getKeyBindingSets(): KeyBindingSet[] { 15 | return this.keyBindingSets; 16 | } 17 | 18 | findKeyBindingSet(id: string): KeyBindingSet | undefined { 19 | return this.keyBindingSets.find(s => s.id === id); 20 | } 21 | } 22 | 23 | export class KeyBindingSet { 24 | constructor( 25 | public readonly applicationName: string, 26 | public readonly os: "win" | "mac" | "linux", 27 | public readonly keyBindings: KeyBinding[] 28 | ) {} 29 | 30 | public get id(): string { 31 | return `${this.applicationName}-${this.os}`; 32 | } 33 | 34 | public get osName(): string { 35 | if (this.os === "win") { 36 | return "Windows"; 37 | } else if (this.os == "linux") { 38 | return "Linux"; 39 | } else if (this.os === "mac") { 40 | return "Mac"; 41 | } 42 | return this.os; 43 | } 44 | } 45 | 46 | function getVsCodeBindings( 47 | bindings: { 48 | key: string; 49 | command: string; 50 | when?: string; 51 | }[] 52 | ): KeyBinding[] { 53 | return bindings 54 | .map(b => { 55 | const key = b.key.toLowerCase(); 56 | const seq = key.split(" "); 57 | const keys = seq.map(s => parseItem(s)!).filter(i => !!i); 58 | const parts = b.command.split("."); 59 | const last = camelCaseToCapitalized(parts[parts.length - 1]); 60 | if (keys.length === 0) { 61 | return undefined!; 62 | } 63 | return new KeyBinding(keys, new Action(b.command, last)); 64 | }) 65 | .filter(b => !!b); 66 | } 67 | 68 | export function camelCaseToCapitalized(camelCasedWord: string) { 69 | return camelCasedWord 70 | .replace(/[a-z][A-Z]/g, x => x[0] + " " + x[1].toUpperCase()) 71 | .replace(/^./, x => x.toUpperCase()); 72 | } 73 | 74 | function parseItem(item: string): KeyWithModifiers | undefined { 75 | const parts = item.split("+"); 76 | 77 | let shift = false; 78 | let ctrl = false; 79 | let alt = false; 80 | let meta = false; 81 | let main: VirtualKey | undefined; 82 | 83 | for (const part of parts) { 84 | if (part === "ctrl") { 85 | ctrl = true; 86 | } else if (part === "shift") { 87 | shift = true; 88 | } else if (part === "alt") { 89 | alt = true; 90 | } else if (part === "cmd" || part === "win") { 91 | meta = true; 92 | } else { 93 | main = VirtualKey.find(part.replace(/_/g, "")); 94 | 95 | if (!main) { 96 | const r = UsQwertyLayout.defaultState.findFunction(p => p.text === part); 97 | if (r) { 98 | main = r.function.virtualKey; 99 | } 100 | } 101 | 102 | if (!main) { 103 | console.warn(`Virtual Key "${part}" in "${item}" not found.`); 104 | } 105 | } 106 | } 107 | if (!main) { 108 | return undefined; 109 | } 110 | 111 | return new KeyWithModifiers(main!, new Modifiers(shift, alt, ctrl, meta)); 112 | } 113 | -------------------------------------------------------------------------------- /frontend/src/Model/keybindings/index.ts: -------------------------------------------------------------------------------- 1 | import { VirtualKey } from "../Keyboard"; 2 | 3 | export class KeyBinding { 4 | constructor( 5 | public readonly keySequence: KeyWithModifiers[], 6 | public readonly action: Action 7 | ) {} 8 | } 9 | 10 | export class Action { 11 | constructor( 12 | public readonly name: string, 13 | public readonly shortName: string 14 | ) {} 15 | } 16 | 17 | export class KeyWithModifiers { 18 | constructor( 19 | public readonly key: VirtualKey, 20 | public readonly modifiers: Modifiers 21 | ) {} 22 | 23 | public hash(): string { 24 | return JSON.stringify({ 25 | modifiers: this.modifiers, 26 | key: this.key.id, 27 | }); 28 | } 29 | 30 | public toString() { 31 | const mods = this.modifiers.toString(); 32 | if (mods !== "") { 33 | return `${mods}+${this.key.id}`; 34 | } 35 | return this.key.id; 36 | } 37 | } 38 | 39 | export class Modifiers { 40 | constructor( 41 | public readonly shift: boolean, 42 | public readonly alt: boolean, 43 | public readonly ctrl: boolean, 44 | public readonly meta: boolean 45 | ) {} 46 | 47 | toString() { 48 | const items = new Array(); 49 | if (this.shift) { 50 | items.push("Shift"); 51 | } 52 | if (this.alt) { 53 | items.push("Alt"); 54 | } 55 | if (this.ctrl) { 56 | items.push("Ctrl"); 57 | } 58 | if (this.meta) { 59 | items.push("Meta"); 60 | } 61 | return items.join("+"); 62 | } 63 | } 64 | 65 | export interface KeyBindingsResult { 66 | bindings: KeyBinding[]; 67 | followingKeyBindings: KeyBindingTrie | undefined; 68 | } 69 | 70 | export class KeyBindingTrie { 71 | public static from(bindings: KeyBinding[]): KeyBindingTrie { 72 | const result = new KeyBindingTrie(); 73 | for (const b of bindings) { 74 | result.addKeybinding(b); 75 | } 76 | return result; 77 | } 78 | 79 | private readonly map = new Map(); 80 | public get size(): number { 81 | return this.map.size; 82 | } 83 | 84 | private getEntry(b: KeyWithModifiers): KeyBindingsResult { 85 | let r = this.map.get(b.hash()); 86 | if (!r) { 87 | r = { 88 | bindings: [], 89 | followingKeyBindings: undefined, 90 | }; 91 | this.map.set(b.hash(), r); 92 | } 93 | return r; 94 | } 95 | 96 | private addKeybinding( 97 | binding: KeyBinding, 98 | keySequencePrefixLength: number = 0 99 | ) { 100 | const k = binding.keySequence[keySequencePrefixLength]; 101 | const r = this.getEntry(k); 102 | if (binding.keySequence.length === keySequencePrefixLength + 1) { 103 | r.bindings.push(binding); 104 | } else { 105 | if (!r.followingKeyBindings) { 106 | r.followingKeyBindings = new KeyBindingTrie(); 107 | } 108 | r.followingKeyBindings.addKeybinding( 109 | binding, 110 | keySequencePrefixLength + 1 111 | ); 112 | } 113 | } 114 | 115 | findKeyBindings(key: KeyWithModifiers): KeyBindingsResult { 116 | return this.getEntry(key); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /frontend/src/index.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | import * as ReactDOM from "react-dom"; 3 | import "./style.scss"; 4 | import { Model } from "./Model"; 5 | import Components = require("./Components/GUI"); 6 | import { runInAction } from "mobx"; 7 | 8 | const model = runInAction("Create model", () => new Model()); 9 | 10 | function render(target: HTMLDivElement) { 11 | const c = require("./Components/GUI") as typeof Components; 12 | ReactDOM.render(, target); 13 | } 14 | 15 | const target = document.createElement("div"); 16 | target.className = "target"; 17 | document.body.appendChild(target); 18 | 19 | const destination = document.createElement("div"); 20 | destination.id = "destination"; 21 | document.body.appendChild(destination); 22 | 23 | render(target); 24 | 25 | declare var module: { 26 | hot?: { accept: (componentName: string, callback: () => void) => void }; 27 | }; 28 | declare var require: (name: string) => any; 29 | 30 | if (module.hot) { 31 | module.hot.accept("./Components/GUI", () => { 32 | render(target); 33 | }); 34 | } 35 | -------------------------------------------------------------------------------- /frontend/src/std/DragBehavior.ts: -------------------------------------------------------------------------------- 1 | import { observable } from "mobx"; 2 | import { EventEmitter } from "@hediet/std/events"; 3 | import { Point, PointLike } from "./Point"; 4 | import { Disposable } from "@hediet/std/disposable"; 5 | 6 | export class PositionTransformation { 7 | constructor(public readonly transform: (position: Point) => Point) {} 8 | 9 | public then( 10 | nextTransform: (position: Point) => Point 11 | ): PositionTransformation { 12 | return new PositionTransformation(p => { 13 | const p2 = this.transform(p); 14 | return nextTransform(p2); 15 | }); 16 | } 17 | 18 | public relative(): PositionTransformation { 19 | let first: Point | undefined = undefined; 20 | return this.then(p => { 21 | if (!first) { 22 | first = p; 23 | } 24 | return p.sub(first); 25 | }); 26 | } 27 | 28 | public translate(offset: PointLike): PositionTransformation { 29 | return this.then(p => p.add(offset)); 30 | } 31 | } 32 | 33 | export const identity = new PositionTransformation(p => p); 34 | 35 | export class DragBehavior { 36 | @observable private _activeOperation: DragOperation | undefined; 37 | @observable private _previousOperation: DragOperation | undefined; 38 | 39 | public start( 40 | data: TData, 41 | mousePositionTransformation = identity 42 | ): DragOperation { 43 | var op = new DragOperation(data, mousePositionTransformation); 44 | op.onEnd.sub(() => { 45 | this._previousOperation = this._activeOperation; 46 | this._activeOperation = undefined; 47 | }); 48 | this._activeOperation = op; 49 | return op; 50 | } 51 | 52 | public get isActive(): boolean { 53 | return this._activeOperation !== undefined; 54 | } 55 | public get activeOperation(): DragOperation | undefined { 56 | return this._activeOperation; 57 | } 58 | public get previousOperation(): DragOperation | undefined { 59 | return this._previousOperation; 60 | } 61 | public get activeOrPreviousOperation(): DragOperation | undefined { 62 | return this.activeOperation || this.previousOperation; 63 | } 64 | 65 | public testActiveData(predicate: (data: TData) => boolean): boolean { 66 | if (!this._activeOperation) return false; 67 | 68 | return predicate(this._activeOperation.data); 69 | } 70 | 71 | public isDataEqualTo(data: TData): boolean { 72 | return this.testActiveData(d => d === data); 73 | } 74 | } 75 | 76 | export class DragOperation { 77 | private readonly dispose = Disposable.fn(); 78 | private lastPosition: Point | undefined; 79 | 80 | private _onDrag = new EventEmitter<{ 81 | position: Point; 82 | data: TData; 83 | }>(); 84 | public readonly onDrag = this._onDrag.asEvent(); 85 | 86 | private _onEnd = new EventEmitter<{ 87 | position: Point; 88 | cancelled: boolean; 89 | data: TData; 90 | }>(); 91 | public readonly onEnd = this._onEnd.asEvent(); 92 | 93 | public readonly handleMouseEvent = (e: MouseEvent) => { 94 | this.lastPosition = this.mousePositionTransformation.transform( 95 | new Point(e.clientX, e.clientY) 96 | ); 97 | 98 | this._onDrag.emit({ 99 | position: this.lastPosition!, 100 | data: this.data, 101 | }); 102 | }; 103 | 104 | constructor( 105 | public readonly data: TData, 106 | private readonly mousePositionTransformation: PositionTransformation 107 | ) { 108 | window.addEventListener("mousemove", this.handleMouseEvent); 109 | 110 | this.dispose.track({ 111 | dispose: () => { 112 | window.removeEventListener("mousemove", this.handleMouseEvent); 113 | }, 114 | }); 115 | } 116 | 117 | public endOnMouseUp(button?: number) { 118 | let f1: any; 119 | window.addEventListener( 120 | "mouseup", 121 | (f1 = (e: MouseEvent) => { 122 | if (button === undefined || e.button === button) this.end(); 123 | }) 124 | ); 125 | 126 | this.dispose.track({ 127 | dispose: () => { 128 | window.removeEventListener("mouseup", f1); 129 | }, 130 | }); 131 | 132 | return this; 133 | } 134 | 135 | private endOrCancelled(cancelled: boolean) { 136 | this.dispose(); 137 | this._onEnd.emit({ 138 | position: this.lastPosition!, 139 | cancelled: false, 140 | data: this.data, 141 | }); 142 | } 143 | 144 | public end(): void { 145 | this.endOrCancelled(false); 146 | } 147 | 148 | public cancel(): void { 149 | this.endOrCancelled(true); 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /frontend/src/std/Point.ts: -------------------------------------------------------------------------------- 1 | function sqr(a: number) { 2 | return a * a; 3 | } 4 | 5 | export type PointLike = 6 | | Point 7 | | { x?: number; y: number } 8 | | { x: number; y?: number }; 9 | 10 | export function point(data: PointLike) { 11 | if (data instanceof Point) { 12 | return data; 13 | } 14 | return new Point(data.x || 0, data.y || 0); 15 | } 16 | 17 | export class Point { 18 | public static readonly Zero = new Point(0, 0); 19 | 20 | constructor(public readonly x: number, public readonly y: number) {} 21 | 22 | public distance(other: PointLike = Point.Zero): number { 23 | const d = this.sub(other); 24 | return Math.sqrt(sqr(d.x) + sqr(d.y)); 25 | } 26 | 27 | public sub(other: PointLike): Point { 28 | const o = point(other); 29 | return new Point(this.x - o.x, this.y - o.y); 30 | } 31 | 32 | public add(other: PointLike): Point { 33 | const o = point(other); 34 | return new Point(this.x + o.x, this.y + o.y); 35 | } 36 | 37 | public mul(scalar: number): Point { 38 | return new Point(this.x * scalar, this.y * scalar); 39 | } 40 | 41 | public div(scalar: number): Point { 42 | return new Point(this.x / scalar, this.y / scalar); 43 | } 44 | 45 | public equals(other: PointLike) { 46 | const o = point(other); 47 | return this.x === o.x && this.y === o.y; 48 | } 49 | 50 | public getPointCloserTo(dest: PointLike, dist: number): Point { 51 | if (this.equals(dest)) return this; 52 | 53 | var p = point(dest).sub(this); 54 | const angle = Math.atan2(p.x, p.y); 55 | 56 | var result = new Point( 57 | this.x + Math.sin(angle) * dist, 58 | this.y + Math.cos(angle) * dist 59 | ); 60 | return result; 61 | } 62 | } 63 | 64 | function turn(p1: Point, p2: Point, p3: Point): number { 65 | const a = p1.x; 66 | const b = p1.y; 67 | const c = p2.x; 68 | const d = p2.y; 69 | const e = p3.x; 70 | const f = p3.y; 71 | const A = (f - b) * (c - a); 72 | const B = (d - b) * (e - a); 73 | return A > B + Number.MIN_VALUE ? 1 : A + Number.MIN_VALUE < B ? -1 : 0; 74 | } 75 | 76 | export function isIntersect( 77 | aStart: Point, 78 | aEnd: Point, 79 | bStart: Point, 80 | bEnd: Point 81 | ): boolean { 82 | return ( 83 | turn(aStart, bStart, bEnd) != turn(aEnd, bStart, bEnd) && 84 | turn(aStart, aEnd, bStart) != turn(aStart, aEnd, bEnd) 85 | ); 86 | } 87 | 88 | // first zoom then translate 89 | export function scale( 90 | clientOffset: Point, 91 | clientSize: Point, 92 | viewSize: Point 93 | ): { clientZoom: number; clientOffset: Point } { 94 | const clientRatio = clientSize.x / clientSize.y; 95 | const viewRatio = viewSize.x / viewSize.y; 96 | 97 | let zoom = 1; 98 | 99 | if (clientRatio < viewRatio) zoom = viewSize.y / clientSize.y; 100 | else zoom = viewSize.x / clientSize.x; 101 | 102 | const clientMid = clientOffset.mul(zoom).add(clientSize.mul(zoom / 2)); 103 | const viewMid = viewSize.div(2); 104 | 105 | const clientOffset2 = viewMid.sub(clientMid); 106 | 107 | return { clientOffset: clientOffset2, clientZoom: zoom }; 108 | } 109 | 110 | export class Rectangle { 111 | public static ofSize(position: Point, size: Point): Rectangle { 112 | return new Rectangle(position, position.add(size)); 113 | } 114 | 115 | constructor( 116 | public readonly topLeft: Point, 117 | public readonly bottomRight: Point 118 | ) {} 119 | 120 | get size(): Point { 121 | return this.bottomRight.sub(this.topLeft); 122 | } 123 | 124 | get topRight(): Point { 125 | return new Point(this.bottomRight.x, this.topLeft.y); 126 | } 127 | 128 | get bottomLeft(): Point { 129 | return new Point(this.topLeft.x, this.bottomRight.y); 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /frontend/src/std/utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Finds the smallest `t` so that `test(t) && !test(t - 1)` 3 | * @param test A monotonous function, 4 | * i.e. `forall e >= 0: test(t) <= test(t + e)`. 5 | */ 6 | export function binarySearch(test: (t: number) => boolean): number { 7 | let range = [-1, 1]; 8 | while (test(range[0])) { 9 | // At this point, we know that `t < range[0]` 10 | range = [range[0] * 2, range[0]]; 11 | } 12 | // now we have `!test(range[0])` and `range[1] != 1 => test(range[1])` 13 | 14 | while (!test(range[1])) { 15 | // At this point, we know that `range[1] < t` 16 | range = [range[1], range[1] * 2]; 17 | } 18 | 19 | // now we have `!test(range[0]) && test(range[1])` 20 | 21 | // [t1, t2] 22 | while (true) { 23 | const mid = Math.floor((range[1] + range[0]) / 2); 24 | if (test(mid)) { 25 | if (mid == range[1]) { 26 | return mid; 27 | } 28 | range = [range[0], mid]; 29 | } else { 30 | if (mid == range[0]) { 31 | return mid + 1; 32 | } 33 | range = [mid, range[1]]; 34 | } 35 | } 36 | } 37 | 38 | export function sortByNumericKey( 39 | keySelector: (item: T) => number 40 | ): (a: T, b: T) => number { 41 | return (a, b) => { 42 | return keySelector(a) - keySelector(b); 43 | }; 44 | } 45 | 46 | export function seq(startInclusive: number, endInclusive: number): number[] { 47 | const result = new Array(); 48 | for (let i = startInclusive; i <= endInclusive; i++) { 49 | result.push(i); 50 | } 51 | return result; 52 | } 53 | -------------------------------------------------------------------------------- /frontend/src/style.scss: -------------------------------------------------------------------------------- 1 | @import "~normalize.css"; 2 | @import "~@blueprintjs/core/lib/css/blueprint.css"; 3 | @import "~@blueprintjs/icons/lib/css/blueprint-icons.css"; 4 | @import "~@blueprintjs/core/lib/scss/variables"; 5 | 6 | body { 7 | font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; 8 | background: rgb(110, 110, 110); 9 | height: 100%; 10 | width: 100%; 11 | } 12 | 13 | html, 14 | body, 15 | .target { 16 | height: 100%; 17 | width: 100%; 18 | margin: 0; 19 | } 20 | 21 | .target { 22 | box-sizing: border-box; 23 | } 24 | 25 | .component-GUI { 26 | display: flex; 27 | flex-direction: column; 28 | padding: 10px; 29 | height: 100%; 30 | width: 100%; 31 | 32 | .part-Header { 33 | //margin: 10px; 34 | 35 | .part-Header-Item { 36 | padding-right: 15px; 37 | } 38 | } 39 | 40 | .part-Keyboard { 41 | flex: 1; 42 | } 43 | 44 | .part-Title { 45 | margin: 10px; 46 | font-size: 20px; 47 | color: #eee; 48 | } 49 | } 50 | 51 | .component-Keyboard { 52 | background-color: #8b8b8b; 53 | border-radius: 0.3em; 54 | border: 1px solid rgb(73, 73, 73); 55 | box-shadow: 0 0.2em 0 0.05em rgb(95, 95, 95); 56 | border-bottom-color: rgb(97, 97, 97); 57 | 58 | position: relative; 59 | > .part-Key { 60 | position: absolute; 61 | padding: 3px 2px; 62 | } 63 | } 64 | 65 | .component-Key { 66 | height: 100%; 67 | width: 100%; 68 | outline: none; 69 | 70 | border-radius: 0.3em; 71 | background: rgb(54, 54, 54); 72 | color: #eee; 73 | border: 1px solid rgb(73, 73, 73); 74 | box-shadow: 0 0.2em 0 0.05em rgb(46, 46, 46); 75 | border-bottom-color: rgb(87, 87, 87); 76 | 77 | &.pressed, 78 | &:active { 79 | margin-top: 1px; 80 | margin-bottom: 2px; 81 | 82 | &.virtualKey-CtrlL, 83 | &.virtualKey-CtrlR { 84 | background: $orange3; 85 | } 86 | 87 | &.virtualKey-ShiftL, 88 | &.virtualKey-ShiftR, 89 | &.virtualKey-CapsLock { 90 | background: $blue3; 91 | } 92 | 93 | &.virtualKey-AltL, 94 | &.virtualKey-AltR { 95 | background: $green3; 96 | } 97 | 98 | margin-top: 1px; 99 | margin-bottom: 2px; 100 | 101 | background: lighten(rgb(54, 54, 54), 20); 102 | .part-Action { 103 | color: wihte; 104 | } 105 | 106 | .part-KeyText { 107 | color: white; 108 | } 109 | } 110 | 111 | &.active { 112 | background: green; 113 | } 114 | 115 | display: flex; 116 | flex-direction: column; 117 | 118 | align-items: stretch; 119 | margin: 0; 120 | padding: 2px; 121 | 122 | .part-KeyText { 123 | margin: 2px 2px 0px 2px; 124 | 125 | font-size: 11px; 126 | color: lightblue; 127 | } 128 | 129 | .part-Action { 130 | font-size: 8px; 131 | /* 132 | align-content: center; 133 | justify-content: center; 134 | align-items: center; 135 | justify-items: center;*/ 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /frontend/src/types.d.ts: -------------------------------------------------------------------------------- 1 | declare module "simple-icons/icons/github" { 2 | export const svg: string; 3 | } 4 | 5 | declare module "simple-icons/icons/twitter" { 6 | export const svg: string; 7 | } 8 | -------------------------------------------------------------------------------- /frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "module": "commonjs", 5 | "strict": true, 6 | "outDir": "dist", 7 | "skipLibCheck": true, 8 | "resolveJsonModule": true, 9 | "newLine": "LF", 10 | "sourceMap": true, 11 | "jsx": "react", 12 | "experimentalDecorators": true 13 | }, 14 | "include": ["src/**/*"] 15 | } 16 | -------------------------------------------------------------------------------- /frontend/webpack.config.js: -------------------------------------------------------------------------------- 1 | require("ts-node").register(); 2 | module.exports = require("./webpack.config.ts"); 3 | -------------------------------------------------------------------------------- /frontend/webpack.config.ts: -------------------------------------------------------------------------------- 1 | import * as webpack from "webpack"; 2 | import path = require("path"); 3 | import HtmlWebpackPlugin = require("html-webpack-plugin"); 4 | import ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin"); 5 | import { CleanWebpackPlugin } from "clean-webpack-plugin"; 6 | import TerserPlugin = require("terser-webpack-plugin"); 7 | 8 | const r = (file: string) => path.resolve(__dirname, file); 9 | 10 | module.exports = { 11 | entry: [r("src/index.tsx")], 12 | output: { 13 | path: r("dist"), 14 | filename: "[name].js", 15 | chunkFilename: "[name]-[hash].js", 16 | }, 17 | resolve: { 18 | extensions: [".webpack.js", ".web.js", ".ts", ".tsx", ".js"], 19 | }, 20 | devtool: "source-map", 21 | module: { 22 | rules: [ 23 | { test: /\.css$/, loader: "style-loader!css-loader" }, 24 | { test: /\.scss$/, loader: "style-loader!css-loader!sass-loader" }, 25 | { 26 | test: /\.(jpe?g|png|gif|eot|ttf|svg|woff|woff2|md)$/i, 27 | loader: "file-loader", 28 | }, 29 | { 30 | test: /\.tsx?$/, 31 | loader: "ts-loader", 32 | options: { transpileOnly: true }, 33 | }, 34 | ], 35 | }, 36 | optimization: { 37 | minimizer: [ 38 | new TerserPlugin({ 39 | terserOptions: { 40 | safari10: true, 41 | }, 42 | }), 43 | ], 44 | }, 45 | plugins: [ 46 | new CleanWebpackPlugin(), 47 | new HtmlWebpackPlugin({ 48 | title: "VS Code Keybindings", 49 | }), 50 | new ForkTsCheckerWebpackPlugin(), 51 | ], 52 | } as webpack.Configuration; 53 | -------------------------------------------------------------------------------- /key-listener-cli/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@hediet/key-listener", 3 | "private": true, 4 | "description": "", 5 | "version": "0.1.0", 6 | "license": "MIT", 7 | "scripts": { 8 | "dev": "tsc --watch", 9 | "run": "node ./dist/index" 10 | }, 11 | "main": "./dist/api", 12 | "types": "./dist/api", 13 | "dependencies": { 14 | "@hediet/cli": "^0.6.3", 15 | "@hediet/std": "^0.6.0", 16 | "iohook": "^0.6.5", 17 | "@hediet/typed-json-rpc": "^0.7.7", 18 | "@hediet/typed-json-rpc-websocket-server": "^0.7.7", 19 | "ws": "^7.2.1", 20 | "rxjs": "^6.5.4", 21 | "crypto-random-string": "^3.1.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^13.7.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /key-listener-cli/src/KeyboardHook.ts: -------------------------------------------------------------------------------- 1 | import * as iohook from "iohook"; 2 | import { EventEmitter } from "@hediet/std/events"; 3 | import { Subject } from "rxjs"; 4 | import * as op from "rxjs/operators"; 5 | 6 | export class KeyboardHook { 7 | private readonly keyAction = new EventEmitter<{ 8 | action: "pressed" | "released"; 9 | physicalKey: string; 10 | keycode: number; 11 | }>(); 12 | public readonly onKeyAction = this.keyAction.asEvent(); 13 | 14 | constructor() { 15 | iohook.useRawcode(true); 16 | 17 | iohook.on("keydown", data => this.handleEvent(data, "pressed")); 18 | iohook.on("keyup", data => this.handleEvent(data, "released")); 19 | 20 | iohook.start(); 21 | 22 | this.subject 23 | .pipe( 24 | op.groupBy(v => v.action), 25 | // We throttle so that no duplicate keys are reported on windows when using neo. 26 | op.map(o => (o.key === "pressed" ? o.pipe(op.throttleTime(10)) : o)), 27 | op.mergeAll() 28 | ) 29 | .subscribe(v => this.keyAction.emit(v)); 30 | } 31 | 32 | public dispose(): void { 33 | iohook.stop(); 34 | } 35 | 36 | private readonly subject = new Subject<{ 37 | action: "pressed" | "released"; 38 | physicalKey: string; 39 | keycode: number; 40 | }>(); 41 | 42 | private handleEvent(data: any, action: "pressed" | "released"): void { 43 | const keycode: number = data.keycode; 44 | const physicalKey = map[keycode.toString()]; 45 | this.subject.next({ action, physicalKey, keycode }); 46 | //this.keyAction.emit({ action, physicalKey, keycode }); 47 | } 48 | } 49 | 50 | const map: Record = { 51 | "0": "IntlBackslash", 52 | "1": "Escape", 53 | "2": "Digit1", 54 | "3": "Digit2", 55 | "4": "Digit3", 56 | "5": "Digit4", 57 | "6": "Digit5", 58 | "7": "Digit6", 59 | "8": "Digit7", 60 | "9": "Digit8", 61 | "10": "Digit9", 62 | "11": "Digit0", 63 | "12": "Slash", 64 | "13": "BracketRight", 65 | "14": "Backspace", 66 | "15": "Tab", 67 | "16": "KeyQ", 68 | "17": "KeyW", 69 | "18": "KeyE", 70 | "19": "KeyR", 71 | "20": "KeyT", 72 | "21": "KeyZ", 73 | "22": "KeyU", 74 | "23": "KeyI", 75 | "24": "KeyO", 76 | "25": "KeyP", 77 | "26": "Minus", 78 | "27": "Equal", 79 | "28": "Enter", 80 | "29": "ControlLeft", 81 | "3640": "0xE038", 82 | "30": "KeyA", 83 | "31": "KeyS", 84 | "32": "KeyD", 85 | "33": "KeyF", 86 | "34": "KeyG", 87 | "35": "KeyH", 88 | "36": "KeyJ", 89 | "37": "KeyK", 90 | "38": "KeyL", 91 | "39": "BracketLeft", 92 | "40": "Quote", 93 | "41": "Semicolon", 94 | "42": "ShiftLeft", 95 | "43": "Backquote", 96 | "44": "KeyY", 97 | "45": "KeyX", 98 | "46": "KeyC", 99 | "47": "KeyV", 100 | "48": "KeyB", 101 | "49": "KeyN", 102 | "50": "KeyM", 103 | "51": "Comma", 104 | "52": "Period", 105 | "53": "Backslash", 106 | "54": "ShiftRight", 107 | "55": "NumpadMultiply", 108 | "56": "AltLeft", 109 | "57": "Space", 110 | "58": "CapsLock", 111 | "59": "F1", 112 | "60": "F2", 113 | "61": "F3", 114 | "62": "F4", 115 | "63": "F5", 116 | "64": "F6", 117 | "65": "F7", 118 | "66": "F8", 119 | "67": "F9", 120 | "68": "F10", 121 | "69": "NumLock", 122 | "74": "NumpadSubtract", 123 | "78": "NumpadAdd", 124 | "87": "F11", 125 | "88": "F12", 126 | "3612": "NumpadEnter", 127 | "3613": "ControlRight", 128 | "3637": "NumpadDivide", 129 | "3639": "0x46", 130 | "3653": "Pause", 131 | "3655": "Numpad7", 132 | "3657": "Numpad9", 133 | "3663": "Numpad1", 134 | "3665": "Numpad3", 135 | "3666": "Numpad0", 136 | "3667": "NumpadDecimal", 137 | "3675": "MetaLeft", 138 | "57416": "Numpad8", 139 | "57419": "Numpad4", 140 | "57420": "Numpad5", 141 | "57421": "Numpad6", 142 | "57424": "Numpad2", 143 | "60999": "Home", 144 | "61000": "ArrowUp", 145 | "61001": "PageUp", 146 | "61003": "ArrowLeft", 147 | "61005": "ArrowRight", 148 | "61007": "End", 149 | "61008": "ArrowDown", 150 | "61009": "PageDown", 151 | "61010": "Insert", 152 | "61011": "Delete", 153 | }; 154 | -------------------------------------------------------------------------------- /key-listener-cli/src/Server.ts: -------------------------------------------------------------------------------- 1 | import { startWebSocketServer } from "@hediet/typed-json-rpc-websocket-server"; 2 | import { keyboardContract } from "./contract"; 3 | import { ConsoleRpcLogger } from "@hediet/typed-json-rpc"; 4 | import { KeyboardHook } from "./KeyboardHook"; 5 | import cryptoRandomString = require("crypto-random-string"); 6 | import { Disposer } from "@hediet/std/disposable"; 7 | 8 | export class Server { 9 | public readonly serverSecret = cryptoRandomString({ length: 20 }); 10 | 11 | public readonly port: number; 12 | 13 | public enableKeyboardHook = true; 14 | 15 | constructor( 16 | options: { 17 | port?: number; 18 | handleClient?: (client: Client) => void; 19 | handleAction?: (action: string) => void; 20 | } = {} 21 | ) { 22 | const authenticatedClients = new Set(); 23 | 24 | const server = startWebSocketServer( 25 | { 26 | port: options.port || 0, 27 | }, 28 | async stream => { 29 | const { client } = keyboardContract.registerServerToStream( 30 | stream, 31 | new ConsoleRpcLogger(), 32 | { 33 | authenticate: async ({ secret }) => { 34 | c.ensureUnauthenticated(); 35 | c.authenticateOrThrow(secret); 36 | 37 | authenticatedClients.add(c); 38 | 39 | if (options.handleClient) { 40 | options.handleClient(c); 41 | } 42 | }, 43 | executeAction: async ({ action }) => { 44 | c.ensureAuthenticated(); 45 | 46 | if (options.handleAction) { 47 | options.handleAction(action); 48 | } 49 | }, 50 | } 51 | ); 52 | 53 | const c = new Client(client, this.serverSecret); 54 | 55 | await stream.onClosed; 56 | authenticatedClients.delete(c); 57 | c.disposer.dispose(); 58 | } 59 | ); 60 | this.port = server.port; 61 | 62 | const keyboardHook = new KeyboardHook(); 63 | keyboardHook.onKeyAction.sub(({ action, physicalKey, keycode }) => { 64 | if (this.enableKeyboardHook) { 65 | for (const c of authenticatedClients) { 66 | c.connection.onKeyEvent({ 67 | action, 68 | physicalKey, 69 | }); 70 | } 71 | } 72 | }); 73 | } 74 | } 75 | 76 | export class Client { 77 | private _authenticated = false; 78 | public get authenticated() { 79 | return this._authenticated; 80 | } 81 | 82 | public readonly disposer = new Disposer(); 83 | 84 | public get connection(): typeof keyboardContract.TClientInterface { 85 | if (!this.authenticated) { 86 | throw new Error("Cliet is not authenticated!"); 87 | } 88 | return this._connection; 89 | } 90 | 91 | constructor( 92 | private readonly _connection: typeof keyboardContract.TClientInterface, 93 | private readonly serverSecret: string 94 | ) {} 95 | 96 | public authenticateOrThrow(secret: string): void { 97 | if (secret !== this.serverSecret) { 98 | throw new Error("Invalid secret!"); 99 | } 100 | this._authenticated = true; 101 | } 102 | 103 | public ensureUnauthenticated(): void { 104 | if (this.authenticated) { 105 | throw new Error("Already authenticated!"); 106 | } 107 | } 108 | 109 | public ensureAuthenticated(): void { 110 | if (!this.authenticated) { 111 | throw new Error("Not authenticated!"); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /key-listener-cli/src/api.ts: -------------------------------------------------------------------------------- 1 | export * from "./contract"; 2 | export * from "./Server"; 3 | -------------------------------------------------------------------------------- /key-listener-cli/src/contract.ts: -------------------------------------------------------------------------------- 1 | import { contract, notificationContract, types, requestContract } from "@hediet/typed-json-rpc"; 2 | 3 | export const keyboardContract = contract({ 4 | client: { 5 | onKeyEvent: notificationContract({ 6 | params: types.type({ 7 | physicalKey: types.string, 8 | action: types.union([types.literal("pressed"), types.literal("released")]), 9 | }), 10 | }), 11 | updateSettings: notificationContract({ 12 | params: types.type({ 13 | physicalLayout: types.union([types.string, types.null]), 14 | functionalLayout: types.union([types.string, types.null]), 15 | keyBindingSet: types.union([types.string, types.null]), 16 | }), 17 | }), 18 | selectNextKey: requestContract({ 19 | result: types.type({ 20 | physicalKey: types.string, 21 | }), 22 | }), 23 | }, 24 | server: { 25 | authenticate: requestContract({ 26 | params: types.type({ 27 | secret: types.string, 28 | }), 29 | }), 30 | executeAction: requestContract({ 31 | params: types.type({ 32 | action: types.string, 33 | }), 34 | }), 35 | }, 36 | }); 37 | -------------------------------------------------------------------------------- /key-listener-cli/src/index.ts: -------------------------------------------------------------------------------- 1 | import { createDefaultCli, runDefaultCli, cliInfoFromPackageJson } from "@hediet/cli"; 2 | import { join } from "path"; 3 | import { Server } from "./Server"; 4 | 5 | const cli = createDefaultCli<() => Promise>().addCmd({ 6 | getData: () => async () => { 7 | const server = new Server(); 8 | console.log(server.port); 9 | }, 10 | }); 11 | 12 | runDefaultCli({ 13 | cli, 14 | dataHandler: data => data(), 15 | info: cliInfoFromPackageJson(join(__dirname, "../package.json")), 16 | }); 17 | -------------------------------------------------------------------------------- /key-listener-cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "strict": true, 7 | "outDir": "dist", 8 | "sourceMap": true, 9 | "declarationMap": true 10 | }, 11 | "include": ["src/**/*", "test/**/*"], 12 | "exclude": [] 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "workspaces": [ 4 | "frontend", 5 | "key-listener-cli", 6 | "extension-vscode" 7 | ], 8 | "iohook": { 9 | "targets": [ 10 | "node-59", 11 | "electron-73" 12 | ], 13 | "platforms": [ 14 | "win32", 15 | "darwin", 16 | "linux" 17 | ], 18 | "arches": [ 19 | "x64", 20 | "ia32" 21 | ] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "recommended", 3 | "rules": { 4 | "await-promise": true 5 | } 6 | } 7 | --------------------------------------------------------------------------------