diff --git a/client/public/keyboard_layouts.json b/client/public/keyboard_layouts.json new file mode 100644 index 0000000..cebd463 --- /dev/null +++ b/client/public/keyboard_layouts.json @@ -0,0 +1 @@ +{"af":"Afghani","al":"Albanian","et":"Amharic","ma":"Arabic (Morocco)","sy":"Arabic (Syria)","am":"Armenian","az":"Azerbaijani","ml":"Bambara","bd":"Bangla","by":"Belarusian","be":"Belgian","dz":"Berber (Algeria, Latin characters)","ba":"Bosnian","bg":"Bulgarian","mm":"Burmese","hr":"Croatian","cz":"Czech","dk":"Danish","mv":"Dhivehi","nl":"Dutch","bt":"Dzongkha","au":"English (Australian)","cm":"English (Cameroon)","gh":"English (Ghana)","ng":"English (Nigeria)","za":"English (South Africa)","us":"English (US)","gb":"English (UK)","ee":"Estonian","fo":"Faroese","ph":"Filipino","fi":"Finnish","fr":"French","ca":"French (Canada)","cd":"French (Democratic Republic of the Congo)","gn":"French (Guinea)","tg":"French (Togo)","ge":"Georgian","de":"German","at":"German (Austria)","ch":"German (Switzerland)","gr":"Greek","il":"Hebrew","hu":"Hungarian","cn":"Chinese","is":"Icelandic","in":"Indian","id":"Indonesian (Jawi)","iq":"Iraqi","ie":"Irish","it":"Italian","jp":"Japanese","kz":"Kazakh","kh":"Khmer (Cambodia)","kr":"Korean","kg":"Kyrgyz","la":"Lao","lv":"Latvian","lt":"Lithuanian","mk":"Macedonian","my":"Malay (Jawi)","mt":"Maltese","md":"Moldavian","mn":"Mongolian","me":"Montenegrin","np":"Nepali","no":"Norwegian","ir":"Persian","pl":"Polish","pt":"Portuguese","br":"Portuguese (Brazil)","ro":"Romanian","ru":"Russian","rs":"Serbian","lk":"Sinhala (phonetic)","sk":"Slovak","si":"Slovenian","es":"Spanish","ke":"Swahili (Kenya)","tz":"Swahili (Tanzania)","se":"Swedish","tw":"Taiwanese","tj":"Tajik","th":"Thai","bw":"Tswana","tr":"Turkish","tm":"Turkmen","ua":"Ukrainian","pk":"Urdu (Pakistan)","uz":"Uzbek","vn":"Vietnamese","sn":"Wolof"} \ No newline at end of file diff --git a/client/src/components/settings.vue b/client/src/components/settings.vue index 91ca371..b5ae941 100644 --- a/client/src/components/settings.vue +++ b/client/src/components/settings.vue @@ -35,6 +35,19 @@ +
  • + {{ $t('setting.keyboard_layout') }} + +
  • @@ -182,6 +195,31 @@ } } } + + .select { + max-width: 120px; + + select { + display: block; + width: 100%; + max-width: 100%; + padding: 4px; + margin: 0; + line-height: 30px; + font-weight: bold; + border: 0; + border-radius: 12px; + + color: black; + background-color: $style-primary; + + option { + font-weight: normal; + color: $text-normal; + background-color: $background-tertiary; + } + } + } } } } @@ -236,6 +274,19 @@ this.$accessor.settings.setSound(value) } + get keyboard_layouts_list() { + return this.$accessor.settings.keyboard_layouts_list + } + + get keyboard_layout() { + return this.$accessor.settings.keyboard_layout + } + + set keyboard_layout(value: string) { + this.$accessor.settings.setKeyboardLayout(value) + this.$accessor.remote.changeKeyboard() + } + logout() { this.$accessor.logout() } diff --git a/client/src/components/video.vue b/client/src/components/video.vue index aea870a..cec547c 100644 --- a/client/src/components/video.vue +++ b/client/src/components/video.vue @@ -20,8 +20,6 @@ @mouseup.stop.prevent="onMouseUp" @mouseenter.stop.prevent="onMouseEnter" @mouseleave.stop.prevent="onMouseLeave" - @keydown.stop.prevent="onKeyDown" - @keyup.stop.prevent="onKeyUp" />
    @@ -158,6 +156,8 @@ import Resolution from './resolution.vue' import Clipboard from './clipboard.vue' + import GuacamoleKeyboard from '~/utils/guacamole-keyboard.ts' + @Component({ name: 'neko-video', components: { @@ -176,10 +176,10 @@ @Ref('resolution') readonly _resolution!: any @Ref('clipboard') readonly _clipboard!: any + private keyboard = GuacamoleKeyboard() private observer = new ResizeObserver(this.onResise.bind(this)) private focused = false private fullscreen = false - private activeKeys: Set = new Set() get admin() { return this.$accessor.user.admin @@ -353,6 +353,24 @@ document.addEventListener('focusin', this.onFocus.bind(this)) document.addEventListener('focusout', this.onBlur.bind(this)) + + /* Initialize Guacamole Keyboard */ + this.keyboard.onkeydown = (key: number) => { + if (!this.focused || !this.hosting || this.locked) { + return true + } + + this.$client.sendData('keydown', { key }) + return false + } + this.keyboard.onkeyup = (key: number) => { + if (!this.focused || !this.hosting || this.locked) { + return + } + + this.$client.sendData('keyup', { key }) + } + this.keyboard.listenTo(this._overlay) } beforeDestroy() { @@ -360,6 +378,7 @@ this.$accessor.video.setPlayable(false) document.removeEventListener('focusin', this.onFocus.bind(this)) document.removeEventListener('focusout', this.onBlur.bind(this)) + /* Guacamole Keyboard does not provide destroy functions */ } play() { @@ -427,10 +446,7 @@ return } - for (let key of this.activeKeys) { - this.$client.sendData('keyup', { key }) - this.activeKeys.delete(key) - } + this.keyboard.reset() } onMousePos(e: MouseEvent) { @@ -467,7 +483,7 @@ return } this.onMousePos(e) - this.$client.sendData('mousedown', { key: e.button }) + this.$client.sendData('mousedown', { key: e.button + 1 }) } onMouseUp(e: MouseEvent) { @@ -475,7 +491,7 @@ return } this.onMousePos(e) - this.$client.sendData('mouseup', { key: e.button }) + this.$client.sendData('mouseup', { key: e.button + 1 }) } onMouseMove(e: MouseEvent) { @@ -495,44 +511,6 @@ this.focused = false } - // frick you firefox - getCode(e: KeyboardEvent): number { - let key = e.keyCode - if (key === 59 && (e.key === ';' || e.key === ':')) { - key = 186 - } - - if (key === 61 && (e.key === '=' || e.key === '+')) { - key = 187 - } - - if (key === 173 && (e.key === '-' || e.key === '_')) { - key = 189 - } - - return key - } - - onKeyDown(e: KeyboardEvent) { - if (!this.focused || !this.hosting || this.locked) { - return - } - - let key = this.getCode(e) - this.$client.sendData('keydown', { key }) - this.activeKeys.add(key) - } - - onKeyUp(e: KeyboardEvent) { - if (!this.focused || !this.hosting || this.locked) { - return - } - - let key = this.getCode(e) - this.$client.sendData('keyup', { key }) - this.activeKeys.delete(key) - } - onResise() { let height = 0 if (!this.fullscreen) { diff --git a/client/src/locale/en-us.ts b/client/src/locale/en-us.ts index 1dfe645..2cb4932 100644 --- a/client/src/locale/en-us.ts +++ b/client/src/locale/en-us.ts @@ -60,6 +60,7 @@ export const setting = { autoplay: 'Autoplay Video', ignore_emotes: 'Ignore Emotes', chat_sound: 'Play Chat Sound', + keyboard_layout: 'Change Keyboard Layout', } export const connection = { diff --git a/client/src/neko/base.ts b/client/src/neko/base.ts index 26fc4f2..d8a40b3 100644 --- a/client/src/neko/base.ts +++ b/client/src/neko/base.ts @@ -123,19 +123,19 @@ export abstract class BaseClient extends EventEmitter { break case 'keydown': case 'mousedown': - buffer = new ArrayBuffer(5) + buffer = new ArrayBuffer(11) payload = new DataView(buffer) payload.setUint8(0, OPCODE.KEY_DOWN) - payload.setUint16(1, 1, true) - payload.setUint16(3, data.key, true) + payload.setUint16(1, 8, true) + payload.setBigUint64(3, BigInt(data.key), true) break case 'keyup': case 'mouseup': - buffer = new ArrayBuffer(5) + buffer = new ArrayBuffer(11) payload = new DataView(buffer) payload.setUint8(0, OPCODE.KEY_UP) - payload.setUint16(1, 1, true) - payload.setUint16(3, data.key, true) + payload.setUint16(1, 8, true) + payload.setBigUint64(3, BigInt(data.key), true) break default: this.emit('warn', `unknown data event: ${event}`) diff --git a/client/src/neko/events.ts b/client/src/neko/events.ts index b61476a..725e675 100644 --- a/client/src/neko/events.ts +++ b/client/src/neko/events.ts @@ -27,6 +27,7 @@ export const EVENT = { REQUESTING: 'control/requesting', CLIPBOARD: 'control/clipboard', GIVE: 'control/give', + KEYBOARD: 'control/keyboard' }, CHAT: { MESSAGE: 'chat/message', @@ -67,6 +68,7 @@ export type ControlEvents = | typeof EVENT.CONTROL.REQUEST | typeof EVENT.CONTROL.GIVE | typeof EVENT.CONTROL.CLIPBOARD + | typeof EVENT.CONTROL.KEYBOARD export type SystemEvents = typeof EVENT.SYSTEM.DISCONNECT export type MemberEvents = typeof EVENT.MEMBER.LIST | typeof EVENT.MEMBER.CONNECTED | typeof EVENT.MEMBER.DISCONNECTED diff --git a/client/src/neko/index.ts b/client/src/neko/index.ts index 880e793..e1f5b83 100644 --- a/client/src/neko/index.ts +++ b/client/src/neko/index.ts @@ -165,6 +165,8 @@ export class NekoClient extends BaseClient implements EventEmitter { ///////////////////////////// protected [EVENT.CONTROL.LOCKED]({ id }: ControlPayload) { this.$accessor.remote.setHost(id) + this.$accessor.remote.changeKeyboard() + const member = this.member(id) if (!member) { return @@ -251,6 +253,8 @@ export class NekoClient extends BaseClient implements EventEmitter { } this.$accessor.remote.setHost(member) + this.$accessor.remote.changeKeyboard() + this.$accessor.chat.newMessage({ id, content: this.$vue.$t('notifications.controls_given', { @@ -431,6 +435,7 @@ export class NekoClient extends BaseClient implements EventEmitter { protected [EVENT.ADMIN.CONTROL]({ id, target }: AdminTargetPayload) { this.$accessor.remote.setHost(id) + this.$accessor.remote.changeKeyboard() if (!target) { this.$accessor.chat.newMessage({ @@ -495,6 +500,7 @@ export class NekoClient extends BaseClient implements EventEmitter { } this.$accessor.remote.setHost(member) + this.$accessor.remote.changeKeyboard() this.$accessor.chat.newMessage({ id, diff --git a/client/src/neko/messages.ts b/client/src/neko/messages.ts index b1eec75..5192b54 100644 --- a/client/src/neko/messages.ts +++ b/client/src/neko/messages.ts @@ -30,6 +30,7 @@ export type WebSocketPayloads = | Member | ControlPayload | ControlClipboardPayload + | ControlKeyboardPayload | ChatPayload | ChatSendPayload | EmojiSendPayload @@ -120,6 +121,10 @@ export interface ControlClipboardPayload { text: string } +export interface ControlKeyboardPayload { + layout: string +} + /* CHAT PAYLOADS */ diff --git a/client/src/store/index.ts b/client/src/store/index.ts index 97ae6f5..ba18617 100644 --- a/client/src/store/index.ts +++ b/client/src/store/index.ts @@ -55,6 +55,7 @@ export const actions = actionTree( { initialise(store) { accessor.emoji.initialise() + accessor.settings.initialise() }, lock() { diff --git a/client/src/store/remote.ts b/client/src/store/remote.ts index 965af5c..991ccc6 100644 --- a/client/src/store/remote.ts +++ b/client/src/store/remote.ts @@ -133,5 +133,13 @@ export const actions = actionTree( $client.sendMessage(EVENT.ADMIN.GIVE, { id: member.id }) }, + + changeKeyboard({ getters }) { + if (!accessor.connected || !getters.hosting) { + return + } + + $client.sendMessage(EVENT.CONTROL.KEYBOARD, { layout: accessor.settings.keyboard_layout }) + } }, ) diff --git a/client/src/store/settings.ts b/client/src/store/settings.ts index 9e1e29a..eb1295d 100644 --- a/client/src/store/settings.ts +++ b/client/src/store/settings.ts @@ -1,8 +1,13 @@ -import { getterTree, mutationTree } from 'typed-vuex' +import { getterTree, mutationTree, actionTree } from 'typed-vuex' import { get, set } from '~/utils/localstorage' +import { accessor } from '~/store' export const namespaced = true +interface KeyboardLayouts { + [code: string]: string +} + export const state = () => { return { scroll: get('scroll', 10), @@ -10,6 +15,9 @@ export const state = () => { autoplay: get('autoplay', true), ignore_emotes: get('ignore_emotes', false), chat_sound: get('chat_sound', true), + keyboard_layout: get('keyboard_layout', 'us'), + + keyboard_layouts_list: {} as KeyboardLayouts, } } @@ -40,4 +48,28 @@ export const mutations = mutationTree(state, { state.chat_sound = value set('chat_sound', value) }, + + setKeyboardLayout(state, value: string) { + state.keyboard_layout = value + set('keyboard_layout', value) + }, + + setKeyboardLayoutsList(state, value: KeyboardLayouts) { + state.keyboard_layouts_list = value + }, }) + +export const actions = actionTree( + { state, getters, mutations }, + { + initialise() { + $http + .get('/keyboard_layouts.json') + .then((req) => { + accessor.settings.setKeyboardLayoutsList(req.data) + console.log(req.data) + }) + .catch(console.error) + }, + }, +) diff --git a/client/src/utils/guacamole-keyboard.js b/client/src/utils/guacamole-keyboard.js new file mode 100644 index 0000000..496dddc --- /dev/null +++ b/client/src/utils/guacamole-keyboard.js @@ -0,0 +1,1515 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +var Guacamole = Guacamole || {}; + +/** + * Provides cross-browser and cross-keyboard keyboard for a specific element. + * Browser and keyboard layout variation is abstracted away, providing events + * which represent keys as their corresponding X11 keysym. + * + * @constructor + * @param {Element|Document} [element] + * The Element to use to provide keyboard events. If omitted, at least one + * Element must be manually provided through the listenTo() function for + * the Guacamole.Keyboard instance to have any effect. + */ +Guacamole.Keyboard = function Keyboard(element) { + + /** + * Reference to this Guacamole.Keyboard. + * @private + */ + var guac_keyboard = this; + + /** + * An integer value which uniquely identifies this Guacamole.Keyboard + * instance with respect to other Guacamole.Keyboard instances. + * + * @private + * @type {Number} + */ + var guacKeyboardID = Guacamole.Keyboard._nextID++; + + /** + * The name of the property which is added to event objects via markEvent() + * to note that they have already been handled by this Guacamole.Keyboard. + * + * @private + * @constant + * @type {String} + */ + var EVENT_MARKER = '_GUAC_KEYBOARD_HANDLED_BY_' + guacKeyboardID; + + /** + * Fired whenever the user presses a key with the element associated + * with this Guacamole.Keyboard in focus. + * + * @event + * @param {Number} keysym The keysym of the key being pressed. + * @return {Boolean} true if the key event should be allowed through to the + * browser, false otherwise. + */ + this.onkeydown = null; + + /** + * Fired whenever the user releases a key with the element associated + * with this Guacamole.Keyboard in focus. + * + * @event + * @param {Number} keysym The keysym of the key being released. + */ + this.onkeyup = null; + + /** + * Set of known platform-specific or browser-specific quirks which must be + * accounted for to properly interpret key events, even if the only way to + * reliably detect that quirk is to platform/browser-sniff. + * + * @private + * @type {Object.} + */ + var quirks = { + + /** + * Whether keyup events are universally unreliable. + * + * @type {Boolean} + */ + keyupUnreliable: false, + + /** + * Whether the Alt key is actually a modifier for typable keys and is + * thus never used for keyboard shortcuts. + * + * @type {Boolean} + */ + altIsTypableOnly: false, + + /** + * Whether we can rely on receiving a keyup event for the Caps Lock + * key. + * + * @type {Boolean} + */ + capsLockKeyupUnreliable: false + + }; + + // Set quirk flags depending on platform/browser, if such information is + // available + if (navigator && navigator.platform) { + + // All keyup events are unreliable on iOS (sadly) + if (navigator.platform.match(/ipad|iphone|ipod/i)) + quirks.keyupUnreliable = true; + + // The Alt key on Mac is never used for keyboard shortcuts, and the + // Caps Lock key never dispatches keyup events + else if (navigator.platform.match(/^mac/i)) { + quirks.altIsTypableOnly = true; + quirks.capsLockKeyupUnreliable = true; + } + + } + + /** + * A key event having a corresponding timestamp. This event is non-specific. + * Its subclasses should be used instead when recording specific key + * events. + * + * @private + * @constructor + */ + var KeyEvent = function() { + + /** + * Reference to this key event. + */ + var key_event = this; + + /** + * An arbitrary timestamp in milliseconds, indicating this event's + * position in time relative to other events. + * + * @type {Number} + */ + this.timestamp = new Date().getTime(); + + /** + * Whether the default action of this key event should be prevented. + * + * @type {Boolean} + */ + this.defaultPrevented = false; + + /** + * The keysym of the key associated with this key event, as determined + * by a best-effort guess using available event properties and keyboard + * state. + * + * @type {Number} + */ + this.keysym = null; + + /** + * Whether the keysym value of this key event is known to be reliable. + * If false, the keysym may still be valid, but it's only a best guess, + * and future key events may be a better source of information. + * + * @type {Boolean} + */ + this.reliable = false; + + /** + * Returns the number of milliseconds elapsed since this event was + * received. + * + * @return {Number} The number of milliseconds elapsed since this + * event was received. + */ + this.getAge = function() { + return new Date().getTime() - key_event.timestamp; + }; + + }; + + /** + * Information related to the pressing of a key, which need not be a key + * associated with a printable character. The presence or absence of any + * information within this object is browser-dependent. + * + * @private + * @constructor + * @augments Guacamole.Keyboard.KeyEvent + * @param {Number} keyCode The JavaScript key code of the key pressed. + * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key + * pressed, as defined at: + * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent + * @param {String} key The standard name of the key pressed, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * @param {Number} location The location on the keyboard corresponding to + * the key pressed, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + */ + var KeydownEvent = function(keyCode, keyIdentifier, key, location) { + + // We extend KeyEvent + KeyEvent.apply(this); + + /** + * The JavaScript key code of the key pressed. + * + * @type {Number} + */ + this.keyCode = keyCode; + + /** + * The legacy DOM3 "keyIdentifier" of the key pressed, as defined at: + * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent + * + * @type {String} + */ + this.keyIdentifier = keyIdentifier; + + /** + * The standard name of the key pressed, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * + * @type {String} + */ + this.key = key; + + /** + * The location on the keyboard corresponding to the key pressed, as + * defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * + * @type {Number} + */ + this.location = location; + + // If key is known from keyCode or DOM3 alone, use that + this.keysym = keysym_from_key_identifier(key, location) + || keysym_from_keycode(keyCode, location); + + /** + * Whether the keyup following this keydown event is known to be + * reliable. If false, we cannot rely on the keyup event to occur. + * + * @type {Boolean} + */ + this.keyupReliable = !quirks.keyupUnreliable; + + // DOM3 and keyCode are reliable sources if the corresponding key is + // not a printable key + if (this.keysym && !isPrintable(this.keysym)) + this.reliable = true; + + // Use legacy keyIdentifier as a last resort, if it looks sane + if (!this.keysym && key_identifier_sane(keyCode, keyIdentifier)) + this.keysym = keysym_from_key_identifier(keyIdentifier, location, guac_keyboard.modifiers.shift); + + // If a key is pressed while meta is held down, the keyup will + // never be sent in Chrome (bug #108404) + if (guac_keyboard.modifiers.meta && this.keysym !== 0xFFE7 && this.keysym !== 0xFFE8) + this.keyupReliable = false; + + // We cannot rely on receiving keyup for Caps Lock on certain platforms + else if (this.keysym === 0xFFE5 && quirks.capsLockKeyupUnreliable) + this.keyupReliable = false; + + // Determine whether default action for Alt+combinations must be prevented + var prevent_alt = !guac_keyboard.modifiers.ctrl && !quirks.altIsTypableOnly; + + // Determine whether default action for Ctrl+combinations must be prevented + var prevent_ctrl = !guac_keyboard.modifiers.alt; + + // We must rely on the (potentially buggy) keyIdentifier if preventing + // the default action is important + if ((prevent_ctrl && guac_keyboard.modifiers.ctrl) + || (prevent_alt && guac_keyboard.modifiers.alt) + || guac_keyboard.modifiers.meta + || guac_keyboard.modifiers.hyper) + this.reliable = true; + + // Record most recently known keysym by associated key code + recentKeysym[keyCode] = this.keysym; + + }; + + KeydownEvent.prototype = new KeyEvent(); + + /** + * Information related to the pressing of a key, which MUST be + * associated with a printable character. The presence or absence of any + * information within this object is browser-dependent. + * + * @private + * @constructor + * @augments Guacamole.Keyboard.KeyEvent + * @param {Number} charCode The Unicode codepoint of the character that + * would be typed by the key pressed. + */ + var KeypressEvent = function(charCode) { + + // We extend KeyEvent + KeyEvent.apply(this); + + /** + * The Unicode codepoint of the character that would be typed by the + * key pressed. + * + * @type {Number} + */ + this.charCode = charCode; + + // Pull keysym from char code + this.keysym = keysym_from_charcode(charCode); + + // Keypress is always reliable + this.reliable = true; + + }; + + KeypressEvent.prototype = new KeyEvent(); + + /** + * Information related to the pressing of a key, which need not be a key + * associated with a printable character. The presence or absence of any + * information within this object is browser-dependent. + * + * @private + * @constructor + * @augments Guacamole.Keyboard.KeyEvent + * @param {Number} keyCode The JavaScript key code of the key released. + * @param {String} keyIdentifier The legacy DOM3 "keyIdentifier" of the key + * released, as defined at: + * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent + * @param {String} key The standard name of the key released, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * @param {Number} location The location on the keyboard corresponding to + * the key released, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + */ + var KeyupEvent = function(keyCode, keyIdentifier, key, location) { + + // We extend KeyEvent + KeyEvent.apply(this); + + /** + * The JavaScript key code of the key released. + * + * @type {Number} + */ + this.keyCode = keyCode; + + /** + * The legacy DOM3 "keyIdentifier" of the key released, as defined at: + * http://www.w3.org/TR/2009/WD-DOM-Level-3-Events-20090908/#events-Events-KeyboardEvent + * + * @type {String} + */ + this.keyIdentifier = keyIdentifier; + + /** + * The standard name of the key released, as defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * + * @type {String} + */ + this.key = key; + + /** + * The location on the keyboard corresponding to the key released, as + * defined at: + * http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + * + * @type {Number} + */ + this.location = location; + + // If key is known from keyCode or DOM3 alone, use that + this.keysym = keysym_from_keycode(keyCode, location) + || keysym_from_key_identifier(key, location); // keyCode is still more reliable for keyup when dead keys are in use + + // Fall back to the most recently pressed keysym associated with the + // keyCode if the inferred key doesn't seem to actually be pressed + if (!guac_keyboard.pressed[this.keysym]) + this.keysym = recentKeysym[keyCode] || this.keysym; + + // Keyup is as reliable as it will ever be + this.reliable = true; + + }; + + KeyupEvent.prototype = new KeyEvent(); + + /** + * An array of recorded events, which can be instances of the private + * KeydownEvent, KeypressEvent, and KeyupEvent classes. + * + * @private + * @type {KeyEvent[]} + */ + var eventLog = []; + + /** + * Map of known JavaScript keycodes which do not map to typable characters + * to their X11 keysym equivalents. + * @private + */ + var keycodeKeysyms = { + 8: [0xFF08], // backspace + 9: [0xFF09], // tab + 12: [0xFF0B, 0xFF0B, 0xFF0B, 0xFFB5], // clear / KP 5 + 13: [0xFF0D], // enter + 16: [0xFFE1, 0xFFE1, 0xFFE2], // shift + 17: [0xFFE3, 0xFFE3, 0xFFE4], // ctrl + 18: [0xFFE9, 0xFFE9, 0xFE03], // alt + 19: [0xFF13], // pause/break + 20: [0xFFE5], // caps lock + 27: [0xFF1B], // escape + 32: [0x0020], // space + 33: [0xFF55, 0xFF55, 0xFF55, 0xFFB9], // page up / KP 9 + 34: [0xFF56, 0xFF56, 0xFF56, 0xFFB3], // page down / KP 3 + 35: [0xFF57, 0xFF57, 0xFF57, 0xFFB1], // end / KP 1 + 36: [0xFF50, 0xFF50, 0xFF50, 0xFFB7], // home / KP 7 + 37: [0xFF51, 0xFF51, 0xFF51, 0xFFB4], // left arrow / KP 4 + 38: [0xFF52, 0xFF52, 0xFF52, 0xFFB8], // up arrow / KP 8 + 39: [0xFF53, 0xFF53, 0xFF53, 0xFFB6], // right arrow / KP 6 + 40: [0xFF54, 0xFF54, 0xFF54, 0xFFB2], // down arrow / KP 2 + 45: [0xFF63, 0xFF63, 0xFF63, 0xFFB0], // insert / KP 0 + 46: [0xFFFF, 0xFFFF, 0xFFFF, 0xFFAE], // delete / KP decimal + 91: [0xFFEB], // left window key (hyper_l) + 92: [0xFF67], // right window key (menu key?) + 93: null, // select key + 96: [0xFFB0], // KP 0 + 97: [0xFFB1], // KP 1 + 98: [0xFFB2], // KP 2 + 99: [0xFFB3], // KP 3 + 100: [0xFFB4], // KP 4 + 101: [0xFFB5], // KP 5 + 102: [0xFFB6], // KP 6 + 103: [0xFFB7], // KP 7 + 104: [0xFFB8], // KP 8 + 105: [0xFFB9], // KP 9 + 106: [0xFFAA], // KP multiply + 107: [0xFFAB], // KP add + 109: [0xFFAD], // KP subtract + 110: [0xFFAE], // KP decimal + 111: [0xFFAF], // KP divide + 112: [0xFFBE], // f1 + 113: [0xFFBF], // f2 + 114: [0xFFC0], // f3 + 115: [0xFFC1], // f4 + 116: [0xFFC2], // f5 + 117: [0xFFC3], // f6 + 118: [0xFFC4], // f7 + 119: [0xFFC5], // f8 + 120: [0xFFC6], // f9 + 121: [0xFFC7], // f10 + 122: [0xFFC8], // f11 + 123: [0xFFC9], // f12 + 144: [0xFF7F], // num lock + 145: [0xFF14], // scroll lock + 225: [0xFE03] // altgraph (iso_level3_shift) + }; + + /** + * Map of known JavaScript keyidentifiers which do not map to typable + * characters to their unshifted X11 keysym equivalents. + * @private + */ + var keyidentifier_keysym = { + "Again": [0xFF66], + "AllCandidates": [0xFF3D], + "Alphanumeric": [0xFF30], + "Alt": [0xFFE9, 0xFFE9, 0xFE03], + "Attn": [0xFD0E], + "AltGraph": [0xFE03], + "ArrowDown": [0xFF54], + "ArrowLeft": [0xFF51], + "ArrowRight": [0xFF53], + "ArrowUp": [0xFF52], + "Backspace": [0xFF08], + "CapsLock": [0xFFE5], + "Cancel": [0xFF69], + "Clear": [0xFF0B], + "Convert": [0xFF21], + "Copy": [0xFD15], + "Crsel": [0xFD1C], + "CrSel": [0xFD1C], + "CodeInput": [0xFF37], + "Compose": [0xFF20], + "Control": [0xFFE3, 0xFFE3, 0xFFE4], + "ContextMenu": [0xFF67], + "Delete": [0xFFFF], + "Down": [0xFF54], + "End": [0xFF57], + "Enter": [0xFF0D], + "EraseEof": [0xFD06], + "Escape": [0xFF1B], + "Execute": [0xFF62], + "Exsel": [0xFD1D], + "ExSel": [0xFD1D], + "F1": [0xFFBE], + "F2": [0xFFBF], + "F3": [0xFFC0], + "F4": [0xFFC1], + "F5": [0xFFC2], + "F6": [0xFFC3], + "F7": [0xFFC4], + "F8": [0xFFC5], + "F9": [0xFFC6], + "F10": [0xFFC7], + "F11": [0xFFC8], + "F12": [0xFFC9], + "F13": [0xFFCA], + "F14": [0xFFCB], + "F15": [0xFFCC], + "F16": [0xFFCD], + "F17": [0xFFCE], + "F18": [0xFFCF], + "F19": [0xFFD0], + "F20": [0xFFD1], + "F21": [0xFFD2], + "F22": [0xFFD3], + "F23": [0xFFD4], + "F24": [0xFFD5], + "Find": [0xFF68], + "GroupFirst": [0xFE0C], + "GroupLast": [0xFE0E], + "GroupNext": [0xFE08], + "GroupPrevious": [0xFE0A], + "FullWidth": null, + "HalfWidth": null, + "HangulMode": [0xFF31], + "Hankaku": [0xFF29], + "HanjaMode": [0xFF34], + "Help": [0xFF6A], + "Hiragana": [0xFF25], + "HiraganaKatakana": [0xFF27], + "Home": [0xFF50], + "Hyper": [0xFFED, 0xFFED, 0xFFEE], + "Insert": [0xFF63], + "JapaneseHiragana": [0xFF25], + "JapaneseKatakana": [0xFF26], + "JapaneseRomaji": [0xFF24], + "JunjaMode": [0xFF38], + "KanaMode": [0xFF2D], + "KanjiMode": [0xFF21], + "Katakana": [0xFF26], + "Left": [0xFF51], + "Meta": [0xFFE7, 0xFFE7, 0xFFE8], + "ModeChange": [0xFF7E], + "NumLock": [0xFF7F], + "PageDown": [0xFF56], + "PageUp": [0xFF55], + "Pause": [0xFF13], + "Play": [0xFD16], + "PreviousCandidate": [0xFF3E], + "PrintScreen": [0xFF61], + "Redo": [0xFF66], + "Right": [0xFF53], + "RomanCharacters": null, + "Scroll": [0xFF14], + "Select": [0xFF60], + "Separator": [0xFFAC], + "Shift": [0xFFE1, 0xFFE1, 0xFFE2], + "SingleCandidate": [0xFF3C], + "Super": [0xFFEB, 0xFFEB, 0xFFEC], + "Tab": [0xFF09], + "UIKeyInputDownArrow": [0xFF54], + "UIKeyInputEscape": [0xFF1B], + "UIKeyInputLeftArrow": [0xFF51], + "UIKeyInputRightArrow": [0xFF53], + "UIKeyInputUpArrow": [0xFF52], + "Up": [0xFF52], + "Undo": [0xFF65], + "Win": [0xFFEB], + "Zenkaku": [0xFF28], + "ZenkakuHankaku": [0xFF2A] + }; + + /** + * All keysyms which should not repeat when held down. + * @private + */ + var no_repeat = { + 0xFE03: true, // ISO Level 3 Shift (AltGr) + 0xFFE1: true, // Left shift + 0xFFE2: true, // Right shift + 0xFFE3: true, // Left ctrl + 0xFFE4: true, // Right ctrl + 0xFFE5: true, // Caps Lock + 0xFFE7: true, // Left meta + 0xFFE8: true, // Right meta + 0xFFE9: true, // Left alt + 0xFFEA: true, // Right alt + 0xFFEB: true, // Left hyper + 0xFFEC: true // Right hyper + }; + + /** + * All modifiers and their states. + */ + this.modifiers = new Guacamole.Keyboard.ModifierState(); + + /** + * The state of every key, indexed by keysym. If a particular key is + * pressed, the value of pressed for that keysym will be true. If a key + * is not currently pressed, it will not be defined. + */ + this.pressed = {}; + + /** + * The state of every key, indexed by keysym, for strictly those keys whose + * status has been indirectly determined thorugh observation of other key + * events. If a particular key is implicitly pressed, the value of + * implicitlyPressed for that keysym will be true. If a key + * is not currently implicitly pressed (the key is not pressed OR the state + * of the key is explicitly known), it will not be defined. + * + * @private + * @tyle {Object.} + */ + var implicitlyPressed = {}; + + /** + * The last result of calling the onkeydown handler for each key, indexed + * by keysym. This is used to prevent/allow default actions for key events, + * even when the onkeydown handler cannot be called again because the key + * is (theoretically) still pressed. + * + * @private + */ + var last_keydown_result = {}; + + /** + * The keysym most recently associated with a given keycode when keydown + * fired. This object maps keycodes to keysyms. + * + * @private + * @type {Object.} + */ + var recentKeysym = {}; + + /** + * Timeout before key repeat starts. + * @private + */ + var key_repeat_timeout = null; + + /** + * Interval which presses and releases the last key pressed while that + * key is still being held down. + * @private + */ + var key_repeat_interval = null; + + /** + * Given an array of keysyms indexed by location, returns the keysym + * for the given location, or the keysym for the standard location if + * undefined. + * + * @private + * @param {Number[]} keysyms + * An array of keysyms, where the index of the keysym in the array is + * the location value. + * + * @param {Number} location + * The location on the keyboard corresponding to the key pressed, as + * defined at: http://www.w3.org/TR/DOM-Level-3-Events/#events-KeyboardEvent + */ + var get_keysym = function get_keysym(keysyms, location) { + + if (!keysyms) + return null; + + return keysyms[location] || keysyms[0]; + }; + + /** + * Returns true if the given keysym corresponds to a printable character, + * false otherwise. + * + * @param {Number} keysym + * The keysym to check. + * + * @returns {Boolean} + * true if the given keysym corresponds to a printable character, + * false otherwise. + */ + var isPrintable = function isPrintable(keysym) { + + // Keysyms with Unicode equivalents are printable + return (keysym >= 0x00 && keysym <= 0xFF) + || (keysym & 0xFFFF0000) === 0x01000000; + + }; + + function keysym_from_key_identifier(identifier, location, shifted) { + + if (!identifier) + return null; + + var typedCharacter; + + // If identifier is U+xxxx, decode Unicode character + var unicodePrefixLocation = identifier.indexOf("U+"); + if (unicodePrefixLocation >= 0) { + var hex = identifier.substring(unicodePrefixLocation+2); + typedCharacter = String.fromCharCode(parseInt(hex, 16)); + } + + // If single character and not keypad, use that as typed character + else if (identifier.length === 1 && location !== 3) + typedCharacter = identifier; + + // Otherwise, look up corresponding keysym + else + return get_keysym(keyidentifier_keysym[identifier], location); + + // Alter case if necessary + if (shifted === true) + typedCharacter = typedCharacter.toUpperCase(); + else if (shifted === false) + typedCharacter = typedCharacter.toLowerCase(); + + // Get codepoint + var codepoint = typedCharacter.charCodeAt(0); + return keysym_from_charcode(codepoint); + + } + + function isControlCharacter(codepoint) { + return codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F); + } + + function keysym_from_charcode(codepoint) { + + // Keysyms for control characters + if (isControlCharacter(codepoint)) return 0xFF00 | codepoint; + + // Keysyms for ASCII chars + if (codepoint >= 0x0000 && codepoint <= 0x00FF) + return codepoint; + + // Keysyms for Unicode + if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) + return 0x01000000 | codepoint; + + return null; + + } + + function keysym_from_keycode(keyCode, location) { + return get_keysym(keycodeKeysyms[keyCode], location); + } + + /** + * Heuristically detects if the legacy keyIdentifier property of + * a keydown/keyup event looks incorrectly derived. Chrome, and + * presumably others, will produce the keyIdentifier by assuming + * the keyCode is the Unicode codepoint for that key. This is not + * correct in all cases. + * + * @private + * @param {Number} keyCode + * The keyCode from a browser keydown/keyup event. + * + * @param {String} keyIdentifier + * The legacy keyIdentifier from a browser keydown/keyup event. + * + * @returns {Boolean} + * true if the keyIdentifier looks sane, false if the keyIdentifier + * appears incorrectly derived or is missing entirely. + */ + var key_identifier_sane = function key_identifier_sane(keyCode, keyIdentifier) { + + // Missing identifier is not sane + if (!keyIdentifier) + return false; + + // Assume non-Unicode keyIdentifier values are sane + var unicodePrefixLocation = keyIdentifier.indexOf("U+"); + if (unicodePrefixLocation === -1) + return true; + + // If the Unicode codepoint isn't identical to the keyCode, + // then the identifier is likely correct + var codepoint = parseInt(keyIdentifier.substring(unicodePrefixLocation+2), 16); + if (keyCode !== codepoint) + return true; + + // The keyCodes for A-Z and 0-9 are actually identical to their + // Unicode codepoints + if ((keyCode >= 65 && keyCode <= 90) || (keyCode >= 48 && keyCode <= 57)) + return true; + + // The keyIdentifier does NOT appear sane + return false; + + }; + + /** + * Marks a key as pressed, firing the keydown event if registered. Key + * repeat for the pressed key will start after a delay if that key is + * not a modifier. The return value of this function depends on the + * return value of the keydown event handler, if any. + * + * @param {Number} keysym The keysym of the key to press. + * @return {Boolean} true if event should NOT be canceled, false otherwise. + */ + this.press = function(keysym) { + + // Don't bother with pressing the key if the key is unknown + if (keysym === null) return; + + // Only press if released + if (!guac_keyboard.pressed[keysym]) { + + // Mark key as pressed + guac_keyboard.pressed[keysym] = true; + + // Send key event + if (guac_keyboard.onkeydown) { + var result = guac_keyboard.onkeydown(keysym); + last_keydown_result[keysym] = result; + + // Stop any current repeat + window.clearTimeout(key_repeat_timeout); + window.clearInterval(key_repeat_interval); + + // Repeat after a delay as long as pressed + if (!no_repeat[keysym]) + key_repeat_timeout = window.setTimeout(function() { + key_repeat_interval = window.setInterval(function() { + guac_keyboard.onkeyup(keysym); + guac_keyboard.onkeydown(keysym); + }, 50); + }, 500); + + return result; + } + } + + // Return the last keydown result by default, resort to false if unknown + return last_keydown_result[keysym] || false; + + }; + + /** + * Marks a key as released, firing the keyup event if registered. + * + * @param {Number} keysym The keysym of the key to release. + */ + this.release = function(keysym) { + + // Only release if pressed + if (guac_keyboard.pressed[keysym]) { + + // Mark key as released + delete guac_keyboard.pressed[keysym]; + delete implicitlyPressed[keysym]; + + // Stop repeat + window.clearTimeout(key_repeat_timeout); + window.clearInterval(key_repeat_interval); + + // Send key event + if (keysym !== null && guac_keyboard.onkeyup) + guac_keyboard.onkeyup(keysym); + + } + + }; + + /** + * Presses and releases the keys necessary to type the given string of + * text. + * + * @param {String} str + * The string to type. + */ + this.type = function type(str) { + + // Press/release the key corresponding to each character in the string + for (var i = 0; i < str.length; i++) { + + // Determine keysym of current character + var codepoint = str.codePointAt ? str.codePointAt(i) : str.charCodeAt(i); + var keysym = keysym_from_charcode(codepoint); + + // Press and release key for current character + guac_keyboard.press(keysym); + guac_keyboard.release(keysym); + + } + + }; + + /** + * Resets the state of this keyboard, releasing all keys, and firing keyup + * events for each released key. + */ + this.reset = function() { + + // Release all pressed keys + for (var keysym in guac_keyboard.pressed) + guac_keyboard.release(parseInt(keysym)); + + // Clear event log + eventLog = []; + + }; + + /** + * Given the remote and local state of a particular key, resynchronizes the + * remote state of that key with the local state through pressing or + * releasing keysyms. + * + * @private + * @param {Boolean} remoteState + * Whether the key is currently pressed remotely. + * + * @param {Boolean} localState + * Whether the key is currently pressed remotely locally. If the state + * of the key is not known, this may be undefined. + * + * @param {Number[]} keysyms + * The keysyms which represent the key being updated. + * + * @param {KeyEvent} keyEvent + * Guacamole's current best interpretation of the key event being + * processed. + */ + var updateModifierState = function updateModifierState(remoteState, + localState, keysyms, keyEvent) { + + var i; + + // Do not trust changes in modifier state for events directly involving + // that modifier: (1) the flag may erroneously be cleared despite + // another version of the same key still being held and (2) the change + // in flag may be due to the current event being processed, thus + // updating things here is at best redundant and at worst incorrect + if (keysyms.indexOf(keyEvent.keysym) !== -1) + return; + + // Release all related keys if modifier is implicitly released + if (remoteState && localState === false) { + for (i = 0; i < keysyms.length; i++) { + guac_keyboard.release(keysyms[i]); + } + } + + // Press if modifier is implicitly pressed + else if (!remoteState && localState) { + + // Verify that modifier flag isn't already pressed or already set + // due to another version of the same key being held down + for (i = 0; i < keysyms.length; i++) { + if (guac_keyboard.pressed[keysyms[i]]) + return; + } + + // Mark as implicitly pressed only if there is other information + // within the key event relating to a different key. Some + // platforms, such as iOS, will send essentially empty key events + // for modifier keys, using only the modifier flags to signal the + // identity of the key. + var keysym = keysyms[0]; + if (keyEvent.keysym) + implicitlyPressed[keysym] = true; + + guac_keyboard.press(keysym); + + } + + }; + + /** + * Given a keyboard event, updates the local modifier state and remote + * key state based on the modifier flags within the event. This function + * pays no attention to keycodes. + * + * @private + * @param {KeyboardEvent} e + * The keyboard event containing the flags to update. + * + * @param {KeyEvent} keyEvent + * Guacamole's current best interpretation of the key event being + * processed. + */ + var syncModifierStates = function syncModifierStates(e, keyEvent) { + + // Get state + var state = Guacamole.Keyboard.ModifierState.fromKeyboardEvent(e); + + // Resync state of alt + updateModifierState(guac_keyboard.modifiers.alt, state.alt, [ + 0xFFE9, // Left alt + 0xFFEA, // Right alt + 0xFE03 // AltGr + ], keyEvent); + + // Resync state of shift + updateModifierState(guac_keyboard.modifiers.shift, state.shift, [ + 0xFFE1, // Left shift + 0xFFE2 // Right shift + ], keyEvent); + + // Resync state of ctrl + updateModifierState(guac_keyboard.modifiers.ctrl, state.ctrl, [ + 0xFFE3, // Left ctrl + 0xFFE4 // Right ctrl + ], keyEvent); + + // Resync state of meta + updateModifierState(guac_keyboard.modifiers.meta, state.meta, [ + 0xFFE7, // Left meta + 0xFFE8 // Right meta + ], keyEvent); + + // Resync state of hyper + updateModifierState(guac_keyboard.modifiers.hyper, state.hyper, [ + 0xFFEB, // Left hyper + 0xFFEC // Right hyper + ], keyEvent); + + // Update state + guac_keyboard.modifiers = state; + + }; + + /** + * Returns whether all currently pressed keys were implicitly pressed. A + * key is implicitly pressed if its status was inferred indirectly from + * inspection of other key events. + * + * @private + * @returns {Boolean} + * true if all currently pressed keys were implicitly pressed, false + * otherwise. + */ + var isStateImplicit = function isStateImplicit() { + + for (var keysym in guac_keyboard.pressed) { + if (!implicitlyPressed[keysym]) + return false; + } + + return true; + + }; + + /** + * Reads through the event log, removing events from the head of the log + * when the corresponding true key presses are known (or as known as they + * can be). + * + * @private + * @return {Boolean} Whether the default action of the latest event should + * be prevented. + */ + function interpret_events() { + + // Do not prevent default if no event could be interpreted + var handled_event = interpret_event(); + if (!handled_event) + return false; + + // Interpret as much as possible + var last_event; + do { + last_event = handled_event; + handled_event = interpret_event(); + } while (handled_event !== null); + + // Reset keyboard state if we cannot expect to receive any further + // keyup events + if (isStateImplicit()) + guac_keyboard.reset(); + + return last_event.defaultPrevented; + + } + + /** + * Releases Ctrl+Alt, if both are currently pressed and the given keysym + * looks like a key that may require AltGr. + * + * @private + * @param {Number} keysym The key that was just pressed. + */ + var release_simulated_altgr = function release_simulated_altgr(keysym) { + + // Both Ctrl+Alt must be pressed if simulated AltGr is in use + if (!guac_keyboard.modifiers.ctrl || !guac_keyboard.modifiers.alt) + return; + + // Assume [A-Z] never require AltGr + if (keysym >= 0x0041 && keysym <= 0x005A) + return; + + // Assume [a-z] never require AltGr + if (keysym >= 0x0061 && keysym <= 0x007A) + return; + + // Release Ctrl+Alt if the keysym is printable + if (keysym <= 0xFF || (keysym & 0xFF000000) === 0x01000000) { + guac_keyboard.release(0xFFE3); // Left ctrl + guac_keyboard.release(0xFFE4); // Right ctrl + guac_keyboard.release(0xFFE9); // Left alt + guac_keyboard.release(0xFFEA); // Right alt + } + + }; + + /** + * Reads through the event log, interpreting the first event, if possible, + * and returning that event. If no events can be interpreted, due to a + * total lack of events or the need for more events, null is returned. Any + * interpreted events are automatically removed from the log. + * + * @private + * @return {KeyEvent} + * The first key event in the log, if it can be interpreted, or null + * otherwise. + */ + var interpret_event = function interpret_event() { + + // Peek at first event in log + var first = eventLog[0]; + if (!first) + return null; + + // Keydown event + if (first instanceof KeydownEvent) { + + var keysym = null; + var accepted_events = []; + + // If event itself is reliable, no need to wait for other events + if (first.reliable) { + keysym = first.keysym; + accepted_events = eventLog.splice(0, 1); + } + + // If keydown is immediately followed by a keypress, use the indicated character + else if (eventLog[1] instanceof KeypressEvent) { + keysym = eventLog[1].keysym; + accepted_events = eventLog.splice(0, 2); + } + + // If keydown is immediately followed by anything else, then no + // keypress can possibly occur to clarify this event, and we must + // handle it now + else if (eventLog[1]) { + keysym = first.keysym; + accepted_events = eventLog.splice(0, 1); + } + + // Fire a key press if valid events were found + if (accepted_events.length > 0) { + + if (keysym) { + + // Fire event + release_simulated_altgr(keysym); + var defaultPrevented = !guac_keyboard.press(keysym); + recentKeysym[first.keyCode] = keysym; + + // Release the key now if we cannot rely on the associated + // keyup event + if (!first.keyupReliable) + guac_keyboard.release(keysym); + + // Record whether default was prevented + for (var i=0; i boolean; + + /** + * Fired whenever the user releases a key with the element associated + * with this Guacamole.Keyboard in focus. + * + * @event + * @param {Number} keysym The keysym of the key being released. + */ + onkeyup?: (keysym: number) => void; + + /** + * Marks a key as pressed, firing the keydown event if registered. Key + * repeat for the pressed key will start after a delay if that key is + * not a modifier. The return value of this function depends on the + * return value of the keydown event handler, if any. + * + * @param {Number} keysym The keysym of the key to press. + * @return {Boolean} true if event should NOT be canceled, false otherwise. + */ + press: (keysym: number) => boolean; + + /** + * Marks a key as released, firing the keyup event if registered. + * + * @param {Number} keysym The keysym of the key to release. + */ + release: (keysym: number) => void; + + /** + * Presses and releases the keys necessary to type the given string of + * text. + * + * @param {String} str + * The string to type. + */ + type: (str: string) => void; + + /** + * Resets the state of this keyboard, releasing all keys, and firing keyup + * events for each released key. + */ + reset: () => void; + + /** + * Attaches event listeners to the given Element, automatically translating + * received key, input, and composition events into simple keydown/keyup + * events signalled through this Guacamole.Keyboard's onkeydown and + * onkeyup handlers. + * + * @param {Element|Document} element + * The Element to attach event listeners to for the sake of handling + * key or input events. + */ + listenTo: (element: Element | Document) => void; +} + +export default function(element?: Element): GuacamoleKeyboardInterface { + var Keyboard = {}; + + GuacamoleKeyboard.bind(Keyboard, element)(); + + return Keyboard as GuacamoleKeyboardInterface; +} diff --git a/server/internal/remote/manager.go b/server/internal/remote/manager.go index f0af950..7878b26 100644 --- a/server/internal/remote/manager.go +++ b/server/internal/remote/manager.go @@ -181,19 +181,19 @@ func (manager *RemoteManager) Scroll(x, y int) { xorg.Scroll(x, y) } -func (manager *RemoteManager) ButtonDown(code int) (*types.Button, error) { +func (manager *RemoteManager) ButtonDown(code int) error { return xorg.ButtonDown(code) } -func (manager *RemoteManager) KeyDown(code int) (*types.Key, error) { +func (manager *RemoteManager) KeyDown(code uint64) error { return xorg.KeyDown(code) } -func (manager *RemoteManager) ButtonUp(code int) (*types.Button, error) { +func (manager *RemoteManager) ButtonUp(code int) error { return xorg.ButtonUp(code) } -func (manager *RemoteManager) KeyUp(code int) (*types.Key, error) { +func (manager *RemoteManager) KeyUp(code uint64) error { return xorg.KeyUp(code) } @@ -216,3 +216,7 @@ func (manager *RemoteManager) ScreenConfigurations() map[int]types.ScreenConfigu func (manager *RemoteManager) GetScreenSize() *types.ScreenSize { return xorg.GetScreenSize() } + +func (manager *RemoteManager) SetKeyboardLayout(layout string) { + xorg.SetKeyboardLayout(layout) +} \ No newline at end of file diff --git a/server/internal/types/event/events.go b/server/internal/types/event/events.go index 1ee171e..7b9d31b 100644 --- a/server/internal/types/event/events.go +++ b/server/internal/types/event/events.go @@ -22,6 +22,7 @@ const ( CONTROL_REQUESTING = "control/requesting" CONTROL_GIVE = "control/give" CONTROL_CLIPBOARD = "control/clipboard" + CONTROL_KEYBOARD = "control/keyboard" ) const ( diff --git a/server/internal/types/message/messages.go b/server/internal/types/message/messages.go index a2f099f..9d99481 100644 --- a/server/internal/types/message/messages.go +++ b/server/internal/types/message/messages.go @@ -46,6 +46,11 @@ type Clipboard struct { Text string `json:"text"` } +type Keyboard struct { + Event string `json:"event"` + Layout string `json:"layout"` +} + type Control struct { Event string `json:"event"` ID string `json:"id"` diff --git a/server/internal/types/remote.go b/server/internal/types/remote.go index ef6ef0e..d2b0e36 100644 --- a/server/internal/types/remote.go +++ b/server/internal/types/remote.go @@ -15,11 +15,12 @@ type RemoteManager interface { ScreenConfigurations() map[int]ScreenConfiguration Move(x, y int) Scroll(x, y int) - ButtonDown(code int) (*Button, error) - KeyDown(code int) (*Key, error) - ButtonUp(code int) (*Button, error) - KeyUp(code int) (*Key, error) + ButtonDown(code int) error + KeyDown(code uint64) error + ButtonUp(code int) error + KeyUp(code uint64) error ReadClipboard() string WriteClipboard(data string) ResetKeys() + SetKeyboardLayout(layout string) } diff --git a/server/internal/webrtc/handle.go b/server/internal/webrtc/handle.go index 321d54c..35c4b23 100644 --- a/server/internal/webrtc/handle.go +++ b/server/internal/webrtc/handle.go @@ -33,7 +33,7 @@ type PayloadScroll struct { type PayloadKey struct { PayloadHeader - Key uint16 + Key uint64 } func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) error { @@ -85,21 +85,21 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e } if payload.Key < 8 { - button, err := manager.remote.ButtonDown(int(payload.Key)) + err := manager.remote.ButtonDown(int(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("key down failed") + manager.logger.Warn().Err(err).Msg("button down failed") return nil } - manager.logger.Debug().Msgf("button down %s(%d)", button.Name, payload.Key) + manager.logger.Debug().Msgf("button down %d", payload.Key) } else { - key, err := manager.remote.KeyDown(int(payload.Key)) + err := manager.remote.KeyDown(uint64(payload.Key)) if err != nil { manager.logger.Warn().Err(err).Msg("key down failed") return nil } - manager.logger.Debug().Msgf("key down %s(%d)", key.Name, payload.Key) + manager.logger.Debug().Msgf("key down %d", payload.Key) } break @@ -111,21 +111,21 @@ func (manager *WebRTCManager) handle(id string, msg webrtc.DataChannelMessage) e } if payload.Key < 8 { - button, err := manager.remote.ButtonUp(int(payload.Key)) + err := manager.remote.ButtonUp(int(payload.Key)) if err != nil { manager.logger.Warn().Err(err).Msg("button up failed") return nil } - manager.logger.Debug().Msgf("button up %s(%d)", button.Name, payload.Key) + manager.logger.Debug().Msgf("button up %d", payload.Key) } else { - key, err := manager.remote.KeyUp(int(payload.Key)) + err := manager.remote.KeyUp(uint64(payload.Key)) if err != nil { - manager.logger.Warn().Err(err).Msg("keyup failed") + manager.logger.Warn().Err(err).Msg("key up failed") return nil } - manager.logger.Debug().Msgf("key up %s(%d)", key.Name, payload.Key) + manager.logger.Debug().Msgf("key up %d", payload.Key) } break case OP_KEY_CLK: diff --git a/server/internal/websocket/control.go b/server/internal/websocket/control.go index bce0aee..7a2ffff 100644 --- a/server/internal/websocket/control.go +++ b/server/internal/websocket/control.go @@ -115,3 +115,14 @@ func (h *MessageHandler) controlClipboard(id string, session types.Session, payl h.remote.WriteClipboard(payload.Text) return nil } + +func (h *MessageHandler) controlKeyboard(id string, session types.Session, payload *message.Keyboard) error { + // check if session is host + if !h.sessions.IsHost(id) { + h.logger.Debug().Str("id", id).Msg("is not the host") + return nil + } + + h.remote.SetKeyboardLayout(payload.Layout) + return nil +} diff --git a/server/internal/websocket/handler.go b/server/internal/websocket/handler.go index 7ad21d3..5693c97 100644 --- a/server/internal/websocket/handler.go +++ b/server/internal/websocket/handler.go @@ -89,6 +89,13 @@ func (h *MessageHandler) Message(id string, raw []byte) error { utils.Unmarshal(payload, raw, func() error { return h.controlClipboard(id, session, payload) }), "%s failed", header.Event) + case event.CONTROL_KEYBOARD: + payload := &message.Keyboard{} + return errors.Wrapf( + utils.Unmarshal(payload, raw, func() error { + return h.controlKeyboard(id, session, payload) + }), "%s failed", header.Event) + // Chat Events case event.CHAT_MESSAGE: diff --git a/server/internal/xorg/keycode/button.go b/server/internal/xorg/keycode/button.go deleted file mode 100644 index 00c3577..0000000 --- a/server/internal/xorg/keycode/button.go +++ /dev/null @@ -1,45 +0,0 @@ -package keycode - -import "n.eko.moe/neko/internal/types" - -var LEFT_BUTTON = types.Button{ - Name: "LEFT", - Code: 0, - Keysym: 1, -} - -var CENTER_BUTTON = types.Button{ - Name: "CENTER", - Code: 1, - Keysym: 2, -} - -var RIGHT_BUTTON = types.Button{ - Name: "RIGHT", - Code: 2, - Keysym: 3, -} - -var SCROLL_UP_BUTTON = types.Button{ - Name: "SCROLL_UP", - Code: 3, - Keysym: 4, -} - -var SCROLL_DOWN_BUTTON = types.Button{ - Name: "SCROLL_DOWN", - Code: 4, - Keysym: 5, -} - -var SCROLL_LEFT_BUTTON = types.Button{ - Name: "SCROLL_LEFT", - Code: 5, - Keysym: 6, -} - -var SCROLL_RIGHT_BUTTON = types.Button{ - Name: "SCROLL_RIGHT", - Code: 6, - Keysym: 7, -} diff --git a/server/internal/xorg/keycode/keys.go b/server/internal/xorg/keycode/keys.go deleted file mode 100644 index 56bae71..0000000 --- a/server/internal/xorg/keycode/keys.go +++ /dev/null @@ -1,696 +0,0 @@ -package keycode - -import "n.eko.moe/neko/internal/types" - -var BACKSPACE = types.Key{ - Name: "BACKSPACE", - Value: "BackSpace", - Code: 8, - Keysym: int(0xff08), -} - -var TAB = types.Key{ - Name: "TAB", - Value: "Tab", - Code: 9, - Keysym: int(0xFF09), -} - -var CLEAR = types.Key{ - Name: "CLEAR", - Value: "Clear", - Code: 12, - Keysym: int(0xFF0B), -} - -var ENTER = types.Key{ - Name: "ENTER", - Value: "Enter", - Code: 13, - Keysym: int(0xFF0D), -} - -var SHIFT = types.Key{ - Name: "SHIFT", - Value: "Shift", - Code: 16, - Keysym: int(0xFFE1), -} - -var CTRL = types.Key{ - Name: "CTRL", - Value: "Ctrl", - Code: 17, - Keysym: int(0xFFE3), -} - -var ALT = types.Key{ - Name: "ALT", - Value: "Alt", - Code: 18, - Keysym: int(0xFFE9), -} - -var PAUSE = types.Key{ - Name: "PAUSE", - Value: "Pause", - Code: 19, - Keysym: int(0xFF13), -} - -var CAPS_LOCK = types.Key{ - Name: "CAPS_LOCK", - Value: "Caps Lock", - Code: 20, - Keysym: int(0xFFE5), -} - -var ESCAPE = types.Key{ - Name: "ESCAPE", - Value: "Escape", - Code: 27, - Keysym: int(0xFF1B), -} - -var SPACE = types.Key{ - Name: "SPACE", - Value: " ", - Code: 32, - Keysym: int(0x0020), -} - -var PAGE_UP = types.Key{ - Name: "PAGE_UP", - Value: "Page Up", - Code: 33, - Keysym: int(0xFF55), -} - -var PAGE_DOWN = types.Key{ - Name: "PAGE_DOWN", - Value: "Page Down", - Code: 34, - Keysym: int(0xFF56), -} - -var END = types.Key{ - Name: "END", - Value: "End", - Code: 35, - Keysym: int(0xFF57), -} - -var HOME = types.Key{ - Name: "HOME", - Value: "Home", - Code: 36, - Keysym: int(0xFF50), -} - -var LEFT_ARROW = types.Key{ - Name: "LEFT_ARROW", - Value: "Left Arrow", - Code: 37, - Keysym: int(0xFF51), -} - -var UP_ARROW = types.Key{ - Name: "UP_ARROW", - Value: "Up Arrow", - Code: 38, - Keysym: int(0xFF52), -} - -var RIGHT_ARROW = types.Key{ - Name: "RIGHT_ARROW", - Value: "Right Arrow", - Code: 39, - Keysym: int(0xFF53), -} - -var DOWN_ARROW = types.Key{ - Name: "DOWN_ARROW", - Value: "Down Arrow", - Code: 40, - Keysym: int(0xFF54), -} - -var INSERT = types.Key{ - Name: "INSERT", - Value: "Insert", - Code: 45, - Keysym: int(0xFF63), -} - -var DELETE = types.Key{ - Name: "DELETE", - Value: "Delete", - Code: 46, - Keysym: int(0xFFFF), -} - -var KEY_0 = types.Key{ - Name: "KEY_0", - Value: "0", - Code: 48, - Keysym: int(0x0030), -} - -var KEY_1 = types.Key{ - Name: "KEY_1", - Value: "1", - Code: 49, - Keysym: int(0x0031), -} - -var KEY_2 = types.Key{ - Name: "KEY_2", - Value: "2", - Code: 50, - Keysym: int(0x0032), -} - -var KEY_3 = types.Key{ - Name: "KEY_3", - Value: "3", - Code: 51, - Keysym: int(0x0033), -} - -var KEY_4 = types.Key{ - Name: "KEY_4", - Value: "4", - Code: 52, - Keysym: int(0x0034), -} - -var KEY_5 = types.Key{ - Name: "KEY_5", - Value: "5", - Code: 53, - Keysym: int(0x0035), -} - -var KEY_6 = types.Key{ - Name: "KEY_6", - Value: "6", - Code: 54, - Keysym: int(0x0036), -} - -var KEY_7 = types.Key{ - Name: "KEY_7", - Value: "7", - Code: 55, - Keysym: int(0x0037), -} - -var KEY_8 = types.Key{ - Name: "KEY_8", - Value: "8", - Code: 56, - Keysym: int(0x0038), -} - -var KEY_9 = types.Key{ - Name: "KEY_9", - Value: "9", - Code: 57, - Keysym: int(0x0039), -} - -var KEY_A = types.Key{ - Name: "KEY_A", - Value: "a", - Code: 65, - Keysym: int(0x0061), -} - -var KEY_B = types.Key{ - Name: "KEY_B", - Value: "b", - Code: 66, - Keysym: int(0x0062), -} - -var KEY_C = types.Key{ - Name: "KEY_C", - Value: "c", - Code: 67, - Keysym: int(0x0063), -} - -var KEY_D = types.Key{ - Name: "KEY_D", - Value: "d", - Code: 68, - Keysym: int(0x0064), -} - -var KEY_E = types.Key{ - Name: "KEY_E", - Value: "e", - Code: 69, - Keysym: int(0x0065), -} - -var KEY_F = types.Key{ - Name: "KEY_F", - Value: "f", - Code: 70, - Keysym: int(0x0066), -} - -var KEY_G = types.Key{ - Name: "KEY_G", - Value: "g", - Code: 71, - Keysym: int(0x0067), -} - -var KEY_H = types.Key{ - Name: "KEY_H", - Value: "h", - Code: 72, - Keysym: int(0x0068), -} - -var KEY_I = types.Key{ - Name: "KEY_I", - Value: "i", - Code: 73, - Keysym: int(0x0069), -} - -var KEY_J = types.Key{ - Name: "KEY_J", - Value: "j", - Code: 74, - Keysym: int(0x006a), -} - -var KEY_K = types.Key{ - Name: "KEY_K", - Value: "k", - Code: 75, - Keysym: int(0x006b), -} - -var KEY_L = types.Key{ - Name: "KEY_L", - Value: "l", - Code: 76, - Keysym: int(0x006c), -} - -var KEY_M = types.Key{ - Name: "KEY_M", - Value: "m", - Code: 77, - Keysym: int(0x006d), -} - -var KEY_N = types.Key{ - Name: "KEY_N", - Value: "n", - Code: 78, - Keysym: int(0x006e), -} - -var KEY_O = types.Key{ - Name: "KEY_O", - Value: "o", - Code: 79, - Keysym: int(0x006f), -} - -var KEY_P = types.Key{ - Name: "KEY_P", - Value: "p", - Code: 80, - Keysym: int(0x0070), -} - -var KEY_Q = types.Key{ - Name: "KEY_Q", - Value: "q", - Code: 81, - Keysym: int(0x0071), -} - -var KEY_R = types.Key{ - Name: "KEY_R", - Value: "r", - Code: 82, - Keysym: int(0x0072), -} - -var KEY_S = types.Key{ - Name: "KEY_S", - Value: "s", - Code: 83, - Keysym: int(0x0073), -} - -var KEY_T = types.Key{ - Name: "KEY_T", - Value: "t", - Code: 84, - Keysym: int(0x0074), -} - -var KEY_U = types.Key{ - Name: "KEY_U", - Value: "u", - Code: 85, - Keysym: int(0x0075), -} - -var KEY_V = types.Key{ - Name: "KEY_V", - Value: "v", - Code: 86, - Keysym: int(0x0076), -} - -var KEY_W = types.Key{ - Name: "KEY_W", - Value: "w", - Code: 87, - Keysym: int(0x0077), -} - -var KEY_X = types.Key{ - Name: "KEY_X", - Value: "x", - Code: 88, - Keysym: int(0x0078), -} - -var KEY_Y = types.Key{ - Name: "KEY_Y", - Value: "y", - Code: 89, - Keysym: int(0x0079), -} - -var KEY_Z = types.Key{ - Name: "KEY_Z", - Value: "z", - Code: 90, - Keysym: int(0x007a), -} - -var WIN_LEFT = types.Key{ - Name: "WIN_LEFT", - Value: "Win Left", - Code: 91, - Keysym: int(0xFFEB), -} - -var WIN_RIGHT = types.Key{ - Name: "WIN_RIGHT", - Value: "Win Right", - Code: 92, - Keysym: int(0xFF67), -} - -var PAD_0 = types.Key{ - Name: "PAD_0", - Value: "Num Pad 0", - Code: 96, - Keysym: int(0xFFB0), -} - -var PAD_1 = types.Key{ - Name: "PAD_1", - Value: "Num Pad 1", - Code: 97, - Keysym: int(0xFFB1), -} - -var PAD_2 = types.Key{ - Name: "PAD_2", - Value: "Num Pad 2", - Code: 98, - Keysym: int(0xFFB2), -} - -var PAD_3 = types.Key{ - Name: "PAD_3", - Value: "Num Pad 3", - Code: 99, - Keysym: int(0xFFB3), -} - -var PAD_4 = types.Key{ - Name: "PAD_4", - Value: "Num Pad 4", - Code: 100, - Keysym: int(0xFFB4), -} - -var PAD_5 = types.Key{ - Name: "PAD_5", - Value: "Num Pad 5", - Code: 101, - Keysym: int(0xFFB5), -} - -var PAD_6 = types.Key{ - Name: "PAD_6", - Value: "Num Pad 6", - Code: 102, - Keysym: int(0xFFB6), -} - -var PAD_7 = types.Key{ - Name: "PAD_7", - Value: "Num Pad 7", - Code: 103, - Keysym: int(0xFFB7), -} - -var PAD_8 = types.Key{ - Name: "PAD_8", - Value: "Num Pad 8", - Code: 104, - Keysym: int(0xFFB8), -} - -var PAD_9 = types.Key{ - Name: "PAD_9", - Value: "Num Pad 9", - Code: 105, - Keysym: int(0xFFB9), -} - -var MULTIPLY = types.Key{ - Name: "MULTIPLY", - Value: "*", - Code: 106, - Keysym: int(0xFFAA), -} - -var ADD = types.Key{ - Name: "ADD", - Value: "+", - Code: 107, - Keysym: int(0xFFAB), -} - -var SUBTRACT = types.Key{ - Name: "SUBTRACT", - Value: "-", - Code: 109, - Keysym: int(0xFFAD), -} - -var DECIMAL = types.Key{ - Name: "DECIMAL", - Value: ".", - Code: 110, - Keysym: int(0xFFAE), -} - -var DIVIDE = types.Key{ - Name: "DIVIDE", - Value: "/", - Code: 111, - Keysym: int(0xFFAF), -} - -var KEY_F1 = types.Key{ - Name: "KEY_F1", - Value: "f1", - Code: 112, - Keysym: int(0xFFBE), -} - -var KEY_F2 = types.Key{ - Name: "KEY_F2", - Value: "f2", - Code: 113, - Keysym: int(0xFFBF), -} - -var KEY_F3 = types.Key{ - Name: "KEY_F3", - Value: "f3", - Code: 114, - Keysym: int(0xFFC0), -} - -var KEY_F4 = types.Key{ - Name: "KEY_F4", - Value: "f4", - Code: 115, - Keysym: int(0xFFC1), -} - -var KEY_F5 = types.Key{ - Name: "KEY_F5", - Value: "f5", - Code: 116, - Keysym: int(0xFFC2), -} - -var KEY_F6 = types.Key{ - Name: "KEY_F6", - Value: "f6", - Code: 117, - Keysym: int(0xFFC3), -} - -var KEY_F7 = types.Key{ - Name: "KEY_F7", - Value: "f7", - Code: 118, - Keysym: int(0xFFC4), -} - -var KEY_F8 = types.Key{ - Name: "KEY_F8", - Value: "f8", - Code: 119, - Keysym: int(0xFFC5), -} - -var KEY_F9 = types.Key{ - Name: "KEY_F9", - Value: "f9", - Code: 120, - Keysym: int(0xFFC6), -} - -var KEY_F10 = types.Key{ - Name: "KEY_F10", - Value: "f10", - Code: 121, - Keysym: int(0xFFC7), -} - -var KEY_F11 = types.Key{ - Name: "KEY_F11", - Value: "f11", - Code: 122, - Keysym: int(0xFFC8), -} - -var KEY_F12 = types.Key{ - Name: "KEY_F12", - Value: "f12", - Code: 123, - Keysym: int(0xFFC9), -} - -var NUM_LOCK = types.Key{ - Name: "NUM_LOCK", - Value: "Num Lock", - Code: 144, - Keysym: int(0xFF7F), -} - -var SCROLL_LOCK = types.Key{ - Name: "SCROLL_LOCK", - Value: "Scroll Lock", - Code: 145, - Keysym: int(0xFF14), -} - -var SEMI_COLON = types.Key{ - Name: "SEMI_COLON", - Value: ";", - Code: 186, - Keysym: int(0x003b), -} - -var EQUAL = types.Key{ - Name: "EQUAL", - Value: "=", - Code: 187, - Keysym: int(0x003d), -} - -var COMMA = types.Key{ - Name: "COMMA", - Value: ",", - Code: 188, - Keysym: int(0x002c), -} - -var DASH = types.Key{ - Name: "DASH", - Value: "-", - Code: 189, - Keysym: int(0x002d), -} - -var PERIOD = types.Key{ - Name: "PERIOD", - Value: ".", - Code: 190, - Keysym: int(0x002e), -} - -var FORWARD_SLASH = types.Key{ - Name: "FORWARD_SLASH", - Value: "/", - Code: 191, - Keysym: int(0x002f), -} - -var GRAVE = types.Key{ - Name: "GRAVE", - Value: "`", - Code: 192, - Keysym: int(0x0060), -} - -var OPEN_BRACKET = types.Key{ - Name: "OPEN_BRACKET", - Value: "[", - Code: 219, - Keysym: int(0x005b), -} - -var BACK_SLASH = types.Key{ - Name: "BACK_SLASH", - Value: "\\", - Code: 220, - Keysym: int(0x005c), -} - -var CLOSE_BRAKET = types.Key{ - Name: "CLOSE_BRAKET", - Value: "]", - Code: 221, - Keysym: int(0x005d), -} - -var SINGLE_QUOTE = types.Key{ - Name: "SINGLE_QUOTE", - Value: "'", - Code: 222, - Keysym: int(0x0022), -} diff --git a/server/internal/xorg/xorg.c b/server/internal/xorg/xorg.c index 17733c0..d49c393 100644 --- a/server/internal/xorg/xorg.c +++ b/server/internal/xorg/xorg.c @@ -89,16 +89,30 @@ void XScroll(int x, int y) { } void XButton(unsigned int button, int down) { - Display *display = getXDisplay(); - XTestFakeButtonEvent(display, button, down, CurrentTime); - XSync(display, 0); + if (button != 0) { + Display *display = getXDisplay(); + XTestFakeButtonEvent(display, button, down, CurrentTime); + XSync(display, 0); + } } void XKey(unsigned long key, int down) { - Display *display = getXDisplay(); - KeyCode code = XKeysymToKeycode(display, key); + if (key != 0) { + Display *display = getXDisplay(); + KeyCode code = XKeysymToKeycode(display, key); + + // Map non-existing keysyms to new keycodes + if(code == 0) { + int min, max, numcodes; + XDisplayKeycodes(display, &min, &max); + XGetKeyboardMapping(display, min, max-min, &numcodes); + + code = (max-min+1)*numcodes; + KeySym keysym_list[numcodes]; + for(int i=0;i