From 98e1f3cb8bab953acd997dfbb115276fcdd0b478 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Sat, 14 Jul 2018 01:03:50 +0200 Subject: [PATCH 1/5] Upgrade eslint to latest --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 70ecdabe..f38e8ee8 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "test": "tests" }, "scripts": { - "lint": "eslint app core po tests utils", + "lint": "eslint app core po/po2js tests utils", "test": "karma start karma.conf.js", "prepare": "node ./utils/use_require.js --as commonjs --clean" }, @@ -42,7 +42,7 @@ "chai": "^3.5.0", "commander": "^2.9.0", "es-module-loader": "^2.1.0", - "eslint": "^4.16.0", + "eslint": "^5.1.0", "fs-extra": "^1.0.0", "jsdom": "*", "karma": "^1.3.0", From 14e900c4ae635ea6fd4bfdd0e66b3464124a0994 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Sat, 14 Jul 2018 01:22:14 +0200 Subject: [PATCH 2/5] Fix style according to airbnb guidelines (only auto-fixes) --- .eslintrc | 514 ++++ app/error-handler.js | 92 +- app/localization.js | 276 +- app/ui.js | 2688 ++++++++++---------- app/webutil.js | 348 +-- core/base64.js | 181 +- core/des.js | 357 +-- core/display.js | 1174 ++++----- core/encodings.js | 54 +- core/inflator.js | 58 +- core/input/domkeytable.js | 379 ++- core/input/fixedkeys.js | 188 +- core/input/keyboard.js | 625 ++--- core/input/keysym.js | 1130 ++++----- core/input/keysymdef.js | 1344 +++++----- core/input/mouse.js | 426 ++-- core/input/util.js | 280 +-- core/input/vkeys.js | 208 +- core/input/xtscancodes.js | 326 +-- core/rfb.js | 4561 +++++++++++++++++----------------- core/util/browser.js | 65 +- core/util/cursor.js | 357 ++- core/util/events.js | 184 +- core/util/eventtarget.js | 54 +- core/util/logging.js | 56 +- core/util/polyfill.js | 73 +- core/util/strings.js | 4 +- core/websock.js | 512 ++-- po/.eslintrc | 8 + po/po2js | 18 +- tests/assertions.js | 188 +- tests/fake.websocket.js | 120 +- tests/karma-test-main.js | 14 +- tests/playback-ui.js | 241 +- tests/playback.js | 300 +-- tests/test.base64.js | 42 +- tests/test.display.js | 940 +++---- tests/test.helper.js | 406 +-- tests/test.keyboard.js | 984 ++++---- tests/test.localization.js | 128 +- tests/test.mouse.js | 636 ++--- tests/test.rfb.js | 4359 ++++++++++++++++---------------- tests/test.util.js | 118 +- tests/test.websock.js | 846 +++---- tests/test.webutil.js | 329 ++- utils/genkeysymdef.js | 141 +- utils/use_require.js | 367 +-- utils/use_require_helpers.js | 122 +- 48 files changed, 13782 insertions(+), 13039 deletions(-) create mode 100644 po/.eslintrc diff --git a/.eslintrc b/.eslintrc index b85e51cd..e4d8b7ca 100644 --- a/.eslintrc +++ b/.eslintrc @@ -18,5 +18,519 @@ "arrow-parens": ["error", "as-needed", { "requireForBlockBody": true }], "arrow-spacing": ["error"], "no-confusing-arrow": ["error", { "allowParens": true }], + + // enforce line breaks after opening and before closing array brackets + // https://eslint.org/docs/rules/array-bracket-newline + // TODO: enable? semver-major + "array-bracket-newline": ["off", "consistent"], // object option alternative: { multiline: true, minItems: 3 } + + // enforce line breaks between array elements + // https://eslint.org/docs/rules/array-element-newline + // TODO: enable? semver-major + "array-element-newline": ["off", { multiline: true, minItems: 3 }], + + // enforce spacing inside array brackets + "array-bracket-spacing": ["error", "never"], + + // enforce spacing inside single-line blocks + // https://eslint.org/docs/rules/block-spacing + "block-spacing": ["error", "always"], + + // enforce one true brace style + "brace-style": ["error", "1tbs", { allowSingleLine: true }], + + // require camel case names + // TODO: semver-major (eslint 5): add ignoreDestructuring: false option + // camelcase: ["error", { properties: "never" }], + + // enforce or disallow capitalization of the first letter of a comment + // https://eslint.org/docs/rules/capitalized-comments + "capitalized-comments": ["off", "never", { + line: { + ignorePattern: ".*", + ignoreInlineComments: true, + ignoreConsecutiveComments: true, + }, + block: { + ignorePattern: ".*", + ignoreInlineComments: true, + ignoreConsecutiveComments: true, + }, + }], + + // require trailing commas in multiline object literals + // "comma-dangle": ["error", { + // arrays: "always-multiline", + // objects: "always-multiline", + // imports: "always-multiline", + // exports: "always-multiline", + // functions: "always-multiline", + // }], + + // enforce spacing before and after comma + "comma-spacing": ["error", { before: false, after: true }], + + // enforce one true comma style + "comma-style": ["error", "last", { + exceptions: { + ArrayExpression: false, + ArrayPattern: false, + ArrowFunctionExpression: false, + CallExpression: false, + FunctionDeclaration: false, + FunctionExpression: false, + ImportDeclaration: false, + ObjectExpression: false, + ObjectPattern: false, + VariableDeclaration: false, + NewExpression: false, + } + }], + + // disallow padding inside computed properties + "computed-property-spacing": ["error", "never"], + + // enforces consistent naming when capturing the current execution context + "consistent-this": "off", + + // enforce newline at the end of file, with no multiple empty lines + "eol-last": ["error", "always"], + + // enforce spacing between functions and their invocations + // https://eslint.org/docs/rules/func-call-spacing + "func-call-spacing": ["error", "never"], + + // requires function names to match the name of the variable or property to which they are + // assigned + // https://eslint.org/docs/rules/func-name-matching + // TODO: semver-major (eslint 5): add considerPropertyDescriptor: true + "func-name-matching": ["off", "always", { + includeCommonJSModuleExports: false + }], + + // require function expressions to have a name + // https://eslint.org/docs/rules/func-names + // "func-names": "warn", + + // enforces use of function declarations or expressions + // https://eslint.org/docs/rules/func-style + // TODO: enable + "func-style": ["off", "expression"], + + // enforce consistent line breaks inside function parentheses + // https://eslint.org/docs/rules/function-paren-newline + "function-paren-newline": ["error", "consistent"], + + // Blacklist certain identifiers to prevent them being used + // https://eslint.org/docs/rules/id-blacklist + "id-blacklist": "off", + + // this option enforces minimum and maximum identifier lengths + // (variable names, property names etc.) + "id-length": "off", + + // require identifiers to match the provided regular expression + "id-match": "off", + + // Enforce the location of arrow function bodies with implicit returns + // https://eslint.org/docs/rules/implicit-arrow-linebreak + "implicit-arrow-linebreak": ["error", "beside"], + + // this option sets a specific tab width for your code + // https://eslint.org/docs/rules/indent + indent: ["error", 2, { + SwitchCase: 1, + VariableDeclarator: 1, + outerIIFEBody: 1, + // MemberExpression: null, + FunctionDeclaration: { + parameters: 1, + body: 1 + }, + FunctionExpression: { + parameters: 1, + body: 1 + }, + CallExpression: { + arguments: 1 + }, + ArrayExpression: 1, + ObjectExpression: 1, + ImportDeclaration: 1, + flatTernaryExpressions: false, + // list derived from https://github.com/benjamn/ast-types/blob/HEAD/def/jsx.js + ignoredNodes: ["JSXElement", "JSXElement > *", "JSXAttribute", "JSXIdentifier", "JSXNamespacedName", "JSXMemberExpression", "JSXSpreadAttribute", "JSXExpressionContainer", "JSXOpeningElement", "JSXClosingElement", "JSXText", "JSXEmptyExpression", "JSXSpreadChild"], + ignoreComments: false + }], + + // specify whether double or single quotes should be used in JSX attributes + // https://eslint.org/docs/rules/jsx-quotes + "jsx-quotes": ["off", "prefer-double"], + + // enforces spacing between keys and values in object literal properties + "key-spacing": ["error", { beforeColon: false, afterColon: true }], + + // require a space before & after certain keywords + "keyword-spacing": ["error", { + before: true, + after: true, + overrides: { + return: { after: true }, + throw: { after: true }, + case: { after: true } + } + }], + + // enforce position of line comments + // https://eslint.org/docs/rules/line-comment-position + // TODO: enable? + "line-comment-position": ["off", { + position: "above", + ignorePattern: "", + applyDefaultPatterns: true, + }], + + // disallow mixed "LF" and "CRLF" as linebreaks + // https://eslint.org/docs/rules/linebreak-style + "linebreak-style": ["error", "unix"], + + // require or disallow an empty line between class members + // https://eslint.org/docs/rules/lines-between-class-members + "lines-between-class-members": ["error", "always", { exceptAfterSingleLine: false }], + + // enforces empty lines around comments + "lines-around-comment": "off", + + // require or disallow newlines around directives + // https://eslint.org/docs/rules/lines-around-directive + // "lines-around-directive": ["error", { + // before: "always", + // after: "always", + // }], + + // specify the maximum depth that blocks can be nested + "max-depth": ["off", 4], + + // specify the maximum length of a line in your program + // https://eslint.org/docs/rules/max-len + // "max-len": ["error", 100, 2, { + // ignoreUrls: true, + // ignoreComments: false, + // ignoreRegExpLiterals: true, + // ignoreStrings: true, + // ignoreTemplateLiterals: true, + // }], + + // specify the max number of lines in a file + // https://eslint.org/docs/rules/max-lines + "max-lines": ["off", { + max: 300, + skipBlankLines: true, + skipComments: true + }], + + // enforce a maximum function length + // https://eslint.org/docs/rules/max-lines-per-function + "max-lines-per-function": ["off", { + max: 50, + skipBlankLines: true, + skipComments: true, + IIFEs: true, + }], + + // specify the maximum depth callbacks can be nested + "max-nested-callbacks": "off", + + // limits the number of parameters that can be used in the function declaration. + "max-params": ["off", 3], + + // specify the maximum number of statement allowed in a function + "max-statements": ["off", 10], + + // restrict the number of statements per line + // https://eslint.org/docs/rules/max-statements-per-line + "max-statements-per-line": ["off", { max: 1 }], + + // enforce a particular style for multiline comments + // https://eslint.org/docs/rules/multiline-comment-style + "multiline-comment-style": ["off", "starred-block"], + + // require multiline ternary + // https://eslint.org/docs/rules/multiline-ternary + // TODO: enable? + "multiline-ternary": ["off", "never"], + + // require a capital letter for constructors + "new-cap": ["error", { + newIsCap: true, + newIsCapExceptions: [], + capIsNew: false, + capIsNewExceptions: ["Immutable.Map", "Immutable.Set", "Immutable.List"], + }], + + // disallow the omission of parentheses when invoking a constructor with no arguments + // https://eslint.org/docs/rules/new-parens + "new-parens": "error", + + // allow/disallow an empty newline after var statement + "newline-after-var": "off", + + // https://eslint.org/docs/rules/newline-before-return + "newline-before-return": "off", + + // enforces new line after each method call in the chain to make it + // more readable and easy to maintain + // https://eslint.org/docs/rules/newline-per-chained-call + "newline-per-chained-call": ["error", { ignoreChainWithDepth: 4 }], + + // disallow use of the Array constructor + "no-array-constructor": "error", + + // disallow use of bitwise operators + // https://eslint.org/docs/rules/no-bitwise + // "no-bitwise": "error", + + // disallow use of the continue statement + // https://eslint.org/docs/rules/no-continue + // "no-continue": "error", + + // disallow comments inline after code + "no-inline-comments": "off", + + // disallow if as the only statement in an else block + // https://eslint.org/docs/rules/no-lonely-if + "no-lonely-if": "error", + + // disallow un-paren"d mixes of different operators + // https://eslint.org/docs/rules/no-mixed-operators + // "no-mixed-operators": ["error", { + // // the list of arthmetic groups disallows mixing `%` and `**` + // // with other arithmetic operators. + // groups: [ + // ["%", "**"], + // ["%", "+"], + // ["%", "-"], + // ["%", "*"], + // ["%", "/"], + // ["**", "+"], + // ["**", "-"], + // ["**", "*"], + // ["**", "/"], + // ["&", "|", "^", "~", "<<", ">>", ">>>"], + // ["==", "!=", "===", "!==", ">", ">=", "<", "<="], + // ["&&", "||"], + // ["in", "instanceof"] + // ], + // allowSamePrecedence: false + // }], + + // disallow mixed spaces and tabs for indentation + "no-mixed-spaces-and-tabs": "error", + + // disallow use of chained assignment expressions + // https://eslint.org/docs/rules/no-multi-assign + // "no-multi-assign": ["error"], + + // disallow multiple empty lines and only one newline at the end + "no-multiple-empty-lines": ["error", { max: 2, maxEOF: 0 }], + + // disallow negated conditions + // https://eslint.org/docs/rules/no-negated-condition + "no-negated-condition": "off", + + // disallow nested ternary expressions + // "no-nested-ternary": "error", + + // disallow use of the Object constructor + "no-new-object": "error", + + // disallow use of unary operators, ++ and -- + // https://eslint.org/docs/rules/no-plusplus + // "no-plusplus": "error", + + // disallow certain syntax forms + // https://eslint.org/docs/rules/no-restricted-syntax + "no-restricted-syntax": [ + "error", + // { + // selector: "ForInStatement", + // message: "for..in loops iterate over the entire prototype chain, which is virtually never what you want. Use Object.{keys,values,entries}, and iterate over the resulting array.", + // }, + { + selector: "ForOfStatement", + message: "iterators/generators require regenerator-runtime, which is too heavyweight for this guide to allow them. Separately, loops should be avoided in favor of array iterations.", + }, + { + selector: "LabeledStatement", + message: "Labels are a form of GOTO; using them makes code confusing and hard to maintain and understand.", + }, + { + selector: "WithStatement", + message: "`with` is disallowed in strict mode because it makes code impossible to predict and optimize.", + }, + ], + + // disallow space between function identifier and application + "no-spaced-func": "error", + + // disallow tab characters entirely + "no-tabs": "error", + + // disallow the use of ternary operators + "no-ternary": "off", + + // disallow trailing whitespace at the end of lines + "no-trailing-spaces": ["error", { + skipBlankLines: false, + ignoreComments: false, + }], + + // disallow dangling underscores in identifiers + // "no-underscore-dangle": ["error", { + // allow: [], + // allowAfterThis: true, + // allowAfterSuper: false, + // enforceInMethodNames: false, + // }], + + // disallow the use of Boolean literals in conditional expressions + // also, prefer `a || b` over `a ? a : b` + // https://eslint.org/docs/rules/no-unneeded-ternary + "no-unneeded-ternary": ["error", { defaultAssignment: false }], + + // disallow whitespace before properties + // https://eslint.org/docs/rules/no-whitespace-before-property + "no-whitespace-before-property": "error", + + // enforce the location of single-line statements + // https://eslint.org/docs/rules/nonblock-statement-body-position + "nonblock-statement-body-position": ["error", "beside", { overrides: {} }], + + // require padding inside curly braces + "object-curly-spacing": ["error", "always"], + + // enforce line breaks between braces + // https://eslint.org/docs/rules/object-curly-newline + "object-curly-newline": ["error", { + ObjectExpression: { minProperties: 4, multiline: true, consistent: true }, + ObjectPattern: { minProperties: 4, multiline: true, consistent: true }, + ImportDeclaration: { minProperties: 4, multiline: true, consistent: true }, + ExportDeclaration: { minProperties: 4, multiline: true, consistent: true }, + }], + + // enforce "same line" or "multiple line" on object properties. + // https://eslint.org/docs/rules/object-property-newline + "object-property-newline": ["error", { + allowAllPropertiesOnSameLine: true, + }], + + // allow just one var statement per function + "one-var": ["error", "never"], + + // require a newline around variable declaration + // https://eslint.org/docs/rules/one-var-declaration-per-line + "one-var-declaration-per-line": ["error", "always"], + + // require assignment operator shorthand where possible or prohibit it entirely + // https://eslint.org/docs/rules/operator-assignment + "operator-assignment": ["error", "always"], + + // Requires operator at the beginning of the line in multiline statements + // https://eslint.org/docs/rules/operator-linebreak + "operator-linebreak": ["error", "before", { overrides: { "=": "none" } }], + + // disallow padding within blocks + "padded-blocks": ["error", { blocks: "never", classes: "never", switches: "never" }], + + // Require or disallow padding lines between statements + // https://eslint.org/docs/rules/padding-line-between-statements + "padding-line-between-statements": "off", + + // Prefer use of an object spread over Object.assign + // https://eslint.org/docs/rules/prefer-object-spread + // TODO: semver-major (eslint 5): enable + "prefer-object-spread": "off", + + // require quotes around object literal property names + // https://eslint.org/docs/rules/quote-props.html + "quote-props": ["error", "as-needed", { keywords: false, unnecessary: true, numbers: false }], + + // specify whether double or single quotes should be used + quotes: ["error", "single", { avoidEscape: true }], + + // do not require jsdoc + // https://eslint.org/docs/rules/require-jsdoc + "require-jsdoc": "off", + + // require or disallow use of semicolons instead of ASI + semi: ["error", "always"], + + // enforce spacing before and after semicolons + "semi-spacing": ["error", { before: false, after: true }], + + // Enforce location of semicolons + // https://eslint.org/docs/rules/semi-style + "semi-style": ["error", "last"], + + // requires object keys to be sorted + "sort-keys": ["off", "asc", { caseSensitive: false, natural: true }], + + // sort variables within the same declaration block + "sort-vars": "off", + + // require or disallow space before blocks + "space-before-blocks": "error", + + // require or disallow space before function opening parenthesis + // https://eslint.org/docs/rules/space-before-function-paren + "space-before-function-paren": ["error", { + anonymous: "always", + named: "never", + asyncArrow: "always" + }], + + // require or disallow spaces inside parentheses + "space-in-parens": ["error", "never"], + + // require spaces around operators + "space-infix-ops": "error", + + // Require or disallow spaces before/after unary operators + // https://eslint.org/docs/rules/space-unary-ops + "space-unary-ops": ["error", { + words: true, + nonwords: false, + overrides: { + }, + }], + + // require or disallow a space immediately following the // or /* in a comment + // https://eslint.org/docs/rules/spaced-comment + "spaced-comment": ["error", "always", { + line: { + exceptions: ["-", "+"], + markers: ["=", "!"], // space here to support sprockets directives + }, + block: { + exceptions: ["-", "+"], + markers: ["=", "!"], // space here to support sprockets directives + balanced: true, + } + }], + + // Enforce spacing around colons of switch statements + // https://eslint.org/docs/rules/switch-colon-spacing + "switch-colon-spacing": ["error", { after: true, before: false }], + + // Require or disallow spacing between template tags and their literals + // https://eslint.org/docs/rules/template-tag-spacing + "template-tag-spacing": ["error", "never"], + + // require or disallow the Unicode Byte Order Mark + // https://eslint.org/docs/rules/unicode-bom + "unicode-bom": ["error", "never"], + + // require regex literals to be wrapped in parentheses + "wrap-regex": "off" } } diff --git a/app/error-handler.js b/app/error-handler.js index 96dc9261..e6dab81a 100644 --- a/app/error-handler.js +++ b/app/error-handler.js @@ -5,54 +5,54 @@ // No ES6 can be used in this file since it's used for the translation /* eslint-disable prefer-arrow-callback */ -(function() { - "use strict"; +(function () { + 'use strict'; - // Fallback for all uncought errors - function handleError (event, err) { - try { - const msg = document.getElementById('noVNC_fallback_errormsg'); + // Fallback for all uncought errors + function handleError(event, err) { + try { + const msg = document.getElementById('noVNC_fallback_errormsg'); - // Only show the initial error - if (msg.hasChildNodes()) { - return false; - } - - let div = document.createElement("div"); - div.classList.add('noVNC_message'); - div.appendChild(document.createTextNode(event.message)); - msg.appendChild(div); - - if (event.filename) { - div = document.createElement("div"); - div.className = 'noVNC_location'; - let text = event.filename; - if (event.lineno !== undefined) { - text += ":" + event.lineno; - if (event.colno !== undefined) { - text += ":" + event.colno; - } - } - div.appendChild(document.createTextNode(text)); - msg.appendChild(div); - } - - if (err && err.stack) { - div = document.createElement("div"); - div.className = 'noVNC_stack'; - div.appendChild(document.createTextNode(err.stack)); - msg.appendChild(div); - } - - document.getElementById('noVNC_fallback_error') - .classList.add("noVNC_open"); - } catch (exc) { - document.write("noVNC encountered an error."); - } - // Don't return true since this would prevent the error - // from being printed to the browser console. + // Only show the initial error + if (msg.hasChildNodes()) { return false; + } + + let div = document.createElement('div'); + div.classList.add('noVNC_message'); + div.appendChild(document.createTextNode(event.message)); + msg.appendChild(div); + + if (event.filename) { + div = document.createElement('div'); + div.className = 'noVNC_location'; + let text = event.filename; + if (event.lineno !== undefined) { + text += ':' + event.lineno; + if (event.colno !== undefined) { + text += ':' + event.colno; + } + } + div.appendChild(document.createTextNode(text)); + msg.appendChild(div); + } + + if (err && err.stack) { + div = document.createElement('div'); + div.className = 'noVNC_stack'; + div.appendChild(document.createTextNode(err.stack)); + msg.appendChild(div); + } + + document.getElementById('noVNC_fallback_error') + .classList.add('noVNC_open'); + } catch (exc) { + document.write('noVNC encountered an error.'); } - window.addEventListener('error', function (evt) { handleError(evt, evt.error); }); - window.addEventListener('unhandledrejection', function (evt) { handleError(evt.reason, evt.reason); }); + // Don't return true since this would prevent the error + // from being printed to the browser console. + return false; + } + window.addEventListener('error', function (evt) { handleError(evt, evt.error); }); + window.addEventListener('unhandledrejection', function (evt) { handleError(evt.reason, evt.reason); }); })(); diff --git a/app/localization.js b/app/localization.js index 6bc8d55f..0ca2cf0d 100644 --- a/app/localization.js +++ b/app/localization.js @@ -11,157 +11,153 @@ */ export class Localizer { - constructor() { - // Currently configured language - this.language = 'en'; + constructor() { + // Currently configured language + this.language = 'en'; - // Current dictionary of translations - this.dictionary = undefined; - } + // Current dictionary of translations + this.dictionary = undefined; + } - // Configure suitable language based on user preferences - setup(supportedLanguages) { - this.language = 'en'; // Default: US English + // Configure suitable language based on user preferences + setup(supportedLanguages) { + this.language = 'en'; // Default: US English - /* + /* * Navigator.languages only available in Chrome (32+) and FireFox (32+) * Fall back to navigator.language for other browsers */ - let userLanguages; - if (typeof window.navigator.languages == 'object') { - userLanguages = window.navigator.languages; - } else { - userLanguages = [navigator.language || navigator.userLanguage]; - } - - for (let i = 0;i < userLanguages.length;i++) { - const userLang = userLanguages[i] - .toLowerCase() - .replace("_", "-") - .split("-"); - - // Built-in default? - if ((userLang[0] === 'en') && - ((userLang[1] === undefined) || (userLang[1] === 'us'))) { - return; - } - - // First pass: perfect match - for (let j = 0; j < supportedLanguages.length; j++) { - const supLang = supportedLanguages[j] - .toLowerCase() - .replace("_", "-") - .split("-"); - - if (userLang[0] !== supLang[0]) - continue; - if (userLang[1] !== supLang[1]) - continue; - - this.language = supportedLanguages[j]; - return; - } - - // Second pass: fallback - for (let j = 0;j < supportedLanguages.length;j++) { - const supLang = supportedLanguages[j] - .toLowerCase() - .replace("_", "-") - .split("-"); - - if (userLang[0] !== supLang[0]) - continue; - if (supLang[1] !== undefined) - continue; - - this.language = supportedLanguages[j]; - return; - } - } + let userLanguages; + if (typeof window.navigator.languages == 'object') { + userLanguages = window.navigator.languages; + } else { + userLanguages = [navigator.language || navigator.userLanguage]; } - // Retrieve localised text - get(id) { - if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { - return this.dictionary[id]; - } else { - return id; + for (let i = 0; i < userLanguages.length; i++) { + const userLang = userLanguages[i] + .toLowerCase() + .replace('_', '-') + .split('-'); + + // Built-in default? + if ((userLang[0] === 'en') + && ((userLang[1] === undefined) || (userLang[1] === 'us'))) { + return; + } + + // First pass: perfect match + for (let j = 0; j < supportedLanguages.length; j++) { + const supLang = supportedLanguages[j] + .toLowerCase() + .replace('_', '-') + .split('-'); + + if (userLang[0] !== supLang[0]) continue; + if (userLang[1] !== supLang[1]) continue; + + this.language = supportedLanguages[j]; + return; + } + + // Second pass: fallback + for (let j = 0; j < supportedLanguages.length; j++) { + const supLang = supportedLanguages[j] + .toLowerCase() + .replace('_', '-') + .split('-'); + + if (userLang[0] !== supLang[0]) continue; + if (supLang[1] !== undefined) continue; + + this.language = supportedLanguages[j]; + return; + } + } + } + + // Retrieve localised text + get(id) { + if (typeof this.dictionary !== 'undefined' && this.dictionary[id]) { + return this.dictionary[id]; + } else { + return id; + } + } + + // Traverses the DOM and translates relevant fields + // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate + translateDOM() { + const self = this; + + function process(elem, enabled) { + function isAnyOf(searchElement, items) { + return items.indexOf(searchElement) !== -1; + } + + function translateAttribute(elem, attr) { + const str = self.get(elem.getAttribute(attr)); + elem.setAttribute(attr, str); + } + + function translateTextNode(node) { + const str = self.get(node.data.trim()); + node.data = str; + } + + if (elem.hasAttribute('translate')) { + if (isAnyOf(elem.getAttribute('translate'), ['', 'yes'])) { + enabled = true; + } else if (isAnyOf(elem.getAttribute('translate'), ['no'])) { + enabled = false; } + } + + if (enabled) { + if (elem.hasAttribute('abbr') + && elem.tagName === 'TH') { + translateAttribute(elem, 'abbr'); + } + if (elem.hasAttribute('alt') + && isAnyOf(elem.tagName, ['AREA', 'IMG', 'INPUT'])) { + translateAttribute(elem, 'alt'); + } + if (elem.hasAttribute('download') + && isAnyOf(elem.tagName, ['A', 'AREA'])) { + translateAttribute(elem, 'download'); + } + if (elem.hasAttribute('label') + && isAnyOf(elem.tagName, ['MENUITEM', 'MENU', 'OPTGROUP', + 'OPTION', 'TRACK'])) { + translateAttribute(elem, 'label'); + } + // FIXME: Should update "lang" + if (elem.hasAttribute('placeholder') + && isAnyOf(elem.tagName, ['INPUT', 'TEXTAREA'])) { + translateAttribute(elem, 'placeholder'); + } + if (elem.hasAttribute('title')) { + translateAttribute(elem, 'title'); + } + if (elem.hasAttribute('value') + && elem.tagName === 'INPUT' + && isAnyOf(elem.getAttribute('type'), ['reset', 'button', 'submit'])) { + translateAttribute(elem, 'value'); + } + } + + for (let i = 0; i < elem.childNodes.length; i++) { + const node = elem.childNodes[i]; + if (node.nodeType === node.ELEMENT_NODE) { + process(node, enabled); + } else if (node.nodeType === node.TEXT_NODE && enabled) { + translateTextNode(node); + } + } } - // Traverses the DOM and translates relevant fields - // See https://html.spec.whatwg.org/multipage/dom.html#attr-translate - translateDOM() { - const self = this; - - function process(elem, enabled) { - function isAnyOf(searchElement, items) { - return items.indexOf(searchElement) !== -1; - } - - function translateAttribute(elem, attr) { - const str = self.get(elem.getAttribute(attr)); - elem.setAttribute(attr, str); - } - - function translateTextNode(node) { - const str = self.get(node.data.trim()); - node.data = str; - } - - if (elem.hasAttribute("translate")) { - if (isAnyOf(elem.getAttribute("translate"), ["", "yes"])) { - enabled = true; - } else if (isAnyOf(elem.getAttribute("translate"), ["no"])) { - enabled = false; - } - } - - if (enabled) { - if (elem.hasAttribute("abbr") && - elem.tagName === "TH") { - translateAttribute(elem, "abbr"); - } - if (elem.hasAttribute("alt") && - isAnyOf(elem.tagName, ["AREA", "IMG", "INPUT"])) { - translateAttribute(elem, "alt"); - } - if (elem.hasAttribute("download") && - isAnyOf(elem.tagName, ["A", "AREA"])) { - translateAttribute(elem, "download"); - } - if (elem.hasAttribute("label") && - isAnyOf(elem.tagName, ["MENUITEM", "MENU", "OPTGROUP", - "OPTION", "TRACK"])) { - translateAttribute(elem, "label"); - } - // FIXME: Should update "lang" - if (elem.hasAttribute("placeholder") && - isAnyOf(elem.tagName, ["INPUT", "TEXTAREA"])) { - translateAttribute(elem, "placeholder"); - } - if (elem.hasAttribute("title")) { - translateAttribute(elem, "title"); - } - if (elem.hasAttribute("value") && - elem.tagName === "INPUT" && - isAnyOf(elem.getAttribute("type"), ["reset", "button", "submit"])) { - translateAttribute(elem, "value"); - } - } - - for (let i = 0; i < elem.childNodes.length; i++) { - const node = elem.childNodes[i]; - if (node.nodeType === node.ELEMENT_NODE) { - process(node, enabled); - } else if (node.nodeType === node.TEXT_NODE && enabled) { - translateTextNode(node); - } - } - } - - process(document.body, true); - } + process(document.body, true); + } } export const l10n = new Localizer(); diff --git a/app/ui.js b/app/ui.js index 4fe2a3fb..ca8f7c81 100644 --- a/app/ui.js +++ b/app/ui.js @@ -12,1636 +12,1628 @@ import * as Log from '../core/util/logging.js'; import _, { l10n } from './localization.js'; import { isTouchDevice } from '../core/util/browser.js'; import { setCapture, getPointerEvent } from '../core/util/events.js'; -import KeyTable from "../core/input/keysym.js"; -import keysyms from "../core/input/keysymdef.js"; -import Keyboard from "../core/input/keyboard.js"; -import RFB from "../core/rfb.js"; -import * as WebUtil from "./webutil.js"; +import KeyTable from '../core/input/keysym.js'; +import keysyms from '../core/input/keysymdef.js'; +import Keyboard from '../core/input/keyboard.js'; +import RFB from '../core/rfb.js'; +import * as WebUtil from './webutil.js'; const UI = { - connected: false, - desktopName: "", + connected: false, + desktopName: '', - statusTimeout: null, - hideKeyboardTimeout: null, - idleControlbarTimeout: null, - closeControlbarTimeout: null, + statusTimeout: null, + hideKeyboardTimeout: null, + idleControlbarTimeout: null, + closeControlbarTimeout: null, - controlbarGrabbed: false, - controlbarDrag: false, - controlbarMouseDownClientY: 0, - controlbarMouseDownOffsetY: 0, + controlbarGrabbed: false, + controlbarDrag: false, + controlbarMouseDownClientY: 0, + controlbarMouseDownOffsetY: 0, - isSafari: false, - lastKeyboardinput: null, - defaultKeyboardinputLen: 100, + isSafari: false, + lastKeyboardinput: null, + defaultKeyboardinputLen: 100, - inhibit_reconnect: true, - reconnect_callback: null, - reconnect_password: null, + inhibit_reconnect: true, + reconnect_callback: null, + reconnect_password: null, - prime(callback) { - if (document.readyState === "interactive" || document.readyState === "complete") { - UI.load(callback); - } else { - document.addEventListener('DOMContentLoaded', UI.load.bind(UI, callback)); + prime(callback) { + if (document.readyState === 'interactive' || document.readyState === 'complete') { + UI.load(callback); + } else { + document.addEventListener('DOMContentLoaded', UI.load.bind(UI, callback)); + } + }, + + // Setup rfb object, load settings from browser storage, then call + // UI.init to setup the UI/menus + load(callback) { + WebUtil.initSettings(UI.start, callback); + }, + + // Render default UI and initialize settings menu + start(callback) { + // Setup global variables first + UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 + && navigator.userAgent.indexOf('Chrome') === -1); + + UI.initSettings(); + + // Translate the DOM + l10n.translateDOM(); + + // Adapt the interface for touch screen devices + if (isTouchDevice) { + document.documentElement.classList.add('noVNC_touch'); + // Remove the address bar + setTimeout(() => window.scrollTo(0, 1), 100); + } + + // Restore control bar position + if (WebUtil.readSetting('controlbar_pos') === 'right') { + UI.toggleControlbarSide(); + } + + UI.initFullscreen(); + + // Setup event handlers + UI.addControlbarHandlers(); + UI.addTouchSpecificHandlers(); + UI.addExtraKeysHandlers(); + UI.addMachineHandlers(); + UI.addConnectionControlHandlers(); + UI.addClipboardHandlers(); + UI.addSettingsHandlers(); + document.getElementById('noVNC_status') + .addEventListener('click', UI.hideStatus); + + // Bootstrap fallback input handler + UI.keyboardinputReset(); + + UI.openControlbar(); + + UI.updateVisualState('init'); + + document.documentElement.classList.remove('noVNC_loading'); + + let autoconnect = WebUtil.getConfigVar('autoconnect', false); + if (autoconnect === 'true' || autoconnect == '1') { + autoconnect = true; + UI.connect(); + } else { + autoconnect = false; + // Show the connect panel on first load unless autoconnecting + UI.openConnectPanel(); + } + + if (typeof callback === 'function') { + callback(UI.rfb); + } + }, + + initFullscreen() { + // Only show the button if fullscreen is properly supported + // * Safari doesn't support alphanumerical input while in fullscreen + if (!UI.isSafari + && (document.documentElement.requestFullscreen + || document.documentElement.mozRequestFullScreen + || document.documentElement.webkitRequestFullscreen + || document.body.msRequestFullscreen)) { + document.getElementById('noVNC_fullscreen_button') + .classList.remove('noVNC_hidden'); + UI.addFullscreenHandlers(); + } + }, + + initSettings() { + // Logging selection dropdown + const llevels = ['error', 'warn', 'info', 'debug']; + for (let i = 0; i < llevels.length; i += 1) { + UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]); + } + + // Settings with immediate effects + UI.initSetting('logging', 'warn'); + UI.updateLogging(); + + // if port == 80 (or 443) then it won't be present and should be + // set manually + let port = window.location.port; + if (!port) { + if (window.location.protocol.substring(0, 5) == 'https') { + port = 443; + } else if (window.location.protocol.substring(0, 4) == 'http') { + port = 80; + } + } + + /* Populate the controls if defaults are provided in the URL */ + UI.initSetting('host', window.location.hostname); + UI.initSetting('port', port); + UI.initSetting('encrypt', (window.location.protocol === 'https:')); + UI.initSetting('view_clip', false); + UI.initSetting('resize', 'off'); + UI.initSetting('shared', true); + UI.initSetting('view_only', false); + UI.initSetting('path', 'websockify'); + UI.initSetting('repeaterID', ''); + UI.initSetting('reconnect', false); + UI.initSetting('reconnect_delay', 5000); + + UI.setupSettingLabels(); + }, + // Adds a link to the label elements on the corresponding input elements + setupSettingLabels() { + const labels = document.getElementsByTagName('LABEL'); + for (let i = 0; i < labels.length; i++) { + const htmlFor = labels[i].htmlFor; + if (htmlFor != '') { + const elem = document.getElementById(htmlFor); + if (elem) elem.label = labels[i]; + } else { + // If 'for' isn't set, use the first input element child + const children = labels[i].children; + for (let j = 0; j < children.length; j++) { + if (children[j].form !== undefined) { + children[j].label = labels[i]; + break; + } } - }, + } + } + }, - // Setup rfb object, load settings from browser storage, then call - // UI.init to setup the UI/menus - load(callback) { - WebUtil.initSettings(UI.start, callback); - }, - - // Render default UI and initialize settings menu - start(callback) { - - // Setup global variables first - UI.isSafari = (navigator.userAgent.indexOf('Safari') !== -1 && - navigator.userAgent.indexOf('Chrome') === -1); - - UI.initSettings(); - - // Translate the DOM - l10n.translateDOM(); - - // Adapt the interface for touch screen devices - if (isTouchDevice) { - document.documentElement.classList.add("noVNC_touch"); - // Remove the address bar - setTimeout(() => window.scrollTo(0, 1), 100); - } - - // Restore control bar position - if (WebUtil.readSetting('controlbar_pos') === 'right') { - UI.toggleControlbarSide(); - } - - UI.initFullscreen(); - - // Setup event handlers - UI.addControlbarHandlers(); - UI.addTouchSpecificHandlers(); - UI.addExtraKeysHandlers(); - UI.addMachineHandlers(); - UI.addConnectionControlHandlers(); - UI.addClipboardHandlers(); - UI.addSettingsHandlers(); - document.getElementById("noVNC_status") - .addEventListener('click', UI.hideStatus); - - // Bootstrap fallback input handler - UI.keyboardinputReset(); - - UI.openControlbar(); - - UI.updateVisualState('init'); - - document.documentElement.classList.remove("noVNC_loading"); - - let autoconnect = WebUtil.getConfigVar('autoconnect', false); - if (autoconnect === 'true' || autoconnect == '1') { - autoconnect = true; - UI.connect(); - } else { - autoconnect = false; - // Show the connect panel on first load unless autoconnecting - UI.openConnectPanel(); - } - - if (typeof callback === "function") { - callback(UI.rfb); - } - }, - - initFullscreen() { - // Only show the button if fullscreen is properly supported - // * Safari doesn't support alphanumerical input while in fullscreen - if (!UI.isSafari && - (document.documentElement.requestFullscreen || - document.documentElement.mozRequestFullScreen || - document.documentElement.webkitRequestFullscreen || - document.body.msRequestFullscreen)) { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_hidden"); - UI.addFullscreenHandlers(); - } - }, - - initSettings() { - // Logging selection dropdown - const llevels = ['error', 'warn', 'info', 'debug']; - for (let i = 0; i < llevels.length; i += 1) { - UI.addOption(document.getElementById('noVNC_setting_logging'), llevels[i], llevels[i]); - } - - // Settings with immediate effects - UI.initSetting('logging', 'warn'); - UI.updateLogging(); - - // if port == 80 (or 443) then it won't be present and should be - // set manually - let port = window.location.port; - if (!port) { - if (window.location.protocol.substring(0,5) == 'https') { - port = 443; - } - else if (window.location.protocol.substring(0,4) == 'http') { - port = 80; - } - } - - /* Populate the controls if defaults are provided in the URL */ - UI.initSetting('host', window.location.hostname); - UI.initSetting('port', port); - UI.initSetting('encrypt', (window.location.protocol === "https:")); - UI.initSetting('view_clip', false); - UI.initSetting('resize', 'off'); - UI.initSetting('shared', true); - UI.initSetting('view_only', false); - UI.initSetting('path', 'websockify'); - UI.initSetting('repeaterID', ''); - UI.initSetting('reconnect', false); - UI.initSetting('reconnect_delay', 5000); - - UI.setupSettingLabels(); - }, - // Adds a link to the label elements on the corresponding input elements - setupSettingLabels() { - const labels = document.getElementsByTagName('LABEL'); - for (let i = 0; i < labels.length; i++) { - const htmlFor = labels[i].htmlFor; - if (htmlFor != '') { - const elem = document.getElementById(htmlFor); - if (elem) elem.label = labels[i]; - } else { - // If 'for' isn't set, use the first input element child - const children = labels[i].children; - for (let j = 0; j < children.length; j++) { - if (children[j].form !== undefined) { - children[j].label = labels[i]; - break; - } - } - } - } - }, - -/* ------^------- + /* ------^------- * /INIT * ============== * EVENT HANDLERS * ------v------*/ - addControlbarHandlers() { - document.getElementById("noVNC_control_bar") - .addEventListener('mousemove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mouseup', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keydown', UI.activateControlbar); + addControlbarHandlers() { + document.getElementById('noVNC_control_bar') + .addEventListener('mousemove', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('mouseup', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('mousedown', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('keydown', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('mousedown', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('keydown', UI.keepControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('mousedown', UI.keepControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('keydown', UI.keepControlbar); - document.getElementById("noVNC_view_drag_button") - .addEventListener('click', UI.toggleViewDrag); + document.getElementById('noVNC_view_drag_button') + .addEventListener('click', UI.toggleViewDrag); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousedown', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mouseup', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('mousemove', UI.dragControlbarHandle); - // resize events aren't available for elements - window.addEventListener('resize', UI.updateControlbarHandle); + document.getElementById('noVNC_control_bar_handle') + .addEventListener('mousedown', UI.controlbarHandleMouseDown); + document.getElementById('noVNC_control_bar_handle') + .addEventListener('mouseup', UI.controlbarHandleMouseUp); + document.getElementById('noVNC_control_bar_handle') + .addEventListener('mousemove', UI.dragControlbarHandle); + // resize events aren't available for elements + window.addEventListener('resize', UI.updateControlbarHandle); - const exps = document.getElementsByClassName("noVNC_expander"); - for (let i = 0;i < exps.length;i++) { - exps[i].addEventListener('click', UI.toggleExpander); - } - }, + const exps = document.getElementsByClassName('noVNC_expander'); + for (let i = 0; i < exps.length; i++) { + exps[i].addEventListener('click', UI.toggleExpander); + } + }, - addTouchSpecificHandlers() { - document.getElementById("noVNC_mouse_button0") - .addEventListener('click', () => UI.setMouseButton(1)); - document.getElementById("noVNC_mouse_button1") - .addEventListener('click', () => UI.setMouseButton(2)); - document.getElementById("noVNC_mouse_button2") - .addEventListener('click', () => UI.setMouseButton(4)); - document.getElementById("noVNC_mouse_button4") - .addEventListener('click', () => UI.setMouseButton(0)); - document.getElementById("noVNC_keyboard_button") - .addEventListener('click', UI.toggleVirtualKeyboard); + addTouchSpecificHandlers() { + document.getElementById('noVNC_mouse_button0') + .addEventListener('click', () => UI.setMouseButton(1)); + document.getElementById('noVNC_mouse_button1') + .addEventListener('click', () => UI.setMouseButton(2)); + document.getElementById('noVNC_mouse_button2') + .addEventListener('click', () => UI.setMouseButton(4)); + document.getElementById('noVNC_mouse_button4') + .addEventListener('click', () => UI.setMouseButton(0)); + document.getElementById('noVNC_keyboard_button') + .addEventListener('click', UI.toggleVirtualKeyboard); - UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput')); - UI.touchKeyboard.onkeyevent = UI.keyEvent; - UI.touchKeyboard.grab(); - document.getElementById("noVNC_keyboardinput") - .addEventListener('input', UI.keyInput); - document.getElementById("noVNC_keyboardinput") - .addEventListener('focus', UI.onfocusVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('blur', UI.onblurVirtualKeyboard); - document.getElementById("noVNC_keyboardinput") - .addEventListener('submit', () => false); + UI.touchKeyboard = new Keyboard(document.getElementById('noVNC_keyboardinput')); + UI.touchKeyboard.onkeyevent = UI.keyEvent; + UI.touchKeyboard.grab(); + document.getElementById('noVNC_keyboardinput') + .addEventListener('input', UI.keyInput); + document.getElementById('noVNC_keyboardinput') + .addEventListener('focus', UI.onfocusVirtualKeyboard); + document.getElementById('noVNC_keyboardinput') + .addEventListener('blur', UI.onblurVirtualKeyboard); + document.getElementById('noVNC_keyboardinput') + .addEventListener('submit', () => false); - document.documentElement - .addEventListener('mousedown', UI.keepVirtualKeyboard, true); + document.documentElement + .addEventListener('mousedown', UI.keepVirtualKeyboard, true); - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchmove', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchend', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('touchstart', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('touchmove', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('touchend', UI.activateControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('input', UI.activateControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('touchstart', UI.keepControlbar); - document.getElementById("noVNC_control_bar") - .addEventListener('input', UI.keepControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('touchstart', UI.keepControlbar); + document.getElementById('noVNC_control_bar') + .addEventListener('input', UI.keepControlbar); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchstart', UI.controlbarHandleMouseDown); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchend', UI.controlbarHandleMouseUp); - document.getElementById("noVNC_control_bar_handle") - .addEventListener('touchmove', UI.dragControlbarHandle); - }, + document.getElementById('noVNC_control_bar_handle') + .addEventListener('touchstart', UI.controlbarHandleMouseDown); + document.getElementById('noVNC_control_bar_handle') + .addEventListener('touchend', UI.controlbarHandleMouseUp); + document.getElementById('noVNC_control_bar_handle') + .addEventListener('touchmove', UI.dragControlbarHandle); + }, - addExtraKeysHandlers() { - document.getElementById("noVNC_toggle_extra_keys_button") - .addEventListener('click', UI.toggleExtraKeys); - document.getElementById("noVNC_toggle_ctrl_button") - .addEventListener('click', UI.toggleCtrl); - document.getElementById("noVNC_toggle_alt_button") - .addEventListener('click', UI.toggleAlt); - document.getElementById("noVNC_send_tab_button") - .addEventListener('click', UI.sendTab); - document.getElementById("noVNC_send_esc_button") - .addEventListener('click', UI.sendEsc); - document.getElementById("noVNC_send_ctrl_alt_del_button") - .addEventListener('click', UI.sendCtrlAltDel); - }, + addExtraKeysHandlers() { + document.getElementById('noVNC_toggle_extra_keys_button') + .addEventListener('click', UI.toggleExtraKeys); + document.getElementById('noVNC_toggle_ctrl_button') + .addEventListener('click', UI.toggleCtrl); + document.getElementById('noVNC_toggle_alt_button') + .addEventListener('click', UI.toggleAlt); + document.getElementById('noVNC_send_tab_button') + .addEventListener('click', UI.sendTab); + document.getElementById('noVNC_send_esc_button') + .addEventListener('click', UI.sendEsc); + document.getElementById('noVNC_send_ctrl_alt_del_button') + .addEventListener('click', UI.sendCtrlAltDel); + }, - addMachineHandlers() { - document.getElementById("noVNC_shutdown_button") - .addEventListener('click', () => UI.rfb.machineShutdown()); - document.getElementById("noVNC_reboot_button") - .addEventListener('click', () => UI.rfb.machineReboot()); - document.getElementById("noVNC_reset_button") - .addEventListener('click', () => UI.rfb.machineReset()); - document.getElementById("noVNC_power_button") - .addEventListener('click', UI.togglePowerPanel); - }, + addMachineHandlers() { + document.getElementById('noVNC_shutdown_button') + .addEventListener('click', () => UI.rfb.machineShutdown()); + document.getElementById('noVNC_reboot_button') + .addEventListener('click', () => UI.rfb.machineReboot()); + document.getElementById('noVNC_reset_button') + .addEventListener('click', () => UI.rfb.machineReset()); + document.getElementById('noVNC_power_button') + .addEventListener('click', UI.togglePowerPanel); + }, - addConnectionControlHandlers() { - document.getElementById("noVNC_disconnect_button") - .addEventListener('click', UI.disconnect); - document.getElementById("noVNC_connect_button") - .addEventListener('click', UI.connect); - document.getElementById("noVNC_cancel_reconnect_button") - .addEventListener('click', UI.cancelReconnect); + addConnectionControlHandlers() { + document.getElementById('noVNC_disconnect_button') + .addEventListener('click', UI.disconnect); + document.getElementById('noVNC_connect_button') + .addEventListener('click', UI.connect); + document.getElementById('noVNC_cancel_reconnect_button') + .addEventListener('click', UI.cancelReconnect); - document.getElementById("noVNC_password_button") - .addEventListener('click', UI.setPassword); - }, + document.getElementById('noVNC_password_button') + .addEventListener('click', UI.setPassword); + }, - addClipboardHandlers() { - document.getElementById("noVNC_clipboard_button") - .addEventListener('click', UI.toggleClipboardPanel); - document.getElementById("noVNC_clipboard_text") - .addEventListener('change', UI.clipboardSend); - document.getElementById("noVNC_clipboard_clear_button") - .addEventListener('click', UI.clipboardClear); - }, + addClipboardHandlers() { + document.getElementById('noVNC_clipboard_button') + .addEventListener('click', UI.toggleClipboardPanel); + document.getElementById('noVNC_clipboard_text') + .addEventListener('change', UI.clipboardSend); + document.getElementById('noVNC_clipboard_clear_button') + .addEventListener('click', UI.clipboardClear); + }, - // Add a call to save settings when the element changes, - // unless the optional parameter changeFunc is used instead. - addSettingChangeHandler(name, changeFunc) { - const settingElem = document.getElementById("noVNC_setting_" + name); - if (changeFunc === undefined) { - changeFunc = () => UI.saveSetting(name); - } - settingElem.addEventListener('change', changeFunc); - }, + // Add a call to save settings when the element changes, + // unless the optional parameter changeFunc is used instead. + addSettingChangeHandler(name, changeFunc) { + const settingElem = document.getElementById('noVNC_setting_' + name); + if (changeFunc === undefined) { + changeFunc = () => UI.saveSetting(name); + } + settingElem.addEventListener('change', changeFunc); + }, - addSettingsHandlers() { - document.getElementById("noVNC_settings_button") - .addEventListener('click', UI.toggleSettingsPanel); + addSettingsHandlers() { + document.getElementById('noVNC_settings_button') + .addEventListener('click', UI.toggleSettingsPanel); - UI.addSettingChangeHandler('encrypt'); - UI.addSettingChangeHandler('resize'); - UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); - UI.addSettingChangeHandler('resize', UI.applyResizeMode); - UI.addSettingChangeHandler('view_clip'); - UI.addSettingChangeHandler('view_clip', UI.updateViewClip); - UI.addSettingChangeHandler('shared'); - UI.addSettingChangeHandler('view_only'); - UI.addSettingChangeHandler('view_only', UI.updateViewOnly); - UI.addSettingChangeHandler('host'); - UI.addSettingChangeHandler('port'); - UI.addSettingChangeHandler('path'); - UI.addSettingChangeHandler('repeaterID'); - UI.addSettingChangeHandler('logging'); - UI.addSettingChangeHandler('logging', UI.updateLogging); - UI.addSettingChangeHandler('reconnect'); - UI.addSettingChangeHandler('reconnect_delay'); - }, + UI.addSettingChangeHandler('encrypt'); + UI.addSettingChangeHandler('resize'); + UI.addSettingChangeHandler('resize', UI.enableDisableViewClip); + UI.addSettingChangeHandler('resize', UI.applyResizeMode); + UI.addSettingChangeHandler('view_clip'); + UI.addSettingChangeHandler('view_clip', UI.updateViewClip); + UI.addSettingChangeHandler('shared'); + UI.addSettingChangeHandler('view_only'); + UI.addSettingChangeHandler('view_only', UI.updateViewOnly); + UI.addSettingChangeHandler('host'); + UI.addSettingChangeHandler('port'); + UI.addSettingChangeHandler('path'); + UI.addSettingChangeHandler('repeaterID'); + UI.addSettingChangeHandler('logging'); + UI.addSettingChangeHandler('logging', UI.updateLogging); + UI.addSettingChangeHandler('reconnect'); + UI.addSettingChangeHandler('reconnect_delay'); + }, - addFullscreenHandlers() { - document.getElementById("noVNC_fullscreen_button") - .addEventListener('click', UI.toggleFullscreen); + addFullscreenHandlers() { + document.getElementById('noVNC_fullscreen_button') + .addEventListener('click', UI.toggleFullscreen); - window.addEventListener('fullscreenchange', UI.updateFullscreenButton); - window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); - window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); - }, + window.addEventListener('fullscreenchange', UI.updateFullscreenButton); + window.addEventListener('mozfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('webkitfullscreenchange', UI.updateFullscreenButton); + window.addEventListener('msfullscreenchange', UI.updateFullscreenButton); + }, -/* ------^------- + /* ------^------- * /EVENT HANDLERS * ============== * VISUAL * ------v------*/ - // Disable/enable controls depending on connection state - updateVisualState(state) { + // Disable/enable controls depending on connection state + updateVisualState(state) { + document.documentElement.classList.remove('noVNC_connecting'); + document.documentElement.classList.remove('noVNC_connected'); + document.documentElement.classList.remove('noVNC_disconnecting'); + document.documentElement.classList.remove('noVNC_reconnecting'); - document.documentElement.classList.remove("noVNC_connecting"); - document.documentElement.classList.remove("noVNC_connected"); - document.documentElement.classList.remove("noVNC_disconnecting"); - document.documentElement.classList.remove("noVNC_reconnecting"); + const transition_elem = document.getElementById('noVNC_transition_text'); + switch (state) { + case 'init': + break; + case 'connecting': + transition_elem.textContent = _('Connecting...'); + document.documentElement.classList.add('noVNC_connecting'); + break; + case 'connected': + document.documentElement.classList.add('noVNC_connected'); + break; + case 'disconnecting': + transition_elem.textContent = _('Disconnecting...'); + document.documentElement.classList.add('noVNC_disconnecting'); + break; + case 'disconnected': + break; + case 'reconnecting': + transition_elem.textContent = _('Reconnecting...'); + document.documentElement.classList.add('noVNC_reconnecting'); + break; + default: + Log.Error('Invalid visual state: ' + state); + UI.showStatus(_('Internal error'), 'error'); + return; + } - const transition_elem = document.getElementById("noVNC_transition_text"); - switch (state) { - case 'init': - break; - case 'connecting': - transition_elem.textContent = _("Connecting..."); - document.documentElement.classList.add("noVNC_connecting"); - break; - case 'connected': - document.documentElement.classList.add("noVNC_connected"); - break; - case 'disconnecting': - transition_elem.textContent = _("Disconnecting..."); - document.documentElement.classList.add("noVNC_disconnecting"); - break; - case 'disconnected': - break; - case 'reconnecting': - transition_elem.textContent = _("Reconnecting..."); - document.documentElement.classList.add("noVNC_reconnecting"); - break; - default: - Log.Error("Invalid visual state: " + state); - UI.showStatus(_("Internal error"), 'error'); - return; - } + UI.enableDisableViewClip(); - UI.enableDisableViewClip(); + if (UI.connected) { + UI.disableSetting('encrypt'); + UI.disableSetting('shared'); + UI.disableSetting('host'); + UI.disableSetting('port'); + UI.disableSetting('path'); + UI.disableSetting('repeaterID'); + UI.setMouseButton(1); - if (UI.connected) { - UI.disableSetting('encrypt'); - UI.disableSetting('shared'); - UI.disableSetting('host'); - UI.disableSetting('port'); - UI.disableSetting('path'); - UI.disableSetting('repeaterID'); - UI.setMouseButton(1); + // Hide the controlbar after 2 seconds + UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); + } else { + UI.enableSetting('encrypt'); + UI.enableSetting('shared'); + UI.enableSetting('host'); + UI.enableSetting('port'); + UI.enableSetting('path'); + UI.enableSetting('repeaterID'); + UI.updatePowerButton(); + UI.keepControlbar(); + } - // Hide the controlbar after 2 seconds - UI.closeControlbarTimeout = setTimeout(UI.closeControlbar, 2000); - } else { - UI.enableSetting('encrypt'); - UI.enableSetting('shared'); - UI.enableSetting('host'); - UI.enableSetting('port'); - UI.enableSetting('path'); - UI.enableSetting('repeaterID'); - UI.updatePowerButton(); - UI.keepControlbar(); - } + // State change disables viewport dragging. + // It is enabled (toggled) by direct click on the button + UI.setViewDrag(false); - // State change disables viewport dragging. - // It is enabled (toggled) by direct click on the button - UI.setViewDrag(false); + // State change also closes the password dialog + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + }, - // State change also closes the password dialog - document.getElementById('noVNC_password_dlg') - .classList.remove('noVNC_open'); - }, + showStatus(text, status_type, time) { + const statusElem = document.getElementById('noVNC_status'); - showStatus(text, status_type, time) { - const statusElem = document.getElementById('noVNC_status'); + clearTimeout(UI.statusTimeout); - clearTimeout(UI.statusTimeout); + if (typeof status_type === 'undefined') { + status_type = 'normal'; + } - if (typeof status_type === 'undefined') { - status_type = 'normal'; - } + // Don't overwrite more severe visible statuses and never + // errors. Only shows the first error. + let visible_status_type = 'none'; + if (statusElem.classList.contains('noVNC_open')) { + if (statusElem.classList.contains('noVNC_status_error')) { + visible_status_type = 'error'; + } else if (statusElem.classList.contains('noVNC_status_warn')) { + visible_status_type = 'warn'; + } else { + visible_status_type = 'normal'; + } + } + if (visible_status_type === 'error' + || (visible_status_type === 'warn' && status_type === 'normal')) { + return; + } - // Don't overwrite more severe visible statuses and never - // errors. Only shows the first error. - let visible_status_type = 'none'; - if (statusElem.classList.contains("noVNC_open")) { - if (statusElem.classList.contains("noVNC_status_error")) { - visible_status_type = 'error'; - } else if (statusElem.classList.contains("noVNC_status_warn")) { - visible_status_type = 'warn'; - } else { - visible_status_type = 'normal'; - } - } - if (visible_status_type === 'error' || - (visible_status_type === 'warn' && status_type === 'normal')) { - return; - } + switch (status_type) { + case 'error': + statusElem.classList.remove('noVNC_status_warn'); + statusElem.classList.remove('noVNC_status_normal'); + statusElem.classList.add('noVNC_status_error'); + break; + case 'warning': + case 'warn': + statusElem.classList.remove('noVNC_status_error'); + statusElem.classList.remove('noVNC_status_normal'); + statusElem.classList.add('noVNC_status_warn'); + break; + case 'normal': + case 'info': + default: + statusElem.classList.remove('noVNC_status_error'); + statusElem.classList.remove('noVNC_status_warn'); + statusElem.classList.add('noVNC_status_normal'); + break; + } - switch (status_type) { - case 'error': - statusElem.classList.remove("noVNC_status_warn"); - statusElem.classList.remove("noVNC_status_normal"); - statusElem.classList.add("noVNC_status_error"); - break; - case 'warning': - case 'warn': - statusElem.classList.remove("noVNC_status_error"); - statusElem.classList.remove("noVNC_status_normal"); - statusElem.classList.add("noVNC_status_warn"); - break; - case 'normal': - case 'info': - default: - statusElem.classList.remove("noVNC_status_error"); - statusElem.classList.remove("noVNC_status_warn"); - statusElem.classList.add("noVNC_status_normal"); - break; - } + statusElem.textContent = text; + statusElem.classList.add('noVNC_open'); - statusElem.textContent = text; - statusElem.classList.add("noVNC_open"); + // If no time was specified, show the status for 1.5 seconds + if (typeof time === 'undefined') { + time = 1500; + } - // If no time was specified, show the status for 1.5 seconds - if (typeof time === 'undefined') { - time = 1500; - } + // Error messages do not timeout + if (status_type !== 'error') { + UI.statusTimeout = window.setTimeout(UI.hideStatus, time); + } + }, - // Error messages do not timeout - if (status_type !== 'error') { - UI.statusTimeout = window.setTimeout(UI.hideStatus, time); - } - }, + hideStatus() { + clearTimeout(UI.statusTimeout); + document.getElementById('noVNC_status').classList.remove('noVNC_open'); + }, - hideStatus() { - clearTimeout(UI.statusTimeout); - document.getElementById('noVNC_status').classList.remove("noVNC_open"); - }, + activateControlbar(event) { + clearTimeout(UI.idleControlbarTimeout); + // We manipulate the anchor instead of the actual control + // bar in order to avoid creating new a stacking group + document.getElementById('noVNC_control_bar_anchor') + .classList.remove('noVNC_idle'); + UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); + }, - activateControlbar(event) { - clearTimeout(UI.idleControlbarTimeout); - // We manipulate the anchor instead of the actual control - // bar in order to avoid creating new a stacking group - document.getElementById('noVNC_control_bar_anchor') - .classList.remove("noVNC_idle"); - UI.idleControlbarTimeout = window.setTimeout(UI.idleControlbar, 2000); - }, + idleControlbar() { + document.getElementById('noVNC_control_bar_anchor') + .classList.add('noVNC_idle'); + }, - idleControlbar() { - document.getElementById('noVNC_control_bar_anchor') - .classList.add("noVNC_idle"); - }, + keepControlbar() { + clearTimeout(UI.closeControlbarTimeout); + }, - keepControlbar() { - clearTimeout(UI.closeControlbarTimeout); - }, + openControlbar() { + document.getElementById('noVNC_control_bar') + .classList.add('noVNC_open'); + }, - openControlbar() { - document.getElementById('noVNC_control_bar') - .classList.add("noVNC_open"); - }, + closeControlbar() { + UI.closeAllPanels(); + document.getElementById('noVNC_control_bar') + .classList.remove('noVNC_open'); + }, - closeControlbar() { - UI.closeAllPanels(); - document.getElementById('noVNC_control_bar') - .classList.remove("noVNC_open"); - }, + toggleControlbar() { + if (document.getElementById('noVNC_control_bar') + .classList.contains('noVNC_open')) { + UI.closeControlbar(); + } else { + UI.openControlbar(); + } + }, - toggleControlbar() { - if (document.getElementById('noVNC_control_bar') - .classList.contains("noVNC_open")) { - UI.closeControlbar(); - } else { - UI.openControlbar(); - } - }, + toggleControlbarSide() { + // Temporarily disable animation, if bar is displayed, to avoid weird + // movement. The transitionend-event will not fire when display=none. + const bar = document.getElementById('noVNC_control_bar'); + const barDisplayStyle = window.getComputedStyle(bar).display; + if (barDisplayStyle !== 'none') { + bar.style.transitionDuration = '0s'; + bar.addEventListener('transitionend', () => bar.style.transitionDuration = ''); + } - toggleControlbarSide() { - // Temporarily disable animation, if bar is displayed, to avoid weird - // movement. The transitionend-event will not fire when display=none. - const bar = document.getElementById('noVNC_control_bar'); - const barDisplayStyle = window.getComputedStyle(bar).display; - if (barDisplayStyle !== 'none') { - bar.style.transitionDuration = '0s'; - bar.addEventListener('transitionend', () => bar.style.transitionDuration = ''); - } + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (anchor.classList.contains('noVNC_right')) { + WebUtil.writeSetting('controlbar_pos', 'left'); + anchor.classList.remove('noVNC_right'); + } else { + WebUtil.writeSetting('controlbar_pos', 'right'); + anchor.classList.add('noVNC_right'); + } - const anchor = document.getElementById('noVNC_control_bar_anchor'); - if (anchor.classList.contains("noVNC_right")) { - WebUtil.writeSetting('controlbar_pos', 'left'); - anchor.classList.remove("noVNC_right"); - } else { - WebUtil.writeSetting('controlbar_pos', 'right'); - anchor.classList.add("noVNC_right"); - } + // Consider this a movement of the handle + UI.controlbarDrag = true; + }, - // Consider this a movement of the handle - UI.controlbarDrag = true; - }, + showControlbarHint(show) { + const hint = document.getElementById('noVNC_control_bar_hint'); + if (show) { + hint.classList.add('noVNC_active'); + } else { + hint.classList.remove('noVNC_active'); + } + }, - showControlbarHint(show) { - const hint = document.getElementById('noVNC_control_bar_hint'); - if (show) { - hint.classList.add("noVNC_active"); - } else { - hint.classList.remove("noVNC_active"); - } - }, + dragControlbarHandle(e) { + if (!UI.controlbarGrabbed) return; - dragControlbarHandle(e) { - if (!UI.controlbarGrabbed) return; + const ptr = getPointerEvent(e); - const ptr = getPointerEvent(e); + const anchor = document.getElementById('noVNC_control_bar_anchor'); + if (ptr.clientX < (window.innerWidth * 0.1)) { + if (anchor.classList.contains('noVNC_right')) { + UI.toggleControlbarSide(); + } + } else if (ptr.clientX > (window.innerWidth * 0.9)) { + if (!anchor.classList.contains('noVNC_right')) { + UI.toggleControlbarSide(); + } + } - const anchor = document.getElementById('noVNC_control_bar_anchor'); - if (ptr.clientX < (window.innerWidth * 0.1)) { - if (anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } else if (ptr.clientX > (window.innerWidth * 0.9)) { - if (!anchor.classList.contains("noVNC_right")) { - UI.toggleControlbarSide(); - } - } + if (!UI.controlbarDrag) { + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + const dragThreshold = 10 * (window.devicePixelRatio || 1); + const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); - if (!UI.controlbarDrag) { - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - const dragThreshold = 10 * (window.devicePixelRatio || 1); - const dragDistance = Math.abs(ptr.clientY - UI.controlbarMouseDownClientY); + if (dragDistance < dragThreshold) return; - if (dragDistance < dragThreshold) return; + UI.controlbarDrag = true; + } - UI.controlbarDrag = true; - } + const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; - const eventY = ptr.clientY - UI.controlbarMouseDownOffsetY; + UI.moveControlbarHandle(eventY); - UI.moveControlbarHandle(eventY); + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - }, + // Move the handle but don't allow any position outside the bounds + moveControlbarHandle(viewportRelativeY) { + const handle = document.getElementById('noVNC_control_bar_handle'); + const handleHeight = handle.getBoundingClientRect().height; + const controlbarBounds = document.getElementById('noVNC_control_bar') + .getBoundingClientRect(); + const margin = 10; - // Move the handle but don't allow any position outside the bounds - moveControlbarHandle(viewportRelativeY) { - const handle = document.getElementById("noVNC_control_bar_handle"); - const handleHeight = handle.getBoundingClientRect().height; - const controlbarBounds = document.getElementById("noVNC_control_bar") - .getBoundingClientRect(); - const margin = 10; + // These heights need to be non-zero for the below logic to work + if (handleHeight === 0 || controlbarBounds.height === 0) { + return; + } - // These heights need to be non-zero for the below logic to work - if (handleHeight === 0 || controlbarBounds.height === 0) { - return; - } + let newY = viewportRelativeY; - let newY = viewportRelativeY; + // Check if the coordinates are outside the control bar + if (newY < controlbarBounds.top + margin) { + // Force coordinates to be below the top of the control bar + newY = controlbarBounds.top + margin; + } else if (newY > controlbarBounds.top + + controlbarBounds.height - handleHeight - margin) { + // Force coordinates to be above the bottom of the control bar + newY = controlbarBounds.top + + controlbarBounds.height - handleHeight - margin; + } - // Check if the coordinates are outside the control bar - if (newY < controlbarBounds.top + margin) { - // Force coordinates to be below the top of the control bar - newY = controlbarBounds.top + margin; + // Corner case: control bar too small for stable position + if (controlbarBounds.height < (handleHeight + margin * 2)) { + newY = controlbarBounds.top + + (controlbarBounds.height - handleHeight) / 2; + } - } else if (newY > controlbarBounds.top + - controlbarBounds.height - handleHeight - margin) { - // Force coordinates to be above the bottom of the control bar - newY = controlbarBounds.top + - controlbarBounds.height - handleHeight - margin; - } + // The transform needs coordinates that are relative to the parent + const parentRelativeY = newY - controlbarBounds.top; + handle.style.transform = 'translateY(' + parentRelativeY + 'px)'; + }, - // Corner case: control bar too small for stable position - if (controlbarBounds.height < (handleHeight + margin * 2)) { - newY = controlbarBounds.top + - (controlbarBounds.height - handleHeight) / 2; - } + updateControlbarHandle() { + // Since the control bar is fixed on the viewport and not the page, + // the move function expects coordinates relative the the viewport. + const handle = document.getElementById('noVNC_control_bar_handle'); + const handleBounds = handle.getBoundingClientRect(); + UI.moveControlbarHandle(handleBounds.top); + }, - // The transform needs coordinates that are relative to the parent - const parentRelativeY = newY - controlbarBounds.top; - handle.style.transform = "translateY(" + parentRelativeY + "px)"; - }, + controlbarHandleMouseUp(e) { + if ((e.type == 'mouseup') && (e.button != 0)) return; - updateControlbarHandle() { - // Since the control bar is fixed on the viewport and not the page, - // the move function expects coordinates relative the the viewport. - const handle = document.getElementById("noVNC_control_bar_handle"); - const handleBounds = handle.getBoundingClientRect(); - UI.moveControlbarHandle(handleBounds.top); - }, + // mouseup and mousedown on the same place toggles the controlbar + if (UI.controlbarGrabbed && !UI.controlbarDrag) { + UI.toggleControlbar(); + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + } + UI.controlbarGrabbed = false; + UI.showControlbarHint(false); + }, - controlbarHandleMouseUp(e) { - if ((e.type == "mouseup") && (e.button != 0)) return; + controlbarHandleMouseDown(e) { + if ((e.type == 'mousedown') && (e.button != 0)) return; - // mouseup and mousedown on the same place toggles the controlbar - if (UI.controlbarGrabbed && !UI.controlbarDrag) { - UI.toggleControlbar(); - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - } - UI.controlbarGrabbed = false; - UI.showControlbarHint(false); - }, + const ptr = getPointerEvent(e); - controlbarHandleMouseDown(e) { - if ((e.type == "mousedown") && (e.button != 0)) return; + const handle = document.getElementById('noVNC_control_bar_handle'); + const bounds = handle.getBoundingClientRect(); - const ptr = getPointerEvent(e); + // Touch events have implicit capture + if (e.type === 'mousedown') { + setCapture(handle); + } - const handle = document.getElementById("noVNC_control_bar_handle"); - const bounds = handle.getBoundingClientRect(); + UI.controlbarGrabbed = true; + UI.controlbarDrag = false; - // Touch events have implicit capture - if (e.type === "mousedown") { - setCapture(handle); - } + UI.showControlbarHint(true); - UI.controlbarGrabbed = true; - UI.controlbarDrag = false; + UI.controlbarMouseDownClientY = ptr.clientY; + UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; + e.preventDefault(); + e.stopPropagation(); + UI.keepControlbar(); + UI.activateControlbar(); + }, - UI.showControlbarHint(true); + toggleExpander(e) { + if (this.classList.contains('noVNC_open')) { + this.classList.remove('noVNC_open'); + } else { + this.classList.add('noVNC_open'); + } + }, - UI.controlbarMouseDownClientY = ptr.clientY; - UI.controlbarMouseDownOffsetY = ptr.clientY - bounds.top; - e.preventDefault(); - e.stopPropagation(); - UI.keepControlbar(); - UI.activateControlbar(); - }, - - toggleExpander(e) { - if (this.classList.contains("noVNC_open")) { - this.classList.remove("noVNC_open"); - } else { - this.classList.add("noVNC_open"); - } - }, - -/* ------^------- + /* ------^------- * /VISUAL * ============== * SETTINGS * ------v------*/ - // Initial page load read/initialization of settings - initSetting(name, defVal) { - // Check Query string followed by cookie - let val = WebUtil.getConfigVar(name); - if (val === null) { - val = WebUtil.readSetting(name, defVal); + // Initial page load read/initialization of settings + initSetting(name, defVal) { + // Check Query string followed by cookie + let val = WebUtil.getConfigVar(name); + if (val === null) { + val = WebUtil.readSetting(name, defVal); + } + WebUtil.setSetting(name, val); + UI.updateSetting(name); + return val; + }, + + // Set the new value, update and disable form control setting + forceSetting(name, val) { + WebUtil.setSetting(name, val); + UI.updateSetting(name); + UI.disableSetting(name); + }, + + // Update cookie and form control setting. If value is not set, then + // updates from control to current cookie setting. + updateSetting(name) { + // Update the settings control + let value = UI.getSetting(name); + + const ctrl = document.getElementById('noVNC_setting_' + name); + if (ctrl.type === 'checkbox') { + ctrl.checked = value; + } else if (typeof ctrl.options !== 'undefined') { + for (let i = 0; i < ctrl.options.length; i += 1) { + if (ctrl.options[i].value === value) { + ctrl.selectedIndex = i; + break; } - WebUtil.setSetting(name, val); - UI.updateSetting(name); - return val; - }, + } + } else { + /* Weird IE9 error leads to 'null' appearring + in textboxes instead of ''. */ + if (value === null) { + value = ''; + } + ctrl.value = value; + } + }, - // Set the new value, update and disable form control setting - forceSetting(name, val) { - WebUtil.setSetting(name, val); - UI.updateSetting(name); - UI.disableSetting(name); - }, + // Save control setting to cookie + saveSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val; + if (ctrl.type === 'checkbox') { + val = ctrl.checked; + } else if (typeof ctrl.options !== 'undefined') { + val = ctrl.options[ctrl.selectedIndex].value; + } else { + val = ctrl.value; + } + WebUtil.writeSetting(name, val); + // Log.Debug("Setting saved '" + name + "=" + val + "'"); + return val; + }, - // Update cookie and form control setting. If value is not set, then - // updates from control to current cookie setting. - updateSetting(name) { + // Read form control compatible setting from cookie + getSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + let val = WebUtil.readSetting(name); + if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { + if (val.toString().toLowerCase() in { 0: 1, no: 1, false: 1 }) { + val = false; + } else { + val = true; + } + } + return val; + }, - // Update the settings control - let value = UI.getSetting(name); + // These helpers compensate for the lack of parent-selectors and + // previous-sibling-selectors in CSS which are needed when we want to + // disable the labels that belong to disabled input elements. + disableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = true; + ctrl.label.classList.add('noVNC_disabled'); + }, - const ctrl = document.getElementById('noVNC_setting_' + name); - if (ctrl.type === 'checkbox') { - ctrl.checked = value; + enableSetting(name) { + const ctrl = document.getElementById('noVNC_setting_' + name); + ctrl.disabled = false; + ctrl.label.classList.remove('noVNC_disabled'); + }, - } else if (typeof ctrl.options !== 'undefined') { - for (let i = 0; i < ctrl.options.length; i += 1) { - if (ctrl.options[i].value === value) { - ctrl.selectedIndex = i; - break; - } - } - } else { - /*Weird IE9 error leads to 'null' appearring - in textboxes instead of ''.*/ - if (value === null) { - value = ""; - } - ctrl.value = value; - } - }, - - // Save control setting to cookie - saveSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - let val; - if (ctrl.type === 'checkbox') { - val = ctrl.checked; - } else if (typeof ctrl.options !== 'undefined') { - val = ctrl.options[ctrl.selectedIndex].value; - } else { - val = ctrl.value; - } - WebUtil.writeSetting(name, val); - //Log.Debug("Setting saved '" + name + "=" + val + "'"); - return val; - }, - - // Read form control compatible setting from cookie - getSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - let val = WebUtil.readSetting(name); - if (typeof val !== 'undefined' && val !== null && ctrl.type === 'checkbox') { - if (val.toString().toLowerCase() in {'0':1, 'no':1, 'false':1}) { - val = false; - } else { - val = true; - } - } - return val; - }, - - // These helpers compensate for the lack of parent-selectors and - // previous-sibling-selectors in CSS which are needed when we want to - // disable the labels that belong to disabled input elements. - disableSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = true; - ctrl.label.classList.add('noVNC_disabled'); - }, - - enableSetting(name) { - const ctrl = document.getElementById('noVNC_setting_' + name); - ctrl.disabled = false; - ctrl.label.classList.remove('noVNC_disabled'); - }, - -/* ------^------- + /* ------^------- * /SETTINGS * ============== * PANELS * ------v------*/ - closeAllPanels() { - UI.closeSettingsPanel(); - UI.closePowerPanel(); - UI.closeClipboardPanel(); - UI.closeExtraKeys(); - }, + closeAllPanels() { + UI.closeSettingsPanel(); + UI.closePowerPanel(); + UI.closeClipboardPanel(); + UI.closeExtraKeys(); + }, -/* ------^------- + /* ------^------- * /PANELS * ============== * SETTINGS (panel) * ------v------*/ - openSettingsPanel() { - UI.closeAllPanels(); - UI.openControlbar(); + openSettingsPanel() { + UI.closeAllPanels(); + UI.openControlbar(); - // Refresh UI elements from saved cookies - UI.updateSetting('encrypt'); - UI.updateSetting('view_clip'); - UI.updateSetting('resize'); - UI.updateSetting('shared'); - UI.updateSetting('view_only'); - UI.updateSetting('path'); - UI.updateSetting('repeaterID'); - UI.updateSetting('logging'); - UI.updateSetting('reconnect'); - UI.updateSetting('reconnect_delay'); + // Refresh UI elements from saved cookies + UI.updateSetting('encrypt'); + UI.updateSetting('view_clip'); + UI.updateSetting('resize'); + UI.updateSetting('shared'); + UI.updateSetting('view_only'); + UI.updateSetting('path'); + UI.updateSetting('repeaterID'); + UI.updateSetting('logging'); + UI.updateSetting('reconnect'); + UI.updateSetting('reconnect_delay'); - document.getElementById('noVNC_settings') - .classList.add("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_settings') + .classList.add('noVNC_open'); + document.getElementById('noVNC_settings_button') + .classList.add('noVNC_selected'); + }, - closeSettingsPanel() { - document.getElementById('noVNC_settings') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_settings_button') - .classList.remove("noVNC_selected"); - }, + closeSettingsPanel() { + document.getElementById('noVNC_settings') + .classList.remove('noVNC_open'); + document.getElementById('noVNC_settings_button') + .classList.remove('noVNC_selected'); + }, - toggleSettingsPanel() { - if (document.getElementById('noVNC_settings') - .classList.contains("noVNC_open")) { - UI.closeSettingsPanel(); - } else { - UI.openSettingsPanel(); - } - }, + toggleSettingsPanel() { + if (document.getElementById('noVNC_settings') + .classList.contains('noVNC_open')) { + UI.closeSettingsPanel(); + } else { + UI.openSettingsPanel(); + } + }, -/* ------^------- + /* ------^------- * /SETTINGS * ============== * POWER * ------v------*/ - openPowerPanel() { - UI.closeAllPanels(); - UI.openControlbar(); + openPowerPanel() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_power') - .classList.add("noVNC_open"); - document.getElementById('noVNC_power_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_power') + .classList.add('noVNC_open'); + document.getElementById('noVNC_power_button') + .classList.add('noVNC_selected'); + }, - closePowerPanel() { - document.getElementById('noVNC_power') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_power_button') - .classList.remove("noVNC_selected"); - }, + closePowerPanel() { + document.getElementById('noVNC_power') + .classList.remove('noVNC_open'); + document.getElementById('noVNC_power_button') + .classList.remove('noVNC_selected'); + }, - togglePowerPanel() { - if (document.getElementById('noVNC_power') - .classList.contains("noVNC_open")) { - UI.closePowerPanel(); - } else { - UI.openPowerPanel(); - } - }, + togglePowerPanel() { + if (document.getElementById('noVNC_power') + .classList.contains('noVNC_open')) { + UI.closePowerPanel(); + } else { + UI.openPowerPanel(); + } + }, - // Disable/enable power button - updatePowerButton() { - if (UI.connected && - UI.rfb.capabilities.power && - !UI.rfb.viewOnly) { - document.getElementById('noVNC_power_button') - .classList.remove("noVNC_hidden"); - } else { - document.getElementById('noVNC_power_button') - .classList.add("noVNC_hidden"); - // Close power panel if open - UI.closePowerPanel(); - } - }, + // Disable/enable power button + updatePowerButton() { + if (UI.connected + && UI.rfb.capabilities.power + && !UI.rfb.viewOnly) { + document.getElementById('noVNC_power_button') + .classList.remove('noVNC_hidden'); + } else { + document.getElementById('noVNC_power_button') + .classList.add('noVNC_hidden'); + // Close power panel if open + UI.closePowerPanel(); + } + }, -/* ------^------- + /* ------^------- * /POWER * ============== * CLIPBOARD * ------v------*/ - openClipboardPanel() { - UI.closeAllPanels(); - UI.openControlbar(); + openClipboardPanel() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_clipboard') - .classList.add("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_clipboard') + .classList.add('noVNC_open'); + document.getElementById('noVNC_clipboard_button') + .classList.add('noVNC_selected'); + }, - closeClipboardPanel() { - document.getElementById('noVNC_clipboard') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_clipboard_button') - .classList.remove("noVNC_selected"); - }, + closeClipboardPanel() { + document.getElementById('noVNC_clipboard') + .classList.remove('noVNC_open'); + document.getElementById('noVNC_clipboard_button') + .classList.remove('noVNC_selected'); + }, - toggleClipboardPanel() { - if (document.getElementById('noVNC_clipboard') - .classList.contains("noVNC_open")) { - UI.closeClipboardPanel(); - } else { - UI.openClipboardPanel(); - } - }, + toggleClipboardPanel() { + if (document.getElementById('noVNC_clipboard') + .classList.contains('noVNC_open')) { + UI.closeClipboardPanel(); + } else { + UI.openClipboardPanel(); + } + }, - clipboardReceive(e) { - Log.Debug(">> UI.clipboardReceive: " + e.detail.text.substr(0,40) + "..."); - document.getElementById('noVNC_clipboard_text').value = e.detail.text; - Log.Debug("<< UI.clipboardReceive"); - }, + clipboardReceive(e) { + Log.Debug('>> UI.clipboardReceive: ' + e.detail.text.substr(0, 40) + '...'); + document.getElementById('noVNC_clipboard_text').value = e.detail.text; + Log.Debug('<< UI.clipboardReceive'); + }, - clipboardClear() { - document.getElementById('noVNC_clipboard_text').value = ""; - UI.rfb.clipboardPasteFrom(""); - }, + clipboardClear() { + document.getElementById('noVNC_clipboard_text').value = ''; + UI.rfb.clipboardPasteFrom(''); + }, - clipboardSend() { - const text = document.getElementById('noVNC_clipboard_text').value; - Log.Debug(">> UI.clipboardSend: " + text.substr(0,40) + "..."); - UI.rfb.clipboardPasteFrom(text); - Log.Debug("<< UI.clipboardSend"); - }, + clipboardSend() { + const text = document.getElementById('noVNC_clipboard_text').value; + Log.Debug('>> UI.clipboardSend: ' + text.substr(0, 40) + '...'); + UI.rfb.clipboardPasteFrom(text); + Log.Debug('<< UI.clipboardSend'); + }, -/* ------^------- + /* ------^------- * /CLIPBOARD * ============== * CONNECTION * ------v------*/ - openConnectPanel() { - document.getElementById('noVNC_connect_dlg') - .classList.add("noVNC_open"); - }, + openConnectPanel() { + document.getElementById('noVNC_connect_dlg') + .classList.add('noVNC_open'); + }, - closeConnectPanel() { - document.getElementById('noVNC_connect_dlg') - .classList.remove("noVNC_open"); - }, + closeConnectPanel() { + document.getElementById('noVNC_connect_dlg') + .classList.remove('noVNC_open'); + }, - connect(event, password) { + connect(event, password) { + // Ignore when rfb already exists + if (typeof UI.rfb !== 'undefined') { + return; + } - // Ignore when rfb already exists - if (typeof UI.rfb !== 'undefined') { - return; - } + const host = UI.getSetting('host'); + const port = UI.getSetting('port'); + const path = UI.getSetting('path'); - const host = UI.getSetting('host'); - const port = UI.getSetting('port'); - const path = UI.getSetting('path'); + if (typeof password === 'undefined') { + password = WebUtil.getConfigVar('password'); + UI.reconnect_password = password; + } - if (typeof password === 'undefined') { - password = WebUtil.getConfigVar('password'); - UI.reconnect_password = password; - } + if (password === null) { + password = undefined; + } - if (password === null) { - password = undefined; - } + UI.hideStatus(); - UI.hideStatus(); + if (!host) { + Log.Error("Can't connect when host is: " + host); + UI.showStatus(_('Must set host'), 'error'); + return; + } - if (!host) { - Log.Error("Can't connect when host is: " + host); - UI.showStatus(_("Must set host"), 'error'); - return; - } + UI.closeAllPanels(); + UI.closeConnectPanel(); - UI.closeAllPanels(); - UI.closeConnectPanel(); + UI.updateVisualState('connecting'); - UI.updateVisualState('connecting'); + let url; - let url; + url = UI.getSetting('encrypt') ? 'wss' : 'ws'; - url = UI.getSetting('encrypt') ? 'wss' : 'ws'; + url += '://' + host; + if (port) { + url += ':' + port; + } + url += '/' + path; - url += '://' + host; - if(port) { - url += ':' + port; - } - url += '/' + path; + UI.rfb = new RFB(document.getElementById('noVNC_container'), url, + { + shared: UI.getSetting('shared'), + repeaterID: UI.getSetting('repeaterID'), + credentials: { password: password } + }); + UI.rfb.addEventListener('connect', UI.connectFinished); + UI.rfb.addEventListener('disconnect', UI.disconnectFinished); + UI.rfb.addEventListener('credentialsrequired', UI.credentials); + UI.rfb.addEventListener('securityfailure', UI.securityFailed); + UI.rfb.addEventListener('capabilities', UI.updatePowerButton); + UI.rfb.addEventListener('clipboard', UI.clipboardReceive); + UI.rfb.addEventListener('bell', UI.bell); + UI.rfb.addEventListener('desktopname', UI.updateDesktopName); + UI.rfb.clipViewport = UI.getSetting('view_clip'); + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; - UI.rfb = new RFB(document.getElementById('noVNC_container'), url, - { shared: UI.getSetting('shared'), - repeaterID: UI.getSetting('repeaterID'), - credentials: { password: password } }); - UI.rfb.addEventListener("connect", UI.connectFinished); - UI.rfb.addEventListener("disconnect", UI.disconnectFinished); - UI.rfb.addEventListener("credentialsrequired", UI.credentials); - UI.rfb.addEventListener("securityfailure", UI.securityFailed); - UI.rfb.addEventListener("capabilities", UI.updatePowerButton); - UI.rfb.addEventListener("clipboard", UI.clipboardReceive); - UI.rfb.addEventListener("bell", UI.bell); - UI.rfb.addEventListener("desktopname", UI.updateDesktopName); - UI.rfb.clipViewport = UI.getSetting('view_clip'); - UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + UI.updateViewOnly(); // requires UI.rfb + }, - UI.updateViewOnly(); // requires UI.rfb - }, + disconnect() { + UI.closeAllPanels(); + UI.rfb.disconnect(); - disconnect() { - UI.closeAllPanels(); - UI.rfb.disconnect(); + UI.connected = false; - UI.connected = false; + // Disable automatic reconnecting + UI.inhibit_reconnect = true; - // Disable automatic reconnecting - UI.inhibit_reconnect = true; + UI.updateVisualState('disconnecting'); - UI.updateVisualState('disconnecting'); + // Don't display the connection settings until we're actually disconnected + }, - // Don't display the connection settings until we're actually disconnected - }, + reconnect() { + UI.reconnect_callback = null; - reconnect() { - UI.reconnect_callback = null; + // if reconnect has been disabled in the meantime, do nothing. + if (UI.inhibit_reconnect) { + return; + } - // if reconnect has been disabled in the meantime, do nothing. - if (UI.inhibit_reconnect) { - return; - } + UI.connect(null, UI.reconnect_password); + }, - UI.connect(null, UI.reconnect_password); - }, + cancelReconnect() { + if (UI.reconnect_callback !== null) { + clearTimeout(UI.reconnect_callback); + UI.reconnect_callback = null; + } - cancelReconnect() { - if (UI.reconnect_callback !== null) { - clearTimeout(UI.reconnect_callback); - UI.reconnect_callback = null; - } + UI.updateVisualState('disconnected'); - UI.updateVisualState('disconnected'); + UI.openControlbar(); + UI.openConnectPanel(); + }, - UI.openControlbar(); - UI.openConnectPanel(); - }, + connectFinished(e) { + UI.connected = true; + UI.inhibit_reconnect = false; - connectFinished(e) { - UI.connected = true; - UI.inhibit_reconnect = false; + let msg; + if (UI.getSetting('encrypt')) { + msg = _('Connected (encrypted) to ') + UI.desktopName; + } else { + msg = _('Connected (unencrypted) to ') + UI.desktopName; + } + UI.showStatus(msg); + UI.updateVisualState('connected'); - let msg; - if (UI.getSetting('encrypt')) { - msg = _("Connected (encrypted) to ") + UI.desktopName; - } else { - msg = _("Connected (unencrypted) to ") + UI.desktopName; - } - UI.showStatus(msg); - UI.updateVisualState('connected'); + // Do this last because it can only be used on rendered elements + UI.rfb.focus(); + }, - // Do this last because it can only be used on rendered elements - UI.rfb.focus(); - }, + disconnectFinished(e) { + const wasConnected = UI.connected; - disconnectFinished(e) { - const wasConnected = UI.connected; + // This variable is ideally set when disconnection starts, but + // when the disconnection isn't clean or if it is initiated by + // the server, we need to do it here as well since + // UI.disconnect() won't be used in those cases. + UI.connected = false; - // This variable is ideally set when disconnection starts, but - // when the disconnection isn't clean or if it is initiated by - // the server, we need to do it here as well since - // UI.disconnect() won't be used in those cases. - UI.connected = false; + UI.rfb = undefined; - UI.rfb = undefined; + if (!e.detail.clean) { + UI.updateVisualState('disconnected'); + if (wasConnected) { + UI.showStatus(_('Something went wrong, connection is closed'), + 'error'); + } else { + UI.showStatus(_('Failed to connect to server'), 'error'); + } + } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { + UI.updateVisualState('reconnecting'); - if (!e.detail.clean) { - UI.updateVisualState('disconnected'); - if (wasConnected) { - UI.showStatus(_("Something went wrong, connection is closed"), - 'error'); - } else { - UI.showStatus(_("Failed to connect to server"), 'error'); - } - } else if (UI.getSetting('reconnect', false) === true && !UI.inhibit_reconnect) { - UI.updateVisualState('reconnecting'); + const delay = parseInt(UI.getSetting('reconnect_delay')); + UI.reconnect_callback = setTimeout(UI.reconnect, delay); + return; + } else { + UI.updateVisualState('disconnected'); + UI.showStatus(_('Disconnected'), 'normal'); + } - const delay = parseInt(UI.getSetting('reconnect_delay')); - UI.reconnect_callback = setTimeout(UI.reconnect, delay); - return; - } else { - UI.updateVisualState('disconnected'); - UI.showStatus(_("Disconnected"), 'normal'); - } + UI.openControlbar(); + UI.openConnectPanel(); + }, - UI.openControlbar(); - UI.openConnectPanel(); - }, + securityFailed(e) { + let msg = ''; + // On security failures we might get a string with a reason + // directly from the server. Note that we can't control if + // this string is translated or not. + if ('reason' in e.detail) { + msg = _('New connection has been rejected with reason: ') + + e.detail.reason; + } else { + msg = _('New connection has been rejected'); + } + UI.showStatus(msg, 'error'); + }, - securityFailed(e) { - let msg = ""; - // On security failures we might get a string with a reason - // directly from the server. Note that we can't control if - // this string is translated or not. - if ('reason' in e.detail) { - msg = _("New connection has been rejected with reason: ") + - e.detail.reason; - } else { - msg = _("New connection has been rejected"); - } - UI.showStatus(msg, 'error'); - }, - -/* ------^------- + /* ------^------- * /CONNECTION * ============== * PASSWORD * ------v------*/ - credentials(e) { - // FIXME: handle more types - document.getElementById('noVNC_password_dlg') - .classList.add('noVNC_open'); + credentials(e) { + // FIXME: handle more types + document.getElementById('noVNC_password_dlg') + .classList.add('noVNC_open'); - setTimeout(() => document - .getElementById('noVNC_password_input').focus(), 100); + setTimeout(() => document + .getElementById('noVNC_password_input').focus(), 100); - Log.Warn("Server asked for a password"); - UI.showStatus(_("Password is required"), "warning"); - }, + Log.Warn('Server asked for a password'); + UI.showStatus(_('Password is required'), 'warning'); + }, - setPassword(e) { - // Prevent actually submitting the form - e.preventDefault(); + setPassword(e) { + // Prevent actually submitting the form + e.preventDefault(); - const inputElem = document.getElementById('noVNC_password_input'); - const password = inputElem.value; - // Clear the input after reading the password - inputElem.value = ""; - UI.rfb.sendCredentials({ password: password }); - UI.reconnect_password = password; - document.getElementById('noVNC_password_dlg') - .classList.remove('noVNC_open'); - }, + const inputElem = document.getElementById('noVNC_password_input'); + const password = inputElem.value; + // Clear the input after reading the password + inputElem.value = ''; + UI.rfb.sendCredentials({ password: password }); + UI.reconnect_password = password; + document.getElementById('noVNC_password_dlg') + .classList.remove('noVNC_open'); + }, -/* ------^------- + /* ------^------- * /PASSWORD * ============== * FULLSCREEN * ------v------*/ - toggleFullscreen() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement) { - if (document.exitFullscreen) { - document.exitFullscreen(); - } else if (document.mozCancelFullScreen) { - document.mozCancelFullScreen(); - } else if (document.webkitExitFullscreen) { - document.webkitExitFullscreen(); - } else if (document.msExitFullscreen) { - document.msExitFullscreen(); - } - } else { - if (document.documentElement.requestFullscreen) { - document.documentElement.requestFullscreen(); - } else if (document.documentElement.mozRequestFullScreen) { - document.documentElement.mozRequestFullScreen(); - } else if (document.documentElement.webkitRequestFullscreen) { - document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); - } else if (document.body.msRequestFullscreen) { - document.body.msRequestFullscreen(); - } - } - UI.enableDisableViewClip(); - UI.updateFullscreenButton(); - }, + toggleFullscreen() { + if (document.fullscreenElement // alternative standard method + || document.mozFullScreenElement // currently working methods + || document.webkitFullscreenElement + || document.msFullscreenElement) { + if (document.exitFullscreen) { + document.exitFullscreen(); + } else if (document.mozCancelFullScreen) { + document.mozCancelFullScreen(); + } else if (document.webkitExitFullscreen) { + document.webkitExitFullscreen(); + } else if (document.msExitFullscreen) { + document.msExitFullscreen(); + } + } else if (document.documentElement.requestFullscreen) { + document.documentElement.requestFullscreen(); + } else if (document.documentElement.mozRequestFullScreen) { + document.documentElement.mozRequestFullScreen(); + } else if (document.documentElement.webkitRequestFullscreen) { + document.documentElement.webkitRequestFullscreen(Element.ALLOW_KEYBOARD_INPUT); + } else if (document.body.msRequestFullscreen) { + document.body.msRequestFullscreen(); + } + UI.enableDisableViewClip(); + UI.updateFullscreenButton(); + }, - updateFullscreenButton() { - if (document.fullscreenElement || // alternative standard method - document.mozFullScreenElement || // currently working methods - document.webkitFullscreenElement || - document.msFullscreenElement ) { - document.getElementById('noVNC_fullscreen_button') - .classList.add("noVNC_selected"); - } else { - document.getElementById('noVNC_fullscreen_button') - .classList.remove("noVNC_selected"); - } - }, + updateFullscreenButton() { + if (document.fullscreenElement // alternative standard method + || document.mozFullScreenElement // currently working methods + || document.webkitFullscreenElement + || document.msFullscreenElement) { + document.getElementById('noVNC_fullscreen_button') + .classList.add('noVNC_selected'); + } else { + document.getElementById('noVNC_fullscreen_button') + .classList.remove('noVNC_selected'); + } + }, -/* ------^------- + /* ------^------- * /FULLSCREEN * ============== * RESIZE * ------v------*/ - // Apply remote resizing or local scaling - applyResizeMode() { - if (!UI.rfb) return; + // Apply remote resizing or local scaling + applyResizeMode() { + if (!UI.rfb) return; - UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; - UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; - }, + UI.rfb.scaleViewport = UI.getSetting('resize') === 'scale'; + UI.rfb.resizeSession = UI.getSetting('resize') === 'remote'; + }, -/* ------^------- + /* ------^------- * /RESIZE * ============== * VIEW CLIPPING * ------v------*/ - // Update parameters that depend on the viewport clip setting - updateViewClip() { - if (!UI.rfb) return; + // Update parameters that depend on the viewport clip setting + updateViewClip() { + if (!UI.rfb) return; - const cur_clip = UI.rfb.clipViewport; - let new_clip = UI.getSetting('view_clip'); + const cur_clip = UI.rfb.clipViewport; + let new_clip = UI.getSetting('view_clip'); - if (isTouchDevice) { - // Touch devices usually have shit scrollbars - new_clip = true; - } + if (isTouchDevice) { + // Touch devices usually have shit scrollbars + new_clip = true; + } - if (cur_clip !== new_clip) { - UI.rfb.clipViewport = new_clip; - } + if (cur_clip !== new_clip) { + UI.rfb.clipViewport = new_clip; + } - // Changing the viewport may change the state of - // the dragging button - UI.updateViewDrag(); - }, + // Changing the viewport may change the state of + // the dragging button + UI.updateViewDrag(); + }, - // Handle special cases where viewport clipping is locked - enableDisableViewClip() { - const resizeSetting = UI.getSetting('resize'); - if (isTouchDevice) { - UI.forceSetting('view_clip', true); - } else if (resizeSetting === 'scale') { - UI.disableSetting('view_clip'); - } else { - UI.enableSetting('view_clip'); - } - }, + // Handle special cases where viewport clipping is locked + enableDisableViewClip() { + const resizeSetting = UI.getSetting('resize'); + if (isTouchDevice) { + UI.forceSetting('view_clip', true); + } else if (resizeSetting === 'scale') { + UI.disableSetting('view_clip'); + } else { + UI.enableSetting('view_clip'); + } + }, -/* ------^------- + /* ------^------- * /VIEW CLIPPING * ============== * VIEWDRAG * ------v------*/ - toggleViewDrag() { - if (!UI.rfb) return; + toggleViewDrag() { + if (!UI.rfb) return; - const drag = UI.rfb.dragViewport; - UI.setViewDrag(!drag); - }, + const drag = UI.rfb.dragViewport; + UI.setViewDrag(!drag); + }, - // Set the view drag mode which moves the viewport on mouse drags - setViewDrag(drag) { - if (!UI.rfb) return; + // Set the view drag mode which moves the viewport on mouse drags + setViewDrag(drag) { + if (!UI.rfb) return; - UI.rfb.dragViewport = drag; + UI.rfb.dragViewport = drag; - UI.updateViewDrag(); - }, + UI.updateViewDrag(); + }, - updateViewDrag() { - if (!UI.connected) return; + updateViewDrag() { + if (!UI.connected) return; - const viewDragButton = document.getElementById('noVNC_view_drag_button'); + const viewDragButton = document.getElementById('noVNC_view_drag_button'); - if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { - // We are no longer clipping the viewport. Make sure - // viewport drag isn't active when it can't be used. - UI.rfb.dragViewport = false; - } + if (!UI.rfb.clipViewport && UI.rfb.dragViewport) { + // We are no longer clipping the viewport. Make sure + // viewport drag isn't active when it can't be used. + UI.rfb.dragViewport = false; + } - if (UI.rfb.dragViewport) { - viewDragButton.classList.add("noVNC_selected"); - } else { - viewDragButton.classList.remove("noVNC_selected"); - } + if (UI.rfb.dragViewport) { + viewDragButton.classList.add('noVNC_selected'); + } else { + viewDragButton.classList.remove('noVNC_selected'); + } - // Different behaviour for touch vs non-touch - // The button is disabled instead of hidden on touch devices - if (isTouchDevice) { - viewDragButton.classList.remove("noVNC_hidden"); + // Different behaviour for touch vs non-touch + // The button is disabled instead of hidden on touch devices + if (isTouchDevice) { + viewDragButton.classList.remove('noVNC_hidden'); - if (UI.rfb.clipViewport) { - viewDragButton.disabled = false; - } else { - viewDragButton.disabled = true; - } - } else { - viewDragButton.disabled = false; + if (UI.rfb.clipViewport) { + viewDragButton.disabled = false; + } else { + viewDragButton.disabled = true; + } + } else { + viewDragButton.disabled = false; - if (UI.rfb.clipViewport) { - viewDragButton.classList.remove("noVNC_hidden"); - } else { - viewDragButton.classList.add("noVNC_hidden"); - } - } - }, + if (UI.rfb.clipViewport) { + viewDragButton.classList.remove('noVNC_hidden'); + } else { + viewDragButton.classList.add('noVNC_hidden'); + } + } + }, -/* ------^------- + /* ------^------- * /VIEWDRAG * ============== * KEYBOARD * ------v------*/ - showVirtualKeyboard() { - if (!isTouchDevice) return; + showVirtualKeyboard() { + if (!isTouchDevice) return; - const input = document.getElementById('noVNC_keyboardinput'); + const input = document.getElementById('noVNC_keyboardinput'); - if (document.activeElement == input) return; + if (document.activeElement == input) return; - input.focus(); + input.focus(); - try { - const l = input.value.length; - // Move the caret to the end - input.setSelectionRange(l, l); - } catch (err) { - // setSelectionRange is undefined in Google Chrome - } - }, + try { + const l = input.value.length; + // Move the caret to the end + input.setSelectionRange(l, l); + } catch (err) { + // setSelectionRange is undefined in Google Chrome + } + }, - hideVirtualKeyboard() { - if (!isTouchDevice) return; + hideVirtualKeyboard() { + if (!isTouchDevice) return; - const input = document.getElementById('noVNC_keyboardinput'); + const input = document.getElementById('noVNC_keyboardinput'); - if (document.activeElement != input) return; + if (document.activeElement != input) return; - input.blur(); - }, + input.blur(); + }, - toggleVirtualKeyboard() { - if (document.getElementById('noVNC_keyboard_button') - .classList.contains("noVNC_selected")) { - UI.hideVirtualKeyboard(); - } else { - UI.showVirtualKeyboard(); - } - }, + toggleVirtualKeyboard() { + if (document.getElementById('noVNC_keyboard_button') + .classList.contains('noVNC_selected')) { + UI.hideVirtualKeyboard(); + } else { + UI.showVirtualKeyboard(); + } + }, - onfocusVirtualKeyboard(event) { - document.getElementById('noVNC_keyboard_button') - .classList.add("noVNC_selected"); - if (UI.rfb) { - UI.rfb.focusOnClick = false; - } - }, + onfocusVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button') + .classList.add('noVNC_selected'); + if (UI.rfb) { + UI.rfb.focusOnClick = false; + } + }, - onblurVirtualKeyboard(event) { - document.getElementById('noVNC_keyboard_button') - .classList.remove("noVNC_selected"); - if (UI.rfb) { - UI.rfb.focusOnClick = true; - } - }, + onblurVirtualKeyboard(event) { + document.getElementById('noVNC_keyboard_button') + .classList.remove('noVNC_selected'); + if (UI.rfb) { + UI.rfb.focusOnClick = true; + } + }, - keepVirtualKeyboard(event) { - const input = document.getElementById('noVNC_keyboardinput'); + keepVirtualKeyboard(event) { + const input = document.getElementById('noVNC_keyboardinput'); - // Only prevent focus change if the virtual keyboard is active - if (document.activeElement != input) { - return; - } + // Only prevent focus change if the virtual keyboard is active + if (document.activeElement != input) { + return; + } - // Only allow focus to move to other elements that need - // focus to function properly - if (event.target.form !== undefined) { - switch (event.target.type) { - case 'text': - case 'email': - case 'search': - case 'password': - case 'tel': - case 'url': - case 'textarea': - case 'select-one': - case 'select-multiple': - return; - } - } + // Only allow focus to move to other elements that need + // focus to function properly + if (event.target.form !== undefined) { + switch (event.target.type) { + case 'text': + case 'email': + case 'search': + case 'password': + case 'tel': + case 'url': + case 'textarea': + case 'select-one': + case 'select-multiple': + return; + } + } - event.preventDefault(); - }, + event.preventDefault(); + }, - keyboardinputReset() { - const kbi = document.getElementById('noVNC_keyboardinput'); - kbi.value = new Array(UI.defaultKeyboardinputLen).join("_"); - UI.lastKeyboardinput = kbi.value; - }, + keyboardinputReset() { + const kbi = document.getElementById('noVNC_keyboardinput'); + kbi.value = new Array(UI.defaultKeyboardinputLen).join('_'); + UI.lastKeyboardinput = kbi.value; + }, - keyEvent(keysym, code, down) { - if (!UI.rfb) return; + keyEvent(keysym, code, down) { + if (!UI.rfb) return; - UI.rfb.sendKey(keysym, code, down); - }, + UI.rfb.sendKey(keysym, code, down); + }, - // When normal keyboard events are left uncought, use the input events from - // the keyboardinput element instead and generate the corresponding key events. - // This code is required since some browsers on Android are inconsistent in - // sending keyCodes in the normal keyboard events when using on screen keyboards. - keyInput(event) { + // When normal keyboard events are left uncought, use the input events from + // the keyboardinput element instead and generate the corresponding key events. + // This code is required since some browsers on Android are inconsistent in + // sending keyCodes in the normal keyboard events when using on screen keyboards. + keyInput(event) { + if (!UI.rfb) return; - if (!UI.rfb) return; + const newValue = event.target.value; - const newValue = event.target.value; + if (!UI.lastKeyboardinput) { + UI.keyboardinputReset(); + } + const oldValue = UI.lastKeyboardinput; - if (!UI.lastKeyboardinput) { - UI.keyboardinputReset(); - } - const oldValue = UI.lastKeyboardinput; + let newLen; + try { + // Try to check caret position since whitespace at the end + // will not be considered by value.length in some browsers + newLen = Math.max(event.target.selectionStart, newValue.length); + } catch (err) { + // selectionStart is undefined in Google Chrome + newLen = newValue.length; + } + const oldLen = oldValue.length; - let newLen; - try { - // Try to check caret position since whitespace at the end - // will not be considered by value.length in some browsers - newLen = Math.max(event.target.selectionStart, newValue.length); - } catch (err) { - // selectionStart is undefined in Google Chrome - newLen = newValue.length; - } - const oldLen = oldValue.length; + let inputs = newLen - oldLen; + let backspaces = inputs < 0 ? -inputs : 0; - let inputs = newLen - oldLen; - let backspaces = inputs < 0 ? -inputs : 0; + // Compare the old string with the new to account for + // text-corrections or other input that modify existing text + for (let i = 0; i < Math.min(oldLen, newLen); i++) { + if (newValue.charAt(i) != oldValue.charAt(i)) { + inputs = newLen - i; + backspaces = oldLen - i; + break; + } + } - // Compare the old string with the new to account for - // text-corrections or other input that modify existing text - for (let i = 0; i < Math.min(oldLen, newLen); i++) { - if (newValue.charAt(i) != oldValue.charAt(i)) { - inputs = newLen - i; - backspaces = oldLen - i; - break; - } - } + // Send the key events + for (let i = 0; i < backspaces; i++) { + UI.rfb.sendKey(KeyTable.XK_BackSpace, 'Backspace'); + } + for (let i = newLen - inputs; i < newLen; i++) { + UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i))); + } - // Send the key events - for (let i = 0; i < backspaces; i++) { - UI.rfb.sendKey(KeyTable.XK_BackSpace, "Backspace"); - } - for (let i = newLen - inputs; i < newLen; i++) { - UI.rfb.sendKey(keysyms.lookup(newValue.charCodeAt(i))); - } + // Control the text content length in the keyboardinput element + if (newLen > 2 * UI.defaultKeyboardinputLen) { + UI.keyboardinputReset(); + } else if (newLen < 1) { + // There always have to be some text in the keyboardinput + // element with which backspace can interact. + UI.keyboardinputReset(); + // This sometimes causes the keyboard to disappear for a second + // but it is required for the android keyboard to recognize that + // text has been added to the field + event.target.blur(); + // This has to be ran outside of the input handler in order to work + setTimeout(event.target.focus.bind(event.target), 0); + } else { + UI.lastKeyboardinput = newValue; + } + }, - // Control the text content length in the keyboardinput element - if (newLen > 2 * UI.defaultKeyboardinputLen) { - UI.keyboardinputReset(); - } else if (newLen < 1) { - // There always have to be some text in the keyboardinput - // element with which backspace can interact. - UI.keyboardinputReset(); - // This sometimes causes the keyboard to disappear for a second - // but it is required for the android keyboard to recognize that - // text has been added to the field - event.target.blur(); - // This has to be ran outside of the input handler in order to work - setTimeout(event.target.focus.bind(event.target), 0); - } else { - UI.lastKeyboardinput = newValue; - } - }, - -/* ------^------- + /* ------^------- * /KEYBOARD * ============== * EXTRA KEYS * ------v------*/ - openExtraKeys() { - UI.closeAllPanels(); - UI.openControlbar(); + openExtraKeys() { + UI.closeAllPanels(); + UI.openControlbar(); - document.getElementById('noVNC_modifiers') - .classList.add("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.add("noVNC_selected"); - }, + document.getElementById('noVNC_modifiers') + .classList.add('noVNC_open'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add('noVNC_selected'); + }, - closeExtraKeys() { - document.getElementById('noVNC_modifiers') - .classList.remove("noVNC_open"); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.remove("noVNC_selected"); - }, + closeExtraKeys() { + document.getElementById('noVNC_modifiers') + .classList.remove('noVNC_open'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove('noVNC_selected'); + }, - toggleExtraKeys() { - if(document.getElementById('noVNC_modifiers') - .classList.contains("noVNC_open")) { - UI.closeExtraKeys(); - } else { - UI.openExtraKeys(); - } - }, + toggleExtraKeys() { + if (document.getElementById('noVNC_modifiers') + .classList.contains('noVNC_open')) { + UI.closeExtraKeys(); + } else { + UI.openExtraKeys(); + } + }, - sendEsc() { - UI.rfb.sendKey(KeyTable.XK_Escape, "Escape"); - }, + sendEsc() { + UI.rfb.sendKey(KeyTable.XK_Escape, 'Escape'); + }, - sendTab() { - UI.rfb.sendKey(KeyTable.XK_Tab); - }, + sendTab() { + UI.rfb.sendKey(KeyTable.XK_Tab); + }, - toggleCtrl() { - const btn = document.getElementById('noVNC_toggle_ctrl_button'); - if (btn.classList.contains("noVNC_selected")) { - UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); - btn.classList.remove("noVNC_selected"); - } else { - UI.rfb.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); - btn.classList.add("noVNC_selected"); - } - }, + toggleCtrl() { + const btn = document.getElementById('noVNC_toggle_ctrl_button'); + if (btn.classList.contains('noVNC_selected')) { + UI.rfb.sendKey(KeyTable.XK_Control_L, 'ControlLeft', false); + btn.classList.remove('noVNC_selected'); + } else { + UI.rfb.sendKey(KeyTable.XK_Control_L, 'ControlLeft', true); + btn.classList.add('noVNC_selected'); + } + }, - toggleAlt() { - const btn = document.getElementById('noVNC_toggle_alt_button'); - if (btn.classList.contains("noVNC_selected")) { - UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); - btn.classList.remove("noVNC_selected"); - } else { - UI.rfb.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); - btn.classList.add("noVNC_selected"); - } - }, + toggleAlt() { + const btn = document.getElementById('noVNC_toggle_alt_button'); + if (btn.classList.contains('noVNC_selected')) { + UI.rfb.sendKey(KeyTable.XK_Alt_L, 'AltLeft', false); + btn.classList.remove('noVNC_selected'); + } else { + UI.rfb.sendKey(KeyTable.XK_Alt_L, 'AltLeft', true); + btn.classList.add('noVNC_selected'); + } + }, - sendCtrlAltDel() { - UI.rfb.sendCtrlAltDel(); - }, + sendCtrlAltDel() { + UI.rfb.sendCtrlAltDel(); + }, -/* ------^------- + /* ------^------- * /EXTRA KEYS * ============== * MISC * ------v------*/ - setMouseButton(num) { - const view_only = UI.rfb.viewOnly; - if (UI.rfb && !view_only) { - UI.rfb.touchButton = num; - } + setMouseButton(num) { + const view_only = UI.rfb.viewOnly; + if (UI.rfb && !view_only) { + UI.rfb.touchButton = num; + } - const blist = [0, 1,2,4]; - for (let b = 0; b < blist.length; b++) { - const button = document.getElementById('noVNC_mouse_button' + - blist[b]); - if (blist[b] === num && !view_only) { - button.classList.remove("noVNC_hidden"); - } else { - button.classList.add("noVNC_hidden"); - } - } - }, + const blist = [0, 1, 2, 4]; + for (let b = 0; b < blist.length; b++) { + const button = document.getElementById('noVNC_mouse_button' + + blist[b]); + if (blist[b] === num && !view_only) { + button.classList.remove('noVNC_hidden'); + } else { + button.classList.add('noVNC_hidden'); + } + } + }, - updateViewOnly() { - if (!UI.rfb) return; - UI.rfb.viewOnly = UI.getSetting('view_only'); + updateViewOnly() { + if (!UI.rfb) return; + UI.rfb.viewOnly = UI.getSetting('view_only'); - // Hide input related buttons in view only mode - if (UI.rfb.viewOnly) { - document.getElementById('noVNC_keyboard_button') - .classList.add('noVNC_hidden'); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.add('noVNC_hidden'); - } else { - document.getElementById('noVNC_keyboard_button') - .classList.remove('noVNC_hidden'); - document.getElementById('noVNC_toggle_extra_keys_button') - .classList.remove('noVNC_hidden'); - } - UI.setMouseButton(1); //has it's own logic for hiding/showing - }, + // Hide input related buttons in view only mode + if (UI.rfb.viewOnly) { + document.getElementById('noVNC_keyboard_button') + .classList.add('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.add('noVNC_hidden'); + } else { + document.getElementById('noVNC_keyboard_button') + .classList.remove('noVNC_hidden'); + document.getElementById('noVNC_toggle_extra_keys_button') + .classList.remove('noVNC_hidden'); + } + UI.setMouseButton(1); // has it's own logic for hiding/showing + }, - updateLogging() { - WebUtil.init_logging(UI.getSetting('logging')); - }, + updateLogging() { + WebUtil.init_logging(UI.getSetting('logging')); + }, - updateDesktopName(e) { - UI.desktopName = e.detail.name; - // Display the desktop name in the document title - document.title = e.detail.name + " - noVNC"; - }, + updateDesktopName(e) { + UI.desktopName = e.detail.name; + // Display the desktop name in the document title + document.title = e.detail.name + ' - noVNC'; + }, - bell(e) { - if (WebUtil.getConfigVar('bell', 'on') === 'on') { - const promise = document.getElementById('noVNC_bell').play(); - // The standards disagree on the return value here - if (promise) { - promise.catch((e) => { - if (e.name === "NotAllowedError") { - // Ignore when the browser doesn't let us play audio. - // It is common that the browsers require audio to be - // initiated from a user action. - } else { - Log.Error("Unable to play bell: " + e); - } - }); - } - } - }, + bell(e) { + if (WebUtil.getConfigVar('bell', 'on') === 'on') { + const promise = document.getElementById('noVNC_bell').play(); + // The standards disagree on the return value here + if (promise) { + promise.catch((e) => { + if (e.name === 'NotAllowedError') { + // Ignore when the browser doesn't let us play audio. + // It is common that the browsers require audio to be + // initiated from a user action. + } else { + Log.Error('Unable to play bell: ' + e); + } + }); + } + } + }, - //Helper to add options to dropdown. - addOption(selectbox, text, value) { - const optn = document.createElement("OPTION"); - optn.text = text; - optn.value = value; - selectbox.options.add(optn); - }, + // Helper to add options to dropdown. + addOption(selectbox, text, value) { + const optn = document.createElement('OPTION'); + optn.text = text; + optn.value = value; + selectbox.options.add(optn); + }, /* ------^------- * /MISC @@ -1650,20 +1642,20 @@ const UI = { }; // Set up translations -const LINGUAS = ["de", "el", "es", "nl", "pl", "sv", "tr", "zh_CN", "zh_TW"]; +const LINGUAS = ['de', 'el', 'es', 'nl', 'pl', 'sv', 'tr', 'zh_CN', 'zh_TW']; l10n.setup(LINGUAS); -if (l10n.language !== "en" && l10n.dictionary === undefined) { - WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', (translations) => { - l10n.dictionary = translations; +if (l10n.language !== 'en' && l10n.dictionary === undefined) { + WebUtil.fetchJSON('app/locale/' + l10n.language + '.json', (translations) => { + l10n.dictionary = translations; - // wait for translations to load before loading the UI - UI.prime(); - }, (err) => { - Log.Error("Failed to load translations: " + err); - UI.prime(); - }); -} else { + // wait for translations to load before loading the UI UI.prime(); + }, (err) => { + Log.Error('Failed to load translations: ' + err); + UI.prime(); + }); +} else { + UI.prime(); } export default UI; diff --git a/app/webutil.js b/app/webutil.js index 33d92922..377c53d3 100644 --- a/app/webutil.js +++ b/app/webutil.js @@ -10,55 +10,63 @@ import { init_logging as main_init_logging } from '../core/util/logging.js'; // init log level reading the logging HTTP param -export function init_logging (level) { - "use strict"; - if (typeof level !== "undefined") { - main_init_logging(level); - } else { - const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); - main_init_logging(param || undefined); - } +export function init_logging(level) { + 'use strict'; + + if (typeof level !== 'undefined') { + main_init_logging(level); + } else { + const param = document.location.href.match(/logging=([A-Za-z0-9._-]*)/); + main_init_logging(param || undefined); + } } // Read a query string variable -export function getQueryVar (name, defVal) { - "use strict"; - const re = new RegExp('.*[?&]' + name + '=([^&#]*)'), - match = document.location.href.match(re); - if (typeof defVal === 'undefined') { defVal = null; } - - if (match) { - return decodeURIComponent(match[1]); - } +export function getQueryVar(name, defVal) { + 'use strict'; - return defVal; + const re = new RegExp('.*[?&]' + name + '=([^&#]*)'); + + + const match = document.location.href.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + + if (match) { + return decodeURIComponent(match[1]); + } + + return defVal; } // Read a hash fragment variable -export function getHashVar (name, defVal) { - "use strict"; - const re = new RegExp('.*[&#]' + name + '=([^&]*)'), - match = document.location.hash.match(re); - if (typeof defVal === 'undefined') { defVal = null; } +export function getHashVar(name, defVal) { + 'use strict'; - if (match) { - return decodeURIComponent(match[1]); - } + const re = new RegExp('.*[&#]' + name + '=([^&]*)'); - return defVal; + + const match = document.location.hash.match(re); + if (typeof defVal === 'undefined') { defVal = null; } + + if (match) { + return decodeURIComponent(match[1]); + } + + return defVal; } // Read a variable from the fragment or the query string // Fragment takes precedence -export function getConfigVar (name, defVal) { - "use strict"; - const val = getHashVar(name); +export function getConfigVar(name, defVal) { + 'use strict'; - if (val === null) { - return getQueryVar(name, defVal); - } + const val = getHashVar(name); - return val; + if (val === null) { + return getQueryVar(name, defVal); + } + + return val; } /* @@ -66,47 +74,51 @@ export function getConfigVar (name, defVal) { */ // No days means only for this browser session -export function createCookie (name, value, days) { - "use strict"; - let date, expires; - if (days) { - date = new Date(); - date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); - expires = "; expires=" + date.toGMTString(); - } else { - expires = ""; - } +export function createCookie(name, value, days) { + 'use strict'; - let secure; - if (document.location.protocol === "https:") { - secure = "; secure"; - } else { - secure = ""; - } - document.cookie = name + "=" + value + expires + "; path=/" + secure; + let date; let + expires; + if (days) { + date = new Date(); + date.setTime(date.getTime() + (days * 24 * 60 * 60 * 1000)); + expires = '; expires=' + date.toGMTString(); + } else { + expires = ''; + } + + let secure; + if (document.location.protocol === 'https:') { + secure = '; secure'; + } else { + secure = ''; + } + document.cookie = name + '=' + value + expires + '; path=/' + secure; } -export function readCookie (name, defaultValue) { - "use strict"; - const nameEQ = name + "="; - const ca = document.cookie.split(';'); +export function readCookie(name, defaultValue) { + 'use strict'; - for (let i = 0; i < ca.length; i += 1) { - let c = ca[i]; - while (c.charAt(0) === ' ') { - c = c.substring(1, c.length); - } - if (c.indexOf(nameEQ) === 0) { - return c.substring(nameEQ.length, c.length); - } + const nameEQ = name + '='; + const ca = document.cookie.split(';'); + + for (let i = 0; i < ca.length; i += 1) { + let c = ca[i]; + while (c.charAt(0) === ' ') { + c = c.substring(1, c.length); } + if (c.indexOf(nameEQ) === 0) { + return c.substring(nameEQ.length, c.length); + } + } - return (typeof defaultValue !== 'undefined') ? defaultValue : null; + return (typeof defaultValue !== 'undefined') ? defaultValue : null; } -export function eraseCookie (name) { - "use strict"; - createCookie(name, "", -1); +export function eraseCookie(name) { + 'use strict'; + + createCookie(name, '', -1); } /* @@ -115,104 +127,108 @@ export function eraseCookie (name) { let settings = {}; -export function initSettings (callback /*, ...callbackArgs */) { - "use strict"; - const callbackArgs = Array.prototype.slice.call(arguments, 1); - if (window.chrome && window.chrome.storage) { - window.chrome.storage.sync.get((cfg) => { - settings = cfg; - if (callback) { - callback.apply(this, callbackArgs); - } - }); - } else { - settings = {}; - if (callback) { - callback.apply(this, callbackArgs); - } +export function initSettings(callback /* , ...callbackArgs */) { + 'use strict'; + + const callbackArgs = Array.prototype.slice.call(arguments, 1); + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.get((cfg) => { + settings = cfg; + if (callback) { + callback.apply(this, callbackArgs); + } + }); + } else { + settings = {}; + if (callback) { + callback.apply(this, callbackArgs); } + } } // Update the settings cache, but do not write to permanent storage -export function setSetting (name, value) { - settings[name] = value; +export function setSetting(name, value) { + settings[name] = value; } // No days means only for this browser session -export function writeSetting (name, value) { - "use strict"; - if (settings[name] === value) return; +export function writeSetting(name, value) { + 'use strict'; + + if (settings[name] === value) return; + settings[name] = value; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.set(settings); + } else { + localStorage.setItem(name, value); + } +} + +export function readSetting(name, defaultValue) { + 'use strict'; + + let value; + if ((name in settings) || (window.chrome && window.chrome.storage)) { + value = settings[name]; + } else { + value = localStorage.getItem(name); settings[name] = value; - if (window.chrome && window.chrome.storage) { - window.chrome.storage.sync.set(settings); - } else { - localStorage.setItem(name, value); - } + } + if (typeof value === 'undefined') { + value = null; + } + + if (value === null && typeof defaultValue !== 'undefined') { + return defaultValue; + } + + return value; } -export function readSetting (name, defaultValue) { - "use strict"; - let value; - if ((name in settings) || (window.chrome && window.chrome.storage)) { - value = settings[name]; - } else { - value = localStorage.getItem(name); - settings[name] = value; - } - if (typeof value === "undefined") { - value = null; - } +export function eraseSetting(name) { + 'use strict'; - if (value === null && typeof defaultValue !== "undefined") { - return defaultValue; - } - - return value; + // Deleting here means that next time the setting is read when using local + // storage, it will be pulled from local storage again. + // If the setting in local storage is changed (e.g. in another tab) + // between this delete and the next read, it could lead to an unexpected + // value change. + delete settings[name]; + if (window.chrome && window.chrome.storage) { + window.chrome.storage.sync.remove(name); + } else { + localStorage.removeItem(name); + } } -export function eraseSetting (name) { - "use strict"; - // Deleting here means that next time the setting is read when using local - // storage, it will be pulled from local storage again. - // If the setting in local storage is changed (e.g. in another tab) - // between this delete and the next read, it could lead to an unexpected - // value change. - delete settings[name]; - if (window.chrome && window.chrome.storage) { - window.chrome.storage.sync.remove(name); - } else { - localStorage.removeItem(name); - } -} +export function injectParamIfMissing(path, param, value) { + // force pretend that we're dealing with a relative path + // (assume that we wanted an extra if we pass one in) + path = '/' + path; -export function injectParamIfMissing (path, param, value) { - // force pretend that we're dealing with a relative path - // (assume that we wanted an extra if we pass one in) - path = "/" + path; + const elem = document.createElement('a'); + elem.href = path; - const elem = document.createElement('a'); - elem.href = path; + const param_eq = encodeURIComponent(param) + '='; + let query; + if (elem.search) { + query = elem.search.slice(1).split('&'); + } else { + query = []; + } - const param_eq = encodeURIComponent(param) + "="; - let query; - if (elem.search) { - query = elem.search.slice(1).split('&'); - } else { - query = []; - } + if (!query.some(v => v.startsWith(param_eq))) { + query.push(param_eq + encodeURIComponent(value)); + elem.search = '?' + query.join('&'); + } - if (!query.some(v => v.startsWith(param_eq))) { - query.push(param_eq + encodeURIComponent(value)); - elem.search = "?" + query.join("&"); - } + // some browsers (e.g. IE11) may occasionally omit the leading slash + // in the elem.pathname string. Handle that case gracefully. + if (elem.pathname.charAt(0) == '/') { + return elem.pathname.slice(1) + elem.search + elem.hash; + } - // some browsers (e.g. IE11) may occasionally omit the leading slash - // in the elem.pathname string. Handle that case gracefully. - if (elem.pathname.charAt(0) == "/") { - return elem.pathname.slice(1) + elem.search + elem.hash; - } - - return elem.pathname + elem.search + elem.hash; + return elem.pathname + elem.search + elem.hash; } // sadly, we can't use the Fetch API until we decide to drop @@ -220,27 +236,27 @@ export function injectParamIfMissing (path, param, value) { // resolve will receive an object on success, while reject // will receive either an event or an error on failure. export function fetchJSON(path, resolve, reject) { - // NB: IE11 doesn't support JSON as a responseType - const req = new XMLHttpRequest(); - req.open('GET', path); + // NB: IE11 doesn't support JSON as a responseType + const req = new XMLHttpRequest(); + req.open('GET', path); - req.onload = () => { - if (req.status === 200) { - let resObj; - try { - resObj = JSON.parse(req.responseText); - } catch (err) { - reject(err); - } - resolve(resObj); - } else { - reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status)); - } - }; + req.onload = () => { + if (req.status === 200) { + let resObj; + try { + resObj = JSON.parse(req.responseText); + } catch (err) { + reject(err); + } + resolve(resObj); + } else { + reject(new Error("XHR got non-200 status while trying to load '" + path + "': " + req.status)); + } + }; - req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message)); + req.onerror = evt => reject(new Error("XHR encountered an error while trying to load '" + path + "': " + evt.message)); - req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'")); + req.ontimeout = evt => reject(new Error("XHR timed out while trying to load '" + path + "'")); - req.send(); + req.send(); } diff --git a/core/base64.js b/core/base64.js index 895aa464..422fe51d 100644 --- a/core/base64.js +++ b/core/base64.js @@ -7,98 +7,99 @@ import * as Log from './util/logging.js'; export default { - /* Convert data (an array of integers) to a Base64 string. */ - toBase64Table : 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), - base64Pad : '=', + /* Convert data (an array of integers) to a Base64 string. */ + toBase64Table: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='.split(''), + base64Pad: '=', - encode(data) { - "use strict"; - let result = ''; - const length = data.length; - const lengthpad = (length % 3); - // Convert every three bytes to 4 ascii characters. + encode(data) { + 'use strict'; - for (let i = 0; i < (length - 2); i += 3) { - result += this.toBase64Table[data[i] >> 2]; - result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; - result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; - result += this.toBase64Table[data[i + 2] & 0x3f]; - } + let result = ''; + const length = data.length; + const lengthpad = (length % 3); + // Convert every three bytes to 4 ascii characters. - // Convert the remaining 1 or 2 bytes, pad out to 4 characters. - const j = length - lengthpad; - if (lengthpad === 2) { - result += this.toBase64Table[data[j] >> 2]; - result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; - result += this.toBase64Table[(data[j + 1] & 0x0f) << 2]; - result += this.toBase64Table[64]; - } else if (lengthpad === 1) { - result += this.toBase64Table[data[j] >> 2]; - result += this.toBase64Table[(data[j] & 0x03) << 4]; - result += this.toBase64Table[64]; - result += this.toBase64Table[64]; - } - - return result; - }, - - /* Convert Base64 data to a string */ - toBinaryTable : [ - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,-1, - -1,-1,-1,-1, -1,-1,-1,-1, -1,-1,-1,62, -1,-1,-1,63, - 52,53,54,55, 56,57,58,59, 60,61,-1,-1, -1, 0,-1,-1, - -1, 0, 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,-1, -1,-1,-1,-1, - -1,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,-1, -1,-1,-1,-1 - ], - - decode(data, offset) { - offset = typeof(offset) !== 'undefined' ? offset : 0; - - let data_length = data.indexOf('=') - offset; - if (data_length < 0) { data_length = data.length - offset; } - - /* Every four characters is 3 resulting numbers */ - const result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); - const result = new Array(result_length); - - // Convert one by one. - - let leftbits = 0; // number of bits decoded, but yet to be appended - let leftdata = 0; // bits decoded, but yet to be appended - for (let idx = 0, i = offset; i < data.length; i++) { - const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f]; - const padding = (data.charAt(i) === this.base64Pad); - // Skip illegal characters and whitespace - if (c === -1) { - Log.Error("Illegal character code " + data.charCodeAt(i) + " at position " + i); - continue; - } - - // Collect data into leftdata, update bitcount - leftdata = (leftdata << 6) | c; - leftbits += 6; - - // If we have 8 or more bits, append 8 bits to the result - if (leftbits >= 8) { - leftbits -= 8; - // Append if not padding. - if (!padding) { - result[idx++] = (leftdata >> leftbits) & 0xff; - } - leftdata &= (1 << leftbits) - 1; - } - } - - // If there are any bits left, the base64 string was corrupted - if (leftbits) { - const err = new Error('Corrupted base64 string'); - err.name = 'Base64-Error'; - throw err; - } - - return result; + for (let i = 0; i < (length - 2); i += 3) { + result += this.toBase64Table[data[i] >> 2]; + result += this.toBase64Table[((data[i] & 0x03) << 4) + (data[i + 1] >> 4)]; + result += this.toBase64Table[((data[i + 1] & 0x0f) << 2) + (data[i + 2] >> 6)]; + result += this.toBase64Table[data[i + 2] & 0x3f]; } + + // Convert the remaining 1 or 2 bytes, pad out to 4 characters. + const j = length - lengthpad; + if (lengthpad === 2) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[((data[j] & 0x03) << 4) + (data[j + 1] >> 4)]; + result += this.toBase64Table[(data[j + 1] & 0x0f) << 2]; + result += this.toBase64Table[64]; + } else if (lengthpad === 1) { + result += this.toBase64Table[data[j] >> 2]; + result += this.toBase64Table[(data[j] & 0x03) << 4]; + result += this.toBase64Table[64]; + result += this.toBase64Table[64]; + } + + return result; + }, + + /* Convert Base64 data to a string */ + toBinaryTable: [ + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 62, -1, -1, -1, 63, + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, -1, -1, -1, 0, -1, -1, + -1, 0, 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, -1, -1, -1, -1, -1, + -1, 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, -1, -1, -1, -1, -1 + ], + + decode(data, offset) { + offset = typeof (offset) !== 'undefined' ? offset : 0; + + let data_length = data.indexOf('=') - offset; + if (data_length < 0) { data_length = data.length - offset; } + + /* Every four characters is 3 resulting numbers */ + const result_length = (data_length >> 2) * 3 + Math.floor((data_length % 4) / 1.5); + const result = new Array(result_length); + + // Convert one by one. + + let leftbits = 0; // number of bits decoded, but yet to be appended + let leftdata = 0; // bits decoded, but yet to be appended + for (let idx = 0, i = offset; i < data.length; i++) { + const c = this.toBinaryTable[data.charCodeAt(i) & 0x7f]; + const padding = (data.charAt(i) === this.base64Pad); + // Skip illegal characters and whitespace + if (c === -1) { + Log.Error('Illegal character code ' + data.charCodeAt(i) + ' at position ' + i); + continue; + } + + // Collect data into leftdata, update bitcount + leftdata = (leftdata << 6) | c; + leftbits += 6; + + // If we have 8 or more bits, append 8 bits to the result + if (leftbits >= 8) { + leftbits -= 8; + // Append if not padding. + if (!padding) { + result[idx++] = (leftdata >> leftbits) & 0xff; + } + leftdata &= (1 << leftbits) - 1; + } + } + + // If there are any bits left, the base64 string was corrupted + if (leftbits) { + const err = new Error('Corrupted base64 string'); + err.name = 'Base64-Error'; + throw err; + } + + return result; + } }; /* End of Base64 namespace */ diff --git a/core/des.js b/core/des.js index 5adc7ae9..653f0808 100644 --- a/core/des.js +++ b/core/des.js @@ -76,192 +76,203 @@ */ export default function DES(passwd) { - "use strict"; + 'use strict'; - // Tables, permutations, S-boxes, etc. - const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, - 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, - 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31 ], - totrot = [ 1, 2, 4, 6, 8,10,12,14,15,17,19,21,23,25,27,28], - z = 0x0, - keys = []; - let a,b,c,d,e,f; + /* eslint-disable indent, comma-spacing, space-infix-ops */ + // Tables, permutations, S-boxes, etc. + const PC2 = [13,16,10,23, 0, 4, 2,27,14, 5,20, 9,22,18,11, 3, + 25, 7,15, 6,26,19,12, 1,40,51,30,36,46,54,29,39, + 50,44,32,47,43,48,38,55,33,52,45,41,49,35,28,31]; + const totrot = [1, 2, 4, 6, 8, 10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28]; - a=1<<16; b=1<<24; c=a|b; d=1<<2; e=1<<10; f=d|e; - const SP1 = [c|e,z|z,a|z,c|f,c|d,a|f,z|d,a|z,z|e,c|e,c|f,z|e,b|f,c|d,b|z,z|d, - z|f,b|e,b|e,a|e,a|e,c|z,c|z,b|f,a|d,b|d,b|d,a|d,z|z,z|f,a|f,b|z, - a|z,c|f,z|d,c|z,c|e,b|z,b|z,z|e,c|d,a|z,a|e,b|d,z|e,z|d,b|f,a|f, - c|f,a|d,c|z,b|f,b|d,z|f,a|f,c|e,z|f,b|e,b|e,z|z,a|d,a|e,z|z,c|d]; - a=1<<20; b=1<<31; c=a|b; d=1<<5; e=1<<15; f=d|e; - const SP2 = [c|f,b|e,z|e,a|f,a|z,z|d,c|d,b|f,b|d,c|f,c|e,b|z,b|e,a|z,z|d,c|d, - a|e,a|d,b|f,z|z,b|z,z|e,a|f,c|z,a|d,b|d,z|z,a|e,z|f,c|e,c|z,z|f, - z|z,a|f,c|d,a|z,b|f,c|z,c|e,z|e,c|z,b|e,z|d,c|f,a|f,z|d,z|e,b|z, - z|f,c|e,a|z,b|d,a|d,b|f,b|d,a|d,a|e,z|z,b|e,z|f,b|z,c|d,c|f,a|e]; - a=1<<17; b=1<<27; c=a|b; d=1<<3; e=1<<9; f=d|e; - const SP3 = [z|f,c|e,z|z,c|d,b|e,z|z,a|f,b|e,a|d,b|d,b|d,a|z,c|f,a|d,c|z,z|f, - b|z,z|d,c|e,z|e,a|e,c|z,c|d,a|f,b|f,a|e,a|z,b|f,z|d,c|f,z|e,b|z, - c|e,b|z,a|d,z|f,a|z,c|e,b|e,z|z,z|e,a|d,c|f,b|e,b|d,z|e,z|z,c|d, - b|f,a|z,b|z,c|f,z|d,a|f,a|e,b|d,c|z,b|f,z|f,c|z,a|f,z|d,c|d,a|e]; - a=1<<13; b=1<<23; c=a|b; d=1<<0; e=1<<7; f=d|e; - const SP4 = [c|d,a|f,a|f,z|e,c|e,b|f,b|d,a|d,z|z,c|z,c|z,c|f,z|f,z|z,b|e,b|d, - z|d,a|z,b|z,c|d,z|e,b|z,a|d,a|e,b|f,z|d,a|e,b|e,a|z,c|e,c|f,z|f, - b|e,b|d,c|z,c|f,z|f,z|z,z|z,c|z,a|e,b|e,b|f,z|d,c|d,a|f,a|f,z|e, - c|f,z|f,z|d,a|z,b|d,a|d,c|e,b|f,a|d,a|e,b|z,c|d,z|e,b|z,a|z,c|e]; - a=1<<25; b=1<<30; c=a|b; d=1<<8; e=1<<19; f=d|e; - const SP5 = [z|d,a|f,a|e,c|d,z|e,z|d,b|z,a|e,b|f,z|e,a|d,b|f,c|d,c|e,z|f,b|z, - a|z,b|e,b|e,z|z,b|d,c|f,c|f,a|d,c|e,b|d,z|z,c|z,a|f,a|z,c|z,z|f, - z|e,c|d,z|d,a|z,b|z,a|e,c|d,b|f,a|d,b|z,c|e,a|f,b|f,z|d,a|z,c|e, - c|f,z|f,c|z,c|f,a|e,z|z,b|e,c|z,z|f,a|d,b|d,z|e,z|z,b|e,a|f,b|d]; - a=1<<22; b=1<<29; c=a|b; d=1<<4; e=1<<14; f=d|e; - const SP6 = [b|d,c|z,z|e,c|f,c|z,z|d,c|f,a|z,b|e,a|f,a|z,b|d,a|d,b|e,b|z,z|f, - z|z,a|d,b|f,z|e,a|e,b|f,z|d,c|d,c|d,z|z,a|f,c|e,z|f,a|e,c|e,b|z, - b|e,z|d,c|d,a|e,c|f,a|z,z|f,b|d,a|z,b|e,b|z,z|f,b|d,c|f,a|e,c|z, - a|f,c|e,z|z,c|d,z|d,z|e,c|z,a|f,z|e,a|d,b|f,z|z,c|e,b|z,a|d,b|f]; - a=1<<21; b=1<<26; c=a|b; d=1<<1; e=1<<11; f=d|e; - const SP7 = [a|z,c|d,b|f,z|z,z|e,b|f,a|f,c|e,c|f,a|z,z|z,b|d,z|d,b|z,c|d,z|f, - b|e,a|f,a|d,b|e,b|d,c|z,c|e,a|d,c|z,z|e,z|f,c|f,a|e,z|d,b|z,a|e, - b|z,a|e,a|z,b|f,b|f,c|d,c|d,z|d,a|d,b|z,b|e,a|z,c|e,z|f,a|f,c|e, - z|f,b|d,c|f,c|z,a|e,z|z,z|d,c|f,z|z,a|f,c|z,z|e,b|d,b|e,z|e,a|d]; - a=1<<18; b=1<<28; c=a|b; d=1<<6; e=1<<12; f=d|e; - const SP8 = [b|f,z|e,a|z,c|f,b|z,b|f,z|d,b|z,a|d,c|z,c|f,a|e,c|e,a|f,z|e,z|d, - c|z,b|d,b|e,z|f,a|e,a|d,c|d,c|e,z|f,z|z,z|z,c|d,b|d,b|e,a|f,a|z, - a|f,a|z,c|e,z|e,z|d,c|d,z|e,a|f,b|e,z|d,b|d,c|z,c|d,b|z,a|z,b|f, - z|z,c|f,a|d,b|d,c|z,b|e,b|f,z|z,c|f,a|e,a|e,z|f,z|f,a|d,b|z,c|e]; + const z = 0x0; - // Set the key. - function setKeys(keyBlock) { - const pc1m = [], pcr = [], kn = []; + const keys = []; + let a; + let b; + let c; + let d; + let e; + let f; - for (let j = 0, l = 56; j < 56; ++j, l -= 8) { - l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 - const m = l & 0x7; - pc1m[j] = ((keyBlock[l >>> 3] & (1<>> 10; - keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; - ++KnLi; - keys[KnLi] = (raw0 & 0x0003f000) << 12; - keys[KnLi] |= (raw0 & 0x0000003f) << 16; - keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; - keys[KnLi] |= (raw1 & 0x0000003f); - ++KnLi; - } + // Set the key. + function setKeys(keyBlock) { + const pc1m = []; const pcr = []; const + kn = []; + + for (let j = 0, l = 56; j < 56; ++j, l -= 8) { + l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + const m = l & 0x7; + pc1m[j] = ((keyBlock[l >>> 3] & (1 << m)) !== 0) ? 1 : 0; } - // Encrypt 8 bytes of text - function enc8(text) { - const b = text.slice(); - let i = 0, l, r, x; // left, right, accumulator - - // Squash 8 bytes to 2 ints - l = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - r = b[i++]<<24 | b[i++]<<16 | b[i++]<<8 | b[i++]; - - x = ((l >>> 4) ^ r) & 0x0f0f0f0f; - r ^= x; - l ^= (x << 4); - x = ((l >>> 16) ^ r) & 0x0000ffff; - r ^= x; - l ^= (x << 16); - x = ((r >>> 2) ^ l) & 0x33333333; - l ^= x; - r ^= (x << 2); - x = ((r >>> 8) ^ l) & 0x00ff00ff; - l ^= x; - r ^= (x << 8); - r = (r << 1) | ((r >>> 31) & 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 1) | ((l >>> 31) & 1); - - for (let i = 0, keysi = 0; i < 8; ++i) { - x = (r << 28) | (r >>> 4); - x ^= keys[keysi++]; - let fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = r ^ keys[keysi++]; - fval |= SP8[x & 0x3f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - l ^= fval; - x = (l << 28) | (l >>> 4); - x ^= keys[keysi++]; - fval = SP7[x & 0x3f]; - fval |= SP5[(x >>> 8) & 0x3f]; - fval |= SP3[(x >>> 16) & 0x3f]; - fval |= SP1[(x >>> 24) & 0x3f]; - x = l ^ keys[keysi++]; - fval |= SP8[x & 0x0000003f]; - fval |= SP6[(x >>> 8) & 0x3f]; - fval |= SP4[(x >>> 16) & 0x3f]; - fval |= SP2[(x >>> 24) & 0x3f]; - r ^= fval; + for (let i = 0; i < 16; ++i) { + const m = i << 1; + const n = m + 1; + kn[m] = kn[n] = 0; + for (let o = 28; o < 59; o += 28) { + for (let j = o - 28; j < o; ++j) { + const l = j + totrot[i]; + pcr[j] = l < o ? pc1m[l] : pc1m[l - 28]; } - - r = (r << 31) | (r >>> 1); - x = (l ^ r) & 0xaaaaaaaa; - l ^= x; - r ^= x; - l = (l << 31) | (l >>> 1); - x = ((l >>> 8) ^ r) & 0x00ff00ff; - r ^= x; - l ^= (x << 8); - x = ((l >>> 2) ^ r) & 0x33333333; - r ^= x; - l ^= (x << 2); - x = ((r >>> 16) ^ l) & 0x0000ffff; - l ^= x; - r ^= (x << 16); - x = ((r >>> 4) ^ l) & 0x0f0f0f0f; - l ^= x; - r ^= (x << 4); - - // Spread ints to bytes - x = [r, l]; - for (i = 0; i < 8; i++) { - b[i] = (x[i>>>2] >>> (8 * (3 - (i % 4)))) % 256; - if (b[i] < 0) { b[i] += 256; } // unsigned + } + for (let j = 0; j < 24; ++j) { + if (pcr[PC2[j]] !== 0) { + kn[m] |= 1 << (23 - j); } - return b; + if (pcr[PC2[j + 24]] !== 0) { + kn[n] |= 1 << (23 - j); + } + } } - // Encrypt 16 bytes of text using passwd as key - function encrypt(t) { - return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); + // cookey + for (let i = 0, rawi = 0, KnLi = 0; i < 16; ++i) { + const raw0 = kn[rawi++]; + const raw1 = kn[rawi++]; + keys[KnLi] = (raw0 & 0x00fc0000) << 6; + keys[KnLi] |= (raw0 & 0x00000fc0) << 10; + keys[KnLi] |= (raw1 & 0x00fc0000) >>> 10; + keys[KnLi] |= (raw1 & 0x00000fc0) >>> 6; + ++KnLi; + keys[KnLi] = (raw0 & 0x0003f000) << 12; + keys[KnLi] |= (raw0 & 0x0000003f) << 16; + keys[KnLi] |= (raw1 & 0x0003f000) >>> 4; + keys[KnLi] |= (raw1 & 0x0000003f); + ++KnLi; + } + } + + // Encrypt 8 bytes of text + function enc8(text) { + const b = text.slice(); + let i = 0; let l; let r; let + x; // left, right, accumulator + + // Squash 8 bytes to 2 ints + l = b[i++] << 24 | b[i++] << 16 | b[i++] << 8 | b[i++]; + r = b[i++] << 24 | b[i++] << 16 | b[i++] << 8 | b[i++]; + + x = ((l >>> 4) ^ r) & 0x0f0f0f0f; + r ^= x; + l ^= (x << 4); + x = ((l >>> 16) ^ r) & 0x0000ffff; + r ^= x; + l ^= (x << 16); + x = ((r >>> 2) ^ l) & 0x33333333; + l ^= x; + r ^= (x << 2); + x = ((r >>> 8) ^ l) & 0x00ff00ff; + l ^= x; + r ^= (x << 8); + r = (r << 1) | ((r >>> 31) & 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 1) | ((l >>> 31) & 1); + + for (let i = 0, keysi = 0; i < 8; ++i) { + x = (r << 28) | (r >>> 4); + x ^= keys[keysi++]; + let fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = r ^ keys[keysi++]; + fval |= SP8[x & 0x3f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + l ^= fval; + x = (l << 28) | (l >>> 4); + x ^= keys[keysi++]; + fval = SP7[x & 0x3f]; + fval |= SP5[(x >>> 8) & 0x3f]; + fval |= SP3[(x >>> 16) & 0x3f]; + fval |= SP1[(x >>> 24) & 0x3f]; + x = l ^ keys[keysi++]; + fval |= SP8[x & 0x0000003f]; + fval |= SP6[(x >>> 8) & 0x3f]; + fval |= SP4[(x >>> 16) & 0x3f]; + fval |= SP2[(x >>> 24) & 0x3f]; + r ^= fval; } - setKeys(passwd); // Setup keys - return {'encrypt': encrypt}; // Public interface + r = (r << 31) | (r >>> 1); + x = (l ^ r) & 0xaaaaaaaa; + l ^= x; + r ^= x; + l = (l << 31) | (l >>> 1); + x = ((l >>> 8) ^ r) & 0x00ff00ff; + r ^= x; + l ^= (x << 8); + x = ((l >>> 2) ^ r) & 0x33333333; + r ^= x; + l ^= (x << 2); + x = ((r >>> 16) ^ l) & 0x0000ffff; + l ^= x; + r ^= (x << 16); + x = ((r >>> 4) ^ l) & 0x0f0f0f0f; + l ^= x; + r ^= (x << 4); + // Spread ints to bytes + x = [r, l]; + for (i = 0; i < 8; i++) { + b[i] = (x[i >>> 2] >>> (8 * (3 - (i % 4)))) % 256; + if (b[i] < 0) { b[i] += 256; } // unsigned + } + return b; + } + + // Encrypt 16 bytes of text using passwd as key + function encrypt(t) { + return enc8(t.slice(0, 8)).concat(enc8(t.slice(8, 16))); + } + + setKeys(passwd); // Setup keys + return { encrypt: encrypt }; // Public interface } diff --git a/core/display.js b/core/display.js index 4955ce20..653f3206 100644 --- a/core/display.js +++ b/core/display.js @@ -8,639 +8,643 @@ */ import * as Log from './util/logging.js'; -import Base64 from "./base64.js"; +import Base64 from './base64.js'; let SUPPORTS_IMAGEDATA_CONSTRUCTOR = false; try { - new ImageData(new Uint8ClampedArray(4), 1, 1); - SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; + new ImageData(new Uint8ClampedArray(4), 1, 1); + SUPPORTS_IMAGEDATA_CONSTRUCTOR = true; } catch (ex) { - // ignore failure + // ignore failure } export default class Display { - constructor(target) { - this._drawCtx = null; - this._c_forceCanvas = false; + constructor(target) { + this._drawCtx = null; + this._c_forceCanvas = false; - this._renderQ = []; // queue drawing actions for in-oder rendering - this._flushing = false; + this._renderQ = []; // queue drawing actions for in-oder rendering + this._flushing = false; - // the full frame buffer (logical canvas) size - this._fb_width = 0; - this._fb_height = 0; + // the full frame buffer (logical canvas) size + this._fb_width = 0; + this._fb_height = 0; - this._prevDrawStyle = ""; - this._tile = null; - this._tile16x16 = null; - this._tile_x = 0; - this._tile_y = 0; + this._prevDrawStyle = ''; + this._tile = null; + this._tile16x16 = null; + this._tile_x = 0; + this._tile_y = 0; - Log.Debug(">> Display.constructor"); + Log.Debug('>> Display.constructor'); - // The visible canvas - this._target = target; + // The visible canvas + this._target = target; - if (!this._target) { - throw new Error("Target must be set"); - } - - if (typeof this._target === 'string') { - throw new Error('target must be a DOM element'); - } - - if (!this._target.getContext) { - throw new Error("no getContext method"); - } - - this._targetCtx = this._target.getContext('2d'); - - // the visible canvas viewport (i.e. what actually gets seen) - this._viewportLoc = { 'x': 0, 'y': 0, 'w': this._target.width, 'h': this._target.height }; - - // The hidden canvas, where we do the actual rendering - this._backbuffer = document.createElement('canvas'); - this._drawCtx = this._backbuffer.getContext('2d'); - - this._damageBounds = { left:0, top:0, - right: this._backbuffer.width, - bottom: this._backbuffer.height }; - - Log.Debug("User Agent: " + navigator.userAgent); - - this.clear(); - - // Check canvas features - if (!('createImageData' in this._drawCtx)) { - throw new Error("Canvas does not support createImageData"); - } - - this._tile16x16 = this._drawCtx.createImageData(16, 16); - Log.Debug("<< Display.constructor"); - - // ===== PROPERTIES ===== - - this._scale = 1.0; - this._clipViewport = false; - this.logo = null; - - // ===== EVENT HANDLERS ===== - - this.onflush = () => {}; // A flush request has finished + if (!this._target) { + throw new Error('Target must be set'); } + if (typeof this._target === 'string') { + throw new Error('target must be a DOM element'); + } + + if (!this._target.getContext) { + throw new Error('no getContext method'); + } + + this._targetCtx = this._target.getContext('2d'); + + // the visible canvas viewport (i.e. what actually gets seen) + this._viewportLoc = { + x: 0, y: 0, w: this._target.width, h: this._target.height + }; + + // The hidden canvas, where we do the actual rendering + this._backbuffer = document.createElement('canvas'); + this._drawCtx = this._backbuffer.getContext('2d'); + + this._damageBounds = { + left: 0, + top: 0, + right: this._backbuffer.width, + bottom: this._backbuffer.height + }; + + Log.Debug('User Agent: ' + navigator.userAgent); + + this.clear(); + + // Check canvas features + if (!('createImageData' in this._drawCtx)) { + throw new Error('Canvas does not support createImageData'); + } + + this._tile16x16 = this._drawCtx.createImageData(16, 16); + Log.Debug('<< Display.constructor'); + // ===== PROPERTIES ===== - get scale() { return this._scale; } - set scale(scale) { - this._rescale(scale); + this._scale = 1.0; + this._clipViewport = false; + this.logo = null; + + // ===== EVENT HANDLERS ===== + + this.onflush = () => {}; // A flush request has finished + } + + // ===== PROPERTIES ===== + + get scale() { return this._scale; } + + set scale(scale) { + this._rescale(scale); + } + + get clipViewport() { return this._clipViewport; } + + set clipViewport(viewport) { + this._clipViewport = viewport; + // May need to readjust the viewport dimensions + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + get width() { + return this._fb_width; + } + + get height() { + return this._fb_height; + } + + // ===== PUBLIC METHODS ===== + + viewportChangePos(deltaX, deltaY) { + const vp = this._viewportLoc; + deltaX = Math.floor(deltaX); + deltaY = Math.floor(deltaY); + + if (!this._clipViewport) { + deltaX = -vp.w; // clamped later of out of bounds + deltaY = -vp.h; } - get clipViewport() { return this._clipViewport; } - set clipViewport(viewport) { - this._clipViewport = viewport; - // May need to readjust the viewport dimensions - const vp = this._viewportLoc; - this.viewportChangeSize(vp.w, vp.h); - this.viewportChangePos(0, 0); + const vx2 = vp.x + vp.w - 1; + const vy2 = vp.y + vp.h - 1; + + // Position change + + if (deltaX < 0 && vp.x + deltaX < 0) { + deltaX = -vp.x; + } + if (vx2 + deltaX >= this._fb_width) { + deltaX -= vx2 + deltaX - this._fb_width + 1; } - get width() { - return this._fb_width; + if (vp.y + deltaY < 0) { + deltaY = -vp.y; + } + if (vy2 + deltaY >= this._fb_height) { + deltaY -= (vy2 + deltaY - this._fb_height + 1); } - get height() { - return this._fb_height; + if (deltaX === 0 && deltaY === 0) { + return; + } + Log.Debug('viewportChange deltaX: ' + deltaX + ', deltaY: ' + deltaY); + + vp.x += deltaX; + vp.y += deltaY; + + this._damage(vp.x, vp.y, vp.w, vp.h); + + this.flip(); + } + + viewportChangeSize(width, height) { + if (!this._clipViewport + || typeof (width) === 'undefined' + || typeof (height) === 'undefined') { + Log.Debug('Setting viewport to full display region'); + width = this._fb_width; + height = this._fb_height; } - // ===== PUBLIC METHODS ===== - - viewportChangePos(deltaX, deltaY) { - const vp = this._viewportLoc; - deltaX = Math.floor(deltaX); - deltaY = Math.floor(deltaY); - - if (!this._clipViewport) { - deltaX = -vp.w; // clamped later of out of bounds - deltaY = -vp.h; - } - - const vx2 = vp.x + vp.w - 1; - const vy2 = vp.y + vp.h - 1; - - // Position change - - if (deltaX < 0 && vp.x + deltaX < 0) { - deltaX = -vp.x; - } - if (vx2 + deltaX >= this._fb_width) { - deltaX -= vx2 + deltaX - this._fb_width + 1; - } - - if (vp.y + deltaY < 0) { - deltaY = -vp.y; - } - if (vy2 + deltaY >= this._fb_height) { - deltaY -= (vy2 + deltaY - this._fb_height + 1); - } - - if (deltaX === 0 && deltaY === 0) { - return; - } - Log.Debug("viewportChange deltaX: " + deltaX + ", deltaY: " + deltaY); - - vp.x += deltaX; - vp.y += deltaY; - - this._damage(vp.x, vp.y, vp.w, vp.h); - - this.flip(); + if (width > this._fb_width) { + width = this._fb_width; + } + if (height > this._fb_height) { + height = this._fb_height; } - viewportChangeSize(width, height) { + const vp = this._viewportLoc; + if (vp.w !== width || vp.h !== height) { + vp.w = width; + vp.h = height; - if (!this._clipViewport || - typeof(width) === "undefined" || - typeof(height) === "undefined") { + const canvas = this._target; + canvas.width = width; + canvas.height = height; - Log.Debug("Setting viewport to full display region"); - width = this._fb_width; - height = this._fb_height; - } + // The position might need to be updated if we've grown + this.viewportChangePos(0, 0); - if (width > this._fb_width) { - width = this._fb_width; - } - if (height > this._fb_height) { - height = this._fb_height; - } + this._damage(vp.x, vp.y, vp.w, vp.h); + this.flip(); - const vp = this._viewportLoc; - if (vp.w !== width || vp.h !== height) { - vp.w = width; - vp.h = height; + // Update the visible size of the target canvas + this._rescale(this._scale); + } + } - const canvas = this._target; - canvas.width = width; - canvas.height = height; + absX(x) { + return x / this._scale + this._viewportLoc.x; + } - // The position might need to be updated if we've grown - this.viewportChangePos(0, 0); + absY(y) { + return y / this._scale + this._viewportLoc.y; + } - this._damage(vp.x, vp.y, vp.w, vp.h); - this.flip(); + resize(width, height) { + this._prevDrawStyle = ''; - // Update the visible size of the target canvas - this._rescale(this._scale); - } + this._fb_width = width; + this._fb_height = height; + + const canvas = this._backbuffer; + if (canvas.width !== width || canvas.height !== height) { + // We have to save the canvas data since changing the size will clear it + let saveImg = null; + if (canvas.width > 0 && canvas.height > 0) { + saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); + } + + if (canvas.width !== width) { + canvas.width = width; + } + if (canvas.height !== height) { + canvas.height = height; + } + + if (saveImg) { + this._drawCtx.putImageData(saveImg, 0, 0); + } } - absX(x) { - return x / this._scale + this._viewportLoc.x; + // Readjust the viewport as it may be incorrectly sized + // and positioned + const vp = this._viewportLoc; + this.viewportChangeSize(vp.w, vp.h); + this.viewportChangePos(0, 0); + } + + // Track what parts of the visible canvas that need updating + _damage(x, y, w, h) { + if (x < this._damageBounds.left) { + this._damageBounds.left = x; + } + if (y < this._damageBounds.top) { + this._damageBounds.top = y; + } + if ((x + w) > this._damageBounds.right) { + this._damageBounds.right = x + w; + } + if ((y + h) > this._damageBounds.bottom) { + this._damageBounds.bottom = y + h; + } + } + + // Update the visible canvas with the contents of the + // rendering canvas + flip(from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + type: 'flip' + }); + } else { + let x = this._damageBounds.left; + let y = this._damageBounds.top; + let w = this._damageBounds.right - x; + let h = this._damageBounds.bottom - y; + + let vx = x - this._viewportLoc.x; + let vy = y - this._viewportLoc.y; + + if (vx < 0) { + w += vx; + x -= vx; + vx = 0; + } + if (vy < 0) { + h += vy; + y -= vy; + vy = 0; + } + + if ((vx + w) > this._viewportLoc.w) { + w = this._viewportLoc.w - vx; + } + if ((vy + h) > this._viewportLoc.h) { + h = this._viewportLoc.h - vy; + } + + if ((w > 0) && (h > 0)) { + // FIXME: We may need to disable image smoothing here + // as well (see copyImage()), but we haven't + // noticed any problem yet. + this._targetCtx.drawImage(this._backbuffer, + x, y, w, h, + vx, vy, w, h); + } + + this._damageBounds.left = this._damageBounds.top = 65535; + this._damageBounds.right = this._damageBounds.bottom = 0; + } + } + + clear() { + if (this._logo) { + this.resize(this._logo.width, this._logo.height); + this.imageRect(0, 0, this._logo.type, this._logo.data); + } else { + this.resize(240, 20); + this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); + } + this.flip(); + } + + pending() { + return this._renderQ.length > 0; + } + + flush() { + if (this._renderQ.length === 0) { + this.onflush(); + } else { + this._flushing = true; + } + } + + fillRect(x, y, width, height, color, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + type: 'fill', + x: x, + y: y, + width: width, + height: height, + color: color + }); + } else { + this._setFillColor(color); + this._drawCtx.fillRect(x, y, width, height); + this._damage(x, y, width, height); + } + } + + copyImage(old_x, old_y, new_x, new_y, w, h, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + this._renderQ_push({ + type: 'copy', + old_x: old_x, + old_y: old_y, + x: new_x, + y: new_y, + width: w, + height: h, + }); + } else { + // Due to this bug among others [1] we need to disable the image-smoothing to + // avoid getting a blur effect when copying data. + // + // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 + // + // We need to set these every time since all properties are reset + // when the the size is changed + this._drawCtx.mozImageSmoothingEnabled = false; + this._drawCtx.webkitImageSmoothingEnabled = false; + this._drawCtx.msImageSmoothingEnabled = false; + this._drawCtx.imageSmoothingEnabled = false; + + this._drawCtx.drawImage(this._backbuffer, + old_x, old_y, w, h, + new_x, new_y, w, h); + this._damage(new_x, new_y, w, h); + } + } + + imageRect(x, y, mime, arr) { + const img = new Image(); + img.src = 'data: ' + mime + ';base64,' + Base64.encode(arr); + this._renderQ_push({ + type: 'img', + img: img, + x: x, + y: y + }); + } + + // start updating a tile + startTile(x, y, width, height, color) { + this._tile_x = x; + this._tile_y = y; + if (width === 16 && height === 16) { + this._tile = this._tile16x16; + } else { + this._tile = this._drawCtx.createImageData(width, height); } - absY(y) { - return y / this._scale + this._viewportLoc.y; + const red = color[2]; + const green = color[1]; + const blue = color[0]; + + const data = this._tile.data; + for (let i = 0; i < width * height * 4; i += 4) { + data[i] = red; + data[i + 1] = green; + data[i + 2] = blue; + data[i + 3] = 255; + } + } + + // update sub-rectangle of the current tile + subTile(x, y, w, h, color) { + const red = color[2]; + const green = color[1]; + const blue = color[0]; + const xend = x + w; + const yend = y + h; + + const data = this._tile.data; + const width = this._tile.width; + for (let j = y; j < yend; j++) { + for (let i = x; i < xend; i++) { + const p = (i + (j * width)) * 4; + data[p] = red; + data[p + 1] = green; + data[p + 2] = blue; + data[p + 3] = 255; + } + } + } + + // draw the current tile to the screen + finishTile() { + this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y); + this._damage(this._tile_x, this._tile_y, + this._tile.width, this._tile.height); + } + + blitImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + const new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + type: 'blit', + data: new_arr, + x: x, + y: y, + width: width, + height: height, + }); + } else { + this._bgrxImageData(x, y, width, height, arr, offset); + } + } + + blitRgbImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + const new_arr = new Uint8Array(width * height * 3); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + type: 'blitRgb', + data: new_arr, + x: x, + y: y, + width: width, + height: height, + }); + } else { + this._rgbImageData(x, y, width, height, arr, offset); + } + } + + blitRgbxImage(x, y, width, height, arr, offset, from_queue) { + if (this._renderQ.length !== 0 && !from_queue) { + // NB(directxman12): it's technically more performant here to use preallocated arrays, + // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, + // this probably isn't getting called *nearly* as much + const new_arr = new Uint8Array(width * height * 4); + new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); + this._renderQ_push({ + type: 'blitRgbx', + data: new_arr, + x: x, + y: y, + width: width, + height: height, + }); + } else { + this._rgbxImageData(x, y, width, height, arr, offset); + } + } + + drawImage(img, x, y) { + this._drawCtx.drawImage(img, x, y); + this._damage(x, y, img.width, img.height); + } + + autoscale(containerWidth, containerHeight) { + const vp = this._viewportLoc; + const targetAspectRatio = containerWidth / containerHeight; + const fbAspectRatio = vp.w / vp.h; + + let scaleRatio; + if (fbAspectRatio >= targetAspectRatio) { + scaleRatio = containerWidth / vp.w; + } else { + scaleRatio = containerHeight / vp.h; } - resize(width, height) { - this._prevDrawStyle = ""; + this._rescale(scaleRatio); + } - this._fb_width = width; - this._fb_height = height; + // ===== PRIVATE METHODS ===== - const canvas = this._backbuffer; - if (canvas.width !== width || canvas.height !== height) { + _rescale(factor) { + this._scale = factor; + const vp = this._viewportLoc; - // We have to save the canvas data since changing the size will clear it - let saveImg = null; - if (canvas.width > 0 && canvas.height > 0) { - saveImg = this._drawCtx.getImageData(0, 0, canvas.width, canvas.height); - } + // NB(directxman12): If you set the width directly, or set the + // style width to a number, the canvas is cleared. + // However, if you set the style width to a string + // ('NNNpx'), the canvas is scaled without clearing. + const width = Math.round(factor * vp.w) + 'px'; + const height = Math.round(factor * vp.h) + 'px'; - if (canvas.width !== width) { - canvas.width = width; - } - if (canvas.height !== height) { - canvas.height = height; - } + if ((this._target.style.width !== width) + || (this._target.style.height !== height)) { + this._target.style.width = width; + this._target.style.height = height; + } + } - if (saveImg) { - this._drawCtx.putImageData(saveImg, 0, 0); - } - } + _setFillColor(color) { + const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')'; + if (newStyle !== this._prevDrawStyle) { + this._drawCtx.fillStyle = newStyle; + this._prevDrawStyle = newStyle; + } + } - // Readjust the viewport as it may be incorrectly sized - // and positioned - const vp = this._viewportLoc; - this.viewportChangeSize(vp.w, vp.h); - this.viewportChangePos(0, 0); + _rgbImageData(x, y, width, height, arr, offset) { + const img = this._drawCtx.createImageData(width, height); + const data = img.data; + for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { + data[i] = arr[j]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j + 2]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + } + + _bgrxImageData(x, y, width, height, arr, offset) { + const img = this._drawCtx.createImageData(width, height); + const data = img.data; + for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { + data[i] = arr[j + 2]; + data[i + 1] = arr[j + 1]; + data[i + 2] = arr[j]; + data[i + 3] = 255; // Alpha + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + } + + _rgbxImageData(x, y, width, height, arr, offset) { + // NB(directxman12): arr must be an Type Array view + let img; + if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { + img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + } else { + img = this._drawCtx.createImageData(width, height); + img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); + } + this._drawCtx.putImageData(img, x, y); + this._damage(x, y, img.width, img.height); + } + + _renderQ_push(action) { + this._renderQ.push(action); + if (this._renderQ.length === 1) { + // If this can be rendered immediately it will be, otherwise + // the scanner will wait for the relevant event + this._scan_renderQ(); + } + } + + _resume_renderQ() { + // "this" is the object that is ready, not the + // display object + this.removeEventListener('load', this._noVNC_display._resume_renderQ); + this._noVNC_display._scan_renderQ(); + } + + _scan_renderQ() { + let ready = true; + while (ready && this._renderQ.length > 0) { + const a = this._renderQ[0]; + switch (a.type) { + case 'flip': + this.flip(true); + break; + case 'copy': + this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); + break; + case 'fill': + this.fillRect(a.x, a.y, a.width, a.height, a.color, true); + break; + case 'blit': + this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgb': + this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'blitRgbx': + this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); + break; + case 'img': + if (a.img.complete) { + this.drawImage(a.img, a.x, a.y); + } else { + a.img._noVNC_display = this; + a.img.addEventListener('load', this._resume_renderQ); + // We need to wait for this image to 'load' + // to keep things in-order + ready = false; + } + break; + } + + if (ready) { + this._renderQ.shift(); + } } - // Track what parts of the visible canvas that need updating - _damage(x, y, w, h) { - if (x < this._damageBounds.left) { - this._damageBounds.left = x; - } - if (y < this._damageBounds.top) { - this._damageBounds.top = y; - } - if ((x + w) > this._damageBounds.right) { - this._damageBounds.right = x + w; - } - if ((y + h) > this._damageBounds.bottom) { - this._damageBounds.bottom = y + h; - } - } - - // Update the visible canvas with the contents of the - // rendering canvas - flip(from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'flip' - }); - } else { - let x = this._damageBounds.left; - let y = this._damageBounds.top; - let w = this._damageBounds.right - x; - let h = this._damageBounds.bottom - y; - - let vx = x - this._viewportLoc.x; - let vy = y - this._viewportLoc.y; - - if (vx < 0) { - w += vx; - x -= vx; - vx = 0; - } - if (vy < 0) { - h += vy; - y -= vy; - vy = 0; - } - - if ((vx + w) > this._viewportLoc.w) { - w = this._viewportLoc.w - vx; - } - if ((vy + h) > this._viewportLoc.h) { - h = this._viewportLoc.h - vy; - } - - if ((w > 0) && (h > 0)) { - // FIXME: We may need to disable image smoothing here - // as well (see copyImage()), but we haven't - // noticed any problem yet. - this._targetCtx.drawImage(this._backbuffer, - x, y, w, h, - vx, vy, w, h); - } - - this._damageBounds.left = this._damageBounds.top = 65535; - this._damageBounds.right = this._damageBounds.bottom = 0; - } - } - - clear() { - if (this._logo) { - this.resize(this._logo.width, this._logo.height); - this.imageRect(0, 0, this._logo.type, this._logo.data); - } else { - this.resize(240, 20); - this._drawCtx.clearRect(0, 0, this._fb_width, this._fb_height); - } - this.flip(); - } - - pending() { - return this._renderQ.length > 0; - } - - flush() { - if (this._renderQ.length === 0) { - this.onflush(); - } else { - this._flushing = true; - } - } - - fillRect(x, y, width, height, color, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'fill', - 'x': x, - 'y': y, - 'width': width, - 'height': height, - 'color': color - }); - } else { - this._setFillColor(color); - this._drawCtx.fillRect(x, y, width, height); - this._damage(x, y, width, height); - } - } - - copyImage(old_x, old_y, new_x, new_y, w, h, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - this._renderQ_push({ - 'type': 'copy', - 'old_x': old_x, - 'old_y': old_y, - 'x': new_x, - 'y': new_y, - 'width': w, - 'height': h, - }); - } else { - // Due to this bug among others [1] we need to disable the image-smoothing to - // avoid getting a blur effect when copying data. - // - // 1. https://bugzilla.mozilla.org/show_bug.cgi?id=1194719 - // - // We need to set these every time since all properties are reset - // when the the size is changed - this._drawCtx.mozImageSmoothingEnabled = false; - this._drawCtx.webkitImageSmoothingEnabled = false; - this._drawCtx.msImageSmoothingEnabled = false; - this._drawCtx.imageSmoothingEnabled = false; - - this._drawCtx.drawImage(this._backbuffer, - old_x, old_y, w, h, - new_x, new_y, w, h); - this._damage(new_x, new_y, w, h); - } - } - - imageRect(x, y, mime, arr) { - const img = new Image(); - img.src = "data: " + mime + ";base64," + Base64.encode(arr); - this._renderQ_push({ - 'type': 'img', - 'img': img, - 'x': x, - 'y': y - }); - } - - // start updating a tile - startTile(x, y, width, height, color) { - this._tile_x = x; - this._tile_y = y; - if (width === 16 && height === 16) { - this._tile = this._tile16x16; - } else { - this._tile = this._drawCtx.createImageData(width, height); - } - - const red = color[2]; - const green = color[1]; - const blue = color[0]; - - const data = this._tile.data; - for (let i = 0; i < width * height * 4; i += 4) { - data[i] = red; - data[i + 1] = green; - data[i + 2] = blue; - data[i + 3] = 255; - } - } - - // update sub-rectangle of the current tile - subTile(x, y, w, h, color) { - const red = color[2]; - const green = color[1]; - const blue = color[0]; - const xend = x + w; - const yend = y + h; - - const data = this._tile.data; - const width = this._tile.width; - for (let j = y; j < yend; j++) { - for (let i = x; i < xend; i++) { - const p = (i + (j * width)) * 4; - data[p] = red; - data[p + 1] = green; - data[p + 2] = blue; - data[p + 3] = 255; - } - } - } - - // draw the current tile to the screen - finishTile() { - this._drawCtx.putImageData(this._tile, this._tile_x, this._tile_y); - this._damage(this._tile_x, this._tile_y, - this._tile.width, this._tile.height); - } - - blitImage(x, y, width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - const new_arr = new Uint8Array(width * height * 4); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blit', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else { - this._bgrxImageData(x, y, width, height, arr, offset); - } - } - - blitRgbImage(x, y , width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - const new_arr = new Uint8Array(width * height * 3); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blitRgb', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else { - this._rgbImageData(x, y, width, height, arr, offset); - } - } - - blitRgbxImage(x, y, width, height, arr, offset, from_queue) { - if (this._renderQ.length !== 0 && !from_queue) { - // NB(directxman12): it's technically more performant here to use preallocated arrays, - // but it's a lot of extra work for not a lot of payoff -- if we're using the render queue, - // this probably isn't getting called *nearly* as much - const new_arr = new Uint8Array(width * height * 4); - new_arr.set(new Uint8Array(arr.buffer, 0, new_arr.length)); - this._renderQ_push({ - 'type': 'blitRgbx', - 'data': new_arr, - 'x': x, - 'y': y, - 'width': width, - 'height': height, - }); - } else { - this._rgbxImageData(x, y, width, height, arr, offset); - } - } - - drawImage(img, x, y) { - this._drawCtx.drawImage(img, x, y); - this._damage(x, y, img.width, img.height); - } - - autoscale(containerWidth, containerHeight) { - const vp = this._viewportLoc; - const targetAspectRatio = containerWidth / containerHeight; - const fbAspectRatio = vp.w / vp.h; - - let scaleRatio; - if (fbAspectRatio >= targetAspectRatio) { - scaleRatio = containerWidth / vp.w; - } else { - scaleRatio = containerHeight / vp.h; - } - - this._rescale(scaleRatio); - } - - // ===== PRIVATE METHODS ===== - - _rescale(factor) { - this._scale = factor; - const vp = this._viewportLoc; - - // NB(directxman12): If you set the width directly, or set the - // style width to a number, the canvas is cleared. - // However, if you set the style width to a string - // ('NNNpx'), the canvas is scaled without clearing. - const width = Math.round(factor * vp.w) + 'px'; - const height = Math.round(factor * vp.h) + 'px'; - - if ((this._target.style.width !== width) || - (this._target.style.height !== height)) { - this._target.style.width = width; - this._target.style.height = height; - } - } - - _setFillColor(color) { - const newStyle = 'rgb(' + color[2] + ',' + color[1] + ',' + color[0] + ')'; - if (newStyle !== this._prevDrawStyle) { - this._drawCtx.fillStyle = newStyle; - this._prevDrawStyle = newStyle; - } - } - - _rgbImageData(x, y, width, height, arr, offset) { - const img = this._drawCtx.createImageData(width, height); - const data = img.data; - for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 3) { - data[i] = arr[j]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j + 2]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - } - - _bgrxImageData(x, y, width, height, arr, offset) { - const img = this._drawCtx.createImageData(width, height); - const data = img.data; - for (let i = 0, j = offset; i < width * height * 4; i += 4, j += 4) { - data[i] = arr[j + 2]; - data[i + 1] = arr[j + 1]; - data[i + 2] = arr[j]; - data[i + 3] = 255; // Alpha - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - } - - _rgbxImageData(x, y, width, height, arr, offset) { - // NB(directxman12): arr must be an Type Array view - let img; - if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { - img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); - } else { - img = this._drawCtx.createImageData(width, height); - img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); - } - this._drawCtx.putImageData(img, x, y); - this._damage(x, y, img.width, img.height); - } - - _renderQ_push(action) { - this._renderQ.push(action); - if (this._renderQ.length === 1) { - // If this can be rendered immediately it will be, otherwise - // the scanner will wait for the relevant event - this._scan_renderQ(); - } - } - - _resume_renderQ() { - // "this" is the object that is ready, not the - // display object - this.removeEventListener('load', this._noVNC_display._resume_renderQ); - this._noVNC_display._scan_renderQ(); - } - - _scan_renderQ() { - let ready = true; - while (ready && this._renderQ.length > 0) { - const a = this._renderQ[0]; - switch (a.type) { - case 'flip': - this.flip(true); - break; - case 'copy': - this.copyImage(a.old_x, a.old_y, a.x, a.y, a.width, a.height, true); - break; - case 'fill': - this.fillRect(a.x, a.y, a.width, a.height, a.color, true); - break; - case 'blit': - this.blitImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgb': - this.blitRgbImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'blitRgbx': - this.blitRgbxImage(a.x, a.y, a.width, a.height, a.data, 0, true); - break; - case 'img': - if (a.img.complete) { - this.drawImage(a.img, a.x, a.y); - } else { - a.img._noVNC_display = this; - a.img.addEventListener('load', this._resume_renderQ); - // We need to wait for this image to 'load' - // to keep things in-order - ready = false; - } - break; - } - - if (ready) { - this._renderQ.shift(); - } - } - - if (this._renderQ.length === 0 && this._flushing) { - this._flushing = false; - this.onflush(); - } + if (this._renderQ.length === 0 && this._flushing) { + this._flushing = false; + this.onflush(); } + } } diff --git a/core/encodings.js b/core/encodings.js index 5a70e664..adc268d3 100644 --- a/core/encodings.js +++ b/core/encodings.js @@ -7,35 +7,35 @@ */ export const encodings = { - encodingRaw: 0, - encodingCopyRect: 1, - encodingRRE: 2, - encodingHextile: 5, - encodingTight: 7, - encodingTightPNG: -260, + encodingRaw: 0, + encodingCopyRect: 1, + encodingRRE: 2, + encodingHextile: 5, + encodingTight: 7, + encodingTightPNG: -260, - pseudoEncodingQualityLevel9: -23, - pseudoEncodingQualityLevel0: -32, - pseudoEncodingDesktopSize: -223, - pseudoEncodingLastRect: -224, - pseudoEncodingCursor: -239, - pseudoEncodingQEMUExtendedKeyEvent: -258, - pseudoEncodingExtendedDesktopSize: -308, - pseudoEncodingXvp: -309, - pseudoEncodingFence: -312, - pseudoEncodingContinuousUpdates: -313, - pseudoEncodingCompressLevel9: -247, - pseudoEncodingCompressLevel0: -256, + pseudoEncodingQualityLevel9: -23, + pseudoEncodingQualityLevel0: -32, + pseudoEncodingDesktopSize: -223, + pseudoEncodingLastRect: -224, + pseudoEncodingCursor: -239, + pseudoEncodingQEMUExtendedKeyEvent: -258, + pseudoEncodingExtendedDesktopSize: -308, + pseudoEncodingXvp: -309, + pseudoEncodingFence: -312, + pseudoEncodingContinuousUpdates: -313, + pseudoEncodingCompressLevel9: -247, + pseudoEncodingCompressLevel0: -256, }; export function encodingName(num) { - switch (num) { - case encodings.encodingRaw: return "Raw"; - case encodings.encodingCopyRect: return "CopyRect"; - case encodings.encodingRRE: return "RRE"; - case encodings.encodingHextile: return "Hextile"; - case encodings.encodingTight: return "Tight"; - case encodings.encodingTightPNG: return "TightPNG"; - default: return "[unknown encoding " + num + "]"; - } + switch (num) { + case encodings.encodingRaw: return 'Raw'; + case encodings.encodingCopyRect: return 'CopyRect'; + case encodings.encodingRRE: return 'RRE'; + case encodings.encodingHextile: return 'Hextile'; + case encodings.encodingTight: return 'Tight'; + case encodings.encodingTightPNG: return 'TightPNG'; + default: return '[unknown encoding ' + num + ']'; + } } diff --git a/core/inflator.js b/core/inflator.js index 0eab8fe4..95a5f1df 100644 --- a/core/inflator.js +++ b/core/inflator.js @@ -1,38 +1,38 @@ -import { inflateInit, inflate, inflateReset } from "../vendor/pako/lib/zlib/inflate.js"; -import ZStream from "../vendor/pako/lib/zlib/zstream.js"; +import { inflateInit, inflate, inflateReset } from '../vendor/pako/lib/zlib/inflate.js'; +import ZStream from '../vendor/pako/lib/zlib/zstream.js'; export default class Inflate { - constructor() { - this.strm = new ZStream(); - this.chunkSize = 1024 * 10 * 10; - this.strm.output = new Uint8Array(this.chunkSize); - this.windowBits = 5; + constructor() { + this.strm = new ZStream(); + this.chunkSize = 1024 * 10 * 10; + this.strm.output = new Uint8Array(this.chunkSize); + this.windowBits = 5; - inflateInit(this.strm, this.windowBits); + inflateInit(this.strm, this.windowBits); + } + + inflate(data, flush, expected) { + this.strm.input = data; + this.strm.avail_in = this.strm.input.length; + this.strm.next_in = 0; + this.strm.next_out = 0; + + // resize our output buffer if it's too small + // (we could just use multiple chunks, but that would cause an extra + // allocation each time to flatten the chunks) + if (expected > this.chunkSize) { + this.chunkSize = expected; + this.strm.output = new Uint8Array(this.chunkSize); } - inflate(data, flush, expected) { - this.strm.input = data; - this.strm.avail_in = this.strm.input.length; - this.strm.next_in = 0; - this.strm.next_out = 0; + this.strm.avail_out = this.chunkSize; - // resize our output buffer if it's too small - // (we could just use multiple chunks, but that would cause an extra - // allocation each time to flatten the chunks) - if (expected > this.chunkSize) { - this.chunkSize = expected; - this.strm.output = new Uint8Array(this.chunkSize); - } + inflate(this.strm, flush); - this.strm.avail_out = this.chunkSize; + return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); + } - inflate(this.strm, flush); - - return new Uint8Array(this.strm.output.buffer, 0, this.strm.next_out); - } - - reset() { - inflateReset(this.strm); - } + reset() { + inflateReset(this.strm); + } } diff --git a/core/input/domkeytable.js b/core/input/domkeytable.js index 60288012..ee0a8340 100644 --- a/core/input/domkeytable.js +++ b/core/input/domkeytable.js @@ -4,7 +4,7 @@ * Licensed under MPL 2.0 or any later version (see LICENSE.txt) */ -import KeyTable from "./keysym.js"; +import KeyTable from './keysym.js'; /* * Mapping between HTML key values and VNC/X11 keysyms for "special" @@ -15,202 +15,199 @@ import KeyTable from "./keysym.js"; const DOMKeyTable = {}; -function addStandard(key, standard) -{ - if (standard === undefined) throw "Undefined keysym for key \"" + key + "\""; - if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; - DOMKeyTable[key] = [standard, standard, standard, standard]; +function addStandard(key, standard) { + if (standard === undefined) throw 'Undefined keysym for key "' + key + '"'; + if (key in DOMKeyTable) throw 'Duplicate entry for key "' + key + '"'; + DOMKeyTable[key] = [standard, standard, standard, standard]; } -function addLeftRight(key, left, right) -{ - if (left === undefined) throw "Undefined keysym for key \"" + key + "\""; - if (right === undefined) throw "Undefined keysym for key \"" + key + "\""; - if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; - DOMKeyTable[key] = [left, left, right, left]; +function addLeftRight(key, left, right) { + if (left === undefined) throw 'Undefined keysym for key "' + key + '"'; + if (right === undefined) throw 'Undefined keysym for key "' + key + '"'; + if (key in DOMKeyTable) throw 'Duplicate entry for key "' + key + '"'; + DOMKeyTable[key] = [left, left, right, left]; } -function addNumpad(key, standard, numpad) -{ - if (standard === undefined) throw "Undefined keysym for key \"" + key + "\""; - if (numpad === undefined) throw "Undefined keysym for key \"" + key + "\""; - if (key in DOMKeyTable) throw "Duplicate entry for key \"" + key + "\""; - DOMKeyTable[key] = [standard, standard, standard, numpad]; +function addNumpad(key, standard, numpad) { + if (standard === undefined) throw 'Undefined keysym for key "' + key + '"'; + if (numpad === undefined) throw 'Undefined keysym for key "' + key + '"'; + if (key in DOMKeyTable) throw 'Duplicate entry for key "' + key + '"'; + DOMKeyTable[key] = [standard, standard, standard, numpad]; } // 2.2. Modifier Keys -addLeftRight("Alt", KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); -addStandard("AltGraph", KeyTable.XK_ISO_Level3_Shift); -addStandard("CapsLock", KeyTable.XK_Caps_Lock); -addLeftRight("Control", KeyTable.XK_Control_L, KeyTable.XK_Control_R); +addLeftRight('Alt', KeyTable.XK_Alt_L, KeyTable.XK_Alt_R); +addStandard('AltGraph', KeyTable.XK_ISO_Level3_Shift); +addStandard('CapsLock', KeyTable.XK_Caps_Lock); +addLeftRight('Control', KeyTable.XK_Control_L, KeyTable.XK_Control_R); // - Fn // - FnLock -addLeftRight("Hyper", KeyTable.XK_Super_L, KeyTable.XK_Super_R); -addLeftRight("Meta", KeyTable.XK_Super_L, KeyTable.XK_Super_R); -addStandard("NumLock", KeyTable.XK_Num_Lock); -addStandard("ScrollLock", KeyTable.XK_Scroll_Lock); -addLeftRight("Shift", KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); -addLeftRight("Super", KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addLeftRight('Hyper', KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addLeftRight('Meta', KeyTable.XK_Super_L, KeyTable.XK_Super_R); +addStandard('NumLock', KeyTable.XK_Num_Lock); +addStandard('ScrollLock', KeyTable.XK_Scroll_Lock); +addLeftRight('Shift', KeyTable.XK_Shift_L, KeyTable.XK_Shift_R); +addLeftRight('Super', KeyTable.XK_Super_L, KeyTable.XK_Super_R); // - Symbol // - SymbolLock // 2.3. Whitespace Keys -addNumpad("Enter", KeyTable.XK_Return, KeyTable.XK_KP_Enter); -addStandard("Tab", KeyTable.XK_Tab); -addNumpad(" ", KeyTable.XK_space, KeyTable.XK_KP_Space); +addNumpad('Enter', KeyTable.XK_Return, KeyTable.XK_KP_Enter); +addStandard('Tab', KeyTable.XK_Tab); +addNumpad(' ', KeyTable.XK_space, KeyTable.XK_KP_Space); // 2.4. Navigation Keys -addNumpad("ArrowDown", KeyTable.XK_Down, KeyTable.XK_KP_Down); -addNumpad("ArrowUp", KeyTable.XK_Up, KeyTable.XK_KP_Up); -addNumpad("ArrowLeft", KeyTable.XK_Left, KeyTable.XK_KP_Left); -addNumpad("ArrowRight", KeyTable.XK_Right, KeyTable.XK_KP_Right); -addNumpad("End", KeyTable.XK_End, KeyTable.XK_KP_End); -addNumpad("Home", KeyTable.XK_Home, KeyTable.XK_KP_Home); -addNumpad("PageDown", KeyTable.XK_Next, KeyTable.XK_KP_Next); -addNumpad("PageUp", KeyTable.XK_Prior, KeyTable.XK_KP_Prior); +addNumpad('ArrowDown', KeyTable.XK_Down, KeyTable.XK_KP_Down); +addNumpad('ArrowUp', KeyTable.XK_Up, KeyTable.XK_KP_Up); +addNumpad('ArrowLeft', KeyTable.XK_Left, KeyTable.XK_KP_Left); +addNumpad('ArrowRight', KeyTable.XK_Right, KeyTable.XK_KP_Right); +addNumpad('End', KeyTable.XK_End, KeyTable.XK_KP_End); +addNumpad('Home', KeyTable.XK_Home, KeyTable.XK_KP_Home); +addNumpad('PageDown', KeyTable.XK_Next, KeyTable.XK_KP_Next); +addNumpad('PageUp', KeyTable.XK_Prior, KeyTable.XK_KP_Prior); // 2.5. Editing Keys -addStandard("Backspace", KeyTable.XK_BackSpace); -addNumpad("Clear", KeyTable.XK_Clear, KeyTable.XK_KP_Begin); -addStandard("Copy", KeyTable.XF86XK_Copy); +addStandard('Backspace', KeyTable.XK_BackSpace); +addNumpad('Clear', KeyTable.XK_Clear, KeyTable.XK_KP_Begin); +addStandard('Copy', KeyTable.XF86XK_Copy); // - CrSel -addStandard("Cut", KeyTable.XF86XK_Cut); -addNumpad("Delete", KeyTable.XK_Delete, KeyTable.XK_KP_Delete); +addStandard('Cut', KeyTable.XF86XK_Cut); +addNumpad('Delete', KeyTable.XK_Delete, KeyTable.XK_KP_Delete); // - EraseEof // - ExSel -addNumpad("Insert", KeyTable.XK_Insert, KeyTable.XK_KP_Insert); -addStandard("Paste", KeyTable.XF86XK_Paste); -addStandard("Redo", KeyTable.XK_Redo); -addStandard("Undo", KeyTable.XK_Undo); +addNumpad('Insert', KeyTable.XK_Insert, KeyTable.XK_KP_Insert); +addStandard('Paste', KeyTable.XF86XK_Paste); +addStandard('Redo', KeyTable.XK_Redo); +addStandard('Undo', KeyTable.XK_Undo); // 2.6. UI Keys // - Accept // - Again (could just be XK_Redo) // - Attn -addStandard("Cancel", KeyTable.XK_Cancel); -addStandard("ContextMenu", KeyTable.XK_Menu); -addStandard("Escape", KeyTable.XK_Escape); -addStandard("Execute", KeyTable.XK_Execute); -addStandard("Find", KeyTable.XK_Find); -addStandard("Help", KeyTable.XK_Help); -addStandard("Pause", KeyTable.XK_Pause); +addStandard('Cancel', KeyTable.XK_Cancel); +addStandard('ContextMenu', KeyTable.XK_Menu); +addStandard('Escape', KeyTable.XK_Escape); +addStandard('Execute', KeyTable.XK_Execute); +addStandard('Find', KeyTable.XK_Find); +addStandard('Help', KeyTable.XK_Help); +addStandard('Pause', KeyTable.XK_Pause); // - Play // - Props -addStandard("Select", KeyTable.XK_Select); -addStandard("ZoomIn", KeyTable.XF86XK_ZoomIn); -addStandard("ZoomOut", KeyTable.XF86XK_ZoomOut); +addStandard('Select', KeyTable.XK_Select); +addStandard('ZoomIn', KeyTable.XF86XK_ZoomIn); +addStandard('ZoomOut', KeyTable.XF86XK_ZoomOut); // 2.7. Device Keys -addStandard("BrightnessDown", KeyTable.XF86XK_MonBrightnessDown); -addStandard("BrightnessUp", KeyTable.XF86XK_MonBrightnessUp); -addStandard("Eject", KeyTable.XF86XK_Eject); -addStandard("LogOff", KeyTable.XF86XK_LogOff); -addStandard("Power", KeyTable.XF86XK_PowerOff); -addStandard("PowerOff", KeyTable.XF86XK_PowerDown); -addStandard("PrintScreen", KeyTable.XK_Print); -addStandard("Hibernate", KeyTable.XF86XK_Hibernate); -addStandard("Standby", KeyTable.XF86XK_Standby); -addStandard("WakeUp", KeyTable.XF86XK_WakeUp); +addStandard('BrightnessDown', KeyTable.XF86XK_MonBrightnessDown); +addStandard('BrightnessUp', KeyTable.XF86XK_MonBrightnessUp); +addStandard('Eject', KeyTable.XF86XK_Eject); +addStandard('LogOff', KeyTable.XF86XK_LogOff); +addStandard('Power', KeyTable.XF86XK_PowerOff); +addStandard('PowerOff', KeyTable.XF86XK_PowerDown); +addStandard('PrintScreen', KeyTable.XK_Print); +addStandard('Hibernate', KeyTable.XF86XK_Hibernate); +addStandard('Standby', KeyTable.XF86XK_Standby); +addStandard('WakeUp', KeyTable.XF86XK_WakeUp); // 2.8. IME and Composition Keys -addStandard("AllCandidates", KeyTable.XK_MultipleCandidate); -addStandard("Alphanumeric", KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle -addStandard("CodeInput", KeyTable.XK_Codeinput); -addStandard("Compose", KeyTable.XK_Multi_key); -addStandard("Convert", KeyTable.XK_Henkan); +addStandard('AllCandidates', KeyTable.XK_MultipleCandidate); +addStandard('Alphanumeric', KeyTable.XK_Eisu_Shift); // could also be _Eisu_Toggle +addStandard('CodeInput', KeyTable.XK_Codeinput); +addStandard('Compose', KeyTable.XK_Multi_key); +addStandard('Convert', KeyTable.XK_Henkan); // - Dead // - FinalMode -addStandard("GroupFirst", KeyTable.XK_ISO_First_Group); -addStandard("GroupLast", KeyTable.XK_ISO_Last_Group); -addStandard("GroupNext", KeyTable.XK_ISO_Next_Group); -addStandard("GroupPrevious", KeyTable.XK_ISO_Prev_Group); +addStandard('GroupFirst', KeyTable.XK_ISO_First_Group); +addStandard('GroupLast', KeyTable.XK_ISO_Last_Group); +addStandard('GroupNext', KeyTable.XK_ISO_Next_Group); +addStandard('GroupPrevious', KeyTable.XK_ISO_Prev_Group); // - ModeChange (XK_Mode_switch is often used for AltGr) // - NextCandidate -addStandard("NonConvert", KeyTable.XK_Muhenkan); -addStandard("PreviousCandidate", KeyTable.XK_PreviousCandidate); +addStandard('NonConvert', KeyTable.XK_Muhenkan); +addStandard('PreviousCandidate', KeyTable.XK_PreviousCandidate); // - Process -addStandard("SingleCandidate", KeyTable.XK_SingleCandidate); -addStandard("HangulMode", KeyTable.XK_Hangul); -addStandard("HanjaMode", KeyTable.XK_Hangul_Hanja); -addStandard("JunjuaMode", KeyTable.XK_Hangul_Jeonja); -addStandard("Eisu", KeyTable.XK_Eisu_toggle); -addStandard("Hankaku", KeyTable.XK_Hankaku); -addStandard("Hiragana", KeyTable.XK_Hiragana); -addStandard("HiraganaKatakana", KeyTable.XK_Hiragana_Katakana); -addStandard("KanaMode", KeyTable.XK_Kana_Shift); // could also be _Kana_Lock -addStandard("KanjiMode", KeyTable.XK_Kanji); -addStandard("Katakana", KeyTable.XK_Katakana); -addStandard("Romaji", KeyTable.XK_Romaji); -addStandard("Zenkaku", KeyTable.XK_Zenkaku); -addStandard("ZenkakuHanaku", KeyTable.XK_Zenkaku_Hankaku); +addStandard('SingleCandidate', KeyTable.XK_SingleCandidate); +addStandard('HangulMode', KeyTable.XK_Hangul); +addStandard('HanjaMode', KeyTable.XK_Hangul_Hanja); +addStandard('JunjuaMode', KeyTable.XK_Hangul_Jeonja); +addStandard('Eisu', KeyTable.XK_Eisu_toggle); +addStandard('Hankaku', KeyTable.XK_Hankaku); +addStandard('Hiragana', KeyTable.XK_Hiragana); +addStandard('HiraganaKatakana', KeyTable.XK_Hiragana_Katakana); +addStandard('KanaMode', KeyTable.XK_Kana_Shift); // could also be _Kana_Lock +addStandard('KanjiMode', KeyTable.XK_Kanji); +addStandard('Katakana', KeyTable.XK_Katakana); +addStandard('Romaji', KeyTable.XK_Romaji); +addStandard('Zenkaku', KeyTable.XK_Zenkaku); +addStandard('ZenkakuHanaku', KeyTable.XK_Zenkaku_Hankaku); // 2.9. General-Purpose Function Keys -addStandard("F1", KeyTable.XK_F1); -addStandard("F2", KeyTable.XK_F2); -addStandard("F3", KeyTable.XK_F3); -addStandard("F4", KeyTable.XK_F4); -addStandard("F5", KeyTable.XK_F5); -addStandard("F6", KeyTable.XK_F6); -addStandard("F7", KeyTable.XK_F7); -addStandard("F8", KeyTable.XK_F8); -addStandard("F9", KeyTable.XK_F9); -addStandard("F10", KeyTable.XK_F10); -addStandard("F11", KeyTable.XK_F11); -addStandard("F12", KeyTable.XK_F12); -addStandard("F13", KeyTable.XK_F13); -addStandard("F14", KeyTable.XK_F14); -addStandard("F15", KeyTable.XK_F15); -addStandard("F16", KeyTable.XK_F16); -addStandard("F17", KeyTable.XK_F17); -addStandard("F18", KeyTable.XK_F18); -addStandard("F19", KeyTable.XK_F19); -addStandard("F20", KeyTable.XK_F20); -addStandard("F21", KeyTable.XK_F21); -addStandard("F22", KeyTable.XK_F22); -addStandard("F23", KeyTable.XK_F23); -addStandard("F24", KeyTable.XK_F24); -addStandard("F25", KeyTable.XK_F25); -addStandard("F26", KeyTable.XK_F26); -addStandard("F27", KeyTable.XK_F27); -addStandard("F28", KeyTable.XK_F28); -addStandard("F29", KeyTable.XK_F29); -addStandard("F30", KeyTable.XK_F30); -addStandard("F31", KeyTable.XK_F31); -addStandard("F32", KeyTable.XK_F32); -addStandard("F33", KeyTable.XK_F33); -addStandard("F34", KeyTable.XK_F34); -addStandard("F35", KeyTable.XK_F35); +addStandard('F1', KeyTable.XK_F1); +addStandard('F2', KeyTable.XK_F2); +addStandard('F3', KeyTable.XK_F3); +addStandard('F4', KeyTable.XK_F4); +addStandard('F5', KeyTable.XK_F5); +addStandard('F6', KeyTable.XK_F6); +addStandard('F7', KeyTable.XK_F7); +addStandard('F8', KeyTable.XK_F8); +addStandard('F9', KeyTable.XK_F9); +addStandard('F10', KeyTable.XK_F10); +addStandard('F11', KeyTable.XK_F11); +addStandard('F12', KeyTable.XK_F12); +addStandard('F13', KeyTable.XK_F13); +addStandard('F14', KeyTable.XK_F14); +addStandard('F15', KeyTable.XK_F15); +addStandard('F16', KeyTable.XK_F16); +addStandard('F17', KeyTable.XK_F17); +addStandard('F18', KeyTable.XK_F18); +addStandard('F19', KeyTable.XK_F19); +addStandard('F20', KeyTable.XK_F20); +addStandard('F21', KeyTable.XK_F21); +addStandard('F22', KeyTable.XK_F22); +addStandard('F23', KeyTable.XK_F23); +addStandard('F24', KeyTable.XK_F24); +addStandard('F25', KeyTable.XK_F25); +addStandard('F26', KeyTable.XK_F26); +addStandard('F27', KeyTable.XK_F27); +addStandard('F28', KeyTable.XK_F28); +addStandard('F29', KeyTable.XK_F29); +addStandard('F30', KeyTable.XK_F30); +addStandard('F31', KeyTable.XK_F31); +addStandard('F32', KeyTable.XK_F32); +addStandard('F33', KeyTable.XK_F33); +addStandard('F34', KeyTable.XK_F34); +addStandard('F35', KeyTable.XK_F35); // - Soft1... // 2.10. Multimedia Keys // - ChannelDown // - ChannelUp -addStandard("Close", KeyTable.XF86XK_Close); -addStandard("MailForward", KeyTable.XF86XK_MailForward); -addStandard("MailReply", KeyTable.XF86XK_Reply); -addStandard("MainSend", KeyTable.XF86XK_Send); -addStandard("MediaFastForward", KeyTable.XF86XK_AudioForward); -addStandard("MediaPause", KeyTable.XF86XK_AudioPause); -addStandard("MediaPlay", KeyTable.XF86XK_AudioPlay); -addStandard("MediaRecord", KeyTable.XF86XK_AudioRecord); -addStandard("MediaRewind", KeyTable.XF86XK_AudioRewind); -addStandard("MediaStop", KeyTable.XF86XK_AudioStop); -addStandard("MediaTrackNext", KeyTable.XF86XK_AudioNext); -addStandard("MediaTrackPrevious", KeyTable.XF86XK_AudioPrev); -addStandard("New", KeyTable.XF86XK_New); -addStandard("Open", KeyTable.XF86XK_Open); -addStandard("Print", KeyTable.XK_Print); -addStandard("Save", KeyTable.XF86XK_Save); -addStandard("SpellCheck", KeyTable.XF86XK_Spell); +addStandard('Close', KeyTable.XF86XK_Close); +addStandard('MailForward', KeyTable.XF86XK_MailForward); +addStandard('MailReply', KeyTable.XF86XK_Reply); +addStandard('MainSend', KeyTable.XF86XK_Send); +addStandard('MediaFastForward', KeyTable.XF86XK_AudioForward); +addStandard('MediaPause', KeyTable.XF86XK_AudioPause); +addStandard('MediaPlay', KeyTable.XF86XK_AudioPlay); +addStandard('MediaRecord', KeyTable.XF86XK_AudioRecord); +addStandard('MediaRewind', KeyTable.XF86XK_AudioRewind); +addStandard('MediaStop', KeyTable.XF86XK_AudioStop); +addStandard('MediaTrackNext', KeyTable.XF86XK_AudioNext); +addStandard('MediaTrackPrevious', KeyTable.XF86XK_AudioPrev); +addStandard('New', KeyTable.XF86XK_New); +addStandard('Open', KeyTable.XF86XK_Open); +addStandard('Print', KeyTable.XK_Print); +addStandard('Save', KeyTable.XF86XK_Save); +addStandard('SpellCheck', KeyTable.XF86XK_Spell); // 2.11. Multimedia Numpad Keys @@ -231,13 +228,13 @@ addStandard("SpellCheck", KeyTable.XF86XK_Spell); // - AudioSurroundModeNext // - AudioTrebleDown // - AudioTrebleUp -addStandard("AudioVolumeDown", KeyTable.XF86XK_AudioLowerVolume); -addStandard("AudioVolumeUp", KeyTable.XF86XK_AudioRaiseVolume); -addStandard("AudioVolumeMute", KeyTable.XF86XK_AudioMute); +addStandard('AudioVolumeDown', KeyTable.XF86XK_AudioLowerVolume); +addStandard('AudioVolumeUp', KeyTable.XF86XK_AudioRaiseVolume); +addStandard('AudioVolumeMute', KeyTable.XF86XK_AudioMute); // - MicrophoneToggle // - MicrophoneVolumeDown // - MicrophoneVolumeUp -addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute); +addStandard('MicrophoneVolumeMute', KeyTable.XF86XK_AudioMicMute); // 2.13. Speech Keys @@ -246,28 +243,28 @@ addStandard("MicrophoneVolumeMute", KeyTable.XF86XK_AudioMicMute); // 2.14. Application Keys -addStandard("LaunchCalculator", KeyTable.XF86XK_Calculator); -addStandard("LaunchCalendar", KeyTable.XF86XK_Calendar); -addStandard("LaunchMail", KeyTable.XF86XK_Mail); -addStandard("LaunchMediaPlayer", KeyTable.XF86XK_AudioMedia); -addStandard("LaunchMusicPlayer", KeyTable.XF86XK_Music); -addStandard("LaunchMyComputer", KeyTable.XF86XK_MyComputer); -addStandard("LaunchPhone", KeyTable.XF86XK_Phone); -addStandard("LaunchScreenSaver", KeyTable.XF86XK_ScreenSaver); -addStandard("LaunchSpreadsheet", KeyTable.XF86XK_Excel); -addStandard("LaunchWebBrowser", KeyTable.XF86XK_WWW); -addStandard("LaunchWebCam", KeyTable.XF86XK_WebCam); -addStandard("LaunchWordProcessor", KeyTable.XF86XK_Word); +addStandard('LaunchCalculator', KeyTable.XF86XK_Calculator); +addStandard('LaunchCalendar', KeyTable.XF86XK_Calendar); +addStandard('LaunchMail', KeyTable.XF86XK_Mail); +addStandard('LaunchMediaPlayer', KeyTable.XF86XK_AudioMedia); +addStandard('LaunchMusicPlayer', KeyTable.XF86XK_Music); +addStandard('LaunchMyComputer', KeyTable.XF86XK_MyComputer); +addStandard('LaunchPhone', KeyTable.XF86XK_Phone); +addStandard('LaunchScreenSaver', KeyTable.XF86XK_ScreenSaver); +addStandard('LaunchSpreadsheet', KeyTable.XF86XK_Excel); +addStandard('LaunchWebBrowser', KeyTable.XF86XK_WWW); +addStandard('LaunchWebCam', KeyTable.XF86XK_WebCam); +addStandard('LaunchWordProcessor', KeyTable.XF86XK_Word); // 2.15. Browser Keys -addStandard("BrowserBack", KeyTable.XF86XK_Back); -addStandard("BrowserFavorites", KeyTable.XF86XK_Favorites); -addStandard("BrowserForward", KeyTable.XF86XK_Forward); -addStandard("BrowserHome", KeyTable.XF86XK_HomePage); -addStandard("BrowserRefresh", KeyTable.XF86XK_Refresh); -addStandard("BrowserSearch", KeyTable.XF86XK_Search); -addStandard("BrowserStop", KeyTable.XF86XK_Stop); +addStandard('BrowserBack', KeyTable.XF86XK_Back); +addStandard('BrowserFavorites', KeyTable.XF86XK_Favorites); +addStandard('BrowserForward', KeyTable.XF86XK_Forward); +addStandard('BrowserHome', KeyTable.XF86XK_HomePage); +addStandard('BrowserRefresh', KeyTable.XF86XK_Refresh); +addStandard('BrowserSearch', KeyTable.XF86XK_Search); +addStandard('BrowserStop', KeyTable.XF86XK_Stop); // 2.16. Mobile Phone Keys @@ -280,31 +277,31 @@ addStandard("BrowserStop", KeyTable.XF86XK_Stop); // 2.18. Media Controller Keys // - A whole bunch... -addStandard("Dimmer", KeyTable.XF86XK_BrightnessAdjust); -addStandard("MediaAudioTrack", KeyTable.XF86XK_AudioCycleTrack); -addStandard("RandomToggle", KeyTable.XF86XK_AudioRandomPlay); -addStandard("SplitScreenToggle", KeyTable.XF86XK_SplitScreen); -addStandard("Subtitle", KeyTable.XF86XK_Subtitle); -addStandard("VideoModeNext", KeyTable.XF86XK_Next_VMode); +addStandard('Dimmer', KeyTable.XF86XK_BrightnessAdjust); +addStandard('MediaAudioTrack', KeyTable.XF86XK_AudioCycleTrack); +addStandard('RandomToggle', KeyTable.XF86XK_AudioRandomPlay); +addStandard('SplitScreenToggle', KeyTable.XF86XK_SplitScreen); +addStandard('Subtitle', KeyTable.XF86XK_Subtitle); +addStandard('VideoModeNext', KeyTable.XF86XK_Next_VMode); // Extra: Numpad -addNumpad("=", KeyTable.XK_equal, KeyTable.XK_KP_Equal); -addNumpad("+", KeyTable.XK_plus, KeyTable.XK_KP_Add); -addNumpad("-", KeyTable.XK_minus, KeyTable.XK_KP_Subtract); -addNumpad("*", KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply); -addNumpad("/", KeyTable.XK_slash, KeyTable.XK_KP_Divide); -addNumpad(".", KeyTable.XK_period, KeyTable.XK_KP_Decimal); -addNumpad(",", KeyTable.XK_comma, KeyTable.XK_KP_Separator); -addNumpad("0", KeyTable.XK_0, KeyTable.XK_KP_0); -addNumpad("1", KeyTable.XK_1, KeyTable.XK_KP_1); -addNumpad("2", KeyTable.XK_2, KeyTable.XK_KP_2); -addNumpad("3", KeyTable.XK_3, KeyTable.XK_KP_3); -addNumpad("4", KeyTable.XK_4, KeyTable.XK_KP_4); -addNumpad("5", KeyTable.XK_5, KeyTable.XK_KP_5); -addNumpad("6", KeyTable.XK_6, KeyTable.XK_KP_6); -addNumpad("7", KeyTable.XK_7, KeyTable.XK_KP_7); -addNumpad("8", KeyTable.XK_8, KeyTable.XK_KP_8); -addNumpad("9", KeyTable.XK_9, KeyTable.XK_KP_9); +addNumpad('=', KeyTable.XK_equal, KeyTable.XK_KP_Equal); +addNumpad('+', KeyTable.XK_plus, KeyTable.XK_KP_Add); +addNumpad('-', KeyTable.XK_minus, KeyTable.XK_KP_Subtract); +addNumpad('*', KeyTable.XK_asterisk, KeyTable.XK_KP_Multiply); +addNumpad('/', KeyTable.XK_slash, KeyTable.XK_KP_Divide); +addNumpad('.', KeyTable.XK_period, KeyTable.XK_KP_Decimal); +addNumpad(',', KeyTable.XK_comma, KeyTable.XK_KP_Separator); +addNumpad('0', KeyTable.XK_0, KeyTable.XK_KP_0); +addNumpad('1', KeyTable.XK_1, KeyTable.XK_KP_1); +addNumpad('2', KeyTable.XK_2, KeyTable.XK_KP_2); +addNumpad('3', KeyTable.XK_3, KeyTable.XK_KP_3); +addNumpad('4', KeyTable.XK_4, KeyTable.XK_KP_4); +addNumpad('5', KeyTable.XK_5, KeyTable.XK_KP_5); +addNumpad('6', KeyTable.XK_6, KeyTable.XK_KP_6); +addNumpad('7', KeyTable.XK_7, KeyTable.XK_KP_7); +addNumpad('8', KeyTable.XK_8, KeyTable.XK_KP_8); +addNumpad('9', KeyTable.XK_9, KeyTable.XK_KP_9); export default DOMKeyTable; diff --git a/core/input/fixedkeys.js b/core/input/fixedkeys.js index 6dd42223..34494fb4 100644 --- a/core/input/fixedkeys.js +++ b/core/input/fixedkeys.js @@ -18,110 +18,110 @@ export default { // 3.1.1.1. Writing System Keys - 'Backspace': 'Backspace', + Backspace: 'Backspace', -// 3.1.1.2. Functional Keys + // 3.1.1.2. Functional Keys - 'AltLeft': 'Alt', - 'AltRight': 'Alt', // This could also be 'AltGraph' - 'CapsLock': 'CapsLock', - 'ContextMenu': 'ContextMenu', - 'ControlLeft': 'Control', - 'ControlRight': 'Control', - 'Enter': 'Enter', - 'MetaLeft': 'Meta', - 'MetaRight': 'Meta', - 'ShiftLeft': 'Shift', - 'ShiftRight': 'Shift', - 'Tab': 'Tab', - // FIXME: Japanese/Korean keys + AltLeft: 'Alt', + AltRight: 'Alt', // This could also be 'AltGraph' + CapsLock: 'CapsLock', + ContextMenu: 'ContextMenu', + ControlLeft: 'Control', + ControlRight: 'Control', + Enter: 'Enter', + MetaLeft: 'Meta', + MetaRight: 'Meta', + ShiftLeft: 'Shift', + ShiftRight: 'Shift', + Tab: 'Tab', + // FIXME: Japanese/Korean keys -// 3.1.2. Control Pad Section + // 3.1.2. Control Pad Section - 'Delete': 'Delete', - 'End': 'End', - 'Help': 'Help', - 'Home': 'Home', - 'Insert': 'Insert', - 'PageDown': 'PageDown', - 'PageUp': 'PageUp', + Delete: 'Delete', + End: 'End', + Help: 'Help', + Home: 'Home', + Insert: 'Insert', + PageDown: 'PageDown', + PageUp: 'PageUp', -// 3.1.3. Arrow Pad Section + // 3.1.3. Arrow Pad Section - 'ArrowDown': 'ArrowDown', - 'ArrowLeft': 'ArrowLeft', - 'ArrowRight': 'ArrowRight', - 'ArrowUp': 'ArrowUp', + ArrowDown: 'ArrowDown', + ArrowLeft: 'ArrowLeft', + ArrowRight: 'ArrowRight', + ArrowUp: 'ArrowUp', -// 3.1.4. Numpad Section + // 3.1.4. Numpad Section - 'NumLock': 'NumLock', - 'NumpadBackspace': 'Backspace', - 'NumpadClear': 'Clear', + NumLock: 'NumLock', + NumpadBackspace: 'Backspace', + NumpadClear: 'Clear', -// 3.1.5. Function Section + // 3.1.5. Function Section - 'Escape': 'Escape', - 'F1': 'F1', - 'F2': 'F2', - 'F3': 'F3', - 'F4': 'F4', - 'F5': 'F5', - 'F6': 'F6', - 'F7': 'F7', - 'F8': 'F8', - 'F9': 'F9', - 'F10': 'F10', - 'F11': 'F11', - 'F12': 'F12', - 'F13': 'F13', - 'F14': 'F14', - 'F15': 'F15', - 'F16': 'F16', - 'F17': 'F17', - 'F18': 'F18', - 'F19': 'F19', - 'F20': 'F20', - 'F21': 'F21', - 'F22': 'F22', - 'F23': 'F23', - 'F24': 'F24', - 'F25': 'F25', - 'F26': 'F26', - 'F27': 'F27', - 'F28': 'F28', - 'F29': 'F29', - 'F30': 'F30', - 'F31': 'F31', - 'F32': 'F32', - 'F33': 'F33', - 'F34': 'F34', - 'F35': 'F35', - 'PrintScreen': 'PrintScreen', - 'ScrollLock': 'ScrollLock', - 'Pause': 'Pause', + Escape: 'Escape', + F1: 'F1', + F2: 'F2', + F3: 'F3', + F4: 'F4', + F5: 'F5', + F6: 'F6', + F7: 'F7', + F8: 'F8', + F9: 'F9', + F10: 'F10', + F11: 'F11', + F12: 'F12', + F13: 'F13', + F14: 'F14', + F15: 'F15', + F16: 'F16', + F17: 'F17', + F18: 'F18', + F19: 'F19', + F20: 'F20', + F21: 'F21', + F22: 'F22', + F23: 'F23', + F24: 'F24', + F25: 'F25', + F26: 'F26', + F27: 'F27', + F28: 'F28', + F29: 'F29', + F30: 'F30', + F31: 'F31', + F32: 'F32', + F33: 'F33', + F34: 'F34', + F35: 'F35', + PrintScreen: 'PrintScreen', + ScrollLock: 'ScrollLock', + Pause: 'Pause', -// 3.1.6. Media Keys + // 3.1.6. Media Keys - 'BrowserBack': 'BrowserBack', - 'BrowserFavorites': 'BrowserFavorites', - 'BrowserForward': 'BrowserForward', - 'BrowserHome': 'BrowserHome', - 'BrowserRefresh': 'BrowserRefresh', - 'BrowserSearch': 'BrowserSearch', - 'BrowserStop': 'BrowserStop', - 'Eject': 'Eject', - 'LaunchApp1': 'LaunchMyComputer', - 'LaunchApp2': 'LaunchCalendar', - 'LaunchMail': 'LaunchMail', - 'MediaPlayPause': 'MediaPlay', - 'MediaStop': 'MediaStop', - 'MediaTrackNext': 'MediaTrackNext', - 'MediaTrackPrevious': 'MediaTrackPrevious', - 'Power': 'Power', - 'Sleep': 'Sleep', - 'AudioVolumeDown': 'AudioVolumeDown', - 'AudioVolumeMute': 'AudioVolumeMute', - 'AudioVolumeUp': 'AudioVolumeUp', - 'WakeUp': 'WakeUp', + BrowserBack: 'BrowserBack', + BrowserFavorites: 'BrowserFavorites', + BrowserForward: 'BrowserForward', + BrowserHome: 'BrowserHome', + BrowserRefresh: 'BrowserRefresh', + BrowserSearch: 'BrowserSearch', + BrowserStop: 'BrowserStop', + Eject: 'Eject', + LaunchApp1: 'LaunchMyComputer', + LaunchApp2: 'LaunchCalendar', + LaunchMail: 'LaunchMail', + MediaPlayPause: 'MediaPlay', + MediaStop: 'MediaStop', + MediaTrackNext: 'MediaTrackNext', + MediaTrackPrevious: 'MediaTrackPrevious', + Power: 'Power', + Sleep: 'Sleep', + AudioVolumeDown: 'AudioVolumeDown', + AudioVolumeMute: 'AudioVolumeMute', + AudioVolumeUp: 'AudioVolumeUp', + WakeUp: 'WakeUp', }; diff --git a/core/input/keyboard.js b/core/input/keyboard.js index 2e7b90ab..249b9856 100644 --- a/core/input/keyboard.js +++ b/core/input/keyboard.js @@ -7,364 +7,365 @@ import * as Log from '../util/logging.js'; import { stopEvent } from '../util/events.js'; -import * as KeyboardUtil from "./util.js"; -import KeyTable from "./keysym.js"; -import * as browser from "../util/browser.js"; +import * as KeyboardUtil from './util.js'; +import KeyTable from './keysym.js'; +import * as browser from '../util/browser.js'; // // Keyboard event handler // export default class Keyboard { - constructor(target) { - this._target = target || null; + constructor(target) { + this._target = target || null; - this._keyDownList = {}; // List of depressed keys - // (even if they are happy) - this._pendingKey = null; // Key waiting for keypress - this._altGrArmed = false; // Windows AltGr detection + this._keyDownList = {}; // List of depressed keys + // (even if they are happy) + this._pendingKey = null; // Key waiting for keypress + this._altGrArmed = false; // Windows AltGr detection - // keep these here so we can refer to them later - this._eventHandlers = { - 'keyup': this._handleKeyUp.bind(this), - 'keydown': this._handleKeyDown.bind(this), - 'keypress': this._handleKeyPress.bind(this), - 'blur': this._allKeysUp.bind(this), - 'checkalt': this._checkAlt.bind(this), - }; + // keep these here so we can refer to them later + this._eventHandlers = { + keyup: this._handleKeyUp.bind(this), + keydown: this._handleKeyDown.bind(this), + keypress: this._handleKeyPress.bind(this), + blur: this._allKeysUp.bind(this), + checkalt: this._checkAlt.bind(this), + }; - // ===== EVENT HANDLERS ===== + // ===== EVENT HANDLERS ===== - this.onkeyevent = () => {}; // Handler for key press/release + this.onkeyevent = () => {}; // Handler for key press/release + } + + // ===== PRIVATE METHODS ===== + + _sendKeyEvent(keysym, code, down) { + if (down) { + this._keyDownList[code] = keysym; + } else { + // Do we really think this key is down? + if (!(code in this._keyDownList)) { + return; + } + delete this._keyDownList[code]; } - // ===== PRIVATE METHODS ===== + Log.Debug('onkeyevent ' + (down ? 'down' : 'up') + + ', keysym: ' + keysym, ', code: ' + code); + this.onkeyevent(keysym, code, down); + } - _sendKeyEvent(keysym, code, down) { - if (down) { - this._keyDownList[code] = keysym; - } else { - // Do we really think this key is down? - if (!(code in this._keyDownList)) { - return; - } - delete this._keyDownList[code]; - } - - Log.Debug("onkeyevent " + (down ? "down" : "up") + - ", keysym: " + keysym, ", code: " + code); - this.onkeyevent(keysym, code, down); + _getKeyCode(e) { + const code = KeyboardUtil.getKeycode(e); + if (code !== 'Unidentified') { + return code; } - _getKeyCode(e) { - const code = KeyboardUtil.getKeycode(e); - if (code !== 'Unidentified') { - return code; - } - - // Unstable, but we don't have anything else to go on - // (don't use it for 'keypress' events thought since - // WebKit sets it to the same as charCode) - if (e.keyCode && (e.type !== 'keypress')) { - // 229 is used for composition events - if (e.keyCode !== 229) { - return 'Platform' + e.keyCode; - } - } - - // A precursor to the final DOM3 standard. Unfortunately it - // is not layout independent, so it is as bad as using keyCode - if (e.keyIdentifier) { - // Non-character key? - if (e.keyIdentifier.substr(0, 2) !== 'U+') { - return e.keyIdentifier; - } - - const codepoint = parseInt(e.keyIdentifier.substr(2), 16); - const char = String.fromCharCode(codepoint).toUpperCase(); - - return 'Platform' + char.charCodeAt(); - } - - return 'Unidentified'; + // Unstable, but we don't have anything else to go on + // (don't use it for 'keypress' events thought since + // WebKit sets it to the same as charCode) + if (e.keyCode && (e.type !== 'keypress')) { + // 229 is used for composition events + if (e.keyCode !== 229) { + return 'Platform' + e.keyCode; + } } - _handleKeyDown(e) { - const code = this._getKeyCode(e); - let keysym = KeyboardUtil.getKeysym(e); + // A precursor to the final DOM3 standard. Unfortunately it + // is not layout independent, so it is as bad as using keyCode + if (e.keyIdentifier) { + // Non-character key? + if (e.keyIdentifier.substr(0, 2) !== 'U+') { + return e.keyIdentifier; + } - // Windows doesn't have a proper AltGr, but handles it using - // fake Ctrl+Alt. However the remote end might not be Windows, - // so we need to merge those in to a single AltGr event. We - // detect this case by seeing the two key events directly after - // each other with a very short time between them (<50ms). - if (this._altGrArmed) { - this._altGrArmed = false; - clearTimeout(this._altGrTimeout); + const codepoint = parseInt(e.keyIdentifier.substr(2), 16); + const char = String.fromCharCode(codepoint).toUpperCase(); - if ((code === "AltRight") && - ((e.timeStamp - this._altGrCtrlTime) < 50)) { - // FIXME: We fail to detect this if either Ctrl key is - // first manually pressed as Windows then no - // longer sends the fake Ctrl down event. It - // does however happily send real Ctrl events - // even when AltGr is already down. Some - // browsers detect this for us though and set the - // key to "AltGraph". - keysym = KeyTable.XK_ISO_Level3_Shift; - } else { - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); - } - } + return 'Platform' + char.charCodeAt(); + } - // We cannot handle keys we cannot track, but we also need - // to deal with virtual keyboards which omit key info - // (iOS omits tracking info on keyup events, which forces us to - // special treat that platform here) - if ((code === 'Unidentified') || browser.isIOS()) { - if (keysym) { - // If it's a virtual keyboard then it should be - // sufficient to just send press and release right - // after each other - this._sendKeyEvent(keysym, code, true); - this._sendKeyEvent(keysym, code, false); - } + return 'Unidentified'; + } - stopEvent(e); - return; - } + _handleKeyDown(e) { + const code = this._getKeyCode(e); + let keysym = KeyboardUtil.getKeysym(e); - // Alt behaves more like AltGraph on macOS, so shuffle the - // keys around a bit to make things more sane for the remote - // server. This method is used by RealVNC and TigerVNC (and - // possibly others). - if (browser.isMac()) { - switch (keysym) { - case KeyTable.XK_Super_L: - keysym = KeyTable.XK_Alt_L; - break; - case KeyTable.XK_Super_R: - keysym = KeyTable.XK_Super_L; - break; - case KeyTable.XK_Alt_L: - keysym = KeyTable.XK_Mode_switch; - break; - case KeyTable.XK_Alt_R: - keysym = KeyTable.XK_ISO_Level3_Shift; - break; - } - } + // Windows doesn't have a proper AltGr, but handles it using + // fake Ctrl+Alt. However the remote end might not be Windows, + // so we need to merge those in to a single AltGr event. We + // detect this case by seeing the two key events directly after + // each other with a very short time between them (<50ms). + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); - // Is this key already pressed? If so, then we must use the - // same keysym or we'll confuse the server - if (code in this._keyDownList) { - keysym = this._keyDownList[code]; - } - - // macOS doesn't send proper key events for modifiers, only - // state change events. That gets extra confusing for CapsLock - // which toggles on each press, but not on release. So pretend - // it was a quick press and release of the button. - if (browser.isMac() && (code === 'CapsLock')) { - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); - stopEvent(e); - return; - } - - // If this is a legacy browser then we'll need to wait for - // a keypress event as well - // (IE and Edge has a broken KeyboardEvent.key, so we can't - // just check for the presence of that field) - if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) { - this._pendingKey = code; - // However we might not get a keypress event if the key - // is non-printable, which needs some special fallback - // handling - setTimeout(this._handleKeyPressTimeout.bind(this), 10, e); - return; - } - - this._pendingKey = null; - stopEvent(e); - - // Possible start of AltGr sequence? (see above) - if ((code === "ControlLeft") && browser.isWindows() && - !("ControlLeft" in this._keyDownList)) { - this._altGrArmed = true; - this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); - this._altGrCtrlTime = e.timeStamp; - return; - } + if ((code === 'AltRight') + && ((e.timeStamp - this._altGrCtrlTime) < 50)) { + // FIXME: We fail to detect this if either Ctrl key is + // first manually pressed as Windows then no + // longer sends the fake Ctrl down event. It + // does however happily send real Ctrl events + // even when AltGr is already down. Some + // browsers detect this for us though and set the + // key to "AltGraph". + keysym = KeyTable.XK_ISO_Level3_Shift; + } else { + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + } + // We cannot handle keys we cannot track, but we also need + // to deal with virtual keyboards which omit key info + // (iOS omits tracking info on keyup events, which forces us to + // special treat that platform here) + if ((code === 'Unidentified') || browser.isIOS()) { + if (keysym) { + // If it's a virtual keyboard then it should be + // sufficient to just send press and release right + // after each other this._sendKeyEvent(keysym, code, true); + this._sendKeyEvent(keysym, code, false); + } + + stopEvent(e); + return; } - // Legacy event for browsers without code/key - _handleKeyPress(e) { - stopEvent(e); - - // Are we expecting a keypress? - if (this._pendingKey === null) { - return; - } - - let code = this._getKeyCode(e); - const keysym = KeyboardUtil.getKeysym(e); - - // The key we were waiting for? - if ((code !== 'Unidentified') && (code != this._pendingKey)) { - return; - } - - code = this._pendingKey; - this._pendingKey = null; - - if (!keysym) { - Log.Info('keypress with no keysym:', e); - return; - } - - this._sendKeyEvent(keysym, code, true); + // Alt behaves more like AltGraph on macOS, so shuffle the + // keys around a bit to make things more sane for the remote + // server. This method is used by RealVNC and TigerVNC (and + // possibly others). + if (browser.isMac()) { + switch (keysym) { + case KeyTable.XK_Super_L: + keysym = KeyTable.XK_Alt_L; + break; + case KeyTable.XK_Super_R: + keysym = KeyTable.XK_Super_L; + break; + case KeyTable.XK_Alt_L: + keysym = KeyTable.XK_Mode_switch; + break; + case KeyTable.XK_Alt_R: + keysym = KeyTable.XK_ISO_Level3_Shift; + break; + } } - _handleKeyPressTimeout(e) { - // Did someone manage to sort out the key already? - if (this._pendingKey === null) { - return; - } - - let keysym; - - const code = this._pendingKey; - this._pendingKey = null; - - // We have no way of knowing the proper keysym with the - // information given, but the following are true for most - // layouts - if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) { - // Digit - keysym = e.keyCode; - } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) { - // Character (A-Z) - let char = String.fromCharCode(e.keyCode); - // A feeble attempt at the correct case - if (e.shiftKey) - char = char.toUpperCase(); - else - char = char.toLowerCase(); - keysym = char.charCodeAt(); - } else { - // Unknown, give up - keysym = 0; - } - - this._sendKeyEvent(keysym, code, true); + // Is this key already pressed? If so, then we must use the + // same keysym or we'll confuse the server + if (code in this._keyDownList) { + keysym = this._keyDownList[code]; } - _handleKeyUp(e) { - stopEvent(e); - - const code = this._getKeyCode(e); - - // We can't get a release in the middle of an AltGr sequence, so - // abort that detection - if (this._altGrArmed) { - this._altGrArmed = false; - clearTimeout(this._altGrTimeout); - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); - } - - // See comment in _handleKeyDown() - if (browser.isMac() && (code === 'CapsLock')) { - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); - this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); - return; - } - - this._sendKeyEvent(this._keyDownList[code], code, false); + // macOS doesn't send proper key events for modifiers, only + // state change events. That gets extra confusing for CapsLock + // which toggles on each press, but not on release. So pretend + // it was a quick press and release of the button. + if (browser.isMac() && (code === 'CapsLock')) { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + stopEvent(e); + return; } - _handleAltGrTimeout() { - this._altGrArmed = false; - clearTimeout(this._altGrTimeout); - this._sendKeyEvent(KeyTable.XK_Control_L, "ControlLeft", true); + // If this is a legacy browser then we'll need to wait for + // a keypress event as well + // (IE and Edge has a broken KeyboardEvent.key, so we can't + // just check for the presence of that field) + if (!keysym && (!e.key || browser.isIE() || browser.isEdge())) { + this._pendingKey = code; + // However we might not get a keypress event if the key + // is non-printable, which needs some special fallback + // handling + setTimeout(this._handleKeyPressTimeout.bind(this), 10, e); + return; } - _allKeysUp() { - Log.Debug(">> Keyboard.allKeysUp"); - for (let code in this._keyDownList) { - this._sendKeyEvent(this._keyDownList[code], code, false); - } - Log.Debug("<< Keyboard.allKeysUp"); + this._pendingKey = null; + stopEvent(e); + + // Possible start of AltGr sequence? (see above) + if ((code === 'ControlLeft') && browser.isWindows() + && !('ControlLeft' in this._keyDownList)) { + this._altGrArmed = true; + this._altGrTimeout = setTimeout(this._handleAltGrTimeout.bind(this), 100); + this._altGrCtrlTime = e.timeStamp; + return; } - // Firefox Alt workaround, see below - _checkAlt(e) { - if (e.altKey) { - return; - } + this._sendKeyEvent(keysym, code, true); + } - const target = this._target; - const downList = this._keyDownList; - ['AltLeft', 'AltRight'].forEach((code) => { - if (!(code in downList)) { - return; - } + // Legacy event for browsers without code/key + _handleKeyPress(e) { + stopEvent(e); - const event = new KeyboardEvent('keyup', - { key: downList[code], - code: code }); - target.dispatchEvent(event); + // Are we expecting a keypress? + if (this._pendingKey === null) { + return; + } + + let code = this._getKeyCode(e); + const keysym = KeyboardUtil.getKeysym(e); + + // The key we were waiting for? + if ((code !== 'Unidentified') && (code != this._pendingKey)) { + return; + } + + code = this._pendingKey; + this._pendingKey = null; + + if (!keysym) { + Log.Info('keypress with no keysym:', e); + return; + } + + this._sendKeyEvent(keysym, code, true); + } + + _handleKeyPressTimeout(e) { + // Did someone manage to sort out the key already? + if (this._pendingKey === null) { + return; + } + + let keysym; + + const code = this._pendingKey; + this._pendingKey = null; + + // We have no way of knowing the proper keysym with the + // information given, but the following are true for most + // layouts + if ((e.keyCode >= 0x30) && (e.keyCode <= 0x39)) { + // Digit + keysym = e.keyCode; + } else if ((e.keyCode >= 0x41) && (e.keyCode <= 0x5a)) { + // Character (A-Z) + let char = String.fromCharCode(e.keyCode); + // A feeble attempt at the correct case + if (e.shiftKey) char = char.toUpperCase(); + else char = char.toLowerCase(); + keysym = char.charCodeAt(); + } else { + // Unknown, give up + keysym = 0; + } + + this._sendKeyEvent(keysym, code, true); + } + + _handleKeyUp(e) { + stopEvent(e); + + const code = this._getKeyCode(e); + + // We can't get a release in the middle of an AltGr sequence, so + // abort that detection + if (this._altGrArmed) { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + + // See comment in _handleKeyDown() + if (browser.isMac() && (code === 'CapsLock')) { + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', true); + this._sendKeyEvent(KeyTable.XK_Caps_Lock, 'CapsLock', false); + return; + } + + this._sendKeyEvent(this._keyDownList[code], code, false); + } + + _handleAltGrTimeout() { + this._altGrArmed = false; + clearTimeout(this._altGrTimeout); + this._sendKeyEvent(KeyTable.XK_Control_L, 'ControlLeft', true); + } + + _allKeysUp() { + Log.Debug('>> Keyboard.allKeysUp'); + for (let code in this._keyDownList) { + this._sendKeyEvent(this._keyDownList[code], code, false); + } + Log.Debug('<< Keyboard.allKeysUp'); + } + + // Firefox Alt workaround, see below + _checkAlt(e) { + if (e.altKey) { + return; + } + + const target = this._target; + const downList = this._keyDownList; + ['AltLeft', 'AltRight'].forEach((code) => { + if (!(code in downList)) { + return; + } + + const event = new KeyboardEvent('keyup', + { + key: downList[code], + code: code }); + target.dispatchEvent(event); + }); + } + + // ===== PUBLIC METHODS ===== + + grab() { + // Log.Debug(">> Keyboard.grab"); + + this._target.addEventListener('keydown', this._eventHandlers.keydown); + this._target.addEventListener('keyup', this._eventHandlers.keyup); + this._target.addEventListener('keypress', this._eventHandlers.keypress); + + // Release (key up) if window loses focus + window.addEventListener('blur', this._eventHandlers.blur); + + // Firefox has broken handling of Alt, so we need to poll as + // best we can for releases (still doesn't prevent the menu + // from popping up though as we can't call preventDefault()) + if (browser.isWindows() && browser.isFirefox()) { + const handler = this._eventHandlers.checkalt; + ['mousedown', 'mouseup', 'mousemove', 'wheel', + 'touchstart', 'touchend', 'touchmove', + 'keydown', 'keyup'].forEach(type => document.addEventListener(type, handler, + { + capture: true, + passive: true + })); } - // ===== PUBLIC METHODS ===== + // Log.Debug("<< Keyboard.grab"); + } - grab() { - //Log.Debug(">> Keyboard.grab"); + ungrab() { + // Log.Debug(">> Keyboard.ungrab"); - this._target.addEventListener('keydown', this._eventHandlers.keydown); - this._target.addEventListener('keyup', this._eventHandlers.keyup); - this._target.addEventListener('keypress', this._eventHandlers.keypress); - - // Release (key up) if window loses focus - window.addEventListener('blur', this._eventHandlers.blur); - - // Firefox has broken handling of Alt, so we need to poll as - // best we can for releases (still doesn't prevent the menu - // from popping up though as we can't call preventDefault()) - if (browser.isWindows() && browser.isFirefox()) { - const handler = this._eventHandlers.checkalt; - ['mousedown', 'mouseup', 'mousemove', 'wheel', - 'touchstart', 'touchend', 'touchmove', - 'keydown', 'keyup'].forEach(type => - document.addEventListener(type, handler, - { capture: true, - passive: true })); - } - - //Log.Debug("<< Keyboard.grab"); + if (browser.isWindows() && browser.isFirefox()) { + const handler = this._eventHandlers.checkalt; + ['mousedown', 'mouseup', 'mousemove', 'wheel', + 'touchstart', 'touchend', 'touchmove', + 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler)); } - ungrab() { - //Log.Debug(">> Keyboard.ungrab"); + this._target.removeEventListener('keydown', this._eventHandlers.keydown); + this._target.removeEventListener('keyup', this._eventHandlers.keyup); + this._target.removeEventListener('keypress', this._eventHandlers.keypress); + window.removeEventListener('blur', this._eventHandlers.blur); - if (browser.isWindows() && browser.isFirefox()) { - const handler = this._eventHandlers.checkalt; - ['mousedown', 'mouseup', 'mousemove', 'wheel', - 'touchstart', 'touchend', 'touchmove', - 'keydown', 'keyup'].forEach(type => document.removeEventListener(type, handler)); - } + // Release (key up) all keys that are in a down state + this._allKeysUp(); - this._target.removeEventListener('keydown', this._eventHandlers.keydown); - this._target.removeEventListener('keyup', this._eventHandlers.keyup); - this._target.removeEventListener('keypress', this._eventHandlers.keypress); - window.removeEventListener('blur', this._eventHandlers.blur); - - // Release (key up) all keys that are in a down state - this._allKeysUp(); - - //Log.Debug(">> Keyboard.ungrab"); - } + // Log.Debug(">> Keyboard.ungrab"); + } } diff --git a/core/input/keysym.js b/core/input/keysym.js index ba58be68..db005d4e 100644 --- a/core/input/keysym.js +++ b/core/input/keysym.js @@ -1,614 +1,614 @@ export default { - XK_VoidSymbol: 0xffffff, /* Void symbol */ + XK_VoidSymbol: 0xffffff, /* Void symbol */ - XK_BackSpace: 0xff08, /* Back space, back char */ - XK_Tab: 0xff09, - XK_Linefeed: 0xff0a, /* Linefeed, LF */ - XK_Clear: 0xff0b, - XK_Return: 0xff0d, /* Return, enter */ - XK_Pause: 0xff13, /* Pause, hold */ - XK_Scroll_Lock: 0xff14, - XK_Sys_Req: 0xff15, - XK_Escape: 0xff1b, - XK_Delete: 0xffff, /* Delete, rubout */ + XK_BackSpace: 0xff08, /* Back space, back char */ + XK_Tab: 0xff09, + XK_Linefeed: 0xff0a, /* Linefeed, LF */ + XK_Clear: 0xff0b, + XK_Return: 0xff0d, /* Return, enter */ + XK_Pause: 0xff13, /* Pause, hold */ + XK_Scroll_Lock: 0xff14, + XK_Sys_Req: 0xff15, + XK_Escape: 0xff1b, + XK_Delete: 0xffff, /* Delete, rubout */ - /* International & multi-key character composition */ + /* International & multi-key character composition */ - XK_Multi_key: 0xff20, /* Multi-key character compose */ - XK_Codeinput: 0xff37, - XK_SingleCandidate: 0xff3c, - XK_MultipleCandidate: 0xff3d, - XK_PreviousCandidate: 0xff3e, + XK_Multi_key: 0xff20, /* Multi-key character compose */ + XK_Codeinput: 0xff37, + XK_SingleCandidate: 0xff3c, + XK_MultipleCandidate: 0xff3d, + XK_PreviousCandidate: 0xff3e, - /* Japanese keyboard support */ + /* Japanese keyboard support */ - XK_Kanji: 0xff21, /* Kanji, Kanji convert */ - XK_Muhenkan: 0xff22, /* Cancel Conversion */ - XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */ - XK_Henkan: 0xff23, /* Alias for Henkan_Mode */ - XK_Romaji: 0xff24, /* to Romaji */ - XK_Hiragana: 0xff25, /* to Hiragana */ - XK_Katakana: 0xff26, /* to Katakana */ - XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */ - XK_Zenkaku: 0xff28, /* to Zenkaku */ - XK_Hankaku: 0xff29, /* to Hankaku */ - XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */ - XK_Touroku: 0xff2b, /* Add to Dictionary */ - XK_Massyo: 0xff2c, /* Delete from Dictionary */ - XK_Kana_Lock: 0xff2d, /* Kana Lock */ - XK_Kana_Shift: 0xff2e, /* Kana Shift */ - XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */ - XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */ - XK_Kanji_Bangou: 0xff37, /* Codeinput */ - XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */ - XK_Mae_Koho: 0xff3e, /* Previous Candidate */ + XK_Kanji: 0xff21, /* Kanji, Kanji convert */ + XK_Muhenkan: 0xff22, /* Cancel Conversion */ + XK_Henkan_Mode: 0xff23, /* Start/Stop Conversion */ + XK_Henkan: 0xff23, /* Alias for Henkan_Mode */ + XK_Romaji: 0xff24, /* to Romaji */ + XK_Hiragana: 0xff25, /* to Hiragana */ + XK_Katakana: 0xff26, /* to Katakana */ + XK_Hiragana_Katakana: 0xff27, /* Hiragana/Katakana toggle */ + XK_Zenkaku: 0xff28, /* to Zenkaku */ + XK_Hankaku: 0xff29, /* to Hankaku */ + XK_Zenkaku_Hankaku: 0xff2a, /* Zenkaku/Hankaku toggle */ + XK_Touroku: 0xff2b, /* Add to Dictionary */ + XK_Massyo: 0xff2c, /* Delete from Dictionary */ + XK_Kana_Lock: 0xff2d, /* Kana Lock */ + XK_Kana_Shift: 0xff2e, /* Kana Shift */ + XK_Eisu_Shift: 0xff2f, /* Alphanumeric Shift */ + XK_Eisu_toggle: 0xff30, /* Alphanumeric toggle */ + XK_Kanji_Bangou: 0xff37, /* Codeinput */ + XK_Zen_Koho: 0xff3d, /* Multiple/All Candidate(s) */ + XK_Mae_Koho: 0xff3e, /* Previous Candidate */ - /* Cursor control & motion */ + /* Cursor control & motion */ - XK_Home: 0xff50, - XK_Left: 0xff51, /* Move left, left arrow */ - XK_Up: 0xff52, /* Move up, up arrow */ - XK_Right: 0xff53, /* Move right, right arrow */ - XK_Down: 0xff54, /* Move down, down arrow */ - XK_Prior: 0xff55, /* Prior, previous */ - XK_Page_Up: 0xff55, - XK_Next: 0xff56, /* Next */ - XK_Page_Down: 0xff56, - XK_End: 0xff57, /* EOL */ - XK_Begin: 0xff58, /* BOL */ + XK_Home: 0xff50, + XK_Left: 0xff51, /* Move left, left arrow */ + XK_Up: 0xff52, /* Move up, up arrow */ + XK_Right: 0xff53, /* Move right, right arrow */ + XK_Down: 0xff54, /* Move down, down arrow */ + XK_Prior: 0xff55, /* Prior, previous */ + XK_Page_Up: 0xff55, + XK_Next: 0xff56, /* Next */ + XK_Page_Down: 0xff56, + XK_End: 0xff57, /* EOL */ + XK_Begin: 0xff58, /* BOL */ - /* Misc functions */ + /* Misc functions */ - XK_Select: 0xff60, /* Select, mark */ - XK_Print: 0xff61, - XK_Execute: 0xff62, /* Execute, run, do */ - XK_Insert: 0xff63, /* Insert, insert here */ - XK_Undo: 0xff65, - XK_Redo: 0xff66, /* Redo, again */ - XK_Menu: 0xff67, - XK_Find: 0xff68, /* Find, search */ - XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */ - XK_Help: 0xff6a, /* Help */ - XK_Break: 0xff6b, - XK_Mode_switch: 0xff7e, /* Character set switch */ - XK_script_switch: 0xff7e, /* Alias for mode_switch */ - XK_Num_Lock: 0xff7f, + XK_Select: 0xff60, /* Select, mark */ + XK_Print: 0xff61, + XK_Execute: 0xff62, /* Execute, run, do */ + XK_Insert: 0xff63, /* Insert, insert here */ + XK_Undo: 0xff65, + XK_Redo: 0xff66, /* Redo, again */ + XK_Menu: 0xff67, + XK_Find: 0xff68, /* Find, search */ + XK_Cancel: 0xff69, /* Cancel, stop, abort, exit */ + XK_Help: 0xff6a, /* Help */ + XK_Break: 0xff6b, + XK_Mode_switch: 0xff7e, /* Character set switch */ + XK_script_switch: 0xff7e, /* Alias for mode_switch */ + XK_Num_Lock: 0xff7f, - /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ + /* Keypad functions, keypad numbers cleverly chosen to map to ASCII */ - XK_KP_Space: 0xff80, /* Space */ - XK_KP_Tab: 0xff89, - XK_KP_Enter: 0xff8d, /* Enter */ - XK_KP_F1: 0xff91, /* PF1, KP_A, ... */ - XK_KP_F2: 0xff92, - XK_KP_F3: 0xff93, - XK_KP_F4: 0xff94, - XK_KP_Home: 0xff95, - XK_KP_Left: 0xff96, - XK_KP_Up: 0xff97, - XK_KP_Right: 0xff98, - XK_KP_Down: 0xff99, - XK_KP_Prior: 0xff9a, - XK_KP_Page_Up: 0xff9a, - XK_KP_Next: 0xff9b, - XK_KP_Page_Down: 0xff9b, - XK_KP_End: 0xff9c, - XK_KP_Begin: 0xff9d, - XK_KP_Insert: 0xff9e, - XK_KP_Delete: 0xff9f, - XK_KP_Equal: 0xffbd, /* Equals */ - XK_KP_Multiply: 0xffaa, - XK_KP_Add: 0xffab, - XK_KP_Separator: 0xffac, /* Separator, often comma */ - XK_KP_Subtract: 0xffad, - XK_KP_Decimal: 0xffae, - XK_KP_Divide: 0xffaf, + XK_KP_Space: 0xff80, /* Space */ + XK_KP_Tab: 0xff89, + XK_KP_Enter: 0xff8d, /* Enter */ + XK_KP_F1: 0xff91, /* PF1, KP_A, ... */ + XK_KP_F2: 0xff92, + XK_KP_F3: 0xff93, + XK_KP_F4: 0xff94, + XK_KP_Home: 0xff95, + XK_KP_Left: 0xff96, + XK_KP_Up: 0xff97, + XK_KP_Right: 0xff98, + XK_KP_Down: 0xff99, + XK_KP_Prior: 0xff9a, + XK_KP_Page_Up: 0xff9a, + XK_KP_Next: 0xff9b, + XK_KP_Page_Down: 0xff9b, + XK_KP_End: 0xff9c, + XK_KP_Begin: 0xff9d, + XK_KP_Insert: 0xff9e, + XK_KP_Delete: 0xff9f, + XK_KP_Equal: 0xffbd, /* Equals */ + XK_KP_Multiply: 0xffaa, + XK_KP_Add: 0xffab, + XK_KP_Separator: 0xffac, /* Separator, often comma */ + XK_KP_Subtract: 0xffad, + XK_KP_Decimal: 0xffae, + XK_KP_Divide: 0xffaf, - XK_KP_0: 0xffb0, - XK_KP_1: 0xffb1, - XK_KP_2: 0xffb2, - XK_KP_3: 0xffb3, - XK_KP_4: 0xffb4, - XK_KP_5: 0xffb5, - XK_KP_6: 0xffb6, - XK_KP_7: 0xffb7, - XK_KP_8: 0xffb8, - XK_KP_9: 0xffb9, + XK_KP_0: 0xffb0, + XK_KP_1: 0xffb1, + XK_KP_2: 0xffb2, + XK_KP_3: 0xffb3, + XK_KP_4: 0xffb4, + XK_KP_5: 0xffb5, + XK_KP_6: 0xffb6, + XK_KP_7: 0xffb7, + XK_KP_8: 0xffb8, + XK_KP_9: 0xffb9, - /* + /* * Auxiliary functions; note the duplicate definitions for left and right * function keys; Sun keyboards and a few other manufacturers have such * function key groups on the left and/or right sides of the keyboard. * We've not found a keyboard with more than 35 function keys total. */ - XK_F1: 0xffbe, - XK_F2: 0xffbf, - XK_F3: 0xffc0, - XK_F4: 0xffc1, - XK_F5: 0xffc2, - XK_F6: 0xffc3, - XK_F7: 0xffc4, - XK_F8: 0xffc5, - XK_F9: 0xffc6, - XK_F10: 0xffc7, - XK_F11: 0xffc8, - XK_L1: 0xffc8, - XK_F12: 0xffc9, - XK_L2: 0xffc9, - XK_F13: 0xffca, - XK_L3: 0xffca, - XK_F14: 0xffcb, - XK_L4: 0xffcb, - XK_F15: 0xffcc, - XK_L5: 0xffcc, - XK_F16: 0xffcd, - XK_L6: 0xffcd, - XK_F17: 0xffce, - XK_L7: 0xffce, - XK_F18: 0xffcf, - XK_L8: 0xffcf, - XK_F19: 0xffd0, - XK_L9: 0xffd0, - XK_F20: 0xffd1, - XK_L10: 0xffd1, - XK_F21: 0xffd2, - XK_R1: 0xffd2, - XK_F22: 0xffd3, - XK_R2: 0xffd3, - XK_F23: 0xffd4, - XK_R3: 0xffd4, - XK_F24: 0xffd5, - XK_R4: 0xffd5, - XK_F25: 0xffd6, - XK_R5: 0xffd6, - XK_F26: 0xffd7, - XK_R6: 0xffd7, - XK_F27: 0xffd8, - XK_R7: 0xffd8, - XK_F28: 0xffd9, - XK_R8: 0xffd9, - XK_F29: 0xffda, - XK_R9: 0xffda, - XK_F30: 0xffdb, - XK_R10: 0xffdb, - XK_F31: 0xffdc, - XK_R11: 0xffdc, - XK_F32: 0xffdd, - XK_R12: 0xffdd, - XK_F33: 0xffde, - XK_R13: 0xffde, - XK_F34: 0xffdf, - XK_R14: 0xffdf, - XK_F35: 0xffe0, - XK_R15: 0xffe0, + XK_F1: 0xffbe, + XK_F2: 0xffbf, + XK_F3: 0xffc0, + XK_F4: 0xffc1, + XK_F5: 0xffc2, + XK_F6: 0xffc3, + XK_F7: 0xffc4, + XK_F8: 0xffc5, + XK_F9: 0xffc6, + XK_F10: 0xffc7, + XK_F11: 0xffc8, + XK_L1: 0xffc8, + XK_F12: 0xffc9, + XK_L2: 0xffc9, + XK_F13: 0xffca, + XK_L3: 0xffca, + XK_F14: 0xffcb, + XK_L4: 0xffcb, + XK_F15: 0xffcc, + XK_L5: 0xffcc, + XK_F16: 0xffcd, + XK_L6: 0xffcd, + XK_F17: 0xffce, + XK_L7: 0xffce, + XK_F18: 0xffcf, + XK_L8: 0xffcf, + XK_F19: 0xffd0, + XK_L9: 0xffd0, + XK_F20: 0xffd1, + XK_L10: 0xffd1, + XK_F21: 0xffd2, + XK_R1: 0xffd2, + XK_F22: 0xffd3, + XK_R2: 0xffd3, + XK_F23: 0xffd4, + XK_R3: 0xffd4, + XK_F24: 0xffd5, + XK_R4: 0xffd5, + XK_F25: 0xffd6, + XK_R5: 0xffd6, + XK_F26: 0xffd7, + XK_R6: 0xffd7, + XK_F27: 0xffd8, + XK_R7: 0xffd8, + XK_F28: 0xffd9, + XK_R8: 0xffd9, + XK_F29: 0xffda, + XK_R9: 0xffda, + XK_F30: 0xffdb, + XK_R10: 0xffdb, + XK_F31: 0xffdc, + XK_R11: 0xffdc, + XK_F32: 0xffdd, + XK_R12: 0xffdd, + XK_F33: 0xffde, + XK_R13: 0xffde, + XK_F34: 0xffdf, + XK_R14: 0xffdf, + XK_F35: 0xffe0, + XK_R15: 0xffe0, - /* Modifiers */ + /* Modifiers */ - XK_Shift_L: 0xffe1, /* Left shift */ - XK_Shift_R: 0xffe2, /* Right shift */ - XK_Control_L: 0xffe3, /* Left control */ - XK_Control_R: 0xffe4, /* Right control */ - XK_Caps_Lock: 0xffe5, /* Caps lock */ - XK_Shift_Lock: 0xffe6, /* Shift lock */ + XK_Shift_L: 0xffe1, /* Left shift */ + XK_Shift_R: 0xffe2, /* Right shift */ + XK_Control_L: 0xffe3, /* Left control */ + XK_Control_R: 0xffe4, /* Right control */ + XK_Caps_Lock: 0xffe5, /* Caps lock */ + XK_Shift_Lock: 0xffe6, /* Shift lock */ - XK_Meta_L: 0xffe7, /* Left meta */ - XK_Meta_R: 0xffe8, /* Right meta */ - XK_Alt_L: 0xffe9, /* Left alt */ - XK_Alt_R: 0xffea, /* Right alt */ - XK_Super_L: 0xffeb, /* Left super */ - XK_Super_R: 0xffec, /* Right super */ - XK_Hyper_L: 0xffed, /* Left hyper */ - XK_Hyper_R: 0xffee, /* Right hyper */ + XK_Meta_L: 0xffe7, /* Left meta */ + XK_Meta_R: 0xffe8, /* Right meta */ + XK_Alt_L: 0xffe9, /* Left alt */ + XK_Alt_R: 0xffea, /* Right alt */ + XK_Super_L: 0xffeb, /* Left super */ + XK_Super_R: 0xffec, /* Right super */ + XK_Hyper_L: 0xffed, /* Left hyper */ + XK_Hyper_R: 0xffee, /* Right hyper */ - /* + /* * Keyboard (XKB) Extension function and modifier keys * (from Appendix C of "The X Keyboard Extension: Protocol Specification") * Byte 3 = 0xfe */ - XK_ISO_Level3_Shift: 0xfe03, /* AltGr */ - XK_ISO_Next_Group: 0xfe08, - XK_ISO_Prev_Group: 0xfe0a, - XK_ISO_First_Group: 0xfe0c, - XK_ISO_Last_Group: 0xfe0e, + XK_ISO_Level3_Shift: 0xfe03, /* AltGr */ + XK_ISO_Next_Group: 0xfe08, + XK_ISO_Prev_Group: 0xfe0a, + XK_ISO_First_Group: 0xfe0c, + XK_ISO_Last_Group: 0xfe0e, - /* + /* * Latin 1 * (ISO/IEC 8859-1: Unicode U+0020..U+00FF) * Byte 3: 0 */ - XK_space: 0x0020, /* U+0020 SPACE */ - XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */ - XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */ - XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */ - XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */ - XK_percent: 0x0025, /* U+0025 PERCENT SIGN */ - XK_ampersand: 0x0026, /* U+0026 AMPERSAND */ - XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */ - XK_quoteright: 0x0027, /* deprecated */ - XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */ - XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */ - XK_asterisk: 0x002a, /* U+002A ASTERISK */ - XK_plus: 0x002b, /* U+002B PLUS SIGN */ - XK_comma: 0x002c, /* U+002C COMMA */ - XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */ - XK_period: 0x002e, /* U+002E FULL STOP */ - XK_slash: 0x002f, /* U+002F SOLIDUS */ - XK_0: 0x0030, /* U+0030 DIGIT ZERO */ - XK_1: 0x0031, /* U+0031 DIGIT ONE */ - XK_2: 0x0032, /* U+0032 DIGIT TWO */ - XK_3: 0x0033, /* U+0033 DIGIT THREE */ - XK_4: 0x0034, /* U+0034 DIGIT FOUR */ - XK_5: 0x0035, /* U+0035 DIGIT FIVE */ - XK_6: 0x0036, /* U+0036 DIGIT SIX */ - XK_7: 0x0037, /* U+0037 DIGIT SEVEN */ - XK_8: 0x0038, /* U+0038 DIGIT EIGHT */ - XK_9: 0x0039, /* U+0039 DIGIT NINE */ - XK_colon: 0x003a, /* U+003A COLON */ - XK_semicolon: 0x003b, /* U+003B SEMICOLON */ - XK_less: 0x003c, /* U+003C LESS-THAN SIGN */ - XK_equal: 0x003d, /* U+003D EQUALS SIGN */ - XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */ - XK_question: 0x003f, /* U+003F QUESTION MARK */ - XK_at: 0x0040, /* U+0040 COMMERCIAL AT */ - XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ - XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ - XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ - XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ - XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ - XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ - XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ - XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ - XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ - XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */ - XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */ - XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */ - XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */ - XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */ - XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */ - XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ - XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ - XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ - XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ - XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ - XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ - XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ - XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ - XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ - XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ - XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ - XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */ - XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */ - XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */ - XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */ - XK_underscore: 0x005f, /* U+005F LOW LINE */ - XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */ - XK_quoteleft: 0x0060, /* deprecated */ - XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */ - XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */ - XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */ - XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */ - XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */ - XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */ - XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */ - XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */ - XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */ - XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */ - XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */ - XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */ - XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */ - XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */ - XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */ - XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */ - XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */ - XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */ - XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */ - XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */ - XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */ - XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */ - XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */ - XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */ - XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */ - XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */ - XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */ - XK_bar: 0x007c, /* U+007C VERTICAL LINE */ - XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */ - XK_asciitilde: 0x007e, /* U+007E TILDE */ + XK_space: 0x0020, /* U+0020 SPACE */ + XK_exclam: 0x0021, /* U+0021 EXCLAMATION MARK */ + XK_quotedbl: 0x0022, /* U+0022 QUOTATION MARK */ + XK_numbersign: 0x0023, /* U+0023 NUMBER SIGN */ + XK_dollar: 0x0024, /* U+0024 DOLLAR SIGN */ + XK_percent: 0x0025, /* U+0025 PERCENT SIGN */ + XK_ampersand: 0x0026, /* U+0026 AMPERSAND */ + XK_apostrophe: 0x0027, /* U+0027 APOSTROPHE */ + XK_quoteright: 0x0027, /* deprecated */ + XK_parenleft: 0x0028, /* U+0028 LEFT PARENTHESIS */ + XK_parenright: 0x0029, /* U+0029 RIGHT PARENTHESIS */ + XK_asterisk: 0x002a, /* U+002A ASTERISK */ + XK_plus: 0x002b, /* U+002B PLUS SIGN */ + XK_comma: 0x002c, /* U+002C COMMA */ + XK_minus: 0x002d, /* U+002D HYPHEN-MINUS */ + XK_period: 0x002e, /* U+002E FULL STOP */ + XK_slash: 0x002f, /* U+002F SOLIDUS */ + XK_0: 0x0030, /* U+0030 DIGIT ZERO */ + XK_1: 0x0031, /* U+0031 DIGIT ONE */ + XK_2: 0x0032, /* U+0032 DIGIT TWO */ + XK_3: 0x0033, /* U+0033 DIGIT THREE */ + XK_4: 0x0034, /* U+0034 DIGIT FOUR */ + XK_5: 0x0035, /* U+0035 DIGIT FIVE */ + XK_6: 0x0036, /* U+0036 DIGIT SIX */ + XK_7: 0x0037, /* U+0037 DIGIT SEVEN */ + XK_8: 0x0038, /* U+0038 DIGIT EIGHT */ + XK_9: 0x0039, /* U+0039 DIGIT NINE */ + XK_colon: 0x003a, /* U+003A COLON */ + XK_semicolon: 0x003b, /* U+003B SEMICOLON */ + XK_less: 0x003c, /* U+003C LESS-THAN SIGN */ + XK_equal: 0x003d, /* U+003D EQUALS SIGN */ + XK_greater: 0x003e, /* U+003E GREATER-THAN SIGN */ + XK_question: 0x003f, /* U+003F QUESTION MARK */ + XK_at: 0x0040, /* U+0040 COMMERCIAL AT */ + XK_A: 0x0041, /* U+0041 LATIN CAPITAL LETTER A */ + XK_B: 0x0042, /* U+0042 LATIN CAPITAL LETTER B */ + XK_C: 0x0043, /* U+0043 LATIN CAPITAL LETTER C */ + XK_D: 0x0044, /* U+0044 LATIN CAPITAL LETTER D */ + XK_E: 0x0045, /* U+0045 LATIN CAPITAL LETTER E */ + XK_F: 0x0046, /* U+0046 LATIN CAPITAL LETTER F */ + XK_G: 0x0047, /* U+0047 LATIN CAPITAL LETTER G */ + XK_H: 0x0048, /* U+0048 LATIN CAPITAL LETTER H */ + XK_I: 0x0049, /* U+0049 LATIN CAPITAL LETTER I */ + XK_J: 0x004a, /* U+004A LATIN CAPITAL LETTER J */ + XK_K: 0x004b, /* U+004B LATIN CAPITAL LETTER K */ + XK_L: 0x004c, /* U+004C LATIN CAPITAL LETTER L */ + XK_M: 0x004d, /* U+004D LATIN CAPITAL LETTER M */ + XK_N: 0x004e, /* U+004E LATIN CAPITAL LETTER N */ + XK_O: 0x004f, /* U+004F LATIN CAPITAL LETTER O */ + XK_P: 0x0050, /* U+0050 LATIN CAPITAL LETTER P */ + XK_Q: 0x0051, /* U+0051 LATIN CAPITAL LETTER Q */ + XK_R: 0x0052, /* U+0052 LATIN CAPITAL LETTER R */ + XK_S: 0x0053, /* U+0053 LATIN CAPITAL LETTER S */ + XK_T: 0x0054, /* U+0054 LATIN CAPITAL LETTER T */ + XK_U: 0x0055, /* U+0055 LATIN CAPITAL LETTER U */ + XK_V: 0x0056, /* U+0056 LATIN CAPITAL LETTER V */ + XK_W: 0x0057, /* U+0057 LATIN CAPITAL LETTER W */ + XK_X: 0x0058, /* U+0058 LATIN CAPITAL LETTER X */ + XK_Y: 0x0059, /* U+0059 LATIN CAPITAL LETTER Y */ + XK_Z: 0x005a, /* U+005A LATIN CAPITAL LETTER Z */ + XK_bracketleft: 0x005b, /* U+005B LEFT SQUARE BRACKET */ + XK_backslash: 0x005c, /* U+005C REVERSE SOLIDUS */ + XK_bracketright: 0x005d, /* U+005D RIGHT SQUARE BRACKET */ + XK_asciicircum: 0x005e, /* U+005E CIRCUMFLEX ACCENT */ + XK_underscore: 0x005f, /* U+005F LOW LINE */ + XK_grave: 0x0060, /* U+0060 GRAVE ACCENT */ + XK_quoteleft: 0x0060, /* deprecated */ + XK_a: 0x0061, /* U+0061 LATIN SMALL LETTER A */ + XK_b: 0x0062, /* U+0062 LATIN SMALL LETTER B */ + XK_c: 0x0063, /* U+0063 LATIN SMALL LETTER C */ + XK_d: 0x0064, /* U+0064 LATIN SMALL LETTER D */ + XK_e: 0x0065, /* U+0065 LATIN SMALL LETTER E */ + XK_f: 0x0066, /* U+0066 LATIN SMALL LETTER F */ + XK_g: 0x0067, /* U+0067 LATIN SMALL LETTER G */ + XK_h: 0x0068, /* U+0068 LATIN SMALL LETTER H */ + XK_i: 0x0069, /* U+0069 LATIN SMALL LETTER I */ + XK_j: 0x006a, /* U+006A LATIN SMALL LETTER J */ + XK_k: 0x006b, /* U+006B LATIN SMALL LETTER K */ + XK_l: 0x006c, /* U+006C LATIN SMALL LETTER L */ + XK_m: 0x006d, /* U+006D LATIN SMALL LETTER M */ + XK_n: 0x006e, /* U+006E LATIN SMALL LETTER N */ + XK_o: 0x006f, /* U+006F LATIN SMALL LETTER O */ + XK_p: 0x0070, /* U+0070 LATIN SMALL LETTER P */ + XK_q: 0x0071, /* U+0071 LATIN SMALL LETTER Q */ + XK_r: 0x0072, /* U+0072 LATIN SMALL LETTER R */ + XK_s: 0x0073, /* U+0073 LATIN SMALL LETTER S */ + XK_t: 0x0074, /* U+0074 LATIN SMALL LETTER T */ + XK_u: 0x0075, /* U+0075 LATIN SMALL LETTER U */ + XK_v: 0x0076, /* U+0076 LATIN SMALL LETTER V */ + XK_w: 0x0077, /* U+0077 LATIN SMALL LETTER W */ + XK_x: 0x0078, /* U+0078 LATIN SMALL LETTER X */ + XK_y: 0x0079, /* U+0079 LATIN SMALL LETTER Y */ + XK_z: 0x007a, /* U+007A LATIN SMALL LETTER Z */ + XK_braceleft: 0x007b, /* U+007B LEFT CURLY BRACKET */ + XK_bar: 0x007c, /* U+007C VERTICAL LINE */ + XK_braceright: 0x007d, /* U+007D RIGHT CURLY BRACKET */ + XK_asciitilde: 0x007e, /* U+007E TILDE */ - XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */ - XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ - XK_cent: 0x00a2, /* U+00A2 CENT SIGN */ - XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */ - XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */ - XK_yen: 0x00a5, /* U+00A5 YEN SIGN */ - XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */ - XK_section: 0x00a7, /* U+00A7 SECTION SIGN */ - XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */ - XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */ - XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ - XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ - XK_notsign: 0x00ac, /* U+00AC NOT SIGN */ - XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */ - XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */ - XK_macron: 0x00af, /* U+00AF MACRON */ - XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */ - XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ - XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ - XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ - XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */ - XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */ - XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */ - XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */ - XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */ - XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ - XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ - XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ - XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ - XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ - XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ - XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */ - XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ - XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ - XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ - XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ - XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ - XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ - XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ - XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ - XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ - XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ - XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ - XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ - XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ - XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ - XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ - XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ - XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ - XK_Eth: 0x00d0, /* deprecated */ - XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ - XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ - XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ - XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ - XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ - XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ - XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ - XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ - XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ - XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ - XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ - XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ - XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ - XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ - XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ - XK_Thorn: 0x00de, /* deprecated */ - XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ - XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ - XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ - XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ - XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ - XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ - XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ - XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ - XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ - XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ - XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ - XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ - XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ - XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ - XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ - XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ - XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ - XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ - XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ - XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ - XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ - XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ - XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ - XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ - XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */ - XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ - XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ - XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ - XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ - XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ - XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ - XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ - XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ - XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ + XK_nobreakspace: 0x00a0, /* U+00A0 NO-BREAK SPACE */ + XK_exclamdown: 0x00a1, /* U+00A1 INVERTED EXCLAMATION MARK */ + XK_cent: 0x00a2, /* U+00A2 CENT SIGN */ + XK_sterling: 0x00a3, /* U+00A3 POUND SIGN */ + XK_currency: 0x00a4, /* U+00A4 CURRENCY SIGN */ + XK_yen: 0x00a5, /* U+00A5 YEN SIGN */ + XK_brokenbar: 0x00a6, /* U+00A6 BROKEN BAR */ + XK_section: 0x00a7, /* U+00A7 SECTION SIGN */ + XK_diaeresis: 0x00a8, /* U+00A8 DIAERESIS */ + XK_copyright: 0x00a9, /* U+00A9 COPYRIGHT SIGN */ + XK_ordfeminine: 0x00aa, /* U+00AA FEMININE ORDINAL INDICATOR */ + XK_guillemotleft: 0x00ab, /* U+00AB LEFT-POINTING DOUBLE ANGLE QUOTATION MARK */ + XK_notsign: 0x00ac, /* U+00AC NOT SIGN */ + XK_hyphen: 0x00ad, /* U+00AD SOFT HYPHEN */ + XK_registered: 0x00ae, /* U+00AE REGISTERED SIGN */ + XK_macron: 0x00af, /* U+00AF MACRON */ + XK_degree: 0x00b0, /* U+00B0 DEGREE SIGN */ + XK_plusminus: 0x00b1, /* U+00B1 PLUS-MINUS SIGN */ + XK_twosuperior: 0x00b2, /* U+00B2 SUPERSCRIPT TWO */ + XK_threesuperior: 0x00b3, /* U+00B3 SUPERSCRIPT THREE */ + XK_acute: 0x00b4, /* U+00B4 ACUTE ACCENT */ + XK_mu: 0x00b5, /* U+00B5 MICRO SIGN */ + XK_paragraph: 0x00b6, /* U+00B6 PILCROW SIGN */ + XK_periodcentered: 0x00b7, /* U+00B7 MIDDLE DOT */ + XK_cedilla: 0x00b8, /* U+00B8 CEDILLA */ + XK_onesuperior: 0x00b9, /* U+00B9 SUPERSCRIPT ONE */ + XK_masculine: 0x00ba, /* U+00BA MASCULINE ORDINAL INDICATOR */ + XK_guillemotright: 0x00bb, /* U+00BB RIGHT-POINTING DOUBLE ANGLE QUOTATION MARK */ + XK_onequarter: 0x00bc, /* U+00BC VULGAR FRACTION ONE QUARTER */ + XK_onehalf: 0x00bd, /* U+00BD VULGAR FRACTION ONE HALF */ + XK_threequarters: 0x00be, /* U+00BE VULGAR FRACTION THREE QUARTERS */ + XK_questiondown: 0x00bf, /* U+00BF INVERTED QUESTION MARK */ + XK_Agrave: 0x00c0, /* U+00C0 LATIN CAPITAL LETTER A WITH GRAVE */ + XK_Aacute: 0x00c1, /* U+00C1 LATIN CAPITAL LETTER A WITH ACUTE */ + XK_Acircumflex: 0x00c2, /* U+00C2 LATIN CAPITAL LETTER A WITH CIRCUMFLEX */ + XK_Atilde: 0x00c3, /* U+00C3 LATIN CAPITAL LETTER A WITH TILDE */ + XK_Adiaeresis: 0x00c4, /* U+00C4 LATIN CAPITAL LETTER A WITH DIAERESIS */ + XK_Aring: 0x00c5, /* U+00C5 LATIN CAPITAL LETTER A WITH RING ABOVE */ + XK_AE: 0x00c6, /* U+00C6 LATIN CAPITAL LETTER AE */ + XK_Ccedilla: 0x00c7, /* U+00C7 LATIN CAPITAL LETTER C WITH CEDILLA */ + XK_Egrave: 0x00c8, /* U+00C8 LATIN CAPITAL LETTER E WITH GRAVE */ + XK_Eacute: 0x00c9, /* U+00C9 LATIN CAPITAL LETTER E WITH ACUTE */ + XK_Ecircumflex: 0x00ca, /* U+00CA LATIN CAPITAL LETTER E WITH CIRCUMFLEX */ + XK_Ediaeresis: 0x00cb, /* U+00CB LATIN CAPITAL LETTER E WITH DIAERESIS */ + XK_Igrave: 0x00cc, /* U+00CC LATIN CAPITAL LETTER I WITH GRAVE */ + XK_Iacute: 0x00cd, /* U+00CD LATIN CAPITAL LETTER I WITH ACUTE */ + XK_Icircumflex: 0x00ce, /* U+00CE LATIN CAPITAL LETTER I WITH CIRCUMFLEX */ + XK_Idiaeresis: 0x00cf, /* U+00CF LATIN CAPITAL LETTER I WITH DIAERESIS */ + XK_ETH: 0x00d0, /* U+00D0 LATIN CAPITAL LETTER ETH */ + XK_Eth: 0x00d0, /* deprecated */ + XK_Ntilde: 0x00d1, /* U+00D1 LATIN CAPITAL LETTER N WITH TILDE */ + XK_Ograve: 0x00d2, /* U+00D2 LATIN CAPITAL LETTER O WITH GRAVE */ + XK_Oacute: 0x00d3, /* U+00D3 LATIN CAPITAL LETTER O WITH ACUTE */ + XK_Ocircumflex: 0x00d4, /* U+00D4 LATIN CAPITAL LETTER O WITH CIRCUMFLEX */ + XK_Otilde: 0x00d5, /* U+00D5 LATIN CAPITAL LETTER O WITH TILDE */ + XK_Odiaeresis: 0x00d6, /* U+00D6 LATIN CAPITAL LETTER O WITH DIAERESIS */ + XK_multiply: 0x00d7, /* U+00D7 MULTIPLICATION SIGN */ + XK_Oslash: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ + XK_Ooblique: 0x00d8, /* U+00D8 LATIN CAPITAL LETTER O WITH STROKE */ + XK_Ugrave: 0x00d9, /* U+00D9 LATIN CAPITAL LETTER U WITH GRAVE */ + XK_Uacute: 0x00da, /* U+00DA LATIN CAPITAL LETTER U WITH ACUTE */ + XK_Ucircumflex: 0x00db, /* U+00DB LATIN CAPITAL LETTER U WITH CIRCUMFLEX */ + XK_Udiaeresis: 0x00dc, /* U+00DC LATIN CAPITAL LETTER U WITH DIAERESIS */ + XK_Yacute: 0x00dd, /* U+00DD LATIN CAPITAL LETTER Y WITH ACUTE */ + XK_THORN: 0x00de, /* U+00DE LATIN CAPITAL LETTER THORN */ + XK_Thorn: 0x00de, /* deprecated */ + XK_ssharp: 0x00df, /* U+00DF LATIN SMALL LETTER SHARP S */ + XK_agrave: 0x00e0, /* U+00E0 LATIN SMALL LETTER A WITH GRAVE */ + XK_aacute: 0x00e1, /* U+00E1 LATIN SMALL LETTER A WITH ACUTE */ + XK_acircumflex: 0x00e2, /* U+00E2 LATIN SMALL LETTER A WITH CIRCUMFLEX */ + XK_atilde: 0x00e3, /* U+00E3 LATIN SMALL LETTER A WITH TILDE */ + XK_adiaeresis: 0x00e4, /* U+00E4 LATIN SMALL LETTER A WITH DIAERESIS */ + XK_aring: 0x00e5, /* U+00E5 LATIN SMALL LETTER A WITH RING ABOVE */ + XK_ae: 0x00e6, /* U+00E6 LATIN SMALL LETTER AE */ + XK_ccedilla: 0x00e7, /* U+00E7 LATIN SMALL LETTER C WITH CEDILLA */ + XK_egrave: 0x00e8, /* U+00E8 LATIN SMALL LETTER E WITH GRAVE */ + XK_eacute: 0x00e9, /* U+00E9 LATIN SMALL LETTER E WITH ACUTE */ + XK_ecircumflex: 0x00ea, /* U+00EA LATIN SMALL LETTER E WITH CIRCUMFLEX */ + XK_ediaeresis: 0x00eb, /* U+00EB LATIN SMALL LETTER E WITH DIAERESIS */ + XK_igrave: 0x00ec, /* U+00EC LATIN SMALL LETTER I WITH GRAVE */ + XK_iacute: 0x00ed, /* U+00ED LATIN SMALL LETTER I WITH ACUTE */ + XK_icircumflex: 0x00ee, /* U+00EE LATIN SMALL LETTER I WITH CIRCUMFLEX */ + XK_idiaeresis: 0x00ef, /* U+00EF LATIN SMALL LETTER I WITH DIAERESIS */ + XK_eth: 0x00f0, /* U+00F0 LATIN SMALL LETTER ETH */ + XK_ntilde: 0x00f1, /* U+00F1 LATIN SMALL LETTER N WITH TILDE */ + XK_ograve: 0x00f2, /* U+00F2 LATIN SMALL LETTER O WITH GRAVE */ + XK_oacute: 0x00f3, /* U+00F3 LATIN SMALL LETTER O WITH ACUTE */ + XK_ocircumflex: 0x00f4, /* U+00F4 LATIN SMALL LETTER O WITH CIRCUMFLEX */ + XK_otilde: 0x00f5, /* U+00F5 LATIN SMALL LETTER O WITH TILDE */ + XK_odiaeresis: 0x00f6, /* U+00F6 LATIN SMALL LETTER O WITH DIAERESIS */ + XK_division: 0x00f7, /* U+00F7 DIVISION SIGN */ + XK_oslash: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ + XK_ooblique: 0x00f8, /* U+00F8 LATIN SMALL LETTER O WITH STROKE */ + XK_ugrave: 0x00f9, /* U+00F9 LATIN SMALL LETTER U WITH GRAVE */ + XK_uacute: 0x00fa, /* U+00FA LATIN SMALL LETTER U WITH ACUTE */ + XK_ucircumflex: 0x00fb, /* U+00FB LATIN SMALL LETTER U WITH CIRCUMFLEX */ + XK_udiaeresis: 0x00fc, /* U+00FC LATIN SMALL LETTER U WITH DIAERESIS */ + XK_yacute: 0x00fd, /* U+00FD LATIN SMALL LETTER Y WITH ACUTE */ + XK_thorn: 0x00fe, /* U+00FE LATIN SMALL LETTER THORN */ + XK_ydiaeresis: 0x00ff, /* U+00FF LATIN SMALL LETTER Y WITH DIAERESIS */ - /* + /* * Korean * Byte 3 = 0x0e */ - XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */ - XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */ - XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */ + XK_Hangul: 0xff31, /* Hangul start/stop(toggle) */ + XK_Hangul_Hanja: 0xff34, /* Start Hangul->Hanja Conversion */ + XK_Hangul_Jeonja: 0xff38, /* Jeonja mode */ - /* + /* * XFree86 vendor specific keysyms. * * The XFree86 keysym range is 0x10080001 - 0x1008FFFF. */ - XF86XK_ModeLock: 0x1008FF01, - XF86XK_MonBrightnessUp: 0x1008FF02, - XF86XK_MonBrightnessDown: 0x1008FF03, - XF86XK_KbdLightOnOff: 0x1008FF04, - XF86XK_KbdBrightnessUp: 0x1008FF05, - XF86XK_KbdBrightnessDown: 0x1008FF06, - XF86XK_Standby: 0x1008FF10, - XF86XK_AudioLowerVolume: 0x1008FF11, - XF86XK_AudioMute: 0x1008FF12, - XF86XK_AudioRaiseVolume: 0x1008FF13, - XF86XK_AudioPlay: 0x1008FF14, - XF86XK_AudioStop: 0x1008FF15, - XF86XK_AudioPrev: 0x1008FF16, - XF86XK_AudioNext: 0x1008FF17, - XF86XK_HomePage: 0x1008FF18, - XF86XK_Mail: 0x1008FF19, - XF86XK_Start: 0x1008FF1A, - XF86XK_Search: 0x1008FF1B, - XF86XK_AudioRecord: 0x1008FF1C, - XF86XK_Calculator: 0x1008FF1D, - XF86XK_Memo: 0x1008FF1E, - XF86XK_ToDoList: 0x1008FF1F, - XF86XK_Calendar: 0x1008FF20, - XF86XK_PowerDown: 0x1008FF21, - XF86XK_ContrastAdjust: 0x1008FF22, - XF86XK_RockerUp: 0x1008FF23, - XF86XK_RockerDown: 0x1008FF24, - XF86XK_RockerEnter: 0x1008FF25, - XF86XK_Back: 0x1008FF26, - XF86XK_Forward: 0x1008FF27, - XF86XK_Stop: 0x1008FF28, - XF86XK_Refresh: 0x1008FF29, - XF86XK_PowerOff: 0x1008FF2A, - XF86XK_WakeUp: 0x1008FF2B, - XF86XK_Eject: 0x1008FF2C, - XF86XK_ScreenSaver: 0x1008FF2D, - XF86XK_WWW: 0x1008FF2E, - XF86XK_Sleep: 0x1008FF2F, - XF86XK_Favorites: 0x1008FF30, - XF86XK_AudioPause: 0x1008FF31, - XF86XK_AudioMedia: 0x1008FF32, - XF86XK_MyComputer: 0x1008FF33, - XF86XK_VendorHome: 0x1008FF34, - XF86XK_LightBulb: 0x1008FF35, - XF86XK_Shop: 0x1008FF36, - XF86XK_History: 0x1008FF37, - XF86XK_OpenURL: 0x1008FF38, - XF86XK_AddFavorite: 0x1008FF39, - XF86XK_HotLinks: 0x1008FF3A, - XF86XK_BrightnessAdjust: 0x1008FF3B, - XF86XK_Finance: 0x1008FF3C, - XF86XK_Community: 0x1008FF3D, - XF86XK_AudioRewind: 0x1008FF3E, - XF86XK_BackForward: 0x1008FF3F, - XF86XK_Launch0: 0x1008FF40, - XF86XK_Launch1: 0x1008FF41, - XF86XK_Launch2: 0x1008FF42, - XF86XK_Launch3: 0x1008FF43, - XF86XK_Launch4: 0x1008FF44, - XF86XK_Launch5: 0x1008FF45, - XF86XK_Launch6: 0x1008FF46, - XF86XK_Launch7: 0x1008FF47, - XF86XK_Launch8: 0x1008FF48, - XF86XK_Launch9: 0x1008FF49, - XF86XK_LaunchA: 0x1008FF4A, - XF86XK_LaunchB: 0x1008FF4B, - XF86XK_LaunchC: 0x1008FF4C, - XF86XK_LaunchD: 0x1008FF4D, - XF86XK_LaunchE: 0x1008FF4E, - XF86XK_LaunchF: 0x1008FF4F, - XF86XK_ApplicationLeft: 0x1008FF50, - XF86XK_ApplicationRight: 0x1008FF51, - XF86XK_Book: 0x1008FF52, - XF86XK_CD: 0x1008FF53, - XF86XK_Calculater: 0x1008FF54, - XF86XK_Clear: 0x1008FF55, - XF86XK_Close: 0x1008FF56, - XF86XK_Copy: 0x1008FF57, - XF86XK_Cut: 0x1008FF58, - XF86XK_Display: 0x1008FF59, - XF86XK_DOS: 0x1008FF5A, - XF86XK_Documents: 0x1008FF5B, - XF86XK_Excel: 0x1008FF5C, - XF86XK_Explorer: 0x1008FF5D, - XF86XK_Game: 0x1008FF5E, - XF86XK_Go: 0x1008FF5F, - XF86XK_iTouch: 0x1008FF60, - XF86XK_LogOff: 0x1008FF61, - XF86XK_Market: 0x1008FF62, - XF86XK_Meeting: 0x1008FF63, - XF86XK_MenuKB: 0x1008FF65, - XF86XK_MenuPB: 0x1008FF66, - XF86XK_MySites: 0x1008FF67, - XF86XK_New: 0x1008FF68, - XF86XK_News: 0x1008FF69, - XF86XK_OfficeHome: 0x1008FF6A, - XF86XK_Open: 0x1008FF6B, - XF86XK_Option: 0x1008FF6C, - XF86XK_Paste: 0x1008FF6D, - XF86XK_Phone: 0x1008FF6E, - XF86XK_Q: 0x1008FF70, - XF86XK_Reply: 0x1008FF72, - XF86XK_Reload: 0x1008FF73, - XF86XK_RotateWindows: 0x1008FF74, - XF86XK_RotationPB: 0x1008FF75, - XF86XK_RotationKB: 0x1008FF76, - XF86XK_Save: 0x1008FF77, - XF86XK_ScrollUp: 0x1008FF78, - XF86XK_ScrollDown: 0x1008FF79, - XF86XK_ScrollClick: 0x1008FF7A, - XF86XK_Send: 0x1008FF7B, - XF86XK_Spell: 0x1008FF7C, - XF86XK_SplitScreen: 0x1008FF7D, - XF86XK_Support: 0x1008FF7E, - XF86XK_TaskPane: 0x1008FF7F, - XF86XK_Terminal: 0x1008FF80, - XF86XK_Tools: 0x1008FF81, - XF86XK_Travel: 0x1008FF82, - XF86XK_UserPB: 0x1008FF84, - XF86XK_User1KB: 0x1008FF85, - XF86XK_User2KB: 0x1008FF86, - XF86XK_Video: 0x1008FF87, - XF86XK_WheelButton: 0x1008FF88, - XF86XK_Word: 0x1008FF89, - XF86XK_Xfer: 0x1008FF8A, - XF86XK_ZoomIn: 0x1008FF8B, - XF86XK_ZoomOut: 0x1008FF8C, - XF86XK_Away: 0x1008FF8D, - XF86XK_Messenger: 0x1008FF8E, - XF86XK_WebCam: 0x1008FF8F, - XF86XK_MailForward: 0x1008FF90, - XF86XK_Pictures: 0x1008FF91, - XF86XK_Music: 0x1008FF92, - XF86XK_Battery: 0x1008FF93, - XF86XK_Bluetooth: 0x1008FF94, - XF86XK_WLAN: 0x1008FF95, - XF86XK_UWB: 0x1008FF96, - XF86XK_AudioForward: 0x1008FF97, - XF86XK_AudioRepeat: 0x1008FF98, - XF86XK_AudioRandomPlay: 0x1008FF99, - XF86XK_Subtitle: 0x1008FF9A, - XF86XK_AudioCycleTrack: 0x1008FF9B, - XF86XK_CycleAngle: 0x1008FF9C, - XF86XK_FrameBack: 0x1008FF9D, - XF86XK_FrameForward: 0x1008FF9E, - XF86XK_Time: 0x1008FF9F, - XF86XK_Select: 0x1008FFA0, - XF86XK_View: 0x1008FFA1, - XF86XK_TopMenu: 0x1008FFA2, - XF86XK_Red: 0x1008FFA3, - XF86XK_Green: 0x1008FFA4, - XF86XK_Yellow: 0x1008FFA5, - XF86XK_Blue: 0x1008FFA6, - XF86XK_Suspend: 0x1008FFA7, - XF86XK_Hibernate: 0x1008FFA8, - XF86XK_TouchpadToggle: 0x1008FFA9, - XF86XK_TouchpadOn: 0x1008FFB0, - XF86XK_TouchpadOff: 0x1008FFB1, - XF86XK_AudioMicMute: 0x1008FFB2, - XF86XK_Switch_VT_1: 0x1008FE01, - XF86XK_Switch_VT_2: 0x1008FE02, - XF86XK_Switch_VT_3: 0x1008FE03, - XF86XK_Switch_VT_4: 0x1008FE04, - XF86XK_Switch_VT_5: 0x1008FE05, - XF86XK_Switch_VT_6: 0x1008FE06, - XF86XK_Switch_VT_7: 0x1008FE07, - XF86XK_Switch_VT_8: 0x1008FE08, - XF86XK_Switch_VT_9: 0x1008FE09, - XF86XK_Switch_VT_10: 0x1008FE0A, - XF86XK_Switch_VT_11: 0x1008FE0B, - XF86XK_Switch_VT_12: 0x1008FE0C, - XF86XK_Ungrab: 0x1008FE20, - XF86XK_ClearGrab: 0x1008FE21, - XF86XK_Next_VMode: 0x1008FE22, - XF86XK_Prev_VMode: 0x1008FE23, - XF86XK_LogWindowTree: 0x1008FE24, - XF86XK_LogGrabInfo: 0x1008FE25, + XF86XK_ModeLock: 0x1008FF01, + XF86XK_MonBrightnessUp: 0x1008FF02, + XF86XK_MonBrightnessDown: 0x1008FF03, + XF86XK_KbdLightOnOff: 0x1008FF04, + XF86XK_KbdBrightnessUp: 0x1008FF05, + XF86XK_KbdBrightnessDown: 0x1008FF06, + XF86XK_Standby: 0x1008FF10, + XF86XK_AudioLowerVolume: 0x1008FF11, + XF86XK_AudioMute: 0x1008FF12, + XF86XK_AudioRaiseVolume: 0x1008FF13, + XF86XK_AudioPlay: 0x1008FF14, + XF86XK_AudioStop: 0x1008FF15, + XF86XK_AudioPrev: 0x1008FF16, + XF86XK_AudioNext: 0x1008FF17, + XF86XK_HomePage: 0x1008FF18, + XF86XK_Mail: 0x1008FF19, + XF86XK_Start: 0x1008FF1A, + XF86XK_Search: 0x1008FF1B, + XF86XK_AudioRecord: 0x1008FF1C, + XF86XK_Calculator: 0x1008FF1D, + XF86XK_Memo: 0x1008FF1E, + XF86XK_ToDoList: 0x1008FF1F, + XF86XK_Calendar: 0x1008FF20, + XF86XK_PowerDown: 0x1008FF21, + XF86XK_ContrastAdjust: 0x1008FF22, + XF86XK_RockerUp: 0x1008FF23, + XF86XK_RockerDown: 0x1008FF24, + XF86XK_RockerEnter: 0x1008FF25, + XF86XK_Back: 0x1008FF26, + XF86XK_Forward: 0x1008FF27, + XF86XK_Stop: 0x1008FF28, + XF86XK_Refresh: 0x1008FF29, + XF86XK_PowerOff: 0x1008FF2A, + XF86XK_WakeUp: 0x1008FF2B, + XF86XK_Eject: 0x1008FF2C, + XF86XK_ScreenSaver: 0x1008FF2D, + XF86XK_WWW: 0x1008FF2E, + XF86XK_Sleep: 0x1008FF2F, + XF86XK_Favorites: 0x1008FF30, + XF86XK_AudioPause: 0x1008FF31, + XF86XK_AudioMedia: 0x1008FF32, + XF86XK_MyComputer: 0x1008FF33, + XF86XK_VendorHome: 0x1008FF34, + XF86XK_LightBulb: 0x1008FF35, + XF86XK_Shop: 0x1008FF36, + XF86XK_History: 0x1008FF37, + XF86XK_OpenURL: 0x1008FF38, + XF86XK_AddFavorite: 0x1008FF39, + XF86XK_HotLinks: 0x1008FF3A, + XF86XK_BrightnessAdjust: 0x1008FF3B, + XF86XK_Finance: 0x1008FF3C, + XF86XK_Community: 0x1008FF3D, + XF86XK_AudioRewind: 0x1008FF3E, + XF86XK_BackForward: 0x1008FF3F, + XF86XK_Launch0: 0x1008FF40, + XF86XK_Launch1: 0x1008FF41, + XF86XK_Launch2: 0x1008FF42, + XF86XK_Launch3: 0x1008FF43, + XF86XK_Launch4: 0x1008FF44, + XF86XK_Launch5: 0x1008FF45, + XF86XK_Launch6: 0x1008FF46, + XF86XK_Launch7: 0x1008FF47, + XF86XK_Launch8: 0x1008FF48, + XF86XK_Launch9: 0x1008FF49, + XF86XK_LaunchA: 0x1008FF4A, + XF86XK_LaunchB: 0x1008FF4B, + XF86XK_LaunchC: 0x1008FF4C, + XF86XK_LaunchD: 0x1008FF4D, + XF86XK_LaunchE: 0x1008FF4E, + XF86XK_LaunchF: 0x1008FF4F, + XF86XK_ApplicationLeft: 0x1008FF50, + XF86XK_ApplicationRight: 0x1008FF51, + XF86XK_Book: 0x1008FF52, + XF86XK_CD: 0x1008FF53, + XF86XK_Calculater: 0x1008FF54, + XF86XK_Clear: 0x1008FF55, + XF86XK_Close: 0x1008FF56, + XF86XK_Copy: 0x1008FF57, + XF86XK_Cut: 0x1008FF58, + XF86XK_Display: 0x1008FF59, + XF86XK_DOS: 0x1008FF5A, + XF86XK_Documents: 0x1008FF5B, + XF86XK_Excel: 0x1008FF5C, + XF86XK_Explorer: 0x1008FF5D, + XF86XK_Game: 0x1008FF5E, + XF86XK_Go: 0x1008FF5F, + XF86XK_iTouch: 0x1008FF60, + XF86XK_LogOff: 0x1008FF61, + XF86XK_Market: 0x1008FF62, + XF86XK_Meeting: 0x1008FF63, + XF86XK_MenuKB: 0x1008FF65, + XF86XK_MenuPB: 0x1008FF66, + XF86XK_MySites: 0x1008FF67, + XF86XK_New: 0x1008FF68, + XF86XK_News: 0x1008FF69, + XF86XK_OfficeHome: 0x1008FF6A, + XF86XK_Open: 0x1008FF6B, + XF86XK_Option: 0x1008FF6C, + XF86XK_Paste: 0x1008FF6D, + XF86XK_Phone: 0x1008FF6E, + XF86XK_Q: 0x1008FF70, + XF86XK_Reply: 0x1008FF72, + XF86XK_Reload: 0x1008FF73, + XF86XK_RotateWindows: 0x1008FF74, + XF86XK_RotationPB: 0x1008FF75, + XF86XK_RotationKB: 0x1008FF76, + XF86XK_Save: 0x1008FF77, + XF86XK_ScrollUp: 0x1008FF78, + XF86XK_ScrollDown: 0x1008FF79, + XF86XK_ScrollClick: 0x1008FF7A, + XF86XK_Send: 0x1008FF7B, + XF86XK_Spell: 0x1008FF7C, + XF86XK_SplitScreen: 0x1008FF7D, + XF86XK_Support: 0x1008FF7E, + XF86XK_TaskPane: 0x1008FF7F, + XF86XK_Terminal: 0x1008FF80, + XF86XK_Tools: 0x1008FF81, + XF86XK_Travel: 0x1008FF82, + XF86XK_UserPB: 0x1008FF84, + XF86XK_User1KB: 0x1008FF85, + XF86XK_User2KB: 0x1008FF86, + XF86XK_Video: 0x1008FF87, + XF86XK_WheelButton: 0x1008FF88, + XF86XK_Word: 0x1008FF89, + XF86XK_Xfer: 0x1008FF8A, + XF86XK_ZoomIn: 0x1008FF8B, + XF86XK_ZoomOut: 0x1008FF8C, + XF86XK_Away: 0x1008FF8D, + XF86XK_Messenger: 0x1008FF8E, + XF86XK_WebCam: 0x1008FF8F, + XF86XK_MailForward: 0x1008FF90, + XF86XK_Pictures: 0x1008FF91, + XF86XK_Music: 0x1008FF92, + XF86XK_Battery: 0x1008FF93, + XF86XK_Bluetooth: 0x1008FF94, + XF86XK_WLAN: 0x1008FF95, + XF86XK_UWB: 0x1008FF96, + XF86XK_AudioForward: 0x1008FF97, + XF86XK_AudioRepeat: 0x1008FF98, + XF86XK_AudioRandomPlay: 0x1008FF99, + XF86XK_Subtitle: 0x1008FF9A, + XF86XK_AudioCycleTrack: 0x1008FF9B, + XF86XK_CycleAngle: 0x1008FF9C, + XF86XK_FrameBack: 0x1008FF9D, + XF86XK_FrameForward: 0x1008FF9E, + XF86XK_Time: 0x1008FF9F, + XF86XK_Select: 0x1008FFA0, + XF86XK_View: 0x1008FFA1, + XF86XK_TopMenu: 0x1008FFA2, + XF86XK_Red: 0x1008FFA3, + XF86XK_Green: 0x1008FFA4, + XF86XK_Yellow: 0x1008FFA5, + XF86XK_Blue: 0x1008FFA6, + XF86XK_Suspend: 0x1008FFA7, + XF86XK_Hibernate: 0x1008FFA8, + XF86XK_TouchpadToggle: 0x1008FFA9, + XF86XK_TouchpadOn: 0x1008FFB0, + XF86XK_TouchpadOff: 0x1008FFB1, + XF86XK_AudioMicMute: 0x1008FFB2, + XF86XK_Switch_VT_1: 0x1008FE01, + XF86XK_Switch_VT_2: 0x1008FE02, + XF86XK_Switch_VT_3: 0x1008FE03, + XF86XK_Switch_VT_4: 0x1008FE04, + XF86XK_Switch_VT_5: 0x1008FE05, + XF86XK_Switch_VT_6: 0x1008FE06, + XF86XK_Switch_VT_7: 0x1008FE07, + XF86XK_Switch_VT_8: 0x1008FE08, + XF86XK_Switch_VT_9: 0x1008FE09, + XF86XK_Switch_VT_10: 0x1008FE0A, + XF86XK_Switch_VT_11: 0x1008FE0B, + XF86XK_Switch_VT_12: 0x1008FE0C, + XF86XK_Ungrab: 0x1008FE20, + XF86XK_ClearGrab: 0x1008FE21, + XF86XK_Next_VMode: 0x1008FE22, + XF86XK_Prev_VMode: 0x1008FE23, + XF86XK_LogWindowTree: 0x1008FE24, + XF86XK_LogGrabInfo: 0x1008FE25, }; diff --git a/core/input/keysymdef.js b/core/input/keysymdef.js index 951cacab..55593c5d 100644 --- a/core/input/keysymdef.js +++ b/core/input/keysymdef.js @@ -8,681 +8,681 @@ /* Functions at the bottom */ const codepoints = { - 0x0100: 0x03c0, // XK_Amacron - 0x0101: 0x03e0, // XK_amacron - 0x0102: 0x01c3, // XK_Abreve - 0x0103: 0x01e3, // XK_abreve - 0x0104: 0x01a1, // XK_Aogonek - 0x0105: 0x01b1, // XK_aogonek - 0x0106: 0x01c6, // XK_Cacute - 0x0107: 0x01e6, // XK_cacute - 0x0108: 0x02c6, // XK_Ccircumflex - 0x0109: 0x02e6, // XK_ccircumflex - 0x010a: 0x02c5, // XK_Cabovedot - 0x010b: 0x02e5, // XK_cabovedot - 0x010c: 0x01c8, // XK_Ccaron - 0x010d: 0x01e8, // XK_ccaron - 0x010e: 0x01cf, // XK_Dcaron - 0x010f: 0x01ef, // XK_dcaron - 0x0110: 0x01d0, // XK_Dstroke - 0x0111: 0x01f0, // XK_dstroke - 0x0112: 0x03aa, // XK_Emacron - 0x0113: 0x03ba, // XK_emacron - 0x0116: 0x03cc, // XK_Eabovedot - 0x0117: 0x03ec, // XK_eabovedot - 0x0118: 0x01ca, // XK_Eogonek - 0x0119: 0x01ea, // XK_eogonek - 0x011a: 0x01cc, // XK_Ecaron - 0x011b: 0x01ec, // XK_ecaron - 0x011c: 0x02d8, // XK_Gcircumflex - 0x011d: 0x02f8, // XK_gcircumflex - 0x011e: 0x02ab, // XK_Gbreve - 0x011f: 0x02bb, // XK_gbreve - 0x0120: 0x02d5, // XK_Gabovedot - 0x0121: 0x02f5, // XK_gabovedot - 0x0122: 0x03ab, // XK_Gcedilla - 0x0123: 0x03bb, // XK_gcedilla - 0x0124: 0x02a6, // XK_Hcircumflex - 0x0125: 0x02b6, // XK_hcircumflex - 0x0126: 0x02a1, // XK_Hstroke - 0x0127: 0x02b1, // XK_hstroke - 0x0128: 0x03a5, // XK_Itilde - 0x0129: 0x03b5, // XK_itilde - 0x012a: 0x03cf, // XK_Imacron - 0x012b: 0x03ef, // XK_imacron - 0x012e: 0x03c7, // XK_Iogonek - 0x012f: 0x03e7, // XK_iogonek - 0x0130: 0x02a9, // XK_Iabovedot - 0x0131: 0x02b9, // XK_idotless - 0x0134: 0x02ac, // XK_Jcircumflex - 0x0135: 0x02bc, // XK_jcircumflex - 0x0136: 0x03d3, // XK_Kcedilla - 0x0137: 0x03f3, // XK_kcedilla - 0x0138: 0x03a2, // XK_kra - 0x0139: 0x01c5, // XK_Lacute - 0x013a: 0x01e5, // XK_lacute - 0x013b: 0x03a6, // XK_Lcedilla - 0x013c: 0x03b6, // XK_lcedilla - 0x013d: 0x01a5, // XK_Lcaron - 0x013e: 0x01b5, // XK_lcaron - 0x0141: 0x01a3, // XK_Lstroke - 0x0142: 0x01b3, // XK_lstroke - 0x0143: 0x01d1, // XK_Nacute - 0x0144: 0x01f1, // XK_nacute - 0x0145: 0x03d1, // XK_Ncedilla - 0x0146: 0x03f1, // XK_ncedilla - 0x0147: 0x01d2, // XK_Ncaron - 0x0148: 0x01f2, // XK_ncaron - 0x014a: 0x03bd, // XK_ENG - 0x014b: 0x03bf, // XK_eng - 0x014c: 0x03d2, // XK_Omacron - 0x014d: 0x03f2, // XK_omacron - 0x0150: 0x01d5, // XK_Odoubleacute - 0x0151: 0x01f5, // XK_odoubleacute - 0x0152: 0x13bc, // XK_OE - 0x0153: 0x13bd, // XK_oe - 0x0154: 0x01c0, // XK_Racute - 0x0155: 0x01e0, // XK_racute - 0x0156: 0x03a3, // XK_Rcedilla - 0x0157: 0x03b3, // XK_rcedilla - 0x0158: 0x01d8, // XK_Rcaron - 0x0159: 0x01f8, // XK_rcaron - 0x015a: 0x01a6, // XK_Sacute - 0x015b: 0x01b6, // XK_sacute - 0x015c: 0x02de, // XK_Scircumflex - 0x015d: 0x02fe, // XK_scircumflex - 0x015e: 0x01aa, // XK_Scedilla - 0x015f: 0x01ba, // XK_scedilla - 0x0160: 0x01a9, // XK_Scaron - 0x0161: 0x01b9, // XK_scaron - 0x0162: 0x01de, // XK_Tcedilla - 0x0163: 0x01fe, // XK_tcedilla - 0x0164: 0x01ab, // XK_Tcaron - 0x0165: 0x01bb, // XK_tcaron - 0x0166: 0x03ac, // XK_Tslash - 0x0167: 0x03bc, // XK_tslash - 0x0168: 0x03dd, // XK_Utilde - 0x0169: 0x03fd, // XK_utilde - 0x016a: 0x03de, // XK_Umacron - 0x016b: 0x03fe, // XK_umacron - 0x016c: 0x02dd, // XK_Ubreve - 0x016d: 0x02fd, // XK_ubreve - 0x016e: 0x01d9, // XK_Uring - 0x016f: 0x01f9, // XK_uring - 0x0170: 0x01db, // XK_Udoubleacute - 0x0171: 0x01fb, // XK_udoubleacute - 0x0172: 0x03d9, // XK_Uogonek - 0x0173: 0x03f9, // XK_uogonek - 0x0178: 0x13be, // XK_Ydiaeresis - 0x0179: 0x01ac, // XK_Zacute - 0x017a: 0x01bc, // XK_zacute - 0x017b: 0x01af, // XK_Zabovedot - 0x017c: 0x01bf, // XK_zabovedot - 0x017d: 0x01ae, // XK_Zcaron - 0x017e: 0x01be, // XK_zcaron - 0x0192: 0x08f6, // XK_function - 0x01d2: 0x10001d1, // XK_Ocaron - 0x02c7: 0x01b7, // XK_caron - 0x02d8: 0x01a2, // XK_breve - 0x02d9: 0x01ff, // XK_abovedot - 0x02db: 0x01b2, // XK_ogonek - 0x02dd: 0x01bd, // XK_doubleacute - 0x0385: 0x07ae, // XK_Greek_accentdieresis - 0x0386: 0x07a1, // XK_Greek_ALPHAaccent - 0x0388: 0x07a2, // XK_Greek_EPSILONaccent - 0x0389: 0x07a3, // XK_Greek_ETAaccent - 0x038a: 0x07a4, // XK_Greek_IOTAaccent - 0x038c: 0x07a7, // XK_Greek_OMICRONaccent - 0x038e: 0x07a8, // XK_Greek_UPSILONaccent - 0x038f: 0x07ab, // XK_Greek_OMEGAaccent - 0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis - 0x0391: 0x07c1, // XK_Greek_ALPHA - 0x0392: 0x07c2, // XK_Greek_BETA - 0x0393: 0x07c3, // XK_Greek_GAMMA - 0x0394: 0x07c4, // XK_Greek_DELTA - 0x0395: 0x07c5, // XK_Greek_EPSILON - 0x0396: 0x07c6, // XK_Greek_ZETA - 0x0397: 0x07c7, // XK_Greek_ETA - 0x0398: 0x07c8, // XK_Greek_THETA - 0x0399: 0x07c9, // XK_Greek_IOTA - 0x039a: 0x07ca, // XK_Greek_KAPPA - 0x039b: 0x07cb, // XK_Greek_LAMDA - 0x039c: 0x07cc, // XK_Greek_MU - 0x039d: 0x07cd, // XK_Greek_NU - 0x039e: 0x07ce, // XK_Greek_XI - 0x039f: 0x07cf, // XK_Greek_OMICRON - 0x03a0: 0x07d0, // XK_Greek_PI - 0x03a1: 0x07d1, // XK_Greek_RHO - 0x03a3: 0x07d2, // XK_Greek_SIGMA - 0x03a4: 0x07d4, // XK_Greek_TAU - 0x03a5: 0x07d5, // XK_Greek_UPSILON - 0x03a6: 0x07d6, // XK_Greek_PHI - 0x03a7: 0x07d7, // XK_Greek_CHI - 0x03a8: 0x07d8, // XK_Greek_PSI - 0x03a9: 0x07d9, // XK_Greek_OMEGA - 0x03aa: 0x07a5, // XK_Greek_IOTAdieresis - 0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis - 0x03ac: 0x07b1, // XK_Greek_alphaaccent - 0x03ad: 0x07b2, // XK_Greek_epsilonaccent - 0x03ae: 0x07b3, // XK_Greek_etaaccent - 0x03af: 0x07b4, // XK_Greek_iotaaccent - 0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis - 0x03b1: 0x07e1, // XK_Greek_alpha - 0x03b2: 0x07e2, // XK_Greek_beta - 0x03b3: 0x07e3, // XK_Greek_gamma - 0x03b4: 0x07e4, // XK_Greek_delta - 0x03b5: 0x07e5, // XK_Greek_epsilon - 0x03b6: 0x07e6, // XK_Greek_zeta - 0x03b7: 0x07e7, // XK_Greek_eta - 0x03b8: 0x07e8, // XK_Greek_theta - 0x03b9: 0x07e9, // XK_Greek_iota - 0x03ba: 0x07ea, // XK_Greek_kappa - 0x03bb: 0x07eb, // XK_Greek_lamda - 0x03bc: 0x07ec, // XK_Greek_mu - 0x03bd: 0x07ed, // XK_Greek_nu - 0x03be: 0x07ee, // XK_Greek_xi - 0x03bf: 0x07ef, // XK_Greek_omicron - 0x03c0: 0x07f0, // XK_Greek_pi - 0x03c1: 0x07f1, // XK_Greek_rho - 0x03c2: 0x07f3, // XK_Greek_finalsmallsigma - 0x03c3: 0x07f2, // XK_Greek_sigma - 0x03c4: 0x07f4, // XK_Greek_tau - 0x03c5: 0x07f5, // XK_Greek_upsilon - 0x03c6: 0x07f6, // XK_Greek_phi - 0x03c7: 0x07f7, // XK_Greek_chi - 0x03c8: 0x07f8, // XK_Greek_psi - 0x03c9: 0x07f9, // XK_Greek_omega - 0x03ca: 0x07b5, // XK_Greek_iotadieresis - 0x03cb: 0x07b9, // XK_Greek_upsilondieresis - 0x03cc: 0x07b7, // XK_Greek_omicronaccent - 0x03cd: 0x07b8, // XK_Greek_upsilonaccent - 0x03ce: 0x07bb, // XK_Greek_omegaaccent - 0x0401: 0x06b3, // XK_Cyrillic_IO - 0x0402: 0x06b1, // XK_Serbian_DJE - 0x0403: 0x06b2, // XK_Macedonia_GJE - 0x0404: 0x06b4, // XK_Ukrainian_IE - 0x0405: 0x06b5, // XK_Macedonia_DSE - 0x0406: 0x06b6, // XK_Ukrainian_I - 0x0407: 0x06b7, // XK_Ukrainian_YI - 0x0408: 0x06b8, // XK_Cyrillic_JE - 0x0409: 0x06b9, // XK_Cyrillic_LJE - 0x040a: 0x06ba, // XK_Cyrillic_NJE - 0x040b: 0x06bb, // XK_Serbian_TSHE - 0x040c: 0x06bc, // XK_Macedonia_KJE - 0x040e: 0x06be, // XK_Byelorussian_SHORTU - 0x040f: 0x06bf, // XK_Cyrillic_DZHE - 0x0410: 0x06e1, // XK_Cyrillic_A - 0x0411: 0x06e2, // XK_Cyrillic_BE - 0x0412: 0x06f7, // XK_Cyrillic_VE - 0x0413: 0x06e7, // XK_Cyrillic_GHE - 0x0414: 0x06e4, // XK_Cyrillic_DE - 0x0415: 0x06e5, // XK_Cyrillic_IE - 0x0416: 0x06f6, // XK_Cyrillic_ZHE - 0x0417: 0x06fa, // XK_Cyrillic_ZE - 0x0418: 0x06e9, // XK_Cyrillic_I - 0x0419: 0x06ea, // XK_Cyrillic_SHORTI - 0x041a: 0x06eb, // XK_Cyrillic_KA - 0x041b: 0x06ec, // XK_Cyrillic_EL - 0x041c: 0x06ed, // XK_Cyrillic_EM - 0x041d: 0x06ee, // XK_Cyrillic_EN - 0x041e: 0x06ef, // XK_Cyrillic_O - 0x041f: 0x06f0, // XK_Cyrillic_PE - 0x0420: 0x06f2, // XK_Cyrillic_ER - 0x0421: 0x06f3, // XK_Cyrillic_ES - 0x0422: 0x06f4, // XK_Cyrillic_TE - 0x0423: 0x06f5, // XK_Cyrillic_U - 0x0424: 0x06e6, // XK_Cyrillic_EF - 0x0425: 0x06e8, // XK_Cyrillic_HA - 0x0426: 0x06e3, // XK_Cyrillic_TSE - 0x0427: 0x06fe, // XK_Cyrillic_CHE - 0x0428: 0x06fb, // XK_Cyrillic_SHA - 0x0429: 0x06fd, // XK_Cyrillic_SHCHA - 0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN - 0x042b: 0x06f9, // XK_Cyrillic_YERU - 0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN - 0x042d: 0x06fc, // XK_Cyrillic_E - 0x042e: 0x06e0, // XK_Cyrillic_YU - 0x042f: 0x06f1, // XK_Cyrillic_YA - 0x0430: 0x06c1, // XK_Cyrillic_a - 0x0431: 0x06c2, // XK_Cyrillic_be - 0x0432: 0x06d7, // XK_Cyrillic_ve - 0x0433: 0x06c7, // XK_Cyrillic_ghe - 0x0434: 0x06c4, // XK_Cyrillic_de - 0x0435: 0x06c5, // XK_Cyrillic_ie - 0x0436: 0x06d6, // XK_Cyrillic_zhe - 0x0437: 0x06da, // XK_Cyrillic_ze - 0x0438: 0x06c9, // XK_Cyrillic_i - 0x0439: 0x06ca, // XK_Cyrillic_shorti - 0x043a: 0x06cb, // XK_Cyrillic_ka - 0x043b: 0x06cc, // XK_Cyrillic_el - 0x043c: 0x06cd, // XK_Cyrillic_em - 0x043d: 0x06ce, // XK_Cyrillic_en - 0x043e: 0x06cf, // XK_Cyrillic_o - 0x043f: 0x06d0, // XK_Cyrillic_pe - 0x0440: 0x06d2, // XK_Cyrillic_er - 0x0441: 0x06d3, // XK_Cyrillic_es - 0x0442: 0x06d4, // XK_Cyrillic_te - 0x0443: 0x06d5, // XK_Cyrillic_u - 0x0444: 0x06c6, // XK_Cyrillic_ef - 0x0445: 0x06c8, // XK_Cyrillic_ha - 0x0446: 0x06c3, // XK_Cyrillic_tse - 0x0447: 0x06de, // XK_Cyrillic_che - 0x0448: 0x06db, // XK_Cyrillic_sha - 0x0449: 0x06dd, // XK_Cyrillic_shcha - 0x044a: 0x06df, // XK_Cyrillic_hardsign - 0x044b: 0x06d9, // XK_Cyrillic_yeru - 0x044c: 0x06d8, // XK_Cyrillic_softsign - 0x044d: 0x06dc, // XK_Cyrillic_e - 0x044e: 0x06c0, // XK_Cyrillic_yu - 0x044f: 0x06d1, // XK_Cyrillic_ya - 0x0451: 0x06a3, // XK_Cyrillic_io - 0x0452: 0x06a1, // XK_Serbian_dje - 0x0453: 0x06a2, // XK_Macedonia_gje - 0x0454: 0x06a4, // XK_Ukrainian_ie - 0x0455: 0x06a5, // XK_Macedonia_dse - 0x0456: 0x06a6, // XK_Ukrainian_i - 0x0457: 0x06a7, // XK_Ukrainian_yi - 0x0458: 0x06a8, // XK_Cyrillic_je - 0x0459: 0x06a9, // XK_Cyrillic_lje - 0x045a: 0x06aa, // XK_Cyrillic_nje - 0x045b: 0x06ab, // XK_Serbian_tshe - 0x045c: 0x06ac, // XK_Macedonia_kje - 0x045e: 0x06ae, // XK_Byelorussian_shortu - 0x045f: 0x06af, // XK_Cyrillic_dzhe - 0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN - 0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn - 0x05d0: 0x0ce0, // XK_hebrew_aleph - 0x05d1: 0x0ce1, // XK_hebrew_bet - 0x05d2: 0x0ce2, // XK_hebrew_gimel - 0x05d3: 0x0ce3, // XK_hebrew_dalet - 0x05d4: 0x0ce4, // XK_hebrew_he - 0x05d5: 0x0ce5, // XK_hebrew_waw - 0x05d6: 0x0ce6, // XK_hebrew_zain - 0x05d7: 0x0ce7, // XK_hebrew_chet - 0x05d8: 0x0ce8, // XK_hebrew_tet - 0x05d9: 0x0ce9, // XK_hebrew_yod - 0x05da: 0x0cea, // XK_hebrew_finalkaph - 0x05db: 0x0ceb, // XK_hebrew_kaph - 0x05dc: 0x0cec, // XK_hebrew_lamed - 0x05dd: 0x0ced, // XK_hebrew_finalmem - 0x05de: 0x0cee, // XK_hebrew_mem - 0x05df: 0x0cef, // XK_hebrew_finalnun - 0x05e0: 0x0cf0, // XK_hebrew_nun - 0x05e1: 0x0cf1, // XK_hebrew_samech - 0x05e2: 0x0cf2, // XK_hebrew_ayin - 0x05e3: 0x0cf3, // XK_hebrew_finalpe - 0x05e4: 0x0cf4, // XK_hebrew_pe - 0x05e5: 0x0cf5, // XK_hebrew_finalzade - 0x05e6: 0x0cf6, // XK_hebrew_zade - 0x05e7: 0x0cf7, // XK_hebrew_qoph - 0x05e8: 0x0cf8, // XK_hebrew_resh - 0x05e9: 0x0cf9, // XK_hebrew_shin - 0x05ea: 0x0cfa, // XK_hebrew_taw - 0x060c: 0x05ac, // XK_Arabic_comma - 0x061b: 0x05bb, // XK_Arabic_semicolon - 0x061f: 0x05bf, // XK_Arabic_question_mark - 0x0621: 0x05c1, // XK_Arabic_hamza - 0x0622: 0x05c2, // XK_Arabic_maddaonalef - 0x0623: 0x05c3, // XK_Arabic_hamzaonalef - 0x0624: 0x05c4, // XK_Arabic_hamzaonwaw - 0x0625: 0x05c5, // XK_Arabic_hamzaunderalef - 0x0626: 0x05c6, // XK_Arabic_hamzaonyeh - 0x0627: 0x05c7, // XK_Arabic_alef - 0x0628: 0x05c8, // XK_Arabic_beh - 0x0629: 0x05c9, // XK_Arabic_tehmarbuta - 0x062a: 0x05ca, // XK_Arabic_teh - 0x062b: 0x05cb, // XK_Arabic_theh - 0x062c: 0x05cc, // XK_Arabic_jeem - 0x062d: 0x05cd, // XK_Arabic_hah - 0x062e: 0x05ce, // XK_Arabic_khah - 0x062f: 0x05cf, // XK_Arabic_dal - 0x0630: 0x05d0, // XK_Arabic_thal - 0x0631: 0x05d1, // XK_Arabic_ra - 0x0632: 0x05d2, // XK_Arabic_zain - 0x0633: 0x05d3, // XK_Arabic_seen - 0x0634: 0x05d4, // XK_Arabic_sheen - 0x0635: 0x05d5, // XK_Arabic_sad - 0x0636: 0x05d6, // XK_Arabic_dad - 0x0637: 0x05d7, // XK_Arabic_tah - 0x0638: 0x05d8, // XK_Arabic_zah - 0x0639: 0x05d9, // XK_Arabic_ain - 0x063a: 0x05da, // XK_Arabic_ghain - 0x0640: 0x05e0, // XK_Arabic_tatweel - 0x0641: 0x05e1, // XK_Arabic_feh - 0x0642: 0x05e2, // XK_Arabic_qaf - 0x0643: 0x05e3, // XK_Arabic_kaf - 0x0644: 0x05e4, // XK_Arabic_lam - 0x0645: 0x05e5, // XK_Arabic_meem - 0x0646: 0x05e6, // XK_Arabic_noon - 0x0647: 0x05e7, // XK_Arabic_ha - 0x0648: 0x05e8, // XK_Arabic_waw - 0x0649: 0x05e9, // XK_Arabic_alefmaksura - 0x064a: 0x05ea, // XK_Arabic_yeh - 0x064b: 0x05eb, // XK_Arabic_fathatan - 0x064c: 0x05ec, // XK_Arabic_dammatan - 0x064d: 0x05ed, // XK_Arabic_kasratan - 0x064e: 0x05ee, // XK_Arabic_fatha - 0x064f: 0x05ef, // XK_Arabic_damma - 0x0650: 0x05f0, // XK_Arabic_kasra - 0x0651: 0x05f1, // XK_Arabic_shadda - 0x0652: 0x05f2, // XK_Arabic_sukun - 0x0e01: 0x0da1, // XK_Thai_kokai - 0x0e02: 0x0da2, // XK_Thai_khokhai - 0x0e03: 0x0da3, // XK_Thai_khokhuat - 0x0e04: 0x0da4, // XK_Thai_khokhwai - 0x0e05: 0x0da5, // XK_Thai_khokhon - 0x0e06: 0x0da6, // XK_Thai_khorakhang - 0x0e07: 0x0da7, // XK_Thai_ngongu - 0x0e08: 0x0da8, // XK_Thai_chochan - 0x0e09: 0x0da9, // XK_Thai_choching - 0x0e0a: 0x0daa, // XK_Thai_chochang - 0x0e0b: 0x0dab, // XK_Thai_soso - 0x0e0c: 0x0dac, // XK_Thai_chochoe - 0x0e0d: 0x0dad, // XK_Thai_yoying - 0x0e0e: 0x0dae, // XK_Thai_dochada - 0x0e0f: 0x0daf, // XK_Thai_topatak - 0x0e10: 0x0db0, // XK_Thai_thothan - 0x0e11: 0x0db1, // XK_Thai_thonangmontho - 0x0e12: 0x0db2, // XK_Thai_thophuthao - 0x0e13: 0x0db3, // XK_Thai_nonen - 0x0e14: 0x0db4, // XK_Thai_dodek - 0x0e15: 0x0db5, // XK_Thai_totao - 0x0e16: 0x0db6, // XK_Thai_thothung - 0x0e17: 0x0db7, // XK_Thai_thothahan - 0x0e18: 0x0db8, // XK_Thai_thothong - 0x0e19: 0x0db9, // XK_Thai_nonu - 0x0e1a: 0x0dba, // XK_Thai_bobaimai - 0x0e1b: 0x0dbb, // XK_Thai_popla - 0x0e1c: 0x0dbc, // XK_Thai_phophung - 0x0e1d: 0x0dbd, // XK_Thai_fofa - 0x0e1e: 0x0dbe, // XK_Thai_phophan - 0x0e1f: 0x0dbf, // XK_Thai_fofan - 0x0e20: 0x0dc0, // XK_Thai_phosamphao - 0x0e21: 0x0dc1, // XK_Thai_moma - 0x0e22: 0x0dc2, // XK_Thai_yoyak - 0x0e23: 0x0dc3, // XK_Thai_rorua - 0x0e24: 0x0dc4, // XK_Thai_ru - 0x0e25: 0x0dc5, // XK_Thai_loling - 0x0e26: 0x0dc6, // XK_Thai_lu - 0x0e27: 0x0dc7, // XK_Thai_wowaen - 0x0e28: 0x0dc8, // XK_Thai_sosala - 0x0e29: 0x0dc9, // XK_Thai_sorusi - 0x0e2a: 0x0dca, // XK_Thai_sosua - 0x0e2b: 0x0dcb, // XK_Thai_hohip - 0x0e2c: 0x0dcc, // XK_Thai_lochula - 0x0e2d: 0x0dcd, // XK_Thai_oang - 0x0e2e: 0x0dce, // XK_Thai_honokhuk - 0x0e2f: 0x0dcf, // XK_Thai_paiyannoi - 0x0e30: 0x0dd0, // XK_Thai_saraa - 0x0e31: 0x0dd1, // XK_Thai_maihanakat - 0x0e32: 0x0dd2, // XK_Thai_saraaa - 0x0e33: 0x0dd3, // XK_Thai_saraam - 0x0e34: 0x0dd4, // XK_Thai_sarai - 0x0e35: 0x0dd5, // XK_Thai_saraii - 0x0e36: 0x0dd6, // XK_Thai_saraue - 0x0e37: 0x0dd7, // XK_Thai_sarauee - 0x0e38: 0x0dd8, // XK_Thai_sarau - 0x0e39: 0x0dd9, // XK_Thai_sarauu - 0x0e3a: 0x0dda, // XK_Thai_phinthu - 0x0e3f: 0x0ddf, // XK_Thai_baht - 0x0e40: 0x0de0, // XK_Thai_sarae - 0x0e41: 0x0de1, // XK_Thai_saraae - 0x0e42: 0x0de2, // XK_Thai_sarao - 0x0e43: 0x0de3, // XK_Thai_saraaimaimuan - 0x0e44: 0x0de4, // XK_Thai_saraaimaimalai - 0x0e45: 0x0de5, // XK_Thai_lakkhangyao - 0x0e46: 0x0de6, // XK_Thai_maiyamok - 0x0e47: 0x0de7, // XK_Thai_maitaikhu - 0x0e48: 0x0de8, // XK_Thai_maiek - 0x0e49: 0x0de9, // XK_Thai_maitho - 0x0e4a: 0x0dea, // XK_Thai_maitri - 0x0e4b: 0x0deb, // XK_Thai_maichattawa - 0x0e4c: 0x0dec, // XK_Thai_thanthakhat - 0x0e4d: 0x0ded, // XK_Thai_nikhahit - 0x0e50: 0x0df0, // XK_Thai_leksun - 0x0e51: 0x0df1, // XK_Thai_leknung - 0x0e52: 0x0df2, // XK_Thai_leksong - 0x0e53: 0x0df3, // XK_Thai_leksam - 0x0e54: 0x0df4, // XK_Thai_leksi - 0x0e55: 0x0df5, // XK_Thai_lekha - 0x0e56: 0x0df6, // XK_Thai_lekhok - 0x0e57: 0x0df7, // XK_Thai_lekchet - 0x0e58: 0x0df8, // XK_Thai_lekpaet - 0x0e59: 0x0df9, // XK_Thai_lekkao - 0x2002: 0x0aa2, // XK_enspace - 0x2003: 0x0aa1, // XK_emspace - 0x2004: 0x0aa3, // XK_em3space - 0x2005: 0x0aa4, // XK_em4space - 0x2007: 0x0aa5, // XK_digitspace - 0x2008: 0x0aa6, // XK_punctspace - 0x2009: 0x0aa7, // XK_thinspace - 0x200a: 0x0aa8, // XK_hairspace - 0x2012: 0x0abb, // XK_figdash - 0x2013: 0x0aaa, // XK_endash - 0x2014: 0x0aa9, // XK_emdash - 0x2015: 0x07af, // XK_Greek_horizbar - 0x2017: 0x0cdf, // XK_hebrew_doublelowline - 0x2018: 0x0ad0, // XK_leftsinglequotemark - 0x2019: 0x0ad1, // XK_rightsinglequotemark - 0x201a: 0x0afd, // XK_singlelowquotemark - 0x201c: 0x0ad2, // XK_leftdoublequotemark - 0x201d: 0x0ad3, // XK_rightdoublequotemark - 0x201e: 0x0afe, // XK_doublelowquotemark - 0x2020: 0x0af1, // XK_dagger - 0x2021: 0x0af2, // XK_doubledagger - 0x2022: 0x0ae6, // XK_enfilledcircbullet - 0x2025: 0x0aaf, // XK_doubbaselinedot - 0x2026: 0x0aae, // XK_ellipsis - 0x2030: 0x0ad5, // XK_permille - 0x2032: 0x0ad6, // XK_minutes - 0x2033: 0x0ad7, // XK_seconds - 0x2038: 0x0afc, // XK_caret - 0x203e: 0x047e, // XK_overline - 0x20a9: 0x0eff, // XK_Korean_Won - 0x20ac: 0x20ac, // XK_EuroSign - 0x2105: 0x0ab8, // XK_careof - 0x2116: 0x06b0, // XK_numerosign - 0x2117: 0x0afb, // XK_phonographcopyright - 0x211e: 0x0ad4, // XK_prescription - 0x2122: 0x0ac9, // XK_trademark - 0x2153: 0x0ab0, // XK_onethird - 0x2154: 0x0ab1, // XK_twothirds - 0x2155: 0x0ab2, // XK_onefifth - 0x2156: 0x0ab3, // XK_twofifths - 0x2157: 0x0ab4, // XK_threefifths - 0x2158: 0x0ab5, // XK_fourfifths - 0x2159: 0x0ab6, // XK_onesixth - 0x215a: 0x0ab7, // XK_fivesixths - 0x215b: 0x0ac3, // XK_oneeighth - 0x215c: 0x0ac4, // XK_threeeighths - 0x215d: 0x0ac5, // XK_fiveeighths - 0x215e: 0x0ac6, // XK_seveneighths - 0x2190: 0x08fb, // XK_leftarrow - 0x2191: 0x08fc, // XK_uparrow - 0x2192: 0x08fd, // XK_rightarrow - 0x2193: 0x08fe, // XK_downarrow - 0x21d2: 0x08ce, // XK_implies - 0x21d4: 0x08cd, // XK_ifonlyif - 0x2202: 0x08ef, // XK_partialderivative - 0x2207: 0x08c5, // XK_nabla - 0x2218: 0x0bca, // XK_jot - 0x221a: 0x08d6, // XK_radical - 0x221d: 0x08c1, // XK_variation - 0x221e: 0x08c2, // XK_infinity - 0x2227: 0x08de, // XK_logicaland - 0x2228: 0x08df, // XK_logicalor - 0x2229: 0x08dc, // XK_intersection - 0x222a: 0x08dd, // XK_union - 0x222b: 0x08bf, // XK_integral - 0x2234: 0x08c0, // XK_therefore - 0x223c: 0x08c8, // XK_approximate - 0x2243: 0x08c9, // XK_similarequal - 0x2245: 0x1002248, // XK_approxeq - 0x2260: 0x08bd, // XK_notequal - 0x2261: 0x08cf, // XK_identical - 0x2264: 0x08bc, // XK_lessthanequal - 0x2265: 0x08be, // XK_greaterthanequal - 0x2282: 0x08da, // XK_includedin - 0x2283: 0x08db, // XK_includes - 0x22a2: 0x0bfc, // XK_righttack - 0x22a3: 0x0bdc, // XK_lefttack - 0x22a4: 0x0bc2, // XK_downtack - 0x22a5: 0x0bce, // XK_uptack - 0x2308: 0x0bd3, // XK_upstile - 0x230a: 0x0bc4, // XK_downstile - 0x2315: 0x0afa, // XK_telephonerecorder - 0x2320: 0x08a4, // XK_topintegral - 0x2321: 0x08a5, // XK_botintegral - 0x2395: 0x0bcc, // XK_quad - 0x239b: 0x08ab, // XK_topleftparens - 0x239d: 0x08ac, // XK_botleftparens - 0x239e: 0x08ad, // XK_toprightparens - 0x23a0: 0x08ae, // XK_botrightparens - 0x23a1: 0x08a7, // XK_topleftsqbracket - 0x23a3: 0x08a8, // XK_botleftsqbracket - 0x23a4: 0x08a9, // XK_toprightsqbracket - 0x23a6: 0x08aa, // XK_botrightsqbracket - 0x23a8: 0x08af, // XK_leftmiddlecurlybrace - 0x23ac: 0x08b0, // XK_rightmiddlecurlybrace - 0x23b7: 0x08a1, // XK_leftradical - 0x23ba: 0x09ef, // XK_horizlinescan1 - 0x23bb: 0x09f0, // XK_horizlinescan3 - 0x23bc: 0x09f2, // XK_horizlinescan7 - 0x23bd: 0x09f3, // XK_horizlinescan9 - 0x2409: 0x09e2, // XK_ht - 0x240a: 0x09e5, // XK_lf - 0x240b: 0x09e9, // XK_vt - 0x240c: 0x09e3, // XK_ff - 0x240d: 0x09e4, // XK_cr - 0x2423: 0x0aac, // XK_signifblank - 0x2424: 0x09e8, // XK_nl - 0x2500: 0x08a3, // XK_horizconnector - 0x2502: 0x08a6, // XK_vertconnector - 0x250c: 0x08a2, // XK_topleftradical - 0x2510: 0x09eb, // XK_uprightcorner - 0x2514: 0x09ed, // XK_lowleftcorner - 0x2518: 0x09ea, // XK_lowrightcorner - 0x251c: 0x09f4, // XK_leftt - 0x2524: 0x09f5, // XK_rightt - 0x252c: 0x09f7, // XK_topt - 0x2534: 0x09f6, // XK_bott - 0x253c: 0x09ee, // XK_crossinglines - 0x2592: 0x09e1, // XK_checkerboard - 0x25aa: 0x0ae7, // XK_enfilledsqbullet - 0x25ab: 0x0ae1, // XK_enopensquarebullet - 0x25ac: 0x0adb, // XK_filledrectbullet - 0x25ad: 0x0ae2, // XK_openrectbullet - 0x25ae: 0x0adf, // XK_emfilledrect - 0x25af: 0x0acf, // XK_emopenrectangle - 0x25b2: 0x0ae8, // XK_filledtribulletup - 0x25b3: 0x0ae3, // XK_opentribulletup - 0x25b6: 0x0add, // XK_filledrighttribullet - 0x25b7: 0x0acd, // XK_rightopentriangle - 0x25bc: 0x0ae9, // XK_filledtribulletdown - 0x25bd: 0x0ae4, // XK_opentribulletdown - 0x25c0: 0x0adc, // XK_filledlefttribullet - 0x25c1: 0x0acc, // XK_leftopentriangle - 0x25c6: 0x09e0, // XK_soliddiamond - 0x25cb: 0x0ace, // XK_emopencircle - 0x25cf: 0x0ade, // XK_emfilledcircle - 0x25e6: 0x0ae0, // XK_enopencircbullet - 0x2606: 0x0ae5, // XK_openstar - 0x260e: 0x0af9, // XK_telephone - 0x2613: 0x0aca, // XK_signaturemark - 0x261c: 0x0aea, // XK_leftpointer - 0x261e: 0x0aeb, // XK_rightpointer - 0x2640: 0x0af8, // XK_femalesymbol - 0x2642: 0x0af7, // XK_malesymbol - 0x2663: 0x0aec, // XK_club - 0x2665: 0x0aee, // XK_heart - 0x2666: 0x0aed, // XK_diamond - 0x266d: 0x0af6, // XK_musicalflat - 0x266f: 0x0af5, // XK_musicalsharp - 0x2713: 0x0af3, // XK_checkmark - 0x2717: 0x0af4, // XK_ballotcross - 0x271d: 0x0ad9, // XK_latincross - 0x2720: 0x0af0, // XK_maltesecross - 0x27e8: 0x0abc, // XK_leftanglebracket - 0x27e9: 0x0abe, // XK_rightanglebracket - 0x3001: 0x04a4, // XK_kana_comma - 0x3002: 0x04a1, // XK_kana_fullstop - 0x300c: 0x04a2, // XK_kana_openingbracket - 0x300d: 0x04a3, // XK_kana_closingbracket - 0x309b: 0x04de, // XK_voicedsound - 0x309c: 0x04df, // XK_semivoicedsound - 0x30a1: 0x04a7, // XK_kana_a - 0x30a2: 0x04b1, // XK_kana_A - 0x30a3: 0x04a8, // XK_kana_i - 0x30a4: 0x04b2, // XK_kana_I - 0x30a5: 0x04a9, // XK_kana_u - 0x30a6: 0x04b3, // XK_kana_U - 0x30a7: 0x04aa, // XK_kana_e - 0x30a8: 0x04b4, // XK_kana_E - 0x30a9: 0x04ab, // XK_kana_o - 0x30aa: 0x04b5, // XK_kana_O - 0x30ab: 0x04b6, // XK_kana_KA - 0x30ad: 0x04b7, // XK_kana_KI - 0x30af: 0x04b8, // XK_kana_KU - 0x30b1: 0x04b9, // XK_kana_KE - 0x30b3: 0x04ba, // XK_kana_KO - 0x30b5: 0x04bb, // XK_kana_SA - 0x30b7: 0x04bc, // XK_kana_SHI - 0x30b9: 0x04bd, // XK_kana_SU - 0x30bb: 0x04be, // XK_kana_SE - 0x30bd: 0x04bf, // XK_kana_SO - 0x30bf: 0x04c0, // XK_kana_TA - 0x30c1: 0x04c1, // XK_kana_CHI - 0x30c3: 0x04af, // XK_kana_tsu - 0x30c4: 0x04c2, // XK_kana_TSU - 0x30c6: 0x04c3, // XK_kana_TE - 0x30c8: 0x04c4, // XK_kana_TO - 0x30ca: 0x04c5, // XK_kana_NA - 0x30cb: 0x04c6, // XK_kana_NI - 0x30cc: 0x04c7, // XK_kana_NU - 0x30cd: 0x04c8, // XK_kana_NE - 0x30ce: 0x04c9, // XK_kana_NO - 0x30cf: 0x04ca, // XK_kana_HA - 0x30d2: 0x04cb, // XK_kana_HI - 0x30d5: 0x04cc, // XK_kana_FU - 0x30d8: 0x04cd, // XK_kana_HE - 0x30db: 0x04ce, // XK_kana_HO - 0x30de: 0x04cf, // XK_kana_MA - 0x30df: 0x04d0, // XK_kana_MI - 0x30e0: 0x04d1, // XK_kana_MU - 0x30e1: 0x04d2, // XK_kana_ME - 0x30e2: 0x04d3, // XK_kana_MO - 0x30e3: 0x04ac, // XK_kana_ya - 0x30e4: 0x04d4, // XK_kana_YA - 0x30e5: 0x04ad, // XK_kana_yu - 0x30e6: 0x04d5, // XK_kana_YU - 0x30e7: 0x04ae, // XK_kana_yo - 0x30e8: 0x04d6, // XK_kana_YO - 0x30e9: 0x04d7, // XK_kana_RA - 0x30ea: 0x04d8, // XK_kana_RI - 0x30eb: 0x04d9, // XK_kana_RU - 0x30ec: 0x04da, // XK_kana_RE - 0x30ed: 0x04db, // XK_kana_RO - 0x30ef: 0x04dc, // XK_kana_WA - 0x30f2: 0x04a6, // XK_kana_WO - 0x30f3: 0x04dd, // XK_kana_N - 0x30fb: 0x04a5, // XK_kana_conjunctive - 0x30fc: 0x04b0, // XK_prolongedsound + 0x0100: 0x03c0, // XK_Amacron + 0x0101: 0x03e0, // XK_amacron + 0x0102: 0x01c3, // XK_Abreve + 0x0103: 0x01e3, // XK_abreve + 0x0104: 0x01a1, // XK_Aogonek + 0x0105: 0x01b1, // XK_aogonek + 0x0106: 0x01c6, // XK_Cacute + 0x0107: 0x01e6, // XK_cacute + 0x0108: 0x02c6, // XK_Ccircumflex + 0x0109: 0x02e6, // XK_ccircumflex + 0x010a: 0x02c5, // XK_Cabovedot + 0x010b: 0x02e5, // XK_cabovedot + 0x010c: 0x01c8, // XK_Ccaron + 0x010d: 0x01e8, // XK_ccaron + 0x010e: 0x01cf, // XK_Dcaron + 0x010f: 0x01ef, // XK_dcaron + 0x0110: 0x01d0, // XK_Dstroke + 0x0111: 0x01f0, // XK_dstroke + 0x0112: 0x03aa, // XK_Emacron + 0x0113: 0x03ba, // XK_emacron + 0x0116: 0x03cc, // XK_Eabovedot + 0x0117: 0x03ec, // XK_eabovedot + 0x0118: 0x01ca, // XK_Eogonek + 0x0119: 0x01ea, // XK_eogonek + 0x011a: 0x01cc, // XK_Ecaron + 0x011b: 0x01ec, // XK_ecaron + 0x011c: 0x02d8, // XK_Gcircumflex + 0x011d: 0x02f8, // XK_gcircumflex + 0x011e: 0x02ab, // XK_Gbreve + 0x011f: 0x02bb, // XK_gbreve + 0x0120: 0x02d5, // XK_Gabovedot + 0x0121: 0x02f5, // XK_gabovedot + 0x0122: 0x03ab, // XK_Gcedilla + 0x0123: 0x03bb, // XK_gcedilla + 0x0124: 0x02a6, // XK_Hcircumflex + 0x0125: 0x02b6, // XK_hcircumflex + 0x0126: 0x02a1, // XK_Hstroke + 0x0127: 0x02b1, // XK_hstroke + 0x0128: 0x03a5, // XK_Itilde + 0x0129: 0x03b5, // XK_itilde + 0x012a: 0x03cf, // XK_Imacron + 0x012b: 0x03ef, // XK_imacron + 0x012e: 0x03c7, // XK_Iogonek + 0x012f: 0x03e7, // XK_iogonek + 0x0130: 0x02a9, // XK_Iabovedot + 0x0131: 0x02b9, // XK_idotless + 0x0134: 0x02ac, // XK_Jcircumflex + 0x0135: 0x02bc, // XK_jcircumflex + 0x0136: 0x03d3, // XK_Kcedilla + 0x0137: 0x03f3, // XK_kcedilla + 0x0138: 0x03a2, // XK_kra + 0x0139: 0x01c5, // XK_Lacute + 0x013a: 0x01e5, // XK_lacute + 0x013b: 0x03a6, // XK_Lcedilla + 0x013c: 0x03b6, // XK_lcedilla + 0x013d: 0x01a5, // XK_Lcaron + 0x013e: 0x01b5, // XK_lcaron + 0x0141: 0x01a3, // XK_Lstroke + 0x0142: 0x01b3, // XK_lstroke + 0x0143: 0x01d1, // XK_Nacute + 0x0144: 0x01f1, // XK_nacute + 0x0145: 0x03d1, // XK_Ncedilla + 0x0146: 0x03f1, // XK_ncedilla + 0x0147: 0x01d2, // XK_Ncaron + 0x0148: 0x01f2, // XK_ncaron + 0x014a: 0x03bd, // XK_ENG + 0x014b: 0x03bf, // XK_eng + 0x014c: 0x03d2, // XK_Omacron + 0x014d: 0x03f2, // XK_omacron + 0x0150: 0x01d5, // XK_Odoubleacute + 0x0151: 0x01f5, // XK_odoubleacute + 0x0152: 0x13bc, // XK_OE + 0x0153: 0x13bd, // XK_oe + 0x0154: 0x01c0, // XK_Racute + 0x0155: 0x01e0, // XK_racute + 0x0156: 0x03a3, // XK_Rcedilla + 0x0157: 0x03b3, // XK_rcedilla + 0x0158: 0x01d8, // XK_Rcaron + 0x0159: 0x01f8, // XK_rcaron + 0x015a: 0x01a6, // XK_Sacute + 0x015b: 0x01b6, // XK_sacute + 0x015c: 0x02de, // XK_Scircumflex + 0x015d: 0x02fe, // XK_scircumflex + 0x015e: 0x01aa, // XK_Scedilla + 0x015f: 0x01ba, // XK_scedilla + 0x0160: 0x01a9, // XK_Scaron + 0x0161: 0x01b9, // XK_scaron + 0x0162: 0x01de, // XK_Tcedilla + 0x0163: 0x01fe, // XK_tcedilla + 0x0164: 0x01ab, // XK_Tcaron + 0x0165: 0x01bb, // XK_tcaron + 0x0166: 0x03ac, // XK_Tslash + 0x0167: 0x03bc, // XK_tslash + 0x0168: 0x03dd, // XK_Utilde + 0x0169: 0x03fd, // XK_utilde + 0x016a: 0x03de, // XK_Umacron + 0x016b: 0x03fe, // XK_umacron + 0x016c: 0x02dd, // XK_Ubreve + 0x016d: 0x02fd, // XK_ubreve + 0x016e: 0x01d9, // XK_Uring + 0x016f: 0x01f9, // XK_uring + 0x0170: 0x01db, // XK_Udoubleacute + 0x0171: 0x01fb, // XK_udoubleacute + 0x0172: 0x03d9, // XK_Uogonek + 0x0173: 0x03f9, // XK_uogonek + 0x0178: 0x13be, // XK_Ydiaeresis + 0x0179: 0x01ac, // XK_Zacute + 0x017a: 0x01bc, // XK_zacute + 0x017b: 0x01af, // XK_Zabovedot + 0x017c: 0x01bf, // XK_zabovedot + 0x017d: 0x01ae, // XK_Zcaron + 0x017e: 0x01be, // XK_zcaron + 0x0192: 0x08f6, // XK_function + 0x01d2: 0x10001d1, // XK_Ocaron + 0x02c7: 0x01b7, // XK_caron + 0x02d8: 0x01a2, // XK_breve + 0x02d9: 0x01ff, // XK_abovedot + 0x02db: 0x01b2, // XK_ogonek + 0x02dd: 0x01bd, // XK_doubleacute + 0x0385: 0x07ae, // XK_Greek_accentdieresis + 0x0386: 0x07a1, // XK_Greek_ALPHAaccent + 0x0388: 0x07a2, // XK_Greek_EPSILONaccent + 0x0389: 0x07a3, // XK_Greek_ETAaccent + 0x038a: 0x07a4, // XK_Greek_IOTAaccent + 0x038c: 0x07a7, // XK_Greek_OMICRONaccent + 0x038e: 0x07a8, // XK_Greek_UPSILONaccent + 0x038f: 0x07ab, // XK_Greek_OMEGAaccent + 0x0390: 0x07b6, // XK_Greek_iotaaccentdieresis + 0x0391: 0x07c1, // XK_Greek_ALPHA + 0x0392: 0x07c2, // XK_Greek_BETA + 0x0393: 0x07c3, // XK_Greek_GAMMA + 0x0394: 0x07c4, // XK_Greek_DELTA + 0x0395: 0x07c5, // XK_Greek_EPSILON + 0x0396: 0x07c6, // XK_Greek_ZETA + 0x0397: 0x07c7, // XK_Greek_ETA + 0x0398: 0x07c8, // XK_Greek_THETA + 0x0399: 0x07c9, // XK_Greek_IOTA + 0x039a: 0x07ca, // XK_Greek_KAPPA + 0x039b: 0x07cb, // XK_Greek_LAMDA + 0x039c: 0x07cc, // XK_Greek_MU + 0x039d: 0x07cd, // XK_Greek_NU + 0x039e: 0x07ce, // XK_Greek_XI + 0x039f: 0x07cf, // XK_Greek_OMICRON + 0x03a0: 0x07d0, // XK_Greek_PI + 0x03a1: 0x07d1, // XK_Greek_RHO + 0x03a3: 0x07d2, // XK_Greek_SIGMA + 0x03a4: 0x07d4, // XK_Greek_TAU + 0x03a5: 0x07d5, // XK_Greek_UPSILON + 0x03a6: 0x07d6, // XK_Greek_PHI + 0x03a7: 0x07d7, // XK_Greek_CHI + 0x03a8: 0x07d8, // XK_Greek_PSI + 0x03a9: 0x07d9, // XK_Greek_OMEGA + 0x03aa: 0x07a5, // XK_Greek_IOTAdieresis + 0x03ab: 0x07a9, // XK_Greek_UPSILONdieresis + 0x03ac: 0x07b1, // XK_Greek_alphaaccent + 0x03ad: 0x07b2, // XK_Greek_epsilonaccent + 0x03ae: 0x07b3, // XK_Greek_etaaccent + 0x03af: 0x07b4, // XK_Greek_iotaaccent + 0x03b0: 0x07ba, // XK_Greek_upsilonaccentdieresis + 0x03b1: 0x07e1, // XK_Greek_alpha + 0x03b2: 0x07e2, // XK_Greek_beta + 0x03b3: 0x07e3, // XK_Greek_gamma + 0x03b4: 0x07e4, // XK_Greek_delta + 0x03b5: 0x07e5, // XK_Greek_epsilon + 0x03b6: 0x07e6, // XK_Greek_zeta + 0x03b7: 0x07e7, // XK_Greek_eta + 0x03b8: 0x07e8, // XK_Greek_theta + 0x03b9: 0x07e9, // XK_Greek_iota + 0x03ba: 0x07ea, // XK_Greek_kappa + 0x03bb: 0x07eb, // XK_Greek_lamda + 0x03bc: 0x07ec, // XK_Greek_mu + 0x03bd: 0x07ed, // XK_Greek_nu + 0x03be: 0x07ee, // XK_Greek_xi + 0x03bf: 0x07ef, // XK_Greek_omicron + 0x03c0: 0x07f0, // XK_Greek_pi + 0x03c1: 0x07f1, // XK_Greek_rho + 0x03c2: 0x07f3, // XK_Greek_finalsmallsigma + 0x03c3: 0x07f2, // XK_Greek_sigma + 0x03c4: 0x07f4, // XK_Greek_tau + 0x03c5: 0x07f5, // XK_Greek_upsilon + 0x03c6: 0x07f6, // XK_Greek_phi + 0x03c7: 0x07f7, // XK_Greek_chi + 0x03c8: 0x07f8, // XK_Greek_psi + 0x03c9: 0x07f9, // XK_Greek_omega + 0x03ca: 0x07b5, // XK_Greek_iotadieresis + 0x03cb: 0x07b9, // XK_Greek_upsilondieresis + 0x03cc: 0x07b7, // XK_Greek_omicronaccent + 0x03cd: 0x07b8, // XK_Greek_upsilonaccent + 0x03ce: 0x07bb, // XK_Greek_omegaaccent + 0x0401: 0x06b3, // XK_Cyrillic_IO + 0x0402: 0x06b1, // XK_Serbian_DJE + 0x0403: 0x06b2, // XK_Macedonia_GJE + 0x0404: 0x06b4, // XK_Ukrainian_IE + 0x0405: 0x06b5, // XK_Macedonia_DSE + 0x0406: 0x06b6, // XK_Ukrainian_I + 0x0407: 0x06b7, // XK_Ukrainian_YI + 0x0408: 0x06b8, // XK_Cyrillic_JE + 0x0409: 0x06b9, // XK_Cyrillic_LJE + 0x040a: 0x06ba, // XK_Cyrillic_NJE + 0x040b: 0x06bb, // XK_Serbian_TSHE + 0x040c: 0x06bc, // XK_Macedonia_KJE + 0x040e: 0x06be, // XK_Byelorussian_SHORTU + 0x040f: 0x06bf, // XK_Cyrillic_DZHE + 0x0410: 0x06e1, // XK_Cyrillic_A + 0x0411: 0x06e2, // XK_Cyrillic_BE + 0x0412: 0x06f7, // XK_Cyrillic_VE + 0x0413: 0x06e7, // XK_Cyrillic_GHE + 0x0414: 0x06e4, // XK_Cyrillic_DE + 0x0415: 0x06e5, // XK_Cyrillic_IE + 0x0416: 0x06f6, // XK_Cyrillic_ZHE + 0x0417: 0x06fa, // XK_Cyrillic_ZE + 0x0418: 0x06e9, // XK_Cyrillic_I + 0x0419: 0x06ea, // XK_Cyrillic_SHORTI + 0x041a: 0x06eb, // XK_Cyrillic_KA + 0x041b: 0x06ec, // XK_Cyrillic_EL + 0x041c: 0x06ed, // XK_Cyrillic_EM + 0x041d: 0x06ee, // XK_Cyrillic_EN + 0x041e: 0x06ef, // XK_Cyrillic_O + 0x041f: 0x06f0, // XK_Cyrillic_PE + 0x0420: 0x06f2, // XK_Cyrillic_ER + 0x0421: 0x06f3, // XK_Cyrillic_ES + 0x0422: 0x06f4, // XK_Cyrillic_TE + 0x0423: 0x06f5, // XK_Cyrillic_U + 0x0424: 0x06e6, // XK_Cyrillic_EF + 0x0425: 0x06e8, // XK_Cyrillic_HA + 0x0426: 0x06e3, // XK_Cyrillic_TSE + 0x0427: 0x06fe, // XK_Cyrillic_CHE + 0x0428: 0x06fb, // XK_Cyrillic_SHA + 0x0429: 0x06fd, // XK_Cyrillic_SHCHA + 0x042a: 0x06ff, // XK_Cyrillic_HARDSIGN + 0x042b: 0x06f9, // XK_Cyrillic_YERU + 0x042c: 0x06f8, // XK_Cyrillic_SOFTSIGN + 0x042d: 0x06fc, // XK_Cyrillic_E + 0x042e: 0x06e0, // XK_Cyrillic_YU + 0x042f: 0x06f1, // XK_Cyrillic_YA + 0x0430: 0x06c1, // XK_Cyrillic_a + 0x0431: 0x06c2, // XK_Cyrillic_be + 0x0432: 0x06d7, // XK_Cyrillic_ve + 0x0433: 0x06c7, // XK_Cyrillic_ghe + 0x0434: 0x06c4, // XK_Cyrillic_de + 0x0435: 0x06c5, // XK_Cyrillic_ie + 0x0436: 0x06d6, // XK_Cyrillic_zhe + 0x0437: 0x06da, // XK_Cyrillic_ze + 0x0438: 0x06c9, // XK_Cyrillic_i + 0x0439: 0x06ca, // XK_Cyrillic_shorti + 0x043a: 0x06cb, // XK_Cyrillic_ka + 0x043b: 0x06cc, // XK_Cyrillic_el + 0x043c: 0x06cd, // XK_Cyrillic_em + 0x043d: 0x06ce, // XK_Cyrillic_en + 0x043e: 0x06cf, // XK_Cyrillic_o + 0x043f: 0x06d0, // XK_Cyrillic_pe + 0x0440: 0x06d2, // XK_Cyrillic_er + 0x0441: 0x06d3, // XK_Cyrillic_es + 0x0442: 0x06d4, // XK_Cyrillic_te + 0x0443: 0x06d5, // XK_Cyrillic_u + 0x0444: 0x06c6, // XK_Cyrillic_ef + 0x0445: 0x06c8, // XK_Cyrillic_ha + 0x0446: 0x06c3, // XK_Cyrillic_tse + 0x0447: 0x06de, // XK_Cyrillic_che + 0x0448: 0x06db, // XK_Cyrillic_sha + 0x0449: 0x06dd, // XK_Cyrillic_shcha + 0x044a: 0x06df, // XK_Cyrillic_hardsign + 0x044b: 0x06d9, // XK_Cyrillic_yeru + 0x044c: 0x06d8, // XK_Cyrillic_softsign + 0x044d: 0x06dc, // XK_Cyrillic_e + 0x044e: 0x06c0, // XK_Cyrillic_yu + 0x044f: 0x06d1, // XK_Cyrillic_ya + 0x0451: 0x06a3, // XK_Cyrillic_io + 0x0452: 0x06a1, // XK_Serbian_dje + 0x0453: 0x06a2, // XK_Macedonia_gje + 0x0454: 0x06a4, // XK_Ukrainian_ie + 0x0455: 0x06a5, // XK_Macedonia_dse + 0x0456: 0x06a6, // XK_Ukrainian_i + 0x0457: 0x06a7, // XK_Ukrainian_yi + 0x0458: 0x06a8, // XK_Cyrillic_je + 0x0459: 0x06a9, // XK_Cyrillic_lje + 0x045a: 0x06aa, // XK_Cyrillic_nje + 0x045b: 0x06ab, // XK_Serbian_tshe + 0x045c: 0x06ac, // XK_Macedonia_kje + 0x045e: 0x06ae, // XK_Byelorussian_shortu + 0x045f: 0x06af, // XK_Cyrillic_dzhe + 0x0490: 0x06bd, // XK_Ukrainian_GHE_WITH_UPTURN + 0x0491: 0x06ad, // XK_Ukrainian_ghe_with_upturn + 0x05d0: 0x0ce0, // XK_hebrew_aleph + 0x05d1: 0x0ce1, // XK_hebrew_bet + 0x05d2: 0x0ce2, // XK_hebrew_gimel + 0x05d3: 0x0ce3, // XK_hebrew_dalet + 0x05d4: 0x0ce4, // XK_hebrew_he + 0x05d5: 0x0ce5, // XK_hebrew_waw + 0x05d6: 0x0ce6, // XK_hebrew_zain + 0x05d7: 0x0ce7, // XK_hebrew_chet + 0x05d8: 0x0ce8, // XK_hebrew_tet + 0x05d9: 0x0ce9, // XK_hebrew_yod + 0x05da: 0x0cea, // XK_hebrew_finalkaph + 0x05db: 0x0ceb, // XK_hebrew_kaph + 0x05dc: 0x0cec, // XK_hebrew_lamed + 0x05dd: 0x0ced, // XK_hebrew_finalmem + 0x05de: 0x0cee, // XK_hebrew_mem + 0x05df: 0x0cef, // XK_hebrew_finalnun + 0x05e0: 0x0cf0, // XK_hebrew_nun + 0x05e1: 0x0cf1, // XK_hebrew_samech + 0x05e2: 0x0cf2, // XK_hebrew_ayin + 0x05e3: 0x0cf3, // XK_hebrew_finalpe + 0x05e4: 0x0cf4, // XK_hebrew_pe + 0x05e5: 0x0cf5, // XK_hebrew_finalzade + 0x05e6: 0x0cf6, // XK_hebrew_zade + 0x05e7: 0x0cf7, // XK_hebrew_qoph + 0x05e8: 0x0cf8, // XK_hebrew_resh + 0x05e9: 0x0cf9, // XK_hebrew_shin + 0x05ea: 0x0cfa, // XK_hebrew_taw + 0x060c: 0x05ac, // XK_Arabic_comma + 0x061b: 0x05bb, // XK_Arabic_semicolon + 0x061f: 0x05bf, // XK_Arabic_question_mark + 0x0621: 0x05c1, // XK_Arabic_hamza + 0x0622: 0x05c2, // XK_Arabic_maddaonalef + 0x0623: 0x05c3, // XK_Arabic_hamzaonalef + 0x0624: 0x05c4, // XK_Arabic_hamzaonwaw + 0x0625: 0x05c5, // XK_Arabic_hamzaunderalef + 0x0626: 0x05c6, // XK_Arabic_hamzaonyeh + 0x0627: 0x05c7, // XK_Arabic_alef + 0x0628: 0x05c8, // XK_Arabic_beh + 0x0629: 0x05c9, // XK_Arabic_tehmarbuta + 0x062a: 0x05ca, // XK_Arabic_teh + 0x062b: 0x05cb, // XK_Arabic_theh + 0x062c: 0x05cc, // XK_Arabic_jeem + 0x062d: 0x05cd, // XK_Arabic_hah + 0x062e: 0x05ce, // XK_Arabic_khah + 0x062f: 0x05cf, // XK_Arabic_dal + 0x0630: 0x05d0, // XK_Arabic_thal + 0x0631: 0x05d1, // XK_Arabic_ra + 0x0632: 0x05d2, // XK_Arabic_zain + 0x0633: 0x05d3, // XK_Arabic_seen + 0x0634: 0x05d4, // XK_Arabic_sheen + 0x0635: 0x05d5, // XK_Arabic_sad + 0x0636: 0x05d6, // XK_Arabic_dad + 0x0637: 0x05d7, // XK_Arabic_tah + 0x0638: 0x05d8, // XK_Arabic_zah + 0x0639: 0x05d9, // XK_Arabic_ain + 0x063a: 0x05da, // XK_Arabic_ghain + 0x0640: 0x05e0, // XK_Arabic_tatweel + 0x0641: 0x05e1, // XK_Arabic_feh + 0x0642: 0x05e2, // XK_Arabic_qaf + 0x0643: 0x05e3, // XK_Arabic_kaf + 0x0644: 0x05e4, // XK_Arabic_lam + 0x0645: 0x05e5, // XK_Arabic_meem + 0x0646: 0x05e6, // XK_Arabic_noon + 0x0647: 0x05e7, // XK_Arabic_ha + 0x0648: 0x05e8, // XK_Arabic_waw + 0x0649: 0x05e9, // XK_Arabic_alefmaksura + 0x064a: 0x05ea, // XK_Arabic_yeh + 0x064b: 0x05eb, // XK_Arabic_fathatan + 0x064c: 0x05ec, // XK_Arabic_dammatan + 0x064d: 0x05ed, // XK_Arabic_kasratan + 0x064e: 0x05ee, // XK_Arabic_fatha + 0x064f: 0x05ef, // XK_Arabic_damma + 0x0650: 0x05f0, // XK_Arabic_kasra + 0x0651: 0x05f1, // XK_Arabic_shadda + 0x0652: 0x05f2, // XK_Arabic_sukun + 0x0e01: 0x0da1, // XK_Thai_kokai + 0x0e02: 0x0da2, // XK_Thai_khokhai + 0x0e03: 0x0da3, // XK_Thai_khokhuat + 0x0e04: 0x0da4, // XK_Thai_khokhwai + 0x0e05: 0x0da5, // XK_Thai_khokhon + 0x0e06: 0x0da6, // XK_Thai_khorakhang + 0x0e07: 0x0da7, // XK_Thai_ngongu + 0x0e08: 0x0da8, // XK_Thai_chochan + 0x0e09: 0x0da9, // XK_Thai_choching + 0x0e0a: 0x0daa, // XK_Thai_chochang + 0x0e0b: 0x0dab, // XK_Thai_soso + 0x0e0c: 0x0dac, // XK_Thai_chochoe + 0x0e0d: 0x0dad, // XK_Thai_yoying + 0x0e0e: 0x0dae, // XK_Thai_dochada + 0x0e0f: 0x0daf, // XK_Thai_topatak + 0x0e10: 0x0db0, // XK_Thai_thothan + 0x0e11: 0x0db1, // XK_Thai_thonangmontho + 0x0e12: 0x0db2, // XK_Thai_thophuthao + 0x0e13: 0x0db3, // XK_Thai_nonen + 0x0e14: 0x0db4, // XK_Thai_dodek + 0x0e15: 0x0db5, // XK_Thai_totao + 0x0e16: 0x0db6, // XK_Thai_thothung + 0x0e17: 0x0db7, // XK_Thai_thothahan + 0x0e18: 0x0db8, // XK_Thai_thothong + 0x0e19: 0x0db9, // XK_Thai_nonu + 0x0e1a: 0x0dba, // XK_Thai_bobaimai + 0x0e1b: 0x0dbb, // XK_Thai_popla + 0x0e1c: 0x0dbc, // XK_Thai_phophung + 0x0e1d: 0x0dbd, // XK_Thai_fofa + 0x0e1e: 0x0dbe, // XK_Thai_phophan + 0x0e1f: 0x0dbf, // XK_Thai_fofan + 0x0e20: 0x0dc0, // XK_Thai_phosamphao + 0x0e21: 0x0dc1, // XK_Thai_moma + 0x0e22: 0x0dc2, // XK_Thai_yoyak + 0x0e23: 0x0dc3, // XK_Thai_rorua + 0x0e24: 0x0dc4, // XK_Thai_ru + 0x0e25: 0x0dc5, // XK_Thai_loling + 0x0e26: 0x0dc6, // XK_Thai_lu + 0x0e27: 0x0dc7, // XK_Thai_wowaen + 0x0e28: 0x0dc8, // XK_Thai_sosala + 0x0e29: 0x0dc9, // XK_Thai_sorusi + 0x0e2a: 0x0dca, // XK_Thai_sosua + 0x0e2b: 0x0dcb, // XK_Thai_hohip + 0x0e2c: 0x0dcc, // XK_Thai_lochula + 0x0e2d: 0x0dcd, // XK_Thai_oang + 0x0e2e: 0x0dce, // XK_Thai_honokhuk + 0x0e2f: 0x0dcf, // XK_Thai_paiyannoi + 0x0e30: 0x0dd0, // XK_Thai_saraa + 0x0e31: 0x0dd1, // XK_Thai_maihanakat + 0x0e32: 0x0dd2, // XK_Thai_saraaa + 0x0e33: 0x0dd3, // XK_Thai_saraam + 0x0e34: 0x0dd4, // XK_Thai_sarai + 0x0e35: 0x0dd5, // XK_Thai_saraii + 0x0e36: 0x0dd6, // XK_Thai_saraue + 0x0e37: 0x0dd7, // XK_Thai_sarauee + 0x0e38: 0x0dd8, // XK_Thai_sarau + 0x0e39: 0x0dd9, // XK_Thai_sarauu + 0x0e3a: 0x0dda, // XK_Thai_phinthu + 0x0e3f: 0x0ddf, // XK_Thai_baht + 0x0e40: 0x0de0, // XK_Thai_sarae + 0x0e41: 0x0de1, // XK_Thai_saraae + 0x0e42: 0x0de2, // XK_Thai_sarao + 0x0e43: 0x0de3, // XK_Thai_saraaimaimuan + 0x0e44: 0x0de4, // XK_Thai_saraaimaimalai + 0x0e45: 0x0de5, // XK_Thai_lakkhangyao + 0x0e46: 0x0de6, // XK_Thai_maiyamok + 0x0e47: 0x0de7, // XK_Thai_maitaikhu + 0x0e48: 0x0de8, // XK_Thai_maiek + 0x0e49: 0x0de9, // XK_Thai_maitho + 0x0e4a: 0x0dea, // XK_Thai_maitri + 0x0e4b: 0x0deb, // XK_Thai_maichattawa + 0x0e4c: 0x0dec, // XK_Thai_thanthakhat + 0x0e4d: 0x0ded, // XK_Thai_nikhahit + 0x0e50: 0x0df0, // XK_Thai_leksun + 0x0e51: 0x0df1, // XK_Thai_leknung + 0x0e52: 0x0df2, // XK_Thai_leksong + 0x0e53: 0x0df3, // XK_Thai_leksam + 0x0e54: 0x0df4, // XK_Thai_leksi + 0x0e55: 0x0df5, // XK_Thai_lekha + 0x0e56: 0x0df6, // XK_Thai_lekhok + 0x0e57: 0x0df7, // XK_Thai_lekchet + 0x0e58: 0x0df8, // XK_Thai_lekpaet + 0x0e59: 0x0df9, // XK_Thai_lekkao + 0x2002: 0x0aa2, // XK_enspace + 0x2003: 0x0aa1, // XK_emspace + 0x2004: 0x0aa3, // XK_em3space + 0x2005: 0x0aa4, // XK_em4space + 0x2007: 0x0aa5, // XK_digitspace + 0x2008: 0x0aa6, // XK_punctspace + 0x2009: 0x0aa7, // XK_thinspace + 0x200a: 0x0aa8, // XK_hairspace + 0x2012: 0x0abb, // XK_figdash + 0x2013: 0x0aaa, // XK_endash + 0x2014: 0x0aa9, // XK_emdash + 0x2015: 0x07af, // XK_Greek_horizbar + 0x2017: 0x0cdf, // XK_hebrew_doublelowline + 0x2018: 0x0ad0, // XK_leftsinglequotemark + 0x2019: 0x0ad1, // XK_rightsinglequotemark + 0x201a: 0x0afd, // XK_singlelowquotemark + 0x201c: 0x0ad2, // XK_leftdoublequotemark + 0x201d: 0x0ad3, // XK_rightdoublequotemark + 0x201e: 0x0afe, // XK_doublelowquotemark + 0x2020: 0x0af1, // XK_dagger + 0x2021: 0x0af2, // XK_doubledagger + 0x2022: 0x0ae6, // XK_enfilledcircbullet + 0x2025: 0x0aaf, // XK_doubbaselinedot + 0x2026: 0x0aae, // XK_ellipsis + 0x2030: 0x0ad5, // XK_permille + 0x2032: 0x0ad6, // XK_minutes + 0x2033: 0x0ad7, // XK_seconds + 0x2038: 0x0afc, // XK_caret + 0x203e: 0x047e, // XK_overline + 0x20a9: 0x0eff, // XK_Korean_Won + 0x20ac: 0x20ac, // XK_EuroSign + 0x2105: 0x0ab8, // XK_careof + 0x2116: 0x06b0, // XK_numerosign + 0x2117: 0x0afb, // XK_phonographcopyright + 0x211e: 0x0ad4, // XK_prescription + 0x2122: 0x0ac9, // XK_trademark + 0x2153: 0x0ab0, // XK_onethird + 0x2154: 0x0ab1, // XK_twothirds + 0x2155: 0x0ab2, // XK_onefifth + 0x2156: 0x0ab3, // XK_twofifths + 0x2157: 0x0ab4, // XK_threefifths + 0x2158: 0x0ab5, // XK_fourfifths + 0x2159: 0x0ab6, // XK_onesixth + 0x215a: 0x0ab7, // XK_fivesixths + 0x215b: 0x0ac3, // XK_oneeighth + 0x215c: 0x0ac4, // XK_threeeighths + 0x215d: 0x0ac5, // XK_fiveeighths + 0x215e: 0x0ac6, // XK_seveneighths + 0x2190: 0x08fb, // XK_leftarrow + 0x2191: 0x08fc, // XK_uparrow + 0x2192: 0x08fd, // XK_rightarrow + 0x2193: 0x08fe, // XK_downarrow + 0x21d2: 0x08ce, // XK_implies + 0x21d4: 0x08cd, // XK_ifonlyif + 0x2202: 0x08ef, // XK_partialderivative + 0x2207: 0x08c5, // XK_nabla + 0x2218: 0x0bca, // XK_jot + 0x221a: 0x08d6, // XK_radical + 0x221d: 0x08c1, // XK_variation + 0x221e: 0x08c2, // XK_infinity + 0x2227: 0x08de, // XK_logicaland + 0x2228: 0x08df, // XK_logicalor + 0x2229: 0x08dc, // XK_intersection + 0x222a: 0x08dd, // XK_union + 0x222b: 0x08bf, // XK_integral + 0x2234: 0x08c0, // XK_therefore + 0x223c: 0x08c8, // XK_approximate + 0x2243: 0x08c9, // XK_similarequal + 0x2245: 0x1002248, // XK_approxeq + 0x2260: 0x08bd, // XK_notequal + 0x2261: 0x08cf, // XK_identical + 0x2264: 0x08bc, // XK_lessthanequal + 0x2265: 0x08be, // XK_greaterthanequal + 0x2282: 0x08da, // XK_includedin + 0x2283: 0x08db, // XK_includes + 0x22a2: 0x0bfc, // XK_righttack + 0x22a3: 0x0bdc, // XK_lefttack + 0x22a4: 0x0bc2, // XK_downtack + 0x22a5: 0x0bce, // XK_uptack + 0x2308: 0x0bd3, // XK_upstile + 0x230a: 0x0bc4, // XK_downstile + 0x2315: 0x0afa, // XK_telephonerecorder + 0x2320: 0x08a4, // XK_topintegral + 0x2321: 0x08a5, // XK_botintegral + 0x2395: 0x0bcc, // XK_quad + 0x239b: 0x08ab, // XK_topleftparens + 0x239d: 0x08ac, // XK_botleftparens + 0x239e: 0x08ad, // XK_toprightparens + 0x23a0: 0x08ae, // XK_botrightparens + 0x23a1: 0x08a7, // XK_topleftsqbracket + 0x23a3: 0x08a8, // XK_botleftsqbracket + 0x23a4: 0x08a9, // XK_toprightsqbracket + 0x23a6: 0x08aa, // XK_botrightsqbracket + 0x23a8: 0x08af, // XK_leftmiddlecurlybrace + 0x23ac: 0x08b0, // XK_rightmiddlecurlybrace + 0x23b7: 0x08a1, // XK_leftradical + 0x23ba: 0x09ef, // XK_horizlinescan1 + 0x23bb: 0x09f0, // XK_horizlinescan3 + 0x23bc: 0x09f2, // XK_horizlinescan7 + 0x23bd: 0x09f3, // XK_horizlinescan9 + 0x2409: 0x09e2, // XK_ht + 0x240a: 0x09e5, // XK_lf + 0x240b: 0x09e9, // XK_vt + 0x240c: 0x09e3, // XK_ff + 0x240d: 0x09e4, // XK_cr + 0x2423: 0x0aac, // XK_signifblank + 0x2424: 0x09e8, // XK_nl + 0x2500: 0x08a3, // XK_horizconnector + 0x2502: 0x08a6, // XK_vertconnector + 0x250c: 0x08a2, // XK_topleftradical + 0x2510: 0x09eb, // XK_uprightcorner + 0x2514: 0x09ed, // XK_lowleftcorner + 0x2518: 0x09ea, // XK_lowrightcorner + 0x251c: 0x09f4, // XK_leftt + 0x2524: 0x09f5, // XK_rightt + 0x252c: 0x09f7, // XK_topt + 0x2534: 0x09f6, // XK_bott + 0x253c: 0x09ee, // XK_crossinglines + 0x2592: 0x09e1, // XK_checkerboard + 0x25aa: 0x0ae7, // XK_enfilledsqbullet + 0x25ab: 0x0ae1, // XK_enopensquarebullet + 0x25ac: 0x0adb, // XK_filledrectbullet + 0x25ad: 0x0ae2, // XK_openrectbullet + 0x25ae: 0x0adf, // XK_emfilledrect + 0x25af: 0x0acf, // XK_emopenrectangle + 0x25b2: 0x0ae8, // XK_filledtribulletup + 0x25b3: 0x0ae3, // XK_opentribulletup + 0x25b6: 0x0add, // XK_filledrighttribullet + 0x25b7: 0x0acd, // XK_rightopentriangle + 0x25bc: 0x0ae9, // XK_filledtribulletdown + 0x25bd: 0x0ae4, // XK_opentribulletdown + 0x25c0: 0x0adc, // XK_filledlefttribullet + 0x25c1: 0x0acc, // XK_leftopentriangle + 0x25c6: 0x09e0, // XK_soliddiamond + 0x25cb: 0x0ace, // XK_emopencircle + 0x25cf: 0x0ade, // XK_emfilledcircle + 0x25e6: 0x0ae0, // XK_enopencircbullet + 0x2606: 0x0ae5, // XK_openstar + 0x260e: 0x0af9, // XK_telephone + 0x2613: 0x0aca, // XK_signaturemark + 0x261c: 0x0aea, // XK_leftpointer + 0x261e: 0x0aeb, // XK_rightpointer + 0x2640: 0x0af8, // XK_femalesymbol + 0x2642: 0x0af7, // XK_malesymbol + 0x2663: 0x0aec, // XK_club + 0x2665: 0x0aee, // XK_heart + 0x2666: 0x0aed, // XK_diamond + 0x266d: 0x0af6, // XK_musicalflat + 0x266f: 0x0af5, // XK_musicalsharp + 0x2713: 0x0af3, // XK_checkmark + 0x2717: 0x0af4, // XK_ballotcross + 0x271d: 0x0ad9, // XK_latincross + 0x2720: 0x0af0, // XK_maltesecross + 0x27e8: 0x0abc, // XK_leftanglebracket + 0x27e9: 0x0abe, // XK_rightanglebracket + 0x3001: 0x04a4, // XK_kana_comma + 0x3002: 0x04a1, // XK_kana_fullstop + 0x300c: 0x04a2, // XK_kana_openingbracket + 0x300d: 0x04a3, // XK_kana_closingbracket + 0x309b: 0x04de, // XK_voicedsound + 0x309c: 0x04df, // XK_semivoicedsound + 0x30a1: 0x04a7, // XK_kana_a + 0x30a2: 0x04b1, // XK_kana_A + 0x30a3: 0x04a8, // XK_kana_i + 0x30a4: 0x04b2, // XK_kana_I + 0x30a5: 0x04a9, // XK_kana_u + 0x30a6: 0x04b3, // XK_kana_U + 0x30a7: 0x04aa, // XK_kana_e + 0x30a8: 0x04b4, // XK_kana_E + 0x30a9: 0x04ab, // XK_kana_o + 0x30aa: 0x04b5, // XK_kana_O + 0x30ab: 0x04b6, // XK_kana_KA + 0x30ad: 0x04b7, // XK_kana_KI + 0x30af: 0x04b8, // XK_kana_KU + 0x30b1: 0x04b9, // XK_kana_KE + 0x30b3: 0x04ba, // XK_kana_KO + 0x30b5: 0x04bb, // XK_kana_SA + 0x30b7: 0x04bc, // XK_kana_SHI + 0x30b9: 0x04bd, // XK_kana_SU + 0x30bb: 0x04be, // XK_kana_SE + 0x30bd: 0x04bf, // XK_kana_SO + 0x30bf: 0x04c0, // XK_kana_TA + 0x30c1: 0x04c1, // XK_kana_CHI + 0x30c3: 0x04af, // XK_kana_tsu + 0x30c4: 0x04c2, // XK_kana_TSU + 0x30c6: 0x04c3, // XK_kana_TE + 0x30c8: 0x04c4, // XK_kana_TO + 0x30ca: 0x04c5, // XK_kana_NA + 0x30cb: 0x04c6, // XK_kana_NI + 0x30cc: 0x04c7, // XK_kana_NU + 0x30cd: 0x04c8, // XK_kana_NE + 0x30ce: 0x04c9, // XK_kana_NO + 0x30cf: 0x04ca, // XK_kana_HA + 0x30d2: 0x04cb, // XK_kana_HI + 0x30d5: 0x04cc, // XK_kana_FU + 0x30d8: 0x04cd, // XK_kana_HE + 0x30db: 0x04ce, // XK_kana_HO + 0x30de: 0x04cf, // XK_kana_MA + 0x30df: 0x04d0, // XK_kana_MI + 0x30e0: 0x04d1, // XK_kana_MU + 0x30e1: 0x04d2, // XK_kana_ME + 0x30e2: 0x04d3, // XK_kana_MO + 0x30e3: 0x04ac, // XK_kana_ya + 0x30e4: 0x04d4, // XK_kana_YA + 0x30e5: 0x04ad, // XK_kana_yu + 0x30e6: 0x04d5, // XK_kana_YU + 0x30e7: 0x04ae, // XK_kana_yo + 0x30e8: 0x04d6, // XK_kana_YO + 0x30e9: 0x04d7, // XK_kana_RA + 0x30ea: 0x04d8, // XK_kana_RI + 0x30eb: 0x04d9, // XK_kana_RU + 0x30ec: 0x04da, // XK_kana_RE + 0x30ed: 0x04db, // XK_kana_RO + 0x30ef: 0x04dc, // XK_kana_WA + 0x30f2: 0x04a6, // XK_kana_WO + 0x30f3: 0x04dd, // XK_kana_N + 0x30fb: 0x04a5, // XK_kana_conjunctive + 0x30fc: 0x04b0, // XK_prolongedsound }; export default { - lookup(u) { - // Latin-1 is one-to-one mapping - if ((u >= 0x20) && (u <= 0xff)) { - return u; - } + lookup(u) { + // Latin-1 is one-to-one mapping + if ((u >= 0x20) && (u <= 0xff)) { + return u; + } - // Lookup table (fairly random) - const keysym = codepoints[u]; - if (keysym !== undefined) { - return keysym; - } + // Lookup table (fairly random) + const keysym = codepoints[u]; + if (keysym !== undefined) { + return keysym; + } - // General mapping as final fallback - return 0x01000000 | u; - }, + // General mapping as final fallback + return 0x01000000 | u; + }, }; diff --git a/core/input/mouse.js b/core/input/mouse.js index 4a8710ef..0cb6772b 100644 --- a/core/input/mouse.js +++ b/core/input/mouse.js @@ -14,268 +14,264 @@ const WHEEL_STEP_TIMEOUT = 50; // ms const WHEEL_LINE_HEIGHT = 19; export default class Mouse { - constructor(target) { - this._target = target || document; + constructor(target) { + this._target = target || document; - this._doubleClickTimer = null; - this._lastTouchPos = null; + this._doubleClickTimer = null; + this._lastTouchPos = null; - this._pos = null; - this._wheelStepXTimer = null; - this._wheelStepYTimer = null; - this._accumulatedWheelDeltaX = 0; - this._accumulatedWheelDeltaY = 0; + this._pos = null; + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + this._accumulatedWheelDeltaX = 0; + this._accumulatedWheelDeltaY = 0; - this._eventHandlers = { - 'mousedown': this._handleMouseDown.bind(this), - 'mouseup': this._handleMouseUp.bind(this), - 'mousemove': this._handleMouseMove.bind(this), - 'mousewheel': this._handleMouseWheel.bind(this), - 'mousedisable': this._handleMouseDisable.bind(this) - }; + this._eventHandlers = { + mousedown: this._handleMouseDown.bind(this), + mouseup: this._handleMouseUp.bind(this), + mousemove: this._handleMouseMove.bind(this), + mousewheel: this._handleMouseWheel.bind(this), + mousedisable: this._handleMouseDisable.bind(this) + }; - // ===== PROPERTIES ===== + // ===== PROPERTIES ===== - this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) - // ===== EVENT HANDLERS ===== + // ===== EVENT HANDLERS ===== - this.onmousebutton = () => {}; // Handler for mouse button click/release - this.onmousemove = () => {}; // Handler for mouse movement - } + this.onmousebutton = () => {}; // Handler for mouse button click/release + this.onmousemove = () => {}; // Handler for mouse movement + } - // ===== PRIVATE METHODS ===== + // ===== PRIVATE METHODS ===== - _resetDoubleClickTimer() { - this._doubleClickTimer = null; - } + _resetDoubleClickTimer() { + this._doubleClickTimer = null; + } - _handleMouseButton(e, down) { - this._updateMousePosition(e); - let pos = this._pos; + _handleMouseButton(e, down) { + this._updateMousePosition(e); + let pos = this._pos; - let bmask; - if (e.touches || e.changedTouches) { - // Touch device + let bmask; + if (e.touches || e.changedTouches) { + // Touch device - // When two touches occur within 500 ms of each other and are - // close enough together a double click is triggered. - if (down == 1) { - if (this._doubleClickTimer === null) { - this._lastTouchPos = pos; - } else { - clearTimeout(this._doubleClickTimer); - - // When the distance between the two touches is small enough - // force the position of the latter touch to the position of - // the first. - - const xs = this._lastTouchPos.x - pos.x; - const ys = this._lastTouchPos.y - pos.y; - const d = Math.sqrt((xs * xs) + (ys * ys)); - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - const threshold = 20 * (window.devicePixelRatio || 1); - if (d < threshold) { - pos = this._lastTouchPos; - } - } - this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); - } - bmask = this.touchButton; - // If bmask is set - } else if (e.which) { - /* everything except IE */ - bmask = 1 << e.button; + // When two touches occur within 500 ms of each other and are + // close enough together a double click is triggered. + if (down == 1) { + if (this._doubleClickTimer === null) { + this._lastTouchPos = pos; } else { - /* IE including 9 */ - bmask = (e.button & 0x1) + // Left - (e.button & 0x2) * 2 + // Right - (e.button & 0x4) / 2; // Middle + clearTimeout(this._doubleClickTimer); + + // When the distance between the two touches is small enough + // force the position of the latter touch to the position of + // the first. + + const xs = this._lastTouchPos.x - pos.x; + const ys = this._lastTouchPos.y - pos.y; + const d = Math.sqrt((xs * xs) + (ys * ys)); + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + const threshold = 20 * (window.devicePixelRatio || 1); + if (d < threshold) { + pos = this._lastTouchPos; + } } - - Log.Debug("onmousebutton " + (down ? "down" : "up") + - ", x: " + pos.x + ", y: " + pos.y + ", bmask: " + bmask); - this.onmousebutton(pos.x, pos.y, down, bmask); - - stopEvent(e); + this._doubleClickTimer = setTimeout(this._resetDoubleClickTimer.bind(this), 500); + } + bmask = this.touchButton; + // If bmask is set + } else if (e.which) { + /* everything except IE */ + bmask = 1 << e.button; + } else { + /* IE including 9 */ + bmask = (e.button & 0x1) // Left + + (e.button & 0x2) * 2 // Right + + (e.button & 0x4) / 2; // Middle } - _handleMouseDown(e) { - // Touch events have implicit capture - if (e.type === "mousedown") { - setCapture(this._target); - } + Log.Debug('onmousebutton ' + (down ? 'down' : 'up') + + ', x: ' + pos.x + ', y: ' + pos.y + ', bmask: ' + bmask); + this.onmousebutton(pos.x, pos.y, down, bmask); - this._handleMouseButton(e, 1); + stopEvent(e); + } + + _handleMouseDown(e) { + // Touch events have implicit capture + if (e.type === 'mousedown') { + setCapture(this._target); } - _handleMouseUp(e) { - this._handleMouseButton(e, 0); + this._handleMouseButton(e, 1); + } + + _handleMouseUp(e) { + this._handleMouseButton(e, 0); + } + + // Mouse wheel events are sent in steps over VNC. This means that the VNC + // protocol can't handle a wheel event with specific distance or speed. + // Therefor, if we get a lot of small mouse wheel events we combine them. + _generateWheelStepX() { + if (this._accumulatedWheelDeltaX < 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5); + } else if (this._accumulatedWheelDeltaX > 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6); } - // Mouse wheel events are sent in steps over VNC. This means that the VNC - // protocol can't handle a wheel event with specific distance or speed. - // Therefor, if we get a lot of small mouse wheel events we combine them. - _generateWheelStepX() { + this._accumulatedWheelDeltaX = 0; + } - if (this._accumulatedWheelDeltaX < 0) { - this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 5); - this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 5); - } else if (this._accumulatedWheelDeltaX > 0) { - this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 6); - this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 6); - } - - this._accumulatedWheelDeltaX = 0; + _generateWheelStepY() { + if (this._accumulatedWheelDeltaY < 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3); + } else if (this._accumulatedWheelDeltaY > 0) { + this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4); + this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4); } - _generateWheelStepY() { + this._accumulatedWheelDeltaY = 0; + } - if (this._accumulatedWheelDeltaY < 0) { - this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 3); - this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 3); - } else if (this._accumulatedWheelDeltaY > 0) { - this.onmousebutton(this._pos.x, this._pos.y, 1, 1 << 4); - this.onmousebutton(this._pos.x, this._pos.y, 0, 1 << 4); - } + _resetWheelStepTimers() { + window.clearTimeout(this._wheelStepXTimer); + window.clearTimeout(this._wheelStepYTimer); + this._wheelStepXTimer = null; + this._wheelStepYTimer = null; + } - this._accumulatedWheelDeltaY = 0; + _handleMouseWheel(e) { + this._resetWheelStepTimers(); + + this._updateMousePosition(e); + + let dX = e.deltaX; + let dY = e.deltaY; + + // Pixel units unless it's non-zero. + // Note that if deltamode is line or page won't matter since we aren't + // sending the mouse wheel delta to the server anyway. + // The difference between pixel and line can be important however since + // we have a threshold that can be smaller than the line height. + if (e.deltaMode !== 0) { + dX *= WHEEL_LINE_HEIGHT; + dY *= WHEEL_LINE_HEIGHT; } - _resetWheelStepTimers() { - window.clearTimeout(this._wheelStepXTimer); - window.clearTimeout(this._wheelStepYTimer); - this._wheelStepXTimer = null; - this._wheelStepYTimer = null; + this._accumulatedWheelDeltaX += dX; + this._accumulatedWheelDeltaY += dY; + + // Generate a mouse wheel step event when the accumulated delta + // for one of the axes is large enough. + // Small delta events that do not pass the threshold get sent + // after a timeout. + if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { + this._generateWheelStepX(); + } else { + this._wheelStepXTimer = window.setTimeout(this._generateWheelStepX.bind(this), + WHEEL_STEP_TIMEOUT); + } + if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { + this._generateWheelStepY(); + } else { + this._wheelStepYTimer = window.setTimeout(this._generateWheelStepY.bind(this), + WHEEL_STEP_TIMEOUT); } - _handleMouseWheel(e) { - this._resetWheelStepTimers(); + stopEvent(e); + } - this._updateMousePosition(e); + _handleMouseMove(e) { + this._updateMousePosition(e); + this.onmousemove(this._pos.x, this._pos.y); + stopEvent(e); + } - let dX = e.deltaX; - let dY = e.deltaY; - - // Pixel units unless it's non-zero. - // Note that if deltamode is line or page won't matter since we aren't - // sending the mouse wheel delta to the server anyway. - // The difference between pixel and line can be important however since - // we have a threshold that can be smaller than the line height. - if (e.deltaMode !== 0) { - dX *= WHEEL_LINE_HEIGHT; - dY *= WHEEL_LINE_HEIGHT; - } - - this._accumulatedWheelDeltaX += dX; - this._accumulatedWheelDeltaY += dY; - - // Generate a mouse wheel step event when the accumulated delta - // for one of the axes is large enough. - // Small delta events that do not pass the threshold get sent - // after a timeout. - if (Math.abs(this._accumulatedWheelDeltaX) > WHEEL_STEP) { - this._generateWheelStepX(); - } else { - this._wheelStepXTimer = - window.setTimeout(this._generateWheelStepX.bind(this), - WHEEL_STEP_TIMEOUT); - } - if (Math.abs(this._accumulatedWheelDeltaY) > WHEEL_STEP) { - this._generateWheelStepY(); - } else { - this._wheelStepYTimer = - window.setTimeout(this._generateWheelStepY.bind(this), - WHEEL_STEP_TIMEOUT); - } - - stopEvent(e); - } - - _handleMouseMove(e) { - this._updateMousePosition(e); - this.onmousemove(this._pos.x, this._pos.y); - stopEvent(e); - } - - _handleMouseDisable(e) { - /* + _handleMouseDisable(e) { + /* * Stop propagation if inside canvas area * Note: This is only needed for the 'click' event as it fails * to fire properly for the target element so we have * to listen on the document element instead. */ - if (e.target == this._target) { - stopEvent(e); - } + if (e.target == this._target) { + stopEvent(e); } + } - // Update coordinates relative to target - _updateMousePosition(e) { - e = getPointerEvent(e); - const bounds = this._target.getBoundingClientRect(); - let x; - let y; - // Clip to target bounds - if (e.clientX < bounds.left) { - x = 0; - } else if (e.clientX >= bounds.right) { - x = bounds.width - 1; - } else { - x = e.clientX - bounds.left; - } - if (e.clientY < bounds.top) { - y = 0; - } else if (e.clientY >= bounds.bottom) { - y = bounds.height - 1; - } else { - y = e.clientY - bounds.top; - } - this._pos = {x:x, y:y}; + // Update coordinates relative to target + _updateMousePosition(e) { + e = getPointerEvent(e); + const bounds = this._target.getBoundingClientRect(); + let x; + let y; + // Clip to target bounds + if (e.clientX < bounds.left) { + x = 0; + } else if (e.clientX >= bounds.right) { + x = bounds.width - 1; + } else { + x = e.clientX - bounds.left; } + if (e.clientY < bounds.top) { + y = 0; + } else if (e.clientY >= bounds.bottom) { + y = bounds.height - 1; + } else { + y = e.clientY - bounds.top; + } + this._pos = { x: x, y: y }; + } - // ===== PUBLIC METHODS ===== + // ===== PUBLIC METHODS ===== - grab() { - const c = this._target; + grab() { + const c = this._target; - if (isTouchDevice) { - c.addEventListener('touchstart', this._eventHandlers.mousedown); - c.addEventListener('touchend', this._eventHandlers.mouseup); - c.addEventListener('touchmove', this._eventHandlers.mousemove); - } - c.addEventListener('mousedown', this._eventHandlers.mousedown); - c.addEventListener('mouseup', this._eventHandlers.mouseup); - c.addEventListener('mousemove', this._eventHandlers.mousemove); - c.addEventListener('wheel', this._eventHandlers.mousewheel); + if (isTouchDevice) { + c.addEventListener('touchstart', this._eventHandlers.mousedown); + c.addEventListener('touchend', this._eventHandlers.mouseup); + c.addEventListener('touchmove', this._eventHandlers.mousemove); + } + c.addEventListener('mousedown', this._eventHandlers.mousedown); + c.addEventListener('mouseup', this._eventHandlers.mouseup); + c.addEventListener('mousemove', this._eventHandlers.mousemove); + c.addEventListener('wheel', this._eventHandlers.mousewheel); - /* Prevent middle-click pasting (see above for why we bind to document) */ - document.addEventListener('click', this._eventHandlers.mousedisable); + /* Prevent middle-click pasting (see above for why we bind to document) */ + document.addEventListener('click', this._eventHandlers.mousedisable); - /* preventDefault() on mousedown doesn't stop this event for some + /* preventDefault() on mousedown doesn't stop this event for some reason so we have to explicitly block it */ - c.addEventListener('contextmenu', this._eventHandlers.mousedisable); + c.addEventListener('contextmenu', this._eventHandlers.mousedisable); + } + + ungrab() { + const c = this._target; + + this._resetWheelStepTimers(); + + if (isTouchDevice) { + c.removeEventListener('touchstart', this._eventHandlers.mousedown); + c.removeEventListener('touchend', this._eventHandlers.mouseup); + c.removeEventListener('touchmove', this._eventHandlers.mousemove); } + c.removeEventListener('mousedown', this._eventHandlers.mousedown); + c.removeEventListener('mouseup', this._eventHandlers.mouseup); + c.removeEventListener('mousemove', this._eventHandlers.mousemove); + c.removeEventListener('wheel', this._eventHandlers.mousewheel); - ungrab() { - const c = this._target; + document.removeEventListener('click', this._eventHandlers.mousedisable); - this._resetWheelStepTimers(); - - if (isTouchDevice) { - c.removeEventListener('touchstart', this._eventHandlers.mousedown); - c.removeEventListener('touchend', this._eventHandlers.mouseup); - c.removeEventListener('touchmove', this._eventHandlers.mousemove); - } - c.removeEventListener('mousedown', this._eventHandlers.mousedown); - c.removeEventListener('mouseup', this._eventHandlers.mouseup); - c.removeEventListener('mousemove', this._eventHandlers.mousemove); - c.removeEventListener('wheel', this._eventHandlers.mousewheel); - - document.removeEventListener('click', this._eventHandlers.mousedisable); - - c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); - } + c.removeEventListener('contextmenu', this._eventHandlers.mousedisable); + } } diff --git a/core/input/util.js b/core/input/util.js index 57e9ce45..4bac90c0 100644 --- a/core/input/util.js +++ b/core/input/util.js @@ -1,164 +1,164 @@ -import keysyms from "./keysymdef.js"; -import vkeys from "./vkeys.js"; -import fixedkeys from "./fixedkeys.js"; -import DOMKeyTable from "./domkeytable.js"; -import * as browser from "../util/browser.js"; +import keysyms from './keysymdef.js'; +import vkeys from './vkeys.js'; +import fixedkeys from './fixedkeys.js'; +import DOMKeyTable from './domkeytable.js'; +import * as browser from '../util/browser.js'; // Get 'KeyboardEvent.code', handling legacy browsers -export function getKeycode(evt){ - // Are we getting proper key identifiers? - // (unfortunately Firefox and Chrome are crappy here and gives - // us an empty string on some platforms, rather than leaving it - // undefined) - if (evt.code) { - // Mozilla isn't fully in sync with the spec yet - switch (evt.code) { - case 'OSLeft': return 'MetaLeft'; - case 'OSRight': return 'MetaRight'; - } - - return evt.code; +export function getKeycode(evt) { + // Are we getting proper key identifiers? + // (unfortunately Firefox and Chrome are crappy here and gives + // us an empty string on some platforms, rather than leaving it + // undefined) + if (evt.code) { + // Mozilla isn't fully in sync with the spec yet + switch (evt.code) { + case 'OSLeft': return 'MetaLeft'; + case 'OSRight': return 'MetaRight'; } - // The de-facto standard is to use Windows Virtual-Key codes - // in the 'keyCode' field for non-printable characters. However - // Webkit sets it to the same as charCode in 'keypress' events. - if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) { - let code = vkeys[evt.keyCode]; + return evt.code; + } - // macOS has messed up this code for some reason - if (browser.isMac() && (code === 'ContextMenu')) { - code = 'MetaRight'; - } + // The de-facto standard is to use Windows Virtual-Key codes + // in the 'keyCode' field for non-printable characters. However + // Webkit sets it to the same as charCode in 'keypress' events. + if ((evt.type !== 'keypress') && (evt.keyCode in vkeys)) { + let code = vkeys[evt.keyCode]; - // The keyCode doesn't distinguish between left and right - // for the standard modifiers - if (evt.location === 2) { - switch (code) { - case 'ShiftLeft': return 'ShiftRight'; - case 'ControlLeft': return 'ControlRight'; - case 'AltLeft': return 'AltRight'; - } - } - - // Nor a bunch of the numpad keys - if (evt.location === 3) { - switch (code) { - case 'Delete': return 'NumpadDecimal'; - case 'Insert': return 'Numpad0'; - case 'End': return 'Numpad1'; - case 'ArrowDown': return 'Numpad2'; - case 'PageDown': return 'Numpad3'; - case 'ArrowLeft': return 'Numpad4'; - case 'ArrowRight': return 'Numpad6'; - case 'Home': return 'Numpad7'; - case 'ArrowUp': return 'Numpad8'; - case 'PageUp': return 'Numpad9'; - case 'Enter': return 'NumpadEnter'; - } - } - - return code; + // macOS has messed up this code for some reason + if (browser.isMac() && (code === 'ContextMenu')) { + code = 'MetaRight'; } - return 'Unidentified'; + // The keyCode doesn't distinguish between left and right + // for the standard modifiers + if (evt.location === 2) { + switch (code) { + case 'ShiftLeft': return 'ShiftRight'; + case 'ControlLeft': return 'ControlRight'; + case 'AltLeft': return 'AltRight'; + } + } + + // Nor a bunch of the numpad keys + if (evt.location === 3) { + switch (code) { + case 'Delete': return 'NumpadDecimal'; + case 'Insert': return 'Numpad0'; + case 'End': return 'Numpad1'; + case 'ArrowDown': return 'Numpad2'; + case 'PageDown': return 'Numpad3'; + case 'ArrowLeft': return 'Numpad4'; + case 'ArrowRight': return 'Numpad6'; + case 'Home': return 'Numpad7'; + case 'ArrowUp': return 'Numpad8'; + case 'PageUp': return 'Numpad9'; + case 'Enter': return 'NumpadEnter'; + } + } + + return code; + } + + return 'Unidentified'; } // Get 'KeyboardEvent.key', handling legacy browsers export function getKey(evt) { - // Are we getting a proper key value? - if (evt.key !== undefined) { - // IE and Edge use some ancient version of the spec - // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/ - switch (evt.key) { - case 'Spacebar': return ' '; - case 'Esc': return 'Escape'; - case 'Scroll': return 'ScrollLock'; - case 'Win': return 'Meta'; - case 'Apps': return 'ContextMenu'; - case 'Up': return 'ArrowUp'; - case 'Left': return 'ArrowLeft'; - case 'Right': return 'ArrowRight'; - case 'Down': return 'ArrowDown'; - case 'Del': return 'Delete'; - case 'Divide': return '/'; - case 'Multiply': return '*'; - case 'Subtract': return '-'; - case 'Add': return '+'; - case 'Decimal': return evt.char; - } - - // Mozilla isn't fully in sync with the spec yet - switch (evt.key) { - case 'OS': return 'Meta'; - } - - // iOS leaks some OS names - switch (evt.key) { - case 'UIKeyInputUpArrow': return 'ArrowUp'; - case 'UIKeyInputDownArrow': return 'ArrowDown'; - case 'UIKeyInputLeftArrow': return 'ArrowLeft'; - case 'UIKeyInputRightArrow': return 'ArrowRight'; - case 'UIKeyInputEscape': return 'Escape'; - } - - // IE and Edge have broken handling of AltGraph so we cannot - // trust them for printable characters - if ((evt.key.length !== 1) || (!browser.isIE() && !browser.isEdge())) { - return evt.key; - } + // Are we getting a proper key value? + if (evt.key !== undefined) { + // IE and Edge use some ancient version of the spec + // https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/8860571/ + switch (evt.key) { + case 'Spacebar': return ' '; + case 'Esc': return 'Escape'; + case 'Scroll': return 'ScrollLock'; + case 'Win': return 'Meta'; + case 'Apps': return 'ContextMenu'; + case 'Up': return 'ArrowUp'; + case 'Left': return 'ArrowLeft'; + case 'Right': return 'ArrowRight'; + case 'Down': return 'ArrowDown'; + case 'Del': return 'Delete'; + case 'Divide': return '/'; + case 'Multiply': return '*'; + case 'Subtract': return '-'; + case 'Add': return '+'; + case 'Decimal': return evt.char; } - // Try to deduce it based on the physical key - const code = getKeycode(evt); - if (code in fixedkeys) { - return fixedkeys[code]; + // Mozilla isn't fully in sync with the spec yet + switch (evt.key) { + case 'OS': return 'Meta'; } - // If that failed, then see if we have a printable character - if (evt.charCode) { - return String.fromCharCode(evt.charCode); + // iOS leaks some OS names + switch (evt.key) { + case 'UIKeyInputUpArrow': return 'ArrowUp'; + case 'UIKeyInputDownArrow': return 'ArrowDown'; + case 'UIKeyInputLeftArrow': return 'ArrowLeft'; + case 'UIKeyInputRightArrow': return 'ArrowRight'; + case 'UIKeyInputEscape': return 'Escape'; } - // At this point we have nothing left to go on - return 'Unidentified'; + // IE and Edge have broken handling of AltGraph so we cannot + // trust them for printable characters + if ((evt.key.length !== 1) || (!browser.isIE() && !browser.isEdge())) { + return evt.key; + } + } + + // Try to deduce it based on the physical key + const code = getKeycode(evt); + if (code in fixedkeys) { + return fixedkeys[code]; + } + + // If that failed, then see if we have a printable character + if (evt.charCode) { + return String.fromCharCode(evt.charCode); + } + + // At this point we have nothing left to go on + return 'Unidentified'; } // Get the most reliable keysym value we can get from a key event -export function getKeysym(evt){ - const key = getKey(evt); - - if (key === 'Unidentified') { - return null; - } - - // First look up special keys - if (key in DOMKeyTable) { - let location = evt.location; - - // Safari screws up location for the right cmd key - if ((key === 'Meta') && (location === 0)) { - location = 2; - } - - if ((location === undefined) || (location > 3)) { - location = 0; - } - - return DOMKeyTable[key][location]; - } - - // Now we need to look at the Unicode symbol instead - - // Special key? (FIXME: Should have been caught earlier) - if (key.length !== 1) { - return null; - } - - const codepoint = key.charCodeAt(); - if (codepoint) { - return keysyms.lookup(codepoint); - } +export function getKeysym(evt) { + const key = getKey(evt); + if (key === 'Unidentified') { return null; + } + + // First look up special keys + if (key in DOMKeyTable) { + let location = evt.location; + + // Safari screws up location for the right cmd key + if ((key === 'Meta') && (location === 0)) { + location = 2; + } + + if ((location === undefined) || (location > 3)) { + location = 0; + } + + return DOMKeyTable[key][location]; + } + + // Now we need to look at the Unicode symbol instead + + // Special key? (FIXME: Should have been caught earlier) + if (key.length !== 1) { + return null; + } + + const codepoint = key.charCodeAt(); + if (codepoint) { + return keysyms.lookup(codepoint); + } + + return null; } diff --git a/core/input/vkeys.js b/core/input/vkeys.js index 66bf32f0..24f13e73 100644 --- a/core/input/vkeys.js +++ b/core/input/vkeys.js @@ -10,108 +10,108 @@ */ export default { - 0x08: 'Backspace', - 0x09: 'Tab', - 0x0a: 'NumpadClear', - 0x0c: 'Numpad5', // IE11 sends evt.keyCode: 12 when numlock is off - 0x0d: 'Enter', - 0x10: 'ShiftLeft', - 0x11: 'ControlLeft', - 0x12: 'AltLeft', - 0x13: 'Pause', - 0x14: 'CapsLock', - 0x15: 'Lang1', - 0x19: 'Lang2', - 0x1b: 'Escape', - 0x1c: 'Convert', - 0x1d: 'NonConvert', - 0x20: 'Space', - 0x21: 'PageUp', - 0x22: 'PageDown', - 0x23: 'End', - 0x24: 'Home', - 0x25: 'ArrowLeft', - 0x26: 'ArrowUp', - 0x27: 'ArrowRight', - 0x28: 'ArrowDown', - 0x29: 'Select', - 0x2c: 'PrintScreen', - 0x2d: 'Insert', - 0x2e: 'Delete', - 0x2f: 'Help', - 0x30: 'Digit0', - 0x31: 'Digit1', - 0x32: 'Digit2', - 0x33: 'Digit3', - 0x34: 'Digit4', - 0x35: 'Digit5', - 0x36: 'Digit6', - 0x37: 'Digit7', - 0x38: 'Digit8', - 0x39: 'Digit9', - 0x5b: 'MetaLeft', - 0x5c: 'MetaRight', - 0x5d: 'ContextMenu', - 0x5f: 'Sleep', - 0x60: 'Numpad0', - 0x61: 'Numpad1', - 0x62: 'Numpad2', - 0x63: 'Numpad3', - 0x64: 'Numpad4', - 0x65: 'Numpad5', - 0x66: 'Numpad6', - 0x67: 'Numpad7', - 0x68: 'Numpad8', - 0x69: 'Numpad9', - 0x6a: 'NumpadMultiply', - 0x6b: 'NumpadAdd', - 0x6c: 'NumpadDecimal', - 0x6d: 'NumpadSubtract', - 0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows - 0x6f: 'NumpadDivide', - 0x70: 'F1', - 0x71: 'F2', - 0x72: 'F3', - 0x73: 'F4', - 0x74: 'F5', - 0x75: 'F6', - 0x76: 'F7', - 0x77: 'F8', - 0x78: 'F9', - 0x79: 'F10', - 0x7a: 'F11', - 0x7b: 'F12', - 0x7c: 'F13', - 0x7d: 'F14', - 0x7e: 'F15', - 0x7f: 'F16', - 0x80: 'F17', - 0x81: 'F18', - 0x82: 'F19', - 0x83: 'F20', - 0x84: 'F21', - 0x85: 'F22', - 0x86: 'F23', - 0x87: 'F24', - 0x90: 'NumLock', - 0x91: 'ScrollLock', - 0xa6: 'BrowserBack', - 0xa7: 'BrowserForward', - 0xa8: 'BrowserRefresh', - 0xa9: 'BrowserStop', - 0xaa: 'BrowserSearch', - 0xab: 'BrowserFavorites', - 0xac: 'BrowserHome', - 0xad: 'AudioVolumeMute', - 0xae: 'AudioVolumeDown', - 0xaf: 'AudioVolumeUp', - 0xb0: 'MediaTrackNext', - 0xb1: 'MediaTrackPrevious', - 0xb2: 'MediaStop', - 0xb3: 'MediaPlayPause', - 0xb4: 'LaunchMail', - 0xb5: 'MediaSelect', - 0xb6: 'LaunchApp1', - 0xb7: 'LaunchApp2', - 0xe1: 'AltRight', // Only when it is AltGraph + 0x08: 'Backspace', + 0x09: 'Tab', + 0x0a: 'NumpadClear', + 0x0c: 'Numpad5', // IE11 sends evt.keyCode: 12 when numlock is off + 0x0d: 'Enter', + 0x10: 'ShiftLeft', + 0x11: 'ControlLeft', + 0x12: 'AltLeft', + 0x13: 'Pause', + 0x14: 'CapsLock', + 0x15: 'Lang1', + 0x19: 'Lang2', + 0x1b: 'Escape', + 0x1c: 'Convert', + 0x1d: 'NonConvert', + 0x20: 'Space', + 0x21: 'PageUp', + 0x22: 'PageDown', + 0x23: 'End', + 0x24: 'Home', + 0x25: 'ArrowLeft', + 0x26: 'ArrowUp', + 0x27: 'ArrowRight', + 0x28: 'ArrowDown', + 0x29: 'Select', + 0x2c: 'PrintScreen', + 0x2d: 'Insert', + 0x2e: 'Delete', + 0x2f: 'Help', + 0x30: 'Digit0', + 0x31: 'Digit1', + 0x32: 'Digit2', + 0x33: 'Digit3', + 0x34: 'Digit4', + 0x35: 'Digit5', + 0x36: 'Digit6', + 0x37: 'Digit7', + 0x38: 'Digit8', + 0x39: 'Digit9', + 0x5b: 'MetaLeft', + 0x5c: 'MetaRight', + 0x5d: 'ContextMenu', + 0x5f: 'Sleep', + 0x60: 'Numpad0', + 0x61: 'Numpad1', + 0x62: 'Numpad2', + 0x63: 'Numpad3', + 0x64: 'Numpad4', + 0x65: 'Numpad5', + 0x66: 'Numpad6', + 0x67: 'Numpad7', + 0x68: 'Numpad8', + 0x69: 'Numpad9', + 0x6a: 'NumpadMultiply', + 0x6b: 'NumpadAdd', + 0x6c: 'NumpadDecimal', + 0x6d: 'NumpadSubtract', + 0x6e: 'NumpadDecimal', // Duplicate, because buggy on Windows + 0x6f: 'NumpadDivide', + 0x70: 'F1', + 0x71: 'F2', + 0x72: 'F3', + 0x73: 'F4', + 0x74: 'F5', + 0x75: 'F6', + 0x76: 'F7', + 0x77: 'F8', + 0x78: 'F9', + 0x79: 'F10', + 0x7a: 'F11', + 0x7b: 'F12', + 0x7c: 'F13', + 0x7d: 'F14', + 0x7e: 'F15', + 0x7f: 'F16', + 0x80: 'F17', + 0x81: 'F18', + 0x82: 'F19', + 0x83: 'F20', + 0x84: 'F21', + 0x85: 'F22', + 0x86: 'F23', + 0x87: 'F24', + 0x90: 'NumLock', + 0x91: 'ScrollLock', + 0xa6: 'BrowserBack', + 0xa7: 'BrowserForward', + 0xa8: 'BrowserRefresh', + 0xa9: 'BrowserStop', + 0xaa: 'BrowserSearch', + 0xab: 'BrowserFavorites', + 0xac: 'BrowserHome', + 0xad: 'AudioVolumeMute', + 0xae: 'AudioVolumeDown', + 0xaf: 'AudioVolumeUp', + 0xb0: 'MediaTrackNext', + 0xb1: 'MediaTrackPrevious', + 0xb2: 'MediaStop', + 0xb3: 'MediaPlayPause', + 0xb4: 'LaunchMail', + 0xb5: 'MediaSelect', + 0xb6: 'LaunchApp1', + 0xb7: 'LaunchApp2', + 0xe1: 'AltRight', // Only when it is AltGraph }; diff --git a/core/input/xtscancodes.js b/core/input/xtscancodes.js index 514809c6..d9aaf78e 100644 --- a/core/input/xtscancodes.js +++ b/core/input/xtscancodes.js @@ -5,167 +5,167 @@ * keymap-gen --lang=js code-map keymaps.csv html atset1 */ export default { - "Again": 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */ - "AltLeft": 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */ - "AltRight": 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */ - "ArrowDown": 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */ - "ArrowLeft": 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */ - "ArrowRight": 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */ - "ArrowUp": 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */ - "AudioVolumeDown": 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */ - "AudioVolumeMute": 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */ - "AudioVolumeUp": 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */ - "Backquote": 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */ - "Backslash": 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */ - "Backspace": 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */ - "BracketLeft": 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */ - "BracketRight": 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */ - "BrowserBack": 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */ - "BrowserFavorites": 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */ - "BrowserForward": 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */ - "BrowserHome": 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */ - "BrowserRefresh": 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */ - "BrowserSearch": 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */ - "BrowserStop": 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */ - "CapsLock": 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */ - "Comma": 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */ - "ContextMenu": 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */ - "ControlLeft": 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */ - "ControlRight": 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */ - "Convert": 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */ - "Copy": 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */ - "Cut": 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */ - "Delete": 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */ - "Digit0": 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */ - "Digit1": 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */ - "Digit2": 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */ - "Digit3": 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */ - "Digit4": 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */ - "Digit5": 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */ - "Digit6": 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */ - "Digit7": 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */ - "Digit8": 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */ - "Digit9": 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */ - "Eject": 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */ - "End": 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */ - "Enter": 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */ - "Equal": 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */ - "Escape": 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */ - "F1": 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */ - "F10": 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */ - "F11": 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */ - "F12": 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */ - "F13": 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */ - "F14": 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */ - "F15": 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */ - "F16": 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */ - "F17": 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */ - "F18": 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */ - "F19": 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */ - "F2": 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */ - "F20": 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */ - "F21": 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */ - "F22": 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */ - "F23": 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */ - "F24": 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */ - "F3": 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */ - "F4": 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */ - "F5": 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */ - "F6": 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */ - "F7": 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */ - "F8": 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */ - "F9": 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */ - "Find": 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */ - "Help": 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */ - "Hiragana": 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ - "Home": 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */ - "Insert": 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */ - "IntlBackslash": 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */ - "IntlRo": 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */ - "IntlYen": 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */ - "KanaMode": 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */ - "Katakana": 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ - "KeyA": 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */ - "KeyB": 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */ - "KeyC": 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */ - "KeyD": 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */ - "KeyE": 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */ - "KeyF": 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */ - "KeyG": 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */ - "KeyH": 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */ - "KeyI": 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */ - "KeyJ": 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */ - "KeyK": 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */ - "KeyL": 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */ - "KeyM": 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */ - "KeyN": 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */ - "KeyO": 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */ - "KeyP": 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */ - "KeyQ": 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */ - "KeyR": 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */ - "KeyS": 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */ - "KeyT": 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */ - "KeyU": 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */ - "KeyV": 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */ - "KeyW": 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */ - "KeyX": 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */ - "KeyY": 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */ - "KeyZ": 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */ - "Lang3": 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ - "Lang4": 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ - "Lang5": 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */ - "LaunchApp1": 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */ - "LaunchApp2": 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */ - "LaunchMail": 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */ - "MediaPlayPause": 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */ - "MediaSelect": 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */ - "MediaStop": 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */ - "MediaTrackNext": 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */ - "MediaTrackPrevious": 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */ - "MetaLeft": 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */ - "MetaRight": 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */ - "Minus": 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */ - "NonConvert": 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */ - "NumLock": 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */ - "Numpad0": 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */ - "Numpad1": 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */ - "Numpad2": 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */ - "Numpad3": 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */ - "Numpad4": 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */ - "Numpad5": 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */ - "Numpad6": 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */ - "Numpad7": 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */ - "Numpad8": 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */ - "Numpad9": 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */ - "NumpadAdd": 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */ - "NumpadComma": 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */ - "NumpadDecimal": 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */ - "NumpadDivide": 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */ - "NumpadEnter": 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */ - "NumpadEqual": 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */ - "NumpadMultiply": 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */ - "NumpadParenLeft": 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */ - "NumpadParenRight": 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */ - "NumpadSubtract": 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */ - "Open": 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */ - "PageDown": 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */ - "PageUp": 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */ - "Paste": 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */ - "Pause": 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */ - "Period": 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */ - "Power": 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */ - "PrintScreen": 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */ - "Props": 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */ - "Quote": 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */ - "ScrollLock": 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */ - "Semicolon": 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */ - "ShiftLeft": 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */ - "ShiftRight": 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */ - "Slash": 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */ - "Sleep": 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */ - "Space": 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */ - "Suspend": 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */ - "Tab": 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */ - "Undo": 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */ - "WakeUp": 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */ + Again: 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */ + AltLeft: 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */ + AltRight: 0xe038, /* html:AltRight (AltRight) -> linux:100 (KEY_RIGHTALT) -> atset1:57400 */ + ArrowDown: 0xe050, /* html:ArrowDown (ArrowDown) -> linux:108 (KEY_DOWN) -> atset1:57424 */ + ArrowLeft: 0xe04b, /* html:ArrowLeft (ArrowLeft) -> linux:105 (KEY_LEFT) -> atset1:57419 */ + ArrowRight: 0xe04d, /* html:ArrowRight (ArrowRight) -> linux:106 (KEY_RIGHT) -> atset1:57421 */ + ArrowUp: 0xe048, /* html:ArrowUp (ArrowUp) -> linux:103 (KEY_UP) -> atset1:57416 */ + AudioVolumeDown: 0xe02e, /* html:AudioVolumeDown (AudioVolumeDown) -> linux:114 (KEY_VOLUMEDOWN) -> atset1:57390 */ + AudioVolumeMute: 0xe020, /* html:AudioVolumeMute (AudioVolumeMute) -> linux:113 (KEY_MUTE) -> atset1:57376 */ + AudioVolumeUp: 0xe030, /* html:AudioVolumeUp (AudioVolumeUp) -> linux:115 (KEY_VOLUMEUP) -> atset1:57392 */ + Backquote: 0x29, /* html:Backquote (Backquote) -> linux:41 (KEY_GRAVE) -> atset1:41 */ + Backslash: 0x2b, /* html:Backslash (Backslash) -> linux:43 (KEY_BACKSLASH) -> atset1:43 */ + Backspace: 0xe, /* html:Backspace (Backspace) -> linux:14 (KEY_BACKSPACE) -> atset1:14 */ + BracketLeft: 0x1a, /* html:BracketLeft (BracketLeft) -> linux:26 (KEY_LEFTBRACE) -> atset1:26 */ + BracketRight: 0x1b, /* html:BracketRight (BracketRight) -> linux:27 (KEY_RIGHTBRACE) -> atset1:27 */ + BrowserBack: 0xe06a, /* html:BrowserBack (BrowserBack) -> linux:158 (KEY_BACK) -> atset1:57450 */ + BrowserFavorites: 0xe066, /* html:BrowserFavorites (BrowserFavorites) -> linux:156 (KEY_BOOKMARKS) -> atset1:57446 */ + BrowserForward: 0xe069, /* html:BrowserForward (BrowserForward) -> linux:159 (KEY_FORWARD) -> atset1:57449 */ + BrowserHome: 0xe032, /* html:BrowserHome (BrowserHome) -> linux:172 (KEY_HOMEPAGE) -> atset1:57394 */ + BrowserRefresh: 0xe067, /* html:BrowserRefresh (BrowserRefresh) -> linux:173 (KEY_REFRESH) -> atset1:57447 */ + BrowserSearch: 0xe065, /* html:BrowserSearch (BrowserSearch) -> linux:217 (KEY_SEARCH) -> atset1:57445 */ + BrowserStop: 0xe068, /* html:BrowserStop (BrowserStop) -> linux:128 (KEY_STOP) -> atset1:57448 */ + CapsLock: 0x3a, /* html:CapsLock (CapsLock) -> linux:58 (KEY_CAPSLOCK) -> atset1:58 */ + Comma: 0x33, /* html:Comma (Comma) -> linux:51 (KEY_COMMA) -> atset1:51 */ + ContextMenu: 0xe05d, /* html:ContextMenu (ContextMenu) -> linux:127 (KEY_COMPOSE) -> atset1:57437 */ + ControlLeft: 0x1d, /* html:ControlLeft (ControlLeft) -> linux:29 (KEY_LEFTCTRL) -> atset1:29 */ + ControlRight: 0xe01d, /* html:ControlRight (ControlRight) -> linux:97 (KEY_RIGHTCTRL) -> atset1:57373 */ + Convert: 0x79, /* html:Convert (Convert) -> linux:92 (KEY_HENKAN) -> atset1:121 */ + Copy: 0xe078, /* html:Copy (Copy) -> linux:133 (KEY_COPY) -> atset1:57464 */ + Cut: 0xe03c, /* html:Cut (Cut) -> linux:137 (KEY_CUT) -> atset1:57404 */ + Delete: 0xe053, /* html:Delete (Delete) -> linux:111 (KEY_DELETE) -> atset1:57427 */ + Digit0: 0xb, /* html:Digit0 (Digit0) -> linux:11 (KEY_0) -> atset1:11 */ + Digit1: 0x2, /* html:Digit1 (Digit1) -> linux:2 (KEY_1) -> atset1:2 */ + Digit2: 0x3, /* html:Digit2 (Digit2) -> linux:3 (KEY_2) -> atset1:3 */ + Digit3: 0x4, /* html:Digit3 (Digit3) -> linux:4 (KEY_3) -> atset1:4 */ + Digit4: 0x5, /* html:Digit4 (Digit4) -> linux:5 (KEY_4) -> atset1:5 */ + Digit5: 0x6, /* html:Digit5 (Digit5) -> linux:6 (KEY_5) -> atset1:6 */ + Digit6: 0x7, /* html:Digit6 (Digit6) -> linux:7 (KEY_6) -> atset1:7 */ + Digit7: 0x8, /* html:Digit7 (Digit7) -> linux:8 (KEY_7) -> atset1:8 */ + Digit8: 0x9, /* html:Digit8 (Digit8) -> linux:9 (KEY_8) -> atset1:9 */ + Digit9: 0xa, /* html:Digit9 (Digit9) -> linux:10 (KEY_9) -> atset1:10 */ + Eject: 0xe07d, /* html:Eject (Eject) -> linux:162 (KEY_EJECTCLOSECD) -> atset1:57469 */ + End: 0xe04f, /* html:End (End) -> linux:107 (KEY_END) -> atset1:57423 */ + Enter: 0x1c, /* html:Enter (Enter) -> linux:28 (KEY_ENTER) -> atset1:28 */ + Equal: 0xd, /* html:Equal (Equal) -> linux:13 (KEY_EQUAL) -> atset1:13 */ + Escape: 0x1, /* html:Escape (Escape) -> linux:1 (KEY_ESC) -> atset1:1 */ + F1: 0x3b, /* html:F1 (F1) -> linux:59 (KEY_F1) -> atset1:59 */ + F10: 0x44, /* html:F10 (F10) -> linux:68 (KEY_F10) -> atset1:68 */ + F11: 0x57, /* html:F11 (F11) -> linux:87 (KEY_F11) -> atset1:87 */ + F12: 0x58, /* html:F12 (F12) -> linux:88 (KEY_F12) -> atset1:88 */ + F13: 0x5d, /* html:F13 (F13) -> linux:183 (KEY_F13) -> atset1:93 */ + F14: 0x5e, /* html:F14 (F14) -> linux:184 (KEY_F14) -> atset1:94 */ + F15: 0x5f, /* html:F15 (F15) -> linux:185 (KEY_F15) -> atset1:95 */ + F16: 0x55, /* html:F16 (F16) -> linux:186 (KEY_F16) -> atset1:85 */ + F17: 0xe003, /* html:F17 (F17) -> linux:187 (KEY_F17) -> atset1:57347 */ + F18: 0xe077, /* html:F18 (F18) -> linux:188 (KEY_F18) -> atset1:57463 */ + F19: 0xe004, /* html:F19 (F19) -> linux:189 (KEY_F19) -> atset1:57348 */ + F2: 0x3c, /* html:F2 (F2) -> linux:60 (KEY_F2) -> atset1:60 */ + F20: 0x5a, /* html:F20 (F20) -> linux:190 (KEY_F20) -> atset1:90 */ + F21: 0x74, /* html:F21 (F21) -> linux:191 (KEY_F21) -> atset1:116 */ + F22: 0xe079, /* html:F22 (F22) -> linux:192 (KEY_F22) -> atset1:57465 */ + F23: 0x6d, /* html:F23 (F23) -> linux:193 (KEY_F23) -> atset1:109 */ + F24: 0x6f, /* html:F24 (F24) -> linux:194 (KEY_F24) -> atset1:111 */ + F3: 0x3d, /* html:F3 (F3) -> linux:61 (KEY_F3) -> atset1:61 */ + F4: 0x3e, /* html:F4 (F4) -> linux:62 (KEY_F4) -> atset1:62 */ + F5: 0x3f, /* html:F5 (F5) -> linux:63 (KEY_F5) -> atset1:63 */ + F6: 0x40, /* html:F6 (F6) -> linux:64 (KEY_F6) -> atset1:64 */ + F7: 0x41, /* html:F7 (F7) -> linux:65 (KEY_F7) -> atset1:65 */ + F8: 0x42, /* html:F8 (F8) -> linux:66 (KEY_F8) -> atset1:66 */ + F9: 0x43, /* html:F9 (F9) -> linux:67 (KEY_F9) -> atset1:67 */ + Find: 0xe041, /* html:Find (Find) -> linux:136 (KEY_FIND) -> atset1:57409 */ + Help: 0xe075, /* html:Help (Help) -> linux:138 (KEY_HELP) -> atset1:57461 */ + Hiragana: 0x77, /* html:Hiragana (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ + Home: 0xe047, /* html:Home (Home) -> linux:102 (KEY_HOME) -> atset1:57415 */ + Insert: 0xe052, /* html:Insert (Insert) -> linux:110 (KEY_INSERT) -> atset1:57426 */ + IntlBackslash: 0x56, /* html:IntlBackslash (IntlBackslash) -> linux:86 (KEY_102ND) -> atset1:86 */ + IntlRo: 0x73, /* html:IntlRo (IntlRo) -> linux:89 (KEY_RO) -> atset1:115 */ + IntlYen: 0x7d, /* html:IntlYen (IntlYen) -> linux:124 (KEY_YEN) -> atset1:125 */ + KanaMode: 0x70, /* html:KanaMode (KanaMode) -> linux:93 (KEY_KATAKANAHIRAGANA) -> atset1:112 */ + Katakana: 0x78, /* html:Katakana (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ + KeyA: 0x1e, /* html:KeyA (KeyA) -> linux:30 (KEY_A) -> atset1:30 */ + KeyB: 0x30, /* html:KeyB (KeyB) -> linux:48 (KEY_B) -> atset1:48 */ + KeyC: 0x2e, /* html:KeyC (KeyC) -> linux:46 (KEY_C) -> atset1:46 */ + KeyD: 0x20, /* html:KeyD (KeyD) -> linux:32 (KEY_D) -> atset1:32 */ + KeyE: 0x12, /* html:KeyE (KeyE) -> linux:18 (KEY_E) -> atset1:18 */ + KeyF: 0x21, /* html:KeyF (KeyF) -> linux:33 (KEY_F) -> atset1:33 */ + KeyG: 0x22, /* html:KeyG (KeyG) -> linux:34 (KEY_G) -> atset1:34 */ + KeyH: 0x23, /* html:KeyH (KeyH) -> linux:35 (KEY_H) -> atset1:35 */ + KeyI: 0x17, /* html:KeyI (KeyI) -> linux:23 (KEY_I) -> atset1:23 */ + KeyJ: 0x24, /* html:KeyJ (KeyJ) -> linux:36 (KEY_J) -> atset1:36 */ + KeyK: 0x25, /* html:KeyK (KeyK) -> linux:37 (KEY_K) -> atset1:37 */ + KeyL: 0x26, /* html:KeyL (KeyL) -> linux:38 (KEY_L) -> atset1:38 */ + KeyM: 0x32, /* html:KeyM (KeyM) -> linux:50 (KEY_M) -> atset1:50 */ + KeyN: 0x31, /* html:KeyN (KeyN) -> linux:49 (KEY_N) -> atset1:49 */ + KeyO: 0x18, /* html:KeyO (KeyO) -> linux:24 (KEY_O) -> atset1:24 */ + KeyP: 0x19, /* html:KeyP (KeyP) -> linux:25 (KEY_P) -> atset1:25 */ + KeyQ: 0x10, /* html:KeyQ (KeyQ) -> linux:16 (KEY_Q) -> atset1:16 */ + KeyR: 0x13, /* html:KeyR (KeyR) -> linux:19 (KEY_R) -> atset1:19 */ + KeyS: 0x1f, /* html:KeyS (KeyS) -> linux:31 (KEY_S) -> atset1:31 */ + KeyT: 0x14, /* html:KeyT (KeyT) -> linux:20 (KEY_T) -> atset1:20 */ + KeyU: 0x16, /* html:KeyU (KeyU) -> linux:22 (KEY_U) -> atset1:22 */ + KeyV: 0x2f, /* html:KeyV (KeyV) -> linux:47 (KEY_V) -> atset1:47 */ + KeyW: 0x11, /* html:KeyW (KeyW) -> linux:17 (KEY_W) -> atset1:17 */ + KeyX: 0x2d, /* html:KeyX (KeyX) -> linux:45 (KEY_X) -> atset1:45 */ + KeyY: 0x15, /* html:KeyY (KeyY) -> linux:21 (KEY_Y) -> atset1:21 */ + KeyZ: 0x2c, /* html:KeyZ (KeyZ) -> linux:44 (KEY_Z) -> atset1:44 */ + Lang3: 0x78, /* html:Lang3 (Lang3) -> linux:90 (KEY_KATAKANA) -> atset1:120 */ + Lang4: 0x77, /* html:Lang4 (Lang4) -> linux:91 (KEY_HIRAGANA) -> atset1:119 */ + Lang5: 0x76, /* html:Lang5 (Lang5) -> linux:85 (KEY_ZENKAKUHANKAKU) -> atset1:118 */ + LaunchApp1: 0xe06b, /* html:LaunchApp1 (LaunchApp1) -> linux:157 (KEY_COMPUTER) -> atset1:57451 */ + LaunchApp2: 0xe021, /* html:LaunchApp2 (LaunchApp2) -> linux:140 (KEY_CALC) -> atset1:57377 */ + LaunchMail: 0xe06c, /* html:LaunchMail (LaunchMail) -> linux:155 (KEY_MAIL) -> atset1:57452 */ + MediaPlayPause: 0xe022, /* html:MediaPlayPause (MediaPlayPause) -> linux:164 (KEY_PLAYPAUSE) -> atset1:57378 */ + MediaSelect: 0xe06d, /* html:MediaSelect (MediaSelect) -> linux:226 (KEY_MEDIA) -> atset1:57453 */ + MediaStop: 0xe024, /* html:MediaStop (MediaStop) -> linux:166 (KEY_STOPCD) -> atset1:57380 */ + MediaTrackNext: 0xe019, /* html:MediaTrackNext (MediaTrackNext) -> linux:163 (KEY_NEXTSONG) -> atset1:57369 */ + MediaTrackPrevious: 0xe010, /* html:MediaTrackPrevious (MediaTrackPrevious) -> linux:165 (KEY_PREVIOUSSONG) -> atset1:57360 */ + MetaLeft: 0xe05b, /* html:MetaLeft (MetaLeft) -> linux:125 (KEY_LEFTMETA) -> atset1:57435 */ + MetaRight: 0xe05c, /* html:MetaRight (MetaRight) -> linux:126 (KEY_RIGHTMETA) -> atset1:57436 */ + Minus: 0xc, /* html:Minus (Minus) -> linux:12 (KEY_MINUS) -> atset1:12 */ + NonConvert: 0x7b, /* html:NonConvert (NonConvert) -> linux:94 (KEY_MUHENKAN) -> atset1:123 */ + NumLock: 0x45, /* html:NumLock (NumLock) -> linux:69 (KEY_NUMLOCK) -> atset1:69 */ + Numpad0: 0x52, /* html:Numpad0 (Numpad0) -> linux:82 (KEY_KP0) -> atset1:82 */ + Numpad1: 0x4f, /* html:Numpad1 (Numpad1) -> linux:79 (KEY_KP1) -> atset1:79 */ + Numpad2: 0x50, /* html:Numpad2 (Numpad2) -> linux:80 (KEY_KP2) -> atset1:80 */ + Numpad3: 0x51, /* html:Numpad3 (Numpad3) -> linux:81 (KEY_KP3) -> atset1:81 */ + Numpad4: 0x4b, /* html:Numpad4 (Numpad4) -> linux:75 (KEY_KP4) -> atset1:75 */ + Numpad5: 0x4c, /* html:Numpad5 (Numpad5) -> linux:76 (KEY_KP5) -> atset1:76 */ + Numpad6: 0x4d, /* html:Numpad6 (Numpad6) -> linux:77 (KEY_KP6) -> atset1:77 */ + Numpad7: 0x47, /* html:Numpad7 (Numpad7) -> linux:71 (KEY_KP7) -> atset1:71 */ + Numpad8: 0x48, /* html:Numpad8 (Numpad8) -> linux:72 (KEY_KP8) -> atset1:72 */ + Numpad9: 0x49, /* html:Numpad9 (Numpad9) -> linux:73 (KEY_KP9) -> atset1:73 */ + NumpadAdd: 0x4e, /* html:NumpadAdd (NumpadAdd) -> linux:78 (KEY_KPPLUS) -> atset1:78 */ + NumpadComma: 0x7e, /* html:NumpadComma (NumpadComma) -> linux:121 (KEY_KPCOMMA) -> atset1:126 */ + NumpadDecimal: 0x53, /* html:NumpadDecimal (NumpadDecimal) -> linux:83 (KEY_KPDOT) -> atset1:83 */ + NumpadDivide: 0xe035, /* html:NumpadDivide (NumpadDivide) -> linux:98 (KEY_KPSLASH) -> atset1:57397 */ + NumpadEnter: 0xe01c, /* html:NumpadEnter (NumpadEnter) -> linux:96 (KEY_KPENTER) -> atset1:57372 */ + NumpadEqual: 0x59, /* html:NumpadEqual (NumpadEqual) -> linux:117 (KEY_KPEQUAL) -> atset1:89 */ + NumpadMultiply: 0x37, /* html:NumpadMultiply (NumpadMultiply) -> linux:55 (KEY_KPASTERISK) -> atset1:55 */ + NumpadParenLeft: 0xe076, /* html:NumpadParenLeft (NumpadParenLeft) -> linux:179 (KEY_KPLEFTPAREN) -> atset1:57462 */ + NumpadParenRight: 0xe07b, /* html:NumpadParenRight (NumpadParenRight) -> linux:180 (KEY_KPRIGHTPAREN) -> atset1:57467 */ + NumpadSubtract: 0x4a, /* html:NumpadSubtract (NumpadSubtract) -> linux:74 (KEY_KPMINUS) -> atset1:74 */ + Open: 0x64, /* html:Open (Open) -> linux:134 (KEY_OPEN) -> atset1:100 */ + PageDown: 0xe051, /* html:PageDown (PageDown) -> linux:109 (KEY_PAGEDOWN) -> atset1:57425 */ + PageUp: 0xe049, /* html:PageUp (PageUp) -> linux:104 (KEY_PAGEUP) -> atset1:57417 */ + Paste: 0x65, /* html:Paste (Paste) -> linux:135 (KEY_PASTE) -> atset1:101 */ + Pause: 0xe046, /* html:Pause (Pause) -> linux:119 (KEY_PAUSE) -> atset1:57414 */ + Period: 0x34, /* html:Period (Period) -> linux:52 (KEY_DOT) -> atset1:52 */ + Power: 0xe05e, /* html:Power (Power) -> linux:116 (KEY_POWER) -> atset1:57438 */ + PrintScreen: 0x54, /* html:PrintScreen (PrintScreen) -> linux:99 (KEY_SYSRQ) -> atset1:84 */ + Props: 0xe006, /* html:Props (Props) -> linux:130 (KEY_PROPS) -> atset1:57350 */ + Quote: 0x28, /* html:Quote (Quote) -> linux:40 (KEY_APOSTROPHE) -> atset1:40 */ + ScrollLock: 0x46, /* html:ScrollLock (ScrollLock) -> linux:70 (KEY_SCROLLLOCK) -> atset1:70 */ + Semicolon: 0x27, /* html:Semicolon (Semicolon) -> linux:39 (KEY_SEMICOLON) -> atset1:39 */ + ShiftLeft: 0x2a, /* html:ShiftLeft (ShiftLeft) -> linux:42 (KEY_LEFTSHIFT) -> atset1:42 */ + ShiftRight: 0x36, /* html:ShiftRight (ShiftRight) -> linux:54 (KEY_RIGHTSHIFT) -> atset1:54 */ + Slash: 0x35, /* html:Slash (Slash) -> linux:53 (KEY_SLASH) -> atset1:53 */ + Sleep: 0xe05f, /* html:Sleep (Sleep) -> linux:142 (KEY_SLEEP) -> atset1:57439 */ + Space: 0x39, /* html:Space (Space) -> linux:57 (KEY_SPACE) -> atset1:57 */ + Suspend: 0xe025, /* html:Suspend (Suspend) -> linux:205 (KEY_SUSPEND) -> atset1:57381 */ + Tab: 0xf, /* html:Tab (Tab) -> linux:15 (KEY_TAB) -> atset1:15 */ + Undo: 0xe007, /* html:Undo (Undo) -> linux:131 (KEY_UNDO) -> atset1:57351 */ + WakeUp: 0xe063, /* html:WakeUp (WakeUp) -> linux:143 (KEY_WAKEUP) -> atset1:57443 */ }; diff --git a/core/rfb.js b/core/rfb.js index 34330302..9f39323c 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -13,905 +13,915 @@ import * as Log from './util/logging.js'; import { decodeUTF8 } from './util/strings.js'; import EventTargetMixin from './util/eventtarget.js'; -import Display from "./display.js"; -import Keyboard from "./input/keyboard.js"; -import Mouse from "./input/mouse.js"; -import Cursor from "./util/cursor.js"; -import Websock from "./websock.js"; -import DES from "./des.js"; -import KeyTable from "./input/keysym.js"; -import XtScancode from "./input/xtscancodes.js"; -import Inflator from "./inflator.js"; -import { encodings, encodingName } from "./encodings.js"; -import "./util/polyfill.js"; +import Display from './display.js'; +import Keyboard from './input/keyboard.js'; +import Mouse from './input/mouse.js'; +import Cursor from './util/cursor.js'; +import Websock from './websock.js'; +import DES from './des.js'; +import KeyTable from './input/keysym.js'; +import XtScancode from './input/xtscancodes.js'; +import Inflator from './inflator.js'; +import { encodings, encodingName } from './encodings.js'; +import './util/polyfill.js'; // How many seconds to wait for a disconnect to finish const DISCONNECT_TIMEOUT = 3; export default class RFB extends EventTargetMixin { - constructor(target, url, options) { - if (!target) { - throw Error("Must specify target"); - } - if (!url) { - throw Error("Must specify URL"); - } + constructor(target, url, options) { + if (!target) { + throw Error('Must specify target'); + } + if (!url) { + throw Error('Must specify URL'); + } - super(); + super(); - this._target = target; - this._url = url; + this._target = target; + this._url = url; - // Connection details - options = options || {}; - this._rfb_credentials = options.credentials || {}; - this._shared = 'shared' in options ? !!options.shared : true; - this._repeaterID = options.repeaterID || ''; + // Connection details + options = options || {}; + this._rfb_credentials = options.credentials || {}; + this._shared = 'shared' in options ? !!options.shared : true; + this._repeaterID = options.repeaterID || ''; - // Internal state - this._rfb_connection_state = ''; - this._rfb_init_state = ''; - this._rfb_auth_scheme = ''; - this._rfb_clean_disconnect = true; + // Internal state + this._rfb_connection_state = ''; + this._rfb_init_state = ''; + this._rfb_auth_scheme = ''; + this._rfb_clean_disconnect = true; - // Server capabilities - this._rfb_version = 0; - this._rfb_max_version = 3.8; - this._rfb_tightvnc = false; - this._rfb_xvp_ver = 0; + // Server capabilities + this._rfb_version = 0; + this._rfb_max_version = 3.8; + this._rfb_tightvnc = false; + this._rfb_xvp_ver = 0; - this._fb_width = 0; - this._fb_height = 0; + this._fb_width = 0; + this._fb_height = 0; - this._fb_name = ""; + this._fb_name = ''; - this._capabilities = { power: false }; + this._capabilities = { power: false }; - this._supportsFence = false; + this._supportsFence = false; - this._supportsContinuousUpdates = false; - this._enabledContinuousUpdates = false; + this._supportsContinuousUpdates = false; + this._enabledContinuousUpdates = false; - this._supportsSetDesktopSize = false; - this._screen_id = 0; - this._screen_flags = 0; + this._supportsSetDesktopSize = false; + this._screen_id = 0; + this._screen_flags = 0; - this._qemuExtKeyEventSupported = false; + this._qemuExtKeyEventSupported = false; - // Internal objects - this._sock = null; // Websock object - this._display = null; // Display object - this._flushing = false; // Display flushing state - this._keyboard = null; // Keyboard input handler object - this._mouse = null; // Mouse input handler object + // Internal objects + this._sock = null; // Websock object + this._display = null; // Display object + this._flushing = false; // Display flushing state + this._keyboard = null; // Keyboard input handler object + this._mouse = null; // Mouse input handler object - // Timers - this._disconnTimer = null; // disconnection timer - this._resizeTimeout = null; // resize rate limiting + // Timers + this._disconnTimer = null; // disconnection timer + this._resizeTimeout = null; // resize rate limiting - // Decoder states and stats - this._encHandlers = {}; - this._encStats = {}; + // Decoder states and stats + this._encHandlers = {}; + this._encStats = {}; - this._FBU = { - rects: 0, - subrects: 0, // RRE and HEXTILE - lines: 0, // RAW - tiles: 0, // HEXTILE - bytes: 0, - x: 0, - y: 0, - width: 0, - height: 0, - encoding: 0, - subencoding: -1, - background: null, - zlibs: [] // TIGHT zlib streams - }; + this._FBU = { + rects: 0, + subrects: 0, // RRE and HEXTILE + lines: 0, // RAW + tiles: 0, // HEXTILE + bytes: 0, + x: 0, + y: 0, + width: 0, + height: 0, + encoding: 0, + subencoding: -1, + background: null, + zlibs: [] // TIGHT zlib streams + }; - for (let i = 0; i < 4; i++) { - this._FBU.zlibs[i] = new Inflator(); - } + for (let i = 0; i < 4; i++) { + this._FBU.zlibs[i] = new Inflator(); + } - this._destBuff = null; - this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) + this._destBuff = null; + this._paletteBuff = new Uint8Array(1024); // 256 * 4 (max palette size * max bytes-per-pixel) - this._rre_chunk_sz = 100; + this._rre_chunk_sz = 100; - this._timing = { - last_fbu: 0, - fbu_total: 0, - fbu_total_cnt: 0, - full_fbu_total: 0, - full_fbu_cnt: 0, + this._timing = { + last_fbu: 0, + fbu_total: 0, + fbu_total_cnt: 0, + full_fbu_total: 0, + full_fbu_cnt: 0, - fbu_rt_start: 0, - fbu_rt_total: 0, - fbu_rt_cnt: 0, - pixels: 0 - }; + fbu_rt_start: 0, + fbu_rt_total: 0, + fbu_rt_cnt: 0, + pixels: 0 + }; - // Mouse state - this._mouse_buttonMask = 0; - this._mouse_arr = []; - this._viewportDragging = false; - this._viewportDragPos = {}; - this._viewportHasMoved = false; + // Mouse state + this._mouse_buttonMask = 0; + this._mouse_arr = []; + this._viewportDragging = false; + this._viewportDragPos = {}; + this._viewportHasMoved = false; - // Bound event handlers - this._eventHandlers = { - focusCanvas: this._focusCanvas.bind(this), - windowResize: this._windowResize.bind(this), - }; + // Bound event handlers + this._eventHandlers = { + focusCanvas: this._focusCanvas.bind(this), + windowResize: this._windowResize.bind(this), + }; - // main setup - Log.Debug(">> RFB.constructor"); + // main setup + Log.Debug('>> RFB.constructor'); - // Create DOM elements - this._screen = document.createElement('div'); - this._screen.style.display = 'flex'; - this._screen.style.width = '100%'; - this._screen.style.height = '100%'; - this._screen.style.overflow = 'auto'; - this._screen.style.backgroundColor = 'rgb(40, 40, 40)'; - this._canvas = document.createElement('canvas'); - this._canvas.style.margin = 'auto'; - // Some browsers add an outline on focus - this._canvas.style.outline = 'none'; - // IE miscalculates width without this :( - this._canvas.style.flexShrink = '0'; - this._canvas.width = 0; - this._canvas.height = 0; - this._canvas.tabIndex = -1; - this._screen.appendChild(this._canvas); + // Create DOM elements + this._screen = document.createElement('div'); + this._screen.style.display = 'flex'; + this._screen.style.width = '100%'; + this._screen.style.height = '100%'; + this._screen.style.overflow = 'auto'; + this._screen.style.backgroundColor = 'rgb(40, 40, 40)'; + this._canvas = document.createElement('canvas'); + this._canvas.style.margin = 'auto'; + // Some browsers add an outline on focus + this._canvas.style.outline = 'none'; + // IE miscalculates width without this :( + this._canvas.style.flexShrink = '0'; + this._canvas.width = 0; + this._canvas.height = 0; + this._canvas.tabIndex = -1; + this._screen.appendChild(this._canvas); this._cursor = new Cursor(); - // populate encHandlers with bound versions - this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); - this._encHandlers[encodings.encodingCopyRect] = RFB.encodingHandlers.COPYRECT.bind(this); - this._encHandlers[encodings.encodingRRE] = RFB.encodingHandlers.RRE.bind(this); - this._encHandlers[encodings.encodingHextile] = RFB.encodingHandlers.HEXTILE.bind(this); - this._encHandlers[encodings.encodingTight] = RFB.encodingHandlers.TIGHT.bind(this, false); - this._encHandlers[encodings.encodingTightPNG] = RFB.encodingHandlers.TIGHT.bind(this, true); + // populate encHandlers with bound versions + this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); + this._encHandlers[encodings.encodingCopyRect] = RFB.encodingHandlers.COPYRECT.bind(this); + this._encHandlers[encodings.encodingRRE] = RFB.encodingHandlers.RRE.bind(this); + this._encHandlers[encodings.encodingHextile] = RFB.encodingHandlers.HEXTILE.bind(this); + this._encHandlers[encodings.encodingTight] = RFB.encodingHandlers.TIGHT.bind(this, false); + this._encHandlers[encodings.encodingTightPNG] = RFB.encodingHandlers.TIGHT.bind(this, true); - this._encHandlers[encodings.pseudoEncodingDesktopSize] = RFB.encodingHandlers.DesktopSize.bind(this); - this._encHandlers[encodings.pseudoEncodingLastRect] = RFB.encodingHandlers.last_rect.bind(this); - this._encHandlers[encodings.pseudoEncodingCursor] = RFB.encodingHandlers.Cursor.bind(this); - this._encHandlers[encodings.pseudoEncodingQEMUExtendedKeyEvent] = RFB.encodingHandlers.QEMUExtendedKeyEvent.bind(this); - this._encHandlers[encodings.pseudoEncodingExtendedDesktopSize] = RFB.encodingHandlers.ExtendedDesktopSize.bind(this); + this._encHandlers[encodings.pseudoEncodingDesktopSize] = RFB.encodingHandlers.DesktopSize.bind(this); + this._encHandlers[encodings.pseudoEncodingLastRect] = RFB.encodingHandlers.last_rect.bind(this); + this._encHandlers[encodings.pseudoEncodingCursor] = RFB.encodingHandlers.Cursor.bind(this); + this._encHandlers[encodings.pseudoEncodingQEMUExtendedKeyEvent] = RFB.encodingHandlers.QEMUExtendedKeyEvent.bind(this); + this._encHandlers[encodings.pseudoEncodingExtendedDesktopSize] = RFB.encodingHandlers.ExtendedDesktopSize.bind(this); - // NB: nothing that needs explicit teardown should be done - // before this point, since this can throw an exception - try { - this._display = new Display(this._canvas); - } catch (exc) { - Log.Error("Display exception: " + exc); - throw exc; - } - this._display.onflush = this._onFlush.bind(this); - this._display.clear(); - - this._keyboard = new Keyboard(this._canvas); - this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); - - this._mouse = new Mouse(this._canvas); - this._mouse.onmousebutton = this._handleMouseButton.bind(this); - this._mouse.onmousemove = this._handleMouseMove.bind(this); - - this._sock = new Websock(); - this._sock.on('message', this._handle_message.bind(this)); - this._sock.on('open', () => { - if ((this._rfb_connection_state === 'connecting') && - (this._rfb_init_state === '')) { - this._rfb_init_state = 'ProtocolVersion'; - Log.Debug("Starting VNC handshake"); - } else { - this._fail("Unexpected server connection while " + - this._rfb_connection_state); - } - }); - this._sock.on('close', (e) => { - Log.Debug("WebSocket on-close event"); - let msg = ""; - if (e.code) { - msg = "(code: " + e.code; - if (e.reason) { - msg += ", reason: " + e.reason; - } - msg += ")"; - } - switch (this._rfb_connection_state) { - case 'connecting': - this._fail("Connection closed " + msg); - break; - case 'connected': - // Handle disconnects that were initiated server-side - this._updateConnectionState('disconnecting'); - this._updateConnectionState('disconnected'); - break; - case 'disconnecting': - // Normal disconnection path - this._updateConnectionState('disconnected'); - break; - case 'disconnected': - this._fail("Unexpected server disconnect " + - "when already disconnected " + msg); - break; - default: - this._fail("Unexpected server disconnect before connecting " + - msg); - break; - } - this._sock.off('close'); - }); - this._sock.on('error', e => Log.Warn("WebSocket on-error event")); - - // Slight delay of the actual connection so that the caller has - // time to set up callbacks - setTimeout(this._updateConnectionState.bind(this, 'connecting')); - - Log.Debug("<< RFB.constructor"); - - // ===== PROPERTIES ===== - - this.dragViewport = false; - this.focusOnClick = true; - - this._viewOnly = false; - this._clipViewport = false; - this._scaleViewport = false; - this._resizeSession = false; + // NB: nothing that needs explicit teardown should be done + // before this point, since this can throw an exception + try { + this._display = new Display(this._canvas); + } catch (exc) { + Log.Error('Display exception: ' + exc); + throw exc; } + this._display.onflush = this._onFlush.bind(this); + this._display.clear(); + + this._keyboard = new Keyboard(this._canvas); + this._keyboard.onkeyevent = this._handleKeyEvent.bind(this); + + this._mouse = new Mouse(this._canvas); + this._mouse.onmousebutton = this._handleMouseButton.bind(this); + this._mouse.onmousemove = this._handleMouseMove.bind(this); + + this._sock = new Websock(); + this._sock.on('message', this._handle_message.bind(this)); + this._sock.on('open', () => { + if ((this._rfb_connection_state === 'connecting') + && (this._rfb_init_state === '')) { + this._rfb_init_state = 'ProtocolVersion'; + Log.Debug('Starting VNC handshake'); + } else { + this._fail('Unexpected server connection while ' + + this._rfb_connection_state); + } + }); + this._sock.on('close', (e) => { + Log.Debug('WebSocket on-close event'); + let msg = ''; + if (e.code) { + msg = '(code: ' + e.code; + if (e.reason) { + msg += ', reason: ' + e.reason; + } + msg += ')'; + } + switch (this._rfb_connection_state) { + case 'connecting': + this._fail('Connection closed ' + msg); + break; + case 'connected': + // Handle disconnects that were initiated server-side + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); + break; + case 'disconnecting': + // Normal disconnection path + this._updateConnectionState('disconnected'); + break; + case 'disconnected': + this._fail('Unexpected server disconnect ' + + 'when already disconnected ' + msg); + break; + default: + this._fail('Unexpected server disconnect before connecting ' + + msg); + break; + } + this._sock.off('close'); + }); + this._sock.on('error', e => Log.Warn('WebSocket on-error event')); + + // Slight delay of the actual connection so that the caller has + // time to set up callbacks + setTimeout(this._updateConnectionState.bind(this, 'connecting')); + + Log.Debug('<< RFB.constructor'); // ===== PROPERTIES ===== - get viewOnly() { return this._viewOnly; } - set viewOnly(viewOnly) { - this._viewOnly = viewOnly; + this.dragViewport = false; + this.focusOnClick = true; - if (this._rfb_connection_state === "connecting" || - this._rfb_connection_state === "connected") { - if (viewOnly) { - this._keyboard.ungrab(); - this._mouse.ungrab(); - } else { - this._keyboard.grab(); - this._mouse.grab(); - } - } - } + this._viewOnly = false; + this._clipViewport = false; + this._scaleViewport = false; + this._resizeSession = false; + } - get capabilities() { return this._capabilities; } + // ===== PROPERTIES ===== - get touchButton() { return this._mouse.touchButton; } - set touchButton(button) { this._mouse.touchButton = button; } + get viewOnly() { return this._viewOnly; } - get clipViewport() { return this._clipViewport; } - set clipViewport(viewport) { - this._clipViewport = viewport; - this._updateClip(); - } + set viewOnly(viewOnly) { + this._viewOnly = viewOnly; - get scaleViewport() { return this._scaleViewport; } - set scaleViewport(scale) { - this._scaleViewport = scale; - // Scaling trumps clipping, so we may need to adjust - // clipping when enabling or disabling scaling - if (scale && this._clipViewport) { - this._updateClip(); - } - this._updateScale(); - if (!scale && this._clipViewport) { - this._updateClip(); - } - } - - get resizeSession() { return this._resizeSession; } - set resizeSession(resize) { - this._resizeSession = resize; - if (resize) { - this._requestRemoteResize(); - } - } - - // ===== PUBLIC METHODS ===== - - disconnect() { - this._updateConnectionState('disconnecting'); - this._sock.off('error'); - this._sock.off('message'); - this._sock.off('open'); - } - - sendCredentials(creds) { - this._rfb_credentials = creds; - setTimeout(this._init_msg.bind(this), 0); - } - - sendCtrlAltDel() { - if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } - Log.Info("Sending Ctrl-Alt-Del"); - - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", true); - this.sendKey(KeyTable.XK_Alt_L, "AltLeft", true); - this.sendKey(KeyTable.XK_Delete, "Delete", true); - this.sendKey(KeyTable.XK_Delete, "Delete", false); - this.sendKey(KeyTable.XK_Alt_L, "AltLeft", false); - this.sendKey(KeyTable.XK_Control_L, "ControlLeft", false); - } - - machineShutdown() { - this._xvpOp(1, 2); - } - - machineReboot() { - this._xvpOp(1, 3); - } - - machineReset() { - this._xvpOp(1, 4); - } - - // Send a key press. If 'down' is not specified then send a down key - // followed by an up key. - sendKey(keysym, code, down) { - if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } - - if (down === undefined) { - this.sendKey(keysym, code, true); - this.sendKey(keysym, code, false); - return; - } - - const scancode = XtScancode[code]; - - if (this._qemuExtKeyEventSupported && scancode) { - // 0 is NoSymbol - keysym = keysym || 0; - - Log.Info("Sending key (" + (down ? "down" : "up") + "): keysym " + keysym + ", scancode " + scancode); - - RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); - } else { - if (!keysym) { - return; - } - Log.Info("Sending keysym (" + (down ? "down" : "up") + "): " + keysym); - RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); - } - } - - focus() { - this._canvas.focus(); - } - - blur() { - this._canvas.blur(); - } - - clipboardPasteFrom(text) { - if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } - RFB.messages.clientCutText(this._sock, text); - } - - // ===== PRIVATE METHODS ===== - - _connect() { - Log.Debug(">> RFB.connect"); - - Log.Info("connecting to " + this._url); - - try { - // WebSocket.onopen transitions to the RFB init states - this._sock.open(this._url, ['binary']); - } catch (e) { - if (e.name === 'SyntaxError') { - this._fail("Invalid host or port (" + e + ")"); - } else { - this._fail("Error when opening socket (" + e + ")"); - } - } - - // Make our elements part of the page - this._target.appendChild(this._screen); - - this._cursor.attach(this._canvas); - - // Monitor size changes of the screen - // FIXME: Use ResizeObserver, or hidden overflow - window.addEventListener('resize', this._eventHandlers.windowResize); - - // Always grab focus on some kind of click event - this._canvas.addEventListener("mousedown", this._eventHandlers.focusCanvas); - this._canvas.addEventListener("touchstart", this._eventHandlers.focusCanvas); - - Log.Debug("<< RFB.connect"); - } - - _disconnect() { - Log.Debug(">> RFB.disconnect"); - this._cursor.detach(); - this._canvas.removeEventListener("mousedown", this._eventHandlers.focusCanvas); - this._canvas.removeEventListener("touchstart", this._eventHandlers.focusCanvas); - window.removeEventListener('resize', this._eventHandlers.windowResize); + if (this._rfb_connection_state === 'connecting' + || this._rfb_connection_state === 'connected') { + if (viewOnly) { this._keyboard.ungrab(); this._mouse.ungrab(); - this._sock.close(); - this._print_stats(); - try { - this._target.removeChild(this._screen); - } catch (e) { - if (e.name === 'NotFoundError') { - // Some cases where the initial connection fails - // can disconnect before the _screen is created - } else { - throw e; - } - } - clearTimeout(this._resizeTimeout); - Log.Debug("<< RFB.disconnect"); + } else { + this._keyboard.grab(); + this._mouse.grab(); + } + } + } + + get capabilities() { return this._capabilities; } + + get touchButton() { return this._mouse.touchButton; } + + set touchButton(button) { this._mouse.touchButton = button; } + + get clipViewport() { return this._clipViewport; } + + set clipViewport(viewport) { + this._clipViewport = viewport; + this._updateClip(); + } + + get scaleViewport() { return this._scaleViewport; } + + set scaleViewport(scale) { + this._scaleViewport = scale; + // Scaling trumps clipping, so we may need to adjust + // clipping when enabling or disabling scaling + if (scale && this._clipViewport) { + this._updateClip(); + } + this._updateScale(); + if (!scale && this._clipViewport) { + this._updateClip(); + } + } + + get resizeSession() { return this._resizeSession; } + + set resizeSession(resize) { + this._resizeSession = resize; + if (resize) { + this._requestRemoteResize(); + } + } + + // ===== PUBLIC METHODS ===== + + disconnect() { + this._updateConnectionState('disconnecting'); + this._sock.off('error'); + this._sock.off('message'); + this._sock.off('open'); + } + + sendCredentials(creds) { + this._rfb_credentials = creds; + setTimeout(this._init_msg.bind(this), 0); + } + + sendCtrlAltDel() { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + Log.Info('Sending Ctrl-Alt-Del'); + + this.sendKey(KeyTable.XK_Control_L, 'ControlLeft', true); + this.sendKey(KeyTable.XK_Alt_L, 'AltLeft', true); + this.sendKey(KeyTable.XK_Delete, 'Delete', true); + this.sendKey(KeyTable.XK_Delete, 'Delete', false); + this.sendKey(KeyTable.XK_Alt_L, 'AltLeft', false); + this.sendKey(KeyTable.XK_Control_L, 'ControlLeft', false); + } + + machineShutdown() { + this._xvpOp(1, 2); + } + + machineReboot() { + this._xvpOp(1, 3); + } + + machineReset() { + this._xvpOp(1, 4); + } + + // Send a key press. If 'down' is not specified then send a down key + // followed by an up key. + sendKey(keysym, code, down) { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + + if (down === undefined) { + this.sendKey(keysym, code, true); + this.sendKey(keysym, code, false); + return; } - _print_stats() { - const stats = this._encStats; + const scancode = XtScancode[code]; - Log.Info("Encoding stats for this connection:"); - Object.keys(stats).forEach((key) => { - const s = stats[key]; - if (s[0] + s[1] > 0) { - Log.Info(" " + encodingName(key) + ": " + s[0] + " rects"); - } - }); + if (this._qemuExtKeyEventSupported && scancode) { + // 0 is NoSymbol + keysym = keysym || 0; - Log.Info("Encoding stats since page load:"); - Object.keys(stats).forEach(key => Log.Info(" " + encodingName(key) + ": " + stats[key][1] + " rects")); + Log.Info('Sending key (' + (down ? 'down' : 'up') + '): keysym ' + keysym + ', scancode ' + scancode); + + RFB.messages.QEMUExtendedKeyEvent(this._sock, keysym, down, scancode); + } else { + if (!keysym) { + return; + } + Log.Info('Sending keysym (' + (down ? 'down' : 'up') + '): ' + keysym); + RFB.messages.keyEvent(this._sock, keysym, down ? 1 : 0); + } + } + + focus() { + this._canvas.focus(); + } + + blur() { + this._canvas.blur(); + } + + clipboardPasteFrom(text) { + if (this._rfb_connection_state !== 'connected' || this._viewOnly) { return; } + RFB.messages.clientCutText(this._sock, text); + } + + // ===== PRIVATE METHODS ===== + + _connect() { + Log.Debug('>> RFB.connect'); + + Log.Info('connecting to ' + this._url); + + try { + // WebSocket.onopen transitions to the RFB init states + this._sock.open(this._url, ['binary']); + } catch (e) { + if (e.name === 'SyntaxError') { + this._fail('Invalid host or port (' + e + ')'); + } else { + this._fail('Error when opening socket (' + e + ')'); + } } - _focusCanvas(event) { - // Respect earlier handlers' request to not do side-effects - if (event.defaultPrevented) { - return; - } + // Make our elements part of the page + this._target.appendChild(this._screen); - if (!this.focusOnClick) { - return; - } + this._cursor.attach(this._canvas); - this.focus(); + // Monitor size changes of the screen + // FIXME: Use ResizeObserver, or hidden overflow + window.addEventListener('resize', this._eventHandlers.windowResize); + + // Always grab focus on some kind of click event + this._canvas.addEventListener('mousedown', this._eventHandlers.focusCanvas); + this._canvas.addEventListener('touchstart', this._eventHandlers.focusCanvas); + + Log.Debug('<< RFB.connect'); + } + + _disconnect() { + Log.Debug('>> RFB.disconnect'); + this._cursor.detach(); + this._canvas.removeEventListener('mousedown', this._eventHandlers.focusCanvas); + this._canvas.removeEventListener('touchstart', this._eventHandlers.focusCanvas); + window.removeEventListener('resize', this._eventHandlers.windowResize); + this._keyboard.ungrab(); + this._mouse.ungrab(); + this._sock.close(); + this._print_stats(); + try { + this._target.removeChild(this._screen); + } catch (e) { + if (e.name === 'NotFoundError') { + // Some cases where the initial connection fails + // can disconnect before the _screen is created + } else { + throw e; + } + } + clearTimeout(this._resizeTimeout); + Log.Debug('<< RFB.disconnect'); + } + + _print_stats() { + const stats = this._encStats; + + Log.Info('Encoding stats for this connection:'); + Object.keys(stats).forEach((key) => { + const s = stats[key]; + if (s[0] + s[1] > 0) { + Log.Info(' ' + encodingName(key) + ': ' + s[0] + ' rects'); + } + }); + + Log.Info('Encoding stats since page load:'); + Object.keys(stats).forEach(key => Log.Info(' ' + encodingName(key) + ': ' + stats[key][1] + ' rects')); + } + + _focusCanvas(event) { + // Respect earlier handlers' request to not do side-effects + if (event.defaultPrevented) { + return; } - _windowResize(event) { - // If the window resized then our screen element might have - // as well. Update the viewport dimensions. - window.requestAnimationFrame(() => { - this._updateClip(); - this._updateScale(); - }); - - if (this._resizeSession) { - // Request changing the resolution of the remote display to - // the size of the local browser viewport. - - // In order to not send multiple requests before the browser-resize - // is finished we wait 0.5 seconds before sending the request. - clearTimeout(this._resizeTimeout); - this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); - } + if (!this.focusOnClick) { + return; } - // Update state of clipping in Display object, and make sure the - // configured viewport matches the current screen size - _updateClip() { - const cur_clip = this._display.clipViewport; - let new_clip = this._clipViewport; + this.focus(); + } - if (this._scaleViewport) { - // Disable viewport clipping if we are scaling - new_clip = false; - } + _windowResize(event) { + // If the window resized then our screen element might have + // as well. Update the viewport dimensions. + window.requestAnimationFrame(() => { + this._updateClip(); + this._updateScale(); + }); - if (cur_clip !== new_clip) { - this._display.clipViewport = new_clip; - } + if (this._resizeSession) { + // Request changing the resolution of the remote display to + // the size of the local browser viewport. - if (new_clip) { - // When clipping is enabled, the screen is limited to - // the size of the container. - const size = this._screenSize(); - this._display.viewportChangeSize(size.w, size.h); - this._fixScrollbars(); - } + // In order to not send multiple requests before the browser-resize + // is finished we wait 0.5 seconds before sending the request. + clearTimeout(this._resizeTimeout); + this._resizeTimeout = setTimeout(this._requestRemoteResize.bind(this), 500); + } + } + + // Update state of clipping in Display object, and make sure the + // configured viewport matches the current screen size + _updateClip() { + const cur_clip = this._display.clipViewport; + let new_clip = this._clipViewport; + + if (this._scaleViewport) { + // Disable viewport clipping if we are scaling + new_clip = false; } - _updateScale() { - if (!this._scaleViewport) { - this._display.scale = 1.0; - } else { - const size = this._screenSize(); - this._display.autoscale(size.w, size.h); - } - this._fixScrollbars(); + if (cur_clip !== new_clip) { + this._display.clipViewport = new_clip; } - // Requests a change of remote desktop size. This message is an extension - // and may only be sent if we have received an ExtendedDesktopSize message - _requestRemoteResize() { - clearTimeout(this._resizeTimeout); - this._resizeTimeout = null; + if (new_clip) { + // When clipping is enabled, the screen is limited to + // the size of the container. + const size = this._screenSize(); + this._display.viewportChangeSize(size.w, size.h); + this._fixScrollbars(); + } + } - if (!this._resizeSession || this._viewOnly || - !this._supportsSetDesktopSize) { - return; - } + _updateScale() { + if (!this._scaleViewport) { + this._display.scale = 1.0; + } else { + const size = this._screenSize(); + this._display.autoscale(size.w, size.h); + } + this._fixScrollbars(); + } - const size = this._screenSize(); - RFB.messages.setDesktopSize(this._sock, size.w, size.h, - this._screen_id, this._screen_flags); + // Requests a change of remote desktop size. This message is an extension + // and may only be sent if we have received an ExtendedDesktopSize message + _requestRemoteResize() { + clearTimeout(this._resizeTimeout); + this._resizeTimeout = null; - Log.Debug('Requested new desktop size: ' + - size.w + 'x' + size.h); + if (!this._resizeSession || this._viewOnly + || !this._supportsSetDesktopSize) { + return; } - // Gets the the size of the available screen - _screenSize() { - return { w: this._screen.offsetWidth, - h: this._screen.offsetHeight }; - } + const size = this._screenSize(); + RFB.messages.setDesktopSize(this._sock, size.w, size.h, + this._screen_id, this._screen_flags); - _fixScrollbars() { - // This is a hack because Chrome screws up the calculation - // for when scrollbars are needed. So to fix it we temporarily - // toggle them off and on. - const orig = this._screen.style.overflow; - this._screen.style.overflow = 'hidden'; - // Force Chrome to recalculate the layout by asking for - // an element's dimensions - this._screen.getBoundingClientRect(); - this._screen.style.overflow = orig; - } + Log.Debug('Requested new desktop size: ' + + size.w + 'x' + size.h); + } - /* + // Gets the the size of the available screen + _screenSize() { + return { + w: this._screen.offsetWidth, + h: this._screen.offsetHeight + }; + } + + _fixScrollbars() { + // This is a hack because Chrome screws up the calculation + // for when scrollbars are needed. So to fix it we temporarily + // toggle them off and on. + const orig = this._screen.style.overflow; + this._screen.style.overflow = 'hidden'; + // Force Chrome to recalculate the layout by asking for + // an element's dimensions + this._screen.getBoundingClientRect(); + this._screen.style.overflow = orig; + } + + /* * Connection states: * connecting * connected * disconnecting * disconnected - permanent state */ - _updateConnectionState(state) { - const oldstate = this._rfb_connection_state; + _updateConnectionState(state) { + const oldstate = this._rfb_connection_state; - if (state === oldstate) { - Log.Debug("Already in state '" + state + "', ignoring"); - return; - } - - // The 'disconnected' state is permanent for each RFB object - if (oldstate === 'disconnected') { - Log.Error("Tried changing state of a disconnected RFB object"); - return; - } - - // Ensure proper transitions before doing anything - switch (state) { - case 'connected': - if (oldstate !== 'connecting') { - Log.Error("Bad transition to connected state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'disconnected': - if (oldstate !== 'disconnecting') { - Log.Error("Bad transition to disconnected state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'connecting': - if (oldstate !== '') { - Log.Error("Bad transition to connecting state, " + - "previous connection state: " + oldstate); - return; - } - break; - - case 'disconnecting': - if (oldstate !== 'connected' && oldstate !== 'connecting') { - Log.Error("Bad transition to disconnecting state, " + - "previous connection state: " + oldstate); - return; - } - break; - - default: - Log.Error("Unknown connection state: " + state); - return; - } - - // State change actions - - this._rfb_connection_state = state; - - Log.Debug("New state '" + state + "', was '" + oldstate + "'."); - - if (this._disconnTimer && state !== 'disconnecting') { - Log.Debug("Clearing disconnect timer"); - clearTimeout(this._disconnTimer); - this._disconnTimer = null; - - // make sure we don't get a double event - this._sock.off('close'); - } - - switch (state) { - case 'connecting': - this._connect(); - break; - - case 'connected': - this.dispatchEvent(new CustomEvent("connect", { detail: {} })); - break; - - case 'disconnecting': - this._disconnect(); - - this._disconnTimer = setTimeout(() => { - Log.Error("Disconnection timed out."); - this._updateConnectionState('disconnected'); - }, DISCONNECT_TIMEOUT * 1000); - break; - - case 'disconnected': - this.dispatchEvent(new CustomEvent( - "disconnect", { detail: - { clean: this._rfb_clean_disconnect } })); - break; - } + if (state === oldstate) { + Log.Debug("Already in state '" + state + "', ignoring"); + return; } - /* Print errors and disconnect + // The 'disconnected' state is permanent for each RFB object + if (oldstate === 'disconnected') { + Log.Error('Tried changing state of a disconnected RFB object'); + return; + } + + // Ensure proper transitions before doing anything + switch (state) { + case 'connected': + if (oldstate !== 'connecting') { + Log.Error('Bad transition to connected state, ' + + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'disconnected': + if (oldstate !== 'disconnecting') { + Log.Error('Bad transition to disconnected state, ' + + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'connecting': + if (oldstate !== '') { + Log.Error('Bad transition to connecting state, ' + + 'previous connection state: ' + oldstate); + return; + } + break; + + case 'disconnecting': + if (oldstate !== 'connected' && oldstate !== 'connecting') { + Log.Error('Bad transition to disconnecting state, ' + + 'previous connection state: ' + oldstate); + return; + } + break; + + default: + Log.Error('Unknown connection state: ' + state); + return; + } + + // State change actions + + this._rfb_connection_state = state; + + Log.Debug("New state '" + state + "', was '" + oldstate + "'."); + + if (this._disconnTimer && state !== 'disconnecting') { + Log.Debug('Clearing disconnect timer'); + clearTimeout(this._disconnTimer); + this._disconnTimer = null; + + // make sure we don't get a double event + this._sock.off('close'); + } + + switch (state) { + case 'connecting': + this._connect(); + break; + + case 'connected': + this.dispatchEvent(new CustomEvent('connect', { detail: {} })); + break; + + case 'disconnecting': + this._disconnect(); + + this._disconnTimer = setTimeout(() => { + Log.Error('Disconnection timed out.'); + this._updateConnectionState('disconnected'); + }, DISCONNECT_TIMEOUT * 1000); + break; + + case 'disconnected': + this.dispatchEvent(new CustomEvent( + 'disconnect', { + detail: + { clean: this._rfb_clean_disconnect } + } + )); + break; + } + } + + /* Print errors and disconnect * * The parameter 'details' is used for information that * should be logged but not sent to the user interface. */ - _fail(details) { - switch (this._rfb_connection_state) { - case 'disconnecting': - Log.Error("Failed when disconnecting: " + details); - break; - case 'connected': - Log.Error("Failed while connected: " + details); - break; - case 'connecting': - Log.Error("Failed when connecting: " + details); - break; - default: - Log.Error("RFB failure: " + details); - break; - } - this._rfb_clean_disconnect = false; //This is sent to the UI + _fail(details) { + switch (this._rfb_connection_state) { + case 'disconnecting': + Log.Error('Failed when disconnecting: ' + details); + break; + case 'connected': + Log.Error('Failed while connected: ' + details); + break; + case 'connecting': + Log.Error('Failed when connecting: ' + details); + break; + default: + Log.Error('RFB failure: ' + details); + break; + } + this._rfb_clean_disconnect = false; // This is sent to the UI - // Transition to disconnected without waiting for socket to close - this._updateConnectionState('disconnecting'); - this._updateConnectionState('disconnected'); + // Transition to disconnected without waiting for socket to close + this._updateConnectionState('disconnecting'); + this._updateConnectionState('disconnected'); - return false; + return false; + } + + _setCapability(cap, val) { + this._capabilities[cap] = val; + this.dispatchEvent(new CustomEvent('capabilities', + { detail: { capabilities: this._capabilities } })); + } + + _handle_message() { + if (this._sock.rQlen() === 0) { + Log.Warn('handle_message called on an empty receive queue'); + return; } - _setCapability(cap, val) { - this._capabilities[cap] = val; - this.dispatchEvent(new CustomEvent("capabilities", - { detail: { capabilities: this._capabilities } })); + switch (this._rfb_connection_state) { + case 'disconnected': + Log.Error('Got data while disconnected'); + break; + case 'connected': + while (true) { + if (this._flushing) { + break; + } + if (!this._normal_msg()) { + break; + } + if (this._sock.rQlen() === 0) { + break; + } + } + break; + default: + this._init_msg(); + break; + } + } + + _handleKeyEvent(keysym, code, down) { + this.sendKey(keysym, code, down); + } + + _handleMouseButton(x, y, down, bmask) { + if (down) { + this._mouse_buttonMask |= bmask; + } else { + this._mouse_buttonMask &= ~bmask; } - _handle_message() { - if (this._sock.rQlen() === 0) { - Log.Warn("handle_message called on an empty receive queue"); - return; + if (this.dragViewport) { + if (down && !this._viewportDragging) { + this._viewportDragging = true; + this._viewportDragPos = { x: x, y: y }; + this._viewportHasMoved = false; + + // Skip sending mouse events + return; + } else { + this._viewportDragging = false; + + // If we actually performed a drag then we are done + // here and should not send any mouse events + if (this._viewportHasMoved) { + return; } - switch (this._rfb_connection_state) { - case 'disconnected': - Log.Error("Got data while disconnected"); - break; - case 'connected': - while (true) { - if (this._flushing) { - break; - } - if (!this._normal_msg()) { - break; - } - if (this._sock.rQlen() === 0) { - break; - } - } - break; - default: - this._init_msg(); - break; - } + // Otherwise we treat this as a mouse click event. + // Send the button down event here, as the button up + // event is sent at the end of this function. + RFB.messages.pointerEvent(this._sock, + this._display.absX(x), + this._display.absY(y), + bmask); + } } - _handleKeyEvent(keysym, code, down) { - this.sendKey(keysym, code, down); + if (this._viewOnly) { return; } // View only, skip mouse events + + if (this._rfb_connection_state !== 'connected') { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } + + _handleMouseMove(x, y) { + if (this._viewportDragging) { + const deltaX = this._viewportDragPos.x - x; + const deltaY = this._viewportDragPos.y - y; + + // The goal is to trigger on a certain physical width, the + // devicePixelRatio brings us a bit closer but is not optimal. + const dragThreshold = 10 * (window.devicePixelRatio || 1); + + if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold + || Math.abs(deltaY) > dragThreshold)) { + this._viewportHasMoved = true; + + this._viewportDragPos = { x: x, y: y }; + this._display.viewportChangePos(deltaX, deltaY); + } + + // Skip sending mouse events + return; } - _handleMouseButton(x, y, down, bmask) { - if (down) { - this._mouse_buttonMask |= bmask; - } else { - this._mouse_buttonMask &= ~bmask; - } + if (this._viewOnly) { return; } // View only, skip mouse events - if (this.dragViewport) { - if (down && !this._viewportDragging) { - this._viewportDragging = true; - this._viewportDragPos = {'x': x, 'y': y}; - this._viewportHasMoved = false; + if (this._rfb_connection_state !== 'connected') { return; } + RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + } - // Skip sending mouse events - return; - } else { - this._viewportDragging = false; + // Message Handlers - // If we actually performed a drag then we are done - // here and should not send any mouse events - if (this._viewportHasMoved) { - return; - } - - // Otherwise we treat this as a mouse click event. - // Send the button down event here, as the button up - // event is sent at the end of this function. - RFB.messages.pointerEvent(this._sock, - this._display.absX(x), - this._display.absY(y), - bmask); - } - } - - if (this._viewOnly) { return; } // View only, skip mouse events - - if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + _negotiate_protocol_version() { + if (this._sock.rQlen() < 12) { + return this._fail('Received incomplete protocol version.'); } - _handleMouseMove(x, y) { - if (this._viewportDragging) { - const deltaX = this._viewportDragPos.x - x; - const deltaY = this._viewportDragPos.y - y; - - // The goal is to trigger on a certain physical width, the - // devicePixelRatio brings us a bit closer but is not optimal. - const dragThreshold = 10 * (window.devicePixelRatio || 1); - - if (this._viewportHasMoved || (Math.abs(deltaX) > dragThreshold || - Math.abs(deltaY) > dragThreshold)) { - this._viewportHasMoved = true; - - this._viewportDragPos = {'x': x, 'y': y}; - this._display.viewportChangePos(deltaX, deltaY); - } - - // Skip sending mouse events - return; - } - - if (this._viewOnly) { return; } // View only, skip mouse events - - if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + const sversion = this._sock.rQshiftStr(12).substr(4, 7); + Log.Info('Server ProtocolVersion: ' + sversion); + let is_repeater = 0; + switch (sversion) { + case '000.000': // UltraVNC repeater + is_repeater = 1; + break; + case '003.003': + case '003.006': // UltraVNC + case '003.889': // Apple Remote Desktop + this._rfb_version = 3.3; + break; + case '003.007': + this._rfb_version = 3.7; + break; + case '003.008': + case '004.000': // Intel AMT KVM + case '004.001': // RealVNC 4.6 + case '005.000': // RealVNC 5.3 + this._rfb_version = 3.8; + break; + default: + return this._fail('Invalid server version ' + sversion); } - // Message Handlers - - _negotiate_protocol_version() { - if (this._sock.rQlen() < 12) { - return this._fail("Received incomplete protocol version."); - } - - const sversion = this._sock.rQshiftStr(12).substr(4, 7); - Log.Info("Server ProtocolVersion: " + sversion); - let is_repeater = 0; - switch (sversion) { - case "000.000": // UltraVNC repeater - is_repeater = 1; - break; - case "003.003": - case "003.006": // UltraVNC - case "003.889": // Apple Remote Desktop - this._rfb_version = 3.3; - break; - case "003.007": - this._rfb_version = 3.7; - break; - case "003.008": - case "004.000": // Intel AMT KVM - case "004.001": // RealVNC 4.6 - case "005.000": // RealVNC 5.3 - this._rfb_version = 3.8; - break; - default: - return this._fail("Invalid server version " + sversion); - } - - if (is_repeater) { - let repeaterID = "ID:" + this._repeaterID; - while (repeaterID.length < 250) { - repeaterID += "\0"; - } - this._sock.send_string(repeaterID); - return true; - } - - if (this._rfb_version > this._rfb_max_version) { - this._rfb_version = this._rfb_max_version; - } - - const cversion = "00" + parseInt(this._rfb_version, 10) + - ".00" + ((this._rfb_version * 10) % 10); - this._sock.send_string("RFB " + cversion + "\n"); - Log.Debug('Sent ProtocolVersion: ' + cversion); - - this._rfb_init_state = 'Security'; + if (is_repeater) { + let repeaterID = 'ID:' + this._repeaterID; + while (repeaterID.length < 250) { + repeaterID += '\0'; + } + this._sock.send_string(repeaterID); + return true; } - _negotiate_security() { - // Polyfill since IE and PhantomJS doesn't have - // TypedArray.includes() - function includes(item, array) { - for (let i = 0; i < array.length; i++) { - if (array[i] === item) { - return true; - } - } - return false; - } - - if (this._rfb_version >= 3.7) { - // Server sends supported list, client decides - const num_types = this._sock.rQshift8(); - if (this._sock.rQwait("security type", num_types, 1)) { return false; } - - if (num_types === 0) { - return this._handle_security_failure("no security types"); - } - - const types = this._sock.rQshiftBytes(num_types); - Log.Debug("Server security types: " + types); - - // Look for each auth in preferred order - this._rfb_auth_scheme = 0; - if (includes(1, types)) { - this._rfb_auth_scheme = 1; // None - } else if (includes(22, types)) { - this._rfb_auth_scheme = 22; // XVP - } else if (includes(16, types)) { - this._rfb_auth_scheme = 16; // Tight - } else if (includes(2, types)) { - this._rfb_auth_scheme = 2; // VNC Auth - } else { - return this._fail("Unsupported security types (types: " + types + ")"); - } - - this._sock.send([this._rfb_auth_scheme]); - } else { - // Server decides - if (this._sock.rQwait("security scheme", 4)) { return false; } - this._rfb_auth_scheme = this._sock.rQshift32(); - } - - this._rfb_init_state = 'Authentication'; - Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); - - return this._init_msg(); // jump to authentication + if (this._rfb_version > this._rfb_max_version) { + this._rfb_version = this._rfb_max_version; } - /* + const cversion = '00' + parseInt(this._rfb_version, 10) + + '.00' + ((this._rfb_version * 10) % 10); + this._sock.send_string('RFB ' + cversion + '\n'); + Log.Debug('Sent ProtocolVersion: ' + cversion); + + this._rfb_init_state = 'Security'; + } + + _negotiate_security() { + // Polyfill since IE and PhantomJS doesn't have + // TypedArray.includes() + function includes(item, array) { + for (let i = 0; i < array.length; i++) { + if (array[i] === item) { + return true; + } + } + return false; + } + + if (this._rfb_version >= 3.7) { + // Server sends supported list, client decides + const num_types = this._sock.rQshift8(); + if (this._sock.rQwait('security type', num_types, 1)) { return false; } + + if (num_types === 0) { + return this._handle_security_failure('no security types'); + } + + const types = this._sock.rQshiftBytes(num_types); + Log.Debug('Server security types: ' + types); + + // Look for each auth in preferred order + this._rfb_auth_scheme = 0; + if (includes(1, types)) { + this._rfb_auth_scheme = 1; // None + } else if (includes(22, types)) { + this._rfb_auth_scheme = 22; // XVP + } else if (includes(16, types)) { + this._rfb_auth_scheme = 16; // Tight + } else if (includes(2, types)) { + this._rfb_auth_scheme = 2; // VNC Auth + } else { + return this._fail('Unsupported security types (types: ' + types + ')'); + } + + this._sock.send([this._rfb_auth_scheme]); + } else { + // Server decides + if (this._sock.rQwait('security scheme', 4)) { return false; } + this._rfb_auth_scheme = this._sock.rQshift32(); + } + + this._rfb_init_state = 'Authentication'; + Log.Debug('Authenticating using scheme: ' + this._rfb_auth_scheme); + + return this._init_msg(); // jump to authentication + } + + /* * Get the security failure reason if sent from the server and * send the 'securityfailure' event. * @@ -921,375 +931,378 @@ export default class RFB extends EventTargetMixin { * - The optional parameter security_result_status can be used to * add a custom status code to the event. */ - _handle_security_failure(context, security_result_status) { - - if (typeof context === 'undefined') { - context = ""; - } else { - context = " on " + context; - } - - if (typeof security_result_status === 'undefined') { - security_result_status = 1; // fail - } - - if (this._sock.rQwait("reason length", 4)) { - return false; - } - const strlen = this._sock.rQshift32(); - let reason = ""; - - if (strlen > 0) { - if (this._sock.rQwait("reason", strlen, 8)) { return false; } - reason = this._sock.rQshiftStr(strlen); - } - - if (reason !== "") { - this.dispatchEvent(new CustomEvent( - "securityfailure", - { detail: { status: security_result_status, reason: reason } })); - - return this._fail("Security negotiation failed" + context + - " (reason: " + reason + ")"); - } else { - this.dispatchEvent(new CustomEvent( - "securityfailure", - { detail: { status: security_result_status } })); - - return this._fail("Security negotiation failed" + context); - } + _handle_security_failure(context, security_result_status) { + if (typeof context === 'undefined') { + context = ''; + } else { + context = ' on ' + context; } - // authentication - _negotiate_xvp_auth() { - if (!this._rfb_credentials.username || - !this._rfb_credentials.password || - !this._rfb_credentials.target) { - this.dispatchEvent(new CustomEvent( - "credentialsrequired", - { detail: { types: ["username", "password", "target"] } })); - return false; - } - - const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) + - String.fromCharCode(this._rfb_credentials.target.length) + - this._rfb_credentials.username + - this._rfb_credentials.target; - this._sock.send_string(xvp_auth_str); - this._rfb_auth_scheme = 2; - return this._negotiate_authentication(); + if (typeof security_result_status === 'undefined') { + security_result_status = 1; // fail } - _negotiate_std_vnc_auth() { - if (this._sock.rQwait("auth challenge", 16)) { return false; } + if (this._sock.rQwait('reason length', 4)) { + return false; + } + const strlen = this._sock.rQshift32(); + let reason = ''; - if (!this._rfb_credentials.password) { - this.dispatchEvent(new CustomEvent( - "credentialsrequired", - { detail: { types: ["password"] } })); - return false; - } - - // TODO(directxman12): make genDES not require an Array - const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); - const response = RFB.genDES(this._rfb_credentials.password, challenge); - this._sock.send(response); - this._rfb_init_state = "SecurityResult"; - return true; + if (strlen > 0) { + if (this._sock.rQwait('reason', strlen, 8)) { return false; } + reason = this._sock.rQshiftStr(strlen); } - _negotiate_tight_tunnels(numTunnels) { - const clientSupportedTunnelTypes = { - 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } - }; - const serverSupportedTunnelTypes = {}; - // receive tunnel capabilities - for (let i = 0; i < numTunnels; i++) { - const cap_code = this._sock.rQshift32(); - const cap_vendor = this._sock.rQshiftStr(4); - const cap_signature = this._sock.rQshiftStr(8); - serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; - } + if (reason !== '') { + this.dispatchEvent(new CustomEvent( + 'securityfailure', + { detail: { status: security_result_status, reason: reason } } + )); - Log.Debug("Server Tight tunnel types: " + serverSupportedTunnelTypes); + return this._fail('Security negotiation failed' + context + + ' (reason: ' + reason + ')'); + } else { + this.dispatchEvent(new CustomEvent( + 'securityfailure', + { detail: { status: security_result_status } } + )); - // Siemens touch panels have a VNC server that supports NOTUNNEL, - // but forgets to advertise it. Try to detect such servers by - // looking for their custom tunnel type. - if (serverSupportedTunnelTypes[1] && - (serverSupportedTunnelTypes[1].vendor === "SICR") && - (serverSupportedTunnelTypes[1].signature === "SCHANNEL")) { - Log.Debug("Detected Siemens server. Assuming NOTUNNEL support."); - serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' }; - } + return this._fail('Security negotiation failed' + context); + } + } - // choose the notunnel type - if (serverSupportedTunnelTypes[0]) { - if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor || - serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { - return this._fail("Client's tunnel type had the incorrect " + - "vendor or signature"); - } - Log.Debug("Selected tunnel type: " + clientSupportedTunnelTypes[0]); - this._sock.send([0, 0, 0, 0]); // use NOTUNNEL - return false; // wait until we receive the sub auth count to continue - } else { - return this._fail("Server wanted tunnels, but doesn't support " + - "the notunnel type"); - } + // authentication + _negotiate_xvp_auth() { + if (!this._rfb_credentials.username + || !this._rfb_credentials.password + || !this._rfb_credentials.target) { + this.dispatchEvent(new CustomEvent( + 'credentialsrequired', + { detail: { types: ['username', 'password', 'target'] } } + )); + return false; } - _negotiate_tight_auth() { - if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation - if (this._sock.rQwait("num tunnels", 4)) { return false; } - const numTunnels = this._sock.rQshift32(); - if (numTunnels > 0 && this._sock.rQwait("tunnel capabilities", 16 * numTunnels, 4)) { return false; } + const xvp_auth_str = String.fromCharCode(this._rfb_credentials.username.length) + + String.fromCharCode(this._rfb_credentials.target.length) + + this._rfb_credentials.username + + this._rfb_credentials.target; + this._sock.send_string(xvp_auth_str); + this._rfb_auth_scheme = 2; + return this._negotiate_authentication(); + } - this._rfb_tightvnc = true; + _negotiate_std_vnc_auth() { + if (this._sock.rQwait('auth challenge', 16)) { return false; } - if (numTunnels > 0) { - this._negotiate_tight_tunnels(numTunnels); - return false; // wait until we receive the sub auth to continue - } - } + if (!this._rfb_credentials.password) { + this.dispatchEvent(new CustomEvent( + 'credentialsrequired', + { detail: { types: ['password'] } } + )); + return false; + } - // second pass, do the sub-auth negotiation - if (this._sock.rQwait("sub auth count", 4)) { return false; } - const subAuthCount = this._sock.rQshift32(); - if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected + // TODO(directxman12): make genDES not require an Array + const challenge = Array.prototype.slice.call(this._sock.rQshiftBytes(16)); + const response = RFB.genDES(this._rfb_credentials.password, challenge); + this._sock.send(response); + this._rfb_init_state = 'SecurityResult'; + return true; + } + + _negotiate_tight_tunnels(numTunnels) { + const clientSupportedTunnelTypes = { + 0: { vendor: 'TGHT', signature: 'NOTUNNEL' } + }; + const serverSupportedTunnelTypes = {}; + // receive tunnel capabilities + for (let i = 0; i < numTunnels; i++) { + const cap_code = this._sock.rQshift32(); + const cap_vendor = this._sock.rQshiftStr(4); + const cap_signature = this._sock.rQshiftStr(8); + serverSupportedTunnelTypes[cap_code] = { vendor: cap_vendor, signature: cap_signature }; + } + + Log.Debug('Server Tight tunnel types: ' + serverSupportedTunnelTypes); + + // Siemens touch panels have a VNC server that supports NOTUNNEL, + // but forgets to advertise it. Try to detect such servers by + // looking for their custom tunnel type. + if (serverSupportedTunnelTypes[1] + && (serverSupportedTunnelTypes[1].vendor === 'SICR') + && (serverSupportedTunnelTypes[1].signature === 'SCHANNEL')) { + Log.Debug('Detected Siemens server. Assuming NOTUNNEL support.'); + serverSupportedTunnelTypes[0] = { vendor: 'TGHT', signature: 'NOTUNNEL' }; + } + + // choose the notunnel type + if (serverSupportedTunnelTypes[0]) { + if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor + || serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + return this._fail("Client's tunnel type had the incorrect " + + 'vendor or signature'); + } + Log.Debug('Selected tunnel type: ' + clientSupportedTunnelTypes[0]); + this._sock.send([0, 0, 0, 0]); // use NOTUNNEL + return false; // wait until we receive the sub auth count to continue + } else { + return this._fail("Server wanted tunnels, but doesn't support " + + 'the notunnel type'); + } + } + + _negotiate_tight_auth() { + if (!this._rfb_tightvnc) { // first pass, do the tunnel negotiation + if (this._sock.rQwait('num tunnels', 4)) { return false; } + const numTunnels = this._sock.rQshift32(); + if (numTunnels > 0 && this._sock.rQwait('tunnel capabilities', 16 * numTunnels, 4)) { return false; } + + this._rfb_tightvnc = true; + + if (numTunnels > 0) { + this._negotiate_tight_tunnels(numTunnels); + return false; // wait until we receive the sub auth to continue + } + } + + // second pass, do the sub-auth negotiation + if (this._sock.rQwait('sub auth count', 4)) { return false; } + const subAuthCount = this._sock.rQshift32(); + if (subAuthCount === 0) { // empty sub-auth list received means 'no auth' subtype selected + this._rfb_init_state = 'SecurityResult'; + return true; + } + + if (this._sock.rQwait('sub auth capabilities', 16 * subAuthCount, 4)) { return false; } + + const clientSupportedTypes = { + STDVNOAUTH__: 1, + STDVVNCAUTH_: 2 + }; + + const serverSupportedTypes = []; + + for (let i = 0; i < subAuthCount; i++) { + this._sock.rQshift32(); // capNum + const capabilities = this._sock.rQshiftStr(12); + serverSupportedTypes.push(capabilities); + } + + Log.Debug('Server Tight authentication types: ' + serverSupportedTypes); + + for (let authType in clientSupportedTypes) { + if (serverSupportedTypes.indexOf(authType) != -1) { + this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); + Log.Debug('Selected authentication type: ' + authType); + + switch (authType) { + case 'STDVNOAUTH__': // no auth this._rfb_init_state = 'SecurityResult'; return true; - } - - if (this._sock.rQwait("sub auth capabilities", 16 * subAuthCount, 4)) { return false; } - - const clientSupportedTypes = { - 'STDVNOAUTH__': 1, - 'STDVVNCAUTH_': 2 - }; - - const serverSupportedTypes = []; - - for (let i = 0; i < subAuthCount; i++) { - this._sock.rQshift32(); // capNum - const capabilities = this._sock.rQshiftStr(12); - serverSupportedTypes.push(capabilities); - } - - Log.Debug("Server Tight authentication types: " + serverSupportedTypes); - - for (let authType in clientSupportedTypes) { - if (serverSupportedTypes.indexOf(authType) != -1) { - this._sock.send([0, 0, 0, clientSupportedTypes[authType]]); - Log.Debug("Selected authentication type: " + authType); - - switch (authType) { - case 'STDVNOAUTH__': // no auth - this._rfb_init_state = 'SecurityResult'; - return true; - case 'STDVVNCAUTH_': // VNC auth - this._rfb_auth_scheme = 2; - return this._init_msg(); - default: - return this._fail("Unsupported tiny auth scheme " + - "(scheme: " + authType + ")"); - } - } - } - - return this._fail("No supported sub-auth types!"); - } - - _negotiate_authentication() { - switch (this._rfb_auth_scheme) { - case 0: // connection failed - return this._handle_security_failure("authentication scheme"); - - case 1: // no auth - if (this._rfb_version >= 3.8) { - this._rfb_init_state = 'SecurityResult'; - return true; - } - this._rfb_init_state = 'ClientInitialisation'; - return this._init_msg(); - - case 22: // XVP auth - return this._negotiate_xvp_auth(); - - case 2: // VNC authentication - return this._negotiate_std_vnc_auth(); - - case 16: // TightVNC Security Type - return this._negotiate_tight_auth(); - - default: - return this._fail("Unsupported auth scheme (scheme: " + - this._rfb_auth_scheme + ")"); - } - } - - _handle_security_result() { - if (this._sock.rQwait('VNC auth response ', 4)) { return false; } - - const status = this._sock.rQshift32(); - - if (status === 0) { // OK - this._rfb_init_state = 'ClientInitialisation'; - Log.Debug('Authentication OK'); + case 'STDVVNCAUTH_': // VNC auth + this._rfb_auth_scheme = 2; return this._init_msg(); - } else { - if (this._rfb_version >= 3.8) { - return this._handle_security_failure("security result", status); - } else { - this.dispatchEvent(new CustomEvent( - "securityfailure", - { detail: { status: status } })); - - return this._fail("Security handshake failed"); - } + default: + return this._fail('Unsupported tiny auth scheme ' + + '(scheme: ' + authType + ')'); } + } } - _negotiate_server_init() { - if (this._sock.rQwait("server initialization", 24)) { return false; } + return this._fail('No supported sub-auth types!'); + } - /* Screen size */ - const width = this._sock.rQshift16(); - const height = this._sock.rQshift16(); + _negotiate_authentication() { + switch (this._rfb_auth_scheme) { + case 0: // connection failed + return this._handle_security_failure('authentication scheme'); - /* PIXEL_FORMAT */ - const bpp = this._sock.rQshift8(); - const depth = this._sock.rQshift8(); - const big_endian = this._sock.rQshift8(); - const true_color = this._sock.rQshift8(); - - const red_max = this._sock.rQshift16(); - const green_max = this._sock.rQshift16(); - const blue_max = this._sock.rQshift16(); - const red_shift = this._sock.rQshift8(); - const green_shift = this._sock.rQshift8(); - const blue_shift = this._sock.rQshift8(); - this._sock.rQskipBytes(3); // padding - - // NB(directxman12): we don't want to call any callbacks or print messages until - // *after* we're past the point where we could backtrack - - /* Connection name/title */ - const name_length = this._sock.rQshift32(); - if (this._sock.rQwait('server init name', name_length, 24)) { return false; } - this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length)); - - if (this._rfb_tightvnc) { - if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } - // In TightVNC mode, ServerInit message is extended - const numServerMessages = this._sock.rQshift16(); - const numClientMessages = this._sock.rQshift16(); - const numEncodings = this._sock.rQshift16(); - this._sock.rQskipBytes(2); // padding - - const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; - if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } - - // we don't actually do anything with the capability information that TIGHT sends, - // so we just skip the all of this. - - // TIGHT server message capabilities - this._sock.rQskipBytes(16 * numServerMessages); - - // TIGHT client message capabilities - this._sock.rQskipBytes(16 * numClientMessages); - - // TIGHT encoding capabilities - this._sock.rQskipBytes(16 * numEncodings); + case 1: // no auth + if (this._rfb_version >= 3.8) { + this._rfb_init_state = 'SecurityResult'; + return true; } + this._rfb_init_state = 'ClientInitialisation'; + return this._init_msg(); - // NB(directxman12): these are down here so that we don't run them multiple times - // if we backtrack - Log.Info("Screen: " + width + "x" + height + - ", bpp: " + bpp + ", depth: " + depth + - ", big_endian: " + big_endian + - ", true_color: " + true_color + - ", red_max: " + red_max + - ", green_max: " + green_max + - ", blue_max: " + blue_max + - ", red_shift: " + red_shift + - ", green_shift: " + green_shift + - ", blue_shift: " + blue_shift); + case 22: // XVP auth + return this._negotiate_xvp_auth(); - if (big_endian !== 0) { - Log.Warn("Server native endian is not little endian"); - } + case 2: // VNC authentication + return this._negotiate_std_vnc_auth(); - if (red_shift !== 16) { - Log.Warn("Server native red-shift is not 16"); - } + case 16: // TightVNC Security Type + return this._negotiate_tight_auth(); - if (blue_shift !== 0) { - Log.Warn("Server native blue-shift is not 0"); - } + default: + return this._fail('Unsupported auth scheme (scheme: ' + + this._rfb_auth_scheme + ')'); + } + } - // we're past the point where we could backtrack, so it's safe to call this - this.dispatchEvent(new CustomEvent( - "desktopname", - { detail: { name: this._fb_name } })); + _handle_security_result() { + if (this._sock.rQwait('VNC auth response ', 4)) { return false; } - this._resize(width, height); + const status = this._sock.rQshift32(); - if (!this._viewOnly) { this._keyboard.grab(); } - if (!this._viewOnly) { this._mouse.grab(); } + if (status === 0) { // OK + this._rfb_init_state = 'ClientInitialisation'; + Log.Debug('Authentication OK'); + return this._init_msg(); + } else if (this._rfb_version >= 3.8) { + return this._handle_security_failure('security result', status); + } else { + this.dispatchEvent(new CustomEvent( + 'securityfailure', + { detail: { status: status } } + )); - this._fb_depth = 24; + return this._fail('Security handshake failed'); + } + } - if (this._fb_name === "Intel(r) AMT KVM") { - Log.Warn("Intel AMT KVM only supports 8/16 bit depths. Using low color mode."); - this._fb_depth = 8; - } + _negotiate_server_init() { + if (this._sock.rQwait('server initialization', 24)) { return false; } - RFB.messages.pixelFormat(this._sock, this._fb_depth, true); - this._sendEncodings(); - RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); + /* Screen size */ + const width = this._sock.rQshift16(); + const height = this._sock.rQshift16(); - this._timing.fbu_rt_start = (new Date()).getTime(); - this._timing.pixels = 0; + /* PIXEL_FORMAT */ + const bpp = this._sock.rQshift8(); + const depth = this._sock.rQshift8(); + const big_endian = this._sock.rQshift8(); + const true_color = this._sock.rQshift8(); - this._updateConnectionState('connected'); - return true; + const red_max = this._sock.rQshift16(); + const green_max = this._sock.rQshift16(); + const blue_max = this._sock.rQshift16(); + const red_shift = this._sock.rQshift8(); + const green_shift = this._sock.rQshift8(); + const blue_shift = this._sock.rQshift8(); + this._sock.rQskipBytes(3); // padding + + // NB(directxman12): we don't want to call any callbacks or print messages until + // *after* we're past the point where we could backtrack + + /* Connection name/title */ + const name_length = this._sock.rQshift32(); + if (this._sock.rQwait('server init name', name_length, 24)) { return false; } + this._fb_name = decodeUTF8(this._sock.rQshiftStr(name_length)); + + if (this._rfb_tightvnc) { + if (this._sock.rQwait('TightVNC extended server init header', 8, 24 + name_length)) { return false; } + // In TightVNC mode, ServerInit message is extended + const numServerMessages = this._sock.rQshift16(); + const numClientMessages = this._sock.rQshift16(); + const numEncodings = this._sock.rQshift16(); + this._sock.rQskipBytes(2); // padding + + const totalMessagesLength = (numServerMessages + numClientMessages + numEncodings) * 16; + if (this._sock.rQwait('TightVNC extended server init header', totalMessagesLength, 32 + name_length)) { return false; } + + // we don't actually do anything with the capability information that TIGHT sends, + // so we just skip the all of this. + + // TIGHT server message capabilities + this._sock.rQskipBytes(16 * numServerMessages); + + // TIGHT client message capabilities + this._sock.rQskipBytes(16 * numClientMessages); + + // TIGHT encoding capabilities + this._sock.rQskipBytes(16 * numEncodings); } - _sendEncodings() { - const encs = []; + // NB(directxman12): these are down here so that we don't run them multiple times + // if we backtrack + Log.Info('Screen: ' + width + 'x' + height + + ', bpp: ' + bpp + ', depth: ' + depth + + ', big_endian: ' + big_endian + + ', true_color: ' + true_color + + ', red_max: ' + red_max + + ', green_max: ' + green_max + + ', blue_max: ' + blue_max + + ', red_shift: ' + red_shift + + ', green_shift: ' + green_shift + + ', blue_shift: ' + blue_shift); - // In preference order - encs.push(encodings.encodingCopyRect); - // Only supported with full depth support - if (this._fb_depth == 24) { - encs.push(encodings.encodingTight); - encs.push(encodings.encodingTightPNG); - encs.push(encodings.encodingHextile); - encs.push(encodings.encodingRRE); - } - encs.push(encodings.encodingRaw); - - // Psuedo-encoding settings - encs.push(encodings.pseudoEncodingQualityLevel0 + 6); - encs.push(encodings.pseudoEncodingCompressLevel0 + 2); - - encs.push(encodings.pseudoEncodingDesktopSize); - encs.push(encodings.pseudoEncodingLastRect); - encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); - encs.push(encodings.pseudoEncodingExtendedDesktopSize); - encs.push(encodings.pseudoEncodingXvp); - encs.push(encodings.pseudoEncodingFence); - encs.push(encodings.pseudoEncodingContinuousUpdates); - - if (this._fb_depth == 24) { - encs.push(encodings.pseudoEncodingCursor); - } - - RFB.messages.clientEncodings(this._sock, encs); + if (big_endian !== 0) { + Log.Warn('Server native endian is not little endian'); } - /* RFB protocol initialization states: + if (red_shift !== 16) { + Log.Warn('Server native red-shift is not 16'); + } + + if (blue_shift !== 0) { + Log.Warn('Server native blue-shift is not 0'); + } + + // we're past the point where we could backtrack, so it's safe to call this + this.dispatchEvent(new CustomEvent( + 'desktopname', + { detail: { name: this._fb_name } } + )); + + this._resize(width, height); + + if (!this._viewOnly) { this._keyboard.grab(); } + if (!this._viewOnly) { this._mouse.grab(); } + + this._fb_depth = 24; + + if (this._fb_name === 'Intel(r) AMT KVM') { + Log.Warn('Intel AMT KVM only supports 8/16 bit depths. Using low color mode.'); + this._fb_depth = 8; + } + + RFB.messages.pixelFormat(this._sock, this._fb_depth, true); + this._sendEncodings(); + RFB.messages.fbUpdateRequest(this._sock, false, 0, 0, this._fb_width, this._fb_height); + + this._timing.fbu_rt_start = (new Date()).getTime(); + this._timing.pixels = 0; + + this._updateConnectionState('connected'); + return true; + } + + _sendEncodings() { + const encs = []; + + // In preference order + encs.push(encodings.encodingCopyRect); + // Only supported with full depth support + if (this._fb_depth == 24) { + encs.push(encodings.encodingTight); + encs.push(encodings.encodingTightPNG); + encs.push(encodings.encodingHextile); + encs.push(encodings.encodingRRE); + } + encs.push(encodings.encodingRaw); + + // Psuedo-encoding settings + encs.push(encodings.pseudoEncodingQualityLevel0 + 6); + encs.push(encodings.pseudoEncodingCompressLevel0 + 2); + + encs.push(encodings.pseudoEncodingDesktopSize); + encs.push(encodings.pseudoEncodingLastRect); + encs.push(encodings.pseudoEncodingQEMUExtendedKeyEvent); + encs.push(encodings.pseudoEncodingExtendedDesktopSize); + encs.push(encodings.pseudoEncodingXvp); + encs.push(encodings.pseudoEncodingFence); + encs.push(encodings.pseudoEncodingContinuousUpdates); + + if (this._fb_depth == 24) { + encs.push(encodings.pseudoEncodingCursor); + } + + RFB.messages.clientEncodings(this._sock, encs); + } + + /* RFB protocol initialization states: * ProtocolVersion * Security * Authentication @@ -1297,77 +1310,78 @@ export default class RFB extends EventTargetMixin { * ClientInitialization - not triggered by server message * ServerInitialization */ - _init_msg() { - switch (this._rfb_init_state) { - case 'ProtocolVersion': - return this._negotiate_protocol_version(); + _init_msg() { + switch (this._rfb_init_state) { + case 'ProtocolVersion': + return this._negotiate_protocol_version(); - case 'Security': - return this._negotiate_security(); + case 'Security': + return this._negotiate_security(); - case 'Authentication': - return this._negotiate_authentication(); + case 'Authentication': + return this._negotiate_authentication(); - case 'SecurityResult': - return this._handle_security_result(); - - case 'ClientInitialisation': - this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation - this._rfb_init_state = 'ServerInitialisation'; - return true; - - case 'ServerInitialisation': - return this._negotiate_server_init(); - - default: - return this._fail("Unknown init state (state: " + - this._rfb_init_state + ")"); - } - } - - _handle_set_colour_map_msg() { - Log.Debug("SetColorMapEntries"); - - return this._fail("Unexpected SetColorMapEntries message"); - } - - _handle_server_cut_text() { - Log.Debug("ServerCutText"); - - if (this._sock.rQwait("ServerCutText header", 7, 1)) { return false; } - this._sock.rQskipBytes(3); // Padding - const length = this._sock.rQshift32(); - if (this._sock.rQwait("ServerCutText", length, 8)) { return false; } - - const text = this._sock.rQshiftStr(length); - - if (this._viewOnly) { return true; } - - this.dispatchEvent(new CustomEvent( - "clipboard", - { detail: { text: text } })); + case 'SecurityResult': + return this._handle_security_result(); + case 'ClientInitialisation': + this._sock.send([this._shared ? 1 : 0]); // ClientInitialisation + this._rfb_init_state = 'ServerInitialisation'; return true; + + case 'ServerInitialisation': + return this._negotiate_server_init(); + + default: + return this._fail('Unknown init state (state: ' + + this._rfb_init_state + ')'); + } + } + + _handle_set_colour_map_msg() { + Log.Debug('SetColorMapEntries'); + + return this._fail('Unexpected SetColorMapEntries message'); + } + + _handle_server_cut_text() { + Log.Debug('ServerCutText'); + + if (this._sock.rQwait('ServerCutText header', 7, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + const length = this._sock.rQshift32(); + if (this._sock.rQwait('ServerCutText', length, 8)) { return false; } + + const text = this._sock.rQshiftStr(length); + + if (this._viewOnly) { return true; } + + this.dispatchEvent(new CustomEvent( + 'clipboard', + { detail: { text: text } } + )); + + return true; + } + + _handle_server_fence_msg() { + if (this._sock.rQwait('ServerFence header', 8, 1)) { return false; } + this._sock.rQskipBytes(3); // Padding + let flags = this._sock.rQshift32(); + let length = this._sock.rQshift8(); + + if (this._sock.rQwait('ServerFence payload', length, 9)) { return false; } + + if (length > 64) { + Log.Warn('Bad payload length (' + length + ') in fence response'); + length = 64; } - _handle_server_fence_msg() { - if (this._sock.rQwait("ServerFence header", 8, 1)) { return false; } - this._sock.rQskipBytes(3); // Padding - let flags = this._sock.rQshift32(); - let length = this._sock.rQshift8(); + const payload = this._sock.rQshiftStr(length); - if (this._sock.rQwait("ServerFence payload", length, 9)) { return false; } + this._supportsFence = true; - if (length > 64) { - Log.Warn("Bad payload length (" + length + ") in fence response"); - length = 64; - } - - const payload = this._sock.rQshiftStr(length); - - this._supportsFence = true; - - /* + /* * Fence flags * * (1<<0) - BlockBefore @@ -1376,793 +1390,794 @@ export default class RFB extends EventTargetMixin { * (1<<31) - Request */ - if (!(flags & (1<<31))) { - return this._fail("Unexpected fence response"); - } - - // Filter out unsupported flags - // FIXME: support syncNext - flags &= (1<<0) | (1<<1); - - // BlockBefore and BlockAfter are automatically handled by - // the fact that we process each incoming message - // synchronuosly. - RFB.messages.clientFence(this._sock, flags, payload); - - return true; + if (!(flags & (1 << 31))) { + return this._fail('Unexpected fence response'); } - _handle_xvp_msg() { - if (this._sock.rQwait("XVP version and message", 3, 1)) { return false; } - this._sock.rQskip8(); // Padding - const xvp_ver = this._sock.rQshift8(); - const xvp_msg = this._sock.rQshift8(); + // Filter out unsupported flags + // FIXME: support syncNext + flags &= (1 << 0) | (1 << 1); - switch (xvp_msg) { - case 0: // XVP_FAIL - Log.Error("XVP Operation Failed"); - break; - case 1: // XVP_INIT - this._rfb_xvp_ver = xvp_ver; - Log.Info("XVP extensions enabled (version " + this._rfb_xvp_ver + ")"); - this._setCapability("power", true); - break; - default: - this._fail("Illegal server XVP message (msg: " + xvp_msg + ")"); - break; - } + // BlockBefore and BlockAfter are automatically handled by + // the fact that we process each incoming message + // synchronuosly. + RFB.messages.clientFence(this._sock, flags, payload); - return true; + return true; + } + + _handle_xvp_msg() { + if (this._sock.rQwait('XVP version and message', 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + const xvp_ver = this._sock.rQshift8(); + const xvp_msg = this._sock.rQshift8(); + + switch (xvp_msg) { + case 0: // XVP_FAIL + Log.Error('XVP Operation Failed'); + break; + case 1: // XVP_INIT + this._rfb_xvp_ver = xvp_ver; + Log.Info('XVP extensions enabled (version ' + this._rfb_xvp_ver + ')'); + this._setCapability('power', true); + break; + default: + this._fail('Illegal server XVP message (msg: ' + xvp_msg + ')'); + break; } - _normal_msg() { - let msg_type; - if (this._FBU.rects > 0) { - msg_type = 0; + return true; + } + + _normal_msg() { + let msg_type; + if (this._FBU.rects > 0) { + msg_type = 0; + } else { + msg_type = this._sock.rQshift8(); + } + + let first; let + ret; + switch (msg_type) { + case 0: // FramebufferUpdate + ret = this._framebufferUpdate(); + if (ret && !this._enabledContinuousUpdates) { + RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } + return ret; + + case 1: // SetColorMapEntries + return this._handle_set_colour_map_msg(); + + case 2: // Bell + Log.Debug('Bell'); + this.dispatchEvent(new CustomEvent( + 'bell', + { detail: {} } + )); + return true; + + case 3: // ServerCutText + return this._handle_server_cut_text(); + + case 150: // EndOfContinuousUpdates + first = !this._supportsContinuousUpdates; + this._supportsContinuousUpdates = true; + this._enabledContinuousUpdates = false; + if (first) { + this._enabledContinuousUpdates = true; + this._updateContinuousUpdates(); + Log.Info('Enabling continuous updates.'); } else { - msg_type = this._sock.rQshift8(); + // FIXME: We need to send a framebufferupdaterequest here + // if we add support for turning off continuous updates + } + return true; + + case 248: // ServerFence + return this._handle_server_fence_msg(); + + case 250: // XVP + return this._handle_xvp_msg(); + + default: + this._fail('Unexpected server message (type ' + msg_type + ')'); + Log.Debug('sock.rQslice(0, 30): ' + this._sock.rQslice(0, 30)); + return true; + } + } + + _onFlush() { + this._flushing = false; + // Resume processing + if (this._sock.rQlen() > 0) { + this._handle_message(); + } + } + + _framebufferUpdate() { + if (this._FBU.rects === 0) { + if (this._sock.rQwait('FBU header', 3, 1)) { return false; } + this._sock.rQskip8(); // Padding + this._FBU.rects = this._sock.rQshift16(); + this._FBU.bytes = 0; + this._timing.cur_fbu = 0; + if (this._timing.fbu_rt_start > 0) { + const now = (new Date()).getTime(); + Log.Info('First FBU latency: ' + (now - this._timing.fbu_rt_start)); + } + + // Make sure the previous frame is fully rendered first + // to avoid building up an excessive queue + if (this._display.pending()) { + this._flushing = true; + this._display.flush(); + return false; + } + } + + while (this._FBU.rects > 0) { + if (this._rfb_connection_state !== 'connected') { return false; } + + if (this._sock.rQwait('FBU', this._FBU.bytes)) { return false; } + if (this._FBU.bytes === 0) { + if (this._sock.rQwait('rect header', 12)) { return false; } + /* New FramebufferUpdate */ + + const hdr = this._sock.rQshiftBytes(12); + this._FBU.x = (hdr[0] << 8) + hdr[1]; + this._FBU.y = (hdr[2] << 8) + hdr[3]; + this._FBU.width = (hdr[4] << 8) + hdr[5]; + this._FBU.height = (hdr[6] << 8) + hdr[7]; + this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + + (hdr[10] << 8) + hdr[11], 10); + + if (!this._encHandlers[this._FBU.encoding]) { + this._fail('Unsupported encoding (encoding: ' + + this._FBU.encoding + ')'); + return false; + } + } + + this._timing.last_fbu = (new Date()).getTime(); + + const ret = this._encHandlers[this._FBU.encoding](); + + const now = (new Date()).getTime(); + this._timing.cur_fbu += (now - this._timing.last_fbu); + + if (ret) { + if (!(this._FBU.encoding in this._encStats)) { + this._encStats[this._FBU.encoding] = [0, 0]; + } + this._encStats[this._FBU.encoding][0]++; + this._encStats[this._FBU.encoding][1]++; + this._timing.pixels += this._FBU.width * this._FBU.height; + } + + if (this._timing.pixels >= (this._fb_width * this._fb_height)) { + if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) + || this._timing.fbu_rt_start > 0) { + this._timing.full_fbu_total += this._timing.cur_fbu; + this._timing.full_fbu_cnt++; + Log.Info('Timing of full FBU, curr: ' + + this._timing.cur_fbu + ', total: ' + + this._timing.full_fbu_total + ', cnt: ' + + this._timing.full_fbu_cnt + ', avg: ' + + (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); } - let first, ret; - switch (msg_type) { - case 0: // FramebufferUpdate - ret = this._framebufferUpdate(); - if (ret && !this._enabledContinuousUpdates) { - RFB.messages.fbUpdateRequest(this._sock, true, 0, 0, - this._fb_width, this._fb_height); - } - return ret; - - case 1: // SetColorMapEntries - return this._handle_set_colour_map_msg(); - - case 2: // Bell - Log.Debug("Bell"); - this.dispatchEvent(new CustomEvent( - "bell", - { detail: {} })); - return true; - - case 3: // ServerCutText - return this._handle_server_cut_text(); - - case 150: // EndOfContinuousUpdates - first = !this._supportsContinuousUpdates; - this._supportsContinuousUpdates = true; - this._enabledContinuousUpdates = false; - if (first) { - this._enabledContinuousUpdates = true; - this._updateContinuousUpdates(); - Log.Info("Enabling continuous updates."); - } else { - // FIXME: We need to send a framebufferupdaterequest here - // if we add support for turning off continuous updates - } - return true; - - case 248: // ServerFence - return this._handle_server_fence_msg(); - - case 250: // XVP - return this._handle_xvp_msg(); - - default: - this._fail("Unexpected server message (type " + msg_type + ")"); - Log.Debug("sock.rQslice(0, 30): " + this._sock.rQslice(0, 30)); - return true; + if (this._timing.fbu_rt_start > 0) { + const fbu_rt_diff = now - this._timing.fbu_rt_start; + this._timing.fbu_rt_total += fbu_rt_diff; + this._timing.fbu_rt_cnt++; + Log.Info('full FBU round-trip, cur: ' + + fbu_rt_diff + ', total: ' + + this._timing.fbu_rt_total + ', cnt: ' + + this._timing.fbu_rt_cnt + ', avg: ' + + (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); + this._timing.fbu_rt_start = 0; } + } + + if (!ret) { return ret; } // need more data } - _onFlush() { - this._flushing = false; - // Resume processing - if (this._sock.rQlen() > 0) { - this._handle_message(); - } - } - - _framebufferUpdate() { - if (this._FBU.rects === 0) { - if (this._sock.rQwait("FBU header", 3, 1)) { return false; } - this._sock.rQskip8(); // Padding - this._FBU.rects = this._sock.rQshift16(); - this._FBU.bytes = 0; - this._timing.cur_fbu = 0; - if (this._timing.fbu_rt_start > 0) { - const now = (new Date()).getTime(); - Log.Info("First FBU latency: " + (now - this._timing.fbu_rt_start)); - } - - // Make sure the previous frame is fully rendered first - // to avoid building up an excessive queue - if (this._display.pending()) { - this._flushing = true; - this._display.flush(); - return false; - } - } - - while (this._FBU.rects > 0) { - if (this._rfb_connection_state !== 'connected') { return false; } - - if (this._sock.rQwait("FBU", this._FBU.bytes)) { return false; } - if (this._FBU.bytes === 0) { - if (this._sock.rQwait("rect header", 12)) { return false; } - /* New FramebufferUpdate */ - - const hdr = this._sock.rQshiftBytes(12); - this._FBU.x = (hdr[0] << 8) + hdr[1]; - this._FBU.y = (hdr[2] << 8) + hdr[3]; - this._FBU.width = (hdr[4] << 8) + hdr[5]; - this._FBU.height = (hdr[6] << 8) + hdr[7]; - this._FBU.encoding = parseInt((hdr[8] << 24) + (hdr[9] << 16) + - (hdr[10] << 8) + hdr[11], 10); - - if (!this._encHandlers[this._FBU.encoding]) { - this._fail("Unsupported encoding (encoding: " + - this._FBU.encoding + ")"); - return false; - } - } - - this._timing.last_fbu = (new Date()).getTime(); - - const ret = this._encHandlers[this._FBU.encoding](); - - const now = (new Date()).getTime(); - this._timing.cur_fbu += (now - this._timing.last_fbu); - - if (ret) { - if (!(this._FBU.encoding in this._encStats)) { - this._encStats[this._FBU.encoding] = [0, 0]; - } - this._encStats[this._FBU.encoding][0]++; - this._encStats[this._FBU.encoding][1]++; - this._timing.pixels += this._FBU.width * this._FBU.height; - } - - if (this._timing.pixels >= (this._fb_width * this._fb_height)) { - if ((this._FBU.width === this._fb_width && this._FBU.height === this._fb_height) || - this._timing.fbu_rt_start > 0) { - this._timing.full_fbu_total += this._timing.cur_fbu; - this._timing.full_fbu_cnt++; - Log.Info("Timing of full FBU, curr: " + - this._timing.cur_fbu + ", total: " + - this._timing.full_fbu_total + ", cnt: " + - this._timing.full_fbu_cnt + ", avg: " + - (this._timing.full_fbu_total / this._timing.full_fbu_cnt)); - } - - if (this._timing.fbu_rt_start > 0) { - const fbu_rt_diff = now - this._timing.fbu_rt_start; - this._timing.fbu_rt_total += fbu_rt_diff; - this._timing.fbu_rt_cnt++; - Log.Info("full FBU round-trip, cur: " + - fbu_rt_diff + ", total: " + - this._timing.fbu_rt_total + ", cnt: " + - this._timing.fbu_rt_cnt + ", avg: " + - (this._timing.fbu_rt_total / this._timing.fbu_rt_cnt)); - this._timing.fbu_rt_start = 0; - } - } - - if (!ret) { return ret; } // need more data - } - - this._display.flip(); - - return true; // We finished this FBU - } - - _updateContinuousUpdates() { - if (!this._enabledContinuousUpdates) { return; } - - RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, - this._fb_width, this._fb_height); - } - - _resize(width, height) { - this._fb_width = width; - this._fb_height = height; - - this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); - - this._display.resize(this._fb_width, this._fb_height); - - // Adjust the visible viewport based on the new dimensions - this._updateClip(); - this._updateScale(); - - this._timing.fbu_rt_start = (new Date()).getTime(); - this._updateContinuousUpdates(); - } - - _xvpOp(ver, op) { - if (this._rfb_xvp_ver < ver) { return; } - Log.Info("Sending XVP operation " + op + " (version " + ver + ")"); - RFB.messages.xvpOp(this._sock, ver, op); - } - - static genDES(password, challenge) { - const passwd = []; - for (let i = 0; i < password.length; i++) { - passwd.push(password.charCodeAt(i)); - } - return (new DES(passwd)).encrypt(challenge); + this._display.flip(); + + return true; // We finished this FBU + } + + _updateContinuousUpdates() { + if (!this._enabledContinuousUpdates) { return; } + + RFB.messages.enableContinuousUpdates(this._sock, true, 0, 0, + this._fb_width, this._fb_height); + } + + _resize(width, height) { + this._fb_width = width; + this._fb_height = height; + + this._destBuff = new Uint8Array(this._fb_width * this._fb_height * 4); + + this._display.resize(this._fb_width, this._fb_height); + + // Adjust the visible viewport based on the new dimensions + this._updateClip(); + this._updateScale(); + + this._timing.fbu_rt_start = (new Date()).getTime(); + this._updateContinuousUpdates(); + } + + _xvpOp(ver, op) { + if (this._rfb_xvp_ver < ver) { return; } + Log.Info('Sending XVP operation ' + op + ' (version ' + ver + ')'); + RFB.messages.xvpOp(this._sock, ver, op); + } + + static genDES(password, challenge) { + const passwd = []; + for (let i = 0; i < password.length; i++) { + passwd.push(password.charCodeAt(i)); } + return (new DES(passwd)).encrypt(challenge); + } } // Class Methods RFB.messages = { - keyEvent(sock, keysym, down) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 4; // msg-type - buff[offset + 1] = down; - - buff[offset + 2] = 0; - buff[offset + 3] = 0; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - sock._sQlen += 8; - sock.flush(); - }, - - QEMUExtendedKeyEvent(sock, keysym, down, keycode) { - function getRFBkeycode(xt_scancode) { - const upperByte = (keycode >> 8); - const lowerByte = (keycode & 0x00ff); - if (upperByte === 0xe0 && lowerByte < 0x7f) { - return lowerByte | 0x80; - } - return xt_scancode; - } - - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 255; // msg-type - buff[offset + 1] = 0; // sub msg-type - - buff[offset + 2] = (down >> 8); - buff[offset + 3] = down; - - buff[offset + 4] = (keysym >> 24); - buff[offset + 5] = (keysym >> 16); - buff[offset + 6] = (keysym >> 8); - buff[offset + 7] = keysym; - - const RFBkeycode = getRFBkeycode(keycode); - - buff[offset + 8] = (RFBkeycode >> 24); - buff[offset + 9] = (RFBkeycode >> 16); - buff[offset + 10] = (RFBkeycode >> 8); - buff[offset + 11] = RFBkeycode; - - sock._sQlen += 12; - sock.flush(); - }, - - pointerEvent(sock, x, y, mask) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 5; // msg-type - - buff[offset + 1] = mask; - - buff[offset + 2] = x >> 8; - buff[offset + 3] = x; - - buff[offset + 4] = y >> 8; - buff[offset + 5] = y; - - sock._sQlen += 6; - sock.flush(); - }, - - // TODO(directxman12): make this unicode compatible? - clientCutText(sock, text) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 6; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - let length = text.length; - - buff[offset + 4] = length >> 24; - buff[offset + 5] = length >> 16; - buff[offset + 6] = length >> 8; - buff[offset + 7] = length; - - sock._sQlen += 8; - - // We have to keep track of from where in the text we begin creating the - // buffer for the flush in the next iteration. - let textOffset = 0; - - let remaining = length; - while (remaining > 0) { - - let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); - if (flushSize <= 0) { - this._fail("Clipboard contents could not be sent"); - break; - } - - for (let i = 0; i < flushSize; i++) { - buff[sock._sQlen + i] = text.charCodeAt(textOffset + i); - } - - sock._sQlen += flushSize; - sock.flush(); - - remaining -= flushSize; - textOffset += flushSize; - } - }, - - setDesktopSize(sock, width, height, id, flags) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 251; // msg-type - buff[offset + 1] = 0; // padding - buff[offset + 2] = width >> 8; // width - buff[offset + 3] = width; - buff[offset + 4] = height >> 8; // height - buff[offset + 5] = height; - - buff[offset + 6] = 1; // number-of-screens - buff[offset + 7] = 0; // padding - - // screen array - buff[offset + 8] = id >> 24; // id - buff[offset + 9] = id >> 16; - buff[offset + 10] = id >> 8; - buff[offset + 11] = id; - buff[offset + 12] = 0; // x-position - buff[offset + 13] = 0; - buff[offset + 14] = 0; // y-position - buff[offset + 15] = 0; - buff[offset + 16] = width >> 8; // width - buff[offset + 17] = width; - buff[offset + 18] = height >> 8; // height - buff[offset + 19] = height; - buff[offset + 20] = flags >> 24; // flags - buff[offset + 21] = flags >> 16; - buff[offset + 22] = flags >> 8; - buff[offset + 23] = flags; - - sock._sQlen += 24; - sock.flush(); - }, - - clientFence(sock, flags, payload) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 248; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - buff[offset + 4] = flags >> 24; // flags - buff[offset + 5] = flags >> 16; - buff[offset + 6] = flags >> 8; - buff[offset + 7] = flags; - - const n = payload.length; - - buff[offset + 8] = n; // length - - for (let i = 0; i < n; i++) { - buff[offset + 9 + i] = payload.charCodeAt(i); - } - - sock._sQlen += 9 + n; - sock.flush(); - }, - - enableContinuousUpdates(sock, enable, x, y, width, height) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 150; // msg-type - buff[offset + 1] = enable; // enable-flag - - buff[offset + 2] = x >> 8; // x - buff[offset + 3] = x; - buff[offset + 4] = y >> 8; // y - buff[offset + 5] = y; - buff[offset + 6] = width >> 8; // width - buff[offset + 7] = width; - buff[offset + 8] = height >> 8; // height - buff[offset + 9] = height; - - sock._sQlen += 10; - sock.flush(); - }, - - pixelFormat(sock, depth, true_color) { - const buff = sock._sQ; - const offset = sock._sQlen; - - let bpp; - - if (depth > 16) { - bpp = 32; - } else if (depth > 8) { - bpp = 16; - } else { - bpp = 8; - } - - const bits = Math.floor(depth/3); - - buff[offset] = 0; // msg-type - - buff[offset + 1] = 0; // padding - buff[offset + 2] = 0; // padding - buff[offset + 3] = 0; // padding - - buff[offset + 4] = bpp; // bits-per-pixel - buff[offset + 5] = depth; // depth - buff[offset + 6] = 0; // little-endian - buff[offset + 7] = true_color ? 1 : 0; // true-color - - buff[offset + 8] = 0; // red-max - buff[offset + 9] = (1 << bits) - 1; // red-max - - buff[offset + 10] = 0; // green-max - buff[offset + 11] = (1 << bits) - 1; // green-max - - buff[offset + 12] = 0; // blue-max - buff[offset + 13] = (1 << bits) - 1; // blue-max - - buff[offset + 14] = bits * 2; // red-shift - buff[offset + 15] = bits * 1; // green-shift - buff[offset + 16] = bits * 0; // blue-shift - - buff[offset + 17] = 0; // padding - buff[offset + 18] = 0; // padding - buff[offset + 19] = 0; // padding - - sock._sQlen += 20; - sock.flush(); - }, - - clientEncodings(sock, encodings) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 2; // msg-type - buff[offset + 1] = 0; // padding - - buff[offset + 2] = encodings.length >> 8; - buff[offset + 3] = encodings.length; - - let j = offset + 4; - for (let i = 0; i < encodings.length; i++) { - const enc = encodings[i]; - buff[j] = enc >> 24; - buff[j + 1] = enc >> 16; - buff[j + 2] = enc >> 8; - buff[j + 3] = enc; - - j += 4; - } - - sock._sQlen += j - offset; - sock.flush(); - }, - - fbUpdateRequest(sock, incremental, x, y, w, h) { - const buff = sock._sQ; - const offset = sock._sQlen; - - if (typeof(x) === "undefined") { x = 0; } - if (typeof(y) === "undefined") { y = 0; } - - buff[offset] = 3; // msg-type - buff[offset + 1] = incremental ? 1 : 0; - - buff[offset + 2] = (x >> 8) & 0xFF; - buff[offset + 3] = x & 0xFF; - - buff[offset + 4] = (y >> 8) & 0xFF; - buff[offset + 5] = y & 0xFF; - - buff[offset + 6] = (w >> 8) & 0xFF; - buff[offset + 7] = w & 0xFF; - - buff[offset + 8] = (h >> 8) & 0xFF; - buff[offset + 9] = h & 0xFF; - - sock._sQlen += 10; - sock.flush(); - }, - - xvpOp(sock, ver, op) { - const buff = sock._sQ; - const offset = sock._sQlen; - - buff[offset] = 250; // msg-type - buff[offset + 1] = 0; // padding - - buff[offset + 2] = ver; - buff[offset + 3] = op; - - sock._sQlen += 4; - sock.flush(); + keyEvent(sock, keysym, down) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 4; // msg-type + buff[offset + 1] = down; + + buff[offset + 2] = 0; + buff[offset + 3] = 0; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + sock._sQlen += 8; + sock.flush(); + }, + + QEMUExtendedKeyEvent(sock, keysym, down, keycode) { + function getRFBkeycode(xt_scancode) { + const upperByte = (keycode >> 8); + const lowerByte = (keycode & 0x00ff); + if (upperByte === 0xe0 && lowerByte < 0x7f) { + return lowerByte | 0x80; + } + return xt_scancode; } + + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 255; // msg-type + buff[offset + 1] = 0; // sub msg-type + + buff[offset + 2] = (down >> 8); + buff[offset + 3] = down; + + buff[offset + 4] = (keysym >> 24); + buff[offset + 5] = (keysym >> 16); + buff[offset + 6] = (keysym >> 8); + buff[offset + 7] = keysym; + + const RFBkeycode = getRFBkeycode(keycode); + + buff[offset + 8] = (RFBkeycode >> 24); + buff[offset + 9] = (RFBkeycode >> 16); + buff[offset + 10] = (RFBkeycode >> 8); + buff[offset + 11] = RFBkeycode; + + sock._sQlen += 12; + sock.flush(); + }, + + pointerEvent(sock, x, y, mask) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 5; // msg-type + + buff[offset + 1] = mask; + + buff[offset + 2] = x >> 8; + buff[offset + 3] = x; + + buff[offset + 4] = y >> 8; + buff[offset + 5] = y; + + sock._sQlen += 6; + sock.flush(); + }, + + // TODO(directxman12): make this unicode compatible? + clientCutText(sock, text) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 6; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + let length = text.length; + + buff[offset + 4] = length >> 24; + buff[offset + 5] = length >> 16; + buff[offset + 6] = length >> 8; + buff[offset + 7] = length; + + sock._sQlen += 8; + + // We have to keep track of from where in the text we begin creating the + // buffer for the flush in the next iteration. + let textOffset = 0; + + let remaining = length; + while (remaining > 0) { + let flushSize = Math.min(remaining, (sock._sQbufferSize - sock._sQlen)); + if (flushSize <= 0) { + this._fail('Clipboard contents could not be sent'); + break; + } + + for (let i = 0; i < flushSize; i++) { + buff[sock._sQlen + i] = text.charCodeAt(textOffset + i); + } + + sock._sQlen += flushSize; + sock.flush(); + + remaining -= flushSize; + textOffset += flushSize; + } + }, + + setDesktopSize(sock, width, height, id, flags) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 251; // msg-type + buff[offset + 1] = 0; // padding + buff[offset + 2] = width >> 8; // width + buff[offset + 3] = width; + buff[offset + 4] = height >> 8; // height + buff[offset + 5] = height; + + buff[offset + 6] = 1; // number-of-screens + buff[offset + 7] = 0; // padding + + // screen array + buff[offset + 8] = id >> 24; // id + buff[offset + 9] = id >> 16; + buff[offset + 10] = id >> 8; + buff[offset + 11] = id; + buff[offset + 12] = 0; // x-position + buff[offset + 13] = 0; + buff[offset + 14] = 0; // y-position + buff[offset + 15] = 0; + buff[offset + 16] = width >> 8; // width + buff[offset + 17] = width; + buff[offset + 18] = height >> 8; // height + buff[offset + 19] = height; + buff[offset + 20] = flags >> 24; // flags + buff[offset + 21] = flags >> 16; + buff[offset + 22] = flags >> 8; + buff[offset + 23] = flags; + + sock._sQlen += 24; + sock.flush(); + }, + + clientFence(sock, flags, payload) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 248; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = flags >> 24; // flags + buff[offset + 5] = flags >> 16; + buff[offset + 6] = flags >> 8; + buff[offset + 7] = flags; + + const n = payload.length; + + buff[offset + 8] = n; // length + + for (let i = 0; i < n; i++) { + buff[offset + 9 + i] = payload.charCodeAt(i); + } + + sock._sQlen += 9 + n; + sock.flush(); + }, + + enableContinuousUpdates(sock, enable, x, y, width, height) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 150; // msg-type + buff[offset + 1] = enable; // enable-flag + + buff[offset + 2] = x >> 8; // x + buff[offset + 3] = x; + buff[offset + 4] = y >> 8; // y + buff[offset + 5] = y; + buff[offset + 6] = width >> 8; // width + buff[offset + 7] = width; + buff[offset + 8] = height >> 8; // height + buff[offset + 9] = height; + + sock._sQlen += 10; + sock.flush(); + }, + + pixelFormat(sock, depth, true_color) { + const buff = sock._sQ; + const offset = sock._sQlen; + + let bpp; + + if (depth > 16) { + bpp = 32; + } else if (depth > 8) { + bpp = 16; + } else { + bpp = 8; + } + + const bits = Math.floor(depth / 3); + + buff[offset] = 0; // msg-type + + buff[offset + 1] = 0; // padding + buff[offset + 2] = 0; // padding + buff[offset + 3] = 0; // padding + + buff[offset + 4] = bpp; // bits-per-pixel + buff[offset + 5] = depth; // depth + buff[offset + 6] = 0; // little-endian + buff[offset + 7] = true_color ? 1 : 0; // true-color + + buff[offset + 8] = 0; // red-max + buff[offset + 9] = (1 << bits) - 1; // red-max + + buff[offset + 10] = 0; // green-max + buff[offset + 11] = (1 << bits) - 1; // green-max + + buff[offset + 12] = 0; // blue-max + buff[offset + 13] = (1 << bits) - 1; // blue-max + + buff[offset + 14] = bits * 2; // red-shift + buff[offset + 15] = bits * 1; // green-shift + buff[offset + 16] = bits * 0; // blue-shift + + buff[offset + 17] = 0; // padding + buff[offset + 18] = 0; // padding + buff[offset + 19] = 0; // padding + + sock._sQlen += 20; + sock.flush(); + }, + + clientEncodings(sock, encodings) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 2; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = encodings.length >> 8; + buff[offset + 3] = encodings.length; + + let j = offset + 4; + for (let i = 0; i < encodings.length; i++) { + const enc = encodings[i]; + buff[j] = enc >> 24; + buff[j + 1] = enc >> 16; + buff[j + 2] = enc >> 8; + buff[j + 3] = enc; + + j += 4; + } + + sock._sQlen += j - offset; + sock.flush(); + }, + + fbUpdateRequest(sock, incremental, x, y, w, h) { + const buff = sock._sQ; + const offset = sock._sQlen; + + if (typeof (x) === 'undefined') { x = 0; } + if (typeof (y) === 'undefined') { y = 0; } + + buff[offset] = 3; // msg-type + buff[offset + 1] = incremental ? 1 : 0; + + buff[offset + 2] = (x >> 8) & 0xFF; + buff[offset + 3] = x & 0xFF; + + buff[offset + 4] = (y >> 8) & 0xFF; + buff[offset + 5] = y & 0xFF; + + buff[offset + 6] = (w >> 8) & 0xFF; + buff[offset + 7] = w & 0xFF; + + buff[offset + 8] = (h >> 8) & 0xFF; + buff[offset + 9] = h & 0xFF; + + sock._sQlen += 10; + sock.flush(); + }, + + xvpOp(sock, ver, op) { + const buff = sock._sQ; + const offset = sock._sQlen; + + buff[offset] = 250; // msg-type + buff[offset + 1] = 0; // padding + + buff[offset + 2] = ver; + buff[offset + 3] = op; + + sock._sQlen += 4; + sock.flush(); + } }; RFB.encodingHandlers = { - RAW() { - if (this._FBU.lines === 0) { - this._FBU.lines = this._FBU.height; - } + RAW() { + if (this._FBU.lines === 0) { + this._FBU.lines = this._FBU.height; + } - const pixelSize = this._fb_depth == 8 ? 1 : 4; - this._FBU.bytes = this._FBU.width * pixelSize; // at least a line - if (this._sock.rQwait("RAW", this._FBU.bytes)) { return false; } - const cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); - const curr_height = Math.min(this._FBU.lines, - Math.floor(this._sock.rQlen() / (this._FBU.width * pixelSize))); - let data = this._sock.get_rQ(); - let index = this._sock.get_rQi(); - if (this._fb_depth == 8) { - const pixels = this._FBU.width * curr_height - const newdata = new Uint8Array(pixels * 4); - for (let i = 0; i < pixels; i++) { - newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; - newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; - newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; - newdata[i * 4 + 4] = 0; - } - data = newdata; - index = 0; - } - this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, - curr_height, data, index); - this._sock.rQskipBytes(this._FBU.width * curr_height * pixelSize); - this._FBU.lines -= curr_height; + const pixelSize = this._fb_depth == 8 ? 1 : 4; + this._FBU.bytes = this._FBU.width * pixelSize; // at least a line + if (this._sock.rQwait('RAW', this._FBU.bytes)) { return false; } + const cur_y = this._FBU.y + (this._FBU.height - this._FBU.lines); + const curr_height = Math.min(this._FBU.lines, + Math.floor(this._sock.rQlen() / (this._FBU.width * pixelSize))); + let data = this._sock.get_rQ(); + let index = this._sock.get_rQi(); + if (this._fb_depth == 8) { + const pixels = this._FBU.width * curr_height; + const newdata = new Uint8Array(pixels * 4); + for (let i = 0; i < pixels; i++) { + newdata[i * 4 + 0] = ((data[index + i] >> 0) & 0x3) * 255 / 3; + newdata[i * 4 + 1] = ((data[index + i] >> 2) & 0x3) * 255 / 3; + newdata[i * 4 + 2] = ((data[index + i] >> 4) & 0x3) * 255 / 3; + newdata[i * 4 + 4] = 0; + } + data = newdata; + index = 0; + } + this._display.blitImage(this._FBU.x, cur_y, this._FBU.width, + curr_height, data, index); + this._sock.rQskipBytes(this._FBU.width * curr_height * pixelSize); + this._FBU.lines -= curr_height; - if (this._FBU.lines > 0) { - this._FBU.bytes = this._FBU.width * pixelSize; // At least another line + if (this._FBU.lines > 0) { + this._FBU.bytes = this._FBU.width * pixelSize; // At least another line + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + COPYRECT() { + this._FBU.bytes = 4; + if (this._sock.rQwait('COPYRECT', 4)) { return false; } + this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), + this._FBU.x, this._FBU.y, this._FBU.width, + this._FBU.height); + + this._FBU.rects--; + this._FBU.bytes = 0; + return true; + }, + + RRE() { + let color; + if (this._FBU.subrects === 0) { + this._FBU.bytes = 4 + 4; + if (this._sock.rQwait('RRE', 4 + 4)) { return false; } + this._FBU.subrects = this._sock.rQshift32(); + color = this._sock.rQshiftBytes(4); // Background + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); + } + + while (this._FBU.subrects > 0 && this._sock.rQlen() >= (4 + 8)) { + color = this._sock.rQshiftBytes(4); + const x = this._sock.rQshift16(); + const y = this._sock.rQshift16(); + const width = this._sock.rQshift16(); + const height = this._sock.rQshift16(); + this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); + this._FBU.subrects--; + } + + if (this._FBU.subrects > 0) { + const chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); + this._FBU.bytes = (4 + 8) * chunk; + } else { + this._FBU.rects--; + this._FBU.bytes = 0; + } + + return true; + }, + + HEXTILE() { + const rQ = this._sock.get_rQ(); + let rQi = this._sock.get_rQi(); + + if (this._FBU.tiles === 0) { + this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); + this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); + this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; + this._FBU.tiles = this._FBU.total_tiles; + } + + while (this._FBU.tiles > 0) { + this._FBU.bytes = 1; + if (this._sock.rQwait('HEXTILE subencoding', this._FBU.bytes)) { return false; } + const subencoding = rQ[rQi]; // Peek + if (subencoding > 30) { // Raw + this._fail('Illegal hextile subencoding (subencoding: ' + + subencoding + ')'); + return false; + } + + let subrects = 0; + const curr_tile = this._FBU.total_tiles - this._FBU.tiles; + const tile_x = curr_tile % this._FBU.tiles_x; + const tile_y = Math.floor(curr_tile / this._FBU.tiles_x); + const x = this._FBU.x + tile_x * 16; + const y = this._FBU.y + tile_y * 16; + const w = Math.min(16, (this._FBU.x + this._FBU.width) - x); + const h = Math.min(16, (this._FBU.y + this._FBU.height) - y); + + // Figure out how much we are expecting + if (subencoding & 0x01) { // Raw + this._FBU.bytes += w * h * 4; + } else { + if (subencoding & 0x02) { // Background + this._FBU.bytes += 4; + } + if (subencoding & 0x04) { // Foreground + this._FBU.bytes += 4; + } + if (subencoding & 0x08) { // AnySubrects + this._FBU.bytes++; // Since we aren't shifting it off + if (this._sock.rQwait('hextile subrects header', this._FBU.bytes)) { return false; } + subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek + if (subencoding & 0x10) { // SubrectsColoured + this._FBU.bytes += subrects * (4 + 2); + } else { + this._FBU.bytes += subrects * 2; + } + } + } + + if (this._sock.rQwait('hextile', this._FBU.bytes)) { return false; } + + // We know the encoding and have a whole tile + this._FBU.subencoding = rQ[rQi]; + rQi++; + if (this._FBU.subencoding === 0) { + if (this._FBU.lastsubencoding & 0x01) { + // Weird: ignore blanks are RAW + Log.Debug(' Ignoring blank after RAW'); } else { - this._FBU.rects--; - this._FBU.bytes = 0; + this._display.fillRect(x, y, w, h, this._FBU.background); + } + } else if (this._FBU.subencoding & 0x01) { // Raw + this._display.blitImage(x, y, w, h, rQ, rQi); + rQi += this._FBU.bytes - 1; + } else { + if (this._FBU.subencoding & 0x02) { // Background + this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; + } + if (this._FBU.subencoding & 0x04) { // Foreground + this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; } - return true; - }, + this._display.startTile(x, y, w, h, this._FBU.background); + if (this._FBU.subencoding & 0x08) { // AnySubrects + subrects = rQ[rQi]; + rQi++; - COPYRECT() { - this._FBU.bytes = 4; - if (this._sock.rQwait("COPYRECT", 4)) { return false; } - this._display.copyImage(this._sock.rQshift16(), this._sock.rQshift16(), - this._FBU.x, this._FBU.y, this._FBU.width, - this._FBU.height); - - this._FBU.rects--; - this._FBU.bytes = 0; - return true; - }, - - RRE() { - let color; - if (this._FBU.subrects === 0) { - this._FBU.bytes = 4 + 4; - if (this._sock.rQwait("RRE", 4 + 4)) { return false; } - this._FBU.subrects = this._sock.rQshift32(); - color = this._sock.rQshiftBytes(4); // Background - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, color); - } - - while (this._FBU.subrects > 0 && this._sock.rQlen() >= (4 + 8)) { - color = this._sock.rQshiftBytes(4); - const x = this._sock.rQshift16(); - const y = this._sock.rQshift16(); - const width = this._sock.rQshift16(); - const height = this._sock.rQshift16(); - this._display.fillRect(this._FBU.x + x, this._FBU.y + y, width, height, color); - this._FBU.subrects--; - } - - if (this._FBU.subrects > 0) { - const chunk = Math.min(this._rre_chunk_sz, this._FBU.subrects); - this._FBU.bytes = (4 + 8) * chunk; - } else { - this._FBU.rects--; - this._FBU.bytes = 0; - } - - return true; - }, - - HEXTILE() { - const rQ = this._sock.get_rQ(); - let rQi = this._sock.get_rQi(); - - if (this._FBU.tiles === 0) { - this._FBU.tiles_x = Math.ceil(this._FBU.width / 16); - this._FBU.tiles_y = Math.ceil(this._FBU.height / 16); - this._FBU.total_tiles = this._FBU.tiles_x * this._FBU.tiles_y; - this._FBU.tiles = this._FBU.total_tiles; - } - - while (this._FBU.tiles > 0) { - this._FBU.bytes = 1; - if (this._sock.rQwait("HEXTILE subencoding", this._FBU.bytes)) { return false; } - const subencoding = rQ[rQi]; // Peek - if (subencoding > 30) { // Raw - this._fail("Illegal hextile subencoding (subencoding: " + - subencoding + ")"); - return false; - } - - let subrects = 0; - const curr_tile = this._FBU.total_tiles - this._FBU.tiles; - const tile_x = curr_tile % this._FBU.tiles_x; - const tile_y = Math.floor(curr_tile / this._FBU.tiles_x); - const x = this._FBU.x + tile_x * 16; - const y = this._FBU.y + tile_y * 16; - const w = Math.min(16, (this._FBU.x + this._FBU.width) - x); - const h = Math.min(16, (this._FBU.y + this._FBU.height) - y); - - // Figure out how much we are expecting - if (subencoding & 0x01) { // Raw - this._FBU.bytes += w * h * 4; + for (let s = 0; s < subrects; s++) { + let color; + if (this._FBU.subencoding & 0x10) { // SubrectsColoured + color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; + rQi += 4; } else { - if (subencoding & 0x02) { // Background - this._FBU.bytes += 4; - } - if (subencoding & 0x04) { // Foreground - this._FBU.bytes += 4; - } - if (subencoding & 0x08) { // AnySubrects - this._FBU.bytes++; // Since we aren't shifting it off - if (this._sock.rQwait("hextile subrects header", this._FBU.bytes)) { return false; } - subrects = rQ[rQi + this._FBU.bytes - 1]; // Peek - if (subencoding & 0x10) { // SubrectsColoured - this._FBU.bytes += subrects * (4 + 2); - } else { - this._FBU.bytes += subrects * 2; - } - } + color = this._FBU.foreground; } - - if (this._sock.rQwait("hextile", this._FBU.bytes)) { return false; } - - // We know the encoding and have a whole tile - this._FBU.subencoding = rQ[rQi]; + const xy = rQ[rQi]; rQi++; - if (this._FBU.subencoding === 0) { - if (this._FBU.lastsubencoding & 0x01) { - // Weird: ignore blanks are RAW - Log.Debug(" Ignoring blank after RAW"); - } else { - this._display.fillRect(x, y, w, h, this._FBU.background); - } - } else if (this._FBU.subencoding & 0x01) { // Raw - this._display.blitImage(x, y, w, h, rQ, rQi); - rQi += this._FBU.bytes - 1; - } else { - if (this._FBU.subencoding & 0x02) { // Background - this._FBU.background = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; - } - if (this._FBU.subencoding & 0x04) { // Foreground - this._FBU.foreground = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; - } + const sx = (xy >> 4); + const sy = (xy & 0x0f); - this._display.startTile(x, y, w, h, this._FBU.background); - if (this._FBU.subencoding & 0x08) { // AnySubrects - subrects = rQ[rQi]; - rQi++; + const wh = rQ[rQi]; + rQi++; + const sw = (wh >> 4) + 1; + const sh = (wh & 0x0f) + 1; - for (let s = 0; s < subrects; s++) { - let color; - if (this._FBU.subencoding & 0x10) { // SubrectsColoured - color = [rQ[rQi], rQ[rQi + 1], rQ[rQi + 2], rQ[rQi + 3]]; - rQi += 4; - } else { - color = this._FBU.foreground; - } - const xy = rQ[rQi]; - rQi++; - const sx = (xy >> 4); - const sy = (xy & 0x0f); - - const wh = rQ[rQi]; - rQi++; - const sw = (wh >> 4) + 1; - const sh = (wh & 0x0f) + 1; - - this._display.subTile(sx, sy, sw, sh, color); - } - } - this._display.finishTile(); - } - this._sock.set_rQi(rQi); - this._FBU.lastsubencoding = this._FBU.subencoding; - this._FBU.bytes = 0; - this._FBU.tiles--; + this._display.subTile(sx, sy, sw, sh, color); + } } + this._display.finishTile(); + } + this._sock.set_rQi(rQi); + this._FBU.lastsubencoding = this._FBU.subencoding; + this._FBU.bytes = 0; + this._FBU.tiles--; + } - if (this._FBU.tiles === 0) { - this._FBU.rects--; + if (this._FBU.tiles === 0) { + this._FBU.rects--; + } + + return true; + }, + + TIGHT(isTightPNG) { + this._FBU.bytes = 1; // compression-control byte + if (this._sock.rQwait('TIGHT compression-control', this._FBU.bytes)) { return false; } + + let resetStreams = 0; + let streamId = -1; + const decompress = (data, expected) => { + for (let i = 0; i < 4; i++) { + if ((resetStreams >> i) & 1) { + this._FBU.zlibs[i].reset(); + Log.Info('Reset zlib stream ' + i); } + } - return true; - }, - - TIGHT(isTightPNG) { - this._FBU.bytes = 1; // compression-control byte - if (this._sock.rQwait("TIGHT compression-control", this._FBU.bytes)) { return false; } - - let resetStreams = 0; - let streamId = -1; - const decompress = (data, expected) => { - for (let i = 0; i < 4; i++) { - if ((resetStreams >> i) & 1) { - this._FBU.zlibs[i].reset(); - Log.Info("Reset zlib stream " + i); - } - } - - //const uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); - const uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); - /*if (uncompressed.status !== 0) { + // const uncompressed = this._FBU.zlibs[streamId].uncompress(data, 0); + const uncompressed = this._FBU.zlibs[streamId].inflate(data, true, expected); + /* if (uncompressed.status !== 0) { Log.Error("Invalid data in zlib stream"); - }*/ + } */ - //return uncompressed.data; - return uncompressed; - }; + // return uncompressed.data; + return uncompressed; + }; - const indexedToRGBX2Color = (data, palette, width, height) => { - // Convert indexed (palette based) image data to RGB - // TODO: reduce number of calculations inside loop - const dest = this._destBuff; - const w = Math.floor((width + 7) / 8); - const w1 = Math.floor(width / 8); + const indexedToRGBX2Color = (data, palette, width, height) => { + // Convert indexed (palette based) image data to RGB + // TODO: reduce number of calculations inside loop + const dest = this._destBuff; + const w = Math.floor((width + 7) / 8); + const w1 = Math.floor(width / 8); - /*for (let y = 0; y < height; y++) { + /* for (let y = 0; y < height; y++) { let b, x, dp, sp; const yoffset = y * width; const ybitoffset = y * w; @@ -2188,296 +2203,300 @@ RFB.encodingHandlers = { dest[dp + 1] = palette[sp + 1]; dest[dp + 2] = palette[sp + 2]; } - }*/ + } */ - for (let y = 0; y < height; y++) { - let dp, sp, x; - for (x = 0; x < w1; x++) { - for (let b = 7; b >= 0; b--) { - dp = (y * width + x * 8 + 7 - b) * 4; - sp = (data[y * w + x] >> b & 1) * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - dest[dp + 3] = 255; - } - } - - for (let b = 7; b >= 8 - width % 8; b--) { - dp = (y * width + x * 8 + 7 - b) * 4; - sp = (data[y * w + x] >> b & 1) * 3; - dest[dp] = palette[sp]; - dest[dp + 1] = palette[sp + 1]; - dest[dp + 2] = palette[sp + 2]; - dest[dp + 3] = 255; - } - } - - return dest; - }; - - const indexedToRGBX = (data, palette, width, height) => { - // Convert indexed (palette based) image data to RGB - const dest = this._destBuff; - const total = width * height * 4; - for (let i = 0, j = 0; i < total; i += 4, j++) { - const sp = data[j] * 3; - dest[i] = palette[sp]; - dest[i + 1] = palette[sp + 1]; - dest[i + 2] = palette[sp + 2]; - dest[i + 3] = 255; - } - - return dest; - }; - - const rQi = this._sock.get_rQi(); - const rQ = this._sock.rQwhole(); - let cmode, data; - let cl_header, cl_data; - - const handlePalette = () => { - const numColors = rQ[rQi + 2] + 1; - const paletteSize = numColors * 3; - this._FBU.bytes += paletteSize; - if (this._sock.rQwait("TIGHT palette " + cmode, this._FBU.bytes)) { return false; } - - const bpp = (numColors <= 2) ? 1 : 8; - const rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); - let raw = false; - if (rowSize * this._FBU.height < 12) { - raw = true; - cl_header = 0; - cl_data = rowSize * this._FBU.height; - //clength = [0, rowSize * this._FBU.height]; - } else { - // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) - const cl_offset = rQi + 3 + paletteSize; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { - cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } - } - // end inline getTightCLength - } - - this._FBU.bytes += cl_header + cl_data; - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - - // Shift ctl, filter id, num colors, palette entries, and clength off - this._sock.rQskipBytes(3); - //const palette = this._sock.rQshiftBytes(paletteSize); - this._sock.rQshiftTo(this._paletteBuff, paletteSize); - this._sock.rQskipBytes(cl_header); - - if (raw) { - data = this._sock.rQshiftBytes(cl_data); - } else { - data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); - } - - // Convert indexed (palette based) image data to RGB - let rgbx; - if (numColors == 2) { - rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); - } else { - rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); - } - - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); - - - return true; - }; - - const handleCopy = () => { - let raw = false; - const uncompressedSize = this._FBU.width * this._FBU.height * 3; - if (uncompressedSize < 12) { - raw = true; - cl_header = 0; - cl_data = uncompressedSize; - } else { - // begin inline getTightCLength (returning two-item arrays is for peformance with GC) - const cl_offset = rQi + 1; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { - cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } - } - // end inline getTightCLength - } - this._FBU.bytes = 1 + cl_header + cl_data; - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } - - // Shift ctl, clength off - this._sock.rQshiftBytes(1 + cl_header); - - if (raw) { - data = this._sock.rQshiftBytes(cl_data); - } else { - data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); - } - - this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); - - return true; - }; - - let ctl = this._sock.rQpeek8(); - - // Keep tight reset bits - resetStreams = ctl & 0xF; - - // Figure out filter - ctl = ctl >> 4; - streamId = ctl & 0x3; - - if (ctl === 0x08) cmode = "fill"; - else if (ctl === 0x09) cmode = "jpeg"; - else if (ctl === 0x0A) cmode = "png"; - else if (ctl & 0x04) cmode = "filter"; - else if (ctl < 0x04) cmode = "copy"; - else return this._fail("Illegal tight compression received (ctl: " + - ctl + ")"); - - if (isTightPNG && (ctl < 0x08)) { - return this._fail("BasicCompression received in TightPNG rect"); - } - if (!isTightPNG && (ctl === 0x0A)) { - return this._fail("PNG received in standard Tight rect"); + for (let y = 0; y < height; y++) { + let dp; let sp; let + x; + for (x = 0; x < w1; x++) { + for (let b = 7; b >= 0; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; + } } - switch (cmode) { - // fill use depth because TPIXELs drop the padding byte - case "fill": // TPIXEL - this._FBU.bytes += 3; - break; - case "jpeg": // max clength - this._FBU.bytes += 3; - break; - case "png": // max clength - this._FBU.bytes += 3; - break; - case "filter": // filter id + num colors if palette - this._FBU.bytes += 2; - break; - case "copy": - break; + for (let b = 7; b >= 8 - width % 8; b--) { + dp = (y * width + x * 8 + 7 - b) * 4; + sp = (data[y * w + x] >> b & 1) * 3; + dest[dp] = palette[sp]; + dest[dp + 1] = palette[sp + 1]; + dest[dp + 2] = palette[sp + 2]; + dest[dp + 3] = 255; } + } - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + return dest; + }; - // Determine FBU.bytes - let cl_offset, filterId; - switch (cmode) { - case "fill": - // skip ctl byte - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); - this._sock.rQskipBytes(4); - break; - case "png": - case "jpeg": - // begin inline getTightCLength (returning two-item arrays is for peformance with GC) - cl_offset = rQi + 1; - cl_header = 1; - cl_data = 0; - cl_data += rQ[cl_offset] & 0x7f; - if (rQ[cl_offset] & 0x80) { - cl_header++; - cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; - if (rQ[cl_offset + 1] & 0x80) { - cl_header++; - cl_data += rQ[cl_offset + 2] << 14; - } - } - // end inline getTightCLength - this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data - if (this._sock.rQwait("TIGHT " + cmode, this._FBU.bytes)) { return false; } + const indexedToRGBX = (data, palette, width, height) => { + // Convert indexed (palette based) image data to RGB + const dest = this._destBuff; + const total = width * height * 4; + for (let i = 0, j = 0; i < total; i += 4, j++) { + const sp = data[j] * 3; + dest[i] = palette[sp]; + dest[i + 1] = palette[sp + 1]; + dest[i + 2] = palette[sp + 2]; + dest[i + 3] = 255; + } - // We have everything, render it - this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length - data = this._sock.rQshiftBytes(cl_data); - this._display.imageRect(this._FBU.x, this._FBU.y, "image/" + cmode, data); - break; - case "filter": - filterId = rQ[rQi + 1]; - if (filterId === 1) { - if (!handlePalette()) { return false; } - } else { - // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter - // Filter 2, Gradient is valid but not use if jpeg is enabled - this._fail("Unsupported tight subencoding received " + - "(filter: " + filterId + ")"); - } - break; - case "copy": - if (!handleCopy()) { return false; } - break; + return dest; + }; + + const rQi = this._sock.get_rQi(); + const rQ = this._sock.rQwhole(); + let cmode; let + data; + let cl_header; let + cl_data; + + const handlePalette = () => { + const numColors = rQ[rQi + 2] + 1; + const paletteSize = numColors * 3; + this._FBU.bytes += paletteSize; + if (this._sock.rQwait('TIGHT palette ' + cmode, this._FBU.bytes)) { return false; } + + const bpp = (numColors <= 2) ? 1 : 8; + const rowSize = Math.floor((this._FBU.width * bpp + 7) / 8); + let raw = false; + if (rowSize * this._FBU.height < 12) { + raw = true; + cl_header = 0; + cl_data = rowSize * this._FBU.height; + // clength = [0, rowSize * this._FBU.height]; + } else { + // begin inline getTightCLength (returning two-item arrays is bad for performance with GC) + const cl_offset = rQi + 3 + paletteSize; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } } + // end inline getTightCLength + } + + this._FBU.bytes += cl_header + cl_data; + if (this._sock.rQwait('TIGHT ' + cmode, this._FBU.bytes)) { return false; } + + // Shift ctl, filter id, num colors, palette entries, and clength off + this._sock.rQskipBytes(3); + // const palette = this._sock.rQshiftBytes(paletteSize); + this._sock.rQshiftTo(this._paletteBuff, paletteSize); + this._sock.rQskipBytes(cl_header); + + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), rowSize * this._FBU.height); + } + + // Convert indexed (palette based) image data to RGB + let rgbx; + if (numColors == 2) { + rgbx = indexedToRGBX2Color(data, this._paletteBuff, this._FBU.width, this._FBU.height); + } else { + rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); + } + + this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); - this._FBU.bytes = 0; - this._FBU.rects--; + return true; + }; - return true; - }, - - last_rect() { - this._FBU.rects = 0; - return true; - }, - - ExtendedDesktopSize() { - this._FBU.bytes = 1; - if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } - - const firstUpdate = !this._supportsSetDesktopSize; - this._supportsSetDesktopSize = true; - - // Normally we only apply the current resize mode after a - // window resize event. However there is no such trigger on the - // initial connect. And we don't know if the server supports - // resizing until we've gotten here. - if (firstUpdate) { - this._requestRemoteResize(); + const handleCopy = () => { + let raw = false; + const uncompressedSize = this._FBU.width * this._FBU.height * 3; + if (uncompressedSize < 12) { + raw = true; + cl_header = 0; + cl_data = uncompressedSize; + } else { + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + const cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } } + // end inline getTightCLength + } + this._FBU.bytes = 1 + cl_header + cl_data; + if (this._sock.rQwait('TIGHT ' + cmode, this._FBU.bytes)) { return false; } - const number_of_screens = this._sock.rQpeek8(); + // Shift ctl, clength off + this._sock.rQshiftBytes(1 + cl_header); - this._FBU.bytes = 4 + (number_of_screens * 16); - if (this._sock.rQwait("ExtendedDesktopSize", this._FBU.bytes)) { return false; } + if (raw) { + data = this._sock.rQshiftBytes(cl_data); + } else { + data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); + } - this._sock.rQskipBytes(1); // number-of-screens - this._sock.rQskipBytes(3); // padding + this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); - for (let i = 0; i < number_of_screens; i += 1) { - // Save the id and flags of the first screen - if (i === 0) { - this._screen_id = this._sock.rQshiftBytes(4); // id - this._sock.rQskipBytes(2); // x-position - this._sock.rQskipBytes(2); // y-position - this._sock.rQskipBytes(2); // width - this._sock.rQskipBytes(2); // height - this._screen_flags = this._sock.rQshiftBytes(4); // flags - } else { - this._sock.rQskipBytes(16); - } + return true; + }; + + let ctl = this._sock.rQpeek8(); + + // Keep tight reset bits + resetStreams = ctl & 0xF; + + // Figure out filter + ctl >>= 4; + streamId = ctl & 0x3; + + if (ctl === 0x08) cmode = 'fill'; + else if (ctl === 0x09) cmode = 'jpeg'; + else if (ctl === 0x0A) cmode = 'png'; + else if (ctl & 0x04) cmode = 'filter'; + else if (ctl < 0x04) cmode = 'copy'; + else return this._fail('Illegal tight compression received (ctl: ' + + ctl + ')'); + + if (isTightPNG && (ctl < 0x08)) { + return this._fail('BasicCompression received in TightPNG rect'); + } + if (!isTightPNG && (ctl === 0x0A)) { + return this._fail('PNG received in standard Tight rect'); + } + + switch (cmode) { + // fill use depth because TPIXELs drop the padding byte + case 'fill': // TPIXEL + this._FBU.bytes += 3; + break; + case 'jpeg': // max clength + this._FBU.bytes += 3; + break; + case 'png': // max clength + this._FBU.bytes += 3; + break; + case 'filter': // filter id + num colors if palette + this._FBU.bytes += 2; + break; + case 'copy': + break; + } + + if (this._sock.rQwait('TIGHT ' + cmode, this._FBU.bytes)) { return false; } + + // Determine FBU.bytes + let cl_offset; let + filterId; + switch (cmode) { + case 'fill': + // skip ctl byte + this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._sock.rQskipBytes(4); + break; + case 'png': + case 'jpeg': + // begin inline getTightCLength (returning two-item arrays is for peformance with GC) + cl_offset = rQi + 1; + cl_header = 1; + cl_data = 0; + cl_data += rQ[cl_offset] & 0x7f; + if (rQ[cl_offset] & 0x80) { + cl_header++; + cl_data += (rQ[cl_offset + 1] & 0x7f) << 7; + if (rQ[cl_offset + 1] & 0x80) { + cl_header++; + cl_data += rQ[cl_offset + 2] << 14; + } } + // end inline getTightCLength + this._FBU.bytes = 1 + cl_header + cl_data; // ctl + clength size + jpeg-data + if (this._sock.rQwait('TIGHT ' + cmode, this._FBU.bytes)) { return false; } - /* + // We have everything, render it + this._sock.rQskipBytes(1 + cl_header); // shift off clt + compact length + data = this._sock.rQshiftBytes(cl_data); + this._display.imageRect(this._FBU.x, this._FBU.y, 'image/' + cmode, data); + break; + case 'filter': + filterId = rQ[rQi + 1]; + if (filterId === 1) { + if (!handlePalette()) { return false; } + } else { + // Filter 0, Copy could be valid here, but servers don't send it as an explicit filter + // Filter 2, Gradient is valid but not use if jpeg is enabled + this._fail('Unsupported tight subencoding received ' + + '(filter: ' + filterId + ')'); + } + break; + case 'copy': + if (!handleCopy()) { return false; } + break; + } + + + this._FBU.bytes = 0; + this._FBU.rects--; + + return true; + }, + + last_rect() { + this._FBU.rects = 0; + return true; + }, + + ExtendedDesktopSize() { + this._FBU.bytes = 1; + if (this._sock.rQwait('ExtendedDesktopSize', this._FBU.bytes)) { return false; } + + const firstUpdate = !this._supportsSetDesktopSize; + this._supportsSetDesktopSize = true; + + // Normally we only apply the current resize mode after a + // window resize event. However there is no such trigger on the + // initial connect. And we don't know if the server supports + // resizing until we've gotten here. + if (firstUpdate) { + this._requestRemoteResize(); + } + + const number_of_screens = this._sock.rQpeek8(); + + this._FBU.bytes = 4 + (number_of_screens * 16); + if (this._sock.rQwait('ExtendedDesktopSize', this._FBU.bytes)) { return false; } + + this._sock.rQskipBytes(1); // number-of-screens + this._sock.rQskipBytes(3); // padding + + for (let i = 0; i < number_of_screens; i += 1) { + // Save the id and flags of the first screen + if (i === 0) { + this._screen_id = this._sock.rQshiftBytes(4); // id + this._sock.rQskipBytes(2); // x-position + this._sock.rQskipBytes(2); // y-position + this._sock.rQskipBytes(2); // width + this._sock.rQskipBytes(2); // height + this._screen_flags = this._sock.rQshiftBytes(4); // flags + } else { + this._sock.rQskipBytes(16); + } + } + + /* * The x-position indicates the reason for the change: * * 0 - server resized on its own @@ -2485,77 +2504,77 @@ RFB.encodingHandlers = { * 2 - another client requested the resize */ - // We need to handle errors when we requested the resize. - if (this._FBU.x === 1 && this._FBU.y !== 0) { - let msg = ""; - // The y-position indicates the status code from the server - switch (this._FBU.y) { - case 1: - msg = "Resize is administratively prohibited"; - break; - case 2: - msg = "Out of resources"; - break; - case 3: - msg = "Invalid screen layout"; - break; - default: - msg = "Unknown reason"; - break; - } - Log.Warn("Server did not accept the resize request: " + // We need to handle errors when we requested the resize. + if (this._FBU.x === 1 && this._FBU.y !== 0) { + let msg = ''; + // The y-position indicates the status code from the server + switch (this._FBU.y) { + case 1: + msg = 'Resize is administratively prohibited'; + break; + case 2: + msg = 'Out of resources'; + break; + case 3: + msg = 'Invalid screen layout'; + break; + default: + msg = 'Unknown reason'; + break; + } + Log.Warn('Server did not accept the resize request: ' + msg); - } else { - this._resize(this._FBU.width, this._FBU.height); - } - - this._FBU.bytes = 0; - this._FBU.rects -= 1; - return true; - }, - - DesktopSize() { - this._resize(this._FBU.width, this._FBU.height); - this._FBU.bytes = 0; - this._FBU.rects -= 1; - return true; - }, - - Cursor() { - Log.Debug(">> set_cursor"); - const x = this._FBU.x; // hotspot-x - const y = this._FBU.y; // hotspot-y - const w = this._FBU.width; - const h = this._FBU.height; - - const pixelslength = w * h * 4; - const masklength = Math.floor((w + 7) / 8) * h; - - this._FBU.bytes = pixelslength + masklength; - if (this._sock.rQwait("cursor encoding", this._FBU.bytes)) { return false; } - - this._cursor.change(this._sock.rQshiftBytes(pixelslength), - this._sock.rQshiftBytes(masklength), - x, y, w, h); - - this._FBU.bytes = 0; - this._FBU.rects--; - - Log.Debug("<< set_cursor"); - return true; - }, - - QEMUExtendedKeyEvent() { - this._FBU.rects--; - - // Old Safari doesn't support creating keyboard events - try { - const keyboardEvent = document.createEvent("keyboardEvent"); - if (keyboardEvent.code !== undefined) { - this._qemuExtKeyEventSupported = true; - } - } catch (err) { - // Do nothing - } + } else { + this._resize(this._FBU.width, this._FBU.height); } -} + + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + DesktopSize() { + this._resize(this._FBU.width, this._FBU.height); + this._FBU.bytes = 0; + this._FBU.rects -= 1; + return true; + }, + + Cursor() { + Log.Debug('>> set_cursor'); + const x = this._FBU.x; // hotspot-x + const y = this._FBU.y; // hotspot-y + const w = this._FBU.width; + const h = this._FBU.height; + + const pixelslength = w * h * 4; + const masklength = Math.floor((w + 7) / 8) * h; + + this._FBU.bytes = pixelslength + masklength; + if (this._sock.rQwait('cursor encoding', this._FBU.bytes)) { return false; } + + this._cursor.change(this._sock.rQshiftBytes(pixelslength), + this._sock.rQshiftBytes(masklength), + x, y, w, h); + + this._FBU.bytes = 0; + this._FBU.rects--; + + Log.Debug('<< set_cursor'); + return true; + }, + + QEMUExtendedKeyEvent() { + this._FBU.rects--; + + // Old Safari doesn't support creating keyboard events + try { + const keyboardEvent = document.createEvent('keyboardEvent'); + if (keyboardEvent.code !== undefined) { + this._qemuExtKeyEventSupported = true; + } + } catch (err) { + // Do nothing + } + } +}; diff --git a/core/util/browser.js b/core/util/browser.js index 80551d40..4fc26588 100644 --- a/core/util/browser.js +++ b/core/util/browser.js @@ -9,65 +9,64 @@ import * as Log from './logging.js'; // Touch detection -export let isTouchDevice = ('ontouchstart' in document.documentElement) || +export let isTouchDevice = ('ontouchstart' in document.documentElement) // requried for Chrome debugger - (document.ontouchstart !== undefined) || + || (document.ontouchstart !== undefined) // required for MS Surface - (navigator.maxTouchPoints > 0) || - (navigator.msMaxTouchPoints > 0); + || (navigator.maxTouchPoints > 0) + || (navigator.msMaxTouchPoints > 0); window.addEventListener('touchstart', function onFirstTouch() { - isTouchDevice = true; - window.removeEventListener('touchstart', onFirstTouch, false); + isTouchDevice = true; + window.removeEventListener('touchstart', onFirstTouch, false); }, false); let _cursor_uris_supported = null; -export function supportsCursorURIs () { - if (_cursor_uris_supported === null) { - try { - const target = document.createElement('canvas'); - target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; +export function supportsCursorURIs() { + if (_cursor_uris_supported === null) { + try { + const target = document.createElement('canvas'); + target.style.cursor = 'url("data:image/x-icon;base64,AAACAAEACAgAAAIAAgA4AQAAFgAAACgAAAAIAAAAEAAAAAEAIAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAD/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////AAAAAAAAAAAAAAAAAAAAAA==") 2 2, default'; - if (target.style.cursor) { - Log.Info("Data URI scheme cursor supported"); - _cursor_uris_supported = true; - } else { - Log.Warn("Data URI scheme cursor not supported"); - _cursor_uris_supported = false; - } - } catch (exc) { - Log.Error("Data URI scheme cursor test exception: " + exc); - _cursor_uris_supported = false; - } + if (target.style.cursor) { + Log.Info('Data URI scheme cursor supported'); + _cursor_uris_supported = true; + } else { + Log.Warn('Data URI scheme cursor not supported'); + _cursor_uris_supported = false; + } + } catch (exc) { + Log.Error('Data URI scheme cursor test exception: ' + exc); + _cursor_uris_supported = false; } + } - return _cursor_uris_supported; + return _cursor_uris_supported; } export function isMac() { - return navigator && !!(/mac/i).exec(navigator.platform); + return navigator && !!(/mac/i).exec(navigator.platform); } export function isIE() { - return navigator && !!(/trident/i).exec(navigator.userAgent); + return navigator && !!(/trident/i).exec(navigator.userAgent); } export function isEdge() { - return navigator && !!(/edge/i).exec(navigator.userAgent); + return navigator && !!(/edge/i).exec(navigator.userAgent); } export function isFirefox() { - return navigator && !!(/firefox/i).exec(navigator.userAgent); + return navigator && !!(/firefox/i).exec(navigator.userAgent); } export function isWindows() { - return navigator && !!(/win/i).exec(navigator.platform); + return navigator && !!(/win/i).exec(navigator.platform); } export function isIOS() { - return navigator && - (!!(/ipad/i).exec(navigator.platform) || - !!(/iphone/i).exec(navigator.platform) || - !!(/ipod/i).exec(navigator.platform)); + return navigator + && (!!(/ipad/i).exec(navigator.platform) + || !!(/iphone/i).exec(navigator.platform) + || !!(/ipod/i).exec(navigator.platform)); } - diff --git a/core/util/cursor.js b/core/util/cursor.js index 18aa7beb..771766ea 100644 --- a/core/util/cursor.js +++ b/core/util/cursor.js @@ -9,220 +9,213 @@ import { supportsCursorURIs, isTouchDevice } from './browser.js'; const useFallback = !supportsCursorURIs() || isTouchDevice; export default class Cursor { - constructor(container) { - this._target = null; + constructor(container) { + this._target = null; - this._canvas = document.createElement('canvas'); + this._canvas = document.createElement('canvas'); - if (useFallback) { - this._canvas.style.position = 'fixed'; - this._canvas.style.zIndex = '65535'; - this._canvas.style.pointerEvents = 'none'; - // Can't use "display" because of Firefox bug #1445997 - this._canvas.style.visibility = 'hidden'; - document.body.appendChild(this._canvas); - } - - this._position = { x: 0, y: 0 }; - this._hotSpot = { x: 0, y: 0 }; - - this._eventHandlers = { - 'mouseover': this._handleMouseOver.bind(this), - 'mouseleave': this._handleMouseLeave.bind(this), - 'mousemove': this._handleMouseMove.bind(this), - 'mouseup': this._handleMouseUp.bind(this), - 'touchstart': this._handleTouchStart.bind(this), - 'touchmove': this._handleTouchMove.bind(this), - 'touchend': this._handleTouchEnd.bind(this), - }; + if (useFallback) { + this._canvas.style.position = 'fixed'; + this._canvas.style.zIndex = '65535'; + this._canvas.style.pointerEvents = 'none'; + // Can't use "display" because of Firefox bug #1445997 + this._canvas.style.visibility = 'hidden'; + document.body.appendChild(this._canvas); } - attach(target) { - if (this._target) { - this.detach(); - } + this._position = { x: 0, y: 0 }; + this._hotSpot = { x: 0, y: 0 }; - this._target = target; + this._eventHandlers = { + mouseover: this._handleMouseOver.bind(this), + mouseleave: this._handleMouseLeave.bind(this), + mousemove: this._handleMouseMove.bind(this), + mouseup: this._handleMouseUp.bind(this), + touchstart: this._handleTouchStart.bind(this), + touchmove: this._handleTouchMove.bind(this), + touchend: this._handleTouchEnd.bind(this), + }; + } - if (useFallback) { - // FIXME: These don't fire properly except for mouse - /// movement in IE. We want to also capture element - // movement, size changes, visibility, etc. - const options = { capture: true, passive: true }; - this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options); - this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); - this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); - this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); - - // There is no "touchleave" so we monitor touchstart globally - window.addEventListener('touchstart', this._eventHandlers.touchstart, options); - this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options); - this._target.addEventListener('touchend', this._eventHandlers.touchend, options); - } - - this.clear(); + attach(target) { + if (this._target) { + this.detach(); } - detach() { - if (useFallback) { - const options = { capture: true, passive: true }; - this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); - this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); - this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); - this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); + this._target = target; - window.removeEventListener('touchstart', this._eventHandlers.touchstart, options); - this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options); - this._target.removeEventListener('touchend', this._eventHandlers.touchend, options); - } + if (useFallback) { + // FIXME: These don't fire properly except for mouse + // / movement in IE. We want to also capture element + // movement, size changes, visibility, etc. + const options = { capture: true, passive: true }; + this._target.addEventListener('mouseover', this._eventHandlers.mouseover, options); + this._target.addEventListener('mouseleave', this._eventHandlers.mouseleave, options); + this._target.addEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.addEventListener('mouseup', this._eventHandlers.mouseup, options); - this._target = null; + // There is no "touchleave" so we monitor touchstart globally + window.addEventListener('touchstart', this._eventHandlers.touchstart, options); + this._target.addEventListener('touchmove', this._eventHandlers.touchmove, options); + this._target.addEventListener('touchend', this._eventHandlers.touchend, options); } - change(pixels, mask, hotx, hoty, w, h) { - if ((w === 0) || (h === 0)) { - this.clear(); - return; - } + this.clear(); + } - let cur = [] - for (let y = 0; y < h; y++) { - for (let x = 0; x < w; x++) { - let idx = y * Math.ceil(w / 8) + Math.floor(x / 8); - let alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; - idx = ((w * y) + x) * 4; - cur.push(pixels[idx + 2]); // red - cur.push(pixels[idx + 1]); // green - cur.push(pixels[idx]); // blue - cur.push(alpha); // alpha - } - } + detach() { + if (useFallback) { + const options = { capture: true, passive: true }; + this._target.removeEventListener('mouseover', this._eventHandlers.mouseover, options); + this._target.removeEventListener('mouseleave', this._eventHandlers.mouseleave, options); + this._target.removeEventListener('mousemove', this._eventHandlers.mousemove, options); + this._target.removeEventListener('mouseup', this._eventHandlers.mouseup, options); - this._position.x = this._position.x + this._hotSpot.x - hotx; - this._position.y = this._position.y + this._hotSpot.y - hoty; - this._hotSpot.x = hotx; - this._hotSpot.y = hoty; - - let ctx = this._canvas.getContext('2d'); - - this._canvas.width = w; - this._canvas.height = h; - - let img; - try { - // IE doesn't support this - img = new ImageData(new Uint8ClampedArray(cur), w, h); - } catch (ex) { - img = ctx.createImageData(w, h); - img.data.set(new Uint8ClampedArray(cur)); - } - ctx.clearRect(0, 0, w, h); - ctx.putImageData(img, 0, 0); - - if (useFallback) { - this._updatePosition(); - } else { - let url = this._canvas.toDataURL(); - this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; - } + window.removeEventListener('touchstart', this._eventHandlers.touchstart, options); + this._target.removeEventListener('touchmove', this._eventHandlers.touchmove, options); + this._target.removeEventListener('touchend', this._eventHandlers.touchend, options); } - clear() { - this._target.style.cursor = 'none'; - this._canvas.width = 0; - this._canvas.height = 0; - this._position.x = this._position.x + this._hotSpot.x; - this._position.y = this._position.y + this._hotSpot.y; - this._hotSpot.x = 0; - this._hotSpot.y = 0; + this._target = null; + } + + change(pixels, mask, hotx, hoty, w, h) { + if ((w === 0) || (h === 0)) { + this.clear(); + return; } - _handleMouseOver(event) { - // This event could be because we're entering the target, or - // moving around amongst its sub elements. Let the move handler - // sort things out. - this._handleMouseMove(event); + let cur = []; + for (let y = 0; y < h; y++) { + for (let x = 0; x < w; x++) { + let idx = y * Math.ceil(w / 8) + Math.floor(x / 8); + let alpha = (mask[idx] << (x % 8)) & 0x80 ? 255 : 0; + idx = ((w * y) + x) * 4; + cur.push(pixels[idx + 2]); // red + cur.push(pixels[idx + 1]); // green + cur.push(pixels[idx]); // blue + cur.push(alpha); // alpha + } } - _handleMouseLeave(event) { - this._hideCursor(); + this._position.x = this._position.x + this._hotSpot.x - hotx; + this._position.y = this._position.y + this._hotSpot.y - hoty; + this._hotSpot.x = hotx; + this._hotSpot.y = hoty; + + let ctx = this._canvas.getContext('2d'); + + this._canvas.width = w; + this._canvas.height = h; + + let img; + try { + // IE doesn't support this + img = new ImageData(new Uint8ClampedArray(cur), w, h); + } catch (ex) { + img = ctx.createImageData(w, h); + img.data.set(new Uint8ClampedArray(cur)); } + ctx.clearRect(0, 0, w, h); + ctx.putImageData(img, 0, 0); - _handleMouseMove(event) { - this._updateVisibility(event.target); - - this._position.x = event.clientX - this._hotSpot.x; - this._position.y = event.clientY - this._hotSpot.y; - - this._updatePosition(); + if (useFallback) { + this._updatePosition(); + } else { + let url = this._canvas.toDataURL(); + this._target.style.cursor = 'url(' + url + ')' + hotx + ' ' + hoty + ', default'; } + } - _handleMouseUp(event) { - // We might get this event because of a drag operation that - // moved outside of the target. Check what's under the cursor - // now and adjust visibility based on that. - let target = document.elementFromPoint(event.clientX, event.clientY); - this._updateVisibility(target); - } + clear() { + this._target.style.cursor = 'none'; + this._canvas.width = 0; + this._canvas.height = 0; + this._position.x = this._position.x + this._hotSpot.x; + this._position.y = this._position.y + this._hotSpot.y; + this._hotSpot.x = 0; + this._hotSpot.y = 0; + } - _handleTouchStart(event) { - // Just as for mouseover, we let the move handler deal with it - this._handleTouchMove(event); - } + _handleMouseOver(event) { + // This event could be because we're entering the target, or + // moving around amongst its sub elements. Let the move handler + // sort things out. + this._handleMouseMove(event); + } - _handleTouchMove(event) { - this._updateVisibility(event.target); + _handleMouseLeave(event) { + this._hideCursor(); + } - this._position.x = event.changedTouches[0].clientX - this._hotSpot.x; - this._position.y = event.changedTouches[0].clientY - this._hotSpot.y; + _handleMouseMove(event) { + this._updateVisibility(event.target); - this._updatePosition(); - } + this._position.x = event.clientX - this._hotSpot.x; + this._position.y = event.clientY - this._hotSpot.y; - _handleTouchEnd(event) { - // Same principle as for mouseup - let target = document.elementFromPoint(event.changedTouches[0].clientX, - event.changedTouches[0].clientY); - this._updateVisibility(target); - } + this._updatePosition(); + } - _showCursor() { - if (this._canvas.style.visibility === 'hidden') - this._canvas.style.visibility = ''; - } + _handleMouseUp(event) { + // We might get this event because of a drag operation that + // moved outside of the target. Check what's under the cursor + // now and adjust visibility based on that. + let target = document.elementFromPoint(event.clientX, event.clientY); + this._updateVisibility(target); + } - _hideCursor() { - if (this._canvas.style.visibility !== 'hidden') - this._canvas.style.visibility = 'hidden'; - } + _handleTouchStart(event) { + // Just as for mouseover, we let the move handler deal with it + this._handleTouchMove(event); + } - // Should we currently display the cursor? - // (i.e. are we over the target, or a child of the target without a - // different cursor set) - _shouldShowCursor(target) { - // Easy case - if (target === this._target) - return true; - // Other part of the DOM? - if (!this._target.contains(target)) - return false; - // Has the child its own cursor? - // FIXME: How can we tell that a sub element has an - // explicit "cursor: none;"? - if (window.getComputedStyle(target).cursor !== 'none') - return false; - return true; - } + _handleTouchMove(event) { + this._updateVisibility(event.target); - _updateVisibility(target) { - if (this._shouldShowCursor(target)) - this._showCursor(); - else - this._hideCursor(); - } + this._position.x = event.changedTouches[0].clientX - this._hotSpot.x; + this._position.y = event.changedTouches[0].clientY - this._hotSpot.y; - _updatePosition() { - this._canvas.style.left = this._position.x + "px"; - this._canvas.style.top = this._position.y + "px"; - } + this._updatePosition(); + } + + _handleTouchEnd(event) { + // Same principle as for mouseup + let target = document.elementFromPoint(event.changedTouches[0].clientX, + event.changedTouches[0].clientY); + this._updateVisibility(target); + } + + _showCursor() { + if (this._canvas.style.visibility === 'hidden') this._canvas.style.visibility = ''; + } + + _hideCursor() { + if (this._canvas.style.visibility !== 'hidden') this._canvas.style.visibility = 'hidden'; + } + + // Should we currently display the cursor? + // (i.e. are we over the target, or a child of the target without a + // different cursor set) + _shouldShowCursor(target) { + // Easy case + if (target === this._target) return true; + // Other part of the DOM? + if (!this._target.contains(target)) return false; + // Has the child its own cursor? + // FIXME: How can we tell that a sub element has an + // explicit "cursor: none;"? + if (window.getComputedStyle(target).cursor !== 'none') return false; + return true; + } + + _updateVisibility(target) { + if (this._shouldShowCursor(target)) this._showCursor(); + else this._hideCursor(); + } + + _updatePosition() { + this._canvas.style.left = this._position.x + 'px'; + this._canvas.style.top = this._position.y + 'px'; + } } diff --git a/core/util/events.js b/core/util/events.js index a0eddc71..efb0a437 100644 --- a/core/util/events.js +++ b/core/util/events.js @@ -10,130 +10,126 @@ * Cross-browser event and position routines */ -export function getPointerEvent (e) { - return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; +export function getPointerEvent(e) { + return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; } -export function stopEvent (e) { - e.stopPropagation(); - e.preventDefault(); +export function stopEvent(e) { + e.stopPropagation(); + e.preventDefault(); } // Emulate Element.setCapture() when not supported let _captureRecursion = false; let _captureElem = null; function _captureProxy(e) { - // Recursion protection as we'll see our own event - if (_captureRecursion) return; + // Recursion protection as we'll see our own event + if (_captureRecursion) return; - // Clone the event as we cannot dispatch an already dispatched event - const newEv = new e.constructor(e.type, e); + // Clone the event as we cannot dispatch an already dispatched event + const newEv = new e.constructor(e.type, e); - _captureRecursion = true; - _captureElem.dispatchEvent(newEv); - _captureRecursion = false; + _captureRecursion = true; + _captureElem.dispatchEvent(newEv); + _captureRecursion = false; - // Avoid double events - e.stopPropagation(); + // Avoid double events + e.stopPropagation(); - // Respect the wishes of the redirected event handlers - if (newEv.defaultPrevented) { - e.preventDefault(); - } + // Respect the wishes of the redirected event handlers + if (newEv.defaultPrevented) { + e.preventDefault(); + } - // Implicitly release the capture on button release - if (e.type === "mouseup") { - releaseCapture(); - } + // Implicitly release the capture on button release + if (e.type === 'mouseup') { + releaseCapture(); + } } // Follow cursor style of target element function _captureElemChanged() { - const captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; + const captureElem = document.getElementById('noVNC_mouse_capture_elem'); + captureElem.style.cursor = window.getComputedStyle(_captureElem).cursor; } const _captureObserver = new MutationObserver(_captureElemChanged); let _captureIndex = 0; -export function setCapture (elem) { - if (elem.setCapture) { +export function setCapture(elem) { + if (elem.setCapture) { + elem.setCapture(); - elem.setCapture(); + // IE releases capture on 'click' events which might not trigger + elem.addEventListener('mouseup', releaseCapture); + } else { + // Release any existing capture in case this method is + // called multiple times without coordination + releaseCapture(); - // IE releases capture on 'click' events which might not trigger - elem.addEventListener('mouseup', releaseCapture); + let captureElem = document.getElementById('noVNC_mouse_capture_elem'); - } else { - // Release any existing capture in case this method is - // called multiple times without coordination - releaseCapture(); + if (captureElem === null) { + captureElem = document.createElement('div'); + captureElem.id = 'noVNC_mouse_capture_elem'; + captureElem.style.position = 'fixed'; + captureElem.style.top = '0px'; + captureElem.style.left = '0px'; + captureElem.style.width = '100%'; + captureElem.style.height = '100%'; + captureElem.style.zIndex = 10000; + captureElem.style.display = 'none'; + document.body.appendChild(captureElem); - let captureElem = document.getElementById("noVNC_mouse_capture_elem"); + // This is to make sure callers don't get confused by having + // our blocking element as the target + captureElem.addEventListener('contextmenu', _captureProxy); - if (captureElem === null) { - captureElem = document.createElement("div"); - captureElem.id = "noVNC_mouse_capture_elem"; - captureElem.style.position = "fixed"; - captureElem.style.top = "0px"; - captureElem.style.left = "0px"; - captureElem.style.width = "100%"; - captureElem.style.height = "100%"; - captureElem.style.zIndex = 10000; - captureElem.style.display = "none"; - document.body.appendChild(captureElem); - - // This is to make sure callers don't get confused by having - // our blocking element as the target - captureElem.addEventListener('contextmenu', _captureProxy); - - captureElem.addEventListener('mousemove', _captureProxy); - captureElem.addEventListener('mouseup', _captureProxy); - } - - _captureElem = elem; - _captureIndex++; - - // Track cursor and get initial cursor - _captureObserver.observe(elem, {attributes:true}); - _captureElemChanged(); - - captureElem.style.display = ""; - - // We listen to events on window in order to keep tracking if it - // happens to leave the viewport - window.addEventListener('mousemove', _captureProxy); - window.addEventListener('mouseup', _captureProxy); + captureElem.addEventListener('mousemove', _captureProxy); + captureElem.addEventListener('mouseup', _captureProxy); } + + _captureElem = elem; + _captureIndex++; + + // Track cursor and get initial cursor + _captureObserver.observe(elem, { attributes: true }); + _captureElemChanged(); + + captureElem.style.display = ''; + + // We listen to events on window in order to keep tracking if it + // happens to leave the viewport + window.addEventListener('mousemove', _captureProxy); + window.addEventListener('mouseup', _captureProxy); + } } -export function releaseCapture () { - if (document.releaseCapture) { - - document.releaseCapture(); - - } else { - if (!_captureElem) { - return; - } - - // There might be events already queued, so we need to wait for - // them to flush. E.g. contextmenu in Microsoft Edge - window.setTimeout((expected) => { - // Only clear it if it's the expected grab (i.e. no one - // else has initiated a new grab) - if (_captureIndex === expected) { - _captureElem = null; - } - }, 0, _captureIndex); - - _captureObserver.disconnect(); - - const captureElem = document.getElementById("noVNC_mouse_capture_elem"); - captureElem.style.display = "none"; - - window.removeEventListener('mousemove', _captureProxy); - window.removeEventListener('mouseup', _captureProxy); +export function releaseCapture() { + if (document.releaseCapture) { + document.releaseCapture(); + } else { + if (!_captureElem) { + return; } + + // There might be events already queued, so we need to wait for + // them to flush. E.g. contextmenu in Microsoft Edge + window.setTimeout((expected) => { + // Only clear it if it's the expected grab (i.e. no one + // else has initiated a new grab) + if (_captureIndex === expected) { + _captureElem = null; + } + }, 0, _captureIndex); + + _captureObserver.disconnect(); + + const captureElem = document.getElementById('noVNC_mouse_capture_elem'); + captureElem.style.display = 'none'; + + window.removeEventListener('mousemove', _captureProxy); + window.removeEventListener('mouseup', _captureProxy); + } } diff --git a/core/util/eventtarget.js b/core/util/eventtarget.js index bb8d6e00..2888e5bd 100644 --- a/core/util/eventtarget.js +++ b/core/util/eventtarget.js @@ -7,34 +7,34 @@ */ export default class EventTargetMixin { - constructor() { - this._listeners = null; + constructor() { + this._listeners = null; + } + + addEventListener(type, callback) { + if (!this._listeners) { + this._listeners = new Map(); } + if (!this._listeners.has(type)) { + this._listeners.set(type, new Set()); + } + this._listeners.get(type).add(callback); + } - addEventListener(type, callback) { - if (!this._listeners) { - this._listeners = new Map(); - } - if (!this._listeners.has(type)) { - this._listeners.set(type, new Set()); - } - this._listeners.get(type).add(callback); - } + removeEventListener(type, callback) { + if (!this._listeners || !this._listeners.has(type)) { + return; + } + this._listeners.get(type).delete(callback); + } - removeEventListener(type, callback) { - if (!this._listeners || !this._listeners.has(type)) { - return; - } - this._listeners.get(type).delete(callback); - } - - dispatchEvent(event) { - if (!this._listeners || !this._listeners.has(event.type)) { - return true; - } - this._listeners.get(event.type).forEach((callback) => { - callback.call(this, event); - }, this); - return !event.defaultPrevented; - } + dispatchEvent(event) { + if (!this._listeners || !this._listeners.has(event.type)) { + return true; + } + this._listeners.get(event.type).forEach((callback) => { + callback.call(this, event); + }, this); + return !event.defaultPrevented; + } } diff --git a/core/util/logging.js b/core/util/logging.js index 1f6a71e0..29347529 100644 --- a/core/util/logging.js +++ b/core/util/logging.js @@ -17,40 +17,42 @@ let Info = () => {}; let Warn = () => {}; let Error = () => {}; -export function init_logging (level) { - if (typeof level === 'undefined') { - level = _log_level; - } else { - _log_level = level; - } +export function init_logging(level) { + if (typeof level === 'undefined') { + level = _log_level; + } else { + _log_level = level; + } - Debug = Info = Warn = Error = () => {}; + Debug = Info = Warn = Error = () => {}; - if (typeof window.console !== "undefined") { - /* eslint-disable no-console, no-fallthrough */ - switch (level) { - case 'debug': - Debug = console.debug.bind(window.console); - case 'info': - Info = console.info.bind(window.console); - case 'warn': - Warn = console.warn.bind(window.console); - case 'error': - Error = console.error.bind(window.console); - case 'none': - break; - default: - throw new Error("invalid logging type '" + level + "'"); - } - /* eslint-enable no-console, no-fallthrough */ + if (typeof window.console !== 'undefined') { + /* eslint-disable no-console, no-fallthrough */ + switch (level) { + case 'debug': + Debug = console.debug.bind(window.console); + case 'info': + Info = console.info.bind(window.console); + case 'warn': + Warn = console.warn.bind(window.console); + case 'error': + Error = console.error.bind(window.console); + case 'none': + break; + default: + throw new Error("invalid logging type '" + level + "'"); } + /* eslint-enable no-console, no-fallthrough */ + } } -export function get_logging () { - return _log_level; +export function get_logging() { + return _log_level; } -export { Debug, Info, Warn, Error }; +export { + Debug, Info, Warn, Error +}; // Initialize logging level init_logging(); diff --git a/core/util/polyfill.js b/core/util/polyfill.js index 1971edf8..c55716d0 100644 --- a/core/util/polyfill.js +++ b/core/util/polyfill.js @@ -8,47 +8,48 @@ /* Object.assign() (taken from MDN) */ if (typeof Object.assign != 'function') { - // Must be writable: true, enumerable: false, configurable: true - Object.defineProperty(Object, "assign", { - value: function assign(target, varArgs) { // .length of function is 2 - 'use strict'; - if (target == null) { // TypeError if undefined or null - throw new TypeError('Cannot convert undefined or null to object'); + // Must be writable: true, enumerable: false, configurable: true + Object.defineProperty(Object, 'assign', { + value: function assign(target, varArgs) { // .length of function is 2 + 'use strict'; + + if (target == null) { // TypeError if undefined or null + throw new TypeError('Cannot convert undefined or null to object'); + } + + const to = Object(target); + + for (let index = 1; index < arguments.length; index++) { + const nextSource = arguments[index]; + + if (nextSource != null) { // Skip over if undefined or null + for (let nextKey in nextSource) { + // Avoid bugs when hasOwnProperty is shadowed + if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { + to[nextKey] = nextSource[nextKey]; } - - const to = Object(target); - - for (let index = 1; index < arguments.length; index++) { - const nextSource = arguments[index]; - - if (nextSource != null) { // Skip over if undefined or null - for (let nextKey in nextSource) { - // Avoid bugs when hasOwnProperty is shadowed - if (Object.prototype.hasOwnProperty.call(nextSource, nextKey)) { - to[nextKey] = nextSource[nextKey]; - } - } - } - } - return to; - }, - writable: true, - configurable: true - }); + } + } + } + return to; + }, + writable: true, + configurable: true + }); } /* CustomEvent constructor (taken from MDN) */ (() => { - function CustomEvent (event, params) { - params = params || { bubbles: false, cancelable: false, detail: undefined }; - const evt = document.createEvent( 'CustomEvent' ); - evt.initCustomEvent( event, params.bubbles, params.cancelable, params.detail ); - return evt; - } + function CustomEvent(event, params) { + params = params || { bubbles: false, cancelable: false, detail: undefined }; + const evt = document.createEvent('CustomEvent'); + evt.initCustomEvent(event, params.bubbles, params.cancelable, params.detail); + return evt; + } - CustomEvent.prototype = window.Event.prototype; + CustomEvent.prototype = window.Event.prototype; - if (typeof window.CustomEvent !== "function") { - window.CustomEvent = CustomEvent; - } + if (typeof window.CustomEvent !== 'function') { + window.CustomEvent = CustomEvent; + } })(); diff --git a/core/util/strings.js b/core/util/strings.js index b3de5476..87266720 100644 --- a/core/util/strings.js +++ b/core/util/strings.js @@ -9,6 +9,6 @@ /* * Decode from UTF-8 */ -export function decodeUTF8 (utf8string) { - return decodeURIComponent(escape(utf8string)); +export function decodeUTF8(utf8string) { + return decodeURIComponent(escape(utf8string)); } diff --git a/core/websock.js b/core/websock.js index 9f09230c..af4bc44b 100644 --- a/core/websock.js +++ b/core/websock.js @@ -21,279 +21,277 @@ const ENABLE_COPYWITHIN = false; const MAX_RQ_GROW_SIZE = 40 * 1024 * 1024; // 40 MiB export default class Websock { - constructor() { - this._websocket = null; // WebSocket object + constructor() { + this._websocket = null; // WebSocket object - this._rQi = 0; // Receive queue index - this._rQlen = 0; // Next write position in the receive queue - this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) - this._rQmax = this._rQbufferSize / 8; - // called in init: this._rQ = new Uint8Array(this._rQbufferSize); - this._rQ = null; // Receive queue + this._rQi = 0; // Receive queue index + this._rQlen = 0; // Next write position in the receive queue + this._rQbufferSize = 1024 * 1024 * 4; // Receive queue buffer size (4 MiB) + this._rQmax = this._rQbufferSize / 8; + // called in init: this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ = null; // Receive queue - this._sQbufferSize = 1024 * 10; // 10 KiB - // called in init: this._sQ = new Uint8Array(this._sQbufferSize); - this._sQlen = 0; - this._sQ = null; // Send queue + this._sQbufferSize = 1024 * 10; // 10 KiB + // called in init: this._sQ = new Uint8Array(this._sQbufferSize); + this._sQlen = 0; + this._sQ = null; // Send queue - this._eventHandlers = { - message: () => {}, - open: () => {}, - close: () => {}, - error: () => {} - }; + this._eventHandlers = { + message: () => {}, + open: () => {}, + close: () => {}, + error: () => {} + }; + } + + // Getters and Setters + get_sQ() { + return this._sQ; + } + + get_rQ() { + return this._rQ; + } + + get_rQi() { + return this._rQi; + } + + set_rQi(val) { + this._rQi = val; + } + + // Receive Queue + rQlen() { + return this._rQlen - this._rQi; + } + + rQpeek8() { + return this._rQ[this._rQi]; + } + + rQshift8() { + return this._rQ[this._rQi++]; + } + + rQskip8() { + this._rQi++; + } + + rQskipBytes(num) { + this._rQi += num; + } + + // TODO(directxman12): test performance with these vs a DataView + rQshift16() { + return (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + } + + rQshift32() { + return (this._rQ[this._rQi++] << 24) + + (this._rQ[this._rQi++] << 16) + + (this._rQ[this._rQi++] << 8) + + this._rQ[this._rQi++]; + } + + rQshiftStr(len) { + if (typeof (len) === 'undefined') { len = this.rQlen(); } + let str = ''; + // Handle large arrays in steps to avoid long strings on the stack + for (let i = 0; i < len; i += 4096) { + let part = this.rQshiftBytes(Math.min(4096, len - i)); + str += String.fromCharCode.apply(null, part); } + return str; + } - // Getters and Setters - get_sQ() { - return this._sQ; + rQshiftBytes(len) { + if (typeof (len) === 'undefined') { len = this.rQlen(); } + this._rQi += len; + return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + } + + rQshiftTo(target, len) { + if (len === undefined) { len = this.rQlen(); } + // TODO: make this just use set with views when using a ArrayBuffer to store the rQ + target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); + this._rQi += len; + } + + rQwhole() { + return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + } + + rQslice(start, end) { + if (end) { + return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); + } else { + return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); } + } - get_rQ() { - return this._rQ; - } - - get_rQi() { - return this._rQi; - } - - set_rQi(val) { - this._rQi = val; - } - - // Receive Queue - rQlen() { - return this._rQlen - this._rQi; - } - - rQpeek8() { - return this._rQ[this._rQi]; - } - - rQshift8() { - return this._rQ[this._rQi++]; - } - - rQskip8() { - this._rQi++; - } - - rQskipBytes(num) { - this._rQi += num; - } - - // TODO(directxman12): test performance with these vs a DataView - rQshift16() { - return (this._rQ[this._rQi++] << 8) + - this._rQ[this._rQi++]; - } - - rQshift32() { - return (this._rQ[this._rQi++] << 24) + - (this._rQ[this._rQi++] << 16) + - (this._rQ[this._rQi++] << 8) + - this._rQ[this._rQi++]; - } - - rQshiftStr(len) { - if (typeof(len) === 'undefined') { len = this.rQlen(); } - let str = ""; - // Handle large arrays in steps to avoid long strings on the stack - for (let i = 0; i < len; i += 4096) { - let part = this.rQshiftBytes(Math.min(4096, len - i)); - str += String.fromCharCode.apply(null, part); + // Check to see if we must wait for 'num' bytes (default to FBU.bytes) + // to be available in the receive queue. Return true if we need to + // wait (and possibly print a debug message), otherwise false. + rQwait(msg, num, goback) { + const rQlen = this._rQlen - this._rQi; // Skip rQlen() function call + if (rQlen < num) { + if (goback) { + if (this._rQi < goback) { + throw new Error('rQwait cannot backup ' + goback + ' bytes'); } - return str; + this._rQi -= goback; + } + return true; // true means need more data + } + return false; + } + + // Send Queue + + flush() { + if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { + this._websocket.send(this._encode_message()); + this._sQlen = 0; + } + } + + send(arr) { + this._sQ.set(arr, this._sQlen); + this._sQlen += arr.length; + this.flush(); + } + + send_string(str) { + this.send(str.split('').map(chr => chr.charCodeAt(0))); + } + + // Event Handlers + off(evt) { + this._eventHandlers[evt] = () => {}; + } + + on(evt, handler) { + this._eventHandlers[evt] = handler; + } + + _allocate_buffers() { + this._rQ = new Uint8Array(this._rQbufferSize); + this._sQ = new Uint8Array(this._sQbufferSize); + } + + init() { + this._allocate_buffers(); + this._rQi = 0; + this._websocket = null; + } + + open(uri, protocols) { + this.init(); + + this._websocket = new WebSocket(uri, protocols); + this._websocket.binaryType = 'arraybuffer'; + + this._websocket.onmessage = this._recv_message.bind(this); + this._websocket.onopen = () => { + Log.Debug('>> WebSock.onopen'); + if (this._websocket.protocol) { + Log.Info('Server choose sub-protocol: ' + this._websocket.protocol); + } + + this._eventHandlers.open(); + Log.Debug('<< WebSock.onopen'); + }; + this._websocket.onclose = (e) => { + Log.Debug('>> WebSock.onclose'); + this._eventHandlers.close(e); + Log.Debug('<< WebSock.onclose'); + }; + this._websocket.onerror = (e) => { + Log.Debug('>> WebSock.onerror: ' + e); + this._eventHandlers.error(e); + Log.Debug('<< WebSock.onerror: ' + e); + }; + } + + close() { + if (this._websocket) { + if ((this._websocket.readyState === WebSocket.OPEN) + || (this._websocket.readyState === WebSocket.CONNECTING)) { + Log.Info('Closing WebSocket connection'); + this._websocket.close(); + } + + this._websocket.onmessage = () => {}; + } + } + + // private methods + _encode_message() { + // Put in a binary arraybuffer + // according to the spec, you can send ArrayBufferViews with the send method + return new Uint8Array(this._sQ.buffer, 0, this._sQlen); + } + + _expand_compact_rQ(min_fit) { + const resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; + if (resizeNeeded) { + if (!min_fit) { + // just double the size if we need to do compaction + this._rQbufferSize *= 2; + } else { + // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 + this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; + } } - rQshiftBytes(len) { - if (typeof(len) === 'undefined') { len = this.rQlen(); } - this._rQi += len; - return new Uint8Array(this._rQ.buffer, this._rQi - len, len); + // we don't want to grow unboundedly + if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { + this._rQbufferSize = MAX_RQ_GROW_SIZE; + if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { + throw new Error('Receive Queue buffer exceeded ' + MAX_RQ_GROW_SIZE + ' bytes, and the new message could not fit'); + } } - rQshiftTo(target, len) { - if (len === undefined) { len = this.rQlen(); } - // TODO: make this just use set with views when using a ArrayBuffer to store the rQ - target.set(new Uint8Array(this._rQ.buffer, this._rQi, len)); - this._rQi += len; + if (resizeNeeded) { + const old_rQbuffer = this._rQ.buffer; + this._rQmax = this._rQbufferSize / 8; + this._rQ = new Uint8Array(this._rQbufferSize); + this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); + } else if (ENABLE_COPYWITHIN) { + this._rQ.copyWithin(0, this._rQi); + } else { + this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); } - rQwhole() { - return new Uint8Array(this._rQ.buffer, 0, this._rQlen); + this._rQlen = this._rQlen - this._rQi; + this._rQi = 0; + } + + _decode_message(data) { + // push arraybuffer values onto the end + const u8 = new Uint8Array(data); + if (u8.length > this._rQbufferSize - this._rQlen) { + this._expand_compact_rQ(u8.length); } + this._rQ.set(u8, this._rQlen); + this._rQlen += u8.length; + } - rQslice(start, end) { - if (end) { - return new Uint8Array(this._rQ.buffer, this._rQi + start, end - start); - } else { - return new Uint8Array(this._rQ.buffer, this._rQi + start, this._rQlen - this._rQi - start); - } - } - - // Check to see if we must wait for 'num' bytes (default to FBU.bytes) - // to be available in the receive queue. Return true if we need to - // wait (and possibly print a debug message), otherwise false. - rQwait(msg, num, goback) { - const rQlen = this._rQlen - this._rQi; // Skip rQlen() function call - if (rQlen < num) { - if (goback) { - if (this._rQi < goback) { - throw new Error("rQwait cannot backup " + goback + " bytes"); - } - this._rQi -= goback; - } - return true; // true means need more data - } - return false; - } - - // Send Queue - - flush() { - if (this._sQlen > 0 && this._websocket.readyState === WebSocket.OPEN) { - this._websocket.send(this._encode_message()); - this._sQlen = 0; - } - } - - send(arr) { - this._sQ.set(arr, this._sQlen); - this._sQlen += arr.length; - this.flush(); - } - - send_string(str) { - this.send(str.split('').map(chr => chr.charCodeAt(0))); - } - - // Event Handlers - off(evt) { - this._eventHandlers[evt] = () => {}; - } - - on(evt, handler) { - this._eventHandlers[evt] = handler; - } - - _allocate_buffers() { - this._rQ = new Uint8Array(this._rQbufferSize); - this._sQ = new Uint8Array(this._sQbufferSize); - } - - init() { - this._allocate_buffers(); + _recv_message(e) { + this._decode_message(e.data); + if (this.rQlen() > 0) { + this._eventHandlers.message(); + // Compact the receive queue + if (this._rQlen == this._rQi) { + this._rQlen = 0; this._rQi = 0; - this._websocket = null; - } - - open(uri, protocols) { - this.init(); - - this._websocket = new WebSocket(uri, protocols); - this._websocket.binaryType = 'arraybuffer'; - - this._websocket.onmessage = this._recv_message.bind(this); - this._websocket.onopen = () => { - Log.Debug('>> WebSock.onopen'); - if (this._websocket.protocol) { - Log.Info("Server choose sub-protocol: " + this._websocket.protocol); - } - - this._eventHandlers.open(); - Log.Debug("<< WebSock.onopen"); - }; - this._websocket.onclose = (e) => { - Log.Debug(">> WebSock.onclose"); - this._eventHandlers.close(e); - Log.Debug("<< WebSock.onclose"); - }; - this._websocket.onerror = (e) => { - Log.Debug(">> WebSock.onerror: " + e); - this._eventHandlers.error(e); - Log.Debug("<< WebSock.onerror: " + e); - }; - } - - close() { - if (this._websocket) { - if ((this._websocket.readyState === WebSocket.OPEN) || - (this._websocket.readyState === WebSocket.CONNECTING)) { - Log.Info("Closing WebSocket connection"); - this._websocket.close(); - } - - this._websocket.onmessage = () => {}; - } - } - - // private methods - _encode_message() { - // Put in a binary arraybuffer - // according to the spec, you can send ArrayBufferViews with the send method - return new Uint8Array(this._sQ.buffer, 0, this._sQlen); - } - - _expand_compact_rQ(min_fit) { - const resizeNeeded = min_fit || this._rQlen - this._rQi > this._rQbufferSize / 2; - if (resizeNeeded) { - if (!min_fit) { - // just double the size if we need to do compaction - this._rQbufferSize *= 2; - } else { - // otherwise, make sure we satisy rQlen - rQi + min_fit < rQbufferSize / 8 - this._rQbufferSize = (this._rQlen - this._rQi + min_fit) * 8; - } - } - - // we don't want to grow unboundedly - if (this._rQbufferSize > MAX_RQ_GROW_SIZE) { - this._rQbufferSize = MAX_RQ_GROW_SIZE; - if (this._rQbufferSize - this._rQlen - this._rQi < min_fit) { - throw new Error("Receive Queue buffer exceeded " + MAX_RQ_GROW_SIZE + " bytes, and the new message could not fit"); - } - } - - if (resizeNeeded) { - const old_rQbuffer = this._rQ.buffer; - this._rQmax = this._rQbufferSize / 8; - this._rQ = new Uint8Array(this._rQbufferSize); - this._rQ.set(new Uint8Array(old_rQbuffer, this._rQi)); - } else { - if (ENABLE_COPYWITHIN) { - this._rQ.copyWithin(0, this._rQi); - } else { - this._rQ.set(new Uint8Array(this._rQ.buffer, this._rQi)); - } - } - - this._rQlen = this._rQlen - this._rQi; - this._rQi = 0; - } - - _decode_message(data) { - // push arraybuffer values onto the end - const u8 = new Uint8Array(data); - if (u8.length > this._rQbufferSize - this._rQlen) { - this._expand_compact_rQ(u8.length); - } - this._rQ.set(u8, this._rQlen); - this._rQlen += u8.length; - } - - _recv_message(e) { - this._decode_message(e.data); - if (this.rQlen() > 0) { - this._eventHandlers.message(); - // Compact the receive queue - if (this._rQlen == this._rQi) { - this._rQlen = 0; - this._rQi = 0; - } else if (this._rQlen > this._rQmax) { - this._expand_compact_rQ(); - } - } else { - Log.Debug("Ignoring empty message"); - } + } else if (this._rQlen > this._rQmax) { + this._expand_compact_rQ(); + } + } else { + Log.Debug('Ignoring empty message'); } + } } diff --git a/po/.eslintrc b/po/.eslintrc new file mode 100644 index 00000000..b7dc129f --- /dev/null +++ b/po/.eslintrc @@ -0,0 +1,8 @@ +{ + "env": { + "node": true + }, + "rules": { + "no-console": 0 + } +} \ No newline at end of file diff --git a/po/po2js b/po/po2js index 65317162..5e998c7b 100755 --- a/po/po2js +++ b/po/po2js @@ -19,25 +19,25 @@ const getopt = require('node-getopt'); const fs = require('fs'); -const po2json = require("po2json"); +const po2json = require('po2json'); const opt = getopt.create([ - ['h' , 'help' , 'display this help'], + ['h', 'help', 'display this help'], ]).bindHelp().parseSystem(); if (opt.argv.length != 2) { - console.error("Incorrect number of arguments given"); + console.error('Incorrect number of arguments given'); process.exit(1); } const data = po2json.parseFileSync(opt.argv[0]); -const bodyPart = Object.keys(data).filter((msgid) => msgid !== "").map((msgid) => { - if (msgid === "") return; - const msgstr = data[msgid][1]; - return " " + JSON.stringify(msgid) + ": " + JSON.stringify(msgstr); -}).join(",\n"); +const bodyPart = Object.keys(data).filter(msgid => msgid !== '').map((msgid) => { + if (msgid === '') return; + const msgstr = data[msgid][1]; + return ' ' + JSON.stringify(msgid) + ': ' + JSON.stringify(msgstr); +}).join(',\n'); -const output = "{\n" + bodyPart + "\n}"; +const output = '{\n' + bodyPart + '\n}'; fs.writeFileSync(opt.argv[1], output); diff --git a/tests/assertions.js b/tests/assertions.js index ba931c22..7d8214f3 100644 --- a/tests/assertions.js +++ b/tests/assertions.js @@ -4,102 +4,106 @@ chai.use(sinonChai); // noVNC specific assertions chai.use(function (_chai, utils) { - _chai.Assertion.addMethod('displayed', function (target_data) { - const obj = this._obj; - const ctx = obj._target.getContext('2d'); - const data_cl = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data; - // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that - const data = new Uint8Array(data_cl); - const len = data_cl.length; - new chai.Assertion(len).to.be.equal(target_data.length, "unexpected display size"); - let same = true; - for (let i = 0; i < len; i++) { - if (data[i] != target_data[i]) { - same = false; - break; - } - } - if (!same) { - // eslint-disable-next-line no-console - console.log("expected data: %o, actual data: %o", target_data, data); - } - this.assert(same, - "expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}", - "expected #{this} not to have displayed the image #{act}", - target_data, - data); - }); + _chai.Assertion.addMethod('displayed', function (target_data) { + const obj = this._obj; + const ctx = obj._target.getContext('2d'); + const data_cl = ctx.getImageData(0, 0, obj._target.width, obj._target.height).data; + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray, so work around that + const data = new Uint8Array(data_cl); + const len = data_cl.length; + new chai.Assertion(len).to.be.equal(target_data.length, 'unexpected display size'); + let same = true; + for (let i = 0; i < len; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + if (!same) { + // eslint-disable-next-line no-console + console.log('expected data: %o, actual data: %o', target_data, data); + } + this.assert(same, + 'expected #{this} to have displayed the image #{exp}, but instead it displayed #{act}', + 'expected #{this} not to have displayed the image #{act}', + target_data, + data); + }); - _chai.Assertion.addMethod('sent', function (target_data) { + _chai.Assertion.addMethod('sent', function (target_data) { + const obj = this._obj; + obj.inspect = () => { + const res = { + _websocket: obj._websocket, + rQi: obj._rQi, + _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen), + _sQ: new Uint8Array(obj._sQ.buffer, 0, obj._sQlen) + }; + res.prototype = obj; + return res; + }; + const data = obj._websocket._get_sent_data(); + let same = true; + if (data.length != target_data.length) { + same = false; + } else { + for (let i = 0; i < data.length; i++) { + if (data[i] != target_data[i]) { + same = false; + break; + } + } + } + if (!same) { + // eslint-disable-next-line no-console + console.log('expected data: %o, actual data: %o', target_data, data); + } + this.assert(same, + 'expected #{this} to have sent the data #{exp}, but it actually sent #{act}', + 'expected #{this} not to have sent the data #{act}', + Array.prototype.slice.call(target_data), + Array.prototype.slice.call(data)); + }); + + _chai.Assertion.addProperty('array', function () { + utils.flag(this, 'array', true); + }); + + _chai.Assertion.overwriteMethod('equal', function (_super) { + return function assertArrayEqual(target) { + if (utils.flag(this, 'array')) { const obj = this._obj; - obj.inspect = () => { - const res = { _websocket: obj._websocket, rQi: obj._rQi, _rQ: new Uint8Array(obj._rQ.buffer, 0, obj._rQlen), - _sQ: new Uint8Array(obj._sQ.buffer, 0, obj._sQlen) }; - res.prototype = obj; - return res; - }; - const data = obj._websocket._get_sent_data(); + let same = true; - if (data.length != target_data.length) { - same = false; + + if (utils.flag(this, 'deep')) { + for (let i = 0; i < obj.length; i++) { + if (!utils.eql(obj[i], target[i])) { + same = false; + break; + } + } + + this.assert(same, + 'expected #{this} to have elements deeply equal to #{exp}', + 'expected #{this} not to have elements deeply equal to #{exp}', + Array.prototype.slice.call(target)); } else { - for (let i = 0; i < data.length; i++) { - if (data[i] != target_data[i]) { - same = false; - break; - } + for (let i = 0; i < obj.length; i++) { + if (obj[i] != target[i]) { + same = false; + break; } + } + + this.assert(same, + 'expected #{this} to have elements equal to #{exp}', + 'expected #{this} not to have elements equal to #{exp}', + Array.prototype.slice.call(target)); } - if (!same) { - // eslint-disable-next-line no-console - console.log("expected data: %o, actual data: %o", target_data, data); - } - this.assert(same, - "expected #{this} to have sent the data #{exp}, but it actually sent #{act}", - "expected #{this} not to have sent the data #{act}", - Array.prototype.slice.call(target_data), - Array.prototype.slice.call(data)); - }); - - _chai.Assertion.addProperty('array', function () { - utils.flag(this, 'array', true); - }); - - _chai.Assertion.overwriteMethod('equal', function (_super) { - return function assertArrayEqual(target) { - if (utils.flag(this, 'array')) { - const obj = this._obj; - - let same = true; - - if (utils.flag(this, 'deep')) { - for (let i = 0; i < obj.length; i++) { - if (!utils.eql(obj[i], target[i])) { - same = false; - break; - } - } - - this.assert(same, - "expected #{this} to have elements deeply equal to #{exp}", - "expected #{this} not to have elements deeply equal to #{exp}", - Array.prototype.slice.call(target)); - } else { - for (let i = 0; i < obj.length; i++) { - if (obj[i] != target[i]) { - same = false; - break; - } - } - - this.assert(same, - "expected #{this} to have elements equal to #{exp}", - "expected #{this} not to have elements equal to #{exp}", - Array.prototype.slice.call(target)); - } - } else { - _super.apply(this, arguments); - } - }; - }); + } else { + _super.apply(this, arguments); + } + }; + }); }); diff --git a/tests/fake.websocket.js b/tests/fake.websocket.js index 2e1a3b95..d8d9d1c0 100644 --- a/tests/fake.websocket.js +++ b/tests/fake.websocket.js @@ -2,69 +2,69 @@ import Base64 from '../core/base64.js'; // PhantomJS can't create Event objects directly, so we need to use this function make_event(name, props) { - const evt = document.createEvent('Event'); - evt.initEvent(name, true, true); - if (props) { - for (let prop in props) { - evt[prop] = props[prop]; - } + const evt = document.createEvent('Event'); + evt.initEvent(name, true, true); + if (props) { + for (let prop in props) { + evt[prop] = props[prop]; } - return evt; + } + return evt; } export default class FakeWebSocket { - constructor(uri, protocols) { - this.url = uri; - this.binaryType = "arraybuffer"; - this.extensions = ""; + constructor(uri, protocols) { + this.url = uri; + this.binaryType = 'arraybuffer'; + this.extensions = ''; - if (!protocols || typeof protocols === 'string') { - this.protocol = protocols; - } else { - this.protocol = protocols[0]; - } - - this._send_queue = new Uint8Array(20000); - - this.readyState = FakeWebSocket.CONNECTING; - this.bufferedAmount = 0; - - this.__is_fake = true; + if (!protocols || typeof protocols === 'string') { + this.protocol = protocols; + } else { + this.protocol = protocols[0]; } - close(code, reason) { - this.readyState = FakeWebSocket.CLOSED; - if (this.onclose) { - this.onclose(make_event("close", { 'code': code, 'reason': reason, 'wasClean': true })); - } - } + this._send_queue = new Uint8Array(20000); - send(data) { - if (this.protocol == 'base64') { - data = Base64.decode(data); - } else { - data = new Uint8Array(data); - } - this._send_queue.set(data, this.bufferedAmount); - this.bufferedAmount += data.length; - } + this.readyState = FakeWebSocket.CONNECTING; + this.bufferedAmount = 0; - _get_sent_data() { - const res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount); - this.bufferedAmount = 0; - return res; - } + this.__is_fake = true; + } - _open() { - this.readyState = FakeWebSocket.OPEN; - if (this.onopen) { - this.onopen(make_event('open')); - } + close(code, reason) { + this.readyState = FakeWebSocket.CLOSED; + if (this.onclose) { + this.onclose(make_event('close', { code: code, reason: reason, wasClean: true })); } + } - _receive_data(data) { - this.onmessage(make_event("message", { 'data': data })); + send(data) { + if (this.protocol == 'base64') { + data = Base64.decode(data); + } else { + data = new Uint8Array(data); } + this._send_queue.set(data, this.bufferedAmount); + this.bufferedAmount += data.length; + } + + _get_sent_data() { + const res = new Uint8Array(this._send_queue.buffer, 0, this.bufferedAmount); + this.bufferedAmount = 0; + return res; + } + + _open() { + this.readyState = FakeWebSocket.OPEN; + if (this.onopen) { + this.onopen(make_event('open')); + } + } + + _receive_data(data) { + this.onmessage(make_event('message', { data: data })); + } } FakeWebSocket.OPEN = WebSocket.OPEN; @@ -75,17 +75,17 @@ FakeWebSocket.CLOSED = WebSocket.CLOSED; FakeWebSocket.__is_fake = true; FakeWebSocket.replace = () => { - if (!WebSocket.__is_fake) { - const real_version = WebSocket; - // eslint-disable-next-line no-global-assign - WebSocket = FakeWebSocket; - FakeWebSocket.__real_version = real_version; - } + if (!WebSocket.__is_fake) { + const real_version = WebSocket; + // eslint-disable-next-line no-global-assign + WebSocket = FakeWebSocket; + FakeWebSocket.__real_version = real_version; + } }; FakeWebSocket.restore = () => { - if (WebSocket.__is_fake) { - // eslint-disable-next-line no-global-assign - WebSocket = WebSocket.__real_version; - } + if (WebSocket.__is_fake) { + // eslint-disable-next-line no-global-assign + WebSocket = WebSocket.__real_version; + } }; diff --git a/tests/karma-test-main.js b/tests/karma-test-main.js index 334b771c..576e8263 100644 --- a/tests/karma-test-main.js +++ b/tests/karma-test-main.js @@ -3,14 +3,14 @@ const allTestFiles = []; const extraFiles = ['/base/tests/assertions.js']; Object.keys(window.__karma__.files).forEach(function (file) { - if (TEST_REGEXP.test(file)) { - // TODO: normalize? - allTestFiles.push(file); - } + if (TEST_REGEXP.test(file)) { + // TODO: normalize? + allTestFiles.push(file); + } }); require.config({ - baseUrl: '/base', - deps: allTestFiles.concat(extraFiles), - callback: window.__karma__.start, + baseUrl: '/base', + deps: allTestFiles.concat(extraFiles), + callback: window.__karma__.start, }); diff --git a/tests/playback-ui.js b/tests/playback-ui.js index 295f983c..bbf0a3d4 100644 --- a/tests/playback-ui.js +++ b/tests/playback-ui.js @@ -7,162 +7,161 @@ let frames = null; let encoding = null; function message(str) { - const cell = document.getElementById('messages'); - cell.textContent += str + "\n"; - cell.scrollTop = cell.scrollHeight; + const cell = document.getElementById('messages'); + cell.textContent += str + '\n'; + cell.scrollTop = cell.scrollHeight; } function loadFile() { - const fname = WebUtil.getQueryVar('data', null); + const fname = WebUtil.getQueryVar('data', null); - if (!fname) { - return Promise.reject("Must specify data=FOO in query string."); - } + if (!fname) { + return Promise.reject('Must specify data=FOO in query string.'); + } - message("Loading " + fname); + message('Loading ' + fname); - return new Promise((resolve, reject) => { - const script = document.createElement("script"); - script.onload = resolve; - script.onerror = reject; - document.body.appendChild(script); - script.src = "../recordings/" + fname; - }); + return new Promise((resolve, reject) => { + const script = document.createElement('script'); + script.onload = resolve; + script.onerror = reject; + document.body.appendChild(script); + script.src = '../recordings/' + fname; + }); } function enableUI() { - const iterations = WebUtil.getQueryVar('iterations', 3); - document.getElementById('iterations').value = iterations; + const iterations = WebUtil.getQueryVar('iterations', 3); + document.getElementById('iterations').value = iterations; - const mode = WebUtil.getQueryVar('mode', 3); - if (mode === 'realtime') { - document.getElementById('mode2').checked = true; - } else { - document.getElementById('mode1').checked = true; - } + const mode = WebUtil.getQueryVar('mode', 3); + if (mode === 'realtime') { + document.getElementById('mode2').checked = true; + } else { + document.getElementById('mode1').checked = true; + } - message("VNC_frame_data.length: " + VNC_frame_data.length); + message('VNC_frame_data.length: ' + VNC_frame_data.length); - const startButton = document.getElementById('startButton'); - startButton.disabled = false; - startButton.addEventListener('click', start); + const startButton = document.getElementById('startButton'); + startButton.disabled = false; + startButton.addEventListener('click', start); - frames = VNC_frame_data; - // Only present in older recordings - if (window.VNC_frame_encoding) - encoding = VNC_frame_encoding; + frames = VNC_frame_data; + // Only present in older recordings + if (window.VNC_frame_encoding) encoding = VNC_frame_encoding; } class IterationPlayer { - constructor(iterations, frames, encoding) { - this._iterations = iterations; + constructor(iterations, frames, encoding) { + this._iterations = iterations; - this._iteration = undefined; - this._player = undefined; + this._iteration = undefined; + this._player = undefined; - this._start_time = undefined; + this._start_time = undefined; - this._frames = frames; - this._encoding = encoding; + this._frames = frames; + this._encoding = encoding; - this._state = 'running'; + this._state = 'running'; - this.onfinish = () => {}; - this.oniterationfinish = () => {}; - this.rfbdisconnected = () => {}; + this.onfinish = () => {}; + this.oniterationfinish = () => {}; + this.rfbdisconnected = () => {}; + } + + start(mode) { + this._iteration = 0; + this._start_time = (new Date()).getTime(); + + this._realtime = mode.startsWith('realtime'); + this._trafficMgmt = !mode.endsWith('-no-mgmt'); + + this._nextIteration(); + } + + _nextIteration() { + const player = new RecordingPlayer(this._frames, this._encoding, this._disconnected.bind(this)); + player.onfinish = this._iterationFinish.bind(this); + + if (this._state !== 'running') { return; } + + this._iteration++; + if (this._iteration > this._iterations) { + this._finish(); + return; } - start(mode) { - this._iteration = 0; - this._start_time = (new Date()).getTime(); + player.run(this._realtime, this._trafficMgmt); + } - this._realtime = mode.startsWith('realtime'); - this._trafficMgmt = !mode.endsWith('-no-mgmt'); + _finish() { + const endTime = (new Date()).getTime(); + const totalDuration = endTime - this._start_time; - this._nextIteration(); + const evt = new Event('finish'); + evt.duration = totalDuration; + evt.iterations = this._iterations; + this.onfinish(evt); + } + + _iterationFinish(duration) { + const evt = new Event('iterationfinish'); + evt.duration = duration; + evt.number = this._iteration; + this.oniterationfinish(evt); + + this._nextIteration(); + } + + _disconnected(clean, frame) { + if (!clean) { + this._state = 'failed'; } - _nextIteration() { - const player = new RecordingPlayer(this._frames, this._encoding, this._disconnected.bind(this)); - player.onfinish = this._iterationFinish.bind(this); + const evt = new Event('rfbdisconnected'); + evt.clean = clean; + evt.frame = frame; + evt.iteration = this._iteration; - if (this._state !== 'running') { return; } - - this._iteration++; - if (this._iteration > this._iterations) { - this._finish(); - return; - } - - player.run(this._realtime, this._trafficMgmt); - } - - _finish() { - const endTime = (new Date()).getTime(); - const totalDuration = endTime - this._start_time; - - const evt = new Event('finish'); - evt.duration = totalDuration; - evt.iterations = this._iterations; - this.onfinish(evt); - } - - _iterationFinish(duration) { - const evt = new Event('iterationfinish'); - evt.duration = duration; - evt.number = this._iteration; - this.oniterationfinish(evt); - - this._nextIteration(); - } - - _disconnected(clean, frame) { - if (!clean) { - this._state = 'failed'; - } - - const evt = new Event('rfbdisconnected'); - evt.clean = clean; - evt.frame = frame; - evt.iteration = this._iteration; - - this.onrfbdisconnected(evt); - } + this.onrfbdisconnected(evt); + } } function start() { - document.getElementById('startButton').value = "Running"; - document.getElementById('startButton').disabled = true; + document.getElementById('startButton').value = 'Running'; + document.getElementById('startButton').disabled = true; - const iterations = document.getElementById('iterations').value; + const iterations = document.getElementById('iterations').value; - let mode; + let mode; - if (document.getElementById('mode1').checked) { - message(`Starting performance playback (fullspeed) [${iterations} iteration(s)]`); - mode = 'perftest'; - } else { - message(`Starting realtime playback [${iterations} iteration(s)]`); - mode = 'realtime'; + if (document.getElementById('mode1').checked) { + message(`Starting performance playback (fullspeed) [${iterations} iteration(s)]`); + mode = 'perftest'; + } else { + message(`Starting realtime playback [${iterations} iteration(s)]`); + mode = 'realtime'; + } + + const player = new IterationPlayer(iterations, frames, encoding); + player.oniterationfinish = (evt) => { + message(`Iteration ${evt.number} took ${evt.duration}ms`); + }; + player.onrfbdisconnected = (evt) => { + if (!evt.clean) { + message(`noVNC sent disconnected during iteration ${evt.iteration} frame ${evt.frame}`); } + }; + player.onfinish = (evt) => { + const iterTime = parseInt(evt.duration / evt.iterations, 10); + message(`${evt.iterations} iterations took ${evt.duration}ms (average ${iterTime}ms / iteration)`); - const player = new IterationPlayer(iterations, frames, encoding); - player.oniterationfinish = (evt) => { - message(`Iteration ${evt.number} took ${evt.duration}ms`); - }; - player.onrfbdisconnected = (evt) => { - if (!evt.clean) { - message(`noVNC sent disconnected during iteration ${evt.iteration} frame ${evt.frame}`); - } - }; - player.onfinish = (evt) => { - const iterTime = parseInt(evt.duration / evt.iterations, 10); - message(`${evt.iterations} iterations took ${evt.duration}ms (average ${iterTime}ms / iteration)`); - - document.getElementById('startButton').disabled = false; - document.getElementById('startButton').value = "Start"; - }; - player.start(mode); + document.getElementById('startButton').disabled = false; + document.getElementById('startButton').value = 'Start'; + }; + player.start(mode); } -loadFile().then(enableUI).catch(e => message("Error loading recording: " + e)); +loadFile().then(enableUI).catch(e => message('Error loading recording: ' + e)); diff --git a/tests/playback.js b/tests/playback.js index c48a2a4e..9021f308 100644 --- a/tests/playback.js +++ b/tests/playback.js @@ -10,185 +10,185 @@ import Base64 from '../core/base64.js'; // Immediate polyfill if (window.setImmediate === undefined) { - let _immediateIdCounter = 1; - const _immediateFuncs = {}; + let _immediateIdCounter = 1; + const _immediateFuncs = {}; - window.setImmediate = (func) => { - const index = _immediateIdCounter++; - _immediateFuncs[index] = func; - window.postMessage("noVNC immediate trigger:" + index, "*"); - return index; - }; + window.setImmediate = (func) => { + const index = _immediateIdCounter++; + _immediateFuncs[index] = func; + window.postMessage('noVNC immediate trigger:' + index, '*'); + return index; + }; - window.clearImmediate = (id) => { - _immediateFuncs[id]; - }; + window.clearImmediate = (id) => { + _immediateFuncs[id]; + }; - window.addEventListener("message", (event) => { - if ((typeof event.data !== "string") || - (event.data.indexOf("noVNC immediate trigger:") !== 0)) { - return; - } + window.addEventListener('message', (event) => { + if ((typeof event.data !== 'string') + || (event.data.indexOf('noVNC immediate trigger:') !== 0)) { + return; + } - const index = event.data.slice("noVNC immediate trigger:".length); + const index = event.data.slice('noVNC immediate trigger:'.length); - const callback = _immediateFuncs[index]; - if (callback === undefined) { - return; - } + const callback = _immediateFuncs[index]; + if (callback === undefined) { + return; + } - delete _immediateFuncs[index]; + delete _immediateFuncs[index]; - callback(); - }); + callback(); + }); } export default class RecordingPlayer { - constructor(frames, encoding, disconnected) { - this._frames = frames; - this._encoding = encoding; + constructor(frames, encoding, disconnected) { + this._frames = frames; + this._encoding = encoding; - this._disconnected = disconnected; + this._disconnected = disconnected; - if (this._encoding === undefined) { - const frame = this._frames[0]; - const start = frame.indexOf('{', 1) + 1; - if (frame.slice(start).startsWith('UkZC')) { - this._encoding = 'base64'; - } else { - this._encoding = 'binary'; - } - } - - this._rfb = undefined; - this._frame_length = this._frames.length; - - this._frame_index = 0; - this._start_time = undefined; - this._realtime = true; - this._trafficManagement = true; - - this._running = false; - - this.onfinish = () => {}; + if (this._encoding === undefined) { + const frame = this._frames[0]; + const start = frame.indexOf('{', 1) + 1; + if (frame.slice(start).startsWith('UkZC')) { + this._encoding = 'base64'; + } else { + this._encoding = 'binary'; + } } - run(realtime, trafficManagement) { - // initialize a new RFB - this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test'); - this._rfb.viewOnly = true; - this._rfb.addEventListener("disconnect", - this._handleDisconnect.bind(this)); - this._enablePlaybackMode(); + this._rfb = undefined; + this._frame_length = this._frames.length; - // reset the frame index and timer - this._frame_index = 0; - this._start_time = (new Date()).getTime(); + this._frame_index = 0; + this._start_time = undefined; + this._realtime = true; + this._trafficManagement = true; - this._realtime = realtime; - this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement; + this._running = false; - this._running = true; + this.onfinish = () => {}; + } - this._queueNextPacket(); + run(realtime, trafficManagement) { + // initialize a new RFB + this._rfb = new RFB(document.getElementById('VNC_screen'), 'wss://test'); + this._rfb.viewOnly = true; + this._rfb.addEventListener('disconnect', + this._handleDisconnect.bind(this)); + this._enablePlaybackMode(); + + // reset the frame index and timer + this._frame_index = 0; + this._start_time = (new Date()).getTime(); + + this._realtime = realtime; + this._trafficManagement = (trafficManagement === undefined) ? !realtime : trafficManagement; + + this._running = true; + + this._queueNextPacket(); + } + + // _enablePlaybackMode mocks out things not required for running playback + _enablePlaybackMode() { + this._rfb._sock.send = () => {}; + this._rfb._sock.close = () => {}; + this._rfb._sock.flush = () => {}; + this._rfb._sock.open = function () { + this.init(); + this._eventHandlers.open(); + }; + } + + _queueNextPacket() { + if (!this._running) { return; } + + let frame = this._frames[this._frame_index]; + + // skip send frames + while (this._frame_index < this._frame_length && frame.charAt(0) === '}') { + this._frame_index++; + frame = this._frames[this._frame_index]; } - // _enablePlaybackMode mocks out things not required for running playback - _enablePlaybackMode() { - this._rfb._sock.send = () => {}; - this._rfb._sock.close = () => {}; - this._rfb._sock.flush = () => {}; - this._rfb._sock.open = function () { - this.init(); - this._eventHandlers.open(); - }; + if (frame === 'EOF') { + Log.Debug('Finished, found EOF'); + this._finish(); + return; } - _queueNextPacket() { - if (!this._running) { return; } - - let frame = this._frames[this._frame_index]; - - // skip send frames - while (this._frame_index < this._frame_length && frame.charAt(0) === "}") { - this._frame_index++; - frame = this._frames[this._frame_index]; - } - - if (frame === 'EOF') { - Log.Debug('Finished, found EOF'); - this._finish(); - return; - } - - if (this._frame_index >= this._frame_length) { - Log.Debug('Finished, no more frames'); - this._finish(); - return; - } - - if (this._realtime) { - const foffset = frame.slice(1, frame.indexOf('{', 1)); - const toffset = (new Date()).getTime() - this._start_time; - let delay = foffset - toffset; - if (delay < 1) delay = 1; - - setTimeout(this._doPacket.bind(this), delay); - } else { - setImmediate(this._doPacket.bind(this)); - } + if (this._frame_index >= this._frame_length) { + Log.Debug('Finished, no more frames'); + this._finish(); + return; } - _doPacket() { - // Avoid having excessive queue buildup in non-realtime mode - if (this._trafficManagement && this._rfb._flushing) { - const orig = this._rfb._display.onflush; - this._rfb._display.onflush = () => { - this._rfb._display.onflush = orig; - this._rfb._onFlush(); - this._doPacket(); - }; - return; - } + if (this._realtime) { + const foffset = frame.slice(1, frame.indexOf('{', 1)); + const toffset = (new Date()).getTime() - this._start_time; + let delay = foffset - toffset; + if (delay < 1) delay = 1; - const frame = this._frames[this._frame_index]; - let start = frame.indexOf('{', 1) + 1; - let u8; - if (this._encoding === 'base64') { - u8 = Base64.decode(frame.slice(start)); - start = 0; - } else { - u8 = new Uint8Array(frame.length - start); - for (let i = 0; i < frame.length - start; i++) { - u8[i] = frame.charCodeAt(start + i); - } - } + setTimeout(this._doPacket.bind(this), delay); + } else { + setImmediate(this._doPacket.bind(this)); + } + } - this._rfb._sock._recv_message({'data': u8}); - this._frame_index++; - - this._queueNextPacket(); + _doPacket() { + // Avoid having excessive queue buildup in non-realtime mode + if (this._trafficManagement && this._rfb._flushing) { + const orig = this._rfb._display.onflush; + this._rfb._display.onflush = () => { + this._rfb._display.onflush = orig; + this._rfb._onFlush(); + this._doPacket(); + }; + return; } - _finish() { - if (this._rfb._display.pending()) { - this._rfb._display.onflush = () => { - if (this._rfb._flushing) { - this._rfb._onFlush(); - } - this._finish(); - }; - this._rfb._display.flush(); - } else { - this._running = false; - this._rfb._sock._eventHandlers.close({code: 1000, reason: ""}); - delete this._rfb; - this.onfinish((new Date()).getTime() - this._start_time); - } + const frame = this._frames[this._frame_index]; + let start = frame.indexOf('{', 1) + 1; + let u8; + if (this._encoding === 'base64') { + u8 = Base64.decode(frame.slice(start)); + start = 0; + } else { + u8 = new Uint8Array(frame.length - start); + for (let i = 0; i < frame.length - start; i++) { + u8[i] = frame.charCodeAt(start + i); + } } - _handleDisconnect(evt) { - this._running = false; - this._disconnected(evt.detail.clean, this._frame_index); + this._rfb._sock._recv_message({ data: u8 }); + this._frame_index++; + + this._queueNextPacket(); + } + + _finish() { + if (this._rfb._display.pending()) { + this._rfb._display.onflush = () => { + if (this._rfb._flushing) { + this._rfb._onFlush(); + } + this._finish(); + }; + this._rfb._display.flush(); + } else { + this._running = false; + this._rfb._sock._eventHandlers.close({ code: 1000, reason: '' }); + delete this._rfb; + this.onfinish((new Date()).getTime() - this._start_time); } + } + + _handleDisconnect(evt) { + this._running = false; + this._disconnected(evt.detail.clean, this._frame_index); + } } diff --git a/tests/test.base64.js b/tests/test.base64.js index a9724489..b6369286 100644 --- a/tests/test.base64.js +++ b/tests/test.base64.js @@ -2,32 +2,32 @@ const expect = chai.expect; import Base64 from '../core/base64.js'; -describe('Base64 Tools', function() { - "use strict"; +describe('Base64 Tools', function () { + 'use strict'; - const BIN_ARR = new Array(256); - for (let i = 0; i < 256; i++) { - BIN_ARR[i] = i; - } + const BIN_ARR = new Array(256); + for (let i = 0; i < 256; i++) { + BIN_ARR[i] = i; + } - const B64_STR = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=="; + const B64_STR = 'AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7PD0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn+AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6/wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w=='; - describe('encode', function() { - it('should encode a binary string into Base64', function() { - const encoded = Base64.encode(BIN_ARR); - expect(encoded).to.equal(B64_STR); - }); + describe('encode', function () { + it('should encode a binary string into Base64', function () { + const encoded = Base64.encode(BIN_ARR); + expect(encoded).to.equal(B64_STR); + }); + }); + + describe('decode', function () { + it('should decode a Base64 string into a normal string', function () { + const decoded = Base64.decode(B64_STR); + expect(decoded).to.deep.equal(BIN_ARR); }); - describe('decode', function() { - it('should decode a Base64 string into a normal string', function() { - const decoded = Base64.decode(B64_STR); - expect(decoded).to.deep.equal(BIN_ARR); - }); - - it('should throw an error if we have extra characters at the end of the string', function() { - expect(() => Base64.decode(B64_STR+'abcdef')).to.throw(Error); - }); + it('should throw an error if we have extra characters at the end of the string', function () { + expect(() => Base64.decode(B64_STR + 'abcdef')).to.throw(Error); }); + }); }); diff --git a/tests/test.display.js b/tests/test.display.js index e3810eff..3324f056 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -6,483 +6,497 @@ import Display from '../core/display.js'; import sinon from '../vendor/sinon.js'; describe('Display/Canvas Helper', function () { - const checked_data = new Uint8Array([ - 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, - 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, - 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 - ]); + const checked_data = new Uint8Array([ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]); - const basic_data = new Uint8Array([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]); + const basic_data = new Uint8Array([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]); - function make_image_canvas (input_data) { - const canvas = document.createElement('canvas'); - canvas.width = 4; - canvas.height = 4; - const ctx = canvas.getContext('2d'); - const data = ctx.createImageData(4, 4); - for (let i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; } - ctx.putImageData(data, 0, 0); - return canvas; - } + function make_image_canvas(input_data) { + const canvas = document.createElement('canvas'); + canvas.width = 4; + canvas.height = 4; + const ctx = canvas.getContext('2d'); + const data = ctx.createImageData(4, 4); + for (let i = 0; i < checked_data.length; i++) { data.data[i] = input_data[i]; } + ctx.putImageData(data, 0, 0); + return canvas; + } - function make_image_png (input_data) { - const canvas = make_image_canvas(input_data); - const url = canvas.toDataURL(); - const data = url.split(",")[1]; - return Base64.decode(data); - } + function make_image_png(input_data) { + const canvas = make_image_canvas(input_data); + const url = canvas.toDataURL(); + const data = url.split(',')[1]; + return Base64.decode(data); + } - describe('viewport handling', function () { - let display; - beforeEach(function () { - display = new Display(document.createElement('canvas')); - display.clipViewport = true; - display.resize(5, 5); - display.viewportChangeSize(3, 3); - display.viewportChangePos(1, 1); - }); - - it('should take viewport location into consideration when drawing images', function () { - display.resize(4, 4); - display.viewportChangeSize(2, 2); - display.drawImage(make_image_canvas(basic_data), 1, 1); - display.flip(); - - const expected = new Uint8Array(16); - for (let i = 0; i < 8; i++) { expected[i] = basic_data[i]; } - for (let i = 8; i < 16; i++) { expected[i] = 0; } - expect(display).to.have.displayed(expected); - }); - - it('should resize the target canvas when resizing the viewport', function() { - display.viewportChangeSize(2, 2); - expect(display._target.width).to.equal(2); - expect(display._target.height).to.equal(2); - }); - - it('should move the viewport if necessary', function() { - display.viewportChangeSize(5, 5); - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(5); - expect(display._target.height).to.equal(5); - }); - - it('should limit the viewport to the framebuffer size', function() { - display.viewportChangeSize(6, 6); - expect(display._target.width).to.equal(5); - expect(display._target.height).to.equal(5); - }); - - it('should redraw when moving the viewport', function () { - display.flip = sinon.spy(); - display.viewportChangePos(-1, 1); - expect(display.flip).to.have.been.calledOnce; - }); - - it('should redraw when resizing the viewport', function () { - display.flip = sinon.spy(); - display.viewportChangeSize(2, 2); - expect(display.flip).to.have.been.calledOnce; - }); - - it('should show the entire framebuffer when disabling the viewport', function() { - display.clipViewport = false; - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(5); - expect(display._target.height).to.equal(5); - }); - - it('should ignore viewport changes when the viewport is disabled', function() { - display.clipViewport = false; - display.viewportChangeSize(2, 2); - display.viewportChangePos(1, 1); - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(5); - expect(display._target.height).to.equal(5); - }); - - it('should show the entire framebuffer just after enabling the viewport', function() { - display.clipViewport = false; - display.clipViewport = true; - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(5); - expect(display._target.height).to.equal(5); - }); + describe('viewport handling', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.clipViewport = true; + display.resize(5, 5); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); }); - describe('resizing', function () { - let display; - beforeEach(function () { - display = new Display(document.createElement('canvas')); - display.clipViewport = false; - display.resize(4, 4); - }); + it('should take viewport location into consideration when drawing images', function () { + display.resize(4, 4); + display.viewportChangeSize(2, 2); + display.drawImage(make_image_canvas(basic_data), 1, 1); + display.flip(); - it('should change the size of the logical canvas', function () { - display.resize(5, 7); - expect(display._fb_width).to.equal(5); - expect(display._fb_height).to.equal(7); - }); - - it('should keep the framebuffer data', function () { - display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); - display.resize(2, 2); - display.flip(); - const expected = []; - for (let i = 0; i < 4 * 2*2; i += 4) { - expected[i] = 0xff; - expected[i+1] = expected[i+2] = 0; - expected[i+3] = 0xff; - } - expect(display).to.have.displayed(new Uint8Array(expected)); - }); - - describe('viewport', function () { - beforeEach(function () { - display.clipViewport = true; - display.viewportChangeSize(3, 3); - display.viewportChangePos(1, 1); - }); - - it('should keep the viewport position and size if possible', function () { - display.resize(6, 6); - expect(display.absX(0)).to.equal(1); - expect(display.absY(0)).to.equal(1); - expect(display._target.width).to.equal(3); - expect(display._target.height).to.equal(3); - }); - - it('should move the viewport if necessary', function () { - display.resize(3, 3); - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(3); - expect(display._target.height).to.equal(3); - }); - - it('should shrink the viewport if necessary', function () { - display.resize(2, 2); - expect(display.absX(0)).to.equal(0); - expect(display.absY(0)).to.equal(0); - expect(display._target.width).to.equal(2); - expect(display._target.height).to.equal(2); - }); - }); + const expected = new Uint8Array(16); + for (let i = 0; i < 8; i++) { expected[i] = basic_data[i]; } + for (let i = 8; i < 16; i++) { expected[i] = 0; } + expect(display).to.have.displayed(expected); }); - describe('rescaling', function () { - let display; - let canvas; - - beforeEach(function () { - canvas = document.createElement('canvas'); - display = new Display(canvas); - display.clipViewport = true; - display.resize(4, 4); - display.viewportChangeSize(3, 3); - display.viewportChangePos(1, 1); - document.body.appendChild(canvas); - }); - - afterEach(function () { - document.body.removeChild(canvas); - }); - - it('should not change the bitmap size of the canvas', function () { - display.scale = 2.0; - expect(canvas.width).to.equal(3); - expect(canvas.height).to.equal(3); - }); - - it('should change the effective rendered size of the canvas', function () { - display.scale = 2.0; - expect(canvas.clientWidth).to.equal(6); - expect(canvas.clientHeight).to.equal(6); - }); - - it('should not change when resizing', function () { - display.scale = 2.0; - display.resize(5, 5); - expect(display.scale).to.equal(2.0); - expect(canvas.width).to.equal(3); - expect(canvas.height).to.equal(3); - expect(canvas.clientWidth).to.equal(6); - expect(canvas.clientHeight).to.equal(6); - }); + it('should resize the target canvas when resizing the viewport', function () { + display.viewportChangeSize(2, 2); + expect(display._target.width).to.equal(2); + expect(display._target.height).to.equal(2); }); - describe('autoscaling', function () { - let display; - let canvas; - - beforeEach(function () { - canvas = document.createElement('canvas'); - display = new Display(canvas); - display.clipViewport = true; - display.resize(4, 3); - document.body.appendChild(canvas); - }); - - afterEach(function () { - document.body.removeChild(canvas); - }); - - it('should preserve aspect ratio while autoscaling', function () { - display.autoscale(16, 9); - expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3); - }); - - it('should use width to determine scale when the current aspect ratio is wider than the target', function () { - display.autoscale(9, 16); - expect(display.absX(9)).to.equal(4); - expect(display.absY(18)).to.equal(8); - expect(canvas.clientWidth).to.equal(9); - expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) - }); - - it('should use height to determine scale when the current aspect ratio is taller than the target', function () { - display.autoscale(16, 9); - expect(display.absX(9)).to.equal(3); - expect(display.absY(18)).to.equal(6); - expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) - expect(canvas.clientHeight).to.equal(9); - - }); - - it('should not change the bitmap size of the canvas', function () { - display.autoscale(16, 9); - expect(canvas.width).to.equal(4); - expect(canvas.height).to.equal(3); - }); + it('should move the viewport if necessary', function () { + display.viewportChangeSize(5, 5); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); }); - describe('drawing', function () { - - // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the - // basic cases - let display; - beforeEach(function () { - display = new Display(document.createElement('canvas')); - display.resize(4, 4); - }); - - it('should clear the screen on #clear without a logo set', function () { - display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); - display._logo = null; - display.clear(); - display.resize(4, 4); - const empty = []; - for (let i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; } - expect(display).to.have.displayed(new Uint8Array(empty)); - }); - - it('should draw the logo on #clear with a logo set', function (done) { - display._logo = { width: 4, height: 4, type: "image/png", data: make_image_png(checked_data) }; - display.clear(); - display.onflush = () => { - expect(display).to.have.displayed(checked_data); - expect(display._fb_width).to.equal(4); - expect(display._fb_height).to.equal(4); - done(); - }; - display.flush(); - }); - - it('should not draw directly on the target canvas', function () { - display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); - display.flip(); - display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); - const expected = []; - for (let i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) { - expected[i] = 0xff; - expected[i+1] = expected[i+2] = 0; - expected[i+3] = 0xff; - } - expect(display).to.have.displayed(new Uint8Array(expected)); - }); - - it('should support filling a rectangle with particular color via #fillRect', function () { - display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); - display.fillRect(0, 0, 2, 2, [0xff, 0, 0]); - display.fillRect(2, 2, 2, 2, [0xff, 0, 0]); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); - - it('should support copying an portion of the canvas via #copyImage', function () { - display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); - display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]); - display.copyImage(0, 0, 2, 2, 2, 2); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); - - it('should support drawing images via #imageRect', function (done) { - display.imageRect(0, 0, "image/png", make_image_png(checked_data)); - display.flip(); - display.onflush = () => { - expect(display).to.have.displayed(checked_data); - done(); - }; - display.flush(); - }); - - it('should support drawing tile data with a background color and sub tiles', function () { - display.startTile(0, 0, 4, 4, [0, 0xff, 0]); - display.subTile(0, 0, 2, 2, [0xff, 0, 0]); - display.subTile(2, 2, 2, 2, [0xff, 0, 0]); - display.finishTile(); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); - - // We have a special cache for 16x16 tiles that we need to test - it('should support drawing a 16x16 tile', function () { - const large_checked_data = new Uint8Array(16*16*4); - display.resize(16, 16); - - for (let y = 0;y < 16;y++) { - for (let x = 0;x < 16;x++) { - let pixel; - if ((x < 4) && (y < 4)) { - // NB: of course IE11 doesn't support #slice on ArrayBufferViews... - pixel = Array.prototype.slice.call(checked_data, (y*4+x)*4, (y*4+x+1)*4); - } else { - pixel = [0, 0xff, 0, 255]; - } - large_checked_data.set(pixel, (y*16+x)*4); - } - } - - display.startTile(0, 0, 16, 16, [0, 0xff, 0]); - display.subTile(0, 0, 2, 2, [0xff, 0, 0]); - display.subTile(2, 2, 2, 2, [0xff, 0, 0]); - display.finishTile(); - display.flip(); - expect(display).to.have.displayed(large_checked_data); - }); - - it('should support drawing BGRX blit images with true color via #blitImage', function () { - const data = []; - for (let i = 0; i < 16; i++) { - data[i * 4] = checked_data[i * 4 + 2]; - data[i * 4 + 1] = checked_data[i * 4 + 1]; - data[i * 4 + 2] = checked_data[i * 4]; - data[i * 4 + 3] = checked_data[i * 4 + 3]; - } - display.blitImage(0, 0, 4, 4, data, 0); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); - - it('should support drawing RGB blit images with true color via #blitRgbImage', function () { - const data = []; - for (let i = 0; i < 16; i++) { - data[i * 3] = checked_data[i * 4]; - data[i * 3 + 1] = checked_data[i * 4 + 1]; - data[i * 3 + 2] = checked_data[i * 4 + 2]; - } - display.blitRgbImage(0, 0, 4, 4, data, 0); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); - - it('should support drawing an image object via #drawImage', function () { - const img = make_image_canvas(checked_data); - display.drawImage(img, 0, 0); - display.flip(); - expect(display).to.have.displayed(checked_data); - }); + it('should limit the viewport to the framebuffer size', function () { + display.viewportChangeSize(6, 6); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); }); - describe('the render queue processor', function () { - let display; - beforeEach(function () { - display = new Display(document.createElement('canvas')); - display.resize(4, 4); - sinon.spy(display, '_scan_renderQ'); - }); - - afterEach(function () { - window.requestAnimationFrame = this.old_requestAnimationFrame; - }); - - it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { - display._renderQ_push({ type: 'noop' }); // does nothing - expect(display._scan_renderQ).to.have.been.calledOnce; - }); - - it('should not try to process an item when it is pushed on if we are waiting for other items', function () { - display._renderQ.length = 2; - display._renderQ_push({ type: 'noop' }); - expect(display._scan_renderQ).to.not.have.been.called; - }); - - it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { - const img = { complete: false, addEventListener: sinon.spy() } - display._renderQ = [{ type: 'img', x: 3, y: 4, img: img }, - { type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 }]; - display.drawImage = sinon.spy(); - display.fillRect = sinon.spy(); - - display._scan_renderQ(); - expect(display.drawImage).to.not.have.been.called; - expect(display.fillRect).to.not.have.been.called; - expect(img.addEventListener).to.have.been.calledOnce; - - display._renderQ[0].img.complete = true; - display._scan_renderQ(); - expect(display.drawImage).to.have.been.calledOnce; - expect(display.fillRect).to.have.been.calledOnce; - expect(img.addEventListener).to.have.been.calledOnce; - }); - - it('should call callback when queue is flushed', function () { - display.onflush = sinon.spy(); - display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); - expect(display.onflush).to.not.have.been.called; - display.flush(); - expect(display.onflush).to.have.been.calledOnce; - }); - - it('should draw a blit image on type "blit"', function () { - display.blitImage = sinon.spy(); - display._renderQ_push({ type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); - expect(display.blitImage).to.have.been.calledOnce; - expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); - }); - - it('should draw a blit RGB image on type "blitRgb"', function () { - display.blitRgbImage = sinon.spy(); - display._renderQ_push({ type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] }); - expect(display.blitRgbImage).to.have.been.calledOnce; - expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); - }); - - it('should copy a region on type "copy"', function () { - display.copyImage = sinon.spy(); - display._renderQ_push({ type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 }); - expect(display.copyImage).to.have.been.calledOnce; - expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); - }); - - it('should fill a rect with a given color on type "fill"', function () { - display.fillRect = sinon.spy(); - display._renderQ_push({ type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9]}); - expect(display.fillRect).to.have.been.calledOnce; - expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); - }); - - it('should draw an image from an image object on type "img" (if complete)', function () { - display.drawImage = sinon.spy(); - display._renderQ_push({ type: 'img', x: 3, y: 4, img: { complete: true } }); - expect(display.drawImage).to.have.been.calledOnce; - expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); - }); + it('should redraw when moving the viewport', function () { + display.flip = sinon.spy(); + display.viewportChangePos(-1, 1); + expect(display.flip).to.have.been.calledOnce; }); + + it('should redraw when resizing the viewport', function () { + display.flip = sinon.spy(); + display.viewportChangeSize(2, 2); + expect(display.flip).to.have.been.calledOnce; + }); + + it('should show the entire framebuffer when disabling the viewport', function () { + display.clipViewport = false; + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should ignore viewport changes when the viewport is disabled', function () { + display.clipViewport = false; + display.viewportChangeSize(2, 2); + display.viewportChangePos(1, 1); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + + it('should show the entire framebuffer just after enabling the viewport', function () { + display.clipViewport = false; + display.clipViewport = true; + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(5); + expect(display._target.height).to.equal(5); + }); + }); + + describe('resizing', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.clipViewport = false; + display.resize(4, 4); + }); + + it('should change the size of the logical canvas', function () { + display.resize(5, 7); + expect(display._fb_width).to.equal(5); + expect(display._fb_height).to.equal(7); + }); + + it('should keep the framebuffer data', function () { + display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); + display.resize(2, 2); + display.flip(); + const expected = []; + for (let i = 0; i < 4 * 2 * 2; i += 4) { + expected[i] = 0xff; + expected[i + 1] = expected[i + 2] = 0; + expected[i + 3] = 0xff; + } + expect(display).to.have.displayed(new Uint8Array(expected)); + }); + + describe('viewport', function () { + beforeEach(function () { + display.clipViewport = true; + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + }); + + it('should keep the viewport position and size if possible', function () { + display.resize(6, 6); + expect(display.absX(0)).to.equal(1); + expect(display.absY(0)).to.equal(1); + expect(display._target.width).to.equal(3); + expect(display._target.height).to.equal(3); + }); + + it('should move the viewport if necessary', function () { + display.resize(3, 3); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(3); + expect(display._target.height).to.equal(3); + }); + + it('should shrink the viewport if necessary', function () { + display.resize(2, 2); + expect(display.absX(0)).to.equal(0); + expect(display.absY(0)).to.equal(0); + expect(display._target.width).to.equal(2); + expect(display._target.height).to.equal(2); + }); + }); + }); + + describe('rescaling', function () { + let display; + let canvas; + + beforeEach(function () { + canvas = document.createElement('canvas'); + display = new Display(canvas); + display.clipViewport = true; + display.resize(4, 4); + display.viewportChangeSize(3, 3); + display.viewportChangePos(1, 1); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should not change the bitmap size of the canvas', function () { + display.scale = 2.0; + expect(canvas.width).to.equal(3); + expect(canvas.height).to.equal(3); + }); + + it('should change the effective rendered size of the canvas', function () { + display.scale = 2.0; + expect(canvas.clientWidth).to.equal(6); + expect(canvas.clientHeight).to.equal(6); + }); + + it('should not change when resizing', function () { + display.scale = 2.0; + display.resize(5, 5); + expect(display.scale).to.equal(2.0); + expect(canvas.width).to.equal(3); + expect(canvas.height).to.equal(3); + expect(canvas.clientWidth).to.equal(6); + expect(canvas.clientHeight).to.equal(6); + }); + }); + + describe('autoscaling', function () { + let display; + let canvas; + + beforeEach(function () { + canvas = document.createElement('canvas'); + display = new Display(canvas); + display.clipViewport = true; + display.resize(4, 3); + document.body.appendChild(canvas); + }); + + afterEach(function () { + document.body.removeChild(canvas); + }); + + it('should preserve aspect ratio while autoscaling', function () { + display.autoscale(16, 9); + expect(canvas.clientWidth / canvas.clientHeight).to.equal(4 / 3); + }); + + it('should use width to determine scale when the current aspect ratio is wider than the target', function () { + display.autoscale(9, 16); + expect(display.absX(9)).to.equal(4); + expect(display.absY(18)).to.equal(8); + expect(canvas.clientWidth).to.equal(9); + expect(canvas.clientHeight).to.equal(7); // round 9 / (4 / 3) + }); + + it('should use height to determine scale when the current aspect ratio is taller than the target', function () { + display.autoscale(16, 9); + expect(display.absX(9)).to.equal(3); + expect(display.absY(18)).to.equal(6); + expect(canvas.clientWidth).to.equal(12); // 16 * (4 / 3) + expect(canvas.clientHeight).to.equal(9); + }); + + it('should not change the bitmap size of the canvas', function () { + display.autoscale(16, 9); + expect(canvas.width).to.equal(4); + expect(canvas.height).to.equal(3); + }); + }); + + describe('drawing', function () { + // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the + // basic cases + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + }); + + it('should clear the screen on #clear without a logo set', function () { + display.fillRect(0, 0, 4, 4, [0x00, 0x00, 0xff]); + display._logo = null; + display.clear(); + display.resize(4, 4); + const empty = []; + for (let i = 0; i < 4 * display._fb_width * display._fb_height; i++) { empty[i] = 0; } + expect(display).to.have.displayed(new Uint8Array(empty)); + }); + + it('should draw the logo on #clear with a logo set', function (done) { + display._logo = { + width: 4, height: 4, type: 'image/png', data: make_image_png(checked_data) + }; + display.clear(); + display.onflush = () => { + expect(display).to.have.displayed(checked_data); + expect(display._fb_width).to.equal(4); + expect(display._fb_height).to.equal(4); + done(); + }; + display.flush(); + }); + + it('should not draw directly on the target canvas', function () { + display.fillRect(0, 0, 4, 4, [0, 0, 0xff]); + display.flip(); + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + const expected = []; + for (let i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) { + expected[i] = 0xff; + expected[i + 1] = expected[i + 2] = 0; + expected[i + 3] = 0xff; + } + expect(display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should support filling a rectangle with particular color via #fillRect', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0]); + display.fillRect(2, 2, 2, 2, [0xff, 0, 0]); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support copying an portion of the canvas via #copyImage', function () { + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + display.fillRect(0, 0, 2, 2, [0xff, 0, 0x00]); + display.copyImage(0, 0, 2, 2, 2, 2); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing images via #imageRect', function (done) { + display.imageRect(0, 0, 'image/png', make_image_png(checked_data)); + display.flip(); + display.onflush = () => { + expect(display).to.have.displayed(checked_data); + done(); + }; + display.flush(); + }); + + it('should support drawing tile data with a background color and sub tiles', function () { + display.startTile(0, 0, 4, 4, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + // We have a special cache for 16x16 tiles that we need to test + it('should support drawing a 16x16 tile', function () { + const large_checked_data = new Uint8Array(16 * 16 * 4); + display.resize(16, 16); + + for (let y = 0; y < 16; y++) { + for (let x = 0; x < 16; x++) { + let pixel; + if ((x < 4) && (y < 4)) { + // NB: of course IE11 doesn't support #slice on ArrayBufferViews... + pixel = Array.prototype.slice.call(checked_data, (y * 4 + x) * 4, (y * 4 + x + 1) * 4); + } else { + pixel = [0, 0xff, 0, 255]; + } + large_checked_data.set(pixel, (y * 16 + x) * 4); + } + } + + display.startTile(0, 0, 16, 16, [0, 0xff, 0]); + display.subTile(0, 0, 2, 2, [0xff, 0, 0]); + display.subTile(2, 2, 2, 2, [0xff, 0, 0]); + display.finishTile(); + display.flip(); + expect(display).to.have.displayed(large_checked_data); + }); + + it('should support drawing BGRX blit images with true color via #blitImage', function () { + const data = []; + for (let i = 0; i < 16; i++) { + data[i * 4] = checked_data[i * 4 + 2]; + data[i * 4 + 1] = checked_data[i * 4 + 1]; + data[i * 4 + 2] = checked_data[i * 4]; + data[i * 4 + 3] = checked_data[i * 4 + 3]; + } + display.blitImage(0, 0, 4, 4, data, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing RGB blit images with true color via #blitRgbImage', function () { + const data = []; + for (let i = 0; i < 16; i++) { + data[i * 3] = checked_data[i * 4]; + data[i * 3 + 1] = checked_data[i * 4 + 1]; + data[i * 3 + 2] = checked_data[i * 4 + 2]; + } + display.blitRgbImage(0, 0, 4, 4, data, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + + it('should support drawing an image object via #drawImage', function () { + const img = make_image_canvas(checked_data); + display.drawImage(img, 0, 0); + display.flip(); + expect(display).to.have.displayed(checked_data); + }); + }); + + describe('the render queue processor', function () { + let display; + beforeEach(function () { + display = new Display(document.createElement('canvas')); + display.resize(4, 4); + sinon.spy(display, '_scan_renderQ'); + }); + + afterEach(function () { + window.requestAnimationFrame = this.old_requestAnimationFrame; + }); + + it('should try to process an item when it is pushed on, if nothing else is on the queue', function () { + display._renderQ_push({ type: 'noop' }); // does nothing + expect(display._scan_renderQ).to.have.been.calledOnce; + }); + + it('should not try to process an item when it is pushed on if we are waiting for other items', function () { + display._renderQ.length = 2; + display._renderQ_push({ type: 'noop' }); + expect(display._scan_renderQ).to.not.have.been.called; + }); + + it('should wait until an image is loaded to attempt to draw it and the rest of the queue', function () { + const img = { complete: false, addEventListener: sinon.spy() }; + display._renderQ = [{ + type: 'img', x: 3, y: 4, img: img + }, + { + type: 'fill', x: 1, y: 2, width: 3, height: 4, color: 5 + }]; + display.drawImage = sinon.spy(); + display.fillRect = sinon.spy(); + + display._scan_renderQ(); + expect(display.drawImage).to.not.have.been.called; + expect(display.fillRect).to.not.have.been.called; + expect(img.addEventListener).to.have.been.calledOnce; + + display._renderQ[0].img.complete = true; + display._scan_renderQ(); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledOnce; + expect(img.addEventListener).to.have.been.calledOnce; + }); + + it('should call callback when queue is flushed', function () { + display.onflush = sinon.spy(); + display.fillRect(0, 0, 4, 4, [0, 0xff, 0]); + expect(display.onflush).to.not.have.been.called; + display.flush(); + expect(display.onflush).to.have.been.calledOnce; + }); + + it('should draw a blit image on type "blit"', function () { + display.blitImage = sinon.spy(); + display._renderQ_push({ + type: 'blit', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] + }); + expect(display.blitImage).to.have.been.calledOnce; + expect(display.blitImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should draw a blit RGB image on type "blitRgb"', function () { + display.blitRgbImage = sinon.spy(); + display._renderQ_push({ + type: 'blitRgb', x: 3, y: 4, width: 5, height: 6, data: [7, 8, 9] + }); + expect(display.blitRgbImage).to.have.been.calledOnce; + expect(display.blitRgbImage).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9], 0); + }); + + it('should copy a region on type "copy"', function () { + display.copyImage = sinon.spy(); + display._renderQ_push({ + type: 'copy', x: 3, y: 4, width: 5, height: 6, old_x: 7, old_y: 8 + }); + expect(display.copyImage).to.have.been.calledOnce; + expect(display.copyImage).to.have.been.calledWith(7, 8, 3, 4, 5, 6); + }); + + it('should fill a rect with a given color on type "fill"', function () { + display.fillRect = sinon.spy(); + display._renderQ_push({ + type: 'fill', x: 3, y: 4, width: 5, height: 6, color: [7, 8, 9] + }); + expect(display.fillRect).to.have.been.calledOnce; + expect(display.fillRect).to.have.been.calledWith(3, 4, 5, 6, [7, 8, 9]); + }); + + it('should draw an image from an image object on type "img" (if complete)', function () { + display.drawImage = sinon.spy(); + display._renderQ_push({ + type: 'img', x: 3, y: 4, img: { complete: true } + }); + expect(display.drawImage).to.have.been.calledOnce; + expect(display.drawImage).to.have.been.calledWith({ complete: true }, 3, 4); + }); + }); }); diff --git a/tests/test.helper.js b/tests/test.helper.js index b04005d0..637678b5 100644 --- a/tests/test.helper.js +++ b/tests/test.helper.js @@ -1,223 +1,225 @@ -const expect = chai.expect; +const expect = chai.expect; import keysyms from '../core/input/keysymdef.js'; -import * as KeyboardUtil from "../core/input/util.js"; +import * as KeyboardUtil from '../core/input/util.js'; import * as browser from '../core/util/browser.js'; -describe('Helpers', function() { - "use strict"; +describe('Helpers', function () { + 'use strict'; - describe('keysyms.lookup', function() { - it('should map ASCII characters to keysyms', function() { - expect(keysyms.lookup('a'.charCodeAt())).to.be.equal(0x61); - expect(keysyms.lookup('A'.charCodeAt())).to.be.equal(0x41); - }); - it('should map Latin-1 characters to keysyms', function() { - expect(keysyms.lookup('ø'.charCodeAt())).to.be.equal(0xf8); + describe('keysyms.lookup', function () { + it('should map ASCII characters to keysyms', function () { + expect(keysyms.lookup('a'.charCodeAt())).to.be.equal(0x61); + expect(keysyms.lookup('A'.charCodeAt())).to.be.equal(0x41); + }); + it('should map Latin-1 characters to keysyms', function () { + expect(keysyms.lookup('ø'.charCodeAt())).to.be.equal(0xf8); - expect(keysyms.lookup('é'.charCodeAt())).to.be.equal(0xe9); - }); - it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function() { - expect(keysyms.lookup('Š'.charCodeAt())).to.be.equal(0x01a9); - }); - it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function() { - expect(keysyms.lookup('ũ'.charCodeAt())).to.be.equal(0x03fd); - }); - it('should map unknown codepoints to the Unicode range', function() { - expect(keysyms.lookup('\n'.charCodeAt())).to.be.equal(0x100000a); - expect(keysyms.lookup('\u262D'.charCodeAt())).to.be.equal(0x100262d); - }); - // This requires very recent versions of most browsers... skipping for now - it.skip('should map UCS-4 codepoints to the Unicode range', function() { - //expect(keysyms.lookup('\u{1F686}'.codePointAt())).to.be.equal(0x101f686); - }); + expect(keysyms.lookup('é'.charCodeAt())).to.be.equal(0xe9); + }); + it('should map characters that are in Windows-1252 but not in Latin-1 to keysyms', function () { + expect(keysyms.lookup('Š'.charCodeAt())).to.be.equal(0x01a9); + }); + it('should map characters which aren\'t in Latin1 *or* Windows-1252 to keysyms', function () { + expect(keysyms.lookup('ũ'.charCodeAt())).to.be.equal(0x03fd); + }); + it('should map unknown codepoints to the Unicode range', function () { + expect(keysyms.lookup('\n'.charCodeAt())).to.be.equal(0x100000a); + expect(keysyms.lookup('\u262D'.charCodeAt())).to.be.equal(0x100262d); + }); + // This requires very recent versions of most browsers... skipping for now + it.skip('should map UCS-4 codepoints to the Unicode range', function () { + // expect(keysyms.lookup('\u{1F686}'.codePointAt())).to.be.equal(0x101f686); + }); + }); + + describe('getKeycode', function () { + it('should pass through proper code', function () { + expect(KeyboardUtil.getKeycode({ code: 'Semicolon' })).to.be.equal('Semicolon'); + }); + it('should map legacy values', function () { + expect(KeyboardUtil.getKeycode({ code: '' })).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKeycode({ code: 'OSLeft' })).to.be.equal('MetaLeft'); + }); + it('should map keyCode to code when possible', function () { + expect(KeyboardUtil.getKeycode({ keyCode: 0x14 })).to.be.equal('CapsLock'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x5b })).to.be.equal('MetaLeft'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x35 })).to.be.equal('Digit5'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x65 })).to.be.equal('Numpad5'); + }); + it('should map keyCode left/right side', function () { + expect(KeyboardUtil.getKeycode({ keyCode: 0x10, location: 1 })).to.be.equal('ShiftLeft'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x10, location: 2 })).to.be.equal('ShiftRight'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x11, location: 1 })).to.be.equal('ControlLeft'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x11, location: 2 })).to.be.equal('ControlRight'); + }); + it('should map keyCode on numpad', function () { + expect(KeyboardUtil.getKeycode({ keyCode: 0x0d, location: 0 })).to.be.equal('Enter'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x0d, location: 3 })).to.be.equal('NumpadEnter'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x23, location: 0 })).to.be.equal('End'); + expect(KeyboardUtil.getKeycode({ keyCode: 0x23, location: 3 })).to.be.equal('Numpad1'); + }); + it('should return Unidentified when it cannot map the keyCode', function () { + expect(KeyboardUtil.getKeycode({ keycode: 0x42 })).to.be.equal('Unidentified'); }); - describe('getKeycode', function() { - it('should pass through proper code', function() { - expect(KeyboardUtil.getKeycode({code: 'Semicolon'})).to.be.equal('Semicolon'); - }); - it('should map legacy values', function() { - expect(KeyboardUtil.getKeycode({code: ''})).to.be.equal('Unidentified'); - expect(KeyboardUtil.getKeycode({code: 'OSLeft'})).to.be.equal('MetaLeft'); - }); - it('should map keyCode to code when possible', function() { - expect(KeyboardUtil.getKeycode({keyCode: 0x14})).to.be.equal('CapsLock'); - expect(KeyboardUtil.getKeycode({keyCode: 0x5b})).to.be.equal('MetaLeft'); - expect(KeyboardUtil.getKeycode({keyCode: 0x35})).to.be.equal('Digit5'); - expect(KeyboardUtil.getKeycode({keyCode: 0x65})).to.be.equal('Numpad5'); - }); - it('should map keyCode left/right side', function() { - expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 1})).to.be.equal('ShiftLeft'); - expect(KeyboardUtil.getKeycode({keyCode: 0x10, location: 2})).to.be.equal('ShiftRight'); - expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 1})).to.be.equal('ControlLeft'); - expect(KeyboardUtil.getKeycode({keyCode: 0x11, location: 2})).to.be.equal('ControlRight'); - }); - it('should map keyCode on numpad', function() { - expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 0})).to.be.equal('Enter'); - expect(KeyboardUtil.getKeycode({keyCode: 0x0d, location: 3})).to.be.equal('NumpadEnter'); - expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 0})).to.be.equal('End'); - expect(KeyboardUtil.getKeycode({keyCode: 0x23, location: 3})).to.be.equal('Numpad1'); - }); - it('should return Unidentified when it cannot map the keyCode', function() { - expect(KeyboardUtil.getKeycode({keycode: 0x42})).to.be.equal('Unidentified'); - }); + describe('Fix Meta on macOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } - describe('Fix Meta on macOS', function() { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } + window.navigator.platform = 'Mac x86_64'; + }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + }); - window.navigator.platform = "Mac x86_64"; - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); + it('should respect ContextMenu on modern browser', function () { + expect(KeyboardUtil.getKeycode({ code: 'ContextMenu', keyCode: 0x5d })).to.be.equal('ContextMenu'); + }); + it('should translate legacy ContextMenu to MetaRight', function () { + expect(KeyboardUtil.getKeycode({ keyCode: 0x5d })).to.be.equal('MetaRight'); + }); + }); + }); - it('should respect ContextMenu on modern browser', function() { - expect(KeyboardUtil.getKeycode({code: 'ContextMenu', keyCode: 0x5d})).to.be.equal('ContextMenu'); - }); - it('should translate legacy ContextMenu to MetaRight', function() { - expect(KeyboardUtil.getKeycode({keyCode: 0x5d})).to.be.equal('MetaRight'); - }); - }); + describe('getKey', function () { + it('should prefer key', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKey({ + key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43 + })).to.be.equal('a'); + }); + it('should map legacy values', function () { + expect(KeyboardUtil.getKey({ key: 'Spacebar' })).to.be.equal(' '); + expect(KeyboardUtil.getKey({ key: 'Left' })).to.be.equal('ArrowLeft'); + expect(KeyboardUtil.getKey({ key: 'OS' })).to.be.equal('Meta'); + expect(KeyboardUtil.getKey({ key: 'Win' })).to.be.equal('Meta'); + expect(KeyboardUtil.getKey({ key: 'UIKeyInputLeftArrow' })).to.be.equal('ArrowLeft'); + }); + it('should use code if no key', function () { + expect(KeyboardUtil.getKey({ code: 'NumpadBackspace' })).to.be.equal('Backspace'); + }); + it('should not use code fallback for character keys', function () { + expect(KeyboardUtil.getKey({ code: 'KeyA' })).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({ code: 'Digit1' })).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({ code: 'Period' })).to.be.equal('Unidentified'); + expect(KeyboardUtil.getKey({ code: 'Numpad1' })).to.be.equal('Unidentified'); + }); + it('should use charCode if no key', function () { + expect(KeyboardUtil.getKey({ charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43 })).to.be.equal('Š'); + }); + it('should return Unidentified when it cannot map the key', function () { + expect(KeyboardUtil.getKey({ keycode: 0x42 })).to.be.equal('Unidentified'); }); - describe('getKey', function() { - it('should prefer key', function() { - if (browser.isIE() || browser.isEdge()) this.skip(); - expect(KeyboardUtil.getKey({key: 'a', charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('a'); - }); - it('should map legacy values', function() { - expect(KeyboardUtil.getKey({key: 'Spacebar'})).to.be.equal(' '); - expect(KeyboardUtil.getKey({key: 'Left'})).to.be.equal('ArrowLeft'); - expect(KeyboardUtil.getKey({key: 'OS'})).to.be.equal('Meta'); - expect(KeyboardUtil.getKey({key: 'Win'})).to.be.equal('Meta'); - expect(KeyboardUtil.getKey({key: 'UIKeyInputLeftArrow'})).to.be.equal('ArrowLeft'); - }); - it('should use code if no key', function() { - expect(KeyboardUtil.getKey({code: 'NumpadBackspace'})).to.be.equal('Backspace'); - }); - it('should not use code fallback for character keys', function() { - expect(KeyboardUtil.getKey({code: 'KeyA'})).to.be.equal('Unidentified'); - expect(KeyboardUtil.getKey({code: 'Digit1'})).to.be.equal('Unidentified'); - expect(KeyboardUtil.getKey({code: 'Period'})).to.be.equal('Unidentified'); - expect(KeyboardUtil.getKey({code: 'Numpad1'})).to.be.equal('Unidentified'); - }); - it('should use charCode if no key', function() { - expect(KeyboardUtil.getKey({charCode: 'Š'.charCodeAt(), keyCode: 0x42, which: 0x43})).to.be.equal('Š'); - }); - it('should return Unidentified when it cannot map the key', function() { - expect(KeyboardUtil.getKey({keycode: 0x42})).to.be.equal('Unidentified'); - }); + describe('Broken key AltGraph on IE/Edge', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } - describe('Broken key AltGraph on IE/Edge', function() { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + }); - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); + it('should ignore printable character key on IE', function () { + window.navigator.userAgent = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'; + expect(KeyboardUtil.getKey({ key: 'a' })).to.be.equal('Unidentified'); + }); + it('should ignore printable character key on Edge', function () { + window.navigator.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393'; + expect(KeyboardUtil.getKey({ key: 'a' })).to.be.equal('Unidentified'); + }); + it('should allow non-printable character key on IE', function () { + window.navigator.userAgent = 'Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko'; + expect(KeyboardUtil.getKey({ key: 'Shift' })).to.be.equal('Shift'); + }); + it('should allow non-printable character key on Edge', function () { + window.navigator.userAgent = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393'; + expect(KeyboardUtil.getKey({ key: 'Shift' })).to.be.equal('Shift'); + }); + }); + }); - it('should ignore printable character key on IE', function() { - window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; - expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); - }); - it('should ignore printable character key on Edge', function() { - window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; - expect(KeyboardUtil.getKey({key: 'a'})).to.be.equal('Unidentified'); - }); - it('should allow non-printable character key on IE', function() { - window.navigator.userAgent = "Mozilla/5.0 (Windows NT 6.3; Trident/7.0; rv:11.0) like Gecko"; - expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); - }); - it('should allow non-printable character key on Edge', function() { - window.navigator.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/51.0.2704.79 Safari/537.36 Edge/14.14393"; - expect(KeyboardUtil.getKey({key: 'Shift'})).to.be.equal('Shift'); - }); - }); + describe('getKeysym', function () { + describe('Non-character keys', function () { + it('should recognize the right keys', function () { + expect(KeyboardUtil.getKeysym({ key: 'Enter' })).to.be.equal(0xFF0D); + expect(KeyboardUtil.getKeysym({ key: 'Backspace' })).to.be.equal(0xFF08); + expect(KeyboardUtil.getKeysym({ key: 'Tab' })).to.be.equal(0xFF09); + expect(KeyboardUtil.getKeysym({ key: 'Shift' })).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({ key: 'Control' })).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({ key: 'Alt' })).to.be.equal(0xFFE9); + expect(KeyboardUtil.getKeysym({ key: 'Meta' })).to.be.equal(0xFFEB); + expect(KeyboardUtil.getKeysym({ key: 'Escape' })).to.be.equal(0xFF1B); + expect(KeyboardUtil.getKeysym({ key: 'ArrowUp' })).to.be.equal(0xFF52); + }); + it('should map left/right side', function () { + expect(KeyboardUtil.getKeysym({ key: 'Shift', location: 1 })).to.be.equal(0xFFE1); + expect(KeyboardUtil.getKeysym({ key: 'Shift', location: 2 })).to.be.equal(0xFFE2); + expect(KeyboardUtil.getKeysym({ key: 'Control', location: 1 })).to.be.equal(0xFFE3); + expect(KeyboardUtil.getKeysym({ key: 'Control', location: 2 })).to.be.equal(0xFFE4); + }); + it('should handle AltGraph', function () { + expect(KeyboardUtil.getKeysym({ code: 'AltRight', key: 'Alt', location: 2 })).to.be.equal(0xFFEA); + expect(KeyboardUtil.getKeysym({ code: 'AltRight', key: 'AltGraph', location: 2 })).to.be.equal(0xFE03); + }); + it('should return null for unknown keys', function () { + expect(KeyboardUtil.getKeysym({ key: 'Semicolon' })).to.be.null; + expect(KeyboardUtil.getKeysym({ key: 'BracketRight' })).to.be.null; + }); + it('should handle remappings', function () { + expect(KeyboardUtil.getKeysym({ code: 'ControlLeft', key: 'Tab' })).to.be.equal(0xFF09); + }); }); - describe('getKeysym', function() { - describe('Non-character keys', function() { - it('should recognize the right keys', function() { - expect(KeyboardUtil.getKeysym({key: 'Enter'})).to.be.equal(0xFF0D); - expect(KeyboardUtil.getKeysym({key: 'Backspace'})).to.be.equal(0xFF08); - expect(KeyboardUtil.getKeysym({key: 'Tab'})).to.be.equal(0xFF09); - expect(KeyboardUtil.getKeysym({key: 'Shift'})).to.be.equal(0xFFE1); - expect(KeyboardUtil.getKeysym({key: 'Control'})).to.be.equal(0xFFE3); - expect(KeyboardUtil.getKeysym({key: 'Alt'})).to.be.equal(0xFFE9); - expect(KeyboardUtil.getKeysym({key: 'Meta'})).to.be.equal(0xFFEB); - expect(KeyboardUtil.getKeysym({key: 'Escape'})).to.be.equal(0xFF1B); - expect(KeyboardUtil.getKeysym({key: 'ArrowUp'})).to.be.equal(0xFF52); - }); - it('should map left/right side', function() { - expect(KeyboardUtil.getKeysym({key: 'Shift', location: 1})).to.be.equal(0xFFE1); - expect(KeyboardUtil.getKeysym({key: 'Shift', location: 2})).to.be.equal(0xFFE2); - expect(KeyboardUtil.getKeysym({key: 'Control', location: 1})).to.be.equal(0xFFE3); - expect(KeyboardUtil.getKeysym({key: 'Control', location: 2})).to.be.equal(0xFFE4); - }); - it('should handle AltGraph', function() { - expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'Alt', location: 2})).to.be.equal(0xFFEA); - expect(KeyboardUtil.getKeysym({code: 'AltRight', key: 'AltGraph', location: 2})).to.be.equal(0xFE03); - }); - it('should return null for unknown keys', function() { - expect(KeyboardUtil.getKeysym({key: 'Semicolon'})).to.be.null; - expect(KeyboardUtil.getKeysym({key: 'BracketRight'})).to.be.null; - }); - it('should handle remappings', function() { - expect(KeyboardUtil.getKeysym({code: 'ControlLeft', key: 'Tab'})).to.be.equal(0xFF09); - }); - }); - - describe('Numpad', function() { - it('should handle Numpad numbers', function() { - if (browser.isIE() || browser.isEdge()) this.skip(); - expect(KeyboardUtil.getKeysym({code: 'Digit5', key: '5', location: 0})).to.be.equal(0x0035); - expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: '5', location: 3})).to.be.equal(0xFFB5); - }); - it('should handle Numpad non-character keys', function() { - expect(KeyboardUtil.getKeysym({code: 'Home', key: 'Home', location: 0})).to.be.equal(0xFF50); - expect(KeyboardUtil.getKeysym({code: 'Numpad5', key: 'Home', location: 3})).to.be.equal(0xFF95); - expect(KeyboardUtil.getKeysym({code: 'Delete', key: 'Delete', location: 0})).to.be.equal(0xFFFF); - expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: 'Delete', location: 3})).to.be.equal(0xFF9F); - }); - it('should handle Numpad Decimal key', function() { - if (browser.isIE() || browser.isEdge()) this.skip(); - expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: '.', location: 3})).to.be.equal(0xFFAE); - expect(KeyboardUtil.getKeysym({code: 'NumpadDecimal', key: ',', location: 3})).to.be.equal(0xFFAC); - }); - }); + describe('Numpad', function () { + it('should handle Numpad numbers', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKeysym({ code: 'Digit5', key: '5', location: 0 })).to.be.equal(0x0035); + expect(KeyboardUtil.getKeysym({ code: 'Numpad5', key: '5', location: 3 })).to.be.equal(0xFFB5); + }); + it('should handle Numpad non-character keys', function () { + expect(KeyboardUtil.getKeysym({ code: 'Home', key: 'Home', location: 0 })).to.be.equal(0xFF50); + expect(KeyboardUtil.getKeysym({ code: 'Numpad5', key: 'Home', location: 3 })).to.be.equal(0xFF95); + expect(KeyboardUtil.getKeysym({ code: 'Delete', key: 'Delete', location: 0 })).to.be.equal(0xFFFF); + expect(KeyboardUtil.getKeysym({ code: 'NumpadDecimal', key: 'Delete', location: 3 })).to.be.equal(0xFF9F); + }); + it('should handle Numpad Decimal key', function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + expect(KeyboardUtil.getKeysym({ code: 'NumpadDecimal', key: '.', location: 3 })).to.be.equal(0xFFAE); + expect(KeyboardUtil.getKeysym({ code: 'NumpadDecimal', key: ',', location: 3 })).to.be.equal(0xFFAC); + }); }); + }); }); diff --git a/tests/test.keyboard.js b/tests/test.keyboard.js index c555b4fb..f76bbc44 100644 --- a/tests/test.keyboard.js +++ b/tests/test.keyboard.js @@ -5,508 +5,516 @@ import sinon from '../vendor/sinon.js'; import Keyboard from '../core/input/keyboard.js'; import * as browser from '../core/util/browser.js'; -describe('Key Event Handling', function() { - "use strict"; +describe('Key Event Handling', function () { + 'use strict'; - // The real KeyboardEvent constructor might not work everywhere we - // want to run these tests - function keyevent(typeArg, KeyboardEventInit) { - const e = { type: typeArg }; - for (let key in KeyboardEventInit) { - e[key] = KeyboardEventInit[key]; - } - e.stopPropagation = sinon.spy(); - e.preventDefault = sinon.spy(); - return e; + // The real KeyboardEvent constructor might not work everywhere we + // want to run these tests + function keyevent(typeArg, KeyboardEventInit) { + const e = { type: typeArg }; + for (let key in KeyboardEventInit) { + e[key] = KeyboardEventInit[key]; } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + } - describe('Decode Keyboard Events', function() { - it('should decode keydown events', function(done) { - if (browser.isIE() || browser.isEdge()) this.skip(); - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - }); - it('should decode keyup events', function(done) { - if (browser.isIE() || browser.isEdge()) this.skip(); - let calls = 0; - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - if (calls++ === 1) { - expect(down).to.be.equal(false); - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); - }); - - describe('Legacy keypress Events', function() { - it('should wait for keypress when needed', function() { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - it('should decode keypress events', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); - kbd._handleKeyPress(keyevent('keypress', {code: 'KeyA', charCode: 0x61})); - }); - it('should ignore keypress with different code', function() { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); - kbd._handleKeyPress(keyevent('keypress', {code: 'KeyB', charCode: 0x61})); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - it('should handle keypress with missing code', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41})); - kbd._handleKeyPress(keyevent('keypress', {charCode: 0x61})); - }); - it('should guess key if no keypress and numeric key', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x32); - expect(code).to.be.equal('Digit2'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'Digit2', keyCode: 0x32})); - }); - it('should guess key if no keypress and alpha key', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: false})); - }); - it('should guess key if no keypress and alpha key (with shift)', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x41); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x41, shiftKey: true})); - }); - it('should not guess key if no keypress and unknown key', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', keyCode: 0x09})); - }); - }); - - describe('suppress the right events at the right time', function() { - beforeEach(function () { - if (browser.isIE() || browser.isEdge()) this.skip(); - }); - it('should suppress anything with a valid key', function() { - const kbd = new Keyboard(document, {}); - const evt1 = keyevent('keydown', {code: 'KeyA', key: 'a'}); - kbd._handleKeyDown(evt1); - expect(evt1.preventDefault).to.have.been.called; - const evt2 = keyevent('keyup', {code: 'KeyA', key: 'a'}); - kbd._handleKeyUp(evt2); - expect(evt2.preventDefault).to.have.been.called; - }); - it('should not suppress keys without key', function() { - const kbd = new Keyboard(document, {}); - const evt = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); - kbd._handleKeyDown(evt); - expect(evt.preventDefault).to.not.have.been.called; - }); - it('should suppress the following keypress event', function() { - const kbd = new Keyboard(document, {}); - const evt1 = keyevent('keydown', {code: 'KeyA', keyCode: 0x41}); - kbd._handleKeyDown(evt1); - const evt2 = keyevent('keypress', {code: 'KeyA', charCode: 0x41}); - kbd._handleKeyPress(evt2); - expect(evt2.preventDefault).to.have.been.called; - }); - }); + describe('Decode Keyboard Events', function () { + it('should decode keydown events', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + }); + it('should decode keyup events', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let calls = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (calls++ === 1) { + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + kbd._handleKeyUp(keyevent('keyup', { code: 'KeyA', key: 'a' })); }); - describe('Fake keyup', function() { - it('should fake keyup events for virtual keyboards', function(done) { - if (browser.isIE() || browser.isEdge()) this.skip(); - let count = 0; - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - switch (count++) { - case 0: - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('Unidentified'); - expect(down).to.be.equal(true); - break; - case 1: - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('Unidentified'); - expect(down).to.be.equal(false); - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'Unidentified', key: 'a'})); - }); - - describe('iOS', function() { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } - - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - - window.navigator.platform = "iPhone 9.0"; - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); - - it('should fake keyup events on iOS', function(done) { - if (browser.isIE() || browser.isEdge()) this.skip(); - let count = 0; - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - switch (count++) { - case 0: - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - break; - case 1: - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(false); - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - }); - }); + describe('Legacy keypress Events', function () { + it('should wait for keypress when needed', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41 })); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should decode keypress events', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41 })); + kbd._handleKeyPress(keyevent('keypress', { code: 'KeyA', charCode: 0x61 })); + }); + it('should ignore keypress with different code', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41 })); + kbd._handleKeyPress(keyevent('keypress', { code: 'KeyB', charCode: 0x61 })); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + it('should handle keypress with missing code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41 })); + kbd._handleKeyPress(keyevent('keypress', { charCode: 0x61 })); + }); + it('should guess key if no keypress and numeric key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x32); + expect(code).to.be.equal('Digit2'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'Digit2', keyCode: 0x32 })); + }); + it('should guess key if no keypress and alpha key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41, shiftKey: false })); + }); + it('should guess key if no keypress and alpha key (with shift)', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x41); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x41, shiftKey: true })); + }); + it('should not guess key if no keypress and unknown key', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', keyCode: 0x09 })); + }); }); - describe('Track Key State', function() { - beforeEach(function () { - if (browser.isIE() || browser.isEdge()) this.skip(); - }); - it('should send release using the same keysym as the press', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - if (!down) { - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'b'})); - }); - it('should send the same keysym for multiple presses', function() { - let count = 0; - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('KeyA'); - expect(down).to.be.equal(true); - count++; - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'b'})); - expect(count).to.be.equal(2); - }); - it('should do nothing on keyup events if no keys are down', function() { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); - expect(kbd.onkeyevent).to.not.have.been.called; - }); + describe('suppress the right events at the right time', function () { + beforeEach(function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + }); + it('should suppress anything with a valid key', function () { + const kbd = new Keyboard(document, {}); + const evt1 = keyevent('keydown', { code: 'KeyA', key: 'a' }); + kbd._handleKeyDown(evt1); + expect(evt1.preventDefault).to.have.been.called; + const evt2 = keyevent('keyup', { code: 'KeyA', key: 'a' }); + kbd._handleKeyUp(evt2); + expect(evt2.preventDefault).to.have.been.called; + }); + it('should not suppress keys without key', function () { + const kbd = new Keyboard(document, {}); + const evt = keyevent('keydown', { code: 'KeyA', keyCode: 0x41 }); + kbd._handleKeyDown(evt); + expect(evt.preventDefault).to.not.have.been.called; + }); + it('should suppress the following keypress event', function () { + const kbd = new Keyboard(document, {}); + const evt1 = keyevent('keydown', { code: 'KeyA', keyCode: 0x41 }); + kbd._handleKeyDown(evt1); + const evt2 = keyevent('keypress', { code: 'KeyA', charCode: 0x41 }); + kbd._handleKeyPress(evt2); + expect(evt2.preventDefault).to.have.been.called; + }); + }); + }); - describe('Legacy Events', function() { - it('should track keys using keyCode if no code', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('Platform65'); - if (!down) { - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {keyCode: 65, key: 'a'})); - kbd._handleKeyUp(keyevent('keyup', {keyCode: 65, key: 'b'})); - }); - it('should ignore compositing code', function() { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('Unidentified'); - }; - kbd._handleKeyDown(keyevent('keydown', {keyCode: 229, key: 'a'})); - }); - it('should track keys using keyIdentifier if no code', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0x61); - expect(code).to.be.equal('Platform65'); - if (!down) { - done(); - } - }; - kbd._handleKeyDown(keyevent('keydown', {keyIdentifier: 'U+0041', key: 'a'})); - kbd._handleKeyUp(keyevent('keyup', {keyIdentifier: 'U+0041', key: 'b'})); - }); - }); + describe('Fake keyup', function () { + it('should fake keyup events for virtual keyboards', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'Unidentified', key: 'a' })); }); - describe('Shuffle modifiers on macOS', function() { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } + describe('iOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } - window.navigator.platform = "Mac x86_64"; - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); + window.navigator.platform = 'iPhone 9.0'; + }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + }); - it('should change Alt to AltGraph', function() { - let count = 0; - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - switch (count++) { - case 0: - expect(keysym).to.be.equal(0xFF7E); - expect(code).to.be.equal('AltLeft'); - break; - case 1: - expect(keysym).to.be.equal(0xFE03); - expect(code).to.be.equal('AltRight'); - break; - } - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'AltLeft', key: 'Alt', location: 1})); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); - expect(count).to.be.equal(2); - }); - it('should change left Super to Alt', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0xFFE9); - expect(code).to.be.equal('MetaLeft'); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'MetaLeft', key: 'Meta', location: 1})); - }); - it('should change right Super to left Super', function(done) { - const kbd = new Keyboard(document); - kbd.onkeyevent = (keysym, code, down) => { - expect(keysym).to.be.equal(0xFFEB); - expect(code).to.be.equal('MetaRight'); - done(); - }; - kbd._handleKeyDown(keyevent('keydown', {code: 'MetaRight', key: 'Meta', location: 2})); - }); + it('should fake keyup events on iOS', function (done) { + if (browser.isIE() || browser.isEdge()) this.skip(); + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + break; + case 1: + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(false); + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + }); + }); + }); + + describe('Track Key State', function () { + beforeEach(function () { + if (browser.isIE() || browser.isEdge()) this.skip(); + }); + it('should send release using the same keysym as the press', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + kbd._handleKeyUp(keyevent('keyup', { code: 'KeyA', key: 'b' })); + }); + it('should send the same keysym for multiple presses', function () { + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('KeyA'); + expect(down).to.be.equal(true); + count++; + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'b' })); + expect(count).to.be.equal(2); + }); + it('should do nothing on keyup events if no keys are down', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyUp(keyevent('keyup', { code: 'KeyA', key: 'a' })); + expect(kbd.onkeyevent).to.not.have.been.called; }); - describe('Escape AltGraph on Windows', function() { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } - - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.platform !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } - - window.navigator.platform = "Windows x86_64"; - - this.clock = sinon.useFakeTimers(); - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - this.clock.restore(); - }); - - it('should supress ControlLeft until it knows if it is AltGr', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - - it('should not trigger on repeating ControlLeft', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(kbd.onkeyevent).to.have.been.calledTwice; - expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); - expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); - }); - - it('should not supress ControlRight', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlRight', key: 'Control', location: 2})); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, "ControlRight", true); - }); - - it('should release ControlLeft after 100 ms', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(kbd.onkeyevent).to.not.have.been.called; - this.clock.tick(100); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, "ControlLeft", true); - }); - - it('should release ControlLeft on other key press', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(kbd.onkeyevent).to.not.have.been.called; - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - expect(kbd.onkeyevent).to.have.been.calledTwice; - expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); - expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, "KeyA", true); - - // Check that the timer is properly dead - kbd.onkeyevent.reset(); - this.clock.tick(100); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - - it('should release ControlLeft on other key release', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'KeyA', key: 'a'})); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1})); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, "KeyA", true); - kbd._handleKeyUp(keyevent('keyup', {code: 'KeyA', key: 'a'})); - expect(kbd.onkeyevent).to.have.been.calledThrice; - expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); - expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, "KeyA", false); - - // Check that the timer is properly dead - kbd.onkeyevent.reset(); - this.clock.tick(100); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - - it('should generate AltGraph for quick Ctrl+Alt sequence', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); - this.clock.tick(20); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); - - // Check that the timer is properly dead - kbd.onkeyevent.reset(); - this.clock.tick(100); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - - it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now()})); - this.clock.tick(60); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now()})); - expect(kbd.onkeyevent).to.have.been.calledTwice; - expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, "ControlLeft", true); - expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, "AltRight", true); - - // Check that the timer is properly dead - kbd.onkeyevent.reset(); - this.clock.tick(100); - expect(kbd.onkeyevent).to.not.have.been.called; - }); - - it('should pass through single Alt', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'Alt', location: 2})); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true); - }); - - it('should pass through single AltGr', function () { - const kbd = new Keyboard(document); - kbd.onkeyevent = sinon.spy(); - kbd._handleKeyDown(keyevent('keydown', {code: 'AltRight', key: 'AltGraph', location: 2})); - expect(kbd.onkeyevent).to.have.been.calledOnce; - expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); - }); + describe('Legacy Events', function () { + it('should track keys using keyCode if no code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Platform65'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { keyCode: 65, key: 'a' })); + kbd._handleKeyUp(keyevent('keyup', { keyCode: 65, key: 'b' })); + }); + it('should ignore compositing code', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Unidentified'); + }; + kbd._handleKeyDown(keyevent('keydown', { keyCode: 229, key: 'a' })); + }); + it('should track keys using keyIdentifier if no code', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0x61); + expect(code).to.be.equal('Platform65'); + if (!down) { + done(); + } + }; + kbd._handleKeyDown(keyevent('keydown', { keyIdentifier: 'U+0041', key: 'a' })); + kbd._handleKeyUp(keyevent('keyup', { keyIdentifier: 'U+0041', key: 'b' })); + }); }); + }); + + describe('Shuffle modifiers on macOS', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = 'Mac x86_64'; + }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + }); + + it('should change Alt to AltGraph', function () { + let count = 0; + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + switch (count++) { + case 0: + expect(keysym).to.be.equal(0xFF7E); + expect(code).to.be.equal('AltLeft'); + break; + case 1: + expect(keysym).to.be.equal(0xFE03); + expect(code).to.be.equal('AltRight'); + break; + } + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'AltLeft', key: 'Alt', location: 1 })); + kbd._handleKeyDown(keyevent('keydown', { code: 'AltRight', key: 'Alt', location: 2 })); + expect(count).to.be.equal(2); + }); + it('should change left Super to Alt', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0xFFE9); + expect(code).to.be.equal('MetaLeft'); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'MetaLeft', key: 'Meta', location: 1 })); + }); + it('should change right Super to left Super', function (done) { + const kbd = new Keyboard(document); + kbd.onkeyevent = (keysym, code, down) => { + expect(keysym).to.be.equal(0xFFEB); + expect(code).to.be.equal('MetaRight'); + done(); + }; + kbd._handleKeyDown(keyevent('keydown', { code: 'MetaRight', key: 'Meta', location: 2 })); + }); + }); + + describe('Escape AltGraph on Windows', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } + + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.platform !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } + + window.navigator.platform = 'Windows x86_64'; + + this.clock = sinon.useFakeTimers(); + }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + this.clock.restore(); + }); + + it('should supress ControlLeft until it knows if it is AltGr', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should not trigger on repeating ControlLeft', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + }); + + it('should not supress ControlRight', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlRight', key: 'Control', location: 2 })); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe4, 'ControlRight', true); + }); + + it('should release ControlLeft after 100 ms', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + expect(kbd.onkeyevent).to.not.have.been.called; + this.clock.tick(100); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + }); + + it('should release ControlLeft on other key press', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + expect(kbd.onkeyevent).to.not.have.been.called; + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0x61, 'KeyA', true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should release ControlLeft on other key release', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'KeyA', key: 'a' })); + kbd._handleKeyDown(keyevent('keydown', { code: 'ControlLeft', key: 'Control', location: 1 })); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0x61, 'KeyA', true); + kbd._handleKeyUp(keyevent('keyup', { code: 'KeyA', key: 'a' })); + expect(kbd.onkeyevent).to.have.been.calledThrice; + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + expect(kbd.onkeyevent.thirdCall).to.have.been.calledWith(0x61, 'KeyA', false); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate AltGraph for quick Ctrl+Alt sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { + code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now() + })); + this.clock.tick(20); + kbd._handleKeyDown(keyevent('keydown', { + code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now() + })); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should generate Ctrl, Alt for slow Ctrl+Alt sequence', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { + code: 'ControlLeft', key: 'Control', location: 1, timeStamp: Date.now() + })); + this.clock.tick(60); + kbd._handleKeyDown(keyevent('keydown', { + code: 'AltRight', key: 'Alt', location: 2, timeStamp: Date.now() + })); + expect(kbd.onkeyevent).to.have.been.calledTwice; + expect(kbd.onkeyevent.firstCall).to.have.been.calledWith(0xffe3, 'ControlLeft', true); + expect(kbd.onkeyevent.secondCall).to.have.been.calledWith(0xffea, 'AltRight', true); + + // Check that the timer is properly dead + kbd.onkeyevent.reset(); + this.clock.tick(100); + expect(kbd.onkeyevent).to.not.have.been.called; + }); + + it('should pass through single Alt', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'AltRight', key: 'Alt', location: 2 })); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xffea, 'AltRight', true); + }); + + it('should pass through single AltGr', function () { + const kbd = new Keyboard(document); + kbd.onkeyevent = sinon.spy(); + kbd._handleKeyDown(keyevent('keydown', { code: 'AltRight', key: 'AltGraph', location: 2 })); + expect(kbd.onkeyevent).to.have.been.calledOnce; + expect(kbd.onkeyevent).to.have.been.calledWith(0xfe03, 'AltRight', true); + }); + }); }); diff --git a/tests/test.localization.js b/tests/test.localization.js index 7ea11d66..93769985 100644 --- a/tests/test.localization.js +++ b/tests/test.localization.js @@ -1,72 +1,72 @@ const expect = chai.expect; import { l10n } from '../app/localization.js'; -describe('Localization', function() { - "use strict"; +describe('Localization', function () { + 'use strict'; - describe('language selection', function () { - let origNavigator; - beforeEach(function () { - // window.navigator is a protected read-only property in many - // environments, so we need to redefine it whilst running these - // tests. - origNavigator = Object.getOwnPropertyDescriptor(window, "navigator"); - if (origNavigator === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } + describe('language selection', function () { + let origNavigator; + beforeEach(function () { + // window.navigator is a protected read-only property in many + // environments, so we need to redefine it whilst running these + // tests. + origNavigator = Object.getOwnPropertyDescriptor(window, 'navigator'); + if (origNavigator === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } - Object.defineProperty(window, "navigator", {value: {}}); - if (window.navigator.languages !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } + Object.defineProperty(window, 'navigator', { value: {} }); + if (window.navigator.languages !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } - window.navigator.languages = []; - }); - afterEach(function () { - Object.defineProperty(window, "navigator", origNavigator); - }); - - it('should use English by default', function() { - expect(l10n.language).to.equal('en'); - }); - it('should use English if no user language matches', function() { - window.navigator.languages = ["nl", "de"]; - l10n.setup(["es", "fr"]); - expect(l10n.language).to.equal('en'); - }); - it('should use the most preferred user language', function() { - window.navigator.languages = ["nl", "de", "fr"]; - l10n.setup(["es", "fr", "de"]); - expect(l10n.language).to.equal('de'); - }); - it('should prefer sub-languages languages', function() { - window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt", "pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); - }); - it('should fall back to language "parents"', function() { - window.navigator.languages = ["pt-BR"]; - l10n.setup(["fr", "pt", "de"]); - expect(l10n.language).to.equal('pt'); - }); - it('should not use specific language when user asks for a generic language', function() { - window.navigator.languages = ["pt", "de"]; - l10n.setup(["fr", "pt-BR", "de"]); - expect(l10n.language).to.equal('de'); - }); - it('should handle underscore as a separator', function() { - window.navigator.languages = ["pt-BR"]; - l10n.setup(["pt_BR"]); - expect(l10n.language).to.equal('pt_BR'); - }); - it('should handle difference in case', function() { - window.navigator.languages = ["pt-br"]; - l10n.setup(["pt-BR"]); - expect(l10n.language).to.equal('pt-BR'); - }); + window.navigator.languages = []; }); + afterEach(function () { + Object.defineProperty(window, 'navigator', origNavigator); + }); + + it('should use English by default', function () { + expect(l10n.language).to.equal('en'); + }); + it('should use English if no user language matches', function () { + window.navigator.languages = ['nl', 'de']; + l10n.setup(['es', 'fr']); + expect(l10n.language).to.equal('en'); + }); + it('should use the most preferred user language', function () { + window.navigator.languages = ['nl', 'de', 'fr']; + l10n.setup(['es', 'fr', 'de']); + expect(l10n.language).to.equal('de'); + }); + it('should prefer sub-languages languages', function () { + window.navigator.languages = ['pt-BR']; + l10n.setup(['pt', 'pt-BR']); + expect(l10n.language).to.equal('pt-BR'); + }); + it('should fall back to language "parents"', function () { + window.navigator.languages = ['pt-BR']; + l10n.setup(['fr', 'pt', 'de']); + expect(l10n.language).to.equal('pt'); + }); + it('should not use specific language when user asks for a generic language', function () { + window.navigator.languages = ['pt', 'de']; + l10n.setup(['fr', 'pt-BR', 'de']); + expect(l10n.language).to.equal('de'); + }); + it('should handle underscore as a separator', function () { + window.navigator.languages = ['pt-BR']; + l10n.setup(['pt_BR']); + expect(l10n.language).to.equal('pt_BR'); + }); + it('should handle difference in case', function () { + window.navigator.languages = ['pt-br']; + l10n.setup(['pt-BR']); + expect(l10n.language).to.equal('pt-BR'); + }); + }); }); diff --git a/tests/test.mouse.js b/tests/test.mouse.js index 61db064a..3267a5b4 100644 --- a/tests/test.mouse.js +++ b/tests/test.mouse.js @@ -5,293 +5,371 @@ import sinon from '../vendor/sinon.js'; import Mouse from '../core/input/mouse.js'; import * as eventUtils from '../core/util/events.js'; -describe('Mouse Event Handling', function() { - "use strict"; +describe('Mouse Event Handling', function () { + 'use strict'; - sinon.stub(eventUtils, 'setCapture'); - // This function is only used on target (the canvas) - // and for these tests we can assume that the canvas is 100x100 - // located at coordinates 10x10 - sinon.stub(Element.prototype, 'getBoundingClientRect').returns( - {left: 10, right: 110, top: 10, bottom: 110, width: 100, height: 100}); - const target = document.createElement('canvas'); + sinon.stub(eventUtils, 'setCapture'); + // This function is only used on target (the canvas) + // and for these tests we can assume that the canvas is 100x100 + // located at coordinates 10x10 + sinon.stub(Element.prototype, 'getBoundingClientRect').returns( + { + left: 10, right: 110, top: 10, bottom: 110, width: 100, height: 100 + } + ); + const target = document.createElement('canvas'); - // The real constructors might not work everywhere we - // want to run these tests - const mouseevent = (typeArg, MouseEventInit) => { - const e = { type: typeArg }; - for (let key in MouseEventInit) { - e[key] = MouseEventInit[key]; + // The real constructors might not work everywhere we + // want to run these tests + const mouseevent = (typeArg, MouseEventInit) => { + const e = { type: typeArg }; + for (let key in MouseEventInit) { + e[key] = MouseEventInit[key]; + } + e.stopPropagation = sinon.spy(); + e.preventDefault = sinon.spy(); + return e; + }; + const touchevent = mouseevent; + + describe('Decode Mouse Events', function () { + it('should decode mousedown events', function (done) { + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + expect(bmask).to.be.equal(0x01); + expect(down).to.be.equal(1); + done(); + }; + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + }); + it('should decode mouseup events', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + expect(bmask).to.be.equal(0x01); + if (calls++ === 1) { + expect(down).to.not.be.equal(1); + done(); } - e.stopPropagation = sinon.spy(); - e.preventDefault = sinon.spy(); - return e; - }; - const touchevent = mouseevent; + }; + mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); + mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' })); + }); + it('should decode mousemove events', function (done) { + const mouse = new Mouse(target); + mouse.onmousemove = (x, y) => { + // Note that target relative coordinates are sent + expect(x).to.be.equal(40); + expect(y).to.be.equal(10); + done(); + }; + mouse._handleMouseMove(mouseevent('mousemove', + { clientX: 50, clientY: 20 })); + }); + it('should decode mousewheel events', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + expect(bmask).to.be.equal(1 << 6); + if (calls === 1) { + expect(down).to.be.equal(1); + } else if (calls === 2) { + expect(down).to.not.be.equal(1); + done(); + } + }; + mouse._handleMouseWheel(mouseevent('mousewheel', + { + deltaX: 50, + deltaY: 0, + deltaMode: 0 + })); + }); + }); - describe('Decode Mouse Events', function() { - it('should decode mousedown events', function(done) { - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - expect(bmask).to.be.equal(0x01); - expect(down).to.be.equal(1); - done(); - }; - mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); - }); - it('should decode mouseup events', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - expect(bmask).to.be.equal(0x01); - if (calls++ === 1) { - expect(down).to.not.be.equal(1); - done(); - } - }; - mouse._handleMouseDown(mouseevent('mousedown', { button: '0x01' })); - mouse._handleMouseUp(mouseevent('mouseup', { button: '0x01' })); - }); - it('should decode mousemove events', function(done) { - const mouse = new Mouse(target); - mouse.onmousemove = (x, y) => { - // Note that target relative coordinates are sent - expect(x).to.be.equal(40); - expect(y).to.be.equal(10); - done(); - }; - mouse._handleMouseMove(mouseevent('mousemove', - { clientX: 50, clientY: 20 })); - }); - it('should decode mousewheel events', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - expect(bmask).to.be.equal(1<<6); - if (calls === 1) { - expect(down).to.be.equal(1); - } else if (calls === 2) { - expect(down).to.not.be.equal(1); - done(); - } - }; - mouse._handleMouseWheel(mouseevent('mousewheel', - { deltaX: 50, deltaY: 0, - deltaMode: 0})); - }); + describe('Double-click for Touch', function () { + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should use same pos for 2nd tap if close enough', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + done(); + } + }; + // touch events are sent in an array of events + // with one item for each touch point + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }] } + )); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }] } + )); }); - describe('Double-click for Touch', function() { - - beforeEach(function () { this.clock = sinon.useFakeTimers(); }); - afterEach(function () { this.clock.restore(); }); - - it('should use same pos for 2nd tap if close enough', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - done(); - } - }; - // touch events are sent in an array of events - // with one item for each touch point - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(200); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if far apart', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(200); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 57, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 56, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if not soon enough', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 78, clientY: 46 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 79, clientY: 45 }]})); - this.clock.tick(500); - mouse._handleMouseDown(touchevent( - 'touchstart', { touches: [{ clientX: 67, clientY: 35 }]})); - this.clock.tick(10); - mouse._handleMouseUp(touchevent( - 'touchend', { touches: [{ clientX: 66, clientY: 36 }]})); - }); - - it('should not modify 2nd tap pos if not touch', function(done) { - let calls = 0; - const mouse = new Mouse(target); - mouse.onmousebutton = (x, y, down, bmask) => { - calls++; - if (calls === 1) { - expect(down).to.be.equal(1); - expect(x).to.be.equal(68); - expect(y).to.be.equal(36); - } else if (calls === 3) { - expect(down).to.be.equal(1); - expect(x).to.not.be.equal(68); - expect(y).to.not.be.equal(36); - done(); - } - }; - mouse._handleMouseDown(mouseevent( - 'mousedown', { button: '0x01', clientX: 78, clientY: 46 })); - this.clock.tick(10); - mouse._handleMouseUp(mouseevent( - 'mouseup', { button: '0x01', clientX: 79, clientY: 45 })); - this.clock.tick(200); - mouse._handleMouseDown(mouseevent( - 'mousedown', { button: '0x01', clientX: 67, clientY: 35 })); - this.clock.tick(10); - mouse._handleMouseUp(mouseevent( - 'mouseup', { button: '0x01', clientX: 66, clientY: 36 })); - }); - + it('should not modify 2nd tap pos if far apart', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }] } + )); + this.clock.tick(200); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 57, clientY: 35 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 56, clientY: 36 }] } + )); }); - describe('Accumulate mouse wheel events with small delta', function() { - - beforeEach(function () { this.clock = sinon.useFakeTimers(); }); - afterEach(function () { this.clock.restore(); }); - - it('should accumulate wheel events if small enough', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - this.clock.tick(10); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - - // threshold is 10 - expect(mouse._accumulatedWheelDeltaX).to.be.equal(8); - - this.clock.tick(10); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 0, deltaMode: 0 })); - - expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up - - this.clock.tick(10); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 4, deltaY: 9, deltaMode: 0 })); - - expect(mouse._accumulatedWheelDeltaX).to.be.equal(4); - expect(mouse._accumulatedWheelDeltaY).to.be.equal(9); - - expect(mouse.onmousebutton).to.have.callCount(2); // still - }); - - it('should not accumulate large wheel events', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 11, deltaY: 0, deltaMode: 0 })); - this.clock.tick(10); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 0, deltaY: 70, deltaMode: 0 })); - this.clock.tick(10); - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 400, deltaY: 400, deltaMode: 0 })); - - expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up - }); - - it('should send even small wheel events after a timeout', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 1, deltaY: 0, deltaMode: 0 })); - this.clock.tick(51); // timeout on 50 ms - - expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up - }); - - it('should account for non-zero deltaMode', function () { - const mouse = new Mouse(target); - mouse.onmousebutton = sinon.spy(); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 0, deltaY: 2, deltaMode: 1 })); - - this.clock.tick(10); - - mouse._handleMouseWheel(mouseevent( - 'mousewheel', { clientX: 18, clientY: 40, - deltaX: 1, deltaY: 0, deltaMode: 2 })); - - expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up - }); + it('should not modify 2nd tap pos if not soon enough', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 78, clientY: 46 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 79, clientY: 45 }] } + )); + this.clock.tick(500); + mouse._handleMouseDown(touchevent( + 'touchstart', { touches: [{ clientX: 67, clientY: 35 }] } + )); + this.clock.tick(10); + mouse._handleMouseUp(touchevent( + 'touchend', { touches: [{ clientX: 66, clientY: 36 }] } + )); }); + it('should not modify 2nd tap pos if not touch', function (done) { + let calls = 0; + const mouse = new Mouse(target); + mouse.onmousebutton = (x, y, down, bmask) => { + calls++; + if (calls === 1) { + expect(down).to.be.equal(1); + expect(x).to.be.equal(68); + expect(y).to.be.equal(36); + } else if (calls === 3) { + expect(down).to.be.equal(1); + expect(x).to.not.be.equal(68); + expect(y).to.not.be.equal(36); + done(); + } + }; + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 78, clientY: 46 } + )); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 79, clientY: 45 } + )); + this.clock.tick(200); + mouse._handleMouseDown(mouseevent( + 'mousedown', { button: '0x01', clientX: 67, clientY: 35 } + )); + this.clock.tick(10); + mouse._handleMouseUp(mouseevent( + 'mouseup', { button: '0x01', clientX: 66, clientY: 36 } + )); + }); + }); + + describe('Accumulate mouse wheel events with small delta', function () { + beforeEach(function () { this.clock = sinon.useFakeTimers(); }); + afterEach(function () { this.clock.restore(); }); + + it('should accumulate wheel events if small enough', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 4, + deltaY: 0, + deltaMode: 0 + } + )); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 4, + deltaY: 0, + deltaMode: 0 + } + )); + + // threshold is 10 + expect(mouse._accumulatedWheelDeltaX).to.be.equal(8); + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 4, + deltaY: 0, + deltaMode: 0 + } + )); + + expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up + + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 4, + deltaY: 9, + deltaMode: 0 + } + )); + + expect(mouse._accumulatedWheelDeltaX).to.be.equal(4); + expect(mouse._accumulatedWheelDeltaY).to.be.equal(9); + + expect(mouse.onmousebutton).to.have.callCount(2); // still + }); + + it('should not accumulate large wheel events', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 11, + deltaY: 0, + deltaMode: 0 + } + )); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 0, + deltaY: 70, + deltaMode: 0 + } + )); + this.clock.tick(10); + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 400, + deltaY: 400, + deltaMode: 0 + } + )); + + expect(mouse.onmousebutton).to.have.callCount(8); // mouse down and up + }); + + it('should send even small wheel events after a timeout', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 1, + deltaY: 0, + deltaMode: 0 + } + )); + this.clock.tick(51); // timeout on 50 ms + + expect(mouse.onmousebutton).to.have.callCount(2); // mouse down and up + }); + + it('should account for non-zero deltaMode', function () { + const mouse = new Mouse(target); + mouse.onmousebutton = sinon.spy(); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 0, + deltaY: 2, + deltaMode: 1 + } + )); + + this.clock.tick(10); + + mouse._handleMouseWheel(mouseevent( + 'mousewheel', { + clientX: 18, + clientY: 40, + deltaX: 1, + deltaY: 0, + deltaMode: 2 + } + )); + + expect(mouse.onmousebutton).to.have.callCount(4); // mouse down and up + }); + }); }); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 8369a5d8..bca822f8 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -9,2250 +9,2345 @@ import sinon from '../vendor/sinon.js'; /* UIEvent constructor polyfill for IE */ (() => { - if (typeof window.UIEvent === "function") return; + if (typeof window.UIEvent === 'function') return; - function UIEvent ( event, params ) { - params = params || { bubbles: false, cancelable: false, view: window, detail: undefined }; - const evt = document.createEvent( 'UIEvent' ); - evt.initUIEvent( event, params.bubbles, params.cancelable, params.view, params.detail ); - return evt; - } + function UIEvent(event, params) { + params = params || { + bubbles: false, cancelable: false, view: window, detail: undefined + }; + const evt = document.createEvent('UIEvent'); + evt.initUIEvent(event, params.bubbles, params.cancelable, params.view, params.detail); + return evt; + } - UIEvent.prototype = window.UIEvent.prototype; + UIEvent.prototype = window.UIEvent.prototype; - window.UIEvent = UIEvent; + window.UIEvent = UIEvent; })(); function push8(arr, num) { - "use strict"; - arr.push(num & 0xFF); + 'use strict'; + + arr.push(num & 0xFF); } function push16(arr, num) { - "use strict"; - arr.push((num >> 8) & 0xFF, - num & 0xFF); + 'use strict'; + + arr.push((num >> 8) & 0xFF, + num & 0xFF); } function push32(arr, num) { - "use strict"; - arr.push((num >> 24) & 0xFF, - (num >> 16) & 0xFF, - (num >> 8) & 0xFF, - num & 0xFF); + 'use strict'; + + arr.push((num >> 24) & 0xFF, + (num >> 16) & 0xFF, + (num >> 8) & 0xFF, + num & 0xFF); } -describe('Remote Frame Buffer Protocol Client', function() { - let clock; - let raf; +describe('Remote Frame Buffer Protocol Client', function () { + let clock; + let raf; - before(FakeWebSocket.replace); - after(FakeWebSocket.restore); + before(FakeWebSocket.replace); + after(FakeWebSocket.restore); - before(function () { - this.clock = clock = sinon.useFakeTimers(); - // sinon doesn't support this yet - raf = window.requestAnimationFrame; - window.requestAnimationFrame = setTimeout; - // Use a single set of buffers instead of reallocating to - // speed up tests - const sock = new Websock(); - const _sQ = new Uint8Array(sock._sQbufferSize); - const rQ = new Uint8Array(sock._rQbufferSize); + before(function () { + this.clock = clock = sinon.useFakeTimers(); + // sinon doesn't support this yet + raf = window.requestAnimationFrame; + window.requestAnimationFrame = setTimeout; + // Use a single set of buffers instead of reallocating to + // speed up tests + const sock = new Websock(); + const _sQ = new Uint8Array(sock._sQbufferSize); + const rQ = new Uint8Array(sock._rQbufferSize); - Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; - Websock.prototype._allocate_buffers = function () { - this._sQ = _sQ; - this._rQ = rQ; - }; + Websock.prototype._old_allocate_buffers = Websock.prototype._allocate_buffers; + Websock.prototype._allocate_buffers = function () { + this._sQ = _sQ; + this._rQ = rQ; + }; + }); + after(function () { + Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; + this.clock.restore(); + window.requestAnimationFrame = raf; + }); + + let container; + let rfbs; + + beforeEach(function () { + // Create a container element for all RFB objects to attach to + container = document.createElement('div'); + container.style.width = '100%'; + container.style.height = '100%'; + document.body.appendChild(container); + + // And track all created RFB objects + rfbs = []; + }); + afterEach(function () { + // Make sure every created RFB object is properly cleaned up + // or they might affect subsequent tests + rfbs.forEach(function (rfb) { + rfb.disconnect(); + expect(rfb._disconnect).to.have.been.called; + }); + rfbs = []; + + document.body.removeChild(container); + container = null; + }); + + function make_rfb(url, options) { + url = url || 'wss://host:8675'; + const rfb = new RFB(container, url, options); + clock.tick(); + rfb._sock._websocket._open(); + rfb._rfb_connection_state = 'connected'; + sinon.spy(rfb, '_disconnect'); + rfbs.push(rfb); + return rfb; + } + + describe('Connecting/Disconnecting', function () { + describe('#RFB', function () { + it('should set the current state to "connecting"', function () { + const client = new RFB(document.createElement('div'), 'wss://host:8675'); + client._rfb_connection_state = ''; + this.clock.tick(); + expect(client._rfb_connection_state).to.equal('connecting'); + }); + + it('should actually connect to the websocket', function () { + const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); + sinon.spy(client._sock, 'open'); + this.clock.tick(); + expect(client._sock.open).to.have.been.calledOnce; + expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); + }); }); - after(function () { - Websock.prototype._allocate_buffers = Websock.prototype._old_allocate_buffers; - this.clock.restore(); - window.requestAnimationFrame = raf; + describe('#disconnect', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should go to state "disconnecting" before "disconnected"', function () { + sinon.spy(client, '_updateConnectionState'); + client.disconnect(); + expect(client._updateConnectionState).to.have.been.calledTwice; + expect(client._updateConnectionState.getCall(0).args[0]) + .to.equal('disconnecting'); + expect(client._updateConnectionState.getCall(1).args[0]) + .to.equal('disconnected'); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should unregister error event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('error'); + }); + + it('should unregister message event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('message'); + }); + + it('should unregister open event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + expect(client._sock.off).to.have.been.calledWith('open'); + }); }); - let container; - let rfbs; + describe('#sendCredentials', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._rfb_connection_state = 'connecting'; + }); + it('should set the rfb credentials properly"', function () { + client.sendCredentials({ password: 'pass' }); + expect(client._rfb_credentials).to.deep.equal({ password: 'pass' }); + }); + + it('should call init_msg "soon"', function () { + client._init_msg = sinon.spy(); + client.sendCredentials({ password: 'pass' }); + this.clock.tick(5); + expect(client._init_msg).to.have.been.calledOnce; + }); + }); + }); + + describe('Public API Basic Behavior', function () { + let client; beforeEach(function () { - // Create a container element for all RFB objects to attach to - container = document.createElement('div'); - container.style.width = "100%"; - container.style.height = "100%"; - document.body.appendChild(container); - - // And track all created RFB objects - rfbs = []; + client = make_rfb(); }); + + describe('#sendCtrlAlDel', function () { + it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { + const expected = { _sQ: new Uint8Array(48), _sQlen: 0, flush: () => {} }; + RFB.messages.keyEvent(expected, 0xFFE3, 1); + RFB.messages.keyEvent(expected, 0xFFE9, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 1); + RFB.messages.keyEvent(expected, 0xFFFF, 0); + RFB.messages.keyEvent(expected, 0xFFE9, 0); + RFB.messages.keyEvent(expected, 0xFFE3, 0); + + client.sendCtrlAltDel(); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the keys if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = 'connecting'; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the keys if we are set as view_only', function () { + sinon.spy(client._sock, 'flush'); + client._viewOnly = true; + client.sendCtrlAltDel(); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('#sendKey', function () { + it('should send a single key with the given code and state (down = true)', function () { + const expected = { _sQ: new Uint8Array(8), _sQlen: 0, flush: () => {} }; + RFB.messages.keyEvent(expected, 123, 1); + client.sendKey(123, 'Key123', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should send both a down and up event if the state is not specified', function () { + const expected = { _sQ: new Uint8Array(16), _sQlen: 0, flush: () => {} }; + RFB.messages.keyEvent(expected, 123, 1); + RFB.messages.keyEvent(expected, 123, 0); + client.sendKey(123, 'Key123'); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send the key if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = 'connecting'; + client.sendKey(123, 'Key123'); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send the key if we are set as view_only', function () { + sinon.spy(client._sock, 'flush'); + client._viewOnly = true; + client.sendKey(123, 'Key123'); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should send QEMU extended events if supported', function () { + client._qemuExtKeyEventSupported = true; + const expected = { _sQ: new Uint8Array(12), _sQlen: 0, flush: () => {} }; + RFB.messages.QEMUExtendedKeyEvent(expected, 0x20, true, 0x0039); + client.sendKey(0x20, 'Space', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should not send QEMU extended events if unknown key code', function () { + client._qemuExtKeyEventSupported = true; + const expected = { _sQ: new Uint8Array(8), _sQlen: 0, flush: () => {} }; + RFB.messages.keyEvent(expected, 123, 1); + client.sendKey(123, 'FooBar', true); + expect(client._sock).to.have.sent(expected._sQ); + }); + }); + + describe('#focus', function () { + it('should move focus to canvas object', function () { + client._canvas.focus = sinon.spy(); + client.focus(); + expect(client._canvas.focus).to.have.been.called.once; + }); + }); + + describe('#blur', function () { + it('should remove focus from canvas object', function () { + client._canvas.blur = sinon.spy(); + client.blur(); + expect(client._canvas.blur).to.have.been.called.once; + }); + }); + + describe('#clipboardPasteFrom', function () { + it('should send the given text in a paste event', function () { + const expected = { + _sQ: new Uint8Array(11), + _sQlen: 0, + _sQbufferSize: 11, + flush: () => {} + }; + RFB.messages.clientCutText(expected, 'abc'); + client.clipboardPasteFrom('abc'); + expect(client._sock).to.have.sent(expected._sQ); + }); + + it('should flush multiple times for large clipboards', function () { + sinon.spy(client._sock, 'flush'); + let long_text = ''; + for (let i = 0; i < client._sock._sQbufferSize + 100; i++) { + long_text += 'a'; + } + client.clipboardPasteFrom(long_text); + expect(client._sock.flush).to.have.been.calledTwice; + }); + + it('should not send the text if we are not in a normal state', function () { + sinon.spy(client._sock, 'flush'); + client._rfb_connection_state = 'connecting'; + client.clipboardPasteFrom('abc'); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('XVP operations', function () { + beforeEach(function () { + client._rfb_xvp_ver = 1; + }); + + it('should send the shutdown signal on #machineShutdown', function () { + client.machineShutdown(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02])); + }); + + it('should send the reboot signal on #machineReboot', function () { + client.machineReboot(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03])); + }); + + it('should send the reset signal on #machineReset', function () { + client.machineReset(); + expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04])); + }); + + it('should not send XVP operations with higher versions than we support', function () { + sinon.spy(client._sock, 'flush'); + client._xvpOp(2, 7); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + }); + + describe('Clipping', function () { + let client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.clipViewport = true; + }); + + it('should update display clip state when changing the property', function () { + const spy = sinon.spy(client._display, 'clipViewport', ['set']); + + client.clipViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + spy.set.reset(); + + client.clipViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + }); + + it('should update the viewport when the container size changes', function () { + sinon.spy(client._display, 'viewportChangeSize'); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.have.been.calledOnce; + expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50); + }); + + it('should update the viewport when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00]; + + sinon.spy(client._display, 'viewportChangeSize'); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + // FIXME: Display implicitly calls viewportChangeSize() when + // resizing the framebuffer, hence calledTwice. + expect(client._display.viewportChangeSize).to.have.been.calledTwice; + expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80); + }); + + it('should not update the viewport if not clipping', function () { + client.clipViewport = false; + sinon.spy(client._display, 'viewportChangeSize'); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + it('should not update the viewport if scaling', function () { + client.scaleViewport = true; + sinon.spy(client._display, 'viewportChangeSize'); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.viewportChangeSize).to.not.have.been.called; + }); + + describe('Dragging', function () { + beforeEach(function () { + client.dragViewport = true; + sinon.spy(RFB.messages, 'pointerEvent'); + }); + + afterEach(function () { + RFB.messages.pointerEvent.restore(); + }); + + it('should not send button messages when initiating viewport dragging', function () { + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should send button messages when release without movement', function () { + // Just up and down + client._handleMouseButton(13, 9, 0x001); + client._handleMouseButton(13, 9, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + + RFB.messages.pointerEvent.reset(); + + // Small movement + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(15, 14); + client._handleMouseButton(15, 14, 0x000); + expect(RFB.messages.pointerEvent).to.have.been.calledTwice; + }); + + it('should send button message directly when drag is disabled', function () { + client.dragViewport = false; + client._handleMouseButton(13, 9, 0x001); + expect(RFB.messages.pointerEvent).to.have.been.calledOnce; + }); + + it('should be initiate viewport dragging on sufficient movement', function () { + sinon.spy(client._display, 'viewportChangePos'); + + // Too small movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(18, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.not.have.been.called; + + // Sufficient movement + + client._handleMouseMove(43, 9); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); + + client._display.viewportChangePos.reset(); + + // Now a small movement should move right away + + client._handleMouseMove(43, 14); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + expect(client._display.viewportChangePos).to.have.been.calledOnce; + expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); + }); + + it('should not send button messages when dragging ends', function () { + // First the movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + expect(RFB.messages.pointerEvent).to.not.have.been.called; + }); + + it('should terminate viewport dragging on a button up event', function () { + // First the dragging movement + + client._handleMouseButton(13, 9, 0x001); + client._handleMouseMove(43, 9); + client._handleMouseButton(43, 9, 0x000); + + // Another movement now should not move the viewport + + sinon.spy(client._display, 'viewportChangePos'); + + client._handleMouseMove(43, 59); + + expect(client._display.viewportChangePos).to.not.have.been.called; + }); + }); + }); + + describe('Scaling', function () { + let client; + beforeEach(function () { + client = make_rfb(); + container.style.width = '70px'; + container.style.height = '80px'; + client.scaleViewport = true; + }); + + it('should update display scale factor when changing the property', function () { + const spy = sinon.spy(client._display, 'scale', ['set']); + sinon.spy(client._display, 'autoscale'); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(1.0); + expect(client._display.autoscale).to.not.have.been.called; + + client.scaleViewport = true; + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should update the clipping setting when changing the property', function () { + client.clipViewport = true; + + const spy = sinon.spy(client._display, 'clipViewport', ['set']); + + client.scaleViewport = false; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(true); + + spy.set.reset(); + + client.scaleViewport = true; + expect(spy.set).to.have.been.calledOnce; + expect(spy.set).to.have.been.calledWith(false); + }); + + it('should update the scaling when the container size changes', function () { + sinon.spy(client._display, 'autoscale'); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(40, 50); + }); + + it('should update the scaling when the remote session resizes', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, + 0x00, 0x00, 0x00, 0x00]; + + sinon.spy(client._display, 'autoscale'); + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(client._display.autoscale).to.have.been.calledOnce; + expect(client._display.autoscale).to.have.been.calledWith(70, 80); + }); + + it('should not update the display scale factor if not scaling', function () { + client.scaleViewport = false; + + sinon.spy(client._display, 'autoscale'); + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(); + + expect(client._display.autoscale).to.not.have.been.called; + }); + }); + + describe('Remote resize', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._supportsSetDesktopSize = true; + client.resizeSession = true; + container.style.width = '70px'; + container.style.height = '80px'; + sinon.spy(RFB.messages, 'setDesktopSize'); + }); + afterEach(function () { - // Make sure every created RFB object is properly cleaned up - // or they might affect subsequent tests - rfbs.forEach(function (rfb) { - rfb.disconnect(); - expect(rfb._disconnect).to.have.been.called; - }); - rfbs = []; - - document.body.removeChild(container); - container = null; + RFB.messages.setDesktopSize.restore(); }); - function make_rfb (url, options) { - url = url || 'wss://host:8675'; - const rfb = new RFB(container, url, options); - clock.tick(); - rfb._sock._websocket._open(); - rfb._rfb_connection_state = 'connected'; - sinon.spy(rfb, "_disconnect"); - rfbs.push(rfb); - return rfb; - } - - describe('Connecting/Disconnecting', function () { - describe('#RFB', function () { - it('should set the current state to "connecting"', function () { - const client = new RFB(document.createElement('div'), 'wss://host:8675'); - client._rfb_connection_state = ''; - this.clock.tick(); - expect(client._rfb_connection_state).to.equal('connecting'); - }); - - it('should actually connect to the websocket', function () { - const client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); - sinon.spy(client._sock, 'open'); - this.clock.tick(); - expect(client._sock.open).to.have.been.calledOnce; - expect(client._sock.open).to.have.been.calledWith('ws://HOST:8675/PATH'); - }); - }); - - describe('#disconnect', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); - - it('should go to state "disconnecting" before "disconnected"', function () { - sinon.spy(client, '_updateConnectionState'); - client.disconnect(); - expect(client._updateConnectionState).to.have.been.calledTwice; - expect(client._updateConnectionState.getCall(0).args[0]) - .to.equal('disconnecting'); - expect(client._updateConnectionState.getCall(1).args[0]) - .to.equal('disconnected'); - expect(client._rfb_connection_state).to.equal('disconnected'); - }); - - it('should unregister error event handler', function () { - sinon.spy(client._sock, 'off'); - client.disconnect(); - expect(client._sock.off).to.have.been.calledWith('error'); - }); - - it('should unregister message event handler', function () { - sinon.spy(client._sock, 'off'); - client.disconnect(); - expect(client._sock.off).to.have.been.calledWith('message'); - }); - - it('should unregister open event handler', function () { - sinon.spy(client._sock, 'off'); - client.disconnect(); - expect(client._sock.off).to.have.been.calledWith('open'); - }); - }); - - describe('#sendCredentials', function () { - let client; - beforeEach(function () { - client = make_rfb(); - client._rfb_connection_state = 'connecting'; - }); - - it('should set the rfb credentials properly"', function () { - client.sendCredentials({ password: 'pass' }); - expect(client._rfb_credentials).to.deep.equal({ password: 'pass' }); - }); - - it('should call init_msg "soon"', function () { - client._init_msg = sinon.spy(); - client.sendCredentials({ password: 'pass' }); - this.clock.tick(5); - expect(client._init_msg).to.have.been.calledOnce; - }); - }); + it('should only request a resize when turned on', function () { + client.resizeSession = false; + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + client.resizeSession = true; + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; }); - describe('Public API Basic Behavior', function () { - let client; + it('should request a resize when initially connecting', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00]; + + // First message should trigger a resize + + client._supportsSetDesktopSize = false; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0); + + RFB.messages.setDesktopSize.reset(); + + // Second message should not trigger a resize + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should request a resize when the container resizes', function () { + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + }); + + it('should not resize until the container size is stable', function () { + container.style.width = '20px'; + container.style.height = '30px'; + const event1 = new UIEvent('resize'); + window.dispatchEvent(event1); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + container.style.width = '40px'; + container.style.height = '50px'; + const event2 = new UIEvent('resize'); + window.dispatchEvent(event2); + clock.tick(400); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + + clock.tick(200); + + expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; + expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); + }); + + it('should not resize when resize is disabled', function () { + client._resizeSession = false; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when resize is not supported', function () { + client._supportsSetDesktopSize = false; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not resize when in view only mode', function () { + client._viewOnly = true; + + container.style.width = '40px'; + container.style.height = '50px'; + const event = new UIEvent('resize'); + window.dispatchEvent(event); + clock.tick(1000); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + + it('should not try to override a server resize', function () { + // Simple ExtendedDesktopSize FBU message + const incoming = [0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, + 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, + 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, + 0x00, 0x00, 0x00, 0x00]; + + client._sock._websocket._receive_data(new Uint8Array(incoming)); + + expect(RFB.messages.setDesktopSize).to.not.have.been.called; + }); + }); + + describe('Misc Internals', function () { + describe('#_updateConnectionState', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should clear the disconnect timer if the state is not "disconnecting"', function () { + const spy = sinon.spy(); + client._disconnTimer = setTimeout(spy, 50); + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + this.clock.tick(51); + expect(spy).to.not.have.been.called; + expect(client._disconnTimer).to.be.null; + }); + + it('should set the rfb_connection_state', function () { + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + expect(client._rfb_connection_state).to.equal('connected'); + }); + + it('should not change the state when we are disconnected', function () { + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnected'); + client._updateConnectionState('connecting'); + expect(client._rfb_connection_state).to.not.equal('connecting'); + }); + + it('should ignore state changes to the same state', function () { + const connectSpy = sinon.spy(); + client.addEventListener('connect', connectSpy); + + expect(client._rfb_connection_state).to.equal('connected'); + client._updateConnectionState('connected'); + expect(connectSpy).to.not.have.been.called; + + client.disconnect(); + + const disconnectSpy = sinon.spy(); + client.addEventListener('disconnect', disconnectSpy); + + expect(client._rfb_connection_state).to.equal('disconnected'); + client._updateConnectionState('disconnected'); + expect(disconnectSpy).to.not.have.been.called; + }); + + it('should ignore illegal state changes', function () { + const spy = sinon.spy(); + client.addEventListener('disconnect', spy); + client._updateConnectionState('disconnected'); + expect(client._rfb_connection_state).to.not.equal('disconnected'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('#_fail', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._fail(); + expect(client._sock.close).to.have.been.calledOnce; + }); + + it('should transition to disconnected', function () { + sinon.spy(client, '_updateConnectionState'); + client._fail(); + this.clock.tick(2000); + expect(client._updateConnectionState).to.have.been.called; + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should set clean_disconnect variable', function () { + client._rfb_clean_disconnect = true; + client._rfb_connection_state = 'connected'; + client._fail(); + expect(client._rfb_clean_disconnect).to.be.false; + }); + + it('should result in disconnect event with clean set to false', function () { + client._rfb_connection_state = 'connected'; + const spy = sinon.spy(); + client.addEventListener('disconnect', spy); + client._fail(); + this.clock.tick(2000); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.clean).to.be.false; + }); + }); + }); + + describe('Connection States', function () { + describe('connecting', function () { + it('should open the websocket connection', function () { + const client = new RFB(document.createElement('div'), + 'ws://HOST:8675/PATH'); + sinon.spy(client._sock, 'open'); + this.clock.tick(); + expect(client._sock.open).to.have.been.calledOnce; + }); + }); + + describe('connected', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should result in a connect event if state becomes connected', function () { + const spy = sinon.spy(); + client.addEventListener('connect', spy); + client._rfb_connection_state = 'connecting'; + client._updateConnectionState('connected'); + expect(spy).to.have.been.calledOnce; + }); + + it('should not result in a connect event if the state is not "connected"', function () { + const spy = sinon.spy(); + client.addEventListener('connect', spy); + client._sock._websocket.open = () => {}; // explicitly don't call onopen + client._updateConnectionState('connecting'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('disconnecting', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () { + sinon.spy(client, '_updateConnectionState'); + client._sock._websocket.close = () => {}; // explicitly don't call onclose + client._updateConnectionState('disconnecting'); + this.clock.tick(3 * 1000); + expect(client._updateConnectionState).to.have.been.calledTwice; + expect(client._rfb_disconnect_reason).to.not.equal(''); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { + client._updateConnectionState('disconnecting'); + this.clock.tick(3 * 1000 / 2); + client._sock._websocket.close(); + this.clock.tick(3 * 1000 / 2 + 1); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should close the WebSocket connection', function () { + sinon.spy(client._sock, 'close'); + client._updateConnectionState('disconnecting'); + expect(client._sock.close).to.have.been.calledOnce; + }); + + it('should not result in a disconnect event', function () { + const spy = sinon.spy(); + client.addEventListener('disconnect', spy); + client._sock._websocket.close = () => {}; // explicitly don't call onclose + client._updateConnectionState('disconnecting'); + expect(spy).to.not.have.been.called; + }); + }); + + describe('disconnected', function () { + let client; + beforeEach(function () { + client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); + }); + + it('should result in a disconnect event if state becomes "disconnected"', function () { + const spy = sinon.spy(); + client.addEventListener('disconnect', spy); + client._rfb_connection_state = 'disconnecting'; + client._updateConnectionState('disconnected'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.clean).to.be.true; + }); + + it('should result in a disconnect event without msg when no reason given', function () { + const spy = sinon.spy(); + client.addEventListener('disconnect', spy); + client._rfb_connection_state = 'disconnecting'; + client._rfb_disconnect_reason = ''; + client._updateConnectionState('disconnected'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0].length).to.equal(1); + }); + }); + }); + + describe('Protocol Initialization States', function () { + let client; + beforeEach(function () { + client = make_rfb(); + client._rfb_connection_state = 'connecting'; + }); + + describe('ProtocolVersion', function () { + function send_ver(ver, client) { + const arr = new Uint8Array(12); + for (let i = 0; i < ver.length; i++) { + arr[i + 4] = ver.charCodeAt(i); + } + arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' '; + arr[11] = '\n'; + client._sock._websocket._receive_data(arr); + } + + describe('version parsing', function () { + it('should interpret version 003.003 as version 3.3', function () { + send_ver('003.003', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.006 as version 3.3', function () { + send_ver('003.006', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.889 as version 3.3', function () { + send_ver('003.889', client); + expect(client._rfb_version).to.equal(3.3); + }); + + it('should interpret version 003.007 as version 3.7', function () { + send_ver('003.007', client); + expect(client._rfb_version).to.equal(3.7); + }); + + it('should interpret version 003.008 as version 3.8', function () { + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.000 as version 3.8', function () { + send_ver('004.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 004.001 as version 3.8', function () { + send_ver('004.001', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should interpret version 005.000 as version 3.8', function () { + send_ver('005.000', client); + expect(client._rfb_version).to.equal(3.8); + }); + + it('should fail on an invalid version', function () { + sinon.spy(client, '_fail'); + send_ver('002.000', client); + expect(client._fail).to.have.been.calledOnce; + }); + }); + + it('should send back the interpreted version', function () { + send_ver('004.000', client); + + const expected_str = 'RFB 003.008\n'; + const expected = []; + for (let i = 0; i < expected_str.length; i++) { + expected[i] = expected_str.charCodeAt(i); + } + + expect(client._sock).to.have.sent(new Uint8Array(expected)); + }); + + it('should transition to the Security state on successful negotiation', function () { + send_ver('003.008', client); + expect(client._rfb_init_state).to.equal('Security'); + }); + + describe('Repeater', function () { beforeEach(function () { - client = make_rfb(); + client = make_rfb('wss://host:8675', { repeaterID: '12345' }); + client._rfb_connection_state = 'connecting'; }); - describe('#sendCtrlAlDel', function () { - it('should sent ctrl[down]-alt[down]-del[down] then del[up]-alt[up]-ctrl[up]', function () { - const expected = {_sQ: new Uint8Array(48), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 0xFFE3, 1); - RFB.messages.keyEvent(expected, 0xFFE9, 1); - RFB.messages.keyEvent(expected, 0xFFFF, 1); - RFB.messages.keyEvent(expected, 0xFFFF, 0); - RFB.messages.keyEvent(expected, 0xFFE9, 0); - RFB.messages.keyEvent(expected, 0xFFE3, 0); + it('should interpret version 000.000 as a repeater', function () { + send_ver('000.000', client); + expect(client._rfb_version).to.equal(0); - client.sendCtrlAltDel(); - expect(client._sock).to.have.sent(expected._sQ); - }); - - it('should not send the keys if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "connecting"; - client.sendCtrlAltDel(); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should not send the keys if we are set as view_only', function () { - sinon.spy(client._sock, 'flush'); - client._viewOnly = true; - client.sendCtrlAltDel(); - expect(client._sock.flush).to.not.have.been.called; - }); + const sent_data = client._sock._websocket._get_sent_data(); + expect(new Uint8Array(sent_data.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0])); + expect(sent_data).to.have.length(250); }); - describe('#sendKey', function () { - it('should send a single key with the given code and state (down = true)', function () { - const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); - client.sendKey(123, 'Key123', true); - expect(client._sock).to.have.sent(expected._sQ); - }); - - it('should send both a down and up event if the state is not specified', function () { - const expected = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); - RFB.messages.keyEvent(expected, 123, 0); - client.sendKey(123, 'Key123'); - expect(client._sock).to.have.sent(expected._sQ); - }); - - it('should not send the key if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "connecting"; - client.sendKey(123, 'Key123'); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should not send the key if we are set as view_only', function () { - sinon.spy(client._sock, 'flush'); - client._viewOnly = true; - client.sendKey(123, 'Key123'); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should send QEMU extended events if supported', function () { - client._qemuExtKeyEventSupported = true; - const expected = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; - RFB.messages.QEMUExtendedKeyEvent(expected, 0x20, true, 0x0039); - client.sendKey(0x20, 'Space', true); - expect(client._sock).to.have.sent(expected._sQ); - }); - - it('should not send QEMU extended events if unknown key code', function () { - client._qemuExtKeyEventSupported = true; - const expected = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(expected, 123, 1); - client.sendKey(123, 'FooBar', true); - expect(client._sock).to.have.sent(expected._sQ); - }); - }); - - describe('#focus', function () { - it('should move focus to canvas object', function () { - client._canvas.focus = sinon.spy(); - client.focus(); - expect(client._canvas.focus).to.have.been.called.once; - }); - }); - - describe('#blur', function () { - it('should remove focus from canvas object', function () { - client._canvas.blur = sinon.spy(); - client.blur(); - expect(client._canvas.blur).to.have.been.called.once; - }); - }); - - describe('#clipboardPasteFrom', function () { - it('should send the given text in a paste event', function () { - const expected = {_sQ: new Uint8Array(11), _sQlen: 0, - _sQbufferSize: 11, flush: () => {}}; - RFB.messages.clientCutText(expected, 'abc'); - client.clipboardPasteFrom('abc'); - expect(client._sock).to.have.sent(expected._sQ); - }); - - it('should flush multiple times for large clipboards', function () { - sinon.spy(client._sock, 'flush'); - let long_text = ""; - for (let i = 0; i < client._sock._sQbufferSize + 100; i++) { - long_text += 'a'; - } - client.clipboardPasteFrom(long_text); - expect(client._sock.flush).to.have.been.calledTwice; - }); - - it('should not send the text if we are not in a normal state', function () { - sinon.spy(client._sock, 'flush'); - client._rfb_connection_state = "connecting"; - client.clipboardPasteFrom('abc'); - expect(client._sock.flush).to.not.have.been.called; - }); - }); - - describe("XVP operations", function () { - beforeEach(function () { - client._rfb_xvp_ver = 1; - }); - - it('should send the shutdown signal on #machineShutdown', function () { - client.machineShutdown(); - expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x02])); - }); - - it('should send the reboot signal on #machineReboot', function () { - client.machineReboot(); - expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x03])); - }); - - it('should send the reset signal on #machineReset', function () { - client.machineReset(); - expect(client._sock).to.have.sent(new Uint8Array([0xFA, 0x00, 0x01, 0x04])); - }); - - it('should not send XVP operations with higher versions than we support', function () { - sinon.spy(client._sock, 'flush'); - client._xvpOp(2, 7); - expect(client._sock.flush).to.not.have.been.called; - }); + it('should handle two step repeater negotiation', function () { + send_ver('000.000', client); + send_ver('003.008', client); + expect(client._rfb_version).to.equal(3.8); }); + }); }); - describe('Clipping', function () { - let client; + describe('Security', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + }); + + it('should simply receive the auth scheme when for versions < 3.7', function () { + client._rfb_version = 3.6; + const auth_scheme_raw = [1, 2, 3, 4]; + const auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) + + (auth_scheme_raw[2] << 8) + auth_scheme_raw[3]; + client._sock._websocket._receive_data(auth_scheme_raw); + expect(client._rfb_auth_scheme).to.equal(auth_scheme); + }); + + it('should prefer no authentication is possible', function () { + client._rfb_version = 3.7; + const auth_schemes = [2, 1, 3]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_auth_scheme).to.equal(1); + expect(client._sock).to.have.sent(new Uint8Array([1, 1])); + }); + + it('should choose for the most prefered scheme possible for versions >= 3.7', function () { + client._rfb_version = 3.7; + const auth_schemes = [2, 22, 16]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_auth_scheme).to.equal(22); + expect(client._sock).to.have.sent(new Uint8Array([22])); + }); + + it('should fail if there are no supported schemes for versions >= 3.7', function () { + sinon.spy(client, '_fail'); + client._rfb_version = 3.7; + const auth_schemes = [1, 32]; + client._sock._websocket._receive_data(auth_schemes); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { + client._rfb_version = 3.7; + const failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(failure_data); + + expect(client._fail).to.have.been.calledOnce; + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on no security types (reason: whoops)' + ); + }); + + it('should transition to the Authentication state and continue on successful negotiation', function () { + client._rfb_version = 3.7; + const auth_schemes = [1, 1]; + client._negotiate_authentication = sinon.spy(); + client._sock._websocket._receive_data(auth_schemes); + expect(client._rfb_init_state).to.equal('Authentication'); + expect(client._negotiate_authentication).to.have.been.calledOnce; + }); + }); + + describe('Authentication', function () { + beforeEach(function () { + client._rfb_init_state = 'Security'; + }); + + function send_security(type, cl) { + cl._sock._websocket._receive_data(new Uint8Array([1, type])); + } + + it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { + client._rfb_version = 3.6; + const err_msg = 'Whoopsies'; + const data = [0, 0, 0, 0]; + const err_len = err_msg.length; + push32(data, err_len); + for (let i = 0; i < err_len; i++) { + data.push(err_msg.charCodeAt(i)); + } + + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on authentication scheme (reason: Whoopsies)' + ); + }); + + it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { + client._rfb_version = 3.8; + send_security(1, client); + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); + + it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { + client._rfb_version = 3.7; + send_security(1, client); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an unknown auth scheme', function () { + sinon.spy(client, '_fail'); + client._rfb_version = 3.8; + send_security(57, client); + expect(client._fail).to.have.been.calledOnce; + }); + + describe('VNC Authentication (type 2) Handler', function () { beforeEach(function () { - client = make_rfb(); - container.style.width = '70px'; - container.style.height = '80px'; - client.clipViewport = true; + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; }); - it('should update display clip state when changing the property', function () { - const spy = sinon.spy(client._display, "clipViewport", ["set"]); + it('should fire the credentialsrequired event if missing a password', function () { + const spy = sinon.spy(); + client.addEventListener('credentialsrequired', spy); + send_security(2, client); - client.clipViewport = false; - expect(spy.set).to.have.been.calledOnce; - expect(spy.set).to.have.been.calledWith(false); - spy.set.reset(); + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); - client.clipViewport = true; - expect(spy.set).to.have.been.calledOnce; - expect(spy.set).to.have.been.calledWith(true); + expect(client._rfb_credentials).to.be.empty; + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(['password']); }); - it('should update the viewport when the container size changes', function () { - sinon.spy(client._display, "viewportChangeSize"); + it('should encrypt the password with DES and then send it back', function () { + client._rfb_credentials = { password: 'passwd' }; + send_security(2, client); + client._sock._websocket._get_sent_data(); // skip the choice of auth reply - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); - expect(client._display.viewportChangeSize).to.have.been.calledOnce; - expect(client._display.viewportChangeSize).to.have.been.calledWith(40, 50); + const des_pass = RFB.genDES('passwd', challenge); + expect(client._sock).to.have.sent(new Uint8Array(des_pass)); }); - it('should update the viewport when the remote session resizes', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, - 0x00, 0x00, 0x00, 0x00 ]; + it('should transition to SecurityResult immediately after sending the password', function () { + client._rfb_credentials = { password: 'passwd' }; + send_security(2, client); - sinon.spy(client._display, "viewportChangeSize"); + const challenge = []; + for (let i = 0; i < 16; i++) { challenge[i] = i; } + client._sock._websocket._receive_data(new Uint8Array(challenge)); - client._sock._websocket._receive_data(new Uint8Array(incoming)); - - // FIXME: Display implicitly calls viewportChangeSize() when - // resizing the framebuffer, hence calledTwice. - expect(client._display.viewportChangeSize).to.have.been.calledTwice; - expect(client._display.viewportChangeSize).to.have.been.calledWith(70, 80); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); + }); - it('should not update the viewport if not clipping', function () { - client.clipViewport = false; - sinon.spy(client._display, "viewportChangeSize"); - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); - - expect(client._display.viewportChangeSize).to.not.have.been.called; - }); - - it('should not update the viewport if scaling', function () { - client.scaleViewport = true; - sinon.spy(client._display, "viewportChangeSize"); - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); - - expect(client._display.viewportChangeSize).to.not.have.been.called; - }); - - describe('Dragging', function () { - beforeEach(function () { - client.dragViewport = true; - sinon.spy(RFB.messages, "pointerEvent"); - }); - - afterEach(function () { - RFB.messages.pointerEvent.restore(); - }); - - it('should not send button messages when initiating viewport dragging', function () { - client._handleMouseButton(13, 9, 0x001); - expect(RFB.messages.pointerEvent).to.not.have.been.called; - }); - - it('should send button messages when release without movement', function () { - // Just up and down - client._handleMouseButton(13, 9, 0x001); - client._handleMouseButton(13, 9, 0x000); - expect(RFB.messages.pointerEvent).to.have.been.calledTwice; - - RFB.messages.pointerEvent.reset(); - - // Small movement - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(15, 14); - client._handleMouseButton(15, 14, 0x000); - expect(RFB.messages.pointerEvent).to.have.been.calledTwice; - }); - - it('should send button message directly when drag is disabled', function () { - client.dragViewport = false; - client._handleMouseButton(13, 9, 0x001); - expect(RFB.messages.pointerEvent).to.have.been.calledOnce; - }); - - it('should be initiate viewport dragging on sufficient movement', function () { - sinon.spy(client._display, "viewportChangePos"); - - // Too small movement - - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(18, 9); - - expect(RFB.messages.pointerEvent).to.not.have.been.called; - expect(client._display.viewportChangePos).to.not.have.been.called; - - // Sufficient movement - - client._handleMouseMove(43, 9); - - expect(RFB.messages.pointerEvent).to.not.have.been.called; - expect(client._display.viewportChangePos).to.have.been.calledOnce; - expect(client._display.viewportChangePos).to.have.been.calledWith(-30, 0); - - client._display.viewportChangePos.reset(); - - // Now a small movement should move right away - - client._handleMouseMove(43, 14); - - expect(RFB.messages.pointerEvent).to.not.have.been.called; - expect(client._display.viewportChangePos).to.have.been.calledOnce; - expect(client._display.viewportChangePos).to.have.been.calledWith(0, -5); - }); - - it('should not send button messages when dragging ends', function () { - // First the movement - - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); - - expect(RFB.messages.pointerEvent).to.not.have.been.called; - }); - - it('should terminate viewport dragging on a button up event', function () { - // First the dragging movement - - client._handleMouseButton(13, 9, 0x001); - client._handleMouseMove(43, 9); - client._handleMouseButton(43, 9, 0x000); - - // Another movement now should not move the viewport - - sinon.spy(client._display, "viewportChangePos"); - - client._handleMouseMove(43, 59); - - expect(client._display.viewportChangePos).to.not.have.been.called; - }); - }); - }); - - describe('Scaling', function () { - let client; + describe('XVP Authentication (type 22) Handler', function () { beforeEach(function () { - client = make_rfb(); - container.style.width = '70px'; - container.style.height = '80px'; - client.scaleViewport = true; + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; }); - it('should update display scale factor when changing the property', function () { - const spy = sinon.spy(client._display, "scale", ["set"]); - sinon.spy(client._display, "autoscale"); - - client.scaleViewport = false; - expect(spy.set).to.have.been.calledOnce; - expect(spy.set).to.have.been.calledWith(1.0); - expect(client._display.autoscale).to.not.have.been.called; - - client.scaleViewport = true; - expect(client._display.autoscale).to.have.been.calledOnce; - expect(client._display.autoscale).to.have.been.calledWith(70, 80); + it('should fall through to standard VNC authentication upon completion', function () { + client._rfb_credentials = { + username: 'user', + target: 'target', + password: 'password' + }; + client._negotiate_std_vnc_auth = sinon.spy(); + send_security(22, client); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; }); - it('should update the clipping setting when changing the property', function () { - client.clipViewport = true; + it('should fire the credentialsrequired event if all credentials are missing', function () { + const spy = sinon.spy(); + client.addEventListener('credentialsrequired', spy); + client._rfb_credentials = {}; + send_security(22, client); - const spy = sinon.spy(client._display, "clipViewport", ["set"]); - - client.scaleViewport = false; - expect(spy.set).to.have.been.calledOnce; - expect(spy.set).to.have.been.calledWith(true); - - spy.set.reset(); - - client.scaleViewport = true; - expect(spy.set).to.have.been.calledOnce; - expect(spy.set).to.have.been.calledWith(false); + expect(client._rfb_credentials).to.be.empty; + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(['username', 'password', 'target']); }); - it('should update the scaling when the container size changes', function () { - sinon.spy(client._display, "autoscale"); + it('should fire the credentialsrequired event if some credentials are missing', function () { + const spy = sinon.spy(); + client.addEventListener('credentialsrequired', spy); + client._rfb_credentials = { + username: 'user', + target: 'target' + }; + send_security(22, client); - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); - - expect(client._display.autoscale).to.have.been.calledOnce; - expect(client._display.autoscale).to.have.been.calledWith(40, 50); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.types).to.have.members(['username', 'password', 'target']); }); - it('should update the scaling when the remote session resizes', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0xff, 0x00, 0xff, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0xff, 0x00, 0xff, - 0x00, 0x00, 0x00, 0x00 ]; + it('should send user and target separately', function () { + client._rfb_credentials = { + username: 'user', + target: 'target', + password: 'password' + }; + client._negotiate_std_vnc_auth = sinon.spy(); - sinon.spy(client._display, "autoscale"); + send_security(22, client); - client._sock._websocket._receive_data(new Uint8Array(incoming)); + const expected = [22, 4, 6]; // auth selection, len user, len target + for (let i = 0; i < 10; i++) { expected[i + 3] = 'usertarget'.charCodeAt(i); } - expect(client._display.autoscale).to.have.been.calledOnce; - expect(client._display.autoscale).to.have.been.calledWith(70, 80); + expect(client._sock).to.have.sent(new Uint8Array(expected)); }); + }); - it('should not update the display scale factor if not scaling', function () { - client.scaleViewport = false; - - sinon.spy(client._display, "autoscale"); - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(); - - expect(client._display.autoscale).to.not.have.been.called; - }); - }); - - describe('Remote resize', function () { - let client; + describe('TightVNC Authentication (type 16) Handler', function () { beforeEach(function () { - client = make_rfb(); - client._supportsSetDesktopSize = true; - client.resizeSession = true; - container.style.width = '70px'; - container.style.height = '80px'; - sinon.spy(RFB.messages, "setDesktopSize"); + client._rfb_init_state = 'Security'; + client._rfb_version = 3.8; + send_security(16, client); + client._sock._websocket._get_sent_data(); // skip the security reply }); - afterEach(function () { - RFB.messages.setDesktopSize.restore(); - }); + function send_num_str_pairs(pairs, client) { + const data = []; + push32(data, pairs.length); - it('should only request a resize when turned on', function () { - client.resizeSession = false; - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - client.resizeSession = true; - expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - }); - - it('should request a resize when initially connecting', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00 ]; - - // First message should trigger a resize - - client._supportsSetDesktopSize = false; - - client._sock._websocket._receive_data(new Uint8Array(incoming)); - - expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 70, 80, 0, 0); - - RFB.messages.setDesktopSize.reset(); - - // Second message should not trigger a resize - - client._sock._websocket._receive_data(new Uint8Array(incoming)); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - }); - - it('should request a resize when the container resizes', function () { - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(1000); - - expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); - }); - - it('should not resize until the container size is stable', function () { - container.style.width = '20px'; - container.style.height = '30px'; - const event1 = new UIEvent('resize'); - window.dispatchEvent(event1); - clock.tick(400); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - - container.style.width = '40px'; - container.style.height = '50px'; - const event2 = new UIEvent('resize'); - window.dispatchEvent(event2); - clock.tick(400); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - - clock.tick(200); - - expect(RFB.messages.setDesktopSize).to.have.been.calledOnce; - expect(RFB.messages.setDesktopSize).to.have.been.calledWith(sinon.match.object, 40, 50, 0, 0); - }); - - it('should not resize when resize is disabled', function () { - client._resizeSession = false; - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(1000); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - }); - - it('should not resize when resize is not supported', function () { - client._supportsSetDesktopSize = false; - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(1000); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - }); - - it('should not resize when in view only mode', function () { - client._viewOnly = true; - - container.style.width = '40px'; - container.style.height = '50px'; - const event = new UIEvent('resize'); - window.dispatchEvent(event); - clock.tick(1000); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - }); - - it('should not try to override a server resize', function () { - // Simple ExtendedDesktopSize FBU message - const incoming = [ 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x04, 0x00, 0x04, 0xff, 0xff, 0xfe, 0xcc, - 0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x04, - 0x00, 0x00, 0x00, 0x00 ]; - - client._sock._websocket._receive_data(new Uint8Array(incoming)); - - expect(RFB.messages.setDesktopSize).to.not.have.been.called; - }); - }); - - describe('Misc Internals', function () { - describe('#_updateConnectionState', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); - - it('should clear the disconnect timer if the state is not "disconnecting"', function () { - const spy = sinon.spy(); - client._disconnTimer = setTimeout(spy, 50); - client._rfb_connection_state = 'connecting'; - client._updateConnectionState('connected'); - this.clock.tick(51); - expect(spy).to.not.have.been.called; - expect(client._disconnTimer).to.be.null; - }); - - it('should set the rfb_connection_state', function () { - client._rfb_connection_state = 'connecting'; - client._updateConnectionState('connected'); - expect(client._rfb_connection_state).to.equal('connected'); - }); - - it('should not change the state when we are disconnected', function () { - client.disconnect(); - expect(client._rfb_connection_state).to.equal('disconnected'); - client._updateConnectionState('connecting'); - expect(client._rfb_connection_state).to.not.equal('connecting'); - }); - - it('should ignore state changes to the same state', function () { - const connectSpy = sinon.spy(); - client.addEventListener("connect", connectSpy); - - expect(client._rfb_connection_state).to.equal('connected'); - client._updateConnectionState('connected'); - expect(connectSpy).to.not.have.been.called; - - client.disconnect(); - - const disconnectSpy = sinon.spy(); - client.addEventListener("disconnect", disconnectSpy); - - expect(client._rfb_connection_state).to.equal('disconnected'); - client._updateConnectionState('disconnected'); - expect(disconnectSpy).to.not.have.been.called; - }); - - it('should ignore illegal state changes', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._updateConnectionState('disconnected'); - expect(client._rfb_connection_state).to.not.equal('disconnected'); - expect(spy).to.not.have.been.called; - }); - }); - - describe('#_fail', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); - - it('should close the WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._fail(); - expect(client._sock.close).to.have.been.calledOnce; - }); - - it('should transition to disconnected', function () { - sinon.spy(client, '_updateConnectionState'); - client._fail(); - this.clock.tick(2000); - expect(client._updateConnectionState).to.have.been.called; - expect(client._rfb_connection_state).to.equal('disconnected'); - }); - - it('should set clean_disconnect variable', function () { - client._rfb_clean_disconnect = true; - client._rfb_connection_state = 'connected'; - client._fail(); - expect(client._rfb_clean_disconnect).to.be.false; - }); - - it('should result in disconnect event with clean set to false', function () { - client._rfb_connection_state = 'connected'; - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._fail(); - this.clock.tick(2000); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.clean).to.be.false; - }); - - }); - }); - - describe('Connection States', function () { - describe('connecting', function () { - it('should open the websocket connection', function () { - const client = new RFB(document.createElement('div'), - 'ws://HOST:8675/PATH'); - sinon.spy(client._sock, 'open'); - this.clock.tick(); - expect(client._sock.open).to.have.been.calledOnce; - }); - }); - - describe('connected', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); - - it('should result in a connect event if state becomes connected', function () { - const spy = sinon.spy(); - client.addEventListener("connect", spy); - client._rfb_connection_state = 'connecting'; - client._updateConnectionState('connected'); - expect(spy).to.have.been.calledOnce; - }); - - it('should not result in a connect event if the state is not "connected"', function () { - const spy = sinon.spy(); - client.addEventListener("connect", spy); - client._sock._websocket.open = () => {}; // explicitly don't call onopen - client._updateConnectionState('connecting'); - expect(spy).to.not.have.been.called; - }); - }); - - describe('disconnecting', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); - - it('should force disconnect if we do not call Websock.onclose within the disconnection timeout', function () { - sinon.spy(client, '_updateConnectionState'); - client._sock._websocket.close = () => {}; // explicitly don't call onclose - client._updateConnectionState('disconnecting'); - this.clock.tick(3 * 1000); - expect(client._updateConnectionState).to.have.been.calledTwice; - expect(client._rfb_disconnect_reason).to.not.equal(""); - expect(client._rfb_connection_state).to.equal("disconnected"); - }); - - it('should not fail if Websock.onclose gets called within the disconnection timeout', function () { - client._updateConnectionState('disconnecting'); - this.clock.tick(3 * 1000 / 2); - client._sock._websocket.close(); - this.clock.tick(3 * 1000 / 2 + 1); - expect(client._rfb_connection_state).to.equal('disconnected'); - }); - - it('should close the WebSocket connection', function () { - sinon.spy(client._sock, 'close'); - client._updateConnectionState('disconnecting'); - expect(client._sock.close).to.have.been.calledOnce; - }); - - it('should not result in a disconnect event', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._sock._websocket.close = () => {}; // explicitly don't call onclose - client._updateConnectionState('disconnecting'); - expect(spy).to.not.have.been.called; - }); - }); - - describe('disconnected', function () { - let client; - beforeEach(function () { - client = new RFB(document.createElement('div'), 'ws://HOST:8675/PATH'); - }); - - it('should result in a disconnect event if state becomes "disconnected"', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._rfb_connection_state = 'disconnecting'; - client._updateConnectionState('disconnected'); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.clean).to.be.true; - }); - - it('should result in a disconnect event without msg when no reason given', function () { - const spy = sinon.spy(); - client.addEventListener("disconnect", spy); - client._rfb_connection_state = 'disconnecting'; - client._rfb_disconnect_reason = ""; - client._updateConnectionState('disconnected'); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0].length).to.equal(1); - }); - }); - }); - - describe('Protocol Initialization States', function () { - let client; - beforeEach(function () { - client = make_rfb(); - client._rfb_connection_state = 'connecting'; - }); - - describe('ProtocolVersion', function () { - function send_ver (ver, client) { - const arr = new Uint8Array(12); - for (let i = 0; i < ver.length; i++) { - arr[i+4] = ver.charCodeAt(i); - } - arr[0] = 'R'; arr[1] = 'F'; arr[2] = 'B'; arr[3] = ' '; - arr[11] = '\n'; - client._sock._websocket._receive_data(arr); + for (let i = 0; i < pairs.length; i++) { + push32(data, pairs[i][0]); + for (let j = 0; j < 4; j++) { + data.push(pairs[i][1].charCodeAt(j)); } - - describe('version parsing', function () { - it('should interpret version 003.003 as version 3.3', function () { - send_ver('003.003', client); - expect(client._rfb_version).to.equal(3.3); - }); - - it('should interpret version 003.006 as version 3.3', function () { - send_ver('003.006', client); - expect(client._rfb_version).to.equal(3.3); - }); - - it('should interpret version 003.889 as version 3.3', function () { - send_ver('003.889', client); - expect(client._rfb_version).to.equal(3.3); - }); - - it('should interpret version 003.007 as version 3.7', function () { - send_ver('003.007', client); - expect(client._rfb_version).to.equal(3.7); - }); - - it('should interpret version 003.008 as version 3.8', function () { - send_ver('003.008', client); - expect(client._rfb_version).to.equal(3.8); - }); - - it('should interpret version 004.000 as version 3.8', function () { - send_ver('004.000', client); - expect(client._rfb_version).to.equal(3.8); - }); - - it('should interpret version 004.001 as version 3.8', function () { - send_ver('004.001', client); - expect(client._rfb_version).to.equal(3.8); - }); - - it('should interpret version 005.000 as version 3.8', function () { - send_ver('005.000', client); - expect(client._rfb_version).to.equal(3.8); - }); - - it('should fail on an invalid version', function () { - sinon.spy(client, "_fail"); - send_ver('002.000', client); - expect(client._fail).to.have.been.calledOnce; - }); - }); - - it('should send back the interpreted version', function () { - send_ver('004.000', client); - - const expected_str = 'RFB 003.008\n'; - const expected = []; - for (let i = 0; i < expected_str.length; i++) { - expected[i] = expected_str.charCodeAt(i); - } - - expect(client._sock).to.have.sent(new Uint8Array(expected)); - }); - - it('should transition to the Security state on successful negotiation', function () { - send_ver('003.008', client); - expect(client._rfb_init_state).to.equal('Security'); - }); - - describe('Repeater', function () { - beforeEach(function () { - client = make_rfb('wss://host:8675', { repeaterID: "12345" }); - client._rfb_connection_state = 'connecting'; - }); - - it('should interpret version 000.000 as a repeater', function () { - send_ver('000.000', client); - expect(client._rfb_version).to.equal(0); - - const sent_data = client._sock._websocket._get_sent_data(); - expect(new Uint8Array(sent_data.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0])); - expect(sent_data).to.have.length(250); - }); - - it('should handle two step repeater negotiation', function () { - send_ver('000.000', client); - send_ver('003.008', client); - expect(client._rfb_version).to.equal(3.8); - }); - }); - }); - - describe('Security', function () { - beforeEach(function () { - client._rfb_init_state = 'Security'; - }); - - it('should simply receive the auth scheme when for versions < 3.7', function () { - client._rfb_version = 3.6; - const auth_scheme_raw = [1, 2, 3, 4]; - const auth_scheme = (auth_scheme_raw[0] << 24) + (auth_scheme_raw[1] << 16) + - (auth_scheme_raw[2] << 8) + auth_scheme_raw[3]; - client._sock._websocket._receive_data(auth_scheme_raw); - expect(client._rfb_auth_scheme).to.equal(auth_scheme); - }); - - it('should prefer no authentication is possible', function () { - client._rfb_version = 3.7; - const auth_schemes = [2, 1, 3]; - client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_auth_scheme).to.equal(1); - expect(client._sock).to.have.sent(new Uint8Array([1, 1])); - }); - - it('should choose for the most prefered scheme possible for versions >= 3.7', function () { - client._rfb_version = 3.7; - const auth_schemes = [2, 22, 16]; - client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_auth_scheme).to.equal(22); - expect(client._sock).to.have.sent(new Uint8Array([22])); - }); - - it('should fail if there are no supported schemes for versions >= 3.7', function () { - sinon.spy(client, "_fail"); - client._rfb_version = 3.7; - const auth_schemes = [1, 32]; - client._sock._websocket._receive_data(auth_schemes); - expect(client._fail).to.have.been.calledOnce; - }); - - it('should fail with the appropriate message if no types are sent for versions >= 3.7', function () { - client._rfb_version = 3.7; - const failure_data = [0, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; - sinon.spy(client, '_fail'); - client._sock._websocket._receive_data(failure_data); - - expect(client._fail).to.have.been.calledOnce; - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on no security types (reason: whoops)'); - }); - - it('should transition to the Authentication state and continue on successful negotiation', function () { - client._rfb_version = 3.7; - const auth_schemes = [1, 1]; - client._negotiate_authentication = sinon.spy(); - client._sock._websocket._receive_data(auth_schemes); - expect(client._rfb_init_state).to.equal('Authentication'); - expect(client._negotiate_authentication).to.have.been.calledOnce; - }); - }); - - describe('Authentication', function () { - beforeEach(function () { - client._rfb_init_state = 'Security'; - }); - - function send_security(type, cl) { - cl._sock._websocket._receive_data(new Uint8Array([1, type])); + for (let j = 0; j < 8; j++) { + data.push(pairs[i][2].charCodeAt(j)); } + } - it('should fail on auth scheme 0 (pre 3.7) with the given message', function () { - client._rfb_version = 3.6; - const err_msg = "Whoopsies"; - const data = [0, 0, 0, 0]; - const err_len = err_msg.length; - push32(data, err_len); - for (let i = 0; i < err_len; i++) { - data.push(err_msg.charCodeAt(i)); - } + client._sock._websocket._receive_data(new Uint8Array(data)); + } - sinon.spy(client, '_fail'); - client._sock._websocket._receive_data(new Uint8Array(data)); - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on authentication scheme (reason: Whoopsies)'); - }); + it('should skip tunnel negotiation if no tunnels are requested', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_tightvnc).to.be.true; + }); - it('should transition straight to SecurityResult on "no auth" (1) for versions >= 3.8', function () { - client._rfb_version = 3.8; - send_security(1, client); - expect(client._rfb_init_state).to.equal('SecurityResult'); - }); + it('should fail if no supported tunnels are listed', function () { + sinon.spy(client, '_fail'); + send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); + expect(client._fail).to.have.been.calledOnce; + }); - it('should transition straight to ServerInitialisation on "no auth" for versions < 3.8', function () { - client._rfb_version = 3.7; - send_security(1, client); - expect(client._rfb_init_state).to.equal('ServerInitialisation'); - }); + it('should choose the notunnel tunnel type', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); + }); - it('should fail on an unknown auth scheme', function () { - sinon.spy(client, "_fail"); - client._rfb_version = 3.8; - send_security(57, client); - expect(client._fail).to.have.been.calledOnce; - }); + it('should choose the notunnel tunnel type for Siemens devices', function () { + send_num_str_pairs([[1, 'SICR', 'SCHANNEL'], [2, 'SICR', 'SCHANLPW']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); + }); - describe('VNC Authentication (type 2) Handler', function () { - beforeEach(function () { - client._rfb_init_state = 'Security'; - client._rfb_version = 3.8; - }); + it('should continue to sub-auth negotiation after tunnel negotiation', function () { + send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); + client._sock._websocket._get_sent_data(); // skip the tunnel choice here + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_init_state).to.equal('SecurityResult'); + }); - it('should fire the credentialsrequired event if missing a password', function () { - const spy = sinon.spy(); - client.addEventListener("credentialsrequired", spy); - send_security(2, client); - - const challenge = []; - for (let i = 0; i < 16; i++) { challenge[i] = i; } - client._sock._websocket._receive_data(new Uint8Array(challenge)); - - expect(client._rfb_credentials).to.be.empty; - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.types).to.have.members(["password"]); - }); - - it('should encrypt the password with DES and then send it back', function () { - client._rfb_credentials = { password: 'passwd' }; - send_security(2, client); - client._sock._websocket._get_sent_data(); // skip the choice of auth reply - - const challenge = []; - for (let i = 0; i < 16; i++) { challenge[i] = i; } - client._sock._websocket._receive_data(new Uint8Array(challenge)); - - const des_pass = RFB.genDES('passwd', challenge); - expect(client._sock).to.have.sent(new Uint8Array(des_pass)); - }); - - it('should transition to SecurityResult immediately after sending the password', function () { - client._rfb_credentials = { password: 'passwd' }; - send_security(2, client); - - const challenge = []; - for (let i = 0; i < 16; i++) { challenge[i] = i; } - client._sock._websocket._receive_data(new Uint8Array(challenge)); - - expect(client._rfb_init_state).to.equal('SecurityResult'); - }); - }); - - describe('XVP Authentication (type 22) Handler', function () { - beforeEach(function () { - client._rfb_init_state = 'Security'; - client._rfb_version = 3.8; - }); - - it('should fall through to standard VNC authentication upon completion', function () { - client._rfb_credentials = { username: 'user', - target: 'target', - password: 'password' }; - client._negotiate_std_vnc_auth = sinon.spy(); - send_security(22, client); - expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; - }); - - it('should fire the credentialsrequired event if all credentials are missing', function() { - const spy = sinon.spy(); - client.addEventListener("credentialsrequired", spy); - client._rfb_credentials = {}; - send_security(22, client); - - expect(client._rfb_credentials).to.be.empty; - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]); - }); - - it('should fire the credentialsrequired event if some credentials are missing', function() { - const spy = sinon.spy(); - client.addEventListener("credentialsrequired", spy); - client._rfb_credentials = { username: 'user', - target: 'target' }; - send_security(22, client); - - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.types).to.have.members(["username", "password", "target"]); - }); - - it('should send user and target separately', function () { - client._rfb_credentials = { username: 'user', - target: 'target', - password: 'password' }; - client._negotiate_std_vnc_auth = sinon.spy(); - - send_security(22, client); - - const expected = [22, 4, 6]; // auth selection, len user, len target - for (let i = 0; i < 10; i++) { expected[i+3] = 'usertarget'.charCodeAt(i); } - - expect(client._sock).to.have.sent(new Uint8Array(expected)); - }); - }); - - describe('TightVNC Authentication (type 16) Handler', function () { - beforeEach(function () { - client._rfb_init_state = 'Security'; - client._rfb_version = 3.8; - send_security(16, client); - client._sock._websocket._get_sent_data(); // skip the security reply - }); - - function send_num_str_pairs(pairs, client) { - const data = []; - push32(data, pairs.length); - - for (let i = 0; i < pairs.length; i++) { - push32(data, pairs[i][0]); - for (let j = 0; j < 4; j++) { - data.push(pairs[i][1].charCodeAt(j)); - } - for (let j = 0; j < 8; j++) { - data.push(pairs[i][2].charCodeAt(j)); - } - } - - client._sock._websocket._receive_data(new Uint8Array(data)); - } - - it('should skip tunnel negotiation if no tunnels are requested', function () { - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._rfb_tightvnc).to.be.true; - }); - - it('should fail if no supported tunnels are listed', function () { - sinon.spy(client, "_fail"); - send_num_str_pairs([[123, 'OTHR', 'SOMETHNG']], client); - expect(client._fail).to.have.been.calledOnce; - }); - - it('should choose the notunnel tunnel type', function () { - send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL'], [123, 'OTHR', 'SOMETHNG']], client); - expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); - }); - - it('should choose the notunnel tunnel type for Siemens devices', function () { - send_num_str_pairs([[1, 'SICR', 'SCHANNEL'], [2, 'SICR', 'SCHANLPW']], client); - expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 0])); - }); - - it('should continue to sub-auth negotiation after tunnel negotiation', function () { - send_num_str_pairs([[0, 'TGHT', 'NOTUNNEL']], client); - client._sock._websocket._get_sent_data(); // skip the tunnel choice here - send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); - expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_init_state).to.equal('SecurityResult'); - }); - - /*it('should attempt to use VNC auth over no auth when possible', function () { + /* it('should attempt to use VNC auth over no auth when possible', function () { client._rfb_tightvnc = true; client._negotiate_std_vnc_auth = sinon.spy(); send_num_str_pairs([[1, 'STDV', 'NOAUTH__'], [2, 'STDV', 'VNCAUTH_']], client); expect(client._sock).to.have.sent([0, 0, 0, 1]); expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; expect(client._rfb_auth_scheme).to.equal(2); - });*/ // while this would make sense, the original code doesn't actually do this + }); */ // while this would make sense, the original code doesn't actually do this - it('should accept the "no auth" auth type and transition to SecurityResult', function () { - client._rfb_tightvnc = true; - send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); - expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); - expect(client._rfb_init_state).to.equal('SecurityResult'); - }); - - it('should accept VNC authentication and transition to that', function () { - client._rfb_tightvnc = true; - client._negotiate_std_vnc_auth = sinon.spy(); - send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); - expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); - expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; - expect(client._rfb_auth_scheme).to.equal(2); - }); - - it('should fail if there are no supported auth types', function () { - sinon.spy(client, "_fail"); - client._rfb_tightvnc = true; - send_num_str_pairs([[23, 'stdv', 'badval__']], client); - expect(client._fail).to.have.been.calledOnce; - }); - }); + it('should accept the "no auth" auth type and transition to SecurityResult', function () { + client._rfb_tightvnc = true; + send_num_str_pairs([[1, 'STDV', 'NOAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 1])); + expect(client._rfb_init_state).to.equal('SecurityResult'); }); - describe('SecurityResult', function () { - beforeEach(function () { - client._rfb_init_state = 'SecurityResult'; - }); - - it('should fall through to ServerInitialisation on a response code of 0', function () { - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._rfb_init_state).to.equal('ServerInitialisation'); - }); - - it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { - client._rfb_version = 3.8; - sinon.spy(client, '_fail'); - const failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; - client._sock._websocket._receive_data(new Uint8Array(failure_data)); - expect(client._fail).to.have.been.calledWith( - 'Security negotiation failed on security result (reason: whoops)'); - }); - - it('should fail on an error code of 1 with a standard message for version < 3.8', function () { - sinon.spy(client, '_fail'); - client._rfb_version = 3.7; - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); - expect(client._fail).to.have.been.calledWith( - 'Security handshake failed'); - }); - - it('should result in securityfailure event when receiving a non zero status', function () { - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.status).to.equal(2); - }); - - it('should include reason when provided in securityfailure event', function () { - client._rfb_version = 3.8; - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - const failure_data = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104, - 32, 102, 97, 105, 108, 117, 114, 101]; - client._sock._websocket._receive_data(new Uint8Array(failure_data)); - expect(spy.args[0][0].detail.status).to.equal(1); - expect(spy.args[0][0].detail.reason).to.equal('such failure'); - }); - - it('should not include reason when length is zero in securityfailure event', function () { - client._rfb_version = 3.9; - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - const failure_data = [0, 0, 0, 1, 0, 0, 0, 0]; - client._sock._websocket._receive_data(new Uint8Array(failure_data)); - expect(spy.args[0][0].detail.status).to.equal(1); - expect('reason' in spy.args[0][0].detail).to.be.false; - }); - - it('should not include reason in securityfailure event for version < 3.8', function () { - client._rfb_version = 3.6; - const spy = sinon.spy(); - client.addEventListener("securityfailure", spy); - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); - expect(spy.args[0][0].detail.status).to.equal(2); - expect('reason' in spy.args[0][0].detail).to.be.false; - }); + it('should accept VNC authentication and transition to that', function () { + client._rfb_tightvnc = true; + client._negotiate_std_vnc_auth = sinon.spy(); + send_num_str_pairs([[2, 'STDV', 'VNCAUTH__']], client); + expect(client._sock).to.have.sent(new Uint8Array([0, 0, 0, 2])); + expect(client._negotiate_std_vnc_auth).to.have.been.calledOnce; + expect(client._rfb_auth_scheme).to.equal(2); }); - describe('ClientInitialisation', function () { - it('should transition to the ServerInitialisation state', function () { - const client = make_rfb(); - client._rfb_connection_state = 'connecting'; - client._rfb_init_state = 'SecurityResult'; - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._rfb_init_state).to.equal('ServerInitialisation'); - }); - - it('should send 1 if we are in shared mode', function () { - const client = make_rfb('wss://host:8675', { shared: true }); - client._rfb_connection_state = 'connecting'; - client._rfb_init_state = 'SecurityResult'; - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._sock).to.have.sent(new Uint8Array([1])); - }); - - it('should send 0 if we are not in shared mode', function () { - const client = make_rfb('wss://host:8675', { shared: false }); - client._rfb_connection_state = 'connecting'; - client._rfb_init_state = 'SecurityResult'; - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); - expect(client._sock).to.have.sent(new Uint8Array([0])); - }); - }); - - describe('ServerInitialisation', function () { - beforeEach(function () { - client._rfb_init_state = 'ServerInitialisation'; - }); - - function send_server_init(opts, client) { - const full_opts = { width: 10, height: 12, bpp: 24, depth: 24, big_endian: 0, - true_color: 1, red_max: 255, green_max: 255, blue_max: 255, - red_shift: 16, green_shift: 8, blue_shift: 0, name: 'a name' }; - for (let opt in opts) { - full_opts[opt] = opts[opt]; - } - const data = []; - - push16(data, full_opts.width); - push16(data, full_opts.height); - - data.push(full_opts.bpp); - data.push(full_opts.depth); - data.push(full_opts.big_endian); - data.push(full_opts.true_color); - - push16(data, full_opts.red_max); - push16(data, full_opts.green_max); - push16(data, full_opts.blue_max); - push8(data, full_opts.red_shift); - push8(data, full_opts.green_shift); - push8(data, full_opts.blue_shift); - - // padding - push8(data, 0); - push8(data, 0); - push8(data, 0); - - client._sock._websocket._receive_data(new Uint8Array(data)); - - const name_data = []; - push32(name_data, full_opts.name.length); - for (let i = 0; i < full_opts.name.length; i++) { - name_data.push(full_opts.name.charCodeAt(i)); - } - client._sock._websocket._receive_data(new Uint8Array(name_data)); - } - - it('should set the framebuffer width and height', function () { - send_server_init({ width: 32, height: 84 }, client); - expect(client._fb_width).to.equal(32); - expect(client._fb_height).to.equal(84); - }); - - // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them - - it('should set the framebuffer name and call the callback', function () { - const spy = sinon.spy(); - client.addEventListener("desktopname", spy); - send_server_init({ name: 'some name' }, client); - - expect(client._fb_name).to.equal('some name'); - expect(spy).to.have.been.calledOnce; - expect(spy.args[0][0].detail.name).to.equal('some name'); - }); - - it('should handle the extended init message of the tight encoding', function () { - // NB(sross): we don't actually do anything with it, so just test that we can - // read it w/o throwing an error - client._rfb_tightvnc = true; - send_server_init({}, client); - - const tight_data = []; - push16(tight_data, 1); - push16(tight_data, 2); - push16(tight_data, 3); - push16(tight_data, 0); - for (let i = 0; i < 16 + 32 + 48; i++) { - tight_data.push(i); - } - client._sock._websocket._receive_data(tight_data); - - expect(client._rfb_connection_state).to.equal('connected'); - }); - - it('should resize the display', function () { - sinon.spy(client._display, 'resize'); - send_server_init({ width: 27, height: 32 }, client); - - expect(client._display.resize).to.have.been.calledOnce; - expect(client._display.resize).to.have.been.calledWith(27, 32); - }); - - it('should grab the mouse and keyboard', function () { - sinon.spy(client._keyboard, 'grab'); - sinon.spy(client._mouse, 'grab'); - send_server_init({}, client); - expect(client._keyboard.grab).to.have.been.calledOnce; - expect(client._mouse.grab).to.have.been.calledOnce; - }); - - describe('Initial Update Request', function () { - beforeEach(function () { - sinon.spy(RFB.messages, "pixelFormat"); - sinon.spy(RFB.messages, "clientEncodings"); - sinon.spy(RFB.messages, "fbUpdateRequest"); - }); - - afterEach(function () { - RFB.messages.pixelFormat.restore(); - RFB.messages.clientEncodings.restore(); - RFB.messages.fbUpdateRequest.restore(); - }); - - // TODO(directxman12): test the various options in this configuration matrix - it('should reply with the pixel format, client encodings, and initial update request', function () { - send_server_init({ width: 27, height: 32 }, client); - - expect(RFB.messages.pixelFormat).to.have.been.calledOnce; - expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true); - expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); - expect(RFB.messages.clientEncodings).to.have.been.calledOnce; - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight); - expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); - expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; - expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); - }); - - it('should reply with restricted settings for Intel AMT servers', function () { - send_server_init({ width: 27, height: 32, name: "Intel(r) AMT KVM"}, client); - - expect(RFB.messages.pixelFormat).to.have.been.calledOnce; - expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true); - expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); - expect(RFB.messages.clientEncodings).to.have.been.calledOnce; - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight); - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile); - expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); - expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; - expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); - }); - }); - - it('should transition to the "connected" state', function () { - send_server_init({}, client); - expect(client._rfb_connection_state).to.equal('connected'); - }); + it('should fail if there are no supported auth types', function () { + sinon.spy(client, '_fail'); + client._rfb_tightvnc = true; + send_num_str_pairs([[23, 'stdv', 'badval__']], client); + expect(client._fail).to.have.been.calledOnce; }); + }); }); - describe('Protocol Message Processing After Completing Initialization', function () { - let client; + describe('SecurityResult', function () { + beforeEach(function () { + client._rfb_init_state = 'SecurityResult'; + }); + it('should fall through to ServerInitialisation on a response code of 0', function () { + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should fail on an error code of 1 with the given message for versions >= 3.8', function () { + client._rfb_version = 3.8; + sinon.spy(client, '_fail'); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 6, 119, 104, 111, 111, 112, 115]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(client._fail).to.have.been.calledWith( + 'Security negotiation failed on security result (reason: whoops)' + ); + }); + + it('should fail on an error code of 1 with a standard message for version < 3.8', function () { + sinon.spy(client, '_fail'); + client._rfb_version = 3.7; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 1])); + expect(client._fail).to.have.been.calledWith( + 'Security handshake failed' + ); + }); + + it('should result in securityfailure event when receiving a non zero status', function () { + const spy = sinon.spy(); + client.addEventListener('securityfailure', spy); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.status).to.equal(2); + }); + + it('should include reason when provided in securityfailure event', function () { + client._rfb_version = 3.8; + const spy = sinon.spy(); + client.addEventListener('securityfailure', spy); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 12, 115, 117, 99, 104, + 32, 102, 97, 105, 108, 117, 114, 101]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(spy.args[0][0].detail.status).to.equal(1); + expect(spy.args[0][0].detail.reason).to.equal('such failure'); + }); + + it('should not include reason when length is zero in securityfailure event', function () { + client._rfb_version = 3.9; + const spy = sinon.spy(); + client.addEventListener('securityfailure', spy); + const failure_data = [0, 0, 0, 1, 0, 0, 0, 0]; + client._sock._websocket._receive_data(new Uint8Array(failure_data)); + expect(spy.args[0][0].detail.status).to.equal(1); + expect('reason' in spy.args[0][0].detail).to.be.false; + }); + + it('should not include reason in securityfailure event for version < 3.8', function () { + client._rfb_version = 3.6; + const spy = sinon.spy(); + client.addEventListener('securityfailure', spy); + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 2])); + expect(spy.args[0][0].detail.status).to.equal(2); + expect('reason' in spy.args[0][0].detail).to.be.false; + }); + }); + + describe('ClientInitialisation', function () { + it('should transition to the ServerInitialisation state', function () { + const client = make_rfb(); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._rfb_init_state).to.equal('ServerInitialisation'); + }); + + it('should send 1 if we are in shared mode', function () { + const client = make_rfb('wss://host:8675', { shared: true }); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([1])); + }); + + it('should send 0 if we are not in shared mode', function () { + const client = make_rfb('wss://host:8675', { shared: false }); + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'SecurityResult'; + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 0])); + expect(client._sock).to.have.sent(new Uint8Array([0])); + }); + }); + + describe('ServerInitialisation', function () { + beforeEach(function () { + client._rfb_init_state = 'ServerInitialisation'; + }); + + function send_server_init(opts, client) { + const full_opts = { + width: 10, + height: 12, + bpp: 24, + depth: 24, + big_endian: 0, + true_color: 1, + red_max: 255, + green_max: 255, + blue_max: 255, + red_shift: 16, + green_shift: 8, + blue_shift: 0, + name: 'a name' + }; + for (let opt in opts) { + full_opts[opt] = opts[opt]; + } + const data = []; + + push16(data, full_opts.width); + push16(data, full_opts.height); + + data.push(full_opts.bpp); + data.push(full_opts.depth); + data.push(full_opts.big_endian); + data.push(full_opts.true_color); + + push16(data, full_opts.red_max); + push16(data, full_opts.green_max); + push16(data, full_opts.blue_max); + push8(data, full_opts.red_shift); + push8(data, full_opts.green_shift); + push8(data, full_opts.blue_shift); + + // padding + push8(data, 0); + push8(data, 0); + push8(data, 0); + + client._sock._websocket._receive_data(new Uint8Array(data)); + + const name_data = []; + push32(name_data, full_opts.name.length); + for (let i = 0; i < full_opts.name.length; i++) { + name_data.push(full_opts.name.charCodeAt(i)); + } + client._sock._websocket._receive_data(new Uint8Array(name_data)); + } + + it('should set the framebuffer width and height', function () { + send_server_init({ width: 32, height: 84 }, client); + expect(client._fb_width).to.equal(32); + expect(client._fb_height).to.equal(84); + }); + + // NB(sross): we just warn, not fail, for endian-ness and shifts, so we don't test them + + it('should set the framebuffer name and call the callback', function () { + const spy = sinon.spy(); + client.addEventListener('desktopname', spy); + send_server_init({ name: 'some name' }, client); + + expect(client._fb_name).to.equal('some name'); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.name).to.equal('some name'); + }); + + it('should handle the extended init message of the tight encoding', function () { + // NB(sross): we don't actually do anything with it, so just test that we can + // read it w/o throwing an error + client._rfb_tightvnc = true; + send_server_init({}, client); + + const tight_data = []; + push16(tight_data, 1); + push16(tight_data, 2); + push16(tight_data, 3); + push16(tight_data, 0); + for (let i = 0; i < 16 + 32 + 48; i++) { + tight_data.push(i); + } + client._sock._websocket._receive_data(tight_data); + + expect(client._rfb_connection_state).to.equal('connected'); + }); + + it('should resize the display', function () { + sinon.spy(client._display, 'resize'); + send_server_init({ width: 27, height: 32 }, client); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(27, 32); + }); + + it('should grab the mouse and keyboard', function () { + sinon.spy(client._keyboard, 'grab'); + sinon.spy(client._mouse, 'grab'); + send_server_init({}, client); + expect(client._keyboard.grab).to.have.been.calledOnce; + expect(client._mouse.grab).to.have.been.calledOnce; + }); + + describe('Initial Update Request', function () { beforeEach(function () { - client = make_rfb(); - client._fb_name = 'some device'; - client._fb_width = 640; - client._fb_height = 20; + sinon.spy(RFB.messages, 'pixelFormat'); + sinon.spy(RFB.messages, 'clientEncodings'); + sinon.spy(RFB.messages, 'fbUpdateRequest'); }); - describe('Framebuffer Update Handling', function () { - const target_data_arr = [ - 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, - 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 - ]; - let target_data; + afterEach(function () { + RFB.messages.pixelFormat.restore(); + RFB.messages.clientEncodings.restore(); + RFB.messages.fbUpdateRequest.restore(); + }); - const target_data_check_arr = [ - 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, - 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, - 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, - 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 - ]; - let target_data_check; + // TODO(directxman12): test the various options in this configuration matrix + it('should reply with the pixel format, client encodings, and initial update request', function () { + send_server_init({ width: 27, height: 32 }, client); - before(function () { - // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray - target_data = new Uint8Array(target_data_arr); - target_data_check = new Uint8Array(target_data_check_arr); - }); + expect(RFB.messages.pixelFormat).to.have.been.calledOnce; + expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true); + expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); + expect(RFB.messages.clientEncodings).to.have.been.calledOnce; + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; + expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + }); - function send_fbu_msg (rect_info, rect_data, client, rect_cnt) { - let data = []; + it('should reply with restricted settings for Intel AMT servers', function () { + send_server_init({ width: 27, height: 32, name: 'Intel(r) AMT KVM' }, client); - if (!rect_cnt || rect_cnt > -1) { - // header - data.push(0); // msg type - data.push(0); // padding - push16(data, rect_cnt || rect_data.length); - } + expect(RFB.messages.pixelFormat).to.have.been.calledOnce; + expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true); + expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); + expect(RFB.messages.clientEncodings).to.have.been.calledOnce; + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile); + expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; + expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + }); + }); - for (let i = 0; i < rect_data.length; i++) { - if (rect_info[i]) { - push16(data, rect_info[i].x); - push16(data, rect_info[i].y); - push16(data, rect_info[i].width); - push16(data, rect_info[i].height); - push32(data, rect_info[i].encoding); - } - data = data.concat(rect_data[i]); - } + it('should transition to the "connected" state', function () { + send_server_init({}, client); + expect(client._rfb_connection_state).to.equal('connected'); + }); + }); + }); - client._sock._websocket._receive_data(new Uint8Array(data)); + describe('Protocol Message Processing After Completing Initialization', function () { + let client; + + beforeEach(function () { + client = make_rfb(); + client._fb_name = 'some device'; + client._fb_width = 640; + client._fb_height = 20; + }); + + describe('Framebuffer Update Handling', function () { + const target_data_arr = [ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0xff, 0x00, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255, + 0xee, 0x00, 0xff, 255, 0x00, 0xee, 0xff, 255, 0xaa, 0xee, 0xff, 255, 0xab, 0xee, 0xff, 255 + ]; + let target_data; + + const target_data_check_arr = [ + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255, + 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 + ]; + let target_data_check; + + before(function () { + // NB(directxman12): PhantomJS 1.x doesn't implement Uint8ClampedArray + target_data = new Uint8Array(target_data_arr); + target_data_check = new Uint8Array(target_data_check_arr); + }); + + function send_fbu_msg(rect_info, rect_data, client, rect_cnt) { + let data = []; + + if (!rect_cnt || rect_cnt > -1) { + // header + data.push(0); // msg type + data.push(0); // padding + push16(data, rect_cnt || rect_data.length); + } + + for (let i = 0; i < rect_data.length; i++) { + if (rect_info[i]) { + push16(data, rect_info[i].x); + push16(data, rect_info[i].y); + push16(data, rect_info[i].width); + push16(data, rect_info[i].height); + push32(data, rect_info[i].encoding); + } + data = data.concat(rect_data[i]); + } + + client._sock._websocket._receive_data(new Uint8Array(data)); + } + + it('should send an update request if there is sufficient data', function () { + const expected_msg = { _sQ: new Uint8Array(10), _sQlen: 0, flush: () => {} }; + RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); + + client._framebufferUpdate = () => true; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send an update request if we need more data', function () { + client._sock._websocket._receive_data(new Uint8Array([0])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should resume receiving an update if we previously did not have enough data', function () { + const expected_msg = { _sQ: new Uint8Array(10), _sQlen: 0, flush: () => {} }; + RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); + + // just enough to set FBU.rects + client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._framebufferUpdate = function () { this._sock.rQskip8(); return true; }; // we magically have enough data + // 247 should *not* be used as the message type here + client._sock._websocket._receive_data(new Uint8Array([247])); + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should not send a request in continuous updates mode', function () { + client._enabledContinuousUpdates = true; + client._framebufferUpdate = () => true; + client._sock._websocket._receive_data(new Uint8Array([0])); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + }); + + it('should fail on an unsupported encoding', function () { + sinon.spy(client, '_fail'); + const rect_info = { + x: 8, y: 11, width: 27, height: 32, encoding: 234 + }; + send_fbu_msg([rect_info], [[]], client); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should be able to pause and resume receiving rects if not enought data', function () { + // seed some initial data to copy + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + + const info = [{ + x: 0, y: 2, width: 2, height: 2, encoding: 0x01 + }, + { + x: 2, y: 2, width: 2, height: 2, encoding: 0x01 + }]; + // data says [{ old_x: 2, old_y: 0 }, { old_x: 0, old_y: 0 }] + const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg([info[0]], [rects[0]], client, 2); + send_fbu_msg([info[1]], [rects[1]], client, -1); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('Message Encoding Handlers', function () { + beforeEach(function () { + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._fb_depth = 24; + client._display.resize(4, 4); + }); + + it('should handle the RAW encoding', function () { + const info = [{ + x: 0, y: 0, width: 2, height: 2, encoding: 0x00 + }, + { + x: 2, y: 0, width: 2, height: 2, encoding: 0x00 + }, + { + x: 0, y: 2, width: 4, height: 1, encoding: 0x00 + }, + { + x: 0, y: 3, width: 4, height: 1, encoding: 0x00 + }]; + // data is in bgrx + const rects = [ + [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], + [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], + [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle the RAW encoding in low colour mode', function () { + const info = [{ + x: 0, y: 0, width: 2, height: 2, encoding: 0x00 + }, + { + x: 2, y: 0, width: 2, height: 2, encoding: 0x00 + }, + { + x: 0, y: 2, width: 4, height: 1, encoding: 0x00 + }, + { + x: 0, y: 3, width: 4, height: 1, encoding: 0x00 + }]; + const rects = [ + [0x03, 0x03, 0x03, 0x03], + [0x0c, 0x0c, 0x0c, 0x0c], + [0x0c, 0x0c, 0x03, 0x03], + [0x0c, 0x0c, 0x03, 0x03]]; + client._fb_depth = 8; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle the COPYRECT encoding', function () { + // seed some initial data to copy + client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + + const info = [{ + x: 0, y: 2, width: 2, height: 2, encoding: 0x01 + }, + { + x: 2, y: 2, width: 2, height: 2, encoding: 0x01 + }]; + // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] + const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; + send_fbu_msg(info, rects, client); + expect(client._display).to.have.displayed(target_data_check); + }); + + // TODO(directxman12): for encodings with subrects, test resuming on partial send? + // TODO(directxman12): test rre_chunk_sz (related to above about subrects)? + + it('should handle the RRE encoding', function () { + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x02 + }]; + const rect = []; + push32(rect, 2); // 2 subrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + push16(rect, 0); // x: 0 + push16(rect, 0); // y: 0 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + push16(rect, 2); // x: 2 + push16(rect, 2); // y: 2 + push16(rect, 2); // width: 2 + push16(rect, 2); // height: 2 + + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + describe('the HEXTILE encoding handler', function () { + it('should handle a tile with fg, bg specified, normal subrects', function () { + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x05 + }]; + const rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2); // 2 subrects + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should handle a raw tile', function () { + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x05 + }]; + const rect = []; + rect.push(0x01); // raw + for (let i = 0; i < target_data.length; i += 4) { + rect.push(target_data[i + 2]); + rect.push(target_data[i + 1]); + rect.push(target_data[i]); + rect.push(target_data[i + 3]); } - - it('should send an update request if there is sufficient data', function () { - const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); - - client._framebufferUpdate = () => true; - client._sock._websocket._receive_data(new Uint8Array([0])); - - expect(client._sock).to.have.sent(expected_msg._sQ); - }); - - it('should not send an update request if we need more data', function () { - client._sock._websocket._receive_data(new Uint8Array([0])); - expect(client._sock._websocket._get_sent_data()).to.have.length(0); - }); - - it('should resume receiving an update if we previously did not have enough data', function () { - const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.fbUpdateRequest(expected_msg, true, 0, 0, 640, 20); - - // just enough to set FBU.rects - client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); - expect(client._sock._websocket._get_sent_data()).to.have.length(0); - - client._framebufferUpdate = function () { this._sock.rQskip8(); return true; }; // we magically have enough data - // 247 should *not* be used as the message type here - client._sock._websocket._receive_data(new Uint8Array([247])); - expect(client._sock).to.have.sent(expected_msg._sQ); - }); - - it('should not send a request in continuous updates mode', function () { - client._enabledContinuousUpdates = true; - client._framebufferUpdate = () => true; - client._sock._websocket._receive_data(new Uint8Array([0])); - - expect(client._sock._websocket._get_sent_data()).to.have.length(0); - }); - - it('should fail on an unsupported encoding', function () { - sinon.spy(client, "_fail"); - const rect_info = { x: 8, y: 11, width: 27, height: 32, encoding: 234 }; - send_fbu_msg([rect_info], [[]], client); - expect(client._fail).to.have.been.calledOnce; - }); - - it('should be able to pause and resume receiving rects if not enought data', function () { - // seed some initial data to copy - client._fb_width = 4; - client._fb_height = 4; - client._display.resize(4, 4); - client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); - - const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, - { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; - // data says [{ old_x: 2, old_y: 0 }, { old_x: 0, old_y: 0 }] - const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; - send_fbu_msg([info[0]], [rects[0]], client, 2); - send_fbu_msg([info[1]], [rects[1]], client, -1); - expect(client._display).to.have.displayed(target_data_check); - }); - - describe('Message Encoding Handlers', function () { - beforeEach(function () { - // a really small frame - client._fb_width = 4; - client._fb_height = 4; - client._fb_depth = 24; - client._display.resize(4, 4); - }); - - it('should handle the RAW encoding', function () { - const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, - { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; - // data is in bgrx - const rects = [ - [0x00, 0x00, 0xff, 0, 0x00, 0xff, 0x00, 0, 0x00, 0xff, 0x00, 0, 0x00, 0x00, 0xff, 0], - [0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0, 0xff, 0x00, 0x00, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0], - [0xff, 0x00, 0xee, 0, 0xff, 0xee, 0x00, 0, 0xff, 0xee, 0xaa, 0, 0xff, 0xee, 0xab, 0]]; - send_fbu_msg(info, rects, client); - expect(client._display).to.have.displayed(target_data); - }); - - it('should handle the RAW encoding in low colour mode', function () { - const info = [{ x: 0, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 2, y: 0, width: 2, height: 2, encoding: 0x00 }, - { x: 0, y: 2, width: 4, height: 1, encoding: 0x00 }, - { x: 0, y: 3, width: 4, height: 1, encoding: 0x00 }]; - const rects = [ - [0x03, 0x03, 0x03, 0x03], - [0x0c, 0x0c, 0x0c, 0x0c], - [0x0c, 0x0c, 0x03, 0x03], - [0x0c, 0x0c, 0x03, 0x03]]; - client._fb_depth = 8; - send_fbu_msg(info, rects, client); - expect(client._display).to.have.displayed(target_data_check); - }); - - it('should handle the COPYRECT encoding', function () { - // seed some initial data to copy - client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); - - const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01}, - { x: 2, y: 2, width: 2, height: 2, encoding: 0x01}]; - // data says [{ old_x: 0, old_y: 0 }, { old_x: 0, old_y: 0 }] - const rects = [[0, 2, 0, 0], [0, 0, 0, 0]]; - send_fbu_msg(info, rects, client); - expect(client._display).to.have.displayed(target_data_check); - }); - - // TODO(directxman12): for encodings with subrects, test resuming on partial send? - // TODO(directxman12): test rre_chunk_sz (related to above about subrects)? - - it('should handle the RRE encoding', function () { - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x02 }]; - const rect = []; - push32(rect, 2); // 2 subrects - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - push16(rect, 0); // x: 0 - push16(rect, 0); // y: 0 - push16(rect, 2); // width: 2 - push16(rect, 2); // height: 2 - rect.push(0xff); // becomes ff0000ff --> #0000FF color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - push16(rect, 2); // x: 2 - push16(rect, 2); // y: 2 - push16(rect, 2); // width: 2 - push16(rect, 2); // height: 2 - - send_fbu_msg(info, [rect], client); - expect(client._display).to.have.displayed(target_data_check); - }); - - describe('the HEXTILE encoding handler', function () { - it('should handle a tile with fg, bg specified, normal subrects', function () { - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; - const rect = []; - rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - rect.push(2); // 2 subrects - rect.push(0); // x: 0, y: 0 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - rect.push(2 | (2 << 4)); // x: 2, y: 2 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - send_fbu_msg(info, [rect], client); - expect(client._display).to.have.displayed(target_data_check); - }); - - it('should handle a raw tile', function () { - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; - const rect = []; - rect.push(0x01); // raw - for (let i = 0; i < target_data.length; i += 4) { - rect.push(target_data[i + 2]); - rect.push(target_data[i + 1]); - rect.push(target_data[i]); - rect.push(target_data[i + 3]); - } - send_fbu_msg(info, [rect], client); - expect(client._display).to.have.displayed(target_data); - }); - - it('should handle a tile with only bg specified (solid bg)', function () { - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; - const rect = []; - rect.push(0x02); - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - send_fbu_msg(info, [rect], client); - - const expected = []; - for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } - expect(client._display).to.have.displayed(new Uint8Array(expected)); - }); - - it('should handle a tile with only bg specified and an empty frame afterwards', function () { - // set the width so we can have two tiles - client._fb_width = 8; - client._display.resize(8, 4); - - const info = [{ x: 0, y: 0, width: 32, height: 4, encoding: 0x05 }]; - - const rect = []; - - // send a bg frame - rect.push(0x02); - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - - // send an empty frame - rect.push(0x00); - - send_fbu_msg(info, [rect], client); - - const expected = []; - for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid - for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color - expect(client._display).to.have.displayed(new Uint8Array(expected)); - }); - - it('should handle a tile with bg and coloured subrects', function () { - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; - const rect = []; - rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(2); // 2 subrects - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - rect.push(0); // x: 0, y: 0 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - rect.push(2 | (2 << 4)); // x: 2, y: 2 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - send_fbu_msg(info, [rect], client); - expect(client._display).to.have.displayed(target_data_check); - }); - - it('should carry over fg and bg colors from the previous tile if not specified', function () { - client._fb_width = 4; - client._fb_height = 17; - client._display.resize(4, 17); - - const info = [{ x: 0, y: 0, width: 4, height: 17, encoding: 0x05}]; - const rect = []; - rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects - push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color - rect.push(0xff); // becomes ff0000ff --> #0000FF fg color - rect.push(0x00); - rect.push(0x00); - rect.push(0xff); - rect.push(8); // 8 subrects - for (let i = 0; i < 4; i++) { - rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - } - rect.push(0x08); // anysubrects - rect.push(1); // 1 subrect - rect.push(0); // x: 0, y: 0 - rect.push(1 | (1 << 4)); // width: 2, height: 2 - send_fbu_msg(info, [rect], client); - - let expected = []; - for (let i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); } - expected = expected.concat(target_data_check_arr.slice(0, 16)); - expect(client._display).to.have.displayed(new Uint8Array(expected)); - }); - - it('should fail on an invalid subencoding', function () { - sinon.spy(client,"_fail"); - const info = [{ x: 0, y: 0, width: 4, height: 4, encoding: 0x05 }]; - const rects = [[45]]; // an invalid subencoding - send_fbu_msg(info, rects, client); - expect(client._fail).to.have.been.calledOnce; - }); - }); - - it.skip('should handle the TIGHT encoding', function () { - // TODO(directxman12): test this - }); - - it.skip('should handle the TIGHT_PNG encoding', function () { - // TODO(directxman12): test this - }); - - it('should handle the DesktopSize pseduo-encoding', function () { - sinon.spy(client._display, 'resize'); - send_fbu_msg([{ x: 0, y: 0, width: 20, height: 50, encoding: -223 }], [[]], client); - - expect(client._fb_width).to.equal(20); - expect(client._fb_height).to.equal(50); - - expect(client._display.resize).to.have.been.calledOnce; - expect(client._display.resize).to.have.been.calledWith(20, 50); - }); - - describe('the ExtendedDesktopSize pseudo-encoding handler', function () { - beforeEach(function () { - // a really small frame - client._fb_width = 4; - client._fb_height = 4; - client._display.resize(4, 4); - sinon.spy(client._display, 'resize'); - }); - - function make_screen_data (nr_of_screens) { - const data = []; - push8(data, nr_of_screens); // number-of-screens - push8(data, 0); // padding - push16(data, 0); // padding - for (let i=0; i {}}; - const incoming_msg = {_sQ: new Uint8Array(16), _sQlen: 0, flush: () => {}}; - - const payload = "foo\x00ab9"; - - // ClientFence and ServerFence are identical in structure - RFB.messages.clientFence(expected_msg, (1<<0) | (1<<1), payload); - RFB.messages.clientFence(incoming_msg, 0xffffffff, payload); - - client._sock._websocket._receive_data(incoming_msg._sQ); - - expect(client._sock).to.have.sent(expected_msg._sQ); - - expected_msg._sQlen = 0; - incoming_msg._sQlen = 0; - - RFB.messages.clientFence(expected_msg, (1<<0), payload); - RFB.messages.clientFence(incoming_msg, (1<<0) | (1<<31), payload); - - client._sock._websocket._receive_data(incoming_msg._sQ); - - expect(client._sock).to.have.sent(expected_msg._sQ); - }); - - it('should enable continuous updates on first EndOfContinousUpdates', function () { - const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - - RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 640, 20); - - expect(client._enabledContinuousUpdates).to.be.false; - - client._sock._websocket._receive_data(new Uint8Array([150])); - - expect(client._enabledContinuousUpdates).to.be.true; - expect(client._sock).to.have.sent(expected_msg._sQ); - }); - - it('should disable continuous updates on subsequent EndOfContinousUpdates', function () { - client._enabledContinuousUpdates = true; - client._supportsContinuousUpdates = true; - - client._sock._websocket._receive_data(new Uint8Array([150])); - - expect(client._enabledContinuousUpdates).to.be.false; - }); - - it('should update continuous updates on resize', function () { - const expected_msg = {_sQ: new Uint8Array(10), _sQlen: 0, flush: () => {}}; - RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 90, 700); - - client._resize(450, 160); - - expect(client._sock._websocket._get_sent_data()).to.have.length(0); - - client._enabledContinuousUpdates = true; - - client._resize(90, 700); - - expect(client._sock).to.have.sent(expected_msg._sQ); - }); - - it('should fail on an unknown message type', function () { - sinon.spy(client, "_fail"); - client._sock._websocket._receive_data(new Uint8Array([87])); + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data); + }); + + it('should handle a tile with only bg specified (solid bg)', function () { + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x05 + }]; + const rect = []; + rect.push(0x02); + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + send_fbu_msg(info, [rect], client); + + const expected = []; + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with only bg specified and an empty frame afterwards', function () { + // set the width so we can have two tiles + client._fb_width = 8; + client._display.resize(8, 4); + + const info = [{ + x: 0, y: 0, width: 32, height: 4, encoding: 0x05 + }]; + + const rect = []; + + // send a bg frame + rect.push(0x02); + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + + // send an empty frame + rect.push(0x00); + + send_fbu_msg(info, [rect], client); + + const expected = []; + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should handle a tile with bg and coloured subrects', function () { + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x05 + }]; + const rect = []; + rect.push(0x02 | 0x08 | 0x10); // bg spec, anysubrects, colouredsubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(2); // 2 subrects + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(2 | (2 << 4)); // x: 2, y: 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + expect(client._display).to.have.displayed(target_data_check); + }); + + it('should carry over fg and bg colors from the previous tile if not specified', function () { + client._fb_width = 4; + client._fb_height = 17; + client._display.resize(4, 17); + + const info = [{ + x: 0, y: 0, width: 4, height: 17, encoding: 0x05 + }]; + const rect = []; + rect.push(0x02 | 0x04 | 0x08); // bg spec, fg spec, anysubrects + push32(rect, 0xff00ff); // becomes 00ff00ff --> #00FF00 bg color + rect.push(0xff); // becomes ff0000ff --> #0000FF fg color + rect.push(0x00); + rect.push(0x00); + rect.push(0xff); + rect.push(8); // 8 subrects + for (let i = 0; i < 4; i++) { + rect.push((0 << 4) | (i * 4)); // x: 0, y: i*4 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + rect.push((2 << 4) | (i * 4 + 2)); // x: 2, y: i * 4 + 2 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + } + rect.push(0x08); // anysubrects + rect.push(1); // 1 subrect + rect.push(0); // x: 0, y: 0 + rect.push(1 | (1 << 4)); // width: 2, height: 2 + send_fbu_msg(info, [rect], client); + + let expected = []; + for (let i = 0; i < 4; i++) { expected = expected.concat(target_data_check_arr); } + expected = expected.concat(target_data_check_arr.slice(0, 16)); + expect(client._display).to.have.displayed(new Uint8Array(expected)); + }); + + it('should fail on an invalid subencoding', function () { + sinon.spy(client, '_fail'); + const info = [{ + x: 0, y: 0, width: 4, height: 4, encoding: 0x05 + }]; + const rects = [[45]]; // an invalid subencoding + send_fbu_msg(info, rects, client); expect(client._fail).to.have.been.calledOnce; + }); }); + + it.skip('should handle the TIGHT encoding', function () { + // TODO(directxman12): test this + }); + + it.skip('should handle the TIGHT_PNG encoding', function () { + // TODO(directxman12): test this + }); + + it('should handle the DesktopSize pseduo-encoding', function () { + sinon.spy(client._display, 'resize'); + send_fbu_msg([{ + x: 0, y: 0, width: 20, height: 50, encoding: -223 + }], [[]], client); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + describe('the ExtendedDesktopSize pseudo-encoding handler', function () { + beforeEach(function () { + // a really small frame + client._fb_width = 4; + client._fb_height = 4; + client._display.resize(4, 4); + sinon.spy(client._display, 'resize'); + }); + + function make_screen_data(nr_of_screens) { + const data = []; + push8(data, nr_of_screens); // number-of-screens + push8(data, 0); // padding + push16(data, 0); // padding + for (let i = 0; i < nr_of_screens; i += 1) { + push32(data, 0); // id + push16(data, 0); // x-position + push16(data, 0); // y-position + push16(data, 20); // width + push16(data, 50); // height + push32(data, 0); // flags + } + return data; + } + + it('should handle a resize requested by this client', function () { + const reason_for_change = 1; // requested by this client + const status_code = 0; // No error + + send_fbu_msg([{ + x: reason_for_change, + y: status_code, + width: 20, + height: 50, + encoding: -308 + }], + make_screen_data(1), client); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + it('should handle a resize requested by another client', function () { + const reason_for_change = 2; // requested by another client + const status_code = 0; // No error + + send_fbu_msg([{ + x: reason_for_change, + y: status_code, + width: 20, + height: 50, + encoding: -308 + }], + make_screen_data(1), client); + + expect(client._fb_width).to.equal(20); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(20, 50); + }); + + it('should be able to recieve requests which contain data for multiple screens', function () { + const reason_for_change = 2; // requested by another client + const status_code = 0; // No error + + send_fbu_msg([{ + x: reason_for_change, + y: status_code, + width: 60, + height: 50, + encoding: -308 + }], + make_screen_data(3), client); + + expect(client._fb_width).to.equal(60); + expect(client._fb_height).to.equal(50); + + expect(client._display.resize).to.have.been.calledOnce; + expect(client._display.resize).to.have.been.calledWith(60, 50); + }); + + it('should not handle a failed request', function () { + const reason_for_change = 1; // requested by this client + const status_code = 1; // Resize is administratively prohibited + + send_fbu_msg([{ + x: reason_for_change, + y: status_code, + width: 20, + height: 50, + encoding: -308 + }], + make_screen_data(1), client); + + expect(client._fb_width).to.equal(4); + expect(client._fb_height).to.equal(4); + + expect(client._display.resize).to.not.have.been.called; + }); + }); + + it.skip('should handle the Cursor pseudo-encoding', function () { + // TODO(directxman12): test + }); + + it('should handle the last_rect pseudo-encoding', function () { + send_fbu_msg([{ + x: 0, y: 0, width: 0, height: 0, encoding: -224 + }], [[]], client, 100); + expect(client._FBU.rects).to.equal(0); + }); + }); }); - describe('Asynchronous Events', function () { - let client; - beforeEach(function () { - client = make_rfb(); - }); + describe('XVP Message Handling', function () { + it('should set the XVP version and fire the callback with the version on XVP_INIT', function () { + const spy = sinon.spy(); + client.addEventListener('capabilities', spy); + client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 1])); + expect(client._rfb_xvp_ver).to.equal(10); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.capabilities.power).to.be.true; + expect(client.capabilities.power).to.be.true; + }); - describe('Mouse event handlers', function () { - it('should not send button messages in view-only mode', function () { - client._viewOnly = true; - sinon.spy(client._sock, 'flush'); - client._handleMouseButton(0, 0, 1, 0x001); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should not send movement messages in view-only mode', function () { - client._viewOnly = true; - sinon.spy(client._sock, 'flush'); - client._handleMouseMove(0, 0); - expect(client._sock.flush).to.not.have.been.called; - }); - - it('should send a pointer event on mouse button presses', function () { - client._handleMouseButton(10, 12, 1, 0x001); - const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; - RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); - expect(client._sock).to.have.sent(pointer_msg._sQ); - }); - - it('should send a mask of 1 on mousedown', function () { - client._handleMouseButton(10, 12, 1, 0x001); - const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; - RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); - expect(client._sock).to.have.sent(pointer_msg._sQ); - }); - - it('should send a mask of 0 on mouseup', function () { - client._mouse_buttonMask = 0x001; - client._handleMouseButton(10, 12, 0, 0x001); - const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; - RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); - expect(client._sock).to.have.sent(pointer_msg._sQ); - }); - - it('should send a pointer event on mouse movement', function () { - client._handleMouseMove(10, 12); - const pointer_msg = {_sQ: new Uint8Array(6), _sQlen: 0, flush: () => {}}; - RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); - expect(client._sock).to.have.sent(pointer_msg._sQ); - }); - - it('should set the button mask so that future mouse movements use it', function () { - client._handleMouseButton(10, 12, 1, 0x010); - client._handleMouseMove(13, 9); - const pointer_msg = {_sQ: new Uint8Array(12), _sQlen: 0, flush: () => {}}; - RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); - RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); - expect(client._sock).to.have.sent(pointer_msg._sQ); - }); - }); - - describe('Keyboard Event Handlers', function () { - it('should send a key message on a key press', function () { - client._handleKeyEvent(0x41, 'KeyA', true); - const key_msg = {_sQ: new Uint8Array(8), _sQlen: 0, flush: () => {}}; - RFB.messages.keyEvent(key_msg, 0x41, 1); - expect(client._sock).to.have.sent(key_msg._sQ); - }); - - it('should not send messages in view-only mode', function () { - client._viewOnly = true; - sinon.spy(client._sock, 'flush'); - client._handleKeyEvent('a', 'KeyA', true); - expect(client._sock.flush).to.not.have.been.called; - }); - }); - - describe('WebSocket event handlers', function () { - // message events - it ('should do nothing if we receive an empty message and have nothing in the queue', function () { - client._normal_msg = sinon.spy(); - client._sock._websocket._receive_data(new Uint8Array([])); - expect(client._normal_msg).to.not.have.been.called; - }); - - it('should handle a message in the connected state as a normal message', function () { - client._normal_msg = sinon.spy(); - client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); - expect(client._normal_msg).to.have.been.calledOnce; - }); - - it('should handle a message in any non-disconnected/failed state like an init message', function () { - client._rfb_connection_state = 'connecting'; - client._rfb_init_state = 'ProtocolVersion'; - client._init_msg = sinon.spy(); - client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); - expect(client._init_msg).to.have.been.calledOnce; - }); - - it('should process all normal messages directly', function () { - const spy = sinon.spy(); - client.addEventListener("bell", spy); - client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); - expect(spy).to.have.been.calledTwice; - }); - - // open events - it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () { - client = new RFB(document.createElement('div'), 'wss://host:8675'); - this.clock.tick(); - client._sock._websocket._open(); - expect(client._rfb_init_state).to.equal('ProtocolVersion'); - }); - - it('should fail if we are not currently ready to connect and we get an "open" event', function () { - sinon.spy(client, "_fail"); - client._rfb_connection_state = 'connected'; - client._sock._websocket._open(); - expect(client._fail).to.have.been.calledOnce; - }); - - // close events - it('should transition to "disconnected" from "disconnecting" on a close event', function () { - const real = client._sock._websocket.close; - client._sock._websocket.close = () => {}; - client.disconnect(); - expect(client._rfb_connection_state).to.equal('disconnecting'); - client._sock._websocket.close = real; - client._sock._websocket.close(); - expect(client._rfb_connection_state).to.equal('disconnected'); - }); - - it('should fail if we get a close event while connecting', function () { - sinon.spy(client, "_fail"); - client._rfb_connection_state = 'connecting'; - client._sock._websocket.close(); - expect(client._fail).to.have.been.calledOnce; - }); - - it('should unregister close event handler', function () { - sinon.spy(client._sock, 'off'); - client.disconnect(); - client._sock._websocket.close(); - expect(client._sock.off).to.have.been.calledWith('close'); - }); - - // error events do nothing - }); + it('should fail on unknown XVP message types', function () { + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array([250, 0, 10, 237])); + expect(client._fail).to.have.been.calledOnce; + }); }); + + it('should fire the clipboard callback with the retrieved text on ServerCutText', function () { + const expected_str = 'cheese!'; + const data = [3, 0, 0, 0]; + push32(data, expected_str.length); + for (let i = 0; i < expected_str.length; i++) { data.push(expected_str.charCodeAt(i)); } + const spy = sinon.spy(); + client.addEventListener('clipboard', spy); + + client._sock._websocket._receive_data(new Uint8Array(data)); + expect(spy).to.have.been.calledOnce; + expect(spy.args[0][0].detail.text).to.equal(expected_str); + }); + + it('should fire the bell callback on Bell', function () { + const spy = sinon.spy(); + client.addEventListener('bell', spy); + client._sock._websocket._receive_data(new Uint8Array([2])); + expect(spy).to.have.been.calledOnce; + }); + + it('should respond correctly to ServerFence', function () { + const expected_msg = { _sQ: new Uint8Array(16), _sQlen: 0, flush: () => {} }; + const incoming_msg = { _sQ: new Uint8Array(16), _sQlen: 0, flush: () => {} }; + + const payload = 'foo\x00ab9'; + + // ClientFence and ServerFence are identical in structure + RFB.messages.clientFence(expected_msg, (1 << 0) | (1 << 1), payload); + RFB.messages.clientFence(incoming_msg, 0xffffffff, payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + + expected_msg._sQlen = 0; + incoming_msg._sQlen = 0; + + RFB.messages.clientFence(expected_msg, (1 << 0), payload); + RFB.messages.clientFence(incoming_msg, (1 << 0) | (1 << 31), payload); + + client._sock._websocket._receive_data(incoming_msg._sQ); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should enable continuous updates on first EndOfContinousUpdates', function () { + const expected_msg = { _sQ: new Uint8Array(10), _sQlen: 0, flush: () => {} }; + + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 640, 20); + + expect(client._enabledContinuousUpdates).to.be.false; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.true; + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should disable continuous updates on subsequent EndOfContinousUpdates', function () { + client._enabledContinuousUpdates = true; + client._supportsContinuousUpdates = true; + + client._sock._websocket._receive_data(new Uint8Array([150])); + + expect(client._enabledContinuousUpdates).to.be.false; + }); + + it('should update continuous updates on resize', function () { + const expected_msg = { _sQ: new Uint8Array(10), _sQlen: 0, flush: () => {} }; + RFB.messages.enableContinuousUpdates(expected_msg, true, 0, 0, 90, 700); + + client._resize(450, 160); + + expect(client._sock._websocket._get_sent_data()).to.have.length(0); + + client._enabledContinuousUpdates = true; + + client._resize(90, 700); + + expect(client._sock).to.have.sent(expected_msg._sQ); + }); + + it('should fail on an unknown message type', function () { + sinon.spy(client, '_fail'); + client._sock._websocket._receive_data(new Uint8Array([87])); + expect(client._fail).to.have.been.calledOnce; + }); + }); + + describe('Asynchronous Events', function () { + let client; + beforeEach(function () { + client = make_rfb(); + }); + + describe('Mouse event handlers', function () { + it('should not send button messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleMouseButton(0, 0, 1, 0x001); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should not send movement messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleMouseMove(0, 0); + expect(client._sock.flush).to.not.have.been.called; + }); + + it('should send a pointer event on mouse button presses', function () { + client._handleMouseButton(10, 12, 1, 0x001); + const pointer_msg = { _sQ: new Uint8Array(6), _sQlen: 0, flush: () => {} }; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a mask of 1 on mousedown', function () { + client._handleMouseButton(10, 12, 1, 0x001); + const pointer_msg = { _sQ: new Uint8Array(6), _sQlen: 0, flush: () => {} }; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x001); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a mask of 0 on mouseup', function () { + client._mouse_buttonMask = 0x001; + client._handleMouseButton(10, 12, 0, 0x001); + const pointer_msg = { _sQ: new Uint8Array(6), _sQlen: 0, flush: () => {} }; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should send a pointer event on mouse movement', function () { + client._handleMouseMove(10, 12); + const pointer_msg = { _sQ: new Uint8Array(6), _sQlen: 0, flush: () => {} }; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x000); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + + it('should set the button mask so that future mouse movements use it', function () { + client._handleMouseButton(10, 12, 1, 0x010); + client._handleMouseMove(13, 9); + const pointer_msg = { _sQ: new Uint8Array(12), _sQlen: 0, flush: () => {} }; + RFB.messages.pointerEvent(pointer_msg, 10, 12, 0x010); + RFB.messages.pointerEvent(pointer_msg, 13, 9, 0x010); + expect(client._sock).to.have.sent(pointer_msg._sQ); + }); + }); + + describe('Keyboard Event Handlers', function () { + it('should send a key message on a key press', function () { + client._handleKeyEvent(0x41, 'KeyA', true); + const key_msg = { _sQ: new Uint8Array(8), _sQlen: 0, flush: () => {} }; + RFB.messages.keyEvent(key_msg, 0x41, 1); + expect(client._sock).to.have.sent(key_msg._sQ); + }); + + it('should not send messages in view-only mode', function () { + client._viewOnly = true; + sinon.spy(client._sock, 'flush'); + client._handleKeyEvent('a', 'KeyA', true); + expect(client._sock.flush).to.not.have.been.called; + }); + }); + + describe('WebSocket event handlers', function () { + // message events + it('should do nothing if we receive an empty message and have nothing in the queue', function () { + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([])); + expect(client._normal_msg).to.not.have.been.called; + }); + + it('should handle a message in the connected state as a normal message', function () { + client._normal_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); + expect(client._normal_msg).to.have.been.calledOnce; + }); + + it('should handle a message in any non-disconnected/failed state like an init message', function () { + client._rfb_connection_state = 'connecting'; + client._rfb_init_state = 'ProtocolVersion'; + client._init_msg = sinon.spy(); + client._sock._websocket._receive_data(new Uint8Array([1, 2, 3])); + expect(client._init_msg).to.have.been.calledOnce; + }); + + it('should process all normal messages directly', function () { + const spy = sinon.spy(); + client.addEventListener('bell', spy); + client._sock._websocket._receive_data(new Uint8Array([0x02, 0x02])); + expect(spy).to.have.been.calledTwice; + }); + + // open events + it('should update the state to ProtocolVersion on open (if the state is "connecting")', function () { + client = new RFB(document.createElement('div'), 'wss://host:8675'); + this.clock.tick(); + client._sock._websocket._open(); + expect(client._rfb_init_state).to.equal('ProtocolVersion'); + }); + + it('should fail if we are not currently ready to connect and we get an "open" event', function () { + sinon.spy(client, '_fail'); + client._rfb_connection_state = 'connected'; + client._sock._websocket._open(); + expect(client._fail).to.have.been.calledOnce; + }); + + // close events + it('should transition to "disconnected" from "disconnecting" on a close event', function () { + const real = client._sock._websocket.close; + client._sock._websocket.close = () => {}; + client.disconnect(); + expect(client._rfb_connection_state).to.equal('disconnecting'); + client._sock._websocket.close = real; + client._sock._websocket.close(); + expect(client._rfb_connection_state).to.equal('disconnected'); + }); + + it('should fail if we get a close event while connecting', function () { + sinon.spy(client, '_fail'); + client._rfb_connection_state = 'connecting'; + client._sock._websocket.close(); + expect(client._fail).to.have.been.calledOnce; + }); + + it('should unregister close event handler', function () { + sinon.spy(client._sock, 'off'); + client.disconnect(); + client._sock._websocket.close(); + expect(client._sock.off).to.have.been.calledWith('close'); + }); + + // error events do nothing + }); + }); }); diff --git a/tests/test.util.js b/tests/test.util.js index c73e4ff1..b95315c2 100644 --- a/tests/test.util.js +++ b/tests/test.util.js @@ -5,67 +5,67 @@ import * as Log from '../core/util/logging.js'; import sinon from '../vendor/sinon.js'; -describe('Utils', function() { - "use strict"; +describe('Utils', function () { + 'use strict'; - describe('logging functions', function () { - beforeEach(function () { - sinon.spy(console, 'log'); - sinon.spy(console, 'debug'); - sinon.spy(console, 'warn'); - sinon.spy(console, 'error'); - sinon.spy(console, 'info'); - }); - - afterEach(function () { - console.log.restore(); - console.debug.restore(); - console.warn.restore(); - console.error.restore(); - console.info.restore(); - Log.init_logging(); - }); - - it('should use noop for levels lower than the min level', function () { - Log.init_logging('warn'); - Log.Debug('hi'); - Log.Info('hello'); - expect(console.log).to.not.have.been.called; - }); - - it('should use console.debug for Debug', function () { - Log.init_logging('debug'); - Log.Debug('dbg'); - expect(console.debug).to.have.been.calledWith('dbg'); - }); - - it('should use console.info for Info', function () { - Log.init_logging('debug'); - Log.Info('inf'); - expect(console.info).to.have.been.calledWith('inf'); - }); - - it('should use console.warn for Warn', function () { - Log.init_logging('warn'); - Log.Warn('wrn'); - expect(console.warn).to.have.been.called; - expect(console.warn).to.have.been.calledWith('wrn'); - }); - - it('should use console.error for Error', function () { - Log.init_logging('error'); - Log.Error('err'); - expect(console.error).to.have.been.called; - expect(console.error).to.have.been.calledWith('err'); - }); + describe('logging functions', function () { + beforeEach(function () { + sinon.spy(console, 'log'); + sinon.spy(console, 'debug'); + sinon.spy(console, 'warn'); + sinon.spy(console, 'error'); + sinon.spy(console, 'info'); }); - // TODO(directxman12): test the conf_default and conf_defaults methods - // TODO(directxman12): test decodeUTF8 - // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent) - // TODO(directxman12): figure out a good way to test getPosition and getEventPosition - // TODO(directxman12): figure out how to test the browser detection functions properly - // (we can't really test them against the browsers, except for Gecko - // via PhantomJS, the default test driver) + afterEach(function () { + console.log.restore(); + console.debug.restore(); + console.warn.restore(); + console.error.restore(); + console.info.restore(); + Log.init_logging(); + }); + + it('should use noop for levels lower than the min level', function () { + Log.init_logging('warn'); + Log.Debug('hi'); + Log.Info('hello'); + expect(console.log).to.not.have.been.called; + }); + + it('should use console.debug for Debug', function () { + Log.init_logging('debug'); + Log.Debug('dbg'); + expect(console.debug).to.have.been.calledWith('dbg'); + }); + + it('should use console.info for Info', function () { + Log.init_logging('debug'); + Log.Info('inf'); + expect(console.info).to.have.been.calledWith('inf'); + }); + + it('should use console.warn for Warn', function () { + Log.init_logging('warn'); + Log.Warn('wrn'); + expect(console.warn).to.have.been.called; + expect(console.warn).to.have.been.calledWith('wrn'); + }); + + it('should use console.error for Error', function () { + Log.init_logging('error'); + Log.Error('err'); + expect(console.error).to.have.been.called; + expect(console.error).to.have.been.calledWith('err'); + }); + }); + + // TODO(directxman12): test the conf_default and conf_defaults methods + // TODO(directxman12): test decodeUTF8 + // TODO(directxman12): test the event methods (addEvent, removeEvent, stopEvent) + // TODO(directxman12): figure out a good way to test getPosition and getEventPosition + // TODO(directxman12): figure out how to test the browser detection functions properly + // (we can't really test them against the browsers, except for Gecko + // via PhantomJS, the default test driver) }); /* eslint-enable no-console */ diff --git a/tests/test.websock.js b/tests/test.websock.js index 124d1bf5..cf74b5f7 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -5,439 +5,439 @@ import FakeWebSocket from './fake.websocket.js'; import sinon from '../vendor/sinon.js'; -describe('Websock', function() { - "use strict"; +describe('Websock', function () { + 'use strict'; - describe('Queue methods', function () { - let sock; - const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); + describe('Queue methods', function () { + let sock; + const RQ_TEMPLATE = new Uint8Array([0, 1, 2, 3, 4, 5, 6, 7]); - beforeEach(function () { - sock = new Websock(); - // skip init - sock._allocate_buffers(); - sock._rQ.set(RQ_TEMPLATE); - sock._rQlen = RQ_TEMPLATE.length; - }); - describe('rQlen', function () { - it('should return the length of the receive queue', function () { - sock.set_rQi(0); + beforeEach(function () { + sock = new Websock(); + // skip init + sock._allocate_buffers(); + sock._rQ.set(RQ_TEMPLATE); + sock._rQlen = RQ_TEMPLATE.length; + }); + describe('rQlen', function () { + it('should return the length of the receive queue', function () { + sock.set_rQi(0); - expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length); - }); + expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length); + }); - it("should return the proper length if we read some from the receive queue", function () { - sock.set_rQi(1); + it('should return the proper length if we read some from the receive queue', function () { + sock.set_rQi(1); - expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1); - }); - }); - - describe('rQpeek8', function () { - it('should peek at the next byte without poping it off the queue', function () { - const bef_len = sock.rQlen(); - const peek = sock.rQpeek8(); - expect(sock.rQpeek8()).to.equal(peek); - expect(sock.rQlen()).to.equal(bef_len); - }); - }); - - describe('rQshift8', function () { - it('should pop a single byte from the receive queue', function () { - const peek = sock.rQpeek8(); - const bef_len = sock.rQlen(); - expect(sock.rQshift8()).to.equal(peek); - expect(sock.rQlen()).to.equal(bef_len - 1); - }); - }); - - describe('rQshift16', function () { - it('should pop two bytes from the receive queue and return a single number', function () { - const bef_len = sock.rQlen(); - const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; - expect(sock.rQshift16()).to.equal(expected); - expect(sock.rQlen()).to.equal(bef_len - 2); - }); - }); - - describe('rQshift32', function () { - it('should pop four bytes from the receive queue and return a single number', function () { - const bef_len = sock.rQlen(); - const expected = (RQ_TEMPLATE[0] << 24) + - (RQ_TEMPLATE[1] << 16) + - (RQ_TEMPLATE[2] << 8) + - RQ_TEMPLATE[3]; - expect(sock.rQshift32()).to.equal(expected); - expect(sock.rQlen()).to.equal(bef_len - 4); - }); - }); - - describe('rQshiftStr', function () { - it('should shift the given number of bytes off of the receive queue and return a string', function () { - const bef_len = sock.rQlen(); - const bef_rQi = sock.get_rQi(); - const shifted = sock.rQshiftStr(3); - expect(shifted).to.be.a('string'); - expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); - expect(sock.rQlen()).to.equal(bef_len - 3); - }); - - it('should shift the entire rest of the queue off if no length is given', function () { - sock.rQshiftStr(); - expect(sock.rQlen()).to.equal(0); - }); - - it('should be able to handle very large strings', function () { - const BIG_LEN = 500000; - const RQ_BIG = new Uint8Array(BIG_LEN); - let expected = ""; - let letterCode = 'a'.charCodeAt(0); - for (let i = 0; i < BIG_LEN; i++) { - RQ_BIG[i] = letterCode; - expected += String.fromCharCode(letterCode); - - if (letterCode < 'z'.charCodeAt(0)) { - letterCode++; - } else { - letterCode = 'a'.charCodeAt(0); - } - } - sock._rQ.set(RQ_BIG); - sock._rQlen = RQ_BIG.length; - - const shifted = sock.rQshiftStr(); - - expect(shifted).to.be.equal(expected); - expect(sock.rQlen()).to.equal(0); - }); - }); - - describe('rQshiftBytes', function () { - it('should shift the given number of bytes of the receive queue and return an array', function () { - const bef_len = sock.rQlen(); - const bef_rQi = sock.get_rQi(); - const shifted = sock.rQshiftBytes(3); - expect(shifted).to.be.an.instanceof(Uint8Array); - expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)); - expect(sock.rQlen()).to.equal(bef_len - 3); - }); - - it('should shift the entire rest of the queue off if no length is given', function () { - sock.rQshiftBytes(); - expect(sock.rQlen()).to.equal(0); - }); - }); - - describe('rQslice', function () { - beforeEach(function () { - sock.set_rQi(0); - }); - - it('should not modify the receive queue', function () { - const bef_len = sock.rQlen(); - sock.rQslice(0, 2); - expect(sock.rQlen()).to.equal(bef_len); - }); - - it('should return an array containing the given slice of the receive queue', function () { - const sl = sock.rQslice(0, 2); - expect(sl).to.be.an.instanceof(Uint8Array); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); - }); - - it('should use the rest of the receive queue if no end is given', function () { - const sl = sock.rQslice(1); - expect(sl).to.have.length(RQ_TEMPLATE.length - 1); - expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1)); - }); - - it('should take the current rQi in to account', function () { - sock.set_rQi(1); - expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); - }); - }); - - describe('rQwait', function () { - beforeEach(function () { - sock.set_rQi(0); - }); - - it('should return true if there are not enough bytes in the receive queue', function () { - expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true; - }); - - it('should return false if there are enough bytes in the receive queue', function () { - expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false; - }); - - it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { - sock.set_rQi(5); - expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; - expect(sock.get_rQi()).to.equal(1); - }); - - it('should raise an error if we try to go back more than possible', function () { - sock.set_rQi(5); - expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); - }); - - it('should not reduce rQi if there are enough bytes', function () { - sock.set_rQi(5); - sock.rQwait('hi', 1, 6); - expect(sock.get_rQi()).to.equal(5); - }); - }); - - describe('flush', function () { - beforeEach(function () { - sock._websocket = { - send: sinon.spy() - }; - }); - - it('should actually send on the websocket', function () { - sock._websocket.bufferedAmount = 8; - sock._websocket.readyState = WebSocket.OPEN - sock._sQ = new Uint8Array([1, 2, 3]); - sock._sQlen = 3; - const encoded = sock._encode_message(); - - sock.flush(); - expect(sock._websocket.send).to.have.been.calledOnce; - expect(sock._websocket.send).to.have.been.calledWith(encoded); - }); - - it('should not call send if we do not have anything queued up', function () { - sock._sQlen = 0; - sock._websocket.bufferedAmount = 8; - - sock.flush(); - - expect(sock._websocket.send).not.to.have.been.called; - }); - }); - - describe('send', function () { - beforeEach(function () { - sock.flush = sinon.spy(); - }); - - it('should add to the send queue', function () { - sock.send([1, 2, 3]); - const sq = sock.get_sQ(); - expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); - }); - - it('should call flush', function () { - sock.send([1, 2, 3]); - expect(sock.flush).to.have.been.calledOnce; - }); - }); - - describe('send_string', function () { - beforeEach(function () { - sock.send = sinon.spy(); - }); - - it('should call send after converting the string to an array', function () { - sock.send_string("\x01\x02\x03"); - expect(sock.send).to.have.been.calledWith([1, 2, 3]); - }); - }); + expect(sock.rQlen()).to.equal(RQ_TEMPLATE.length - 1); + }); }); - describe('lifecycle methods', function () { - let old_WS; - before(function () { - old_WS = WebSocket; - }); - - let sock; - beforeEach(function () { - sock = new Websock(); - // eslint-disable-next-line no-global-assign - WebSocket = sinon.spy(); - WebSocket.OPEN = old_WS.OPEN; - WebSocket.CONNECTING = old_WS.CONNECTING; - WebSocket.CLOSING = old_WS.CLOSING; - WebSocket.CLOSED = old_WS.CLOSED; - - WebSocket.prototype.binaryType = 'arraybuffer'; - }); - - describe('opening', function () { - it('should pick the correct protocols if none are given' , function () { - - }); - - it('should open the actual websocket', function () { - sock.open('ws://localhost:8675', 'binary'); - expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary'); - }); - - // it('should initialize the event handlers')? - }); - - describe('closing', function () { - beforeEach(function () { - sock.open('ws://'); - sock._websocket.close = sinon.spy(); - }); - - it('should close the actual websocket if it is open', function () { - sock._websocket.readyState = WebSocket.OPEN; - sock.close(); - expect(sock._websocket.close).to.have.been.calledOnce; - }); - - it('should close the actual websocket if it is connecting', function () { - sock._websocket.readyState = WebSocket.CONNECTING; - sock.close(); - expect(sock._websocket.close).to.have.been.calledOnce; - }); - - it('should not try to close the actual websocket if closing', function () { - sock._websocket.readyState = WebSocket.CLOSING; - sock.close(); - expect(sock._websocket.close).not.to.have.been.called; - }); - - it('should not try to close the actual websocket if closed', function () { - sock._websocket.readyState = WebSocket.CLOSED; - sock.close(); - expect(sock._websocket.close).not.to.have.been.called; - }); - - it('should reset onmessage to not call _recv_message', function () { - sinon.spy(sock, '_recv_message'); - sock.close(); - sock._websocket.onmessage(null); - try { - expect(sock._recv_message).not.to.have.been.called; - } finally { - sock._recv_message.restore(); - } - }); - }); - - describe('event handlers', function () { - beforeEach(function () { - sock._recv_message = sinon.spy(); - sock.on('open', sinon.spy()); - sock.on('close', sinon.spy()); - sock.on('error', sinon.spy()); - sock.open('ws://'); - }); - - it('should call _recv_message on a message', function () { - sock._websocket.onmessage(null); - expect(sock._recv_message).to.have.been.calledOnce; - }); - - it('should call the open event handler on opening', function () { - sock._websocket.onopen(); - expect(sock._eventHandlers.open).to.have.been.calledOnce; - }); - - it('should call the close event handler on closing', function () { - sock._websocket.onclose(); - expect(sock._eventHandlers.close).to.have.been.calledOnce; - }); - - it('should call the error event handler on error', function () { - sock._websocket.onerror(); - expect(sock._eventHandlers.error).to.have.been.calledOnce; - }); - }); - - after(function () { - // eslint-disable-next-line no-global-assign - WebSocket = old_WS; - }); + describe('rQpeek8', function () { + it('should peek at the next byte without poping it off the queue', function () { + const bef_len = sock.rQlen(); + const peek = sock.rQpeek8(); + expect(sock.rQpeek8()).to.equal(peek); + expect(sock.rQlen()).to.equal(bef_len); + }); }); - describe('WebSocket Receiving', function () { - let sock; - beforeEach(function () { - sock = new Websock(); - sock._allocate_buffers(); - }); - - it('should support adding binary Uint8Array data to the receive queue', function () { - const msg = { data: new Uint8Array([1, 2, 3]) }; - sock._mode = 'binary'; - sock._recv_message(msg); - expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); - }); - - it('should call the message event handler if present', function () { - sock._eventHandlers.message = sinon.spy(); - const msg = { data: new Uint8Array([1, 2, 3]).buffer }; - sock._mode = 'binary'; - sock._recv_message(msg); - expect(sock._eventHandlers.message).to.have.been.calledOnce; - }); - - it('should not call the message event handler if there is nothing in the receive queue', function () { - sock._eventHandlers.message = sinon.spy(); - const msg = { data: new Uint8Array([]).buffer }; - sock._mode = 'binary'; - sock._recv_message(msg); - expect(sock._eventHandlers.message).not.to.have.been.called; - }); - - it('should compact the receive queue', function () { - // NB(sross): while this is an internal implementation detail, it's important to - // test, otherwise the receive queue could become very large very quickly - sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); - sock._rQlen = 6; - sock.set_rQi(6); - sock._rQmax = 3; - const msg = { data: new Uint8Array([1, 2, 3]).buffer }; - sock._mode = 'binary'; - sock._recv_message(msg); - expect(sock._rQlen).to.equal(3); - expect(sock.get_rQi()).to.equal(0); - }); - - it('should automatically resize the receive queue if the incoming message is too large', function () { - sock._rQ = new Uint8Array(20); - sock._rQlen = 0; - sock.set_rQi(0); - sock._rQbufferSize = 20; - sock._rQmax = 2; - const msg = { data: new Uint8Array(30).buffer }; - sock._mode = 'binary'; - sock._recv_message(msg); - expect(sock._rQlen).to.equal(30); - expect(sock.get_rQi()).to.equal(0); - expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen - }); + describe('rQshift8', function () { + it('should pop a single byte from the receive queue', function () { + const peek = sock.rQpeek8(); + const bef_len = sock.rQlen(); + expect(sock.rQshift8()).to.equal(peek); + expect(sock.rQlen()).to.equal(bef_len - 1); + }); }); - describe('Data encoding', function () { - before(function () { FakeWebSocket.replace(); }); - after(function () { FakeWebSocket.restore(); }); - - describe('as binary data', function () { - let sock; - beforeEach(function () { - sock = new Websock(); - sock.open('ws://', 'binary'); - sock._websocket._open(); - }); - - it('should only send the send queue up to the send queue length', function () { - sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); - sock._sQlen = 3; - const res = sock._encode_message(); - expect(res).to.array.equal(new Uint8Array([1, 2, 3])); - }); - - it('should properly pass the encoded data off to the actual WebSocket', function () { - sock.send([1, 2, 3]); - expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3])); - }); - }); + describe('rQshift16', function () { + it('should pop two bytes from the receive queue and return a single number', function () { + const bef_len = sock.rQlen(); + const expected = (RQ_TEMPLATE[0] << 8) + RQ_TEMPLATE[1]; + expect(sock.rQshift16()).to.equal(expected); + expect(sock.rQlen()).to.equal(bef_len - 2); + }); }); + + describe('rQshift32', function () { + it('should pop four bytes from the receive queue and return a single number', function () { + const bef_len = sock.rQlen(); + const expected = (RQ_TEMPLATE[0] << 24) + + (RQ_TEMPLATE[1] << 16) + + (RQ_TEMPLATE[2] << 8) + + RQ_TEMPLATE[3]; + expect(sock.rQshift32()).to.equal(expected); + expect(sock.rQlen()).to.equal(bef_len - 4); + }); + }); + + describe('rQshiftStr', function () { + it('should shift the given number of bytes off of the receive queue and return a string', function () { + const bef_len = sock.rQlen(); + const bef_rQi = sock.get_rQi(); + const shifted = sock.rQshiftStr(3); + expect(shifted).to.be.a('string'); + expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); + expect(sock.rQlen()).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftStr(); + expect(sock.rQlen()).to.equal(0); + }); + + it('should be able to handle very large strings', function () { + const BIG_LEN = 500000; + const RQ_BIG = new Uint8Array(BIG_LEN); + let expected = ''; + let letterCode = 'a'.charCodeAt(0); + for (let i = 0; i < BIG_LEN; i++) { + RQ_BIG[i] = letterCode; + expected += String.fromCharCode(letterCode); + + if (letterCode < 'z'.charCodeAt(0)) { + letterCode++; + } else { + letterCode = 'a'.charCodeAt(0); + } + } + sock._rQ.set(RQ_BIG); + sock._rQlen = RQ_BIG.length; + + const shifted = sock.rQshiftStr(); + + expect(shifted).to.be.equal(expected); + expect(sock.rQlen()).to.equal(0); + }); + }); + + describe('rQshiftBytes', function () { + it('should shift the given number of bytes of the receive queue and return an array', function () { + const bef_len = sock.rQlen(); + const bef_rQi = sock.get_rQi(); + const shifted = sock.rQshiftBytes(3); + expect(shifted).to.be.an.instanceof(Uint8Array); + expect(shifted).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)); + expect(sock.rQlen()).to.equal(bef_len - 3); + }); + + it('should shift the entire rest of the queue off if no length is given', function () { + sock.rQshiftBytes(); + expect(sock.rQlen()).to.equal(0); + }); + }); + + describe('rQslice', function () { + beforeEach(function () { + sock.set_rQi(0); + }); + + it('should not modify the receive queue', function () { + const bef_len = sock.rQlen(); + sock.rQslice(0, 2); + expect(sock.rQlen()).to.equal(bef_len); + }); + + it('should return an array containing the given slice of the receive queue', function () { + const sl = sock.rQslice(0, 2); + expect(sl).to.be.an.instanceof(Uint8Array); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 0, 2)); + }); + + it('should use the rest of the receive queue if no end is given', function () { + const sl = sock.rQslice(1); + expect(sl).to.have.length(RQ_TEMPLATE.length - 1); + expect(sl).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1)); + }); + + it('should take the current rQi in to account', function () { + sock.set_rQi(1); + expect(sock.rQslice(0, 2)).to.array.equal(new Uint8Array(RQ_TEMPLATE.buffer, 1, 2)); + }); + }); + + describe('rQwait', function () { + beforeEach(function () { + sock.set_rQi(0); + }); + + it('should return true if there are not enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length + 1)).to.be.true; + }); + + it('should return false if there are enough bytes in the receive queue', function () { + expect(sock.rQwait('hi', RQ_TEMPLATE.length)).to.be.false; + }); + + it('should return true and reduce rQi by "goback" if there are not enough bytes', function () { + sock.set_rQi(5); + expect(sock.rQwait('hi', RQ_TEMPLATE.length, 4)).to.be.true; + expect(sock.get_rQi()).to.equal(1); + }); + + it('should raise an error if we try to go back more than possible', function () { + sock.set_rQi(5); + expect(() => sock.rQwait('hi', RQ_TEMPLATE.length, 6)).to.throw(Error); + }); + + it('should not reduce rQi if there are enough bytes', function () { + sock.set_rQi(5); + sock.rQwait('hi', 1, 6); + expect(sock.get_rQi()).to.equal(5); + }); + }); + + describe('flush', function () { + beforeEach(function () { + sock._websocket = { + send: sinon.spy() + }; + }); + + it('should actually send on the websocket', function () { + sock._websocket.bufferedAmount = 8; + sock._websocket.readyState = WebSocket.OPEN; + sock._sQ = new Uint8Array([1, 2, 3]); + sock._sQlen = 3; + const encoded = sock._encode_message(); + + sock.flush(); + expect(sock._websocket.send).to.have.been.calledOnce; + expect(sock._websocket.send).to.have.been.calledWith(encoded); + }); + + it('should not call send if we do not have anything queued up', function () { + sock._sQlen = 0; + sock._websocket.bufferedAmount = 8; + + sock.flush(); + + expect(sock._websocket.send).not.to.have.been.called; + }); + }); + + describe('send', function () { + beforeEach(function () { + sock.flush = sinon.spy(); + }); + + it('should add to the send queue', function () { + sock.send([1, 2, 3]); + const sq = sock.get_sQ(); + expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); + }); + + it('should call flush', function () { + sock.send([1, 2, 3]); + expect(sock.flush).to.have.been.calledOnce; + }); + }); + + describe('send_string', function () { + beforeEach(function () { + sock.send = sinon.spy(); + }); + + it('should call send after converting the string to an array', function () { + sock.send_string('\x01\x02\x03'); + expect(sock.send).to.have.been.calledWith([1, 2, 3]); + }); + }); + }); + + describe('lifecycle methods', function () { + let old_WS; + before(function () { + old_WS = WebSocket; + }); + + let sock; + beforeEach(function () { + sock = new Websock(); + // eslint-disable-next-line no-global-assign + WebSocket = sinon.spy(); + WebSocket.OPEN = old_WS.OPEN; + WebSocket.CONNECTING = old_WS.CONNECTING; + WebSocket.CLOSING = old_WS.CLOSING; + WebSocket.CLOSED = old_WS.CLOSED; + + WebSocket.prototype.binaryType = 'arraybuffer'; + }); + + describe('opening', function () { + it('should pick the correct protocols if none are given', function () { + + }); + + it('should open the actual websocket', function () { + sock.open('ws://localhost:8675', 'binary'); + expect(WebSocket).to.have.been.calledWith('ws://localhost:8675', 'binary'); + }); + + // it('should initialize the event handlers')? + }); + + describe('closing', function () { + beforeEach(function () { + sock.open('ws://'); + sock._websocket.close = sinon.spy(); + }); + + it('should close the actual websocket if it is open', function () { + sock._websocket.readyState = WebSocket.OPEN; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should close the actual websocket if it is connecting', function () { + sock._websocket.readyState = WebSocket.CONNECTING; + sock.close(); + expect(sock._websocket.close).to.have.been.calledOnce; + }); + + it('should not try to close the actual websocket if closing', function () { + sock._websocket.readyState = WebSocket.CLOSING; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should not try to close the actual websocket if closed', function () { + sock._websocket.readyState = WebSocket.CLOSED; + sock.close(); + expect(sock._websocket.close).not.to.have.been.called; + }); + + it('should reset onmessage to not call _recv_message', function () { + sinon.spy(sock, '_recv_message'); + sock.close(); + sock._websocket.onmessage(null); + try { + expect(sock._recv_message).not.to.have.been.called; + } finally { + sock._recv_message.restore(); + } + }); + }); + + describe('event handlers', function () { + beforeEach(function () { + sock._recv_message = sinon.spy(); + sock.on('open', sinon.spy()); + sock.on('close', sinon.spy()); + sock.on('error', sinon.spy()); + sock.open('ws://'); + }); + + it('should call _recv_message on a message', function () { + sock._websocket.onmessage(null); + expect(sock._recv_message).to.have.been.calledOnce; + }); + + it('should call the open event handler on opening', function () { + sock._websocket.onopen(); + expect(sock._eventHandlers.open).to.have.been.calledOnce; + }); + + it('should call the close event handler on closing', function () { + sock._websocket.onclose(); + expect(sock._eventHandlers.close).to.have.been.calledOnce; + }); + + it('should call the error event handler on error', function () { + sock._websocket.onerror(); + expect(sock._eventHandlers.error).to.have.been.calledOnce; + }); + }); + + after(function () { + // eslint-disable-next-line no-global-assign + WebSocket = old_WS; + }); + }); + + describe('WebSocket Receiving', function () { + let sock; + beforeEach(function () { + sock = new Websock(); + sock._allocate_buffers(); + }); + + it('should support adding binary Uint8Array data to the receive queue', function () { + const msg = { data: new Uint8Array([1, 2, 3]) }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock.rQshiftStr(3)).to.equal('\x01\x02\x03'); + }); + + it('should call the message event handler if present', function () { + sock._eventHandlers.message = sinon.spy(); + const msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).to.have.been.calledOnce; + }); + + it('should not call the message event handler if there is nothing in the receive queue', function () { + sock._eventHandlers.message = sinon.spy(); + const msg = { data: new Uint8Array([]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._eventHandlers.message).not.to.have.been.called; + }); + + it('should compact the receive queue', function () { + // NB(sross): while this is an internal implementation detail, it's important to + // test, otherwise the receive queue could become very large very quickly + sock._rQ = new Uint8Array([0, 1, 2, 3, 4, 5, 0, 0, 0, 0]); + sock._rQlen = 6; + sock.set_rQi(6); + sock._rQmax = 3; + const msg = { data: new Uint8Array([1, 2, 3]).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._rQlen).to.equal(3); + expect(sock.get_rQi()).to.equal(0); + }); + + it('should automatically resize the receive queue if the incoming message is too large', function () { + sock._rQ = new Uint8Array(20); + sock._rQlen = 0; + sock.set_rQi(0); + sock._rQbufferSize = 20; + sock._rQmax = 2; + const msg = { data: new Uint8Array(30).buffer }; + sock._mode = 'binary'; + sock._recv_message(msg); + expect(sock._rQlen).to.equal(30); + expect(sock.get_rQi()).to.equal(0); + expect(sock._rQ.length).to.equal(240); // keep the invariant that rQbufferSize / 8 >= rQlen + }); + }); + + describe('Data encoding', function () { + before(function () { FakeWebSocket.replace(); }); + after(function () { FakeWebSocket.restore(); }); + + describe('as binary data', function () { + let sock; + beforeEach(function () { + sock = new Websock(); + sock.open('ws://', 'binary'); + sock._websocket._open(); + }); + + it('should only send the send queue up to the send queue length', function () { + sock._sQ = new Uint8Array([1, 2, 3, 4, 5]); + sock._sQlen = 3; + const res = sock._encode_message(); + expect(res).to.array.equal(new Uint8Array([1, 2, 3])); + }); + + it('should properly pass the encoded data off to the actual WebSocket', function () { + sock.send([1, 2, 3]); + expect(sock._websocket._get_sent_data()).to.array.equal(new Uint8Array([1, 2, 3])); + }); + }); + }); }); diff --git a/tests/test.webutil.js b/tests/test.webutil.js index ae289746..69480e01 100644 --- a/tests/test.webutil.js +++ b/tests/test.webutil.js @@ -6,181 +6,180 @@ import * as WebUtil from '../app/webutil.js'; import sinon from '../vendor/sinon.js'; -describe('WebUtil', function() { - "use strict"; +describe('WebUtil', function () { + 'use strict'; - describe('settings', function () { + describe('settings', function () { + describe('localStorage', function () { + let chrome = window.chrome; + before(function () { + chrome = window.chrome; + window.chrome = null; + }); + after(function () { + window.chrome = chrome; + }); - describe('localStorage', function() { - let chrome = window.chrome; - before(function() { - chrome = window.chrome; - window.chrome = null; - }); - after(function() { - window.chrome = chrome; - }); + let origLocalStorage; + beforeEach(function () { + origLocalStorage = Object.getOwnPropertyDescriptor(window, 'localStorage'); + if (origLocalStorage === undefined) { + // Object.getOwnPropertyDescriptor() doesn't work + // properly in any version of IE + this.skip(); + } - let origLocalStorage; - beforeEach(function() { - origLocalStorage = Object.getOwnPropertyDescriptor(window, "localStorage"); - if (origLocalStorage === undefined) { - // Object.getOwnPropertyDescriptor() doesn't work - // properly in any version of IE - this.skip(); - } + Object.defineProperty(window, 'localStorage', { value: {} }); + if (window.localStorage.setItem !== undefined) { + // Object.defineProperty() doesn't work properly in old + // versions of Chrome + this.skip(); + } - Object.defineProperty(window, "localStorage", {value: {}}); - if (window.localStorage.setItem !== undefined) { - // Object.defineProperty() doesn't work properly in old - // versions of Chrome - this.skip(); - } + window.localStorage.setItem = sinon.stub(); + window.localStorage.getItem = sinon.stub(); + window.localStorage.removeItem = sinon.stub(); - window.localStorage.setItem = sinon.stub(); - window.localStorage.getItem = sinon.stub(); - window.localStorage.removeItem = sinon.stub(); + WebUtil.initSettings(); + }); + afterEach(function () { + Object.defineProperty(window, 'localStorage', origLocalStorage); + }); - WebUtil.initSettings(); - }); - afterEach(function() { - Object.defineProperty(window, "localStorage", origLocalStorage); - }); + describe('writeSetting', function () { + it('should save the setting value to local storage', function () { + WebUtil.writeSetting('test', 'value'); + expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); - describe('writeSetting', function() { - it('should save the setting value to local storage', function() { - WebUtil.writeSetting('test', 'value'); - expect(window.localStorage.setItem).to.have.been.calledWithExactly('test', 'value'); - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - }); + describe('setSetting', function () { + it('should update the setting but not save to local storage', function () { + WebUtil.setSetting('test', 'value'); + expect(window.localStorage.setItem).to.not.have.been.called; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); - describe('setSetting', function() { - it('should update the setting but not save to local storage', function() { - WebUtil.setSetting('test', 'value'); - expect(window.localStorage.setItem).to.not.have.been.called; - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - }); - - describe('readSetting', function() { - it('should read the setting value from local storage', function() { - localStorage.getItem.returns('value'); - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - - it('should return the default value when not in local storage', function() { - expect(WebUtil.readSetting('test', 'default')).to.equal('default'); - }); - - it('should return the cached value even if local storage changed', function() { - localStorage.getItem.returns('value'); - expect(WebUtil.readSetting('test')).to.equal('value'); - localStorage.getItem.returns('something else'); - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - - it('should cache the value even if it is not initially in local storage', function() { - expect(WebUtil.readSetting('test')).to.be.null; - localStorage.getItem.returns('value'); - expect(WebUtil.readSetting('test')).to.be.null; - }); - - it('should return the default value always if the first read was not in local storage', function() { - expect(WebUtil.readSetting('test', 'default')).to.equal('default'); - localStorage.getItem.returns('value'); - expect(WebUtil.readSetting('test', 'another default')).to.equal('another default'); - }); - - it('should return the last local written value', function() { - localStorage.getItem.returns('value'); - expect(WebUtil.readSetting('test')).to.equal('value'); - WebUtil.writeSetting('test', 'something else'); - expect(WebUtil.readSetting('test')).to.equal('something else'); - }); - }); - - // this doesn't appear to be used anywhere - describe('eraseSetting', function() { - it('should remove the setting from local storage', function() { - WebUtil.eraseSetting('test'); - expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test'); - }); - }); + describe('readSetting', function () { + it('should read the setting value from local storage', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); }); - describe('chrome.storage', function() { - let chrome = window.chrome; - let settings = {}; - before(function() { - chrome = window.chrome; - window.chrome = { - storage: { - sync: { - get(cb){ cb(settings); }, - set(){}, - remove() {} - } - } - }; - }); - after(function() { - window.chrome = chrome; - }); - - const csSandbox = sinon.createSandbox(); - - beforeEach(function() { - settings = {}; - csSandbox.spy(window.chrome.storage.sync, 'set'); - csSandbox.spy(window.chrome.storage.sync, 'remove'); - WebUtil.initSettings(); - }); - afterEach(function() { - csSandbox.restore(); - }); - - describe('writeSetting', function() { - it('should save the setting value to chrome storage', function() { - WebUtil.writeSetting('test', 'value'); - expect(window.chrome.storage.sync.set).to.have.been.calledWithExactly(sinon.match({ test: 'value' })); - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - }); - - describe('setSetting', function() { - it('should update the setting but not save to chrome storage', function() { - WebUtil.setSetting('test', 'value'); - expect(window.chrome.storage.sync.set).to.not.have.been.called; - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - }); - - describe('readSetting', function() { - it('should read the setting value from chrome storage', function() { - settings.test = 'value'; - expect(WebUtil.readSetting('test')).to.equal('value'); - }); - - it('should return the default value when not in chrome storage', function() { - expect(WebUtil.readSetting('test', 'default')).to.equal('default'); - }); - - it('should return the last local written value', function() { - settings.test = 'value'; - expect(WebUtil.readSetting('test')).to.equal('value'); - WebUtil.writeSetting('test', 'something else'); - expect(WebUtil.readSetting('test')).to.equal('something else'); - }); - }); - - // this doesn't appear to be used anywhere - describe('eraseSetting', function() { - it('should remove the setting from chrome storage', function() { - WebUtil.eraseSetting('test'); - expect(window.chrome.storage.sync.remove).to.have.been.calledWithExactly('test'); - }); - }); + it('should return the default value when not in local storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); }); + + it('should return the cached value even if local storage changed', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + localStorage.getItem.returns('something else'); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + + it('should cache the value even if it is not initially in local storage', function () { + expect(WebUtil.readSetting('test')).to.be.null; + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.be.null; + }); + + it('should return the default value always if the first read was not in local storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test', 'another default')).to.equal('another default'); + }); + + it('should return the last local written value', function () { + localStorage.getItem.returns('value'); + expect(WebUtil.readSetting('test')).to.equal('value'); + WebUtil.writeSetting('test', 'something else'); + expect(WebUtil.readSetting('test')).to.equal('something else'); + }); + }); + + // this doesn't appear to be used anywhere + describe('eraseSetting', function () { + it('should remove the setting from local storage', function () { + WebUtil.eraseSetting('test'); + expect(window.localStorage.removeItem).to.have.been.calledWithExactly('test'); + }); + }); }); + + describe('chrome.storage', function () { + let chrome = window.chrome; + let settings = {}; + before(function () { + chrome = window.chrome; + window.chrome = { + storage: { + sync: { + get(cb) { cb(settings); }, + set() {}, + remove() {} + } + } + }; + }); + after(function () { + window.chrome = chrome; + }); + + const csSandbox = sinon.createSandbox(); + + beforeEach(function () { + settings = {}; + csSandbox.spy(window.chrome.storage.sync, 'set'); + csSandbox.spy(window.chrome.storage.sync, 'remove'); + WebUtil.initSettings(); + }); + afterEach(function () { + csSandbox.restore(); + }); + + describe('writeSetting', function () { + it('should save the setting value to chrome storage', function () { + WebUtil.writeSetting('test', 'value'); + expect(window.chrome.storage.sync.set).to.have.been.calledWithExactly(sinon.match({ test: 'value' })); + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('setSetting', function () { + it('should update the setting but not save to chrome storage', function () { + WebUtil.setSetting('test', 'value'); + expect(window.chrome.storage.sync.set).to.not.have.been.called; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + }); + + describe('readSetting', function () { + it('should read the setting value from chrome storage', function () { + settings.test = 'value'; + expect(WebUtil.readSetting('test')).to.equal('value'); + }); + + it('should return the default value when not in chrome storage', function () { + expect(WebUtil.readSetting('test', 'default')).to.equal('default'); + }); + + it('should return the last local written value', function () { + settings.test = 'value'; + expect(WebUtil.readSetting('test')).to.equal('value'); + WebUtil.writeSetting('test', 'something else'); + expect(WebUtil.readSetting('test')).to.equal('something else'); + }); + }); + + // this doesn't appear to be used anywhere + describe('eraseSetting', function () { + it('should remove the setting from chrome storage', function () { + WebUtil.eraseSetting('test'); + expect(window.chrome.storage.sync.remove).to.have.been.calledWithExactly('test'); + }); + }); + }); + }); }); diff --git a/utils/genkeysymdef.js b/utils/genkeysymdef.js index cd89a683..cf4fb3be 100755 --- a/utils/genkeysymdef.js +++ b/utils/genkeysymdef.js @@ -6,7 +6,7 @@ * Licensed under MPL 2.0 (see LICENSE.txt) */ -"use strict"; +'use strict'; const fs = require('fs'); @@ -15,12 +15,12 @@ let filename; for (let i = 2; i < process.argv.length; ++i) { switch (process.argv[i]) { - case "--help": - case "-h": + case '--help': + case '-h': show_help = true; break; - case "--file": - case "-f": + case '--file': + case '-f': default: filename = process.argv[i]; } @@ -28,14 +28,14 @@ for (let i = 2; i < process.argv.length; ++i) { if (!filename) { show_help = true; - console.log("Error: No filename specified\n"); + console.log('Error: No filename specified\n'); } if (show_help) { - console.log("Parses a *nix keysymdef.h to generate Unicode code point mappings"); - console.log("Usage: node parse.js [options] filename:"); - console.log(" -h [ --help ] Produce this help message"); - console.log(" filename The keysymdef.h file to parse"); + console.log('Parses a *nix keysymdef.h to generate Unicode code point mappings'); + console.log('Usage: node parse.js [options] filename:'); + console.log(' -h [ --help ] Produce this help message'); + console.log(' filename The keysymdef.h file to parse'); process.exit(0); } @@ -49,80 +49,79 @@ const arr = str.split('\n'); const codepoints = {}; for (let i = 0; i < arr.length; ++i) { - const result = re.exec(arr[i]); - if (result){ - const keyname = result[1]; - const keysym = parseInt(result[2], 16); - const remainder = result[3]; + const result = re.exec(arr[i]); + if (result) { + const keyname = result[1]; + const keysym = parseInt(result[2], 16); + const remainder = result[3]; - const unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder); - if (unicodeRes) { - const unicode = parseInt(unicodeRes[1], 16); - // The first entry is the preferred one - if (!codepoints[unicode]){ - codepoints[unicode] = { keysym: keysym, name: keyname }; - } - } + const unicodeRes = /U\+([0-9a-fA-F]+)/.exec(remainder); + if (unicodeRes) { + const unicode = parseInt(unicodeRes[1], 16); + // The first entry is the preferred one + if (!codepoints[unicode]) { + codepoints[unicode] = { keysym: keysym, name: keyname }; + } } + } } -let out = -"/*\n" + -" * Mapping from Unicode codepoints to X11/RFB keysyms\n" + -" *\n" + -" * This file was automatically generated from keysymdef.h\n" + -" * DO NOT EDIT!\n" + -" */\n" + -"\n" + -"/* Functions at the bottom */\n" + -"\n" + -"const codepoints = {\n"; +let out = '/*\n' ++ ' * Mapping from Unicode codepoints to X11/RFB keysyms\n' ++ ' *\n' ++ ' * This file was automatically generated from keysymdef.h\n' ++ ' * DO NOT EDIT!\n' ++ ' */\n' ++ '\n' ++ '/* Functions at the bottom */\n' ++ '\n' ++ 'const codepoints = {\n'; function toHex(num) { - let s = num.toString(16); - if (s.length < 4) { - s = ("0000" + s).slice(-4); - } - return "0x" + s; + let s = num.toString(16); + if (s.length < 4) { + s = ('0000' + s).slice(-4); + } + return '0x' + s; } for (let codepoint in codepoints) { - codepoint = parseInt(codepoint); + codepoint = parseInt(codepoint); - // Latin-1? - if ((codepoint >= 0x20) && (codepoint <= 0xff)) { - continue; - } + // Latin-1? + if ((codepoint >= 0x20) && (codepoint <= 0xff)) { + continue; + } - // Handled by the general Unicode mapping? - if ((codepoint | 0x01000000) === codepoints[codepoint].keysym) { - continue; - } + // Handled by the general Unicode mapping? + if ((codepoint | 0x01000000) === codepoints[codepoint].keysym) { + continue; + } - out += " " + toHex(codepoint) + ": " + - toHex(codepoints[codepoint].keysym) + - ", // XK_" + codepoints[codepoint].name + "\n"; + out += ' ' + toHex(codepoint) + ': ' + + toHex(codepoints[codepoint].keysym) + + ', // XK_' + codepoints[codepoint].name + '\n'; } -out += -"};\n" + -"\n" + -"export default {\n" + -" lookup(u) {\n" + -" // Latin-1 is one-to-one mapping\n" + -" if ((u >= 0x20) && (u <= 0xff)) {\n" + -" return u;\n" + -" }\n" + -"\n" + -" // Lookup table (fairly random)\n" + -" const keysym = codepoints[u];\n" + -" if (keysym !== undefined) {\n" + -" return keysym;\n" + -" }\n" + -"\n" + -" // General mapping as final fallback\n" + -" return 0x01000000 | u;\n" + -" },\n" + -"};"; +out ++= '};\n' ++ '\n' ++ 'export default {\n' ++ ' lookup(u) {\n' ++ ' // Latin-1 is one-to-one mapping\n' ++ ' if ((u >= 0x20) && (u <= 0xff)) {\n' ++ ' return u;\n' ++ ' }\n' ++ '\n' ++ ' // Lookup table (fairly random)\n' ++ ' const keysym = codepoints[u];\n' ++ ' if (keysym !== undefined) {\n' ++ ' return keysym;\n' ++ ' }\n' ++ '\n' ++ ' // General mapping as final fallback\n' ++ ' return 0x01000000 | u;\n' ++ ' },\n' ++ '};'; console.log(out); diff --git a/utils/use_require.js b/utils/use_require.js index 0dbdb0a8..52a54d3d 100755 --- a/utils/use_require.js +++ b/utils/use_require.js @@ -9,49 +9,49 @@ const babel = require('babel-core'); const SUPPORTED_FORMATS = new Set(['amd', 'commonjs', 'systemjs', 'umd']); program - .option('--as [format]', `output files using various import formats instead of ES6 import and export. Supports ${Array.from(SUPPORTED_FORMATS)}.`) - .option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ') - .option('--with-app', 'process app files as well as core files') - .option('--only-legacy', 'only output legacy files (no ES6 modules) for the app') - .option('--clean', 'clear the lib folder before building') - .parse(process.argv); + .option('--as [format]', `output files using various import formats instead of ES6 import and export. Supports ${Array.from(SUPPORTED_FORMATS)}.`) + .option('-m, --with-source-maps [type]', 'output source maps when not generating a bundled app (type may be empty for external source maps, inline for inline source maps, or both) ') + .option('--with-app', 'process app files as well as core files') + .option('--only-legacy', 'only output legacy files (no ES6 modules) for the app') + .option('--clean', 'clear the lib folder before building') + .parse(process.argv); // the various important paths const paths = { - main: path.resolve(__dirname, '..'), - core: path.resolve(__dirname, '..', 'core'), - app: path.resolve(__dirname, '..', 'app'), - vendor: path.resolve(__dirname, '..', 'vendor'), - out_dir_base: path.resolve(__dirname, '..', 'build'), - lib_dir_base: path.resolve(__dirname, '..', 'lib'), + main: path.resolve(__dirname, '..'), + core: path.resolve(__dirname, '..', 'core'), + app: path.resolve(__dirname, '..', 'app'), + vendor: path.resolve(__dirname, '..', 'vendor'), + out_dir_base: path.resolve(__dirname, '..', 'build'), + lib_dir_base: path.resolve(__dirname, '..', 'lib'), }; const no_copy_files = new Set([ - // skip these -- they don't belong in the processed application - path.join(paths.vendor, 'sinon.js'), - path.join(paths.vendor, 'browser-es-module-loader'), - path.join(paths.vendor, 'promise.js'), - path.join(paths.app, 'images', 'icons', 'Makefile'), + // skip these -- they don't belong in the processed application + path.join(paths.vendor, 'sinon.js'), + path.join(paths.vendor, 'browser-es-module-loader'), + path.join(paths.vendor, 'promise.js'), + path.join(paths.app, 'images', 'icons', 'Makefile'), ]); const no_transform_files = new Set([ - // don't transform this -- we want it imported as-is to properly catch loading errors - path.join(paths.app, 'error-handler.js'), + // don't transform this -- we want it imported as-is to properly catch loading errors + path.join(paths.app, 'error-handler.js'), ]); no_copy_files.forEach(file => no_transform_files.add(file)); // util.promisify requires Node.js 8.x, so we have our own function promisify(original) { - return function () { - const args = Array.prototype.slice.call(arguments); - return new Promise((resolve, reject) => { - original.apply(this, args.concat((err, value) => { - if (err) return reject(err); - resolve(value); - })); - }); - } + return function () { + const args = Array.prototype.slice.call(arguments); + return new Promise((resolve, reject) => { + original.apply(this, args.concat((err, value) => { + if (err) return reject(err); + resolve(value); + })); + }); + }; } const readFile = promisify(fs.readFile); @@ -70,44 +70,44 @@ const babelTransformFile = promisify(babel.transformFile); // walkDir *recursively* walks directories trees, // calling the callback for all normal files found. function walkDir(base_path, cb, filter) { - return readdir(base_path) + return readdir(base_path) .then((files) => { - const paths = files.map(filename => path.join(base_path, filename)); - return Promise.all(paths.map(filepath => lstat(filepath) + const paths = files.map(filename => path.join(base_path, filename)); + return Promise.all(paths.map(filepath => lstat(filepath) .then((stats) => { - if (filter !== undefined && !filter(filepath, stats)) return; + if (filter !== undefined && !filter(filepath, stats)) return; - if (stats.isSymbolicLink()) return; - if (stats.isFile()) return cb(filepath); - if (stats.isDirectory()) return walkDir(filepath, cb, filter); + if (stats.isSymbolicLink()) return; + if (stats.isFile()) return cb(filepath); + if (stats.isDirectory()) return walkDir(filepath, cb, filter); }))); }); } -function transform_html (legacy_scripts, only_legacy) { - // write out the modified vnc.html file that works with the bundle - const src_html_path = path.resolve(__dirname, '..', 'vnc.html'); - const out_html_path = path.resolve(paths.out_dir_base, 'vnc.html'); - return readFile(src_html_path) +function transform_html(legacy_scripts, only_legacy) { + // write out the modified vnc.html file that works with the bundle + const src_html_path = path.resolve(__dirname, '..', 'vnc.html'); + const out_html_path = path.resolve(paths.out_dir_base, 'vnc.html'); + return readFile(src_html_path) .then((contents_raw) => { - let contents = contents_raw.toString(); + let contents = contents_raw.toString(); - const start_marker = '\n'; - const end_marker = ''; - const start_ind = contents.indexOf(start_marker) + start_marker.length; - const end_ind = contents.indexOf(end_marker, start_ind); + const start_marker = '\n'; + const end_marker = ''; + const start_ind = contents.indexOf(start_marker) + start_marker.length; + const end_ind = contents.indexOf(end_marker, start_ind); - let new_script = ''; + let new_script = ''; - if (only_legacy) { - // Only legacy version, so include things directly - for (let i = 0;i < legacy_scripts.length;i++) { - new_script += ` \n`; - } - } else { - // Otherwise detect if it's a modern browser and select - // variant accordingly - new_script += `\ + if (only_legacy) { + // Only legacy version, so include things directly + for (let i = 0; i < legacy_scripts.length; i++) { + new_script += ` \n`; + } + } else { + // Otherwise detect if it's a modern browser and select + // variant accordingly + new_script += `\ \n\ @@ -124,190 +124,191 @@ function transform_html (legacy_scripts, only_legacy) { });\n\ \n`; - // Original, ES6 modules - new_script += ' \n'; - } + // Original, ES6 modules + new_script += ' \n'; + } - contents = contents.slice(0, start_ind) + `${new_script}\n` + contents.slice(end_ind); + contents = contents.slice(0, start_ind) + `${new_script}\n` + contents.slice(end_ind); - return contents; + return contents; }) .then((contents) => { - console.log(`Writing ${out_html_path}`); - return writeFile(out_html_path, contents); + console.log(`Writing ${out_html_path}`); + return writeFile(out_html_path, contents); }); } function make_lib_files(import_format, source_maps, with_app_dir, only_legacy) { - if (!import_format) { - throw new Error("you must specify an import format to generate compiled noVNC libraries"); - } else if (!SUPPORTED_FORMATS.has(import_format)) { - throw new Error(`unsupported output format "${import_format}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`); - } + if (!import_format) { + throw new Error('you must specify an import format to generate compiled noVNC libraries'); + } else if (!SUPPORTED_FORMATS.has(import_format)) { + throw new Error(`unsupported output format "${import_format}" for import/export -- only ${Array.from(SUPPORTED_FORMATS)} are supported`); + } - // NB: we need to make a copy of babel_opts, since babel sets some defaults on it - const babel_opts = () => ({ - plugins: [`transform-es2015-modules-${import_format}`], - presets: ['es2015'], - ast: false, - sourceMaps: source_maps, - }); + // NB: we need to make a copy of babel_opts, since babel sets some defaults on it + const babel_opts = () => ({ + plugins: [`transform-es2015-modules-${import_format}`], + presets: ['es2015'], + ast: false, + sourceMaps: source_maps, + }); // No point in duplicate files without the app, so force only converted files - if (!with_app_dir) { - only_legacy = true; - } + if (!with_app_dir) { + only_legacy = true; + } - let in_path; - let out_path_base; - if (with_app_dir) { - out_path_base = paths.out_dir_base; - in_path = paths.main; - } else { - out_path_base = paths.lib_dir_base; - } - const legacy_path_base = only_legacy ? out_path_base : path.join(out_path_base, 'legacy'); + let in_path; + let out_path_base; + if (with_app_dir) { + out_path_base = paths.out_dir_base; + in_path = paths.main; + } else { + out_path_base = paths.lib_dir_base; + } + const legacy_path_base = only_legacy ? out_path_base : path.join(out_path_base, 'legacy'); - fse.ensureDirSync(out_path_base); + fse.ensureDirSync(out_path_base); - const helpers = require('./use_require_helpers'); - const helper = helpers[import_format]; + const helpers = require('./use_require_helpers'); + const helper = helpers[import_format]; - const outFiles = []; + const outFiles = []; - const handleDir = (js_only, vendor_rewrite, in_path_base, filename) => Promise.resolve() + const handleDir = (js_only, vendor_rewrite, in_path_base, filename) => Promise.resolve() .then(() => { - if (no_copy_files.has(filename)) return; + if (no_copy_files.has(filename)) return; - const out_path = path.join(out_path_base, path.relative(in_path_base, filename)); - const legacy_path = path.join(legacy_path_base, path.relative(in_path_base, filename)); + const out_path = path.join(out_path_base, path.relative(in_path_base, filename)); + const legacy_path = path.join(legacy_path_base, path.relative(in_path_base, filename)); - if(path.extname(filename) !== '.js') { - if (!js_only) { - console.log(`Writing ${out_path}`); - return copy(filename, out_path); - } - return; // skip non-javascript files + if (path.extname(filename) !== '.js') { + if (!js_only) { + console.log(`Writing ${out_path}`); + return copy(filename, out_path); } + return; // skip non-javascript files + } - return Promise.resolve() + return Promise.resolve() .then(() => { - if (only_legacy && !no_transform_files.has(filename)) { - return; - } - return ensureDir(path.dirname(out_path)) + if (only_legacy && !no_transform_files.has(filename)) { + return; + } + return ensureDir(path.dirname(out_path)) .then(() => { - console.log(`Writing ${out_path}`); - return copy(filename, out_path); - }) + console.log(`Writing ${out_path}`); + return copy(filename, out_path); + }); }) .then(() => ensureDir(path.dirname(legacy_path))) .then(() => { - if (no_transform_files.has(filename)) { - return; - } + if (no_transform_files.has(filename)) { + return; + } - const opts = babel_opts(); - if (helper && helpers.optionsOverride) { - helper.optionsOverride(opts); - } - // Adjust for the fact that we move the core files relative - // to the vendor directory - if (vendor_rewrite) { - opts.plugins.push(["import-redirect", - {"root": legacy_path_base, - "redirect": { "vendor/(.+)": "./vendor/$1"}}]); - } + const opts = babel_opts(); + if (helper && helpers.optionsOverride) { + helper.optionsOverride(opts); + } + // Adjust for the fact that we move the core files relative + // to the vendor directory + if (vendor_rewrite) { + opts.plugins.push(['import-redirect', + { + root: legacy_path_base, + redirect: { 'vendor/(.+)': './vendor/$1' } + }]); + } - return babelTransformFile(filename, opts) + return babelTransformFile(filename, opts) .then((res) => { - console.log(`Writing ${legacy_path}`); - const {map} = res; - let {code} = res; - if (source_maps === true) { - // append URL for external source map - code += `\n//# sourceMappingURL=${path.basename(legacy_path)}.map\n`; - } - outFiles.push(`${legacy_path}`); - return writeFile(legacy_path, code) + console.log(`Writing ${legacy_path}`); + const { map } = res; + let { code } = res; + if (source_maps === true) { + // append URL for external source map + code += `\n//# sourceMappingURL=${path.basename(legacy_path)}.map\n`; + } + outFiles.push(`${legacy_path}`); + return writeFile(legacy_path, code) .then(() => { - if (source_maps === true || source_maps === 'both') { - console.log(` and ${legacy_path}.map`); - outFiles.push(`${legacy_path}.map`); - return writeFile(`${legacy_path}.map`, JSON.stringify(map)); - } + if (source_maps === true || source_maps === 'both') { + console.log(` and ${legacy_path}.map`); + outFiles.push(`${legacy_path}.map`); + return writeFile(`${legacy_path}.map`, JSON.stringify(map)); + } }); }); }); }); - if (with_app_dir && helper && helper.noCopyOverride) { - helper.noCopyOverride(paths, no_copy_files); - } + if (with_app_dir && helper && helper.noCopyOverride) { + helper.noCopyOverride(paths, no_copy_files); + } - Promise.resolve() + Promise.resolve() .then(() => { - const handler = handleDir.bind(null, true, false, in_path || paths.main); - const filter = (filename, stats) => !no_copy_files.has(filename); - return walkDir(paths.vendor, handler, filter); + const handler = handleDir.bind(null, true, false, in_path || paths.main); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.vendor, handler, filter); }) .then(() => { - const handler = handleDir.bind(null, true, !in_path, in_path || paths.core); - const filter = (filename, stats) => !no_copy_files.has(filename); - return walkDir(paths.core, handler, filter); + const handler = handleDir.bind(null, true, !in_path, in_path || paths.core); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.core, handler, filter); }) .then(() => { - if (!with_app_dir) return; - const handler = handleDir.bind(null, false, false, in_path); - const filter = (filename, stats) => !no_copy_files.has(filename); - return walkDir(paths.app, handler, filter); + if (!with_app_dir) return; + const handler = handleDir.bind(null, false, false, in_path); + const filter = (filename, stats) => !no_copy_files.has(filename); + return walkDir(paths.app, handler, filter); }) .then(() => { - if (!with_app_dir) return; + if (!with_app_dir) return; - if (!helper || !helper.appWriter) { - throw new Error(`Unable to generate app for the ${import_format} format!`); - } + if (!helper || !helper.appWriter) { + throw new Error(`Unable to generate app for the ${import_format} format!`); + } - const out_app_path = path.join(legacy_path_base, 'app.js'); - console.log(`Writing ${out_app_path}`); - return helper.appWriter(out_path_base, legacy_path_base, out_app_path) + const out_app_path = path.join(legacy_path_base, 'app.js'); + console.log(`Writing ${out_app_path}`); + return helper.appWriter(out_path_base, legacy_path_base, out_app_path) .then((extra_scripts) => { - const rel_app_path = path.relative(out_path_base, out_app_path); - const legacy_scripts = extra_scripts.concat([rel_app_path]); - transform_html(legacy_scripts, only_legacy); + const rel_app_path = path.relative(out_path_base, out_app_path); + const legacy_scripts = extra_scripts.concat([rel_app_path]); + transform_html(legacy_scripts, only_legacy); }) .then(() => { - if (!helper.removeModules) return; - console.log(`Cleaning up temporary files...`); - return Promise.all(outFiles.map((filepath) => { - unlink(filepath) - .then(() => { - // Try to clean up any empty directories if this - // was the last file in there - const rmdir_r = dir => - rmdir(dir) - .then(() => rmdir_r(path.dirname(dir))) - .catch(() => { - // Assume the error was ENOTEMPTY and ignore it - }); - return rmdir_r(path.dirname(filepath)); - }); - })); + if (!helper.removeModules) return; + console.log('Cleaning up temporary files...'); + return Promise.all(outFiles.map((filepath) => { + unlink(filepath) + .then(() => { + // Try to clean up any empty directories if this + // was the last file in there + const rmdir_r = dir => rmdir(dir) + .then(() => rmdir_r(path.dirname(dir))) + .catch(() => { + // Assume the error was ENOTEMPTY and ignore it + }); + return rmdir_r(path.dirname(filepath)); + }); + })); }); }) .catch((err) => { - console.error(`Failure converting modules: ${err}`); - process.exit(1); + console.error(`Failure converting modules: ${err}`); + process.exit(1); }); } if (program.clean) { - console.log(`Removing ${paths.lib_dir_base}`); - fse.removeSync(paths.lib_dir_base); + console.log(`Removing ${paths.lib_dir_base}`); + fse.removeSync(paths.lib_dir_base); - console.log(`Removing ${paths.out_dir_base}`); - fse.removeSync(paths.out_dir_base); + console.log(`Removing ${paths.out_dir_base}`); + fse.removeSync(paths.out_dir_base); } make_lib_files(program.as, program.withSourceMaps, program.withApp, program.onlyLegacy); diff --git a/utils/use_require_helpers.js b/utils/use_require_helpers.js index f0cbcf5e..dfd42056 100644 --- a/utils/use_require_helpers.js +++ b/utils/use_require_helpers.js @@ -4,73 +4,73 @@ const path = require('path'); // util.promisify requires Node.js 8.x, so we have our own function promisify(original) { - return function () { - const args = Array.prototype.slice.call(arguments); - return new Promise((resolve, reject) => { - original.apply(this, args.concat((err, value) => { - if (err) return reject(err); - resolve(value); - })); - }); - } + return function () { + const args = Array.prototype.slice.call(arguments); + return new Promise((resolve, reject) => { + original.apply(this, args.concat((err, value) => { + if (err) return reject(err); + resolve(value); + })); + }); + }; } const writeFile = promisify(fs.writeFile); module.exports = { - 'amd': { - appWriter: (base_out_path, script_base_path, out_path) => { - // setup for requirejs - const ui_path = path.relative(base_out_path, - path.join(script_base_path, 'app', 'ui')); - return writeFile(out_path, `requirejs(["${ui_path}"], (ui) => {});`) - .then(() => { - console.log(`Please place RequireJS in ${path.join(script_base_path, 'require.js')}`); - const require_path = path.relative(base_out_path, - path.join(script_base_path, 'require.js')) - return [ require_path ]; - }); - }, - noCopyOverride: () => {}, + amd: { + appWriter: (base_out_path, script_base_path, out_path) => { + // setup for requirejs + const ui_path = path.relative(base_out_path, + path.join(script_base_path, 'app', 'ui')); + return writeFile(out_path, `requirejs(["${ui_path}"], (ui) => {});`) + .then(() => { + console.log(`Please place RequireJS in ${path.join(script_base_path, 'require.js')}`); + const require_path = path.relative(base_out_path, + path.join(script_base_path, 'require.js')); + return [require_path]; + }); }, - 'commonjs': { - optionsOverride: (opts) => { - // CommonJS supports properly shifting the default export to work as normal - opts.plugins.unshift("add-module-exports"); - }, - appWriter: (base_out_path, script_base_path, out_path) => { - const browserify = require('browserify'); - const b = browserify(path.join(script_base_path, 'app/ui.js'), {}); - return promisify(b.bundle).call(b) - .then(buf => writeFile(out_path, buf)) - .then(() => []); - }, - noCopyOverride: () => {}, - removeModules: true, + noCopyOverride: () => {}, + }, + commonjs: { + optionsOverride: (opts) => { + // CommonJS supports properly shifting the default export to work as normal + opts.plugins.unshift('add-module-exports'); }, - 'systemjs': { - appWriter: (base_out_path, script_base_path, out_path) => { - const ui_path = path.relative(base_out_path, - path.join(script_base_path, 'app', 'ui.js')); - return writeFile(out_path, `SystemJS.import("${ui_path}");`) - .then(() => { - console.log(`Please place SystemJS in ${path.join(script_base_path, 'system-production.js')}`); - // FIXME: Should probably be in the legacy directory - const promise_path = path.relative(base_out_path, - path.join(base_out_path, 'vendor', 'promise.js')) - const systemjs_path = path.relative(base_out_path, - path.join(script_base_path, 'system-production.js')) - return [ promise_path, systemjs_path ]; - }); - }, - noCopyOverride: (paths, no_copy_files) => { - no_copy_files.delete(path.join(paths.vendor, 'promise.js')); - }, + appWriter: (base_out_path, script_base_path, out_path) => { + const browserify = require('browserify'); + const b = browserify(path.join(script_base_path, 'app/ui.js'), {}); + return promisify(b.bundle).call(b) + .then(buf => writeFile(out_path, buf)) + .then(() => []); }, - 'umd': { - optionsOverride: (opts) => { - // umd supports properly shifting the default export to work as normal - opts.plugins.unshift("add-module-exports"); - }, + noCopyOverride: () => {}, + removeModules: true, + }, + systemjs: { + appWriter: (base_out_path, script_base_path, out_path) => { + const ui_path = path.relative(base_out_path, + path.join(script_base_path, 'app', 'ui.js')); + return writeFile(out_path, `SystemJS.import("${ui_path}");`) + .then(() => { + console.log(`Please place SystemJS in ${path.join(script_base_path, 'system-production.js')}`); + // FIXME: Should probably be in the legacy directory + const promise_path = path.relative(base_out_path, + path.join(base_out_path, 'vendor', 'promise.js')); + const systemjs_path = path.relative(base_out_path, + path.join(script_base_path, 'system-production.js')); + return [promise_path, systemjs_path]; + }); }, -} + noCopyOverride: (paths, no_copy_files) => { + no_copy_files.delete(path.join(paths.vendor, 'promise.js')); + }, + }, + umd: { + optionsOverride: (opts) => { + // umd supports properly shifting the default export to work as normal + opts.plugins.unshift('add-module-exports'); + }, + }, +}; From b527d65c149f01dd47cca78e4f838866ee068271 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Sat, 14 Jul 2018 01:27:29 +0200 Subject: [PATCH 3/5] Enforce line max length of 100 --- .eslintrc | 14 +++++++------- core/display.js | 6 +++++- core/input/mouse.js | 2 +- core/input/xtscancodes.js | 1 + core/rfb.js | 36 ++++++++++++++++++++++++++++++------ tests/test.display.js | 8 +++++--- tests/test.rfb.js | 37 ++++++++++++++++++++++++------------- tests/test.websock.js | 6 ++++-- 8 files changed, 77 insertions(+), 33 deletions(-) diff --git a/.eslintrc b/.eslintrc index e4d8b7ca..a838680b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -213,13 +213,13 @@ // specify the maximum length of a line in your program // https://eslint.org/docs/rules/max-len - // "max-len": ["error", 100, 2, { - // ignoreUrls: true, - // ignoreComments: false, - // ignoreRegExpLiterals: true, - // ignoreStrings: true, - // ignoreTemplateLiterals: true, - // }], + "max-len": ["error", 100, 2, { + ignoreUrls: true, + ignoreComments: false, + ignoreRegExpLiterals: true, + ignoreStrings: true, + ignoreTemplateLiterals: true, + }], // specify the max number of lines in a file // https://eslint.org/docs/rules/max-lines diff --git a/core/display.js b/core/display.js index 653f3206..d5784122 100644 --- a/core/display.js +++ b/core/display.js @@ -576,7 +576,11 @@ export default class Display { // NB(directxman12): arr must be an Type Array view let img; if (SUPPORTS_IMAGEDATA_CONSTRUCTOR) { - img = new ImageData(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), width, height); + img = new ImageData( + new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4), + width, + height + ); } else { img = this._drawCtx.createImageData(width, height); img.data.set(new Uint8ClampedArray(arr.buffer, arr.byteOffset, width * height * 4)); diff --git a/core/input/mouse.js b/core/input/mouse.js index 0cb6772b..953e686d 100644 --- a/core/input/mouse.js +++ b/core/input/mouse.js @@ -36,7 +36,7 @@ export default class Mouse { // ===== PROPERTIES ===== - this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) + this.touchButton = 1; // Button mask (1, 2, 4) for touch devices (0 means ignore clicks) // ===== EVENT HANDLERS ===== diff --git a/core/input/xtscancodes.js b/core/input/xtscancodes.js index d9aaf78e..e08aa9b9 100644 --- a/core/input/xtscancodes.js +++ b/core/input/xtscancodes.js @@ -4,6 +4,7 @@ * To re-generate, run: * keymap-gen --lang=js code-map keymaps.csv html atset1 */ +/* eslint-disable max-len */ export default { Again: 0xe005, /* html:Again (Again) -> linux:129 (KEY_AGAIN) -> atset1:57349 */ AltLeft: 0x38, /* html:AltLeft (AltLeft) -> linux:56 (KEY_LEFTALT) -> atset1:56 */ diff --git a/core/rfb.js b/core/rfb.js index 9f39323c..e8baa21d 100644 --- a/core/rfb.js +++ b/core/rfb.js @@ -168,6 +168,7 @@ export default class RFB extends EventTargetMixin { this._cursor = new Cursor(); // populate encHandlers with bound versions + /* eslint-disable max-len */ this._encHandlers[encodings.encodingRaw] = RFB.encodingHandlers.RAW.bind(this); this._encHandlers[encodings.encodingCopyRect] = RFB.encodingHandlers.COPYRECT.bind(this); this._encHandlers[encodings.encodingRRE] = RFB.encodingHandlers.RRE.bind(this); @@ -180,6 +181,7 @@ export default class RFB extends EventTargetMixin { this._encHandlers[encodings.pseudoEncodingCursor] = RFB.encodingHandlers.Cursor.bind(this); this._encHandlers[encodings.pseudoEncodingQEMUExtendedKeyEvent] = RFB.encodingHandlers.QEMUExtendedKeyEvent.bind(this); this._encHandlers[encodings.pseudoEncodingExtendedDesktopSize] = RFB.encodingHandlers.ExtendedDesktopSize.bind(this); + /* eslint-enable max-len */ // NB: nothing that needs explicit teardown should be done // before this point, since this can throw an exception @@ -787,7 +789,12 @@ export default class RFB extends EventTargetMixin { if (this._viewOnly) { return; } // View only, skip mouse events if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + RFB.messages.pointerEvent( + this._sock, + this._display.absX(x), + this._display.absY(y), + this._mouse_buttonMask + ); } _handleMouseMove(x, y) { @@ -814,7 +821,12 @@ export default class RFB extends EventTargetMixin { if (this._viewOnly) { return; } // View only, skip mouse events if (this._rfb_connection_state !== 'connected') { return; } - RFB.messages.pointerEvent(this._sock, this._display.absX(x), this._display.absY(y), this._mouse_buttonMask); + RFB.messages.pointerEvent( + this._sock, + this._display.absX(x), + this._display.absY(y), + this._mouse_buttonMask + ); } // Message Handlers @@ -1039,7 +1051,7 @@ export default class RFB extends EventTargetMixin { // choose the notunnel type if (serverSupportedTunnelTypes[0]) { if (serverSupportedTunnelTypes[0].vendor != clientSupportedTunnelTypes[0].vendor - || serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { + || serverSupportedTunnelTypes[0].signature != clientSupportedTunnelTypes[0].signature) { return this._fail("Client's tunnel type had the incorrect " + 'vendor or signature'); } @@ -2308,7 +2320,11 @@ RFB.encodingHandlers = { rgbx = indexedToRGBX(data, this._paletteBuff, this._FBU.width, this._FBU.height); } - this._display.blitRgbxImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, rgbx, 0, false); + this._display.blitRgbxImage( + this._FBU.x, this._FBU.y, + this._FBU.width, this._FBU.height, + rgbx, 0, false + ); return true; @@ -2349,7 +2365,11 @@ RFB.encodingHandlers = { data = decompress(this._sock.rQshiftBytes(cl_data), uncompressedSize); } - this._display.blitRgbImage(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, data, 0, false); + this._display.blitRgbImage( + this._FBU.x, this._FBU.y, + this._FBU.width, this._FBU.height, + data, 0, false + ); return true; }; @@ -2404,7 +2424,11 @@ RFB.encodingHandlers = { switch (cmode) { case 'fill': // skip ctl byte - this._display.fillRect(this._FBU.x, this._FBU.y, this._FBU.width, this._FBU.height, [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false); + this._display.fillRect( + this._FBU.x, this._FBU.y, + this._FBU.width, this._FBU.height, + [rQ[rQi + 3], rQ[rQi + 2], rQ[rQi + 1]], false + ); this._sock.rQskipBytes(4); break; case 'png': diff --git a/tests/test.display.js b/tests/test.display.js index 3324f056..9d4257f0 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -13,7 +13,9 @@ describe('Display/Canvas Helper', function () { 0x00, 0xff, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0x00, 0x00, 0xff, 255 ]); - const basic_data = new Uint8Array([0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255]); + const basic_data = new Uint8Array([ + 0xff, 0x00, 0x00, 255, 0x00, 0xff, 0x00, 255, 0x00, 0x00, 0xff, 255, 0xff, 0xff, 0xff, 255 + ]); function make_image_canvas(input_data) { const canvas = document.createElement('canvas'); @@ -261,8 +263,8 @@ describe('Display/Canvas Helper', function () { }); describe('drawing', function () { - // TODO(directxman12): improve the tests for each of the drawing functions to cover more than just the - // basic cases + // TODO(directxman12): improve the tests for each of the drawing functions to cover + // more than just the basic cases let display; beforeEach(function () { display = new Display(document.createElement('canvas')); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index bca822f8..115ccc90 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -1025,7 +1025,8 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(client._rfb_version).to.equal(0); const sent_data = client._sock._websocket._get_sent_data(); - expect(new Uint8Array(sent_data.buffer, 0, 9)).to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0])); + expect(new Uint8Array(sent_data.buffer, 0, 9)) + .to.array.equal(new Uint8Array([73, 68, 58, 49, 50, 51, 52, 53, 0])); expect(sent_data).to.have.length(250); }); @@ -1557,10 +1558,13 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 24, true); expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); expect(RFB.messages.clientEncodings).to.have.been.calledOnce; - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.include(encodings.encodingTight); - expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.clientEncodings.getCall(0).args[1]) + .to.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings) + .to.have.been.calledBefore(RFB.messages.fbUpdateRequest); expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; - expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + expect(RFB.messages.fbUpdateRequest) + .to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); }); it('should reply with restricted settings for Intel AMT servers', function () { @@ -1570,11 +1574,15 @@ describe('Remote Frame Buffer Protocol Client', function () { expect(RFB.messages.pixelFormat).to.have.been.calledWith(client._sock, 8, true); expect(RFB.messages.pixelFormat).to.have.been.calledBefore(RFB.messages.clientEncodings); expect(RFB.messages.clientEncodings).to.have.been.calledOnce; - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingTight); - expect(RFB.messages.clientEncodings.getCall(0).args[1]).to.not.include(encodings.encodingHextile); - expect(RFB.messages.clientEncodings).to.have.been.calledBefore(RFB.messages.fbUpdateRequest); + expect(RFB.messages.clientEncodings.getCall(0).args[1]) + .to.not.include(encodings.encodingTight); + expect(RFB.messages.clientEncodings.getCall(0).args[1]) + .to.not.include(encodings.encodingHextile); + expect(RFB.messages.clientEncodings) + .to.have.been.calledBefore(RFB.messages.fbUpdateRequest); expect(RFB.messages.fbUpdateRequest).to.have.been.calledOnce; - expect(RFB.messages.fbUpdateRequest).to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); + expect(RFB.messages.fbUpdateRequest) + .to.have.been.calledWith(client._sock, false, 0, 0, 27, 32); }); }); @@ -1665,7 +1673,8 @@ describe('Remote Frame Buffer Protocol Client', function () { client._sock._websocket._receive_data(new Uint8Array([0, 0, 0, 3])); expect(client._sock._websocket._get_sent_data()).to.have.length(0); - client._framebufferUpdate = function () { this._sock.rQskip8(); return true; }; // we magically have enough data + // we magically have enough data + client._framebufferUpdate = function () { this._sock.rQskip8(); return true; }; // 247 should *not* be used as the message type here client._sock._websocket._receive_data(new Uint8Array([247])); expect(client._sock).to.have.sent(expected_msg._sQ); @@ -1693,7 +1702,8 @@ describe('Remote Frame Buffer Protocol Client', function () { client._fb_width = 4; client._fb_height = 4; client._display.resize(4, 4); - client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + client._display.blitRgbxImage(0, 0, 4, 2, + new Uint8Array(target_data_check_arr.slice(0, 32)), 0); const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01 @@ -1765,7 +1775,8 @@ describe('Remote Frame Buffer Protocol Client', function () { it('should handle the COPYRECT encoding', function () { // seed some initial data to copy - client._display.blitRgbxImage(0, 0, 4, 2, new Uint8Array(target_data_check_arr.slice(0, 32)), 0); + client._display.blitRgbxImage(0, 0, 4, 2, + new Uint8Array(target_data_check_arr.slice(0, 32)), 0); const info = [{ x: 0, y: 2, width: 2, height: 2, encoding: 0x01 @@ -1882,8 +1893,8 @@ describe('Remote Frame Buffer Protocol Client', function () { send_fbu_msg(info, [rect], client); const expected = []; - for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid - for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bkground color + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 1: solid + for (let i = 0; i < 16; i++) { push32(expected, 0xff00ff); } // rect 2: same bg color expect(client._display).to.have.displayed(new Uint8Array(expected)); }); diff --git a/tests/test.websock.js b/tests/test.websock.js index cf74b5f7..12f22704 100644 --- a/tests/test.websock.js +++ b/tests/test.websock.js @@ -78,7 +78,8 @@ describe('Websock', function () { const bef_rQi = sock.get_rQi(); const shifted = sock.rQshiftStr(3); expect(shifted).to.be.a('string'); - expect(shifted).to.equal(String.fromCharCode.apply(null, Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); + expect(shifted).to.equal(String.fromCharCode.apply(null, + Array.prototype.slice.call(new Uint8Array(RQ_TEMPLATE.buffer, bef_rQi, 3)))); expect(sock.rQlen()).to.equal(bef_len - 3); }); @@ -225,7 +226,8 @@ describe('Websock', function () { it('should add to the send queue', function () { sock.send([1, 2, 3]); const sq = sock.get_sQ(); - expect(new Uint8Array(sq.buffer, sock._sQlen - 3, 3)).to.array.equal(new Uint8Array([1, 2, 3])); + const sendQueue = new Uint8Array(sq.buffer, sock._sQlen - 3, 3); + expect(sendQueue).to.array.equal(new Uint8Array([1, 2, 3])); }); it('should call flush', function () { From 3ab566204dd371e5194616228452edf4f6cafa08 Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Sat, 14 Jul 2018 01:39:32 +0200 Subject: [PATCH 4/5] Disallow multi-assign --- .eslintrc | 2 +- core/des.js | 3 ++- core/display.js | 6 ++++-- core/util/logging.js | 5 ++++- tests/test.display.js | 6 ++++-- tests/test.rfb.js | 3 ++- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.eslintrc b/.eslintrc index a838680b..e0c2b582 100644 --- a/.eslintrc +++ b/.eslintrc @@ -329,7 +329,7 @@ // disallow use of chained assignment expressions // https://eslint.org/docs/rules/no-multi-assign - // "no-multi-assign": ["error"], + "no-multi-assign": ["error"], // disallow multiple empty lines and only one newline at the end "no-multiple-empty-lines": ["error", { max: 2, maxEOF: 0 }], diff --git a/core/des.js b/core/des.js index 653f0808..8e24fcc3 100644 --- a/core/des.js +++ b/core/des.js @@ -152,7 +152,8 @@ export default function DES(passwd) { for (let i = 0; i < 16; ++i) { const m = i << 1; const n = m + 1; - kn[m] = kn[n] = 0; + kn[m] = 0; + kn[n] = 0; for (let o = 28; o < 59; o += 28) { for (let j = o - 28; j < o; ++j) { const l = j + totrot[i]; diff --git a/core/display.js b/core/display.js index d5784122..229a5a24 100644 --- a/core/display.js +++ b/core/display.js @@ -300,8 +300,10 @@ export default class Display { vx, vy, w, h); } - this._damageBounds.left = this._damageBounds.top = 65535; - this._damageBounds.right = this._damageBounds.bottom = 0; + this._damageBounds.top = 65535; + this._damageBounds.left = 65535; + this._damageBounds.bottom = 0; + this._damageBounds.right = 0; } } diff --git a/core/util/logging.js b/core/util/logging.js index 29347529..61ac6964 100644 --- a/core/util/logging.js +++ b/core/util/logging.js @@ -24,7 +24,10 @@ export function init_logging(level) { _log_level = level; } - Debug = Info = Warn = Error = () => {}; + Debug = () => {}; + Info = () => {}; + Warn = () => {}; + Error = () => {}; if (typeof window.console !== 'undefined') { /* eslint-disable no-console, no-fallthrough */ diff --git a/tests/test.display.js b/tests/test.display.js index 9d4257f0..e3de22cb 100644 --- a/tests/test.display.js +++ b/tests/test.display.js @@ -138,7 +138,8 @@ describe('Display/Canvas Helper', function () { const expected = []; for (let i = 0; i < 4 * 2 * 2; i += 4) { expected[i] = 0xff; - expected[i + 1] = expected[i + 2] = 0; + expected[i + 1] = 0; + expected[i + 2] = 0; expected[i + 3] = 0xff; } expect(display).to.have.displayed(new Uint8Array(expected)); @@ -302,7 +303,8 @@ describe('Display/Canvas Helper', function () { const expected = []; for (let i = 0; i < 4 * display._fb_width * display._fb_height; i += 4) { expected[i] = 0xff; - expected[i + 1] = expected[i + 2] = 0; + expected[i + 1] = 0; + expected[i + 2] = 0; expected[i + 3] = 0xff; } expect(display).to.have.displayed(new Uint8Array(expected)); diff --git a/tests/test.rfb.js b/tests/test.rfb.js index 115ccc90..ddcd3cb8 100644 --- a/tests/test.rfb.js +++ b/tests/test.rfb.js @@ -55,7 +55,8 @@ describe('Remote Frame Buffer Protocol Client', function () { after(FakeWebSocket.restore); before(function () { - this.clock = clock = sinon.useFakeTimers(); + clock = sinon.useFakeTimers(); + this.clock = clock; // sinon doesn't support this yet raf = window.requestAnimationFrame; window.requestAnimationFrame = setTimeout; From 8eedf9e7c6041c8589193e449c8e695a7e89c93e Mon Sep 17 00:00:00 2001 From: Juanjo Diaz Date: Sat, 14 Jul 2018 01:39:55 +0200 Subject: [PATCH 5/5] Disallow nested ternaries --- .eslintrc | 2 +- core/des.js | 11 ++++++++++- core/util/events.js | 4 +++- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/.eslintrc b/.eslintrc index e0c2b582..3e540253 100644 --- a/.eslintrc +++ b/.eslintrc @@ -339,7 +339,7 @@ "no-negated-condition": "off", // disallow nested ternary expressions - // "no-nested-ternary": "error", + "no-nested-ternary": "error", // disallow use of the Object constructor "no-new-object": "error", diff --git a/core/des.js b/core/des.js index 8e24fcc3..abcc5892 100644 --- a/core/des.js +++ b/core/des.js @@ -144,7 +144,16 @@ export default function DES(passwd) { kn = []; for (let j = 0, l = 56; j < 56; ++j, l -= 8) { - l += l < -5 ? 65 : l < -3 ? 31 : l < -1 ? 63 : l === 27 ? 35 : 0; // PC1 + // PC1 + if (l < -5) { + l += 65; + } else if (l < -3) { + l += 31; + } else if (l < -1) { + l += 63; + } else if (l === 27) { + l += 35; + } const m = l & 0x7; pc1m[j] = ((keyBlock[l >>> 3] & (1 << m)) !== 0) ? 1 : 0; } diff --git a/core/util/events.js b/core/util/events.js index efb0a437..22c70236 100644 --- a/core/util/events.js +++ b/core/util/events.js @@ -11,7 +11,9 @@ */ export function getPointerEvent(e) { - return e.changedTouches ? e.changedTouches[0] : e.touches ? e.touches[0] : e; + if (e.changedTouches) return e.changedTouches[0]; + if (e.touches) return e.touches[0]; + return e; } export function stopEvent(e) {