Skip to content

Commit e07a154

Browse files
committed
Move library code from PR 2486
Create project utilities: 1. GitHub actions - based on GH provide templates 2. simple build based on tsup 3. test via jest 4. lintings with eslint Reference-Url: kubevirt-ui/kubevirt-plugin#2486 Reference-Url: https://github.com/qemu/qemu/blob/b69801dd6b1eb4d107f7c2f643adf0a4e3ec9124/ui/vnc_keysym.h Reference-Url: https://github.com/qemu/qemu/tree/b69801dd6b1eb4d107f7c2f643adf0a4e3ec9124/pc-bios/keymaps Reference-Url: https://www.x.org/releases/X11R7.7/src/xserver/xorg-server-1.12.2.tar.gz Reference-Url: https://danielhb.github.io/article/2019/05/06/noVNC-QEMU-RFB.html Signed-off-by: Radoslaw Szwajkowski <[email protected]>
1 parent f092aa8 commit e07a154

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+40462
-1
lines changed

.github/workflows/node.js.yml

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3+
4+
name: Node.js CI
5+
6+
on:
7+
push:
8+
branches: ["main"]
9+
pull_request:
10+
branches: ["main"]
11+
12+
jobs:
13+
build:
14+
runs-on: ubuntu-latest
15+
16+
strategy:
17+
matrix:
18+
node-version: [20.x, 22.x, 24.x]
19+
# See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20+
21+
steps:
22+
- uses: actions/checkout@v4
23+
- name: Use Node.js ${{ matrix.node-version }}
24+
uses: actions/setup-node@v4
25+
with:
26+
node-version: ${{ matrix.node-version }}
27+
cache: "yarn"
28+
- run: yarn --frozen-lockfile
29+
- run: yarn build
30+
- run: yarn test

.github/workflows/npm-publish.yml

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
2+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
3+
4+
name: Node.js Package
5+
6+
on:
7+
release:
8+
types: [created]
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 22
18+
- run: yarn --frozen-lockfile
19+
- run: yarn build
20+
- run: yarn test
21+
22+
publish-npm:
23+
needs: build
24+
runs-on: ubuntu-latest
25+
steps:
26+
- uses: actions/checkout@v4
27+
- uses: actions/setup-node@v4
28+
with:
29+
node-version: 22
30+
registry-url: https://registry.npmjs.org/
31+
- run: yarn
32+
- run: yarn publish
33+
env:
34+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}

.gitignore

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
2+
node_modules/
3+
dist/
4+
5+
# logs
6+
logs
7+
*.log
8+
npm-debug.log*
9+
yarn-debug.log*
10+
yarn-error.log*
11+
12+
# misc
13+
.DS_Store
14+
.vscode/*
15+
16+
# cache
17+
.eslintcache
18+
*.tsbuildinfo

README.md

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,35 @@
1-
# vnc-keymaps
1+
# vnc-keymaps
2+
3+
Libary part extracted from [PR 2486](https://github.com/kubevirt-ui/kubevirt-plugin/pull/2486)
4+
5+
The goal is to convert text copy-pasted to the VNC console to sequence of scancodes matching the language configured on the (remote) virtual machine.
6+
General steps in this use case:
7+
1. get text from clipboard
8+
2. for each Unicode code point get the keystrokes required to trigger it in the keyboard layout used on the remote machine
9+
3. emulate user typing in the text - send the keystrokes to the server using QEMUExtendedKeyEvent
10+
11+
Mapping code point to keystrokes:
12+
13+
1. use ucs2keysym() method to convert Unicode code to X11 keysym code. Functionality is part of xorg-server-1.12.2/hw/xquartz/keysym2ucs.c that was ported to TypeScript.
14+
2. map keysym code to mnemonic name using Qemu vnc_keysym.h
15+
3. check if there is a mapping for that name in the keymap for the chosen keyboard layout. The keymaps originate from Qemu project where they are used to enforce a keyboard layout (-k switch)
16+
4. resolved mapping consist of the scancode coressponding to physical key and a list of modifieres i.e. shift, altgr, numlock.
17+
18+
Example:
19+
20+
1. assume single char ":"
21+
2. Unicode code point is 0x03a
22+
3. X11 keysym code is 0x03a (Latin-1 set has 1:1 mapping)
23+
4. resolved mnemonic name is "colon"
24+
5. in the keymap en-us name "colon" has following mapping: ['colon', 0x27, 'shift']
25+
6. based on this mapping we need to:
26+
1. press shift key
27+
2. press and release key with scancode 0x27
28+
3. release shift key
29+
30+
31+
Helpful links:
32+
1. RFB protocol in Qemu - https://danielhb.github.io/article/2019/05/06/noVNC-QEMU-RFB.html
33+
2. https://github.com/qemu/qemu/blob/b69801dd6b1eb4d107f7c2f643adf0a4e3ec9124/ui/vnc_keysym.h
34+
3. https://github.com/qemu/qemu/tree/b69801dd6b1eb4d107f7c2f643adf0a4e3ec9124/pc-bios/keymaps
35+
4. https://www.x.org/releases/X11R7.7/src/xserver/xorg-server-1.12.2.tar.gz

eslint.config.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import js from "@eslint/js";
2+
import globals from "globals";
3+
import tseslint from "typescript-eslint";
4+
import { defineConfig } from "eslint/config";
5+
6+
7+
export default defineConfig([
8+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"] },
9+
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], languageOptions: { globals: globals.browser } },
10+
tseslint.configs.recommended,
11+
]);

jest.config.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"verbose": true,
3+
"roots": [
4+
"<rootDir>/src",
5+
"<rootDir>/tests"
6+
],
7+
"modulePaths": [
8+
"<rootDir>/src",
9+
"<rootDir>/tests"
10+
],
11+
"preset": "ts-jest/presets/js-with-ts",
12+
"testEnvironment": "node",
13+
"testPathIgnorePatterns": [
14+
"<rootDir>/node_modules/",
15+
"<rootDir>/dist/"
16+
],
17+
"transform": {
18+
"^.+\\.ts$": "ts-jest"
19+
},
20+
"testMatch": [
21+
"<rootDir>/tests/**/*.test.ts"
22+
]
23+
}

package.json

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
{
2+
"name": "vnc-keymaps",
3+
"version": "0.0.0",
4+
"description": "Keymps for VNC",
5+
"exports": {
6+
"./package.json": "./package.json",
7+
".": {
8+
"types": "./dist/index.d.mts",
9+
"import": "./dist/index.mjs"
10+
}
11+
},
12+
"repository": {
13+
"type": "git",
14+
"url": "https://github.com:kubevirt-ui/vnc-keymaps.git"
15+
},
16+
"author": "Radoslaw Szwajkowski <[email protected]>",
17+
"license": "Apache-2.0",
18+
"keywords": [
19+
"QEMUExtendedKeyEvent",
20+
"VNC",
21+
"Qemu",
22+
"scancode",
23+
"keysym",
24+
"keystroke"
25+
],
26+
"engines": {
27+
"yarn": "^1.22.0 ",
28+
"node": ">=20.19.0"
29+
},
30+
"scripts": {
31+
"build": "tsup",
32+
"pretest": "yarn lint",
33+
"test": "jest --rootDir=. --config=jest.config.json",
34+
"lint": "yarn eslint src --ext .ts",
35+
"lint:fix": "yarn lint --fix "
36+
},
37+
"tsup": {
38+
"entry": [
39+
"src/index.ts"
40+
],
41+
"splitting": false,
42+
"sourcemap": true,
43+
"clean": true,
44+
"format": [
45+
"esm"
46+
],
47+
"dts": true
48+
},
49+
"devDependencies": {
50+
"@eslint/js": "^9.28.0",
51+
"@types/jest": "^29.5.14",
52+
"eslint": "^9.28.0",
53+
"globals": "^16.2.0",
54+
"jest": "^29.7.0",
55+
"rimraf": "^6.0.1",
56+
"ts-jest": "^29.3.4",
57+
"tsup": "^8.5.0",
58+
"typescript": "^5.8.3",
59+
"typescript-eslint": "^8.33.1"
60+
}
61+
}

src/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export * from "./keymaps/keymaps";
2+
export * from "./keyboard";
3+
export * from "./types";
4+
export * from "./keysym2ucs";
5+
export * from "./name2keysym";

src/keyboard.ts

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ucs2keysym } from "./keysym2ucs";
2+
import { name2keysym } from "./name2keysym";
3+
import {
4+
CharMapping,
5+
CharMappingWithModifiers,
6+
HORIZONTAL_TAB,
7+
KeyMap,
8+
KeyModifier,
9+
LINE_FEED,
10+
} from "./types";
11+
12+
// scancodes based on '@novnc/novnc/lib/input/xtscancodes'
13+
export const modifierToCharMapping: { [key in KeyModifier]: CharMapping } = {
14+
altgr: {
15+
keysym: 0xffea /* XK_Alt_R */,
16+
scanCode: 0xe038 /* "AltRight" */,
17+
},
18+
control: {
19+
keysym: 0xffe4 /* XK_Control_R */,
20+
scanCode: 0xe01d /* "ControlRight" */,
21+
},
22+
numlock: {
23+
keysym: 0xff7f /* XK_Num_Lock */,
24+
scanCode: 0x45 /*"NumLock"*/,
25+
},
26+
shift: {
27+
keysym: 0xffe1 /* XK_Shift_L */,
28+
scanCode: 0x36 /*"ShiftRight"*/,
29+
},
30+
};
31+
32+
export const ENTER_MAPPING: CharMapping = {
33+
char: "Enter",
34+
keysym: 0xff0d /*XK_Return*/,
35+
scanCode: 0x1c /*"Enter" */,
36+
};
37+
export const HORIZONTAL_TAB_MAPPING = {
38+
char: "Tab",
39+
keysym: 0xff09 /* XK_Tab*/,
40+
scanCode: 0xf /*"Tab" */,
41+
};
42+
43+
const emptyMapping = (
44+
char: string,
45+
keysym: number
46+
): CharMappingWithModifiers => ({
47+
mapping: { char, keysym, scanCode: 0 },
48+
modifiers: [],
49+
});
50+
51+
export const resolveCharMapping = (
52+
char: string,
53+
keymap: KeyMap
54+
): {
55+
mapping: CharMapping;
56+
modifiers: CharMapping[];
57+
} => {
58+
const codePoint = char.codePointAt(0);
59+
if (codePoint === LINE_FEED) {
60+
return {
61+
mapping: ENTER_MAPPING,
62+
modifiers: [],
63+
};
64+
}
65+
66+
if (codePoint === HORIZONTAL_TAB) {
67+
return {
68+
mapping: HORIZONTAL_TAB_MAPPING,
69+
modifiers: [],
70+
};
71+
}
72+
73+
const unicode = char.codePointAt(0);
74+
if (unicode === undefined) {
75+
return emptyMapping(char, 0);
76+
}
77+
const keysym = ucs2keysym(unicode);
78+
79+
// based on https://github.com/qemu/qemu/blob/b69801dd6b1eb4d107f7c2f643adf0a4e3ec9124/ui/keymaps.c#L43
80+
// some rules are based on names in format UXXXX
81+
// FIXME: (only)'no' keymap uses keysyms as names in few cases (likely a bug)
82+
// example: 0x010000d7 which seems U00D7
83+
const unicodeBasedName = `U${Number(unicode)
84+
.toString(16)
85+
.toUpperCase()
86+
.padStart(4, "0")}`;
87+
const [mnemonicName = unicodeBasedName] = name2keysym
88+
.filter(([, keysymCode]) => keysymCode === keysym)
89+
.map(([_name]) => _name);
90+
91+
const [resolved] = keymap.filter(([name]) => name === mnemonicName);
92+
if (!resolved) {
93+
// no match in the keymap (by name)
94+
return emptyMapping(char, keysym);
95+
}
96+
97+
const [, scanCode, ...modifiers] = resolved;
98+
99+
return {
100+
mapping: {
101+
char,
102+
keysym,
103+
scanCode,
104+
},
105+
modifiers: modifiers.map((m) => modifierToCharMapping[m]),
106+
};
107+
};

0 commit comments

Comments
 (0)