From bfa0fc9fe50d7159f2b8d9d81ac95ec7edcc9ef8 Mon Sep 17 00:00:00 2001 From: catborise Date: Tue, 27 Oct 2026 08:37:04 +0300 Subject: [PATCH] feat(console): typed text follows the VM's keyboard layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "Type it" sent each character's keysym and let QEMU pick the key from its own US map, with Shift added for the US shifted characters. A VM on another layout got other characters: on Turkish Q, '"' typed 'İ', '@' typed "'", and ğ, ü, ş, ı, ö, ç typed nothing. The Clipboard dialog now asks for the VM's keyboard layout and remembers it per browser. Each character is typed as the key that makes it on that layout (its scancode), with Shift, AltGr or both (AltGr first: pressed after Shift, the right Alt is Alt on the Linux console), so the VM's own layout makes the character. The layout is fixed when typing starts. Text with a character no key types on the layout is not typed at all, and the dialog lists those characters: a password or a command with a character left out would be a wrong one. A layout is one file in console/layouts/: its name and, for each key, the characters it types plainly, with Shift, with AltGr and with Shift+AltGr. The view offers every file, US first. dev/console_layout.py writes a file from the system's XKB data as libxkbcommon computes it (through ctypes, no new dependency), mapping keys to DOM codes with noVNC's table from QEMU's keycodemapdb; it lists the dead keys it leaves out, and drops the keys the keyboard model lacks (the key left of Z on US pc104 keyboards). English (US) and Turkish Q come with it; the README says how to add one. --- README.md | 12 +++ console/layouts/tr.json | 54 ++++++++++++ console/layouts/us.json | 53 ++++++++++++ console/templates/console-vnc.html | 11 ++- console/tests/test_layouts.py | 47 +++++++++++ console/tests/test_views.py | 14 +++ console/views.py | 22 ++++- dev/console_layout.py | 131 +++++++++++++++++++++++++++++ static/js/console_layouts.js | 29 +++++++ static/js/console_vnc.js | 79 ++++++++++++----- 10 files changed, 431 insertions(+), 21 deletions(-) create mode 100644 console/layouts/tr.json create mode 100644 console/layouts/us.json create mode 100644 console/tests/test_layouts.py create mode 100644 dev/console_layout.py create mode 100644 static/js/console_layouts.js diff --git a/README.md b/README.md index 2f546a11..b8c1a8a6 100644 --- a/README.md +++ b/README.md @@ -698,6 +698,18 @@ Run every step as your normal user, not with `sudo`: if `manage.py` ever runs as Open `http://127.0.0.1:8000`. The browser consoles connect to `/novncd/` and `/socket.io/` on the panel's own address, which the dev server does not proxy: try them in the Docker image, or behind nginx with `conf/nginx/webvirtcloud.conf`. +### Keyboard layouts + +The VNC console's "Type it" sends the keys that type each character on the VM's keyboard layout. A layout is one file in `console/layouts/`, written from the XKB layout of the same name (needs `libxkbcommon` and `xkeyboard-config`, present on Linux desktops): + +```bash +python3 dev/console_layout.py de "German" # XKB layout, the name the console shows +python3 dev/console_layout.py tr "Turkish F" --variant f # a variant +python3 dev/console_layout.py us "English (US)" --model pc104 # a keyboard without the key left of Z +``` + +It lists the dead keys it leaves out: a character only a dead key types cannot be typed. Type a sample with every character group of the layout into a VM set to that layout before committing the file. + ### Running tests ```bash diff --git a/console/layouts/tr.json b/console/layouts/tr.json new file mode 100644 index 00000000..cc508dc5 --- /dev/null +++ b/console/layouts/tr.json @@ -0,0 +1,54 @@ +{ + "name": "Turkish Q", + "source": {"xkb_layout": "tr", "xkb_variant": "", "xkb_model": "pc105"}, + "keys": { + "Digit1": ["1", "!", ">", "¡"], + "Digit2": ["2", "'", "£", "²"], + "Digit3": ["3", "^", "#", "³"], + "Digit4": ["4", "+", "$", "¼"], + "Digit5": ["5", "%", "½", "⅜"], + "Digit6": ["6", "&", "¾", null], + "Digit7": ["7", "/", "{", null], + "Digit8": ["8", "(", "[", null], + "Digit9": ["9", ")", "]", "±"], + "Digit0": ["0", "=", "}", "°"], + "Minus": ["*", "?", "\\", "¿"], + "Equal": ["-", "_", "|", null], + "KeyQ": ["q", "Q", "@", "Ω"], + "KeyW": ["w", "W", null, null], + "KeyE": ["e", "E", "€", null], + "KeyR": ["r", "R", "¶", "®"], + "KeyT": ["t", "T", "₺", null], + "KeyY": ["y", "Y", "←", "¥"], + "KeyU": ["u", "U", "û", "Û"], + "KeyI": ["ı", "I", "î", "Î"], + "KeyO": ["o", "O", "ô", "Ô"], + "KeyP": ["p", "P", null, null], + "BracketLeft": ["ğ", "Ğ", null, null], + "BracketRight": ["ü", "Ü", "~", null], + "KeyA": ["a", "A", "â", "Â"], + "KeyS": ["s", "S", "ß", null], + "KeyD": ["d", "D", null, null], + "KeyF": ["f", "F", "ª", null], + "KeyG": ["g", "G", null, null], + "KeyH": ["h", "H", null, null], + "KeyJ": ["j", "J", null, null], + "KeyK": ["k", "K", null, null], + "KeyL": ["l", "L", null, null], + "Semicolon": ["ş", "Ş", "´", null], + "Quote": ["i", "İ", "'", null], + "Backquote": ["\"", "é", "<", "°"], + "Backslash": [",", ";", "`", null], + "KeyZ": ["z", "Z", "«", "<"], + "KeyX": ["x", "X", "»", ">"], + "KeyC": ["c", "C", "¢", "©"], + "KeyV": ["v", "V", "„", "‚"], + "KeyB": ["b", "B", "“", "‘"], + "KeyN": ["n", "N", "”", "’"], + "KeyM": ["m", "M", "µ", "º"], + "Comma": ["ö", "Ö", "×", null], + "Period": ["ç", "Ç", "·", "÷"], + "Slash": [".", ":", null, null], + "IntlBackslash": ["<", ">", "|", "¦"] + } +} diff --git a/console/layouts/us.json b/console/layouts/us.json new file mode 100644 index 00000000..68f8b572 --- /dev/null +++ b/console/layouts/us.json @@ -0,0 +1,53 @@ +{ + "name": "English (US)", + "source": {"xkb_layout": "us", "xkb_variant": "", "xkb_model": "pc104"}, + "keys": { + "Digit1": ["1", "!", null, null], + "Digit2": ["2", "@", null, null], + "Digit3": ["3", "#", null, null], + "Digit4": ["4", "$", null, null], + "Digit5": ["5", "%", null, null], + "Digit6": ["6", "^", null, null], + "Digit7": ["7", "&", null, null], + "Digit8": ["8", "*", null, null], + "Digit9": ["9", "(", null, null], + "Digit0": ["0", ")", null, null], + "Minus": ["-", "_", null, null], + "Equal": ["=", "+", null, null], + "KeyQ": ["q", "Q", null, null], + "KeyW": ["w", "W", null, null], + "KeyE": ["e", "E", null, null], + "KeyR": ["r", "R", null, null], + "KeyT": ["t", "T", null, null], + "KeyY": ["y", "Y", null, null], + "KeyU": ["u", "U", null, null], + "KeyI": ["i", "I", null, null], + "KeyO": ["o", "O", null, null], + "KeyP": ["p", "P", null, null], + "BracketLeft": ["[", "{", null, null], + "BracketRight": ["]", "}", null, null], + "KeyA": ["a", "A", null, null], + "KeyS": ["s", "S", null, null], + "KeyD": ["d", "D", null, null], + "KeyF": ["f", "F", null, null], + "KeyG": ["g", "G", null, null], + "KeyH": ["h", "H", null, null], + "KeyJ": ["j", "J", null, null], + "KeyK": ["k", "K", null, null], + "KeyL": ["l", "L", null, null], + "Semicolon": [";", ":", null, null], + "Quote": ["'", "\"", null, null], + "Backquote": ["`", "~", null, null], + "Backslash": ["\\", "|", null, null], + "KeyZ": ["z", "Z", null, null], + "KeyX": ["x", "X", null, null], + "KeyC": ["c", "C", null, null], + "KeyV": ["v", "V", null, null], + "KeyB": ["b", "B", null, null], + "KeyN": ["n", "N", null, null], + "KeyM": ["m", "M", null, null], + "Comma": [",", "<", null, null], + "Period": [".", ">", null, null], + "Slash": ["/", "?", null, null] + } +} diff --git a/console/templates/console-vnc.html b/console/templates/console-vnc.html index e408b757..a8f98898 100644 --- a/console/templates/console-vnc.html +++ b/console/templates/console-vnc.html @@ -7,6 +7,7 @@ #screen { flex: 1 1 auto; min-height: 0; overflow: hidden; } {{ vnc|json_script:"vnc-config" }} +{{ layouts|json_script:"keyboard-layouts" }} {% endblock %} @@ -103,8 +104,16 @@
- {% trans "Type it: the VM gets it key by key, also in its firmware, an installer or a login prompt. The VM's clipboard takes it only with clipboard support in the VM; what the VM copies shows here." %} + {% trans "Type it: the VM gets it key by key, as the keys of its keyboard layout, also in its firmware, an installer or a login prompt. The VM's clipboard takes it only with clipboard support in the VM; what the VM copies shows here." %}
+ + +